@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/Cache.js ADDED
@@ -0,0 +1,329 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * An in-memory cache with a size limit (the least recently used entries go
5
+ * first), expiry per entry, and stats. `getOrSet()` loads a missing value
6
+ * only once, even when many callers ask for it at the same moment.
7
+ *
8
+ * @example
9
+ * const users = new nc.Cache({ max: 1000, ttl: "5m" });
10
+ * const user = await users.getOrSet(id, () => db.users.find(id));
11
+ * users.stats; // { hits: 12, misses: 3, hitRate: 0.8, ... }
12
+ */
13
+
14
+ const { parseDuration } = require("./Time.js");
15
+
16
+ /**
17
+ * Why an entry left the cache.
18
+ * @typedef {"evict" | "expire" | "delete" | "set" | "clear"} CacheRemovalReason
19
+ */
20
+
21
+ /**
22
+ * Options for `new Cache()`.
23
+ * @template K
24
+ * @template V
25
+ * @typedef {object} CacheOptions
26
+ * @property {number} [max] Maximum number of entries. When full, the least recently used goes. No limit by default.
27
+ * @property {number | string} [ttl] How long entries live, in ms or like `"10m"`. `0` means forever, the default.
28
+ * @property {boolean} [updateAgeOnGet] Reading an entry restarts its time to live.
29
+ * @property {(key: K, value: V, reason: CacheRemovalReason) => void} [onRemove] Called whenever an entry leaves the cache.
30
+ */
31
+
32
+ /**
33
+ * Options for `set()` and `getOrSet()`.
34
+ * @typedef {object} CacheSetOptions
35
+ * @property {number | string} [ttl] Time to live of this entry. `0` means forever.
36
+ */
37
+
38
+ /**
39
+ * Cache stats.
40
+ * @typedef {object} CacheStats
41
+ * @property {number} hits Reads that found a fresh entry.
42
+ * @property {number} misses Reads that found nothing or an expired entry.
43
+ * @property {number} sets
44
+ * @property {number} evictions Entries dropped because the cache was full.
45
+ * @property {number} expirations Entries dropped because they expired.
46
+ * @property {number} hitRate From 0 to 1.
47
+ */
48
+
49
+ /**
50
+ * @param {number | string | undefined} value
51
+ * @returns {number}
52
+ */
53
+ const _ttl = (value) => {
54
+ if (value === undefined) return 0;
55
+ if (typeof value === "number") return Math.max(0, value);
56
+ const ms = parseDuration(value);
57
+ if (ms === null) throw new TypeError(`Invalid ttl: ${value}`);
58
+ return ms;
59
+ };
60
+
61
+ /**
62
+ * An in-memory cache with a size limit and expiry.
63
+ *
64
+ * @template K
65
+ * @template V
66
+ */
67
+ class Cache {
68
+ /** @type {Map<K, { value: V, expires: number, ttl: number }>} */
69
+ #map = new Map();
70
+ /** @type {Map<K, Promise<V>>} */
71
+ #loading = new Map();
72
+ #max;
73
+ #ttl;
74
+ #updateAgeOnGet;
75
+ /** @type {((key: K, value: V, reason: CacheRemovalReason) => void) | undefined} */
76
+ #onRemove;
77
+ #stats = { hits: 0, misses: 0, sets: 0, evictions: 0, expirations: 0 };
78
+
79
+ /**
80
+ * @example
81
+ * const cache = new nc.Cache({ max: 500, ttl: "1h", updateAgeOnGet: true });
82
+ *
83
+ * @param {CacheOptions<K, V>} [options]
84
+ */
85
+ constructor(options = {}) {
86
+ this.#max = Math.max(1, options.max ?? Infinity);
87
+ this.#ttl = _ttl(options.ttl);
88
+ this.#updateAgeOnGet = options.updateAgeOnGet ?? false;
89
+ this.#onRemove = options.onRemove;
90
+ }
91
+
92
+ /**
93
+ * @param {K} key
94
+ * @param {{ value: V }} entry
95
+ * @param {CacheRemovalReason} reason
96
+ */
97
+ #remove(key, entry, reason) {
98
+ this.#map.delete(key);
99
+ if (reason === "expire") this.#stats.expirations++;
100
+ if (reason === "evict") this.#stats.evictions++;
101
+ this.#onRemove?.(key, entry.value, reason);
102
+ }
103
+
104
+ /**
105
+ * The entry if it exists and is fresh. Expired entries are removed.
106
+ * @param {K} key
107
+ */
108
+ #fresh(key) {
109
+ const entry = this.#map.get(key);
110
+ if (!entry) return undefined;
111
+ if (entry.expires && entry.expires <= Date.now()) {
112
+ this.#remove(key, entry, "expire");
113
+ return undefined;
114
+ }
115
+ return entry;
116
+ }
117
+
118
+ /**
119
+ * Reads a value and marks it as recently used.
120
+ *
121
+ * @param {K} key
122
+ * @returns {V | undefined} `undefined` if missing or expired.
123
+ */
124
+ get(key) {
125
+ const entry = this.#fresh(key);
126
+ if (!entry) {
127
+ this.#stats.misses++;
128
+ return undefined;
129
+ }
130
+ this.#stats.hits++;
131
+ this.#map.delete(key);
132
+ if (this.#updateAgeOnGet && entry.ttl) entry.expires = Date.now() + entry.ttl;
133
+ this.#map.set(key, entry);
134
+ return entry.value;
135
+ }
136
+
137
+ /**
138
+ * Reads a value without counting it as a use or touching the stats.
139
+ *
140
+ * @param {K} key
141
+ * @returns {V | undefined}
142
+ */
143
+ peek(key) {
144
+ return this.#fresh(key)?.value;
145
+ }
146
+
147
+ /**
148
+ * Stores a value. When the cache is full, the least recently used entry
149
+ * goes.
150
+ *
151
+ * @example
152
+ * cache.set("token", token, { ttl: "15m" });
153
+ *
154
+ * @param {K} key
155
+ * @param {V} value
156
+ * @param {CacheSetOptions} [options]
157
+ * @returns {this}
158
+ */
159
+ set(key, value, options = {}) {
160
+ const ttl = options.ttl === undefined ? this.#ttl : _ttl(options.ttl);
161
+ const previous = this.#map.get(key);
162
+ if (previous) this.#remove(key, previous, "set");
163
+ this.#map.set(key, { value, ttl, expires: ttl ? Date.now() + ttl : 0 });
164
+ this.#stats.sets++;
165
+ while (this.#map.size > this.#max) {
166
+ const [oldestKey, oldest] = /** @type {[K, { value: V }]} */ (this.#map.entries().next().value);
167
+ this.#remove(oldestKey, oldest, "evict");
168
+ }
169
+ return this;
170
+ }
171
+
172
+ /**
173
+ * Returns the cached value, or calls `loader`, stores the result and
174
+ * returns it. Simultaneous calls for the same key share one `loader` call,
175
+ * and a failed load caches nothing.
176
+ *
177
+ * @example
178
+ * const profile = await cache.getOrSet(`profile:${id}`, () => api.fetchProfile(id), { ttl: "10m" });
179
+ *
180
+ * @param {K} key
181
+ * @param {() => V | PromiseLike<V>} loader
182
+ * @param {CacheSetOptions} [options]
183
+ * @returns {Promise<V>}
184
+ */
185
+ async getOrSet(key, loader, options = {}) {
186
+ const entry = this.#fresh(key);
187
+ if (entry) return /** @type {V} */ (this.get(key));
188
+ const inflight = this.#loading.get(key);
189
+ if (inflight) return inflight;
190
+ this.#stats.misses++;
191
+ const promise = Promise.resolve()
192
+ .then(loader)
193
+ .then((value) => {
194
+ this.set(key, value, options);
195
+ return value;
196
+ })
197
+ .finally(() => this.#loading.delete(key));
198
+ this.#loading.set(key, promise);
199
+ return promise;
200
+ }
201
+
202
+ /**
203
+ * Is there a fresh entry for this key? Doesn't count as a use.
204
+ *
205
+ * @param {K} key
206
+ * @returns {boolean}
207
+ */
208
+ has(key) {
209
+ return this.#fresh(key) !== undefined;
210
+ }
211
+
212
+ /**
213
+ * Removes an entry.
214
+ *
215
+ * @param {K} key
216
+ * @returns {boolean} `true` if there was one.
217
+ */
218
+ delete(key) {
219
+ const entry = this.#map.get(key);
220
+ if (!entry) return false;
221
+ this.#remove(key, entry, "delete");
222
+ return true;
223
+ }
224
+
225
+ /**
226
+ * Removes every entry. Stats are kept.
227
+ *
228
+ * @returns {void}
229
+ */
230
+ clear() {
231
+ for (const [key, entry] of [...this.#map]) this.#remove(key, entry, "clear");
232
+ }
233
+
234
+ /**
235
+ * Removes expired entries now instead of waiting for them to be read.
236
+ *
237
+ * @returns {number} How many were removed.
238
+ */
239
+ prune() {
240
+ let removed = 0;
241
+ const now = Date.now();
242
+ for (const [key, entry] of [...this.#map]) {
243
+ if (entry.expires && entry.expires <= now) {
244
+ this.#remove(key, entry, "expire");
245
+ removed++;
246
+ }
247
+ }
248
+ return removed;
249
+ }
250
+
251
+ /**
252
+ * Time left before an entry expires.
253
+ *
254
+ * @param {K} key
255
+ * @returns {number | undefined} Milliseconds, `Infinity` if it never expires, `undefined` if missing.
256
+ */
257
+ ttl(key) {
258
+ const entry = this.#fresh(key);
259
+ if (!entry) return undefined;
260
+ return entry.expires ? entry.expires - Date.now() : Infinity;
261
+ }
262
+
263
+ /**
264
+ * Wraps a function so its results are cached here.
265
+ *
266
+ * @example
267
+ * const getWeather = cache.wrap((city) => api.weather(city), { key: (city) => city.toLowerCase(), ttl: "30m" });
268
+ * await getWeather("Paris");
269
+ *
270
+ * @template {any[]} A
271
+ * @param {(...args: A) => V | PromiseLike<V>} fn
272
+ * @param {{ key?: (...args: A) => K, ttl?: number | string }} [options] `key` builds the cache key from the arguments; by default they're turned into JSON.
273
+ * @returns {(...args: A) => Promise<V>}
274
+ */
275
+ wrap(fn, options = {}) {
276
+ return (...args) => {
277
+ const key = options.key ? options.key(...args) : /** @type {K} */ (/** @type {unknown} */ (JSON.stringify(args)));
278
+ return this.getOrSet(key, () => fn(...args), { ttl: options.ttl });
279
+ };
280
+ }
281
+
282
+ /** Number of entries, including expired ones not yet removed. */
283
+ get size() {
284
+ return this.#map.size;
285
+ }
286
+
287
+ /** Hit and miss counts. */
288
+ get stats() {
289
+ const total = this.#stats.hits + this.#stats.misses;
290
+ return /** @type {CacheStats} */ ({ ...this.#stats, hitRate: total ? this.#stats.hits / total : 0 });
291
+ }
292
+
293
+ /**
294
+ * Fresh keys, least recently used first.
295
+ * @returns {K[]}
296
+ */
297
+ keys() {
298
+ this.prune();
299
+ return [...this.#map.keys()];
300
+ }
301
+
302
+ /**
303
+ * Fresh values, least recently used first.
304
+ * @returns {V[]}
305
+ */
306
+ values() {
307
+ this.prune();
308
+ return [...this.#map.values()].map((entry) => entry.value);
309
+ }
310
+
311
+ /**
312
+ * Fresh `[key, value]` pairs, least recently used first.
313
+ * @returns {Array<[K, V]>}
314
+ */
315
+ entries() {
316
+ this.prune();
317
+ return [...this.#map].map(([key, entry]) => /** @type {[K, V]} */ ([key, entry.value]));
318
+ }
319
+
320
+ /**
321
+ * Lets you write `for (const [key, value] of cache)`.
322
+ * @returns {IterableIterator<[K, V]>}
323
+ */
324
+ [Symbol.iterator]() {
325
+ return this.entries()[Symbol.iterator]();
326
+ }
327
+ }
328
+
329
+ module.exports = Cache;