@richardmcquiston01/error-log-adapter 0.9.0 → 0.11.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.d.cts CHANGED
@@ -466,13 +466,6 @@ declare class FlatFileErrorAdapter<TCode extends string = string> implements Err
466
466
  /** Uses the injected file system, or loads `node:fs/promises` on first use. */
467
467
  private getFileSystem;
468
468
  private prepareDirectory;
469
- /**
470
- * Moves `app.log` to `app.log.1` (shifting older files up, dropping the
471
- * oldest) when the next line would push it past `maxBytes`. A failed rotation
472
- * is reported, but the entry is still written so it is not lost.
473
- */
474
- private rotateIfNeeded;
475
- private renameIfPresent;
476
469
  }
477
470
 
478
471
  /** A stored record: the serialized entry plus its generated UUID. */
@@ -488,6 +481,13 @@ interface JsonFileErrorOptions {
488
481
  mode?: 'ndjson' | 'array';
489
482
  /** Array mode only: most entries to keep. The oldest are dropped first. */
490
483
  maxEntries?: number;
484
+ /**
485
+ * NDJSON mode only: rotate the file before it would grow past this many
486
+ * bytes, moving it to `<path>.1` (older files shift up). Off by default.
487
+ */
488
+ maxBytes?: number;
489
+ /** NDJSON mode only: rotated files to keep (`errors.ndjson.1` is newest). Defaults to `3`. Needs `maxBytes`. */
490
+ maxFiles?: number;
491
491
  /** Array mode only: spaces of indentation. Defaults to `2`; `0` is compact. */
492
492
  indent?: number;
493
493
  /** File system to write to. Defaults to `node:fs/promises`, loaded on first use. */
@@ -506,13 +506,15 @@ declare class JsonFileErrorAdapter<TCode extends string = string> implements Err
506
506
  private readonly filePath;
507
507
  private readonly mode;
508
508
  private readonly maxEntries;
509
+ private readonly maxBytes;
510
+ private readonly maxFiles;
509
511
  private readonly indent;
510
512
  private readonly createDirectory;
511
513
  private readonly prefix;
512
514
  private fs;
513
515
  private directoryReady;
514
516
  private readonly queue;
515
- /** @throws {RangeError} if `maxEntries` or `indent` is invalid, or `maxEntries` is used outside array mode. */
517
+ /** @throws {RangeError} if `maxEntries`, `maxBytes`, `maxFiles` or `indent` is invalid, or an option is used in the wrong mode. */
516
518
  constructor(filePath: string, options?: JsonFileErrorOptions);
517
519
  log(entry: ErrorLogEntry<TCode>): Promise<ErrorLogResult>;
518
520
  private write;
@@ -802,9 +804,16 @@ interface FirebaseErrorRecord {
802
804
  */
803
805
  interface FirestoreCollectionLike {
804
806
  doc(documentId: string): {
805
- set(data: FirebaseErrorRecord): Promise<unknown>;
807
+ set(data: FirestoreErrorRecord): Promise<unknown>;
806
808
  };
807
809
  }
810
+ /**
811
+ * What the Firestore adapter writes: the usual fields, plus the expiry field
812
+ * (a `Date`, which Firestore stores as a timestamp) when `expireAfterMs` is set.
813
+ */
814
+ type FirestoreErrorRecord = FirebaseErrorRecord & {
815
+ [field: string]: unknown;
816
+ };
808
817
  /**
809
818
  * The only thing the Realtime Database adapter needs from a reference:
810
819
  * `push(value)`, which resolves once the write is saved. A `firebase-admin`
@@ -843,6 +852,25 @@ interface FirebaseErrorOptions {
843
852
  /** Text printed before failure messages. Defaults to `[error-log-adapter]`. */
844
853
  prefix?: string;
845
854
  }
855
+ interface FirestoreErrorOptions extends FirebaseErrorOptions {
856
+ /**
857
+ * Add an expiry time to every document: the entry's `timestamp` plus this
858
+ * many milliseconds, stored as a Firestore timestamp in `expireField`. This
859
+ * only writes the field. Firestore deletes nothing until you turn on a TTL
860
+ * policy for that field, for example:
861
+ *
862
+ * ```sh
863
+ * gcloud firestore fields ttls update expire_at \
864
+ * --collection-group=error_log --enable-ttl
865
+ * ```
866
+ *
867
+ * Firestore then removes documents some time after they expire (typically
868
+ * within a day), and the deletes are billed like any other. Off by default.
869
+ */
870
+ expireAfterMs?: number;
871
+ /** Name of the expiry field. Defaults to `expire_at`. Only used with `expireAfterMs`. */
872
+ expireField?: string;
873
+ }
846
874
  /**
847
875
  * Writes each entry as a document in a Firestore collection. The document id is
848
876
  * the entry's UUID, so the SDK finishing one queued write twice cannot create two
@@ -855,8 +883,10 @@ declare class FirestoreErrorAdapter<TCode extends string = string> implements Er
855
883
  private readonly collection;
856
884
  private readonly prefix;
857
885
  private readonly timeoutMs;
858
- /** @throws {RangeError} if `options.timeoutMs` is not a positive number or `Infinity`. */
859
- constructor(collection: FirestoreCollectionLike, options?: FirebaseErrorOptions);
886
+ private readonly expireAfterMs;
887
+ private readonly expireField;
888
+ /** @throws {RangeError} if `options.timeoutMs` is not a positive number or `Infinity`, or the expiry options are invalid. */
889
+ constructor(collection: FirestoreCollectionLike, options?: FirestoreErrorOptions);
860
890
  log(entry: ErrorLogEntry<TCode>): Promise<ErrorLogResult>;
861
891
  }
862
892
  /**
@@ -1303,4 +1333,4 @@ declare class BatchingErrorAdapter<TCode extends string = string> implements Err
1303
1333
  private drop;
1304
1334
  }
1305
1335
 
1306
- export { type BatchCapableAdapter, BatchingErrorAdapter, type BatchingErrorOptions, type CloudWatchBatchOptions, type CloudWatchLogEvent, type CloudWatchLogsClientLike, CloudWatchLogsErrorAdapter, type CloudWatchLogsErrorOptions, type CloudWatchPutLogEventsInput, type CollectionLike, CompositeErrorAdapter, type CompositeErrorOptions, ConsoleErrorAdapter, type ConsoleErrorOptions, type EmitterLike, type ErrorLogAdapter, type ErrorLogEntry, type ErrorLogResult, EventEmitterErrorAdapter, type EventEmitterErrorOptions, type EventEmitterErrorRecord, type FileSystemLike, type FirebaseErrorOptions, type FirebaseErrorRecord, type FirestoreCollectionLike, FirestoreErrorAdapter, FlatFileErrorAdapter, type FlatFileErrorOptions, type IdbDatabaseLike, type IdbObjectStoreLike, type IdbRequestLike, type IdbTransactionLike, type IdbUpgradeDatabaseLike, IndexedDbErrorAdapter, type IndexedDbErrorOptions, type IndexedDbErrorRecord, JsonFileErrorAdapter, type JsonFileErrorOptions, type JsonFileErrorRecord, type LockManagerLike, MongoDbErrorAdapter, type MongoDbErrorOptions, MySqlErrorAdapter, PostgresErrorAdapter, type ReadWriteFileSystemLike, RealtimeDatabaseErrorAdapter, type RealtimeDatabaseRefLike, type RedisClientLike, RedisErrorAdapter, type RedisErrorOptions, type S3BatchOptions, type S3ClientLike, S3ErrorAdapter, type S3ErrorOptions, type S3PutObjectInput, type SerializedErrorLogEntry, type SqlClientLike, type SqlDialect, type SqlErrorOptions, type SqliteClientLike, SqliteErrorAdapter, type SqliteErrorOptions, type StorageLike, type SupabaseClientLike, SupabaseErrorAdapter, type SupabaseErrorLike, type SupabaseErrorOptions, type SupabaseErrorRow, type SupabaseInsertResult, type SupabaseSetupOptions, WebSocketErrorAdapter, type WebSocketErrorOptions, type WebSocketLike, WebStorageErrorAdapter, type WebStorageErrorOptions, type WebStorageErrorRecord, WebhookErrorAdapter, type WebhookErrorOptions, buildCreateErrorLogTableSql, buildErrorLogTableDdl, buildMySqlWidenColumnsSql, buildSupabaseSetupSql, createErrorLogStore, formatErrorLogLine, serializeCause, serializeEntry };
1336
+ export { type BatchCapableAdapter, BatchingErrorAdapter, type BatchingErrorOptions, type CloudWatchBatchOptions, type CloudWatchLogEvent, type CloudWatchLogsClientLike, CloudWatchLogsErrorAdapter, type CloudWatchLogsErrorOptions, type CloudWatchPutLogEventsInput, type CollectionLike, CompositeErrorAdapter, type CompositeErrorOptions, ConsoleErrorAdapter, type ConsoleErrorOptions, type EmitterLike, type ErrorLogAdapter, type ErrorLogEntry, type ErrorLogResult, EventEmitterErrorAdapter, type EventEmitterErrorOptions, type EventEmitterErrorRecord, type FileSystemLike, type FirebaseErrorOptions, type FirebaseErrorRecord, type FirestoreCollectionLike, FirestoreErrorAdapter, type FirestoreErrorOptions, type FirestoreErrorRecord, FlatFileErrorAdapter, type FlatFileErrorOptions, type IdbDatabaseLike, type IdbObjectStoreLike, type IdbRequestLike, type IdbTransactionLike, type IdbUpgradeDatabaseLike, IndexedDbErrorAdapter, type IndexedDbErrorOptions, type IndexedDbErrorRecord, JsonFileErrorAdapter, type JsonFileErrorOptions, type JsonFileErrorRecord, type LockManagerLike, MongoDbErrorAdapter, type MongoDbErrorOptions, MySqlErrorAdapter, PostgresErrorAdapter, type ReadWriteFileSystemLike, RealtimeDatabaseErrorAdapter, type RealtimeDatabaseRefLike, type RedisClientLike, RedisErrorAdapter, type RedisErrorOptions, type S3BatchOptions, type S3ClientLike, S3ErrorAdapter, type S3ErrorOptions, type S3PutObjectInput, type SerializedErrorLogEntry, type SqlClientLike, type SqlDialect, type SqlErrorOptions, type SqliteClientLike, SqliteErrorAdapter, type SqliteErrorOptions, type StorageLike, type SupabaseClientLike, SupabaseErrorAdapter, type SupabaseErrorLike, type SupabaseErrorOptions, type SupabaseErrorRow, type SupabaseInsertResult, type SupabaseSetupOptions, WebSocketErrorAdapter, type WebSocketErrorOptions, type WebSocketLike, WebStorageErrorAdapter, type WebStorageErrorOptions, type WebStorageErrorRecord, WebhookErrorAdapter, type WebhookErrorOptions, buildCreateErrorLogTableSql, buildErrorLogTableDdl, buildMySqlWidenColumnsSql, buildSupabaseSetupSql, createErrorLogStore, formatErrorLogLine, serializeCause, serializeEntry };
package/dist/index.d.ts CHANGED
@@ -466,13 +466,6 @@ declare class FlatFileErrorAdapter<TCode extends string = string> implements Err
466
466
  /** Uses the injected file system, or loads `node:fs/promises` on first use. */
467
467
  private getFileSystem;
468
468
  private prepareDirectory;
469
- /**
470
- * Moves `app.log` to `app.log.1` (shifting older files up, dropping the
471
- * oldest) when the next line would push it past `maxBytes`. A failed rotation
472
- * is reported, but the entry is still written so it is not lost.
473
- */
474
- private rotateIfNeeded;
475
- private renameIfPresent;
476
469
  }
477
470
 
478
471
  /** A stored record: the serialized entry plus its generated UUID. */
@@ -488,6 +481,13 @@ interface JsonFileErrorOptions {
488
481
  mode?: 'ndjson' | 'array';
489
482
  /** Array mode only: most entries to keep. The oldest are dropped first. */
490
483
  maxEntries?: number;
484
+ /**
485
+ * NDJSON mode only: rotate the file before it would grow past this many
486
+ * bytes, moving it to `<path>.1` (older files shift up). Off by default.
487
+ */
488
+ maxBytes?: number;
489
+ /** NDJSON mode only: rotated files to keep (`errors.ndjson.1` is newest). Defaults to `3`. Needs `maxBytes`. */
490
+ maxFiles?: number;
491
491
  /** Array mode only: spaces of indentation. Defaults to `2`; `0` is compact. */
492
492
  indent?: number;
493
493
  /** File system to write to. Defaults to `node:fs/promises`, loaded on first use. */
@@ -506,13 +506,15 @@ declare class JsonFileErrorAdapter<TCode extends string = string> implements Err
506
506
  private readonly filePath;
507
507
  private readonly mode;
508
508
  private readonly maxEntries;
509
+ private readonly maxBytes;
510
+ private readonly maxFiles;
509
511
  private readonly indent;
510
512
  private readonly createDirectory;
511
513
  private readonly prefix;
512
514
  private fs;
513
515
  private directoryReady;
514
516
  private readonly queue;
515
- /** @throws {RangeError} if `maxEntries` or `indent` is invalid, or `maxEntries` is used outside array mode. */
517
+ /** @throws {RangeError} if `maxEntries`, `maxBytes`, `maxFiles` or `indent` is invalid, or an option is used in the wrong mode. */
516
518
  constructor(filePath: string, options?: JsonFileErrorOptions);
517
519
  log(entry: ErrorLogEntry<TCode>): Promise<ErrorLogResult>;
518
520
  private write;
@@ -802,9 +804,16 @@ interface FirebaseErrorRecord {
802
804
  */
803
805
  interface FirestoreCollectionLike {
804
806
  doc(documentId: string): {
805
- set(data: FirebaseErrorRecord): Promise<unknown>;
807
+ set(data: FirestoreErrorRecord): Promise<unknown>;
806
808
  };
807
809
  }
810
+ /**
811
+ * What the Firestore adapter writes: the usual fields, plus the expiry field
812
+ * (a `Date`, which Firestore stores as a timestamp) when `expireAfterMs` is set.
813
+ */
814
+ type FirestoreErrorRecord = FirebaseErrorRecord & {
815
+ [field: string]: unknown;
816
+ };
808
817
  /**
809
818
  * The only thing the Realtime Database adapter needs from a reference:
810
819
  * `push(value)`, which resolves once the write is saved. A `firebase-admin`
@@ -843,6 +852,25 @@ interface FirebaseErrorOptions {
843
852
  /** Text printed before failure messages. Defaults to `[error-log-adapter]`. */
844
853
  prefix?: string;
845
854
  }
855
+ interface FirestoreErrorOptions extends FirebaseErrorOptions {
856
+ /**
857
+ * Add an expiry time to every document: the entry's `timestamp` plus this
858
+ * many milliseconds, stored as a Firestore timestamp in `expireField`. This
859
+ * only writes the field. Firestore deletes nothing until you turn on a TTL
860
+ * policy for that field, for example:
861
+ *
862
+ * ```sh
863
+ * gcloud firestore fields ttls update expire_at \
864
+ * --collection-group=error_log --enable-ttl
865
+ * ```
866
+ *
867
+ * Firestore then removes documents some time after they expire (typically
868
+ * within a day), and the deletes are billed like any other. Off by default.
869
+ */
870
+ expireAfterMs?: number;
871
+ /** Name of the expiry field. Defaults to `expire_at`. Only used with `expireAfterMs`. */
872
+ expireField?: string;
873
+ }
846
874
  /**
847
875
  * Writes each entry as a document in a Firestore collection. The document id is
848
876
  * the entry's UUID, so the SDK finishing one queued write twice cannot create two
@@ -855,8 +883,10 @@ declare class FirestoreErrorAdapter<TCode extends string = string> implements Er
855
883
  private readonly collection;
856
884
  private readonly prefix;
857
885
  private readonly timeoutMs;
858
- /** @throws {RangeError} if `options.timeoutMs` is not a positive number or `Infinity`. */
859
- constructor(collection: FirestoreCollectionLike, options?: FirebaseErrorOptions);
886
+ private readonly expireAfterMs;
887
+ private readonly expireField;
888
+ /** @throws {RangeError} if `options.timeoutMs` is not a positive number or `Infinity`, or the expiry options are invalid. */
889
+ constructor(collection: FirestoreCollectionLike, options?: FirestoreErrorOptions);
860
890
  log(entry: ErrorLogEntry<TCode>): Promise<ErrorLogResult>;
861
891
  }
862
892
  /**
@@ -1303,4 +1333,4 @@ declare class BatchingErrorAdapter<TCode extends string = string> implements Err
1303
1333
  private drop;
1304
1334
  }
1305
1335
 
1306
- export { type BatchCapableAdapter, BatchingErrorAdapter, type BatchingErrorOptions, type CloudWatchBatchOptions, type CloudWatchLogEvent, type CloudWatchLogsClientLike, CloudWatchLogsErrorAdapter, type CloudWatchLogsErrorOptions, type CloudWatchPutLogEventsInput, type CollectionLike, CompositeErrorAdapter, type CompositeErrorOptions, ConsoleErrorAdapter, type ConsoleErrorOptions, type EmitterLike, type ErrorLogAdapter, type ErrorLogEntry, type ErrorLogResult, EventEmitterErrorAdapter, type EventEmitterErrorOptions, type EventEmitterErrorRecord, type FileSystemLike, type FirebaseErrorOptions, type FirebaseErrorRecord, type FirestoreCollectionLike, FirestoreErrorAdapter, FlatFileErrorAdapter, type FlatFileErrorOptions, type IdbDatabaseLike, type IdbObjectStoreLike, type IdbRequestLike, type IdbTransactionLike, type IdbUpgradeDatabaseLike, IndexedDbErrorAdapter, type IndexedDbErrorOptions, type IndexedDbErrorRecord, JsonFileErrorAdapter, type JsonFileErrorOptions, type JsonFileErrorRecord, type LockManagerLike, MongoDbErrorAdapter, type MongoDbErrorOptions, MySqlErrorAdapter, PostgresErrorAdapter, type ReadWriteFileSystemLike, RealtimeDatabaseErrorAdapter, type RealtimeDatabaseRefLike, type RedisClientLike, RedisErrorAdapter, type RedisErrorOptions, type S3BatchOptions, type S3ClientLike, S3ErrorAdapter, type S3ErrorOptions, type S3PutObjectInput, type SerializedErrorLogEntry, type SqlClientLike, type SqlDialect, type SqlErrorOptions, type SqliteClientLike, SqliteErrorAdapter, type SqliteErrorOptions, type StorageLike, type SupabaseClientLike, SupabaseErrorAdapter, type SupabaseErrorLike, type SupabaseErrorOptions, type SupabaseErrorRow, type SupabaseInsertResult, type SupabaseSetupOptions, WebSocketErrorAdapter, type WebSocketErrorOptions, type WebSocketLike, WebStorageErrorAdapter, type WebStorageErrorOptions, type WebStorageErrorRecord, WebhookErrorAdapter, type WebhookErrorOptions, buildCreateErrorLogTableSql, buildErrorLogTableDdl, buildMySqlWidenColumnsSql, buildSupabaseSetupSql, createErrorLogStore, formatErrorLogLine, serializeCause, serializeEntry };
1336
+ export { type BatchCapableAdapter, BatchingErrorAdapter, type BatchingErrorOptions, type CloudWatchBatchOptions, type CloudWatchLogEvent, type CloudWatchLogsClientLike, CloudWatchLogsErrorAdapter, type CloudWatchLogsErrorOptions, type CloudWatchPutLogEventsInput, type CollectionLike, CompositeErrorAdapter, type CompositeErrorOptions, ConsoleErrorAdapter, type ConsoleErrorOptions, type EmitterLike, type ErrorLogAdapter, type ErrorLogEntry, type ErrorLogResult, EventEmitterErrorAdapter, type EventEmitterErrorOptions, type EventEmitterErrorRecord, type FileSystemLike, type FirebaseErrorOptions, type FirebaseErrorRecord, type FirestoreCollectionLike, FirestoreErrorAdapter, type FirestoreErrorOptions, type FirestoreErrorRecord, FlatFileErrorAdapter, type FlatFileErrorOptions, type IdbDatabaseLike, type IdbObjectStoreLike, type IdbRequestLike, type IdbTransactionLike, type IdbUpgradeDatabaseLike, IndexedDbErrorAdapter, type IndexedDbErrorOptions, type IndexedDbErrorRecord, JsonFileErrorAdapter, type JsonFileErrorOptions, type JsonFileErrorRecord, type LockManagerLike, MongoDbErrorAdapter, type MongoDbErrorOptions, MySqlErrorAdapter, PostgresErrorAdapter, type ReadWriteFileSystemLike, RealtimeDatabaseErrorAdapter, type RealtimeDatabaseRefLike, type RedisClientLike, RedisErrorAdapter, type RedisErrorOptions, type S3BatchOptions, type S3ClientLike, S3ErrorAdapter, type S3ErrorOptions, type S3PutObjectInput, type SerializedErrorLogEntry, type SqlClientLike, type SqlDialect, type SqlErrorOptions, type SqliteClientLike, SqliteErrorAdapter, type SqliteErrorOptions, type StorageLike, type SupabaseClientLike, SupabaseErrorAdapter, type SupabaseErrorLike, type SupabaseErrorOptions, type SupabaseErrorRow, type SupabaseInsertResult, type SupabaseSetupOptions, WebSocketErrorAdapter, type WebSocketErrorOptions, type WebSocketLike, WebStorageErrorAdapter, type WebStorageErrorOptions, type WebStorageErrorRecord, WebhookErrorAdapter, type WebhookErrorOptions, buildCreateErrorLogTableSql, buildErrorLogTableDdl, buildMySqlWidenColumnsSql, buildSupabaseSetupSql, createErrorLogStore, formatErrorLogLine, serializeCause, serializeEntry };
package/dist/index.mjs CHANGED
@@ -920,8 +920,55 @@ function isMissingFileError(error) {
920
920
  return typeof error === "object" && error !== null && error.code === "ENOENT";
921
921
  }
922
922
 
923
- // src/adapters/flat-file-error-adapter.ts
923
+ // src/file-rotation.ts
924
924
  var DEFAULT_MAX_FILES = 3;
925
+ function assertPositiveInteger(adapterName, value, name) {
926
+ if (!Number.isInteger(value) || value < 1) {
927
+ throw new RangeError(
928
+ `${adapterName}: ${name} must be a positive integer, received ${String(value)}.`
929
+ );
930
+ }
931
+ }
932
+ async function renameIfPresent(fs, from, to) {
933
+ try {
934
+ await fs.rename(from, to);
935
+ } catch (error) {
936
+ if (!isMissingFileError(error)) {
937
+ throw error;
938
+ }
939
+ }
940
+ }
941
+ async function rotateIfNeeded(fs, filePath, writeBytes, settings, report) {
942
+ try {
943
+ let currentSize;
944
+ try {
945
+ currentSize = (await fs.stat(filePath)).size;
946
+ } catch (error) {
947
+ if (isMissingFileError(error)) {
948
+ return;
949
+ }
950
+ throw error;
951
+ }
952
+ if (currentSize === 0 || currentSize + writeBytes <= settings.maxBytes) {
953
+ return;
954
+ }
955
+ await fs.rm(`${filePath}.${settings.maxFiles}`, { force: true });
956
+ for (let index = settings.maxFiles - 1; index >= 1; index -= 1) {
957
+ await renameIfPresent(
958
+ fs,
959
+ `${filePath}.${index}`,
960
+ `${filePath}.${index + 1}`
961
+ );
962
+ }
963
+ await fs.rename(filePath, `${filePath}.1`);
964
+ } catch (error) {
965
+ console.error(
966
+ `${report.prefix} ${report.adapterName}: could not rotate "${filePath}", so it will keep growing: ${describeError(error)}`
967
+ );
968
+ }
969
+ }
970
+
971
+ // src/adapters/flat-file-error-adapter.ts
925
972
  function escapeLineBreaks(text) {
926
973
  return text.replace(/\r?\n/g, "\\n");
927
974
  }
@@ -948,13 +995,6 @@ function formatErrorLogLine(entry) {
948
995
  `${entry.timestamp} [${entry.code}] ${source}${entry.message}${cause}`
949
996
  );
950
997
  }
951
- function assertPositiveInteger(value, name) {
952
- if (!Number.isInteger(value) || value < 1) {
953
- throw new RangeError(
954
- `FlatFileErrorAdapter: ${name} must be a positive integer, received ${String(value)}.`
955
- );
956
- }
957
- }
958
998
  var FlatFileErrorAdapter = class {
959
999
  /** @throws {RangeError} if `maxBytes` or `maxFiles` is not a positive integer. */
960
1000
  constructor(filePath, options = {}) {
@@ -968,9 +1008,9 @@ var FlatFileErrorAdapter = class {
968
1008
  this.createDirectory = options.createDirectory ?? true;
969
1009
  this.prefix = options.prefix ?? DEFAULT_PREFIX;
970
1010
  if (this.maxBytes !== void 0) {
971
- assertPositiveInteger(this.maxBytes, "maxBytes");
1011
+ assertPositiveInteger("FlatFileErrorAdapter", this.maxBytes, "maxBytes");
972
1012
  }
973
- assertPositiveInteger(this.maxFiles, "maxFiles");
1013
+ assertPositiveInteger("FlatFileErrorAdapter", this.maxFiles, "maxFiles");
974
1014
  }
975
1015
  log(entry) {
976
1016
  return this.queue.run(() => this.write(entry));
@@ -982,7 +1022,15 @@ var FlatFileErrorAdapter = class {
982
1022
  const line = formatted.endsWith("\n") ? formatted : `${formatted}
983
1023
  `;
984
1024
  await this.prepareDirectory(fs);
985
- await this.rotateIfNeeded(fs, new TextEncoder().encode(line).length);
1025
+ if (this.maxBytes !== void 0) {
1026
+ await rotateIfNeeded(
1027
+ fs,
1028
+ this.filePath,
1029
+ new TextEncoder().encode(line).length,
1030
+ { maxBytes: this.maxBytes, maxFiles: this.maxFiles },
1031
+ { adapterName: "FlatFileErrorAdapter", prefix: this.prefix }
1032
+ );
1033
+ }
986
1034
  await fs.appendFile(this.filePath, line);
987
1035
  return { ok: true };
988
1036
  } catch (error) {
@@ -1011,64 +1059,20 @@ var FlatFileErrorAdapter = class {
1011
1059
  }
1012
1060
  this.directoryReady = true;
1013
1061
  }
1014
- /**
1015
- * Moves `app.log` to `app.log.1` (shifting older files up, dropping the
1016
- * oldest) when the next line would push it past `maxBytes`. A failed rotation
1017
- * is reported, but the entry is still written so it is not lost.
1018
- */
1019
- async rotateIfNeeded(fs, lineBytes) {
1020
- if (this.maxBytes === void 0) {
1021
- return;
1022
- }
1023
- try {
1024
- let currentSize;
1025
- try {
1026
- currentSize = (await fs.stat(this.filePath)).size;
1027
- } catch (error) {
1028
- if (isMissingFileError(error)) {
1029
- return;
1030
- }
1031
- throw error;
1032
- }
1033
- if (currentSize === 0 || currentSize + lineBytes <= this.maxBytes) {
1034
- return;
1035
- }
1036
- await fs.rm(`${this.filePath}.${this.maxFiles}`, { force: true });
1037
- for (let index = this.maxFiles - 1; index >= 1; index -= 1) {
1038
- await this.renameIfPresent(
1039
- fs,
1040
- `${this.filePath}.${index}`,
1041
- `${this.filePath}.${index + 1}`
1042
- );
1043
- }
1044
- await fs.rename(this.filePath, `${this.filePath}.1`);
1045
- } catch (error) {
1046
- console.error(
1047
- `${this.prefix} FlatFileErrorAdapter: could not rotate "${this.filePath}", so it will keep growing: ${describeError(error)}`
1048
- );
1049
- }
1050
- }
1051
- async renameIfPresent(fs, from, to) {
1052
- try {
1053
- await fs.rename(from, to);
1054
- } catch (error) {
1055
- if (!isMissingFileError(error)) {
1056
- throw error;
1057
- }
1058
- }
1059
- }
1060
1062
  };
1061
1063
 
1062
1064
  // src/adapters/json-file-error-adapter.ts
1063
1065
  var DEFAULT_INDENT = 2;
1064
1066
  var JsonFileErrorAdapter = class {
1065
- /** @throws {RangeError} if `maxEntries` or `indent` is invalid, or `maxEntries` is used outside array mode. */
1067
+ /** @throws {RangeError} if `maxEntries`, `maxBytes`, `maxFiles` or `indent` is invalid, or an option is used in the wrong mode. */
1066
1068
  constructor(filePath, options = {}) {
1067
1069
  this.filePath = filePath;
1068
1070
  this.directoryReady = false;
1069
1071
  this.queue = new SerialQueue();
1070
1072
  this.mode = options.mode ?? "ndjson";
1071
1073
  this.maxEntries = options.maxEntries;
1074
+ this.maxBytes = options.maxBytes;
1075
+ this.maxFiles = options.maxFiles ?? DEFAULT_MAX_FILES;
1072
1076
  this.indent = options.indent ?? DEFAULT_INDENT;
1073
1077
  this.fs = options.fs;
1074
1078
  this.createDirectory = options.createDirectory ?? true;
@@ -1085,6 +1089,20 @@ var JsonFileErrorAdapter = class {
1085
1089
  );
1086
1090
  }
1087
1091
  }
1092
+ if (this.maxBytes !== void 0 || options.maxFiles !== void 0) {
1093
+ if (this.mode !== "ndjson") {
1094
+ throw new RangeError(
1095
+ `JsonFileErrorAdapter: maxBytes and maxFiles only apply to mode "ndjson", but mode is "${this.mode}". Use mode "ndjson", or use maxEntries to limit array mode.`
1096
+ );
1097
+ }
1098
+ if (this.maxBytes === void 0) {
1099
+ throw new RangeError(
1100
+ "JsonFileErrorAdapter: maxFiles needs maxBytes; set maxBytes to turn rotation on."
1101
+ );
1102
+ }
1103
+ assertPositiveInteger("JsonFileErrorAdapter", this.maxBytes, "maxBytes");
1104
+ assertPositiveInteger("JsonFileErrorAdapter", this.maxFiles, "maxFiles");
1105
+ }
1088
1106
  if (!Number.isInteger(this.indent) || this.indent < 0) {
1089
1107
  throw new RangeError(
1090
1108
  `JsonFileErrorAdapter: indent must be a non-negative integer, received ${String(this.indent)}.`
@@ -1103,8 +1121,18 @@ var JsonFileErrorAdapter = class {
1103
1121
  const fs = await this.getFileSystem();
1104
1122
  await this.prepareDirectory(fs);
1105
1123
  if (this.mode === "ndjson") {
1106
- await fs.appendFile(this.filePath, `${JSON.stringify(record)}
1107
- `);
1124
+ const line = `${JSON.stringify(record)}
1125
+ `;
1126
+ if (this.maxBytes !== void 0) {
1127
+ await rotateIfNeeded(
1128
+ fs,
1129
+ this.filePath,
1130
+ new TextEncoder().encode(line).length,
1131
+ { maxBytes: this.maxBytes, maxFiles: this.maxFiles },
1132
+ { adapterName: "JsonFileErrorAdapter", prefix: this.prefix }
1133
+ );
1134
+ }
1135
+ await fs.appendFile(this.filePath, line);
1108
1136
  } else {
1109
1137
  await this.rewriteArray(fs, record);
1110
1138
  }
@@ -1574,6 +1602,15 @@ var S3ErrorAdapter = class {
1574
1602
  };
1575
1603
 
1576
1604
  // src/adapters/firebase-error-adapter.ts
1605
+ var DEFAULT_EXPIRE_FIELD = "expire_at";
1606
+ var RESERVED_FIELDS = /* @__PURE__ */ new Set([
1607
+ "id",
1608
+ "code",
1609
+ "message",
1610
+ "timestamp",
1611
+ "source",
1612
+ "cause"
1613
+ ]);
1577
1614
  function waitForWrite(start, timeoutMs) {
1578
1615
  return withTimeout(
1579
1616
  start,
@@ -1598,7 +1635,7 @@ function buildRecord(entry) {
1598
1635
  return record;
1599
1636
  }
1600
1637
  var FirestoreErrorAdapter = class {
1601
- /** @throws {RangeError} if `options.timeoutMs` is not a positive number or `Infinity`. */
1638
+ /** @throws {RangeError} if `options.timeoutMs` is not a positive number or `Infinity`, or the expiry options are invalid. */
1602
1639
  constructor(collection, options = {}) {
1603
1640
  this.collection = collection;
1604
1641
  this.prefix = options.prefix ?? DEFAULT_PREFIX;
@@ -1606,10 +1643,31 @@ var FirestoreErrorAdapter = class {
1606
1643
  options.timeoutMs,
1607
1644
  "FirestoreErrorAdapter"
1608
1645
  );
1646
+ this.expireAfterMs = options.expireAfterMs;
1647
+ this.expireField = options.expireField ?? DEFAULT_EXPIRE_FIELD;
1648
+ if (this.expireAfterMs !== void 0 && (!Number.isFinite(this.expireAfterMs) || this.expireAfterMs <= 0)) {
1649
+ throw new RangeError(
1650
+ `FirestoreErrorAdapter: expireAfterMs must be a positive number of milliseconds, received ${String(options.expireAfterMs)}.`
1651
+ );
1652
+ }
1653
+ if (this.expireField === "" || /[./]/.test(this.expireField) || RESERVED_FIELDS.has(this.expireField)) {
1654
+ throw new RangeError(
1655
+ `FirestoreErrorAdapter: expireField must be a name other than ${[...RESERVED_FIELDS].join(", ")}, without "." or "/", received ${JSON.stringify(this.expireField)}.`
1656
+ );
1657
+ }
1609
1658
  }
1610
1659
  async log(entry) {
1611
1660
  try {
1612
- const record = buildRecord(entry);
1661
+ const record = { ...buildRecord(entry) };
1662
+ if (this.expireAfterMs !== void 0) {
1663
+ const occurredAt = Date.parse(record.timestamp);
1664
+ if (Number.isNaN(occurredAt)) {
1665
+ throw new RangeError(
1666
+ `timestamp "${record.timestamp}" is not a valid ISO 8601 date, so the expiry time cannot be worked out`
1667
+ );
1668
+ }
1669
+ record[this.expireField] = new Date(occurredAt + this.expireAfterMs);
1670
+ }
1613
1671
  await waitForWrite(
1614
1672
  () => this.collection.doc(record.id).set(record),
1615
1673
  this.timeoutMs