@webreflection/utils 0.2.18 → 0.2.20

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/README.md CHANGED
@@ -11,6 +11,7 @@ A curated, *TypeScript*-friendly [collection](./src/) of utilities:
11
11
  * **[ascii](./src#ascii)** - basic string-to-buffer conversion without validation
12
12
  * **[bound-once](./src#bound-once)** - retrieve unique bound methods per realm
13
13
  * **[bound](./src#bound)** - retrieve one-off bound methods
14
+ * **[cache](./src#cache)** - use a temporal `Map` to reuse repeated key lookups within the same tick, or for a short timed window
14
15
  * **[iterable](./src#iterable)** - make plain objects iterable as `Object.entries(object)` pairs, without touching objects that are already iterable
15
16
  * **[json-storage](./src#json-storage)** - use `localStorage` or `sessionStorage` through a JSON-aware, iterable, *Map*-friendly API
16
17
  * **[registry](./src#registry)** - use a `Map`-like API with key/value validation and duplicate-key protection by default
package/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "@webreflection/utils",
3
- "version": "0.2.18",
3
+ "version": "0.2.20",
4
4
  "type": "module",
5
5
  "types": {
6
6
  "./all": "./types/all.d.ts",
7
7
  "./ascii": "./types/ascii.d.ts",
8
8
  "./bound-once": "./types/bound-once.d.ts",
9
9
  "./bound": "./types/bound.d.ts",
10
+ "./cache": "./types/cache.d.ts",
10
11
  "./iterable": "./types/iterable.d.ts",
11
12
  "./json-storage": "./types/json-storage.d.ts",
12
13
  "./registry": "./types/registry.d.ts",
@@ -31,6 +32,10 @@
31
32
  "types": "./types/bound.d.ts",
32
33
  "import": "./src/bound.js"
33
34
  },
35
+ "./cache": {
36
+ "types": "./types/cache.d.ts",
37
+ "import": "./src/cache.js"
38
+ },
34
39
  "./iterable": {
35
40
  "types": "./types/iterable.d.ts",
36
41
  "import": "./src/iterable.js"
@@ -62,6 +67,7 @@
62
67
  "ascii",
63
68
  "bound-once",
64
69
  "bound",
70
+ "cache",
65
71
  "iterable",
66
72
  "json-storage",
67
73
  "registry",
@@ -96,7 +102,7 @@
96
102
  "bugs": {
97
103
  "url": "https://github.com/WebReflection/utils/issues"
98
104
  },
99
- "homepage": "https://github.com/WebReflection/utils#readme",
105
+ "homepage": "https://webreflection.github.io/utils/",
100
106
  "overrides": {
101
107
  "c8": {
102
108
  "yargs": "^18.0.0"
package/src/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @webreflection/utils
2
2
 
3
- Each utility can be loaded from a *CDN* via either `https://esm.run/@webreflection/utils/{{UTILITY}}` or `https://cdn.jsdelivr.net/npm/@webreflection/utils/src/{{UTILITY}}.js`.
3
+ Each utility can be loaded from a *CDN* via either `https://esm.run/@webreflection/utils/UTILITY` or `https://cdn.jsdelivr.net/npm/@webreflection/utils/src/UTILITY.js`.
4
4
 
5
5
 
6
6
  This document describes each utility separately.
@@ -53,7 +53,7 @@ whatever argument limit your runtime has for `String.fromCharCode`.
53
53
 
54
54
  This is equivalent to **bound**, except each bound method is created only once. It is useful when bound method identity must be preserved across multiple calls.
55
55
 
56
- This variant uses **sticky** to ensure that weakly referenced targets always produce the same bound method within the same realm.
56
+ This variant uses [sticky](#sticky) to ensure that weakly referenced targets always produce the same bound method within the same realm.
57
57
 
58
58
 
59
59
  ## bound
@@ -71,6 +71,35 @@ resolve(4);
71
71
  The **bound-once** variant ensures that repeated accesses, such as `boundOnce(Promise).all`, always return the same bound method.
72
72
 
73
73
 
74
+ ## cache
75
+
76
+ A temporal `Map` subclass for short-lived memoization. It keeps newly added
77
+ entries only until its scheduled cleanup runs, making it useful to reuse
78
+ expensive work for repeated access to the same key without keeping the value
79
+ around as a long-term cache.
80
+
81
+ ```js
82
+ import Cache from '@webreflection/utils/cache';
83
+
84
+ const users = new Cache;
85
+
86
+ const loadUser = id => users.getOrInsertComputed(
87
+ id,
88
+ id => fetch(`/users/${id}`).then(response => response.json())
89
+ );
90
+ ```
91
+
92
+ When the constructor `delay` is omitted, `0`, or less than `0`, cleanup is
93
+ queued as a microtask, so same-tick lookups can share the stored value and the
94
+ map clears itself before the next task. Pass a positive delay, such as
95
+ `new Cache(100)`, to keep entries until a timer removes them instead.
96
+
97
+ Use `getOrInsert(key, value)` or `getOrInsertComputed(key, callback)` when the
98
+ value should only be stored if missing. Use `put(key, value)` for the faster
99
+ `cache.get(key) ?? cache.put(key, value)` pattern when duplicate queue entries
100
+ are acceptable.
101
+
102
+
74
103
  ## iterable
75
104
 
76
105
  Ensures an object can be consumed by `for...of`, spread, `Array.from`, and
@@ -134,8 +163,10 @@ long as it provides compatible `parse(source)` and `stringify(value)` methods.
134
163
  ## registry
135
164
 
136
165
  A `Map` subclass that validates keys and values before storing them. By default,
137
- keys must be unique, so setting the same key twice throws a `TypeError`; pass
138
- `unique: false` when replacement should behave like a regular `Map`.
166
+ keys are permanent: setting the same key twice throws a `TypeError`, and
167
+ deleting an existing key also throws so it cannot be re-appended later. Pass
168
+ `unique: false` when replacement and deletion should behave like a regular
169
+ `Map`.
139
170
 
140
171
  ```js
141
172
  import Registry from '@webreflection/utils/registry';
@@ -171,10 +202,15 @@ const mutable = new Registry(
171
202
 
172
203
  console.log(mutable.get('answer'));
173
204
  // 42
205
+
206
+ console.log(mutable.delete('answer'));
207
+ // true
174
208
  ```
175
209
 
176
210
  Initial iterable entries are validated with the same rules used by `set()`, so
177
- invalid keys, invalid values, or duplicate keys fail during construction.
211
+ invalid keys, invalid values, or duplicate keys fail during construction. With
212
+ the default `unique: true` behavior, only missing keys can be passed to
213
+ `delete()` without throwing, in which case it returns `false` like `Map`.
178
214
 
179
215
 
180
216
  ## shared-array-buffer
package/src/cache.js ADDED
@@ -0,0 +1,103 @@
1
+ // @ts-check
2
+
3
+ /**
4
+ * @template K,V
5
+ * @type {(self: Cache<K,V>) => void}
6
+ */
7
+ let cleanup;
8
+
9
+ /**
10
+ * @template K,V
11
+ * @extends Map<K,V>
12
+ */
13
+ export default class Cache extends Map {
14
+ static {
15
+ cleanup = self => {
16
+ self.#run = true;
17
+ self.#queue.splice(0).forEach(self.delete, self);
18
+ };
19
+ }
20
+
21
+ /** @type {number} */
22
+ #delay;
23
+
24
+ /** @type {K[]} */
25
+ #queue = [];
26
+
27
+ /** @type {boolean} */
28
+ #run = true;
29
+
30
+ /**
31
+ * @param {K} key
32
+ */
33
+ #push(key) {
34
+ this.#queue.push(key);
35
+ if (this.#run) {
36
+ this.#run = false;
37
+ if (this.#delay < 1) {
38
+ // @ts-ignore
39
+ queueMicrotask(() => cleanup(this));
40
+ }
41
+ else {
42
+ // @ts-ignore
43
+ setTimeout(cleanup, this.#delay, this);
44
+ }
45
+ }
46
+ }
47
+
48
+ clear() {
49
+ this.#queue.splice(0);
50
+ super.clear();
51
+ }
52
+
53
+ /**
54
+ * @param {number} [delay]
55
+ */
56
+ constructor(delay = 0) {
57
+ super();
58
+ this.#delay = delay;
59
+ }
60
+
61
+ /**
62
+ * @param {K} key
63
+ * @param {V} value
64
+ * @returns
65
+ */
66
+ getOrInsert(key, value) {
67
+ return this.has(key) ? this.get(key) : this.put(key, value);
68
+ }
69
+
70
+ /**
71
+ * @param {K} key
72
+ * @param {(key: K) => V} callback
73
+ * @returns
74
+ */
75
+ getOrInsertComputed(key, callback) {
76
+ return this.has(key) ? this.get(key) : this.put(key, callback(key));
77
+ }
78
+
79
+ /**
80
+ * This method is slightly faster than `getOrInsert` or `getOrInsertComputed`
81
+ * but it does not check if the key already exists, possibly pushing it twice.
82
+ * However, doing `ref.get(key) ?? ref.put(key, value)` provides same functionality as
83
+ * `ref.getOrInsert(key, value)` or `ref.getOrInsertComputed(key, callback)`.
84
+ * @param {K} key
85
+ * @param {V} value
86
+ * @returns
87
+ */
88
+ put(key, value) {
89
+ this.#push(key);
90
+ super.set(key, value);
91
+ return value;
92
+ }
93
+
94
+ /**
95
+ * @param {K} key
96
+ * @param {V} value
97
+ * @returns
98
+ */
99
+ set(key, value) {
100
+ if (!this.has(key)) this.#push(key);
101
+ return super.set(key, value);
102
+ }
103
+ }
package/src/index.md ADDED
@@ -0,0 +1,6 @@
1
+ ---
2
+ title: Utilities
3
+ permalink: /src/
4
+ ---
5
+
6
+ {% include_relative README.md %}
package/src/registry.js CHANGED
@@ -8,11 +8,10 @@
8
8
  /**
9
9
  * @template [Key=unknown]
10
10
  * @template [Value=unknown]
11
- * @typedef {{
12
- * key?: RegistryValidator<Key>,
13
- * value?: RegistryValidator<Value>,
14
- * unique?: boolean,
15
- * }} RegistryOptions
11
+ * @typedef {object} RegistryOptions
12
+ * @property {RegistryValidator<Key>} [key] Accepts or rejects candidate keys.
13
+ * @property {RegistryValidator<Value>} [value] Accepts or rejects candidate values.
14
+ * @property {boolean} [unique] When true, stored keys cannot be replaced or removed.
16
15
  */
17
16
 
18
17
  /**
@@ -30,7 +29,9 @@ const defaultOptions = {
30
29
  };
31
30
 
32
31
  /**
33
- * Map with optional key/value validation and duplicate-key protection.
32
+ * Map with optional key/value validation and permanent-key protection.
33
+ *
34
+ * When unique keys are enabled, stored keys cannot be replaced or deleted.
34
35
  *
35
36
  * @template [Key=unknown]
36
37
  * @template [Value=unknown]
@@ -49,6 +50,7 @@ export default class Registry extends Map {
49
50
  /**
50
51
  * @param {Iterable<[Key, Value]> | null} [iterable]
51
52
  * @param {RegistryOptions<Key, Value>} [options]
53
+ * @throws {TypeError} If an initial entry violates validation or uniqueness.
52
54
  */
53
55
  constructor(
54
56
  iterable,
@@ -66,10 +68,23 @@ export default class Registry extends Map {
66
68
  for (const [key, value] of iterable ?? []) this.set(key, value);
67
69
  }
68
70
 
71
+ /**
72
+ * Remove a key when unique-key protection is disabled.
73
+ *
74
+ * @param {Key} key
75
+ * @returns {boolean}
76
+ * @throws {TypeError} If the key exists and unique-key protection is enabled.
77
+ */
78
+ delete(key) {
79
+ if (this.#unique && super.has(key)) fail('Unable to remove key:', key);
80
+ return super.delete(key);
81
+ }
82
+
69
83
  /**
70
84
  * @param {Key} key
71
85
  * @param {Value} value
72
86
  * @returns {this}
87
+ * @throws {TypeError} If the key, value, or uniqueness check fails.
73
88
  */
74
89
  set(key, value) {
75
90
  if (!this.#key(key)) fail('Invalid key:', key);
@@ -0,0 +1,39 @@
1
+ /**
2
+ * @template K,V
3
+ * @extends Map<K,V>
4
+ */
5
+ export default class Cache<K, V> extends Map<K, V> {
6
+ /**
7
+ * @param {number} [delay]
8
+ */
9
+ constructor(delay?: number);
10
+ /**
11
+ * @param {K} key
12
+ * @param {V} value
13
+ * @returns
14
+ */
15
+ getOrInsert(key: K, value: V): V | undefined;
16
+ /**
17
+ * @param {K} key
18
+ * @param {(key: K) => V} callback
19
+ * @returns
20
+ */
21
+ getOrInsertComputed(key: K, callback: (key: K) => V): V | undefined;
22
+ /**
23
+ * This method is slightly faster than `getOrInsert` or `getOrInsertComputed`
24
+ * but it does not check if the key already exists, possibly pushing it twice.
25
+ * However, doing `ref.get(key) ?? ref.put(key, value)` provides same functionality as
26
+ * `ref.getOrInsert(key, value)` or `ref.getOrInsertComputed(key, callback)`.
27
+ * @param {K} key
28
+ * @param {V} value
29
+ * @returns
30
+ */
31
+ put(key: K, value: V): V;
32
+ /**
33
+ * @param {K} key
34
+ * @param {V} value
35
+ * @returns
36
+ */
37
+ set(key: K, value: V): this;
38
+ #private;
39
+ }
@@ -1,5 +1,7 @@
1
1
  /**
2
- * Map with optional key/value validation and duplicate-key protection.
2
+ * Map with optional key/value validation and permanent-key protection.
3
+ *
4
+ * When unique keys are enabled, stored keys cannot be replaced or deleted.
3
5
  *
4
6
  * @template [Key=unknown]
5
7
  * @template [Value=unknown]
@@ -9,19 +11,30 @@ export default class Registry<Key = unknown, Value = unknown> extends Map<Key, V
9
11
  /**
10
12
  * @param {Iterable<[Key, Value]> | null} [iterable]
11
13
  * @param {RegistryOptions<Key, Value>} [options]
14
+ * @throws {TypeError} If an initial entry violates validation or uniqueness.
12
15
  */
13
16
  constructor(iterable?: Iterable<[Key, Value]> | null, options?: RegistryOptions<Key, Value>);
14
17
  /**
15
18
  * @param {Key} key
16
19
  * @param {Value} value
17
20
  * @returns {this}
21
+ * @throws {TypeError} If the key, value, or uniqueness check fails.
18
22
  */
19
23
  set(key: Key, value: Value): this;
20
24
  #private;
21
25
  }
22
26
  export type RegistryValidator<Type> = ((value: unknown) => value is Type) | ((value: unknown) => boolean);
23
27
  export type RegistryOptions<Key = unknown, Value = unknown> = {
24
- key?: RegistryValidator<Key>;
25
- value?: RegistryValidator<Value>;
26
- unique?: boolean;
28
+ /**
29
+ * Accepts or rejects candidate keys.
30
+ */
31
+ key?: RegistryValidator<Key> | undefined;
32
+ /**
33
+ * Accepts or rejects candidate values.
34
+ */
35
+ value?: RegistryValidator<Value> | undefined;
36
+ /**
37
+ * When true, stored keys cannot be replaced or removed.
38
+ */
39
+ unique?: boolean | undefined;
27
40
  };