@richardmcquiston01/error-log-adapter 0.10.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;
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;
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
  }