@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.
- package/CHANGELOG.md +340 -0
- package/README.md +137 -548
- package/index.js +80 -35
- package/package.json +161 -33
- package/src/Arr.js +1050 -418
- package/src/Async.js +817 -0
- package/src/Cache.js +433 -0
- package/src/Checker.js +1202 -539
- package/src/Cli.js +1293 -0
- package/src/Color.js +556 -0
- package/src/Crypto.js +1082 -0
- package/src/Emitter.js +356 -0
- package/src/Env.js +689 -0
- package/src/FS.js +1626 -894
- package/src/Func.js +823 -356
- package/src/Http.js +700 -0
- package/src/Id.js +568 -200
- package/src/Logger.js +963 -378
- package/src/Num.js +965 -360
- package/src/Obj.js +1077 -379
- package/src/SQLite.js +1629 -1005
- package/src/Schema.js +2101 -0
- package/src/Str.js +1209 -432
- package/src/Sys.js +654 -0
- package/src/Time.js +1533 -440
- package/src/Utils.js +342 -325
- package/src/errors.js +335 -0
- package/src/internal/shared.js +415 -0
- package/types/index.d.ts +735 -526
- package/types/src/Arr.d.ts +533 -145
- package/types/src/Arr.d.ts.map +1 -1
- package/types/src/Async.d.ts +372 -0
- package/types/src/Async.d.ts.map +1 -0
- package/types/src/Cache.d.ts +221 -0
- package/types/src/Cache.d.ts.map +1 -0
- package/types/src/Checker.d.ts +651 -232
- package/types/src/Checker.d.ts.map +1 -1
- package/types/src/Cli.d.ts +664 -0
- package/types/src/Cli.d.ts.map +1 -0
- package/types/src/Color.d.ts +300 -0
- package/types/src/Color.d.ts.map +1 -0
- package/types/src/Crypto.d.ts +620 -0
- package/types/src/Crypto.d.ts.map +1 -0
- package/types/src/Emitter.d.ts +189 -0
- package/types/src/Emitter.d.ts.map +1 -0
- package/types/src/Env.d.ts +412 -0
- package/types/src/Env.d.ts.map +1 -0
- package/types/src/FS.d.ts +717 -238
- package/types/src/FS.d.ts.map +1 -1
- package/types/src/Func.d.ts +508 -135
- package/types/src/Func.d.ts.map +1 -1
- package/types/src/Http.d.ts +444 -0
- package/types/src/Http.d.ts.map +1 -0
- package/types/src/Id.d.ts +199 -66
- package/types/src/Id.d.ts.map +1 -1
- package/types/src/Logger.d.ts +414 -46
- package/types/src/Logger.d.ts.map +1 -1
- package/types/src/Num.d.ts +514 -137
- package/types/src/Num.d.ts.map +1 -1
- package/types/src/Obj.d.ts +453 -104
- package/types/src/Obj.d.ts.map +1 -1
- package/types/src/SQLite.d.ts +754 -308
- package/types/src/SQLite.d.ts.map +1 -1
- package/types/src/Schema.d.ts +1189 -0
- package/types/src/Schema.d.ts.map +1 -0
- package/types/src/Str.d.ts +617 -166
- package/types/src/Str.d.ts.map +1 -1
- package/types/src/Sys.d.ts +314 -0
- package/types/src/Sys.d.ts.map +1 -0
- package/types/src/Time.d.ts +759 -204
- package/types/src/Time.d.ts.map +1 -1
- package/types/src/Utils.d.ts +158 -102
- package/types/src/Utils.d.ts.map +1 -1
- package/types/src/errors.d.ts +316 -0
- package/types/src/errors.d.ts.map +1 -0
- package/types/src/internal/shared.d.ts +123 -0
- package/types/src/internal/shared.d.ts.map +1 -0
- 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;
|