feat(handler): implement LocalStorageHandler with rotation

This commit is contained in:
Tiara Rodney 2026-03-14 04:55:37 +01:00
parent 7479931714
commit 6fd8c22ceb
No known key found for this signature in database
GPG key ID: 5CD8EC1D46106723

View file

@ -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