@ix-xs/node-comfort 1.2.0 → 2.1.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 (78) hide show
  1. package/CHANGELOG.md +340 -0
  2. package/README.md +137 -548
  3. package/index.js +80 -35
  4. package/package.json +161 -33
  5. package/src/Arr.js +1050 -418
  6. package/src/Async.js +817 -0
  7. package/src/Cache.js +433 -0
  8. package/src/Checker.js +1202 -539
  9. package/src/Cli.js +1293 -0
  10. package/src/Color.js +556 -0
  11. package/src/Crypto.js +1082 -0
  12. package/src/Emitter.js +356 -0
  13. package/src/Env.js +689 -0
  14. package/src/FS.js +1626 -894
  15. package/src/Func.js +823 -356
  16. package/src/Http.js +700 -0
  17. package/src/Id.js +568 -200
  18. package/src/Logger.js +963 -378
  19. package/src/Num.js +965 -360
  20. package/src/Obj.js +1077 -379
  21. package/src/SQLite.js +1629 -1005
  22. package/src/Schema.js +2101 -0
  23. package/src/Str.js +1209 -432
  24. package/src/Sys.js +654 -0
  25. package/src/Time.js +1533 -440
  26. package/src/Utils.js +342 -325
  27. package/src/errors.js +335 -0
  28. package/src/internal/shared.js +415 -0
  29. package/types/index.d.ts +735 -526
  30. package/types/src/Arr.d.ts +533 -145
  31. package/types/src/Arr.d.ts.map +1 -1
  32. package/types/src/Async.d.ts +372 -0
  33. package/types/src/Async.d.ts.map +1 -0
  34. package/types/src/Cache.d.ts +221 -0
  35. package/types/src/Cache.d.ts.map +1 -0
  36. package/types/src/Checker.d.ts +651 -232
  37. package/types/src/Checker.d.ts.map +1 -1
  38. package/types/src/Cli.d.ts +664 -0
  39. package/types/src/Cli.d.ts.map +1 -0
  40. package/types/src/Color.d.ts +300 -0
  41. package/types/src/Color.d.ts.map +1 -0
  42. package/types/src/Crypto.d.ts +620 -0
  43. package/types/src/Crypto.d.ts.map +1 -0
  44. package/types/src/Emitter.d.ts +189 -0
  45. package/types/src/Emitter.d.ts.map +1 -0
  46. package/types/src/Env.d.ts +412 -0
  47. package/types/src/Env.d.ts.map +1 -0
  48. package/types/src/FS.d.ts +717 -238
  49. package/types/src/FS.d.ts.map +1 -1
  50. package/types/src/Func.d.ts +508 -135
  51. package/types/src/Func.d.ts.map +1 -1
  52. package/types/src/Http.d.ts +444 -0
  53. package/types/src/Http.d.ts.map +1 -0
  54. package/types/src/Id.d.ts +199 -66
  55. package/types/src/Id.d.ts.map +1 -1
  56. package/types/src/Logger.d.ts +414 -46
  57. package/types/src/Logger.d.ts.map +1 -1
  58. package/types/src/Num.d.ts +514 -137
  59. package/types/src/Num.d.ts.map +1 -1
  60. package/types/src/Obj.d.ts +453 -104
  61. package/types/src/Obj.d.ts.map +1 -1
  62. package/types/src/SQLite.d.ts +754 -308
  63. package/types/src/SQLite.d.ts.map +1 -1
  64. package/types/src/Schema.d.ts +1189 -0
  65. package/types/src/Schema.d.ts.map +1 -0
  66. package/types/src/Str.d.ts +617 -166
  67. package/types/src/Str.d.ts.map +1 -1
  68. package/types/src/Sys.d.ts +314 -0
  69. package/types/src/Sys.d.ts.map +1 -0
  70. package/types/src/Time.d.ts +759 -204
  71. package/types/src/Time.d.ts.map +1 -1
  72. package/types/src/Utils.d.ts +158 -102
  73. package/types/src/Utils.d.ts.map +1 -1
  74. package/types/src/errors.d.ts +316 -0
  75. package/types/src/errors.d.ts.map +1 -0
  76. package/types/src/internal/shared.d.ts +123 -0
  77. package/types/src/internal/shared.d.ts.map +1 -0
  78. package/types/index.d.ts.map +0 -1
package/src/Emitter.js ADDED
@@ -0,0 +1,356 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * A small typed event emitter. Declare your events once, and your editor
5
+ * checks every event name and listener argument. `on()` returns a function
6
+ * that removes the listener, and you can `await` an event or loop over
7
+ * events with `for await`.
8
+ *
9
+ * @example
10
+ * const chat = new nc.Emitter<{ message: [text: string, from: string]; close: [] }>();
11
+ * const off = chat.on("message", (text, from) => console.log(`${from}: ${text}`));
12
+ * chat.emit("message", "hello", "ada");
13
+ * off();
14
+ * const [text] = await chat.waitFor("message", { timeout: "5s" });
15
+ */
16
+
17
+ const { TimeoutError, AbortError } = require("./errors.js");
18
+ const { toMs, setLongTimeout } = require("./internal/shared.js");
19
+
20
+ /**
21
+ * Event names mapped to their arguments, like
22
+ * `{ ready: [], data: [chunk: Buffer], error: [error: Error] }`.
23
+ * @typedef {Record<string, any[]>} EventMap
24
+ */
25
+
26
+ /**
27
+ * Options for `on()` and `once()`.
28
+ * @typedef {object} ListenOptions
29
+ * @property {AbortSignal} [signal] Removes the listener when aborted. One signal can clean up many listeners at once.
30
+ */
31
+
32
+ /**
33
+ * Options for `waitFor()`.
34
+ * @template {any[]} A
35
+ * @typedef {object} WaitForOptions
36
+ * @property {number | string} [timeout] Reject with a `TimeoutError` after this long.
37
+ * @property {AbortSignal} [signal] Reject with an `AbortError` when aborted.
38
+ * @property {(...args: A) => boolean} [filter] Only resolve for events that pass this test.
39
+ */
40
+
41
+ /**
42
+ * A listener as stored: `once()` wraps the function it was given and keeps
43
+ * it as `listener`, so `off()` can find it.
44
+ * @typedef {((...args: any[]) => unknown) & { listener?: Function }} _StoredListener
45
+ */
46
+
47
+ /** @type {readonly _StoredListener[]} */
48
+ const NONE = Object.freeze([]);
49
+
50
+ /**
51
+ * A typed event emitter. In JavaScript, give the variable a JSDoc type such
52
+ * as `import("@ix-xs/node-comfort").Emitter<{ ready: [port: number] }>`.
53
+ *
54
+ * @template {EventMap} [Events=EventMap]
55
+ */
56
+ class Emitter {
57
+ // Listener lists are replaced, never changed in place: emit() can walk one
58
+ // without copying it, even if a listener adds or removes others meanwhile.
59
+ /** @type {Map<keyof Events, readonly _StoredListener[]>} */
60
+ #listeners = new Map();
61
+ /** @type {readonly _StoredListener[]} */
62
+ #any = NONE;
63
+
64
+ /**
65
+ * @param {keyof Events} event
66
+ * @param {_StoredListener} stored
67
+ * @param {ListenOptions | undefined} options
68
+ * @returns {() => void}
69
+ */
70
+ #add(event, stored, options) {
71
+ const signal = options?.signal;
72
+ if (signal?.aborted) return () => {};
73
+ const current = this.#listeners.get(event) ?? NONE;
74
+ if (!current.includes(stored)) this.#listeners.set(event, [...current, stored]);
75
+ const off = () => {
76
+ signal?.removeEventListener("abort", off);
77
+ this.#drop(event, stored);
78
+ };
79
+ signal?.addEventListener("abort", off, { once: true });
80
+ return off;
81
+ }
82
+
83
+ /**
84
+ * @param {keyof Events} event
85
+ * @param {Function} listener The stored function, or the one given to `once()`.
86
+ */
87
+ #drop(event, listener) {
88
+ const current = this.#listeners.get(event);
89
+ if (!current) return;
90
+ const next = current.filter((stored) => stored !== listener && stored.listener !== listener);
91
+ if (next.length === current.length) return;
92
+ if (next.length) this.#listeners.set(event, next);
93
+ else this.#listeners.delete(event);
94
+ }
95
+
96
+ /**
97
+ * Listens to an event.
98
+ *
99
+ * @example
100
+ * const off = emitter.on("data", (chunk) => chunks.push(chunk));
101
+ * off(); // stop listening
102
+ *
103
+ * const controller = new AbortController();
104
+ * emitter.on("data", onData, { signal: controller.signal });
105
+ * emitter.on("end", onEnd, { signal: controller.signal });
106
+ * controller.abort(); // removes both
107
+ *
108
+ * @template {keyof Events & string} K
109
+ * @param {K} event
110
+ * @param {(...args: Events[K]) => unknown} listener
111
+ * @param {ListenOptions} [options]
112
+ * @returns {() => void} Removes the listener.
113
+ */
114
+ on(event, listener, options) {
115
+ return this.#add(event, listener, options);
116
+ }
117
+
118
+ /**
119
+ * Listens to an event once.
120
+ *
121
+ * @template {keyof Events & string} K
122
+ * @param {K} event
123
+ * @param {(...args: Events[K]) => unknown} listener
124
+ * @param {ListenOptions} [options]
125
+ * @returns {() => void} Removes the listener if it hasn't run yet.
126
+ */
127
+ once(event, listener, options) {
128
+ /** @type {_StoredListener} */
129
+ const wrapper = (...args) => {
130
+ off();
131
+ return listener(.../** @type {Events[K]} */ (args));
132
+ };
133
+ wrapper.listener = listener;
134
+ const off = this.#add(event, wrapper, options);
135
+ return off;
136
+ }
137
+
138
+ /**
139
+ * Listens to every event; the listener gets the event name first. Handy for
140
+ * logging.
141
+ *
142
+ * @example
143
+ * emitter.onAny((event, ...args) => nc.debug(event, args));
144
+ *
145
+ * @param {(event: keyof Events & string, ...args: any[]) => unknown} listener
146
+ * @returns {() => void} Removes the listener.
147
+ */
148
+ onAny(listener) {
149
+ if (!this.#any.includes(listener)) this.#any = [...this.#any, listener];
150
+ return () => {
151
+ this.#any = this.#any.filter((stored) => stored !== listener);
152
+ };
153
+ }
154
+
155
+ /**
156
+ * Removes a listener, or all listeners of the event if you don't pass one.
157
+ *
158
+ * @template {keyof Events & string} K
159
+ * @param {K} event
160
+ * @param {(...args: Events[K]) => unknown} [listener] The function given to `on()` or `once()`.
161
+ * @returns {this}
162
+ */
163
+ off(event, listener) {
164
+ if (!listener) this.#listeners.delete(event);
165
+ else this.#drop(event, listener);
166
+ return this;
167
+ }
168
+
169
+ /**
170
+ * Calls the event's listeners right away, in the order they were added.
171
+ * If one throws, the error propagates, as with Node's `EventEmitter`.
172
+ *
173
+ * @example
174
+ * emitter.emit("message", "hello", "ada");
175
+ *
176
+ * @template {keyof Events & string} K
177
+ * @param {K} event
178
+ * @param {Events[K]} args
179
+ * @returns {boolean} `true` if anyone was listening.
180
+ */
181
+ emit(event, ...args) {
182
+ const listeners = this.#listeners.get(event);
183
+ const any = this.#any;
184
+ if (listeners !== undefined) for (let i = 0; i < listeners.length; i++) listeners[i](...args);
185
+ for (let i = 0; i < any.length; i++) any[i](event, ...args);
186
+ return listeners !== undefined || any.length > 0;
187
+ }
188
+
189
+ /**
190
+ * Calls the listeners one at a time, waiting for each, and resolves with
191
+ * what they returned. Good for hooks that may be async.
192
+ *
193
+ * @example
194
+ * await hooks.emitAsync("beforeSave", record);
195
+ *
196
+ * @template {keyof Events & string} K
197
+ * @param {K} event
198
+ * @param {Events[K]} args
199
+ * @returns {Promise<unknown[]>}
200
+ */
201
+ async emitAsync(event, ...args) {
202
+ const results = [];
203
+ const any = this.#any;
204
+ for (const listener of this.#listeners.get(event) ?? NONE) results.push(await listener(...args));
205
+ for (const listener of any) await listener(event, ...args);
206
+ return results;
207
+ }
208
+
209
+ /**
210
+ * Waits for the next time the event fires and resolves with its arguments.
211
+ *
212
+ * @example
213
+ * const [code] = await child.waitFor("exit", { timeout: "30s" });
214
+ * const [text] = await chat.waitFor("message", { filter: (text, from) => from === "ada" });
215
+ *
216
+ * @template {keyof Events & string} K
217
+ * @param {K} event
218
+ * @param {WaitForOptions<Events[K]>} [options]
219
+ * @returns {Promise<Events[K]>}
220
+ * @throws {TimeoutError}
221
+ * @throws {AbortError}
222
+ */
223
+ waitFor(event, options = {}) {
224
+ return new Promise((resolve, reject) => {
225
+ const limit = options.timeout === undefined ? undefined : toMs(options.timeout, "timeout");
226
+ /** @type {() => void} */
227
+ let cancelTimer = () => {};
228
+ const cleanup = () => {
229
+ cancelTimer();
230
+ off();
231
+ options.signal?.removeEventListener("abort", onAbort);
232
+ };
233
+ const onAbort = () => {
234
+ cleanup();
235
+ reject(new AbortError(`Stopped waiting for "${event}"`, { cause: options.signal?.reason }));
236
+ };
237
+ const off = this.on(event, /** @type {any} */ ((/** @type {Events[K]} */ ...args) => {
238
+ if (options.filter && !options.filter(...args)) return;
239
+ cleanup();
240
+ resolve(args);
241
+ }));
242
+ if (options.signal?.aborted) return onAbort();
243
+ options.signal?.addEventListener("abort", onAbort, { once: true });
244
+ if (limit !== undefined) {
245
+ cancelTimer = setLongTimeout(() => {
246
+ cleanup();
247
+ reject(new TimeoutError(`Timed out waiting for "${event}" after ${limit}ms`, { timeout: limit }));
248
+ }, limit);
249
+ }
250
+ });
251
+ }
252
+
253
+ /**
254
+ * Loops over an event with `for await`. Events that fire while your loop
255
+ * body runs are queued, and leaving the loop removes the listener.
256
+ *
257
+ * @example
258
+ * for await (const [job] of queue.iterate("job")) {
259
+ * await process(job);
260
+ * }
261
+ *
262
+ * @template {keyof Events & string} K
263
+ * @param {K} event
264
+ * @param {{ signal?: AbortSignal }} [options] Ends the loop when aborted.
265
+ * @returns {AsyncIterableIterator<Events[K]>}
266
+ */
267
+ iterate(event, options = {}) {
268
+ /** @type {Array<Events[K]>} */
269
+ const buffer = [];
270
+ /** @type {Array<(result: IteratorResult<Events[K]>) => void>} */
271
+ const waiting = [];
272
+ let done = false;
273
+ const finish = () => {
274
+ if (done) return;
275
+ done = true;
276
+ off();
277
+ options.signal?.removeEventListener("abort", finish);
278
+ for (const resolve of waiting.splice(0)) resolve({ value: undefined, done: true });
279
+ };
280
+ const off = this.on(event, /** @type {any} */ ((/** @type {Events[K]} */ ...args) => {
281
+ const next = waiting.shift();
282
+ if (next) next({ value: args, done: false });
283
+ else buffer.push(args);
284
+ }));
285
+ if (options.signal?.aborted) finish();
286
+ options.signal?.addEventListener("abort", finish, { once: true });
287
+ /** @type {AsyncIterableIterator<Events[K]>} */
288
+ const iterator = {
289
+ next: () => {
290
+ if (buffer.length) return Promise.resolve({ value: /** @type {Events[K]} */ (buffer.shift()), done: false });
291
+ if (done) return Promise.resolve({ value: undefined, done: true });
292
+ return new Promise((resolve) => waiting.push(resolve));
293
+ },
294
+ return: () => {
295
+ finish();
296
+ return Promise.resolve({ value: undefined, done: true });
297
+ },
298
+ [Symbol.asyncIterator]() {
299
+ return iterator;
300
+ },
301
+ };
302
+ return iterator;
303
+ }
304
+
305
+ /**
306
+ * How many listeners an event has, or all events together. `onAny`
307
+ * listeners aren't counted.
308
+ *
309
+ * @param {keyof Events & string} [event]
310
+ * @returns {number}
311
+ */
312
+ listenerCount(event) {
313
+ if (event !== undefined) return this.#listeners.get(event)?.length ?? 0;
314
+ let total = 0;
315
+ for (const list of this.#listeners.values()) total += list.length;
316
+ return total;
317
+ }
318
+
319
+ /**
320
+ * The listeners of an event, in the order they run. For `once()`, you get
321
+ * the function you passed, not its wrapper.
322
+ *
323
+ * @template {keyof Events & string} K
324
+ * @param {K} event
325
+ * @returns {Array<(...args: Events[K]) => unknown>}
326
+ */
327
+ listeners(event) {
328
+ return (this.#listeners.get(event) ?? NONE).map((stored) => /** @type {any} */ (stored.listener ?? stored));
329
+ }
330
+
331
+ /**
332
+ * The events that have listeners.
333
+ *
334
+ * @returns {Array<keyof Events & string>}
335
+ */
336
+ eventNames() {
337
+ return /** @type {Array<keyof Events & string>} */ ([...this.#listeners.keys()]);
338
+ }
339
+
340
+ /**
341
+ * Removes all listeners of an event, or everything if you don't name one.
342
+ *
343
+ * @param {keyof Events & string} [event]
344
+ * @returns {this}
345
+ */
346
+ clear(event) {
347
+ if (event !== undefined) this.#listeners.delete(event);
348
+ else {
349
+ this.#listeners.clear();
350
+ this.#any = NONE;
351
+ }
352
+ return this;
353
+ }
354
+ }
355
+
356
+ module.exports = Emitter;