@rhombus-toolkit/collections 2.1.0 → 3.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.
@@ -1,3 +1,5 @@
1
+ import { Func } from '@rhombus-toolkit/types';
2
+
1
3
  /**
2
4
  * A stack whose entries leave when their scope does.
3
5
  *
@@ -47,22 +49,84 @@ declare class ImmutableLinkedList<T> implements Iterable<T> {
47
49
  tailToHead(): readonly T[];
48
50
  }
49
51
 
50
- declare class KindaWeakMap<K, V extends WeakKey> {
52
+ /**
53
+ * A map that holds each key weakly when it can: objects, functions, and unregistered symbols go
54
+ * in a `WeakMap`; everything else goes in a `Map`.
55
+ *
56
+ * @remarks
57
+ * An entry under a weakly held key goes when the key is collected; one under any other key lives
58
+ * as long as the map.
59
+ */
60
+ declare class KindaWeakMap<in out K = unknown, in out V = unknown> {
61
+ #private;
62
+ /** Entries held right now; a collected key still counts until its cleanup has run, so this can read high, never low. */
63
+ get size(): number;
64
+ get [Symbol.toStringTag](): string;
65
+ get(key: K): V | undefined;
66
+ has(key: K): boolean;
67
+ set(key: K, value: V): this;
68
+ delete(key: K): boolean;
69
+ /** The entry under `key`, storing `value` there first when there is none. */
70
+ getOrInsert(key: K, value: V): V;
71
+ /** The entry under `key`, storing what `compute` answers there first when there is none. A throw stores nothing. */
72
+ getOrInsertComputed(key: K, compute: Func<[K], V>): V;
73
+ }
74
+
75
+ /** Where a tuple ends: the entry stored there, and the node each further key leads to. */
76
+ declare class Node<Value> {
77
+ readonly next: KindaWeakMap<unknown, Node<Value>>;
78
+ /** Boxed, so a stored `undefined` is an entry rather than an absence. */
79
+ entry: {
80
+ value: Value;
81
+ } | undefined;
82
+ }
83
+ /**
84
+ * A `KindaWeakMap` keyed by a tuple, matched key by key rather than by the tuple's identity.
85
+ *
86
+ * @remarks
87
+ * Tuples of every length share one map, the empty tuple included. An entry goes when any weakly
88
+ * held key in its tuple is collected; a tuple of primitives lives as long as the map.
89
+ */
90
+ declare class MultiKeyWeakMap<in out Keys extends readonly unknown[] = unknown[], in out Value = unknown> implements WeakMap<Keys, Value> {
91
+ #private;
92
+ /** @internal The spec reads it to see pruning. */
93
+ readonly _root: Node<Value>;
94
+ get [Symbol.toStringTag](): string;
95
+ get(keys: Keys): Value | undefined;
96
+ has(keys: Keys): boolean;
97
+ set(keys: Keys, value: Value): this;
98
+ /** Longer tuples through `keys` keep their entries; a prefix left holding nothing is released. */
99
+ delete(keys: Keys): boolean;
100
+ /** The entry at `keys`, storing `value` there first when there is none. */
101
+ getOrInsert(keys: Keys, value: Value): Value;
102
+ /** The entry at `keys`, storing what `compute` answers there first when there is none. A throw stores nothing. */
103
+ getOrInsertComputed(keys: Keys, compute: Func<[Keys], Value>): Value;
104
+ }
105
+
106
+ /**
107
+ * A `Map` whose values are held weakly, so an entry is removed once nothing else holds its value.
108
+ *
109
+ * @remarks
110
+ * Not a `Map` by `instanceof`.
111
+ */
112
+ declare class WeakValuedMap<K, V extends WeakKey> implements Map<K, V> {
51
113
  #private;
52
- /** An existing key keeps its place in iteration order, as with `Map`. */
114
+ constructor();
115
+ constructor(entries: Iterable<readonly [K, V]> | null);
116
+ /** Re-setting an existing key leaves its place in iteration order unchanged, as with `Map`. */
53
117
  set(key: K, value: V): this;
54
118
  delete(key: K): boolean;
55
119
  get(key: K): V | undefined;
56
120
  has(key: K): boolean;
57
121
  clear(): void;
58
122
  forEach(callbackfn: (value: V, key: K, map: this) => void, thisArg?: any): void;
59
- /** Counts only entries whose value is still alive, so it walks every ref. */
123
+ /** Computed by walking every entry and counting those whose value is still alive. */
60
124
  get size(): number;
61
125
  entries(): MapIterator<[K, V]>;
62
126
  keys(): MapIterator<K>;
63
127
  values(): MapIterator<V>;
64
128
  [Symbol.iterator](): MapIterator<[K, V]>;
65
- readonly [Symbol.toStringTag]: "KindaWeakMap";
129
+ readonly [Symbol.toStringTag]: "WeakValuedMap";
66
130
  }
67
131
 
68
- export { AutoStack, ImmutableLinkedList, KindaWeakMap };
132
+ export { AutoStack, ImmutableLinkedList, KindaWeakMap, MultiKeyWeakMap, WeakValuedMap };
@@ -81,13 +81,156 @@ function tailOf(link) {
81
81
  return tail;
82
82
  }
83
83
  // src/KindaWeakMap.ts
84
+ function isWeaklyHoldable(key) {
85
+ const type = typeof key;
86
+ if (type === "object") {
87
+ return key !== null;
88
+ }
89
+ if (type === "function") {
90
+ return true;
91
+ }
92
+ if (type === "symbol") {
93
+ return Symbol.keyFor(key) === undefined;
94
+ }
95
+ return false;
96
+ }
97
+
84
98
  class KindaWeakMap {
99
+ #weak = new WeakMap;
100
+ #weakSize = 0;
101
+ #collected = new FinalizationRegistry(() => {
102
+ this.#weakSize--;
103
+ });
104
+ #strong;
105
+ get size() {
106
+ return (this.#strong?.size ?? 0) + this.#weakSize;
107
+ }
108
+ get [Symbol.toStringTag]() {
109
+ return "KindaWeakMap";
110
+ }
111
+ get(key) {
112
+ if (isWeaklyHoldable(key)) {
113
+ return this.#weak.get(key);
114
+ }
115
+ return this.#strong?.get(key);
116
+ }
117
+ has(key) {
118
+ if (isWeaklyHoldable(key)) {
119
+ return this.#weak.has(key);
120
+ }
121
+ return this.#strong?.has(key) ?? false;
122
+ }
123
+ set(key, value) {
124
+ if (isWeaklyHoldable(key)) {
125
+ if (!this.#weak.has(key)) {
126
+ this.#weakSize++;
127
+ this.#collected.register(key, undefined, key);
128
+ }
129
+ this.#weak.set(key, value);
130
+ } else {
131
+ (this.#strong ??= new Map).set(key, value);
132
+ }
133
+ return this;
134
+ }
135
+ delete(key) {
136
+ if (isWeaklyHoldable(key)) {
137
+ if (!this.#weak.delete(key)) {
138
+ return false;
139
+ }
140
+ this.#weakSize--;
141
+ this.#collected.unregister(key);
142
+ return true;
143
+ }
144
+ return this.#strong?.delete(key) ?? false;
145
+ }
146
+ getOrInsert(key, value) {
147
+ const existing = this.get(key);
148
+ if (existing !== undefined || this.has(key)) {
149
+ return existing;
150
+ }
151
+ this.set(key, value);
152
+ return value;
153
+ }
154
+ getOrInsertComputed(key, compute) {
155
+ const existing = this.get(key);
156
+ if (existing !== undefined || this.has(key)) {
157
+ return existing;
158
+ }
159
+ const value = compute(key);
160
+ this.set(key, value);
161
+ return value;
162
+ }
163
+ }
164
+ // src/MultiKeyWeakMap.ts
165
+ class Node {
166
+ next = new KindaWeakMap;
167
+ entry;
168
+ }
169
+
170
+ class MultiKeyWeakMap {
171
+ _root = new Node;
172
+ get [Symbol.toStringTag]() {
173
+ return "MultiKeyWeakMap";
174
+ }
175
+ #findNodeAt(keys) {
176
+ return keys.reduce((node, key) => node?.next.get(key), this._root);
177
+ }
178
+ #ensureNodeAt(keys) {
179
+ return keys.reduce((node, key) => node.next.getOrInsertComputed(key, () => new Node), this._root);
180
+ }
181
+ get(keys) {
182
+ return this.#findNodeAt(keys)?.entry?.value;
183
+ }
184
+ has(keys) {
185
+ return this.#findNodeAt(keys)?.entry !== undefined;
186
+ }
187
+ set(keys, value) {
188
+ this.#ensureNodeAt(keys).entry = { value };
189
+ return this;
190
+ }
191
+ delete(keys) {
192
+ const path = [this._root];
193
+ for (const key of keys) {
194
+ const next = path[path.length - 1].next.get(key);
195
+ if (!next) {
196
+ return false;
197
+ }
198
+ path.push(next);
199
+ }
200
+ const leaf = path[path.length - 1];
201
+ if (leaf.entry === undefined) {
202
+ return false;
203
+ }
204
+ leaf.entry = undefined;
205
+ for (let depth = keys.length;depth > 0; depth--) {
206
+ const node = path[depth];
207
+ if (node.entry !== undefined || node.next.size) {
208
+ break;
209
+ }
210
+ path[depth - 1].next.delete(keys[depth - 1]);
211
+ }
212
+ return true;
213
+ }
214
+ getOrInsert(keys, value) {
215
+ return (this.#ensureNodeAt(keys).entry ??= { value }).value;
216
+ }
217
+ getOrInsertComputed(keys, compute) {
218
+ return (this.#ensureNodeAt(keys).entry ??= { value: compute(keys) }).value;
219
+ }
220
+ }
221
+ // src/WeakValuedMap.ts
222
+ class WeakValuedMap {
85
223
  #map = new Map;
86
224
  #registry = new FinalizationRegistry(({ key, ref }) => {
87
225
  if (this.#map.get(key) === ref) {
88
226
  this.#map.delete(key);
89
227
  }
90
228
  });
229
+ constructor(entries) {
230
+ for (const [key, value] of entries ?? []) {
231
+ this.set(key, value);
232
+ }
233
+ }
91
234
  set(key, value) {
92
235
  const held = this.#map.get(key);
93
236
  if (held) {
@@ -138,10 +281,12 @@ class KindaWeakMap {
138
281
  [Symbol.iterator]() {
139
282
  return this.entries();
140
283
  }
141
- [Symbol.toStringTag] = "KindaWeakMap";
284
+ [Symbol.toStringTag] = "WeakValuedMap";
142
285
  }
143
286
  export {
144
287
  AutoStack,
145
288
  ImmutableLinkedList,
146
- KindaWeakMap
289
+ KindaWeakMap,
290
+ MultiKeyWeakMap,
291
+ WeakValuedMap
147
292
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@rhombus-toolkit/collections",
3
- "version": "2.1.0",
4
- "description": "Collection data structures: AutoStack (entries leave with their scope), ImmutableLinkedList (persistent, both ends known), KindaWeakMap (strong keys, weak values).",
3
+ "version": "3.1.0",
4
+ "description": "Collection data structures: AutoStack (entries leave with their scope), ImmutableLinkedList (persistent, both ends known), KindaWeakMap (weak where it can, strong otherwise), MultiKeyWeakMap (a KindaWeakMap keyed by a tuple), WeakValuedMap (values held weakly).",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "https://github.com/rhombus-toolkit/ts.git",
@@ -23,6 +23,9 @@
23
23
  ],
24
24
  "keywords": [],
25
25
  "author": "Thomas Butler",
26
+ "dependencies": {
27
+ "@rhombus-toolkit/types": "4.0.1"
28
+ },
26
29
  "publishConfig": {
27
30
  "access": "public",
28
31
  "provenance": true