@warlock.js/logger 4.5.0 → 4.6.1

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.
@@ -2,30 +2,49 @@ import { log } from "../logger.mjs";
2
2
 
3
3
  //#region ../@warlock.js/logger/src/utils/capture-unhandled-errors.ts
4
4
  /**
5
+ * Best-effort budget (ms) for draining async channels (file, Sentry, …) before
6
+ * the forced exit. Bounded so a single stuck channel can't hang the process —
7
+ * `process.exit()` skips `beforeExit`, so an `autoFlushOn: ["beforeExit"]`
8
+ * handler would not cover this path.
9
+ */
10
+ const FLUSH_BUDGET_BEFORE_EXIT = 1e3;
11
+ /**
5
12
  * Route Node's process-level failure events through the logger so they land in
6
- * every configured channel with full stack context. Registers one listener for
7
- * `unhandledRejection` and one for `uncaughtException`; call once at startup
8
- * after channels are configured. Pair with `autoFlushOn: ["beforeExit"]` so the
9
- * final entry survives the process exit that follows an uncaught exception.
13
+ * every configured channel with full stack context, and make an
14
+ * `uncaughtException` loud + visible instead of silently swallowed.
10
15
  *
11
- * Levels chosen for semantic honesty:
16
+ * Registers one listener for `unhandledRejection` and one for
17
+ * `uncaughtException`; call once at startup after channels are configured.
12
18
  *
13
- * - `uncaughtException` → `log.fatal` by default Node terminates the process,
14
- * so the failure is unrecoverable.
15
- * - `unhandledRejection` `log.error`a rejected promise is a failure, but
16
- * not necessarily process-ending (depends on Node's `--unhandled-rejections`
17
- * policy and your app's recovery), so it stays at error.
19
+ * - `uncaughtException` → `log.fatal`, then (by default) `process.exit(1)`.
20
+ * When no terminal channel has been configured yet — the early-boot window a
21
+ * config file throwing at import time falls into the stack is also written
22
+ * to `console.error`, standing in for the Node default this listener
23
+ * suppresses, so a fatal boot error is never invisible. A configured
24
+ * `ConsoleLog` already prints it, so the fallback is skipped when a terminal
25
+ * channel exists (no double output).
26
+ * - `unhandledRejection` → `log.error` — a rejected promise is a failure but
27
+ * not necessarily process-ending, so it stays at error and never exits.
18
28
  *
19
29
  * @example
20
30
  * log.configure({ channels: [new ConsoleLog(), new FileLog()] });
21
- * captureAnyUnhandledRejection();
31
+ * captureAnyUnhandledRejection(); // exits on uncaught
32
+ * captureAnyUnhandledRejection({ exitOnUncaughtException: false }); // dev: log only
22
33
  */
23
- function captureAnyUnhandledRejection() {
34
+ function captureAnyUnhandledRejection(options = {}) {
35
+ const { exitOnUncaughtException = true } = options;
24
36
  process.on("unhandledRejection", (reason) => {
25
37
  log.error("app", "unhandledRejection", reason);
26
38
  });
27
39
  process.on("uncaughtException", (error) => {
28
- log.fatal("app", "uncaughtException", error);
40
+ try {
41
+ log.fatal("app", "uncaughtException", error);
42
+ } catch {}
43
+ if (!log.channels.some((channel) => channel.terminal !== false)) console.error(error);
44
+ if (!exitOnUncaughtException) return;
45
+ Promise.race([log.flush(), new Promise((resolve) => {
46
+ setTimeout(resolve, FLUSH_BUDGET_BEFORE_EXIT).unref?.();
47
+ })]).then(() => process.exit(1), () => process.exit(1));
29
48
  });
30
49
  }
31
50
 
@@ -1 +1 @@
1
- {"version":3,"file":"capture-unhandled-errors.mjs","names":[],"sources":["../../../../../../../@warlock.js/logger/src/utils/capture-unhandled-errors.ts"],"sourcesContent":["import { log } from \"../logger\";\r\n\r\n/**\r\n * Route Node's process-level failure events through the logger so they land in\r\n * every configured channel with full stack context. Registers one listener for\r\n * `unhandledRejection` and one for `uncaughtException`; call once at startup\r\n * after channels are configured. Pair with `autoFlushOn: [\"beforeExit\"]` so the\r\n * final entry survives the process exit that follows an uncaught exception.\r\n *\r\n * Levels chosen for semantic honesty:\r\n *\r\n * - `uncaughtException` → `log.fatal` by default Node terminates the process,\r\n * so the failure is unrecoverable.\r\n * - `unhandledRejection` → `log.error` — a rejected promise is a failure, but\r\n * not necessarily process-ending (depends on Node's `--unhandled-rejections`\r\n * policy and your app's recovery), so it stays at error.\r\n *\r\n * @example\r\n * log.configure({ channels: [new ConsoleLog(), new FileLog()] });\r\n * captureAnyUnhandledRejection();\r\n */\r\nexport function captureAnyUnhandledRejection() {\r\n process.on(\"unhandledRejection\", (reason: any) => {\r\n log.error(\"app\", \"unhandledRejection\", reason);\r\n });\r\n\r\n process.on(\"uncaughtException\", (error) => {\r\n log.fatal(\"app\", \"uncaughtException\", error);\r\n });\r\n}\r\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AAqBA,SAAgB,+BAA+B;CAC7C,QAAQ,GAAG,uBAAuB,WAAgB;EAChD,IAAI,MAAM,OAAO,sBAAsB,MAAM;CAC/C,CAAC;CAED,QAAQ,GAAG,sBAAsB,UAAU;EACzC,IAAI,MAAM,OAAO,qBAAqB,KAAK;CAC7C,CAAC;AACH"}
1
+ {"version":3,"file":"capture-unhandled-errors.mjs","names":[],"sources":["../../../../../../../@warlock.js/logger/src/utils/capture-unhandled-errors.ts"],"sourcesContent":["import { log } from \"../logger\";\r\n\r\n/**\r\n * Options for {@link captureAnyUnhandledRejection}.\r\n */\r\nexport type CaptureUnhandledOptions = {\r\n /**\r\n * Take the process down with `process.exit(1)` after an `uncaughtException`\r\n * is logged. Defaults to `true` — an uncaught exception leaves the process\r\n * in an undefined state, so Node's own default is to exit non-zero.\r\n *\r\n * Registering an `uncaughtException` listener *suppresses* that default, so a\r\n * listener that only logs turns an unrecoverable crash into a silent\r\n * `exit 0` (which is how a config file throwing at boot looks like \"the\r\n * server just stopped\"). Keep this `true` in production so supervisors\r\n * restart and the failure is never silent. Set it to `false` where the\r\n * process is expected to recover on its own — e.g. a dev server that reloads\r\n * via HMR — to log the exception without exiting.\r\n */\r\n exitOnUncaughtException?: boolean;\r\n};\r\n\r\n/**\r\n * Best-effort budget (ms) for draining async channels (file, Sentry, …) before\r\n * the forced exit. Bounded so a single stuck channel can't hang the process —\r\n * `process.exit()` skips `beforeExit`, so an `autoFlushOn: [\"beforeExit\"]`\r\n * handler would not cover this path.\r\n */\r\nconst FLUSH_BUDGET_BEFORE_EXIT = 1000;\r\n\r\n/**\r\n * Route Node's process-level failure events through the logger so they land in\r\n * every configured channel with full stack context, and make an\r\n * `uncaughtException` loud + visible instead of silently swallowed.\r\n *\r\n * Registers one listener for `unhandledRejection` and one for\r\n * `uncaughtException`; call once at startup after channels are configured.\r\n *\r\n * - `uncaughtException` → `log.fatal`, then (by default) `process.exit(1)`.\r\n * When no terminal channel has been configured yet — the early-boot window a\r\n * config file throwing at import time falls into — the stack is also written\r\n * to `console.error`, standing in for the Node default this listener\r\n * suppresses, so a fatal boot error is never invisible. A configured\r\n * `ConsoleLog` already prints it, so the fallback is skipped when a terminal\r\n * channel exists (no double output).\r\n * - `unhandledRejection` → `log.error` — a rejected promise is a failure but\r\n * not necessarily process-ending, so it stays at error and never exits.\r\n *\r\n * @example\r\n * log.configure({ channels: [new ConsoleLog(), new FileLog()] });\r\n * captureAnyUnhandledRejection(); // exits on uncaught\r\n * captureAnyUnhandledRejection({ exitOnUncaughtException: false }); // dev: log only\r\n */\r\nexport function captureAnyUnhandledRejection(options: CaptureUnhandledOptions = {}) {\r\n const { exitOnUncaughtException = true } = options;\r\n\r\n process.on(\"unhandledRejection\", (reason: any) => {\r\n log.error(\"app\", \"unhandledRejection\", reason);\r\n });\r\n\r\n process.on(\"uncaughtException\", (error) => {\r\n // Route through the logger first so file/Sentry channels capture it. A\r\n // crash handler must never itself throw (that would re-enter Node's\r\n // uncaught path), so the logging call is guarded.\r\n try {\r\n log.fatal(\"app\", \"uncaughtException\", error);\r\n } catch {\r\n // fall through to the stderr fallback + exit below\r\n }\r\n\r\n // Guarantee terminal visibility. `log.fatal` reaches nothing the user can\r\n // see when this fires before any terminal channel is configured — exactly\r\n // the early-boot window a config file throwing at import time falls into,\r\n // which is how a fatal boot error used to masquerade as a silent\r\n // `exit 0`. Stand in for the Node default this listener suppressed, but\r\n // only when no terminal channel already printed it, so a configured\r\n // `ConsoleLog` is never doubled.\r\n const hasTerminalChannel = log.channels.some((channel) => channel.terminal !== false);\r\n\r\n if (!hasTerminalChannel) {\r\n console.error(error);\r\n }\r\n\r\n if (!exitOnUncaughtException) {\r\n return;\r\n }\r\n\r\n // Best-effort, time-bounded flush of async channels, then exit non-zero so\r\n // process supervisors restart and `warlock start` surfaces the failure.\r\n void Promise.race([\r\n log.flush(),\r\n new Promise<void>((resolve) => {\r\n const timer = setTimeout(resolve, FLUSH_BUDGET_BEFORE_EXIT);\r\n timer.unref?.();\r\n }),\r\n ]).then(\r\n () => process.exit(1),\r\n () => process.exit(1),\r\n );\r\n });\r\n}\r\n"],"mappings":";;;;;;;;;AA4BA,MAAM,2BAA2B;;;;;;;;;;;;;;;;;;;;;;;;AAyBjC,SAAgB,6BAA6B,UAAmC,CAAC,GAAG;CAClF,MAAM,EAAE,0BAA0B,SAAS;CAE3C,QAAQ,GAAG,uBAAuB,WAAgB;EAChD,IAAI,MAAM,OAAO,sBAAsB,MAAM;CAC/C,CAAC;CAED,QAAQ,GAAG,sBAAsB,UAAU;EAIzC,IAAI;GACF,IAAI,MAAM,OAAO,qBAAqB,KAAK;EAC7C,QAAQ,CAER;EAWA,IAAI,CAFuB,IAAI,SAAS,MAAM,YAAY,QAAQ,aAAa,KAEzD,GACpB,QAAQ,MAAM,KAAK;EAGrB,IAAI,CAAC,yBACH;EAKF,AAAK,QAAQ,KAAK,CAChB,IAAI,MAAM,GACV,IAAI,SAAe,YAAY;GAE7B,AADc,WAAW,SAAS,wBAC9B,CAAC,CAAC,QAAQ;EAChB,CAAC,CACH,CAAC,CAAC,CAAC,WACK,QAAQ,KAAK,CAAC,SACd,QAAQ,KAAK,CAAC,CACtB;CACF,CAAC;AACH"}