@richardmcquiston01/error-log-adapter 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -128,11 +128,519 @@ var CompositeErrorAdapter = class {
128
128
  };
129
129
  }
130
130
  };
131
+
132
+ // src/adapters/indexeddb-error-adapter.ts
133
+ var DEFAULT_STORE_NAME = "error_log";
134
+ function createErrorLogStore(database, storeName = DEFAULT_STORE_NAME) {
135
+ if (database.objectStoreNames.contains(storeName)) {
136
+ return;
137
+ }
138
+ database.createObjectStore(storeName, { keyPath: "id" }).createIndex("timestamp", "timestamp");
139
+ }
140
+ var IndexedDbErrorAdapter = class {
141
+ constructor(database, options = {}) {
142
+ this.database = database;
143
+ this.storeName = options.storeName ?? DEFAULT_STORE_NAME;
144
+ this.prefix = options.prefix ?? "[error-log-adapter]";
145
+ }
146
+ async log(entry) {
147
+ try {
148
+ const record = {
149
+ id: crypto.randomUUID(),
150
+ ...serializeEntry(entry)
151
+ };
152
+ await this.write(record);
153
+ return { ok: true };
154
+ } catch (error) {
155
+ console.error(`${this.prefix} IndexedDbErrorAdapter:`, error);
156
+ return {
157
+ ok: false,
158
+ error: `IndexedDbErrorAdapter failed to write to object store "${this.storeName}": ${describeError(error)}`
159
+ };
160
+ }
161
+ }
162
+ /** Resolves once the transaction commits; rejects if it errors or aborts. */
163
+ write(record) {
164
+ return new Promise(
165
+ (resolve, reject) => {
166
+ const transaction = this.database.transaction(
167
+ this.storeName,
168
+ "readwrite"
169
+ );
170
+ const fail = (action) => () => reject(
171
+ new Error(
172
+ `transaction ${action}: ${transaction.error?.message ?? "no details provided"}`
173
+ )
174
+ );
175
+ transaction.oncomplete = () => resolve();
176
+ transaction.onerror = fail("failed");
177
+ transaction.onabort = fail("aborted");
178
+ transaction.objectStore(this.storeName).add(record);
179
+ }
180
+ );
181
+ }
182
+ };
183
+
184
+ // src/adapters/sqlite-error-adapter.ts
185
+ var DEFAULT_TABLE_NAME = "error_log";
186
+ var IDENTIFIER_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
187
+ function assertValidTableName(tableName) {
188
+ if (!IDENTIFIER_PATTERN.test(tableName)) {
189
+ throw new RangeError(
190
+ `SqliteErrorAdapter: invalid table name "${tableName}". Use only letters, digits and underscores, and do not start with a digit.`
191
+ );
192
+ }
193
+ }
194
+ function buildCreateErrorLogTableSql(tableName = DEFAULT_TABLE_NAME) {
195
+ assertValidTableName(tableName);
196
+ return [
197
+ `CREATE TABLE IF NOT EXISTS ${tableName} (id TEXT PRIMARY KEY NOT NULL, code TEXT NOT NULL, message TEXT NOT NULL, cause TEXT, source TEXT, occurred_at TEXT NOT NULL, created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')))`,
198
+ `CREATE INDEX IF NOT EXISTS ${tableName}_occurred_at_idx ON ${tableName} (occurred_at)`
199
+ ];
200
+ }
201
+ var SqliteErrorAdapter = class {
202
+ /** @throws {RangeError} if `options.tableName` is not a plain identifier. */
203
+ constructor(client, options = {}) {
204
+ this.client = client;
205
+ this.tableReady = null;
206
+ this.tableName = options.tableName ?? DEFAULT_TABLE_NAME;
207
+ assertValidTableName(this.tableName);
208
+ this.createTable = options.createTable ?? true;
209
+ this.prefix = options.prefix ?? "[error-log-adapter]";
210
+ }
211
+ async log(entry) {
212
+ try {
213
+ await this.ensureTable();
214
+ const serialized = serializeEntry(entry);
215
+ await this.client.run(
216
+ `INSERT INTO ${this.tableName} (id, code, message, cause, source, occurred_at) VALUES (?, ?, ?, ?, ?, ?)`,
217
+ [
218
+ crypto.randomUUID(),
219
+ serialized.code,
220
+ serialized.message,
221
+ serialized.cause === void 0 ? null : JSON.stringify(serialized.cause),
222
+ serialized.source ?? null,
223
+ serialized.timestamp
224
+ ]
225
+ );
226
+ return { ok: true };
227
+ } catch (error) {
228
+ console.error(`${this.prefix} SqliteErrorAdapter:`, error);
229
+ return {
230
+ ok: false,
231
+ error: `SqliteErrorAdapter failed to write to table "${this.tableName}": ${describeError(error)}`
232
+ };
233
+ }
234
+ }
235
+ /** Creates the table once. A failed attempt is retried on the next log call. */
236
+ ensureTable() {
237
+ if (!this.createTable) {
238
+ return Promise.resolve();
239
+ }
240
+ if (this.tableReady === null) {
241
+ this.tableReady = this.runCreateStatements().catch(
242
+ (error) => {
243
+ this.tableReady = null;
244
+ throw error;
245
+ }
246
+ );
247
+ }
248
+ return this.tableReady;
249
+ }
250
+ async runCreateStatements() {
251
+ for (const sql of buildCreateErrorLogTableSql(this.tableName)) {
252
+ await this.client.run(sql, []);
253
+ }
254
+ }
255
+ };
256
+
257
+ // src/adapters/web-storage-error-adapter.ts
258
+ var DEFAULT_STORAGE_KEY = "error_log";
259
+ var DEFAULT_MAX_ENTRIES = 100;
260
+ function isQuotaError(error) {
261
+ if (typeof error !== "object" || error === null) {
262
+ return false;
263
+ }
264
+ const { name, code } = error;
265
+ return typeof name === "string" && name.toLowerCase().includes("quota") || code === 22 || code === 1014;
266
+ }
267
+ var WebStorageErrorAdapter = class {
268
+ /** @throws {RangeError} if `options.maxEntries` is not a positive integer. */
269
+ constructor(storage, options = {}) {
270
+ this.storage = storage;
271
+ this.key = options.key ?? DEFAULT_STORAGE_KEY;
272
+ this.maxEntries = options.maxEntries ?? DEFAULT_MAX_ENTRIES;
273
+ this.prefix = options.prefix ?? "[error-log-adapter]";
274
+ if (!Number.isInteger(this.maxEntries) || this.maxEntries < 1) {
275
+ throw new RangeError(
276
+ `WebStorageErrorAdapter: maxEntries must be a positive integer, received ${String(options.maxEntries)}.`
277
+ );
278
+ }
279
+ }
280
+ async log(entry) {
281
+ try {
282
+ const record = {
283
+ id: crypto.randomUUID(),
284
+ ...serializeEntry(entry)
285
+ };
286
+ this.append(record);
287
+ return { ok: true };
288
+ } catch (error) {
289
+ console.error(`${this.prefix} WebStorageErrorAdapter:`, error);
290
+ return {
291
+ ok: false,
292
+ error: `WebStorageErrorAdapter failed to write to storage key "${this.key}": ${describeError(error)}`
293
+ };
294
+ }
295
+ }
296
+ /** Reads, appends, trims, and writes back. Runs without awaiting, so calls never interleave. */
297
+ append(record) {
298
+ const records = [
299
+ ...this.readRecords(),
300
+ record
301
+ ].slice(-this.maxEntries);
302
+ for (; ; ) {
303
+ try {
304
+ this.storage.setItem(this.key, JSON.stringify(records));
305
+ return;
306
+ } catch (error) {
307
+ if (!isQuotaError(error) || records.length <= 1) {
308
+ throw error;
309
+ }
310
+ records.shift();
311
+ }
312
+ }
313
+ }
314
+ /** Returns the stored records, or none if the value is missing or unreadable. */
315
+ readRecords() {
316
+ const stored = this.storage.getItem(this.key);
317
+ if (stored === null) {
318
+ return [];
319
+ }
320
+ try {
321
+ const parsed = JSON.parse(stored);
322
+ if (Array.isArray(parsed)) {
323
+ return parsed;
324
+ }
325
+ console.error(
326
+ `${this.prefix} WebStorageErrorAdapter: storage key "${this.key}" did not hold an array, so it was replaced.`
327
+ );
328
+ } catch (error) {
329
+ console.error(
330
+ `${this.prefix} WebStorageErrorAdapter: storage key "${this.key}" held invalid JSON (${describeError(error)}), so it was replaced.`
331
+ );
332
+ }
333
+ return [];
334
+ }
335
+ };
336
+
337
+ // src/file-system.ts
338
+ async function loadNodeFileSystem() {
339
+ const moduleName = "node:fs/promises";
340
+ return await import(moduleName);
341
+ }
342
+ function parentDirectory(filePath) {
343
+ const lastSeparator = Math.max(
344
+ filePath.lastIndexOf("/"),
345
+ filePath.lastIndexOf("\\")
346
+ );
347
+ if (lastSeparator < 0) {
348
+ return null;
349
+ }
350
+ return lastSeparator === 0 ? "/" : filePath.slice(0, lastSeparator);
351
+ }
352
+ function isMissingFileError(error) {
353
+ return typeof error === "object" && error !== null && error.code === "ENOENT";
354
+ }
355
+
356
+ // src/adapters/flat-file-error-adapter.ts
357
+ var DEFAULT_MAX_FILES = 3;
358
+ function escapeLineBreaks(text) {
359
+ return text.replace(/\r?\n/g, "\\n");
360
+ }
361
+ function describeCause(cause) {
362
+ if (typeof cause === "string") {
363
+ return cause;
364
+ }
365
+ if (typeof cause === "object" && cause !== null) {
366
+ const { name, message } = cause;
367
+ if (typeof name === "string" && typeof message === "string") {
368
+ return `${name}: ${message}`;
369
+ }
370
+ }
371
+ try {
372
+ return JSON.stringify(cause) ?? String(cause);
373
+ } catch {
374
+ return String(cause);
375
+ }
376
+ }
377
+ function formatErrorLogLine(entry) {
378
+ const source = entry.source === void 0 ? "" : `${entry.source}: `;
379
+ const cause = entry.cause === void 0 ? "" : ` (cause: ${describeCause(entry.cause)})`;
380
+ return escapeLineBreaks(
381
+ `${entry.timestamp} [${entry.code}] ${source}${entry.message}${cause}`
382
+ );
383
+ }
384
+ function assertPositiveInteger(value, name) {
385
+ if (!Number.isInteger(value) || value < 1) {
386
+ throw new RangeError(
387
+ `FlatFileErrorAdapter: ${name} must be a positive integer, received ${String(value)}.`
388
+ );
389
+ }
390
+ }
391
+ var FlatFileErrorAdapter = class {
392
+ /** @throws {RangeError} if `maxBytes` or `maxFiles` is not a positive integer. */
393
+ constructor(filePath, options = {}) {
394
+ this.filePath = filePath;
395
+ this.directoryReady = false;
396
+ this.queue = Promise.resolve();
397
+ this.fs = options.fs;
398
+ this.formatLine = options.formatLine ?? formatErrorLogLine;
399
+ this.maxBytes = options.maxBytes;
400
+ this.maxFiles = options.maxFiles ?? DEFAULT_MAX_FILES;
401
+ this.createDirectory = options.createDirectory ?? true;
402
+ this.prefix = options.prefix ?? "[error-log-adapter]";
403
+ if (this.maxBytes !== void 0) {
404
+ assertPositiveInteger(this.maxBytes, "maxBytes");
405
+ }
406
+ assertPositiveInteger(this.maxFiles, "maxFiles");
407
+ }
408
+ log(entry) {
409
+ const result = this.queue.then(
410
+ () => this.write(entry)
411
+ );
412
+ this.queue = result.catch(() => void 0);
413
+ return result;
414
+ }
415
+ async write(entry) {
416
+ try {
417
+ const fs = await this.getFileSystem();
418
+ const formatted = this.formatLine(serializeEntry(entry));
419
+ const line = formatted.endsWith("\n") ? formatted : `${formatted}
420
+ `;
421
+ await this.prepareDirectory(fs);
422
+ await this.rotateIfNeeded(fs, new TextEncoder().encode(line).length);
423
+ await fs.appendFile(this.filePath, line);
424
+ return { ok: true };
425
+ } catch (error) {
426
+ console.error(`${this.prefix} FlatFileErrorAdapter:`, error);
427
+ return {
428
+ ok: false,
429
+ error: `FlatFileErrorAdapter failed to append to "${this.filePath}": ${describeError(error)}`
430
+ };
431
+ }
432
+ }
433
+ /** Uses the injected file system, or loads `node:fs/promises` on first use. */
434
+ async getFileSystem() {
435
+ if (this.fs === void 0) {
436
+ this.fs = await loadNodeFileSystem();
437
+ }
438
+ return this.fs;
439
+ }
440
+ async prepareDirectory(fs) {
441
+ if (!this.createDirectory || this.directoryReady) {
442
+ return;
443
+ }
444
+ const directory = parentDirectory(this.filePath);
445
+ if (directory !== null) {
446
+ await fs.mkdir(directory, { recursive: true });
447
+ }
448
+ this.directoryReady = true;
449
+ }
450
+ /**
451
+ * Moves `app.log` to `app.log.1` (shifting older files up, dropping the
452
+ * oldest) when the next line would push it past `maxBytes`. A failed rotation
453
+ * is reported, but the entry is still written so it is not lost.
454
+ */
455
+ async rotateIfNeeded(fs, lineBytes) {
456
+ if (this.maxBytes === void 0) {
457
+ return;
458
+ }
459
+ try {
460
+ let currentSize;
461
+ try {
462
+ currentSize = (await fs.stat(this.filePath)).size;
463
+ } catch (error) {
464
+ if (isMissingFileError(error)) {
465
+ return;
466
+ }
467
+ throw error;
468
+ }
469
+ if (currentSize === 0 || currentSize + lineBytes <= this.maxBytes) {
470
+ return;
471
+ }
472
+ await fs.rm(`${this.filePath}.${this.maxFiles}`, { force: true });
473
+ for (let index = this.maxFiles - 1; index >= 1; index -= 1) {
474
+ await this.renameIfPresent(
475
+ fs,
476
+ `${this.filePath}.${index}`,
477
+ `${this.filePath}.${index + 1}`
478
+ );
479
+ }
480
+ await fs.rename(this.filePath, `${this.filePath}.1`);
481
+ } catch (error) {
482
+ console.error(
483
+ `${this.prefix} FlatFileErrorAdapter: could not rotate "${this.filePath}", so it will keep growing: ${describeError(error)}`
484
+ );
485
+ }
486
+ }
487
+ async renameIfPresent(fs, from, to) {
488
+ try {
489
+ await fs.rename(from, to);
490
+ } catch (error) {
491
+ if (!isMissingFileError(error)) {
492
+ throw error;
493
+ }
494
+ }
495
+ }
496
+ };
497
+
498
+ // src/adapters/json-file-error-adapter.ts
499
+ var DEFAULT_INDENT = 2;
500
+ var JsonFileErrorAdapter = class {
501
+ /** @throws {RangeError} if `maxEntries` or `indent` is invalid, or `maxEntries` is used outside array mode. */
502
+ constructor(filePath, options = {}) {
503
+ this.filePath = filePath;
504
+ this.directoryReady = false;
505
+ this.queue = Promise.resolve();
506
+ this.mode = options.mode ?? "ndjson";
507
+ this.maxEntries = options.maxEntries;
508
+ this.indent = options.indent ?? DEFAULT_INDENT;
509
+ this.fs = options.fs;
510
+ this.createDirectory = options.createDirectory ?? true;
511
+ this.prefix = options.prefix ?? "[error-log-adapter]";
512
+ if (this.maxEntries !== void 0) {
513
+ if (this.mode !== "array") {
514
+ throw new RangeError(
515
+ `JsonFileErrorAdapter: maxEntries only applies to mode "array", but mode is "${this.mode}". Use mode "array" or remove maxEntries.`
516
+ );
517
+ }
518
+ if (!Number.isInteger(this.maxEntries) || this.maxEntries < 1) {
519
+ throw new RangeError(
520
+ `JsonFileErrorAdapter: maxEntries must be a positive integer, received ${String(this.maxEntries)}.`
521
+ );
522
+ }
523
+ }
524
+ if (!Number.isInteger(this.indent) || this.indent < 0) {
525
+ throw new RangeError(
526
+ `JsonFileErrorAdapter: indent must be a non-negative integer, received ${String(this.indent)}.`
527
+ );
528
+ }
529
+ }
530
+ log(entry) {
531
+ const result = this.queue.then(
532
+ () => this.write(entry)
533
+ );
534
+ this.queue = result.catch(() => void 0);
535
+ return result;
536
+ }
537
+ async write(entry) {
538
+ try {
539
+ const record = {
540
+ id: crypto.randomUUID(),
541
+ ...serializeEntry(entry)
542
+ };
543
+ const fs = await this.getFileSystem();
544
+ await this.prepareDirectory(fs);
545
+ if (this.mode === "ndjson") {
546
+ await fs.appendFile(this.filePath, `${JSON.stringify(record)}
547
+ `);
548
+ } else {
549
+ await this.rewriteArray(fs, record);
550
+ }
551
+ return { ok: true };
552
+ } catch (error) {
553
+ console.error(`${this.prefix} JsonFileErrorAdapter:`, error);
554
+ return {
555
+ ok: false,
556
+ error: `JsonFileErrorAdapter failed to write "${this.filePath}" in ${this.mode} mode: ${describeError(error)}`
557
+ };
558
+ }
559
+ }
560
+ async getFileSystem() {
561
+ if (this.fs === void 0) {
562
+ this.fs = await loadNodeFileSystem();
563
+ }
564
+ return this.fs;
565
+ }
566
+ async prepareDirectory(fs) {
567
+ if (!this.createDirectory || this.directoryReady) {
568
+ return;
569
+ }
570
+ const directory = parentDirectory(this.filePath);
571
+ if (directory !== null) {
572
+ await fs.mkdir(directory, { recursive: true });
573
+ }
574
+ this.directoryReady = true;
575
+ }
576
+ /**
577
+ * Adds the record to the array and replaces the file. The new content is
578
+ * written to a temporary file first and renamed into place, so a crash
579
+ * mid-write cannot leave a half-written file.
580
+ */
581
+ async rewriteArray(fs, record) {
582
+ const records = [
583
+ ...await this.readRecords(fs),
584
+ record
585
+ ];
586
+ const kept = this.maxEntries === void 0 ? records : records.slice(-this.maxEntries);
587
+ const temporaryPath = `${this.filePath}.tmp`;
588
+ await fs.writeFile(
589
+ temporaryPath,
590
+ `${JSON.stringify(kept, null, this.indent)}
591
+ `
592
+ );
593
+ await fs.rename(temporaryPath, this.filePath);
594
+ }
595
+ /**
596
+ * Returns the records already in the file. A missing or empty file has none.
597
+ * A file that is not a JSON array is moved to `<path>.corrupt` (so nothing is
598
+ * lost) and logging continues with a fresh file.
599
+ */
600
+ async readRecords(fs) {
601
+ let content;
602
+ try {
603
+ content = await fs.readFile(this.filePath, "utf8");
604
+ } catch (error) {
605
+ if (isMissingFileError(error)) {
606
+ return [];
607
+ }
608
+ throw error;
609
+ }
610
+ if (content.trim() === "") {
611
+ return [];
612
+ }
613
+ let problem;
614
+ try {
615
+ const parsed = JSON.parse(content);
616
+ if (Array.isArray(parsed)) {
617
+ return parsed;
618
+ }
619
+ problem = "did not hold a JSON array";
620
+ } catch (error) {
621
+ problem = `held invalid JSON (${describeError(error)})`;
622
+ }
623
+ const corruptPath = `${this.filePath}.corrupt`;
624
+ await fs.rename(this.filePath, corruptPath);
625
+ console.error(
626
+ `${this.prefix} JsonFileErrorAdapter: "${this.filePath}" ${problem}, so it was moved to "${corruptPath}" and a new file was started.`
627
+ );
628
+ return [];
629
+ }
630
+ };
131
631
  export {
132
632
  CompositeErrorAdapter,
133
633
  ConsoleErrorAdapter,
634
+ FlatFileErrorAdapter,
635
+ IndexedDbErrorAdapter,
636
+ JsonFileErrorAdapter,
134
637
  MongoDbErrorAdapter,
638
+ SqliteErrorAdapter,
639
+ WebStorageErrorAdapter,
135
640
  WebhookErrorAdapter,
641
+ buildCreateErrorLogTableSql,
642
+ createErrorLogStore,
643
+ formatErrorLogLine,
136
644
  serializeCause,
137
645
  serializeEntry
138
646
  };
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/serialize.ts","../src/adapters/console-error-adapter.ts","../src/adapters/webhook-error-adapter.ts","../src/adapters/mongodb-error-adapter.ts","../src/adapters/composite-error-adapter.ts"],"sourcesContent":["import type { ErrorLogEntry } from './types.js';\n\n/** A JSON-safe copy of an `ErrorLogEntry`. */\nexport interface SerializedErrorLogEntry {\n code: string;\n message: string;\n cause?: unknown;\n source?: string;\n timestamp: string;\n}\n\n/**\n * Converts `Error` instances into plain objects, since `JSON.stringify`\n * renders them as `{}`. Other values are returned unchanged.\n */\nexport function serializeCause(cause: unknown): unknown {\n if (cause instanceof Error) {\n return { name: cause.name, message: cause.message, stack: cause.stack };\n }\n return cause;\n}\n\n/** Returns a JSON-safe copy of the entry, adding a timestamp if missing. */\nexport function serializeEntry(entry: ErrorLogEntry): SerializedErrorLogEntry {\n const serialized: SerializedErrorLogEntry = {\n code: entry.code,\n message: entry.message,\n timestamp: entry.timestamp ?? new Date().toISOString(),\n };\n if (entry.cause !== undefined) {\n serialized.cause = serializeCause(entry.cause);\n }\n if (entry.source !== undefined) {\n serialized.source = entry.source;\n }\n return serialized;\n}\n\n/** Turns an unknown thrown value into a descriptive message. */\nexport function describeError(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n","import type {\n ErrorLogAdapter,\n ErrorLogEntry,\n ErrorLogResult,\n} from '../types.js';\n\nexport interface ConsoleErrorOptions {\n /** Text printed before each entry. Defaults to `[error-log-adapter]`. */\n prefix?: string;\n}\n\nexport class ConsoleErrorAdapter<\n TCode extends string = string,\n> implements ErrorLogAdapter<TCode> {\n private readonly prefix: string;\n\n constructor(options: ConsoleErrorOptions = {}) {\n this.prefix = options.prefix ?? '[error-log-adapter]';\n }\n\n async log(entry: ErrorLogEntry<TCode>): Promise<ErrorLogResult> {\n try {\n console.error(this.prefix, entry);\n return { ok: true };\n } catch (error: unknown) {\n return {\n ok: false,\n error: `ConsoleErrorAdapter failed: ${String(error)}`,\n };\n }\n }\n}\n","import { describeError, serializeEntry } from '../serialize.js';\nimport type {\n ErrorLogAdapter,\n ErrorLogEntry,\n ErrorLogResult,\n} from '../types.js';\n\nexport interface WebhookErrorOptions {\n /** Extra headers merged into the request. */\n headers?: Record<string, string>;\n /** Text printed before failure messages. Defaults to `[error-log-adapter]`. */\n prefix?: string;\n}\n\nexport class WebhookErrorAdapter<\n TCode extends string = string,\n> implements ErrorLogAdapter<TCode> {\n private readonly prefix: string;\n\n constructor(\n private readonly url: string,\n private readonly options: WebhookErrorOptions = {}\n ) {\n this.prefix = options.prefix ?? '[error-log-adapter]';\n }\n\n async log(entry: ErrorLogEntry<TCode>): Promise<ErrorLogResult> {\n try {\n const response: Response = await fetch(this.url, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n ...this.options.headers,\n },\n body: JSON.stringify(serializeEntry(entry)),\n });\n if (!response.ok) {\n return this.fail(\n `webhook ${this.url} responded with HTTP ${response.status}`\n );\n }\n return { ok: true };\n } catch (error: unknown) {\n console.error(`${this.prefix} WebhookErrorAdapter:`, error);\n return this.fail(\n `could not reach webhook ${this.url}: ${describeError(error)}`\n );\n }\n }\n\n private fail(reason: string): ErrorLogResult {\n return { ok: false, error: `WebhookErrorAdapter failed: ${reason}` };\n }\n}\n","import { describeError, serializeEntry } from '../serialize.js';\nimport type {\n ErrorLogAdapter,\n ErrorLogEntry,\n ErrorLogResult,\n} from '../types.js';\n\n/** The subset of a MongoDB `Collection` this adapter uses. */\nexport interface CollectionLike {\n insertOne(doc: object): Promise<unknown>;\n}\n\nexport interface MongoDbErrorOptions {\n /** Text printed before failure messages. Defaults to `[error-log-adapter]`. */\n prefix?: string;\n}\n\nexport class MongoDbErrorAdapter<\n TCode extends string = string,\n> implements ErrorLogAdapter<TCode> {\n private readonly prefix: string;\n\n constructor(\n private readonly collection: CollectionLike,\n options: MongoDbErrorOptions = {}\n ) {\n this.prefix = options.prefix ?? '[error-log-adapter]';\n }\n\n async log(entry: ErrorLogEntry<TCode>): Promise<ErrorLogResult> {\n try {\n await this.collection.insertOne(serializeEntry(entry));\n return { ok: true };\n } catch (error: unknown) {\n console.error(`${this.prefix} MongoDbErrorAdapter:`, error);\n return {\n ok: false,\n error: `MongoDbErrorAdapter failed to insert entry: ${describeError(error)}`,\n };\n }\n }\n}\n","import type {\n ErrorLogAdapter,\n ErrorLogEntry,\n ErrorLogResult,\n} from '../types.js';\n\n/** Sends every entry to several adapters. Succeeds only if all of them do. */\nexport class CompositeErrorAdapter<\n TCode extends string = string,\n> implements ErrorLogAdapter<TCode> {\n constructor(\n private readonly adapters: ReadonlyArray<ErrorLogAdapter<TCode>>\n ) {}\n\n async log(entry: ErrorLogEntry<TCode>): Promise<ErrorLogResult> {\n const results: ErrorLogResult[] = await Promise.all(\n this.adapters.map(\n async (adapter: ErrorLogAdapter<TCode>): Promise<ErrorLogResult> => {\n try {\n return await adapter.log(entry);\n } catch (error: unknown) {\n // Adapters should not throw, but a faulty custom one must not break the others.\n return {\n ok: false,\n error: `adapter threw instead of returning a result: ${String(error)}`,\n };\n }\n }\n )\n );\n const failures: string[] = results.flatMap(\n (result: ErrorLogResult): string[] =>\n result.ok ? [] : [result.error ?? 'unknown failure']\n );\n if (failures.length === 0) {\n return { ok: true };\n }\n return {\n ok: false,\n error: `${failures.length} of ${results.length} adapters failed: ${failures.join('; ')}`,\n };\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/serialize.ts","../src/adapters/console-error-adapter.ts","../src/adapters/webhook-error-adapter.ts","../src/adapters/mongodb-error-adapter.ts","../src/adapters/composite-error-adapter.ts","../src/adapters/indexeddb-error-adapter.ts","../src/adapters/sqlite-error-adapter.ts","../src/adapters/web-storage-error-adapter.ts","../src/file-system.ts","../src/adapters/flat-file-error-adapter.ts","../src/adapters/json-file-error-adapter.ts"],"sourcesContent":["import type { ErrorLogEntry } from './types.js';\n\n/** A JSON-safe copy of an `ErrorLogEntry`. */\nexport interface SerializedErrorLogEntry {\n code: string;\n message: string;\n cause?: unknown;\n source?: string;\n timestamp: string;\n}\n\n/**\n * Converts `Error` instances into plain objects, since `JSON.stringify`\n * renders them as `{}`. Other values are returned unchanged.\n */\nexport function serializeCause(cause: unknown): unknown {\n if (cause instanceof Error) {\n return { name: cause.name, message: cause.message, stack: cause.stack };\n }\n return cause;\n}\n\n/** Returns a JSON-safe copy of the entry, adding a timestamp if missing. */\nexport function serializeEntry(entry: ErrorLogEntry): SerializedErrorLogEntry {\n const serialized: SerializedErrorLogEntry = {\n code: entry.code,\n message: entry.message,\n timestamp: entry.timestamp ?? new Date().toISOString(),\n };\n if (entry.cause !== undefined) {\n serialized.cause = serializeCause(entry.cause);\n }\n if (entry.source !== undefined) {\n serialized.source = entry.source;\n }\n return serialized;\n}\n\n/** Turns an unknown thrown value into a descriptive message. */\nexport function describeError(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n","import type {\n ErrorLogAdapter,\n ErrorLogEntry,\n ErrorLogResult,\n} from '../types.js';\n\nexport interface ConsoleErrorOptions {\n /** Text printed before each entry. Defaults to `[error-log-adapter]`. */\n prefix?: string;\n}\n\nexport class ConsoleErrorAdapter<\n TCode extends string = string,\n> implements ErrorLogAdapter<TCode> {\n private readonly prefix: string;\n\n constructor(options: ConsoleErrorOptions = {}) {\n this.prefix = options.prefix ?? '[error-log-adapter]';\n }\n\n async log(entry: ErrorLogEntry<TCode>): Promise<ErrorLogResult> {\n try {\n console.error(this.prefix, entry);\n return { ok: true };\n } catch (error: unknown) {\n return {\n ok: false,\n error: `ConsoleErrorAdapter failed: ${String(error)}`,\n };\n }\n }\n}\n","import { describeError, serializeEntry } from '../serialize.js';\nimport type {\n ErrorLogAdapter,\n ErrorLogEntry,\n ErrorLogResult,\n} from '../types.js';\n\nexport interface WebhookErrorOptions {\n /** Extra headers merged into the request. */\n headers?: Record<string, string>;\n /** Text printed before failure messages. Defaults to `[error-log-adapter]`. */\n prefix?: string;\n}\n\nexport class WebhookErrorAdapter<\n TCode extends string = string,\n> implements ErrorLogAdapter<TCode> {\n private readonly prefix: string;\n\n constructor(\n private readonly url: string,\n private readonly options: WebhookErrorOptions = {}\n ) {\n this.prefix = options.prefix ?? '[error-log-adapter]';\n }\n\n async log(entry: ErrorLogEntry<TCode>): Promise<ErrorLogResult> {\n try {\n const response: Response = await fetch(this.url, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n ...this.options.headers,\n },\n body: JSON.stringify(serializeEntry(entry)),\n });\n if (!response.ok) {\n return this.fail(\n `webhook ${this.url} responded with HTTP ${response.status}`\n );\n }\n return { ok: true };\n } catch (error: unknown) {\n console.error(`${this.prefix} WebhookErrorAdapter:`, error);\n return this.fail(\n `could not reach webhook ${this.url}: ${describeError(error)}`\n );\n }\n }\n\n private fail(reason: string): ErrorLogResult {\n return { ok: false, error: `WebhookErrorAdapter failed: ${reason}` };\n }\n}\n","import { describeError, serializeEntry } from '../serialize.js';\nimport type {\n ErrorLogAdapter,\n ErrorLogEntry,\n ErrorLogResult,\n} from '../types.js';\n\n/** The subset of a MongoDB `Collection` this adapter uses. */\nexport interface CollectionLike {\n insertOne(doc: object): Promise<unknown>;\n}\n\nexport interface MongoDbErrorOptions {\n /** Text printed before failure messages. Defaults to `[error-log-adapter]`. */\n prefix?: string;\n}\n\nexport class MongoDbErrorAdapter<\n TCode extends string = string,\n> implements ErrorLogAdapter<TCode> {\n private readonly prefix: string;\n\n constructor(\n private readonly collection: CollectionLike,\n options: MongoDbErrorOptions = {}\n ) {\n this.prefix = options.prefix ?? '[error-log-adapter]';\n }\n\n async log(entry: ErrorLogEntry<TCode>): Promise<ErrorLogResult> {\n try {\n await this.collection.insertOne(serializeEntry(entry));\n return { ok: true };\n } catch (error: unknown) {\n console.error(`${this.prefix} MongoDbErrorAdapter:`, error);\n return {\n ok: false,\n error: `MongoDbErrorAdapter failed to insert entry: ${describeError(error)}`,\n };\n }\n }\n}\n","import type {\n ErrorLogAdapter,\n ErrorLogEntry,\n ErrorLogResult,\n} from '../types.js';\n\n/** Sends every entry to several adapters. Succeeds only if all of them do. */\nexport class CompositeErrorAdapter<\n TCode extends string = string,\n> implements ErrorLogAdapter<TCode> {\n constructor(\n private readonly adapters: ReadonlyArray<ErrorLogAdapter<TCode>>\n ) {}\n\n async log(entry: ErrorLogEntry<TCode>): Promise<ErrorLogResult> {\n const results: ErrorLogResult[] = await Promise.all(\n this.adapters.map(\n async (adapter: ErrorLogAdapter<TCode>): Promise<ErrorLogResult> => {\n try {\n return await adapter.log(entry);\n } catch (error: unknown) {\n // Adapters should not throw, but a faulty custom one must not break the others.\n return {\n ok: false,\n error: `adapter threw instead of returning a result: ${String(error)}`,\n };\n }\n }\n )\n );\n const failures: string[] = results.flatMap(\n (result: ErrorLogResult): string[] =>\n result.ok ? [] : [result.error ?? 'unknown failure']\n );\n if (failures.length === 0) {\n return { ok: true };\n }\n return {\n ok: false,\n error: `${failures.length} of ${results.length} adapters failed: ${failures.join('; ')}`,\n };\n }\n}\n","import { describeError, serializeEntry } from '../serialize.js';\nimport type { SerializedErrorLogEntry } from '../serialize.js';\nimport type {\n ErrorLogAdapter,\n ErrorLogEntry,\n ErrorLogResult,\n} from '../types.js';\n\n/** Default object store name. */\nexport const DEFAULT_STORE_NAME = 'error_log';\n\n/** Event handler slot. Typed loosely so real DOM `IDBTransaction` objects fit. */\ntype IdbHandler = ((event: never) => unknown) | null;\n\n/** The subset of an `IDBObjectStore` this adapter uses. */\nexport interface IdbObjectStoreLike {\n add(value: unknown): unknown;\n}\n\n/** The subset of an `IDBTransaction` this adapter uses. */\nexport interface IdbTransactionLike {\n objectStore(name: string): IdbObjectStoreLike;\n oncomplete: IdbHandler;\n onerror: IdbHandler;\n onabort: IdbHandler;\n readonly error?: { message?: string } | null;\n}\n\n/** The subset of an `IDBDatabase` this adapter uses. */\nexport interface IdbDatabaseLike {\n transaction(storeNames: string, mode: 'readwrite'): IdbTransactionLike;\n}\n\n/** The subset of an `IDBDatabase` available inside `onupgradeneeded`. */\nexport interface IdbUpgradeDatabaseLike {\n readonly objectStoreNames: { contains(name: string): boolean };\n createObjectStore(\n name: string,\n options: { keyPath: string }\n ): { createIndex(name: string, keyPath: string): unknown };\n}\n\n/** A stored record: the serialized entry plus its generated UUID key. */\nexport interface IndexedDbErrorRecord extends SerializedErrorLogEntry {\n id: string;\n}\n\nexport interface IndexedDbErrorOptions {\n /** Object store to write to. Defaults to `error_log`. */\n storeName?: string;\n /** Text printed before failure messages. Defaults to `[error-log-adapter]`. */\n prefix?: string;\n}\n\n/**\n * Creates the error log object store (UUID `id` key, `timestamp` index).\n * Call it from your `onupgradeneeded` handler. Does nothing if it already exists.\n */\nexport function createErrorLogStore(\n database: IdbUpgradeDatabaseLike,\n storeName: string = DEFAULT_STORE_NAME\n): void {\n if (database.objectStoreNames.contains(storeName)) {\n return;\n }\n database\n .createObjectStore(storeName, { keyPath: 'id' })\n .createIndex('timestamp', 'timestamp');\n}\n\nexport class IndexedDbErrorAdapter<\n TCode extends string = string,\n> implements ErrorLogAdapter<TCode> {\n private readonly storeName: string;\n private readonly prefix: string;\n\n constructor(\n private readonly database: IdbDatabaseLike,\n options: IndexedDbErrorOptions = {}\n ) {\n this.storeName = options.storeName ?? DEFAULT_STORE_NAME;\n this.prefix = options.prefix ?? '[error-log-adapter]';\n }\n\n async log(entry: ErrorLogEntry<TCode>): Promise<ErrorLogResult> {\n try {\n const record: IndexedDbErrorRecord = {\n id: crypto.randomUUID(),\n ...serializeEntry(entry),\n };\n await this.write(record);\n return { ok: true };\n } catch (error: unknown) {\n console.error(`${this.prefix} IndexedDbErrorAdapter:`, error);\n return {\n ok: false,\n error: `IndexedDbErrorAdapter failed to write to object store \"${this.storeName}\": ${describeError(error)}`,\n };\n }\n }\n\n /** Resolves once the transaction commits; rejects if it errors or aborts. */\n private write(record: IndexedDbErrorRecord): Promise<void> {\n return new Promise<void>(\n (resolve: () => void, reject: (reason: unknown) => void): void => {\n const transaction: IdbTransactionLike = this.database.transaction(\n this.storeName,\n 'readwrite'\n );\n const fail = (action: string) => (): void =>\n reject(\n new Error(\n `transaction ${action}: ${transaction.error?.message ?? 'no details provided'}`\n )\n );\n transaction.oncomplete = (): void => resolve();\n transaction.onerror = fail('failed');\n transaction.onabort = fail('aborted');\n transaction.objectStore(this.storeName).add(record);\n }\n );\n }\n}\n","import { describeError, serializeEntry } from '../serialize.js';\nimport type {\n ErrorLogAdapter,\n ErrorLogEntry,\n ErrorLogResult,\n} from '../types.js';\n\n/** Default table name. */\nexport const DEFAULT_TABLE_NAME = 'error_log';\n\nconst IDENTIFIER_PATTERN: RegExp = /^[A-Za-z_][A-Za-z0-9_]*$/;\n\n/**\n * The only thing this adapter needs from a SQLite driver: run one statement\n * with positional (`?`) parameters. `bun:sqlite` already matches this shape.\n * Other drivers need a one-line wrapper, for example:\n *\n * ```ts\n * // better-sqlite3 or node:sqlite\n * const client: SqliteClientLike = {\n * run: (sql, params) => db.prepare(sql).run(...params),\n * };\n * ```\n */\nexport interface SqliteClientLike {\n run(sql: string, params: unknown[]): unknown;\n}\n\nexport interface SqliteErrorOptions {\n /** Table to write to. Defaults to `error_log`. Letters, digits and `_` only. */\n tableName?: string;\n /** Create the table (and index) on first use. Defaults to `true`. */\n createTable?: boolean;\n /** Text printed before failure messages. Defaults to `[error-log-adapter]`. */\n prefix?: string;\n}\n\n/** Throws a descriptive error unless `tableName` is a plain SQL identifier. */\nfunction assertValidTableName(tableName: string): void {\n if (!IDENTIFIER_PATTERN.test(tableName)) {\n throw new RangeError(\n `SqliteErrorAdapter: invalid table name \"${tableName}\". Use only letters, digits and underscores, and do not start with a digit.`\n );\n }\n}\n\n/**\n * Returns the statements that create the error log table and its index.\n * Use this to run your own migration when `createTable` is disabled.\n */\nexport function buildCreateErrorLogTableSql(\n tableName: string = DEFAULT_TABLE_NAME\n): string[] {\n assertValidTableName(tableName);\n return [\n `CREATE TABLE IF NOT EXISTS ${tableName} (` +\n 'id TEXT PRIMARY KEY NOT NULL, ' +\n 'code TEXT NOT NULL, ' +\n 'message TEXT NOT NULL, ' +\n 'cause TEXT, ' +\n 'source TEXT, ' +\n 'occurred_at TEXT NOT NULL, ' +\n \"created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))\" +\n ')',\n `CREATE INDEX IF NOT EXISTS ${tableName}_occurred_at_idx ON ${tableName} (occurred_at)`,\n ];\n}\n\nexport class SqliteErrorAdapter<\n TCode extends string = string,\n> implements ErrorLogAdapter<TCode> {\n private readonly tableName: string;\n private readonly createTable: boolean;\n private readonly prefix: string;\n private tableReady: Promise<void> | null = null;\n\n /** @throws {RangeError} if `options.tableName` is not a plain identifier. */\n constructor(\n private readonly client: SqliteClientLike,\n options: SqliteErrorOptions = {}\n ) {\n this.tableName = options.tableName ?? DEFAULT_TABLE_NAME;\n assertValidTableName(this.tableName);\n this.createTable = options.createTable ?? true;\n this.prefix = options.prefix ?? '[error-log-adapter]';\n }\n\n async log(entry: ErrorLogEntry<TCode>): Promise<ErrorLogResult> {\n try {\n await this.ensureTable();\n const serialized = serializeEntry(entry);\n await this.client.run(\n `INSERT INTO ${this.tableName} (id, code, message, cause, source, occurred_at) VALUES (?, ?, ?, ?, ?, ?)`,\n [\n crypto.randomUUID(),\n serialized.code,\n serialized.message,\n serialized.cause === undefined\n ? null\n : JSON.stringify(serialized.cause),\n serialized.source ?? null,\n serialized.timestamp,\n ]\n );\n return { ok: true };\n } catch (error: unknown) {\n console.error(`${this.prefix} SqliteErrorAdapter:`, error);\n return {\n ok: false,\n error: `SqliteErrorAdapter failed to write to table \"${this.tableName}\": ${describeError(error)}`,\n };\n }\n }\n\n /** Creates the table once. A failed attempt is retried on the next log call. */\n private ensureTable(): Promise<void> {\n if (!this.createTable) {\n return Promise.resolve();\n }\n if (this.tableReady === null) {\n this.tableReady = this.runCreateStatements().catch(\n (error: unknown): never => {\n this.tableReady = null;\n throw error;\n }\n );\n }\n return this.tableReady;\n }\n\n private async runCreateStatements(): Promise<void> {\n for (const sql of buildCreateErrorLogTableSql(this.tableName)) {\n await this.client.run(sql, []);\n }\n }\n}\n","import { describeError, serializeEntry } from '../serialize.js';\nimport type { SerializedErrorLogEntry } from '../serialize.js';\nimport type {\n ErrorLogAdapter,\n ErrorLogEntry,\n ErrorLogResult,\n} from '../types.js';\n\n/** Default storage key. */\nexport const DEFAULT_STORAGE_KEY = 'error_log';\n\n/** Default number of entries kept before the oldest are dropped. */\nexport const DEFAULT_MAX_ENTRIES = 100;\n\n/**\n * The subset of the Web Storage API this adapter uses. Both `sessionStorage`\n * and `localStorage` fit.\n */\nexport interface StorageLike {\n getItem(key: string): string | null;\n setItem(key: string, value: string): void;\n}\n\n/** A stored record: the serialized entry plus its generated UUID. */\nexport interface WebStorageErrorRecord extends SerializedErrorLogEntry {\n id: string;\n}\n\nexport interface WebStorageErrorOptions {\n /** Key the entries are stored under, as one JSON array. Defaults to `error_log`. */\n key?: string;\n /** Most entries to keep. The oldest are dropped first. Defaults to `100`. */\n maxEntries?: number;\n /** Text printed before failure messages. Defaults to `[error-log-adapter]`. */\n prefix?: string;\n}\n\n/** Browsers report a full store with different names and codes. */\nfunction isQuotaError(error: unknown): boolean {\n if (typeof error !== 'object' || error === null) {\n return false;\n }\n const { name, code } = error as { name?: unknown; code?: unknown };\n return (\n (typeof name === 'string' && name.toLowerCase().includes('quota')) ||\n code === 22 ||\n code === 1014\n );\n}\n\n/**\n * Keeps the newest entries in a Web Storage object (`sessionStorage` or\n * `localStorage`) as a JSON array under one key, capped at `maxEntries`.\n * When the store is full, the oldest entries are dropped until the new one fits.\n */\nexport class WebStorageErrorAdapter<\n TCode extends string = string,\n> implements ErrorLogAdapter<TCode> {\n private readonly key: string;\n private readonly maxEntries: number;\n private readonly prefix: string;\n\n /** @throws {RangeError} if `options.maxEntries` is not a positive integer. */\n constructor(\n private readonly storage: StorageLike,\n options: WebStorageErrorOptions = {}\n ) {\n this.key = options.key ?? DEFAULT_STORAGE_KEY;\n this.maxEntries = options.maxEntries ?? DEFAULT_MAX_ENTRIES;\n this.prefix = options.prefix ?? '[error-log-adapter]';\n if (!Number.isInteger(this.maxEntries) || this.maxEntries < 1) {\n throw new RangeError(\n `WebStorageErrorAdapter: maxEntries must be a positive integer, received ${String(options.maxEntries)}.`\n );\n }\n }\n\n async log(entry: ErrorLogEntry<TCode>): Promise<ErrorLogResult> {\n try {\n const record: WebStorageErrorRecord = {\n id: crypto.randomUUID(),\n ...serializeEntry(entry),\n };\n this.append(record);\n return { ok: true };\n } catch (error: unknown) {\n console.error(`${this.prefix} WebStorageErrorAdapter:`, error);\n return {\n ok: false,\n error: `WebStorageErrorAdapter failed to write to storage key \"${this.key}\": ${describeError(error)}`,\n };\n }\n }\n\n /** Reads, appends, trims, and writes back. Runs without awaiting, so calls never interleave. */\n private append(record: WebStorageErrorRecord): void {\n const records: WebStorageErrorRecord[] = [\n ...this.readRecords(),\n record,\n ].slice(-this.maxEntries);\n for (;;) {\n try {\n this.storage.setItem(this.key, JSON.stringify(records));\n return;\n } catch (error: unknown) {\n if (!isQuotaError(error) || records.length <= 1) {\n throw error;\n }\n records.shift();\n }\n }\n }\n\n /** Returns the stored records, or none if the value is missing or unreadable. */\n private readRecords(): WebStorageErrorRecord[] {\n const stored: string | null = this.storage.getItem(this.key);\n if (stored === null) {\n return [];\n }\n try {\n const parsed: unknown = JSON.parse(stored);\n if (Array.isArray(parsed)) {\n return parsed as WebStorageErrorRecord[];\n }\n console.error(\n `${this.prefix} WebStorageErrorAdapter: storage key \"${this.key}\" did not hold an array, so it was replaced.`\n );\n } catch (error: unknown) {\n console.error(\n `${this.prefix} WebStorageErrorAdapter: storage key \"${this.key}\" held invalid JSON (${describeError(error)}), so it was replaced.`\n );\n }\n return [];\n }\n}\n","/**\n * The subset of `node:fs/promises` the file adapters use. Pass `fs.promises`\n * (or `node:fs/promises`) yourself, or leave `fs` unset in Node and Bun.\n */\nexport interface FileSystemLike {\n appendFile(path: string, data: string): Promise<unknown>;\n mkdir(path: string, options: { recursive: true }): Promise<unknown>;\n stat(path: string): Promise<{ size: number }>;\n rename(from: string, to: string): Promise<unknown>;\n rm(path: string, options: { force: true }): Promise<unknown>;\n}\n\n/** `FileSystemLike` plus the reading and replacing that whole-file writes need. */\nexport interface ReadWriteFileSystemLike extends FileSystemLike {\n readFile(path: string, encoding: 'utf8'): Promise<string>;\n writeFile(path: string, data: string): Promise<unknown>;\n}\n\n/** Loads `node:fs/promises`. Only call this from code that runs in Node or Bun. */\nexport async function loadNodeFileSystem(): Promise<ReadWriteFileSystemLike> {\n // A variable specifier keeps browser bundlers from resolving it.\n const moduleName: string = 'node:fs/promises';\n return (await import(moduleName)) as ReadWriteFileSystemLike;\n}\n\n/** Returns the folder part of a path, or `null` if the path has none. */\nexport function parentDirectory(filePath: string): string | null {\n const lastSeparator: number = Math.max(\n filePath.lastIndexOf('/'),\n filePath.lastIndexOf('\\\\')\n );\n if (lastSeparator < 0) {\n return null;\n }\n return lastSeparator === 0 ? '/' : filePath.slice(0, lastSeparator);\n}\n\n/** True for the error Node throws when a file or folder does not exist. */\nexport function isMissingFileError(error: unknown): boolean {\n return (\n typeof error === 'object' &&\n error !== null &&\n (error as { code?: unknown }).code === 'ENOENT'\n );\n}\n","import {\n isMissingFileError,\n loadNodeFileSystem,\n parentDirectory,\n} from '../file-system.js';\nimport type { FileSystemLike } from '../file-system.js';\nimport { describeError, serializeEntry } from '../serialize.js';\nimport type { SerializedErrorLogEntry } from '../serialize.js';\nimport type {\n ErrorLogAdapter,\n ErrorLogEntry,\n ErrorLogResult,\n} from '../types.js';\n\n/** Default number of rotated files kept when `maxBytes` is set. */\nexport const DEFAULT_MAX_FILES = 3;\n\nexport interface FlatFileErrorOptions {\n /** File system to write to. Defaults to `node:fs/promises`, loaded on first use. */\n fs?: FileSystemLike;\n /** Turns an entry into one line of text. Defaults to `formatErrorLogLine`. */\n formatLine?: (entry: SerializedErrorLogEntry) => string;\n /** Rotate the file before it would grow past this many bytes. Off by default. */\n maxBytes?: number;\n /** Rotated files to keep (`app.log.1` is newest). Defaults to `3`. Needs `maxBytes`. */\n maxFiles?: number;\n /** Create missing parent directories on first use. Defaults to `true`. */\n createDirectory?: boolean;\n /** Text printed before failure messages. Defaults to `[error-log-adapter]`. */\n prefix?: string;\n}\n\n/** Keeps one entry on one line by escaping line breaks. */\nfunction escapeLineBreaks(text: string): string {\n return text.replace(/\\r?\\n/g, '\\\\n');\n}\n\nfunction describeCause(cause: unknown): string {\n if (typeof cause === 'string') {\n return cause;\n }\n if (typeof cause === 'object' && cause !== null) {\n const { name, message } = cause as { name?: unknown; message?: unknown };\n if (typeof name === 'string' && typeof message === 'string') {\n return `${name}: ${message}`;\n }\n }\n try {\n return JSON.stringify(cause) ?? String(cause);\n } catch {\n return String(cause);\n }\n}\n\n/**\n * The default line format:\n * `2026-01-02T03:04:05.000Z [CODE] source: message (cause: Error: details)`.\n * `source` and `cause` are left out when the entry has none.\n */\nexport function formatErrorLogLine(entry: SerializedErrorLogEntry): string {\n const source: string = entry.source === undefined ? '' : `${entry.source}: `;\n const cause: string =\n entry.cause === undefined ? '' : ` (cause: ${describeCause(entry.cause)})`;\n return escapeLineBreaks(\n `${entry.timestamp} [${entry.code}] ${source}${entry.message}${cause}`\n );\n}\n\nfunction assertPositiveInteger(value: number, name: string): void {\n if (!Number.isInteger(value) || value < 1) {\n throw new RangeError(\n `FlatFileErrorAdapter: ${name} must be a positive integer, received ${String(value)}.`\n );\n }\n}\n\n/**\n * Appends one text line per entry to a file, optionally rotating it by size.\n * Writes are queued, so concurrent `log` calls never interleave or race a rotation.\n */\nexport class FlatFileErrorAdapter<\n TCode extends string = string,\n> implements ErrorLogAdapter<TCode> {\n private readonly formatLine: (entry: SerializedErrorLogEntry) => string;\n private readonly maxBytes: number | undefined;\n private readonly maxFiles: number;\n private readonly createDirectory: boolean;\n private readonly prefix: string;\n private fs: FileSystemLike | undefined;\n private directoryReady: boolean = false;\n private queue: Promise<unknown> = Promise.resolve();\n\n /** @throws {RangeError} if `maxBytes` or `maxFiles` is not a positive integer. */\n constructor(\n private readonly filePath: string,\n options: FlatFileErrorOptions = {}\n ) {\n this.fs = options.fs;\n this.formatLine = options.formatLine ?? formatErrorLogLine;\n this.maxBytes = options.maxBytes;\n this.maxFiles = options.maxFiles ?? DEFAULT_MAX_FILES;\n this.createDirectory = options.createDirectory ?? true;\n this.prefix = options.prefix ?? '[error-log-adapter]';\n if (this.maxBytes !== undefined) {\n assertPositiveInteger(this.maxBytes, 'maxBytes');\n }\n assertPositiveInteger(this.maxFiles, 'maxFiles');\n }\n\n log(entry: ErrorLogEntry<TCode>): Promise<ErrorLogResult> {\n const result: Promise<ErrorLogResult> = this.queue.then(() =>\n this.write(entry)\n );\n // write() never rejects, but keep the queue alive no matter what.\n this.queue = result.catch(() => undefined);\n return result;\n }\n\n private async write(entry: ErrorLogEntry<TCode>): Promise<ErrorLogResult> {\n try {\n const fs: FileSystemLike = await this.getFileSystem();\n const formatted: string = this.formatLine(serializeEntry(entry));\n const line: string = formatted.endsWith('\\n')\n ? formatted\n : `${formatted}\\n`;\n await this.prepareDirectory(fs);\n await this.rotateIfNeeded(fs, new TextEncoder().encode(line).length);\n await fs.appendFile(this.filePath, line);\n return { ok: true };\n } catch (error: unknown) {\n console.error(`${this.prefix} FlatFileErrorAdapter:`, error);\n return {\n ok: false,\n error: `FlatFileErrorAdapter failed to append to \"${this.filePath}\": ${describeError(error)}`,\n };\n }\n }\n\n /** Uses the injected file system, or loads `node:fs/promises` on first use. */\n private async getFileSystem(): Promise<FileSystemLike> {\n if (this.fs === undefined) {\n this.fs = await loadNodeFileSystem();\n }\n return this.fs;\n }\n\n private async prepareDirectory(fs: FileSystemLike): Promise<void> {\n if (!this.createDirectory || this.directoryReady) {\n return;\n }\n const directory: string | null = parentDirectory(this.filePath);\n if (directory !== null) {\n await fs.mkdir(directory, { recursive: true });\n }\n this.directoryReady = true;\n }\n\n /**\n * Moves `app.log` to `app.log.1` (shifting older files up, dropping the\n * oldest) when the next line would push it past `maxBytes`. A failed rotation\n * is reported, but the entry is still written so it is not lost.\n */\n private async rotateIfNeeded(\n fs: FileSystemLike,\n lineBytes: number\n ): Promise<void> {\n if (this.maxBytes === undefined) {\n return;\n }\n try {\n let currentSize: number;\n try {\n currentSize = (await fs.stat(this.filePath)).size;\n } catch (error: unknown) {\n if (isMissingFileError(error)) {\n return;\n }\n throw error;\n }\n if (currentSize === 0 || currentSize + lineBytes <= this.maxBytes) {\n return;\n }\n await fs.rm(`${this.filePath}.${this.maxFiles}`, { force: true });\n for (let index = this.maxFiles - 1; index >= 1; index -= 1) {\n await this.renameIfPresent(\n fs,\n `${this.filePath}.${index}`,\n `${this.filePath}.${index + 1}`\n );\n }\n await fs.rename(this.filePath, `${this.filePath}.1`);\n } catch (error: unknown) {\n console.error(\n `${this.prefix} FlatFileErrorAdapter: could not rotate \"${this.filePath}\", so it will keep growing: ${describeError(error)}`\n );\n }\n }\n\n private async renameIfPresent(\n fs: FileSystemLike,\n from: string,\n to: string\n ): Promise<void> {\n try {\n await fs.rename(from, to);\n } catch (error: unknown) {\n if (!isMissingFileError(error)) {\n throw error;\n }\n }\n }\n}\n","import {\n isMissingFileError,\n loadNodeFileSystem,\n parentDirectory,\n} from '../file-system.js';\nimport type { ReadWriteFileSystemLike } from '../file-system.js';\nimport { describeError, serializeEntry } from '../serialize.js';\nimport type { SerializedErrorLogEntry } from '../serialize.js';\nimport type {\n ErrorLogAdapter,\n ErrorLogEntry,\n ErrorLogResult,\n} from '../types.js';\n\n/** Default number of spaces used to indent array mode output. */\nexport const DEFAULT_INDENT = 2;\n\n/** A stored record: the serialized entry plus its generated UUID. */\nexport interface JsonFileErrorRecord extends SerializedErrorLogEntry {\n id: string;\n}\n\nexport interface JsonFileErrorOptions {\n /**\n * `ndjson` (default) appends one compact JSON object per line, which is cheap\n * and safe to tail. `array` keeps the whole file as one JSON array that is\n * rewritten on every entry.\n */\n mode?: 'ndjson' | 'array';\n /** Array mode only: most entries to keep. The oldest are dropped first. */\n maxEntries?: number;\n /** Array mode only: spaces of indentation. Defaults to `2`; `0` is compact. */\n indent?: number;\n /** File system to write to. Defaults to `node:fs/promises`, loaded on first use. */\n fs?: ReadWriteFileSystemLike;\n /** Create missing parent directories on first use. Defaults to `true`. */\n createDirectory?: boolean;\n /** Text printed before failure messages. Defaults to `[error-log-adapter]`. */\n prefix?: string;\n}\n\n/**\n * Stores entries as JSON in a file (Node and Bun), either as newline-delimited\n * JSON or as one array. Writes are queued, so concurrent `log` calls stay in\n * order. Only one process should write to a given file.\n */\nexport class JsonFileErrorAdapter<\n TCode extends string = string,\n> implements ErrorLogAdapter<TCode> {\n private readonly mode: 'ndjson' | 'array';\n private readonly maxEntries: number | undefined;\n private readonly indent: number;\n private readonly createDirectory: boolean;\n private readonly prefix: string;\n private fs: ReadWriteFileSystemLike | undefined;\n private directoryReady: boolean = false;\n private queue: Promise<unknown> = Promise.resolve();\n\n /** @throws {RangeError} if `maxEntries` or `indent` is invalid, or `maxEntries` is used outside array mode. */\n constructor(\n private readonly filePath: string,\n options: JsonFileErrorOptions = {}\n ) {\n this.mode = options.mode ?? 'ndjson';\n this.maxEntries = options.maxEntries;\n this.indent = options.indent ?? DEFAULT_INDENT;\n this.fs = options.fs;\n this.createDirectory = options.createDirectory ?? true;\n this.prefix = options.prefix ?? '[error-log-adapter]';\n if (this.maxEntries !== undefined) {\n if (this.mode !== 'array') {\n throw new RangeError(\n `JsonFileErrorAdapter: maxEntries only applies to mode \"array\", but mode is \"${this.mode}\". Use mode \"array\" or remove maxEntries.`\n );\n }\n if (!Number.isInteger(this.maxEntries) || this.maxEntries < 1) {\n throw new RangeError(\n `JsonFileErrorAdapter: maxEntries must be a positive integer, received ${String(this.maxEntries)}.`\n );\n }\n }\n if (!Number.isInteger(this.indent) || this.indent < 0) {\n throw new RangeError(\n `JsonFileErrorAdapter: indent must be a non-negative integer, received ${String(this.indent)}.`\n );\n }\n }\n\n log(entry: ErrorLogEntry<TCode>): Promise<ErrorLogResult> {\n const result: Promise<ErrorLogResult> = this.queue.then(() =>\n this.write(entry)\n );\n // write() never rejects, but keep the queue alive no matter what.\n this.queue = result.catch(() => undefined);\n return result;\n }\n\n private async write(entry: ErrorLogEntry<TCode>): Promise<ErrorLogResult> {\n try {\n const record: JsonFileErrorRecord = {\n id: crypto.randomUUID(),\n ...serializeEntry(entry),\n };\n const fs: ReadWriteFileSystemLike = await this.getFileSystem();\n await this.prepareDirectory(fs);\n if (this.mode === 'ndjson') {\n await fs.appendFile(this.filePath, `${JSON.stringify(record)}\\n`);\n } else {\n await this.rewriteArray(fs, record);\n }\n return { ok: true };\n } catch (error: unknown) {\n console.error(`${this.prefix} JsonFileErrorAdapter:`, error);\n return {\n ok: false,\n error: `JsonFileErrorAdapter failed to write \"${this.filePath}\" in ${this.mode} mode: ${describeError(error)}`,\n };\n }\n }\n\n private async getFileSystem(): Promise<ReadWriteFileSystemLike> {\n if (this.fs === undefined) {\n this.fs = await loadNodeFileSystem();\n }\n return this.fs;\n }\n\n private async prepareDirectory(fs: ReadWriteFileSystemLike): Promise<void> {\n if (!this.createDirectory || this.directoryReady) {\n return;\n }\n const directory: string | null = parentDirectory(this.filePath);\n if (directory !== null) {\n await fs.mkdir(directory, { recursive: true });\n }\n this.directoryReady = true;\n }\n\n /**\n * Adds the record to the array and replaces the file. The new content is\n * written to a temporary file first and renamed into place, so a crash\n * mid-write cannot leave a half-written file.\n */\n private async rewriteArray(\n fs: ReadWriteFileSystemLike,\n record: JsonFileErrorRecord\n ): Promise<void> {\n const records: JsonFileErrorRecord[] = [\n ...(await this.readRecords(fs)),\n record,\n ];\n const kept: JsonFileErrorRecord[] =\n this.maxEntries === undefined ? records : records.slice(-this.maxEntries);\n const temporaryPath: string = `${this.filePath}.tmp`;\n await fs.writeFile(\n temporaryPath,\n `${JSON.stringify(kept, null, this.indent)}\\n`\n );\n await fs.rename(temporaryPath, this.filePath);\n }\n\n /**\n * Returns the records already in the file. A missing or empty file has none.\n * A file that is not a JSON array is moved to `<path>.corrupt` (so nothing is\n * lost) and logging continues with a fresh file.\n */\n private async readRecords(\n fs: ReadWriteFileSystemLike\n ): Promise<JsonFileErrorRecord[]> {\n let content: string;\n try {\n content = await fs.readFile(this.filePath, 'utf8');\n } catch (error: unknown) {\n if (isMissingFileError(error)) {\n return [];\n }\n throw error;\n }\n if (content.trim() === '') {\n return [];\n }\n let problem: string;\n try {\n const parsed: unknown = JSON.parse(content);\n if (Array.isArray(parsed)) {\n return parsed as JsonFileErrorRecord[];\n }\n problem = 'did not hold a JSON array';\n } catch (error: unknown) {\n problem = `held invalid JSON (${describeError(error)})`;\n }\n const corruptPath: string = `${this.filePath}.corrupt`;\n await fs.rename(this.filePath, corruptPath);\n console.error(\n `${this.prefix} JsonFileErrorAdapter: \"${this.filePath}\" ${problem}, so it was moved to \"${corruptPath}\" and a new file was started.`\n );\n return [];\n }\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@richardmcquiston01/error-log-adapter",
3
- "version": "0.1.0",
4
- "description": "Dependency-free TypeScript error logging with pluggable adapters for console, webhook, and MongoDB sinks.",
3
+ "version": "0.2.0",
4
+ "description": "Dependency-free TypeScript error logging with pluggable adapters for console, webhook, MongoDB, SQLite, IndexedDB, Web Storage, and file sinks.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Richard McQuiston <richard.mcquiston01@gmail.com>",
7
7
  "homepage": "https://github.com/RichardMcQuiston01/error-log-adapter#readme",
@@ -20,6 +20,9 @@
20
20
  "error-handling",
21
21
  "webhook",
22
22
  "mongodb",
23
+ "sqlite",
24
+ "indexeddb",
25
+ "ndjson",
23
26
  "zero-dependencies"
24
27
  ],
25
28
  "type": "module",