@ix-xs/node-comfort 1.0.57 → 1.1.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.
package/src/Utils.js ADDED
@@ -0,0 +1,325 @@
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
+ };
package/src/checker.js DELETED
@@ -1,76 +0,0 @@
1
- const { types } = require("node:util");
2
-
3
- module.exports = {
4
- /**
5
- * @param {*} value
6
- */
7
- isArray(value) {
8
- return Array.isArray(value);
9
- },
10
- /**
11
- * @param {*} value
12
- */
13
- isNumber(value) {
14
- return typeof value === "number" && !Number.isNaN(value);
15
- },
16
- /**
17
- * @param {*} value
18
- */
19
- isFunction(value) {
20
- return typeof value === "function";
21
- },
22
- /**
23
- * @param {*} value
24
- */
25
- isObject(value) {
26
- return typeof value === "object" && value !== null;
27
- },
28
- /**
29
- * @param {*} value
30
- */
31
- isBoolean(value) {
32
- return typeof value === "boolean";
33
- },
34
- /**
35
- * @param {*} value
36
- */
37
- isString(value) {
38
- return typeof value === "string";
39
- },
40
- /**
41
- * @param {*} value
42
- */
43
- isUndefined(value) {
44
- return typeof value === "undefined";
45
- },
46
- /**
47
- * @param {*} value
48
- */
49
- isNull(value) {
50
- return value === null;
51
- },
52
- /**
53
- * @param {*} value
54
- */
55
- isDate(value) {
56
- return types.isDate(value);
57
- },
58
- /**
59
- * @param {*} value
60
- */
61
- isMap(value) {
62
- return types.isMap(value);
63
- },
64
- /**
65
- * @param {*} value
66
- */
67
- isSet(value) {
68
- return types.isSet(value);
69
- },
70
- /**
71
- * @param {*} value
72
- */
73
- isError(value) {
74
- return types.isNativeError(value);
75
- },
76
- };
package/src/cli.js DELETED
@@ -1,86 +0,0 @@
1
- const { execSync, spawn } = require("node:child_process");
2
- const EventEmitter = require("node:events");
3
-
4
- module.exports = {
5
- exec(command) {
6
- return execSync(command, {
7
- shell: true,
8
- stdio: "pipe",
9
- });
10
- },
11
- stream: class {
12
- #processus;
13
- #events = new EventEmitter();
14
-
15
- constructor(command, args) {
16
- this.#processus = spawn(command, args, {
17
- shell: true,
18
- stdio: ["ignore", "pipe", "pipe"],
19
- });
20
-
21
- this.#processus.stdout.on("data", (data) => {
22
- this.#events.emit("data", data.toString());
23
- });
24
- this.#processus.stderr.on("data", (data) => {
25
- this.#events.emit("data", data.toString());
26
- });
27
- this.#processus.on("error", (error) => {
28
- this.#events.emit("error", error);
29
- });
30
- this.#processus.on("exit", async (code, signal) => {
31
- await this.stop();
32
- });
33
- this.#processus.on("close", (code, signal) => {
34
- this.#events.emit("stopped", code, signal);
35
- });
36
- }
37
-
38
- /**
39
- * @typedef {"error"|"data"|"stopped"} EventName
40
- * @typedef {{
41
- * error: (error: Error) => void|Promise<void>,
42
- * data: (chunk: string) => void|Promise<void>,
43
- * stopped: (code: number|null, signal: keyof import("node:child_process").ChildProcessEventMap) => void|Promise<void>,
44
- * }} EventHandler
45
- */
46
- /**
47
- * @template {EventName} E
48
- * @param {E} event
49
- * @param {EventHandler[E]} handler
50
- */
51
- on(event, handler) {
52
- this.#events.on(event, handler);
53
- return this;
54
- }
55
-
56
- async stop() {
57
- return new Promise((resolve) => {
58
- if (!this.#processus || this.#processus.killed) {
59
- this.#processus = null;
60
- return resolve();
61
- }
62
-
63
- let resolved = false;
64
-
65
- const done = () => {
66
- if (!resolved) {
67
- resolved = true;
68
- this.#processus = null;
69
- resolve();
70
- }
71
- };
72
-
73
- this.#processus.once("exit", done);
74
-
75
- this.#processus.kill("SIGTERM");
76
-
77
- setTimeout(() => {
78
- if (this.#processus && !this.#processus.killed && !resolved) {
79
- this.#processus.kill("SIGKILL");
80
- }
81
- done();
82
- }, 5000);
83
- });
84
- }
85
- },
86
- };