@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 +1 -0
- package/package.json +8 -2
- package/src/README.md +41 -5
- package/src/cache.js +103 -0
- package/src/index.md +6 -0
- package/src/registry.js +21 -6
- package/types/cache.d.ts +39 -0
- package/types/registry.d.ts +17 -4
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.
|
|
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.
|
|
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/
|
|
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
|
|
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
|
|
138
|
-
|
|
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
package/src/registry.js
CHANGED
|
@@ -8,11 +8,10 @@
|
|
|
8
8
|
/**
|
|
9
9
|
* @template [Key=unknown]
|
|
10
10
|
* @template [Value=unknown]
|
|
11
|
-
* @typedef {
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
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
|
|
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);
|
package/types/cache.d.ts
ADDED
|
@@ -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
|
+
}
|
package/types/registry.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Map with optional key/value validation and
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
};
|