@ix-xs/node-comfort 1.1.31 → 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 (77) hide show
  1. package/CHANGELOG.md +190 -0
  2. package/LICENSE +21 -0
  3. package/README.md +210 -748
  4. package/index.js +84 -13
  5. package/package.json +201 -21
  6. package/src/Arr.js +901 -0
  7. package/src/Async.js +585 -0
  8. package/src/Cache.js +329 -0
  9. package/src/Checker.js +979 -188
  10. package/src/Cli.js +1034 -0
  11. package/src/Color.js +415 -0
  12. package/src/Crypto.js +707 -0
  13. package/src/Emitter.js +289 -0
  14. package/src/Env.js +555 -0
  15. package/src/FS.js +1483 -768
  16. package/src/Func.js +692 -0
  17. package/src/Http.js +538 -0
  18. package/src/Id.js +418 -0
  19. package/src/Logger.js +739 -307
  20. package/src/Num.js +849 -0
  21. package/src/Obj.js +904 -0
  22. package/src/SQLite.js +1308 -994
  23. package/src/Schema.js +1586 -0
  24. package/src/Str.js +999 -0
  25. package/src/Sys.js +511 -0
  26. package/src/Time.js +1323 -0
  27. package/src/Utils.js +321 -325
  28. package/src/errors.js +292 -0
  29. package/types/index.d.ts +693 -120
  30. package/types/src/Arr.d.ts +562 -0
  31. package/types/src/Arr.d.ts.map +1 -0
  32. package/types/src/Async.d.ts +321 -0
  33. package/types/src/Async.d.ts.map +1 -0
  34. package/types/src/Cache.d.ts +195 -0
  35. package/types/src/Cache.d.ts.map +1 -0
  36. package/types/src/Checker.d.ts +609 -81
  37. package/types/src/Checker.d.ts.map +1 -1
  38. package/types/src/Cli.d.ts +582 -0
  39. package/types/src/Cli.d.ts.map +1 -0
  40. package/types/src/Color.d.ts +294 -0
  41. package/types/src/Color.d.ts.map +1 -0
  42. package/types/src/Crypto.d.ts +534 -0
  43. package/types/src/Crypto.d.ts.map +1 -0
  44. package/types/src/Emitter.d.ts +170 -0
  45. package/types/src/Emitter.d.ts.map +1 -0
  46. package/types/src/Env.d.ts +382 -0
  47. package/types/src/Env.d.ts.map +1 -0
  48. package/types/src/FS.d.ts +709 -201
  49. package/types/src/FS.d.ts.map +1 -1
  50. package/types/src/Func.d.ts +455 -0
  51. package/types/src/Func.d.ts.map +1 -0
  52. package/types/src/Http.d.ts +374 -0
  53. package/types/src/Http.d.ts.map +1 -0
  54. package/types/src/Id.d.ts +200 -0
  55. package/types/src/Id.d.ts.map +1 -0
  56. package/types/src/Logger.d.ts +374 -24
  57. package/types/src/Logger.d.ts.map +1 -1
  58. package/types/src/Num.d.ts +578 -0
  59. package/types/src/Num.d.ts.map +1 -0
  60. package/types/src/Obj.d.ts +475 -0
  61. package/types/src/Obj.d.ts.map +1 -0
  62. package/types/src/SQLite.d.ts +647 -305
  63. package/types/src/SQLite.d.ts.map +1 -1
  64. package/types/src/Schema.d.ts +940 -0
  65. package/types/src/Schema.d.ts.map +1 -0
  66. package/types/src/Str.d.ts +696 -0
  67. package/types/src/Str.d.ts.map +1 -0
  68. package/types/src/Sys.d.ts +269 -0
  69. package/types/src/Sys.d.ts.map +1 -0
  70. package/types/src/Time.d.ts +664 -0
  71. package/types/src/Time.d.ts.map +1 -0
  72. package/types/src/Utils.d.ts +154 -102
  73. package/types/src/Utils.d.ts.map +1 -1
  74. package/types/src/errors.d.ts +292 -0
  75. package/types/src/errors.d.ts.map +1 -0
  76. package/tsconfig.json +0 -18
  77. package/types/index.d.ts.map +0 -1
package/src/Emitter.js ADDED
@@ -0,0 +1,289 @@
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
+
19
+ /**
20
+ * Event names mapped to their arguments, like
21
+ * `{ ready: [], data: [chunk: Buffer], error: [error: Error] }`.
22
+ * @typedef {Record<string, any[]>} EventMap
23
+ */
24
+
25
+ /**
26
+ * Options for `waitFor()`.
27
+ * @template {any[]} A
28
+ * @typedef {object} WaitForOptions
29
+ * @property {number | string} [timeout] Reject with a `TimeoutError` after this long.
30
+ * @property {AbortSignal} [signal] Reject with an `AbortError` when aborted.
31
+ * @property {(...args: A) => boolean} [filter] Only resolve for events that pass this test.
32
+ */
33
+
34
+ /**
35
+ * A typed event emitter. In JavaScript, give the variable a JSDoc type such
36
+ * as `import("@ix-xs/node-comfort").Emitter<{ ready: [port: number] }>`.
37
+ *
38
+ * @template {EventMap} [Events=EventMap]
39
+ */
40
+ class Emitter {
41
+ /** @type {Map<keyof Events, Set<(...args: any[]) => unknown>>} */
42
+ #listeners = new Map();
43
+ /** @type {Set<(event: keyof Events, ...args: any[]) => unknown>} */
44
+ #any = new Set();
45
+
46
+ /**
47
+ * Listens to an event.
48
+ *
49
+ * @example
50
+ * const off = emitter.on("data", (chunk) => chunks.push(chunk));
51
+ * off(); // stop listening
52
+ *
53
+ * @template {keyof Events & string} K
54
+ * @param {K} event
55
+ * @param {(...args: Events[K]) => unknown} listener
56
+ * @returns {() => void} Removes the listener.
57
+ */
58
+ on(event, listener) {
59
+ let set = this.#listeners.get(event);
60
+ if (!set) this.#listeners.set(event, (set = new Set()));
61
+ set.add(listener);
62
+ return () => this.off(event, listener);
63
+ }
64
+
65
+ /**
66
+ * Listens to an event once.
67
+ *
68
+ * @template {keyof Events & string} K
69
+ * @param {K} event
70
+ * @param {(...args: Events[K]) => unknown} listener
71
+ * @returns {() => void} Removes the listener if it hasn't run yet.
72
+ */
73
+ once(event, listener) {
74
+ /** @type {(...args: Events[K]) => unknown} */
75
+ const wrapper = (...args) => {
76
+ off();
77
+ return listener(...args);
78
+ };
79
+ const off = this.on(event, wrapper);
80
+ return off;
81
+ }
82
+
83
+ /**
84
+ * Listens to every event; the listener gets the event name first. Handy for
85
+ * logging.
86
+ *
87
+ * @example
88
+ * emitter.onAny((event, ...args) => nc.debug(event, args));
89
+ *
90
+ * @param {(event: keyof Events & string, ...args: any[]) => unknown} listener
91
+ * @returns {() => void} Removes the listener.
92
+ */
93
+ onAny(listener) {
94
+ this.#any.add(/** @type {any} */ (listener));
95
+ return () => void this.#any.delete(/** @type {any} */ (listener));
96
+ }
97
+
98
+ /**
99
+ * Removes a listener, or all listeners of the event if you don't pass one.
100
+ *
101
+ * @template {keyof Events & string} K
102
+ * @param {K} event
103
+ * @param {(...args: Events[K]) => unknown} [listener]
104
+ * @returns {this}
105
+ */
106
+ off(event, listener) {
107
+ if (!listener) this.#listeners.delete(event);
108
+ else {
109
+ const set = this.#listeners.get(event);
110
+ set?.delete(listener);
111
+ if (set && !set.size) this.#listeners.delete(event);
112
+ }
113
+ return this;
114
+ }
115
+
116
+ /**
117
+ * Calls the event's listeners right away, in the order they were added.
118
+ * If one throws, the error propagates, as with Node's `EventEmitter`.
119
+ *
120
+ * @example
121
+ * emitter.emit("message", "hello", "ada");
122
+ *
123
+ * @template {keyof Events & string} K
124
+ * @param {K} event
125
+ * @param {Events[K]} args
126
+ * @returns {boolean} `true` if anyone was listening.
127
+ */
128
+ emit(event, ...args) {
129
+ const listeners = [...(this.#listeners.get(event) ?? [])];
130
+ for (const listener of listeners) listener(...args);
131
+ for (const listener of [...this.#any]) listener(event, ...args);
132
+ return listeners.length > 0 || this.#any.size > 0;
133
+ }
134
+
135
+ /**
136
+ * Calls the listeners one at a time, waiting for each, and resolves with
137
+ * what they returned. Good for hooks that may be async.
138
+ *
139
+ * @example
140
+ * await hooks.emitAsync("beforeSave", record);
141
+ *
142
+ * @template {keyof Events & string} K
143
+ * @param {K} event
144
+ * @param {Events[K]} args
145
+ * @returns {Promise<unknown[]>}
146
+ */
147
+ async emitAsync(event, ...args) {
148
+ const results = [];
149
+ for (const listener of [...(this.#listeners.get(event) ?? [])]) results.push(await listener(...args));
150
+ for (const listener of [...this.#any]) await listener(event, ...args);
151
+ return results;
152
+ }
153
+
154
+ /**
155
+ * Waits for the next time the event fires and resolves with its arguments.
156
+ *
157
+ * @example
158
+ * const [code] = await child.waitFor("exit", { timeout: "30s" });
159
+ * const [text] = await chat.waitFor("message", { filter: (text, from) => from === "ada" });
160
+ *
161
+ * @template {keyof Events & string} K
162
+ * @param {K} event
163
+ * @param {WaitForOptions<Events[K]>} [options]
164
+ * @returns {Promise<Events[K]>}
165
+ * @throws {TimeoutError}
166
+ * @throws {AbortError}
167
+ */
168
+ waitFor(event, options = {}) {
169
+ return new Promise((resolve, reject) => {
170
+ /** @type {NodeJS.Timeout | undefined} */
171
+ let timer;
172
+ const cleanup = () => {
173
+ clearTimeout(timer);
174
+ off();
175
+ options.signal?.removeEventListener("abort", onAbort);
176
+ };
177
+ const onAbort = () => {
178
+ cleanup();
179
+ reject(new AbortError(`Stopped waiting for "${event}"`, { cause: options.signal?.reason }));
180
+ };
181
+ const off = this.on(event, /** @type {any} */ ((/** @type {Events[K]} */ ...args) => {
182
+ if (options.filter && !options.filter(...args)) return;
183
+ cleanup();
184
+ resolve(args);
185
+ }));
186
+ if (options.signal?.aborted) return onAbort();
187
+ options.signal?.addEventListener("abort", onAbort, { once: true });
188
+ if (options.timeout !== undefined) {
189
+ const ms = typeof options.timeout === "number" ? options.timeout : require("./Time.js").parseDuration(options.timeout) ?? 0;
190
+ timer = setTimeout(() => {
191
+ cleanup();
192
+ reject(new TimeoutError(`Timed out waiting for "${event}" after ${ms}ms`, { timeout: ms }));
193
+ }, ms);
194
+ }
195
+ });
196
+ }
197
+
198
+ /**
199
+ * Loops over an event with `for await`. Events that fire while your loop
200
+ * body runs are queued, and leaving the loop removes the listener.
201
+ *
202
+ * @example
203
+ * for await (const [job] of queue.iterate("job")) {
204
+ * await process(job);
205
+ * }
206
+ *
207
+ * @template {keyof Events & string} K
208
+ * @param {K} event
209
+ * @param {{ signal?: AbortSignal }} [options] Ends the loop when aborted.
210
+ * @returns {AsyncIterableIterator<Events[K]>}
211
+ */
212
+ iterate(event, options = {}) {
213
+ /** @type {Array<Events[K]>} */
214
+ const buffer = [];
215
+ /** @type {Array<(result: IteratorResult<Events[K]>) => void>} */
216
+ const waiting = [];
217
+ let done = false;
218
+ const finish = () => {
219
+ if (done) return;
220
+ done = true;
221
+ off();
222
+ options.signal?.removeEventListener("abort", finish);
223
+ for (const resolve of waiting.splice(0)) resolve({ value: undefined, done: true });
224
+ };
225
+ const off = this.on(event, /** @type {any} */ ((/** @type {Events[K]} */ ...args) => {
226
+ const next = waiting.shift();
227
+ if (next) next({ value: args, done: false });
228
+ else buffer.push(args);
229
+ }));
230
+ if (options.signal?.aborted) finish();
231
+ options.signal?.addEventListener("abort", finish, { once: true });
232
+ /** @type {AsyncIterableIterator<Events[K]>} */
233
+ const iterator = {
234
+ next: () => {
235
+ if (buffer.length) return Promise.resolve({ value: /** @type {Events[K]} */ (buffer.shift()), done: false });
236
+ if (done) return Promise.resolve({ value: undefined, done: true });
237
+ return new Promise((resolve) => waiting.push(resolve));
238
+ },
239
+ return: () => {
240
+ finish();
241
+ return Promise.resolve({ value: undefined, done: true });
242
+ },
243
+ [Symbol.asyncIterator]() {
244
+ return iterator;
245
+ },
246
+ };
247
+ return iterator;
248
+ }
249
+
250
+ /**
251
+ * How many listeners an event has, or all events together. `onAny`
252
+ * listeners aren't counted.
253
+ *
254
+ * @param {keyof Events & string} [event]
255
+ * @returns {number}
256
+ */
257
+ listenerCount(event) {
258
+ if (event !== undefined) return this.#listeners.get(event)?.size ?? 0;
259
+ let total = 0;
260
+ for (const set of this.#listeners.values()) total += set.size;
261
+ return total;
262
+ }
263
+
264
+ /**
265
+ * The events that have listeners.
266
+ *
267
+ * @returns {Array<keyof Events & string>}
268
+ */
269
+ eventNames() {
270
+ return /** @type {Array<keyof Events & string>} */ ([...this.#listeners.keys()]);
271
+ }
272
+
273
+ /**
274
+ * Removes all listeners of an event, or everything if you don't name one.
275
+ *
276
+ * @param {keyof Events & string} [event]
277
+ * @returns {this}
278
+ */
279
+ clear(event) {
280
+ if (event !== undefined) this.#listeners.delete(event);
281
+ else {
282
+ this.#listeners.clear();
283
+ this.#any.clear();
284
+ }
285
+ return this;
286
+ }
287
+ }
288
+
289
+ module.exports = Emitter;