@ix-xs/node-comfort 1.2.0 → 2.0.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.
Files changed (75) hide show
  1. package/CHANGELOG.md +190 -0
  2. package/README.md +136 -549
  3. package/index.js +80 -35
  4. package/package.json +160 -33
  5. package/src/Arr.js +887 -419
  6. package/src/Async.js +585 -0
  7. package/src/Cache.js +329 -0
  8. package/src/Checker.js +971 -539
  9. package/src/Cli.js +1034 -0
  10. package/src/Color.js +415 -0
  11. package/src/Crypto.js +707 -0
  12. package/src/Emitter.js +289 -0
  13. package/src/Env.js +555 -0
  14. package/src/FS.js +1483 -894
  15. package/src/Func.js +670 -353
  16. package/src/Http.js +538 -0
  17. package/src/Id.js +384 -201
  18. package/src/Logger.js +739 -378
  19. package/src/Num.js +844 -361
  20. package/src/Obj.js +855 -372
  21. package/src/SQLite.js +1308 -1005
  22. package/src/Schema.js +1586 -0
  23. package/src/Str.js +983 -436
  24. package/src/Sys.js +511 -0
  25. package/src/Time.js +1260 -422
  26. package/src/Utils.js +321 -325
  27. package/src/errors.js +292 -0
  28. package/types/index.d.ts +693 -526
  29. package/types/src/Arr.d.ts +455 -145
  30. package/types/src/Arr.d.ts.map +1 -1
  31. package/types/src/Async.d.ts +321 -0
  32. package/types/src/Async.d.ts.map +1 -0
  33. package/types/src/Cache.d.ts +195 -0
  34. package/types/src/Cache.d.ts.map +1 -0
  35. package/types/src/Checker.d.ts +501 -235
  36. package/types/src/Checker.d.ts.map +1 -1
  37. package/types/src/Cli.d.ts +582 -0
  38. package/types/src/Cli.d.ts.map +1 -0
  39. package/types/src/Color.d.ts +294 -0
  40. package/types/src/Color.d.ts.map +1 -0
  41. package/types/src/Crypto.d.ts +534 -0
  42. package/types/src/Crypto.d.ts.map +1 -0
  43. package/types/src/Emitter.d.ts +170 -0
  44. package/types/src/Emitter.d.ts.map +1 -0
  45. package/types/src/Env.d.ts +382 -0
  46. package/types/src/Env.d.ts.map +1 -0
  47. package/types/src/FS.d.ts +686 -238
  48. package/types/src/FS.d.ts.map +1 -1
  49. package/types/src/Func.d.ts +380 -135
  50. package/types/src/Func.d.ts.map +1 -1
  51. package/types/src/Http.d.ts +374 -0
  52. package/types/src/Http.d.ts.map +1 -0
  53. package/types/src/Id.d.ts +149 -68
  54. package/types/src/Id.d.ts.map +1 -1
  55. package/types/src/Logger.d.ts +363 -46
  56. package/types/src/Logger.d.ts.map +1 -1
  57. package/types/src/Num.d.ts +473 -137
  58. package/types/src/Num.d.ts.map +1 -1
  59. package/types/src/Obj.d.ts +390 -104
  60. package/types/src/Obj.d.ts.map +1 -1
  61. package/types/src/SQLite.d.ts +646 -309
  62. package/types/src/SQLite.d.ts.map +1 -1
  63. package/types/src/Schema.d.ts +940 -0
  64. package/types/src/Schema.d.ts.map +1 -0
  65. package/types/src/Str.d.ts +569 -166
  66. package/types/src/Str.d.ts.map +1 -1
  67. package/types/src/Sys.d.ts +269 -0
  68. package/types/src/Sys.d.ts.map +1 -0
  69. package/types/src/Time.d.ts +663 -204
  70. package/types/src/Time.d.ts.map +1 -1
  71. package/types/src/Utils.d.ts +154 -102
  72. package/types/src/Utils.d.ts.map +1 -1
  73. package/types/src/errors.d.ts +292 -0
  74. package/types/src/errors.d.ts.map +1 -0
  75. package/types/index.d.ts.map +0 -1
package/src/Utils.js CHANGED
@@ -1,325 +1,321 @@
1
- const logger = require("./Logger");
2
-
3
- /**
4
- * Miscellaneous async and process helpers.
5
- */
6
- module.exports = {
7
- /**
8
- * Waits for a given number of milliseconds.
9
- *
10
- * @example
11
- * await nodeComfort.wait(500); // waits 500 ms
12
- *
13
- * @param {number} ms - Delay in milliseconds.
14
- * @returns {Promise<void>} A promise that resolves after the delay.
15
- */
16
- wait(ms) {
17
- return new Promise((resolve) => setTimeout(resolve, ms));
18
- },
19
-
20
- /**
21
- * Polls a predicate at a given interval and emits events when it matches or times out.
22
- *
23
- * The predicate is evaluated every `options.interval` milliseconds (default: 50 ms).
24
- * Be careful with the default interval: 50 ms is very frequent polling and can be
25
- * unnecessarily CPU‑intensive if your predicate or trigger handlers are heavy.
26
- * When `options.max` is `null`, the predicate can match indefinitely (infinite triggers),
27
- * so you should choose an interval that makes sense for your use‑case.
28
- *
29
- * A `when(...)` task is one‑shot: once it has been stopped (manually or via `max`/`timeout`),
30
- * calling `.start()` again on the same task will not restart it.
31
- *
32
- * @param {boolean|Promise<boolean>|(() => boolean|Promise<boolean>)} predicate
33
- * Condition to evaluate at each tick, or a function returning it.
34
- * @param {*} [payload={}]
35
- * Value passed to listeners when events are emitted (e.g. an object or message).
36
- * @param {object} [options]
37
- * @param {number} [options.interval=50]
38
- * Polling interval in milliseconds. Increase this value if your predicate is
39
- * expensive or you expect many triggers without a `max` limit.
40
- * @param {number|null} [options.timeout=null]
41
- * Maximum time in milliseconds before emitting a "timeout" event and stopping.
42
- * If `null`, no timeout is applied.
43
- * @param {number|null} [options.max=null]
44
- * Maximum number of successful predicate evaluations ("trigger" events) before
45
- * automatically stopping. If `null`, triggers are infinite until `stop()` is called.
46
- *
47
- * @example
48
- * // Basic usage: infinite triggers every 50 ms (default interval) – use with caution.
49
- * const task = nodeComfort.when(() => true, { message: "Ok" });
50
- *
51
- * task
52
- * .on("trigger", (payload) => {
53
- * console.log(payload.message);
54
- * // task.stop(); // stop when you’re done
55
- * })
56
- * .start();
57
- *
58
- * @example
59
- * // Safer usage: limit triggers and/or lower polling frequency.
60
- * nodeComfort.when(() => true, { message: "Ok" }, { max: 1, interval: 500 })
61
- * .on("trigger", (payload) => {
62
- * console.log(payload.message);
63
- * })
64
- * .on("timeout", (payload) => {
65
- * console.log("Timed out", payload);
66
- * })
67
- * .start();
68
- *
69
- * @returns {{
70
- * start: () => any,
71
- * stop: () => any,
72
- * on: <E extends "error"|"trigger"|"timeout">(event: E, handler: E extends "error"
73
- * ? (error: unknown) => void|Promise<void>
74
- * : (payload: any) => void|Promise<void>) => any
75
- * }} A small task controller with `start`, `stop` and `on`.
76
- */
77
- when(predicate, payload = {}, options) {
78
- const interval = options?.interval ?? 50;
79
- const timeout = options?.timeout ?? null;
80
- const max = options?.max ?? null;
81
-
82
- /** @type {{ trigger: Set<Function>, error: Set<Function>, timeout: Set<Function> }} */
83
- const listeners = {
84
- trigger: new Set(),
85
- error: new Set(),
86
- timeout: new Set(),
87
- };
88
-
89
- let _timer = null;
90
- let _timeoutTimer = null;
91
- let _stopped = false;
92
- let _count = 0;
93
-
94
- /**
95
- * Emits an event to all registered listeners.
96
- * Errors in listeners are caught and ignored to avoid breaking the polling loop.
97
- *
98
- * @param {"trigger"|"error"|"timeout"} event
99
- * @param {*} p
100
- */
101
- function emit(event, p) {
102
- for (const fn of listeners[event]) {
103
- try {
104
- fn(p);
105
- } catch {
106
- // Best effort: we don't re-throw here to keep the task alive.
107
- }
108
- }
109
- }
110
-
111
- async function evalPredicate() {
112
- if (typeof predicate === "function") {
113
- return await predicate();
114
- }
115
- return Boolean(await predicate);
116
- }
117
-
118
- async function tick() {
119
- if (_stopped) return;
120
-
121
- try {
122
- const ok = await evalPredicate();
123
-
124
- if (ok) {
125
- _count++;
126
- emit("trigger", payload);
127
-
128
- if (max != null && _count >= max) {
129
- _stopInternal();
130
- return;
131
- }
132
- }
133
-
134
- if (!_stopped) {
135
- _timer = setTimeout(tick, interval);
136
- }
137
- } catch (error) {
138
- emit("error", error);
139
- if (!_stopped) {
140
- _timer = setTimeout(tick, interval);
141
- }
142
- }
143
- }
144
-
145
- function _stopInternal() {
146
- _stopped = true;
147
- if (_timer) {
148
- clearTimeout(_timer);
149
- _timer = null;
150
- }
151
- if (_timeoutTimer) {
152
- clearTimeout(_timeoutTimer);
153
- _timeoutTimer = null;
154
- }
155
- }
156
-
157
- const controller = {
158
- /**
159
- * Starts the polling task (one-shot: no effect if it was already stopped).
160
- * @returns {typeof controller}
161
- */
162
- start() {
163
- if (_stopped) return controller;
164
-
165
- if (!_timer) {
166
- _timer = setTimeout(tick, 0);
167
- }
168
-
169
- if (timeout != null && !_timeoutTimer) {
170
- _timeoutTimer = setTimeout(() => {
171
- if (_stopped) return;
172
- emit("timeout", payload);
173
- _stopInternal();
174
- }, timeout);
175
- }
176
-
177
- return controller;
178
- },
179
- /**
180
- * Stops the polling task and clears all timers.
181
- * @returns {typeof controller}
182
- */
183
- stop() {
184
- _stopInternal();
185
- return controller;
186
- },
187
- /**
188
- * Registers an event handler.
189
- *
190
- * @typedef { "error" | "trigger" | "timeout" } EventNames
191
- * @typedef {{
192
- * error: (error: unknown) => void|Promise<void>,
193
- * trigger: (payload: any) => void|Promise<void>,
194
- * timeout: (payload: any) => void|Promise<void>,
195
- * }} EventHandlers
196
- *
197
- * @template {EventNames} E
198
- * @param {E} event - Event name.
199
- * @param {EventHandlers[E]} handler - Handler function.
200
- * @returns {typeof controller}
201
- */
202
- on(event, handler) {
203
- listeners[event]?.add(handler);
204
- return controller;
205
- },
206
- };
207
-
208
- return controller;
209
- },
210
-
211
- /**
212
- * Installs process-level safety nets for errors, exits and signals.
213
- *
214
- * By default, it:
215
- * - logs uncaught exceptions and unhandled rejections,
216
- * - logs process exit codes,
217
- * - logs SIGINT/SIGTERM/SIGQUIT and exits with code 0.
218
- *
219
- * You can override each handler with `.on(event, handler)`.
220
- *
221
- * @example
222
- * // Use defaults:
223
- * nodeComfort.dontCrash();
224
- *
225
- * @example
226
- * // Custom error handler:
227
- * nodeComfort.dontCrash().on("error", (err) => {
228
- * console.error("Global error:", err);
229
- * process.exit(1);
230
- * });
231
- *
232
- * @returns {{
233
- * on: <E extends "error"|"exit"|"sig"|"beforeExit">(event: E, handler?: (
234
- * E extends "error" ? (error: Error|unknown) => void|Promise<void> :
235
- * E extends "exit" ? (code: number) => void|Promise<void> :
236
- * E extends "sig" ? (signal: "SIGINT"|"SIGTERM"|"SIGQUIT") => void|Promise<void> :
237
- * (code: number) => void|Promise<void>
238
- * )) => any
239
- * }} A small controller to configure global handlers.
240
- */
241
- dontCrash() {
242
- const defaults = {
243
- error: (error) => {
244
- logger.log(
245
- `<% red [@ix-xs/node-comfort] Process Error %>\n<% gray ${error.stack ?? error} %>`,
246
- );
247
- },
248
- exit: (code) => {
249
- logger.log(
250
- `<% yellow [@ix-xs/node-comfort] Process Exit %>\n→ Code: <% gray ${code} %>`,
251
- );
252
- },
253
- sig: (signal) => {
254
- logger.log(
255
- `<% yellow [@ix-xs/node-comfort] Process SIG* %>\n→ Signal: <% gray ${signal} %>`,
256
- );
257
- process.exit(0);
258
- },
259
- beforeExit: (code) => {},
260
- };
261
-
262
- const controller = {
263
- /**
264
- * Registers global process handlers.
265
- *
266
- * @template { "error" | "exit" | "sig" | "beforeExit" } E
267
- * @param {E} event - Event name.
268
- * @param {{
269
- * error: (error: Error | unknown) => void | Promise<void>,
270
- * exit: (code: number) => void | Promise<void>,
271
- * sig: (signal: "SIGINT" | "SIGTERM" | "SIGQUIT") => void | Promise<void>,
272
- * beforeExit: (code: number) => void|Promise<void>
273
- * }[E]} [handler] - Custom handler, or use the default when omitted.
274
- * @returns {typeof controller}
275
- */
276
- on(event, handler) {
277
- if (event === "error") {
278
- process.removeAllListeners("uncaughtException");
279
- process.removeAllListeners("unhandledRejection");
280
- const fn = handler ?? defaults.error;
281
- process.on("uncaughtException", fn);
282
- process.on("unhandledRejection", fn);
283
- } else if (event === "exit") {
284
- process.removeAllListeners("exit");
285
- process.on("exit", handler ?? defaults.exit);
286
- } else if (event === "sig") {
287
- for (const signal of ["SIGINT", "SIGTERM", "SIGQUIT"]) {
288
- process.removeAllListeners(signal);
289
- process.on(signal, handler ?? defaults.sig);
290
- }
291
- } else if (event === "beforeExit") {
292
- process.removeAllListeners("beforeExit");
293
- process.on("beforeExit", handler ?? defaults.beforeExit);
294
- }
295
-
296
- return controller;
297
- },
298
- };
299
-
300
- // Install default handlers for error, exit, sig
301
- controller.on("error").on("exit").on("sig");
302
-
303
- return controller;
304
- },
305
-
306
- /**
307
- * Safely stringifies a value to pretty-printed JSON (4 spaces indent).
308
- *
309
- * @param {*} value - Value to stringify.
310
- * @returns {string} JSON string representation.
311
- */
312
- JSONString(value) {
313
- return JSON.stringify(value, null, 4);
314
- },
315
-
316
- /**
317
- * Parses a JSON string.
318
- *
319
- * @param {string} value - JSON string to parse.
320
- * @returns {*} Parsed value.
321
- */
322
- JSONParse(value) {
323
- return JSON.parse(value);
324
- },
325
- };
1
+ "use strict";
2
+
3
+ /**
4
+ * Everyday helpers: `wait`, `when`, `dontCrash`, and JSON functions that
5
+ * don't throw. They are also available at the top level (`nc.wait()`).
6
+ *
7
+ * @example
8
+ * await nc.wait("1.5s");
9
+ * const settings = nc.JSONParse(text, {});
10
+ */
11
+
12
+ const { AbortError } = require("./errors.js");
13
+ const { types } = require("node:util");
14
+
15
+ /**
16
+ * Options for `wait()`.
17
+ * @typedef {object} WaitOptions
18
+ * @property {AbortSignal} [signal] Cancels the wait; the promise rejects with an `AbortError`.
19
+ * @property {boolean} [unref] Let the process exit even if the timer is still pending.
20
+ */
21
+
22
+ /**
23
+ * Options for `when()`.
24
+ * @typedef {object} WhenOptions
25
+ * @property {number} [interval] Milliseconds between two checks. Defaults to `50`.
26
+ * @property {number | null} [timeout] Give up after this many milliseconds and emit `"timeout"`.
27
+ * @property {number | null} [max] Stop after this many triggers.
28
+ */
29
+
30
+ /**
31
+ * The task returned by `when()`. Every method returns the task, so calls chain.
32
+ * @typedef {object} WhenTask
33
+ * @property {() => WhenTask} start Starts checking. A stopped task can't be restarted.
34
+ * @property {() => WhenTask} stop Stops checking and clears the timers.
35
+ * @property {<E extends "trigger" | "error" | "timeout">(event: E, handler: E extends "error" ? (error: unknown) => void : (payload: any) => void) => WhenTask} on Listens to `"trigger"` (the condition is true), `"error"` (it threw) or `"timeout"`.
36
+ */
37
+
38
+ /**
39
+ * Events you can customize on `dontCrash()`.
40
+ * @typedef {"error" | "exit" | "sig" | "beforeExit"} DontCrashEvent
41
+ */
42
+
43
+ /**
44
+ * Returned by `dontCrash()`.
45
+ * @typedef {object} DontCrashController
46
+ * @property {<E extends DontCrashEvent>(event: E, handler?: E extends "error" ? (error: unknown) => void : E extends "sig" ? (signal: NodeJS.Signals) => void : (code: number) => void) => DontCrashController} on Replaces the handler for an event. Call it without a handler to restore the default one.
47
+ * @property {() => void} dispose Removes everything `dontCrash()` installed.
48
+ */
49
+
50
+ /**
51
+ * Resolves after a delay, given in milliseconds or as a duration string.
52
+ *
53
+ * @example
54
+ * await nc.wait(500);
55
+ * await nc.wait("2s");
56
+ * await nc.wait("1m", { signal: controller.signal });
57
+ *
58
+ * @param {number | string} duration Milliseconds, or a string like `"250ms"`, `"2s"`, `"1m30s"`.
59
+ * @param {WaitOptions} [options]
60
+ * @returns {Promise<void>}
61
+ * @throws {AbortError} If the signal is aborted.
62
+ * @throws {TypeError} If the duration can't be parsed.
63
+ */
64
+ function wait(duration, options = {}) {
65
+ const ms = typeof duration === "number" ? duration : require("./Time.js").parseDuration(duration);
66
+ if (ms === null || !Number.isFinite(ms)) return Promise.reject(new TypeError(`Invalid duration: ${String(duration)}`));
67
+ const { signal } = options;
68
+ return new Promise((resolve, reject) => {
69
+ if (signal?.aborted) return reject(new AbortError(undefined, { cause: signal.reason }));
70
+ const onAbort = () => {
71
+ clearTimeout(timer);
72
+ reject(new AbortError(undefined, { cause: signal?.reason }));
73
+ };
74
+ const timer = setTimeout(() => {
75
+ signal?.removeEventListener("abort", onAbort);
76
+ resolve();
77
+ }, Math.max(0, ms));
78
+ if (options.unref) timer.unref();
79
+ signal?.addEventListener("abort", onAbort, { once: true });
80
+ });
81
+ }
82
+
83
+ /**
84
+ * Checks a condition at a regular interval and tells you when it's true.
85
+ * Call `.start()` once your listeners are in place. If you just want to
86
+ * await a condition, `nc.async.poll()` is simpler.
87
+ *
88
+ * @example
89
+ * nc.when(() => queue.length > 0, { reason: "jobs waiting" }, { interval: 500, max: 1, timeout: 10_000 })
90
+ * .on("trigger", (payload) => nc.info(payload.reason))
91
+ * .on("timeout", () => nc.warn("Nothing happened in 10s"))
92
+ * .start();
93
+ *
94
+ * @param {boolean | PromiseLike<boolean> | (() => unknown)} predicate The condition, or a function (sync or async) that returns it.
95
+ * @param {any} [payload={}] Passed to the `"trigger"` and `"timeout"` listeners.
96
+ * @param {WhenOptions} [options]
97
+ * @returns {WhenTask}
98
+ */
99
+ function when(predicate, payload = {}, options = {}) {
100
+ const interval = options.interval ?? 50;
101
+ const timeout = options.timeout ?? null;
102
+ const max = options.max ?? null;
103
+ /** @type {Record<"trigger" | "error" | "timeout", Set<Function>>} */
104
+ const listeners = { trigger: new Set(), error: new Set(), timeout: new Set() };
105
+ /** @type {NodeJS.Timeout | undefined} */
106
+ let timer;
107
+ /** @type {NodeJS.Timeout | undefined} */
108
+ let timeoutTimer;
109
+ let stopped = false;
110
+ let started = false;
111
+ let count = 0;
112
+
113
+ const emit = (/** @type {"trigger" | "error" | "timeout"} */ event, /** @type {unknown} */ value) => {
114
+ for (const fn of listeners[event]) {
115
+ try {
116
+ fn(value);
117
+ } catch {
118
+ // A failing listener must not stop the task.
119
+ }
120
+ }
121
+ };
122
+ const halt = () => {
123
+ stopped = true;
124
+ clearTimeout(timer);
125
+ clearTimeout(timeoutTimer);
126
+ };
127
+ const tick = async () => {
128
+ if (stopped) return;
129
+ try {
130
+ const ok = typeof predicate === "function" ? await predicate() : await predicate;
131
+ if (stopped) return;
132
+ if (ok) {
133
+ count++;
134
+ emit("trigger", payload);
135
+ if (max !== null && count >= max) return halt();
136
+ }
137
+ } catch (error) {
138
+ emit("error", error);
139
+ }
140
+ if (!stopped) timer = setTimeout(tick, interval);
141
+ };
142
+
143
+ /** @type {WhenTask} */
144
+ const task = {
145
+ start() {
146
+ if (stopped || started) return task;
147
+ started = true;
148
+ timer = setTimeout(tick, 0);
149
+ if (timeout !== null) {
150
+ timeoutTimer = setTimeout(() => {
151
+ if (stopped) return;
152
+ emit("timeout", payload);
153
+ halt();
154
+ }, timeout);
155
+ }
156
+ return task;
157
+ },
158
+ stop() {
159
+ halt();
160
+ return task;
161
+ },
162
+ on(event, handler) {
163
+ listeners[event]?.add(handler);
164
+ return task;
165
+ },
166
+ };
167
+ return task;
168
+ }
169
+
170
+ /**
171
+ * Keeps the process alive when something goes wrong. Uncaught exceptions and
172
+ * unhandled rejections are logged instead of crashing, and `SIGINT`/`SIGTERM`
173
+ * are logged before a clean exit. Customize any of it with `.on()`.
174
+ *
175
+ * Only its own handlers are ever removed; listeners added by your code or by
176
+ * other libraries are left alone.
177
+ *
178
+ * @example
179
+ * nc.dontCrash()
180
+ * .on("error", (error) => sentry.captureException(error))
181
+ * .on("sig", async () => {
182
+ * await server.close();
183
+ * process.exit(0);
184
+ * });
185
+ *
186
+ * @returns {DontCrashController}
187
+ */
188
+ function dontCrash() {
189
+ _dontCrashDispose?.();
190
+ const logger = require("./Logger.js");
191
+ /** @type {Array<[string, (...args: any[]) => void]>} */
192
+ let installed = [];
193
+
194
+ const defaults = {
195
+ error: (/** @type {unknown} */ error) => logger.error("[@ix-xs/node-comfort] Uncaught error:", error),
196
+ exit: (/** @type {number} */ code) => logger.log(`<% gray [@ix-xs/node-comfort] Process exit with code ${code} %>`),
197
+ sig: (/** @type {NodeJS.Signals} */ signal) => {
198
+ logger.warn(`[@ix-xs/node-comfort] Received ${signal}, exiting`);
199
+ process.exit(0);
200
+ },
201
+ beforeExit: () => {},
202
+ };
203
+
204
+ /**
205
+ * @param {string} event
206
+ * @param {(...args: any[]) => void} handler
207
+ */
208
+ const listen = (event, handler) => {
209
+ try {
210
+ process.on(/** @type {any} */ (event), handler);
211
+ installed.push([event, handler]);
212
+ } catch {
213
+ // Signal not supported on this platform (for example SIGQUIT on Windows).
214
+ }
215
+ };
216
+ /** @param {string[]} events */
217
+ const unlisten = (events) => {
218
+ installed = installed.filter(([event, handler]) => {
219
+ if (!events.includes(event)) return true;
220
+ process.off(/** @type {any} */ (event), handler);
221
+ return false;
222
+ });
223
+ };
224
+
225
+ /** @type {DontCrashController} */
226
+ const controller = {
227
+ on(event, handler) {
228
+ if (event === "error") {
229
+ unlisten(["uncaughtException", "unhandledRejection"]);
230
+ const fn = /** @type {(error: unknown) => void} */ (handler ?? defaults.error);
231
+ listen("uncaughtException", fn);
232
+ listen("unhandledRejection", fn);
233
+ } else if (event === "exit") {
234
+ unlisten(["exit"]);
235
+ listen("exit", /** @type {(code: number) => void} */ (handler ?? defaults.exit));
236
+ } else if (event === "sig") {
237
+ const signals = ["SIGINT", "SIGTERM", "SIGQUIT"];
238
+ unlisten(signals);
239
+ for (const signal of signals) listen(signal, /** @type {(signal: NodeJS.Signals) => void} */ (handler ?? defaults.sig));
240
+ } else if (event === "beforeExit") {
241
+ unlisten(["beforeExit"]);
242
+ listen("beforeExit", /** @type {(code: number) => void} */ (handler ?? defaults.beforeExit));
243
+ }
244
+ return controller;
245
+ },
246
+ dispose() {
247
+ unlisten(installed.map(([event]) => event));
248
+ if (_dontCrashDispose === controller.dispose) _dontCrashDispose = undefined;
249
+ },
250
+ };
251
+ _dontCrashDispose = controller.dispose;
252
+ controller.on("error").on("exit").on("sig");
253
+ return controller;
254
+ }
255
+
256
+ /** @type {(() => void) | undefined} */
257
+ let _dontCrashDispose;
258
+
259
+ /**
260
+ * `JSON.stringify` that never throws. Circular references become
261
+ * `"[Circular]"`, bigints become strings, Maps become objects and Sets become
262
+ * arrays.
263
+ *
264
+ * @example
265
+ * nc.JSONString({ a: 1 }); // '{\n "a": 1\n}'
266
+ * nc.JSONString({ id: 10n, tags: new Set(["x"]) }, 0); // '{"id":"10","tags":["x"]}'
267
+ *
268
+ * @param {unknown} value
269
+ * @param {number} [spaces=4] Indentation. Use `0` for a single line.
270
+ * @returns {string} The JSON text, or `"undefined"` for values JSON can't represent.
271
+ */
272
+ function JSONString(value, spaces = 4) {
273
+ /** @type {Array<[holder: object, original: object]>} ancestors of the value being serialized */
274
+ const stack = [];
275
+ const text = JSON.stringify(
276
+ value,
277
+ /** @this {any} */ function(_, raw) {
278
+ if (typeof raw === "bigint") return raw.toString();
279
+ if (raw === null || typeof raw !== "object") return raw;
280
+ while (stack.length && stack[stack.length - 1][0] !== this) stack.pop();
281
+ if (stack.some(([, original]) => original === raw)) return "[Circular]";
282
+ const v = types.isMap(raw) ? Object.fromEntries(raw) : types.isSet(raw) ? [...raw] : raw;
283
+ stack.push([v, raw]);
284
+ return v;
285
+ },
286
+ spaces,
287
+ );
288
+ return text ?? String(text);
289
+ }
290
+
291
+ /**
292
+ * `JSON.parse` with an optional fallback. With a fallback, invalid input
293
+ * returns it instead of throwing.
294
+ *
295
+ * @example
296
+ * nc.JSONParse('{"a":1}'); // { a: 1 }
297
+ * nc.JSONParse("oops", null); // null
298
+ * nc.JSONParse("oops"); // throws SyntaxError
299
+ *
300
+ * @template [T=any]
301
+ * @param {string} text
302
+ * @param {T} [fallback] Returned when `text` isn't valid JSON.
303
+ * @returns {T}
304
+ * @throws {SyntaxError} If `text` is invalid and there's no fallback.
305
+ */
306
+ function JSONParse(text, fallback) {
307
+ if (arguments.length < 2) return JSON.parse(text);
308
+ try {
309
+ return JSON.parse(text);
310
+ } catch {
311
+ return /** @type {T} */ (fallback);
312
+ }
313
+ }
314
+
315
+ module.exports = {
316
+ wait,
317
+ when,
318
+ dontCrash,
319
+ JSONString,
320
+ JSONParse,
321
+ };