diff --git a/src/handler.ts b/src/handler.ts index e174133..6f66f50 100644 --- a/src/handler.ts +++ b/src/handler.ts @@ -219,6 +219,114 @@ export class FileHandler extends StreamHandler { } } +/** + * A handler that persists log records to browser localStorage. + * + * Records are stored as a JSON array of formatted strings under a + * configurable key. Log rotation is supported by entry count and/or + * byte size to avoid exceeding storage quotas. + */ +export interface LocalStorageHandlerOptions { + /** + * localStorage key to store entries under. + * Defaults to 'esm-logging'. + */ + key?: string; + + /** + * Maximum number of entries to retain. Oldest entries are discarded + * first. Set to 0 for no entry limit. Defaults to 1000. + */ + maxEntries?: number; + + /** + * Maximum size in bytes (as measured by the JSON-serialized string + * length). Oldest entries are discarded to stay within this limit. + * Set to 0 for no byte limit. Defaults to 0. + */ + maxBytes?: number; +} + +export class LocalStorageHandler extends Handler { + protected key: string; + protected maxEntries: number; + protected maxBytes: number; + + constructor(options?: LocalStorageHandlerOptions) { + super(); + + if (typeof localStorage === 'undefined') { + throw new NotImplementedError( + 'LocalStorageHandler requires a browser environment with localStorage' + ); + } + + this.key = options?.key ?? 'esm-logging'; + this.maxEntries = options?.maxEntries ?? 1000; + this.maxBytes = options?.maxBytes ?? 0; + } + + emit(record: LogRecord) { + try { + const msg = this.format(record); + const entries = this.getEntries(); + entries.push(msg); + this._rotate(entries); + this._setEntries(entries); + } + catch (e) { + this.handleError(record); + } + } + + /** + * Retrieve all stored log entries. + */ + getEntries(): string[] { + try { + const data = localStorage.getItem(this.key); + return data ? JSON.parse(data) : []; + } + catch { + return []; + } + } + + /** + * Remove all stored log entries. + */ + clearEntries() { + localStorage.removeItem(this.key); + } + + close() { + super.close(); + } + + /** + * Discard oldest entries to stay within maxEntries and maxBytes limits. + */ + protected _rotate(entries: string[]) { + if (this.maxEntries > 0) { + while (entries.length > this.maxEntries) { + entries.shift(); + } + } + + if (this.maxBytes > 0) { + while (entries.length > 0) { + const serialized = JSON.stringify(entries); + if (serialized.length <= this.maxBytes) { break } + entries.shift(); + } + } + } + + protected _setEntries(entries: string[]) { + localStorage.setItem(this.key, JSON.stringify(entries)); + } +} + /** * This class is like a StreamHandler using sys.stderr, but always uses * whatever sys.stderr is currently set to rather than the value of