@rhombus-toolkit/collections 2.1.0 → 3.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.
@@ -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,58 @@ 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> {
51
61
  #private;
52
- /** An existing key keeps its place in iteration order, as with `Map`. */
53
- set(key: K, value: V): this;
54
- delete(key: K): boolean;
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;
55
65
  get(key: K): V | undefined;
56
66
  has(key: K): boolean;
57
- clear(): void;
58
- 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. */
60
- get size(): number;
61
- entries(): MapIterator<[K, V]>;
62
- keys(): MapIterator<K>;
63
- values(): MapIterator<V>;
64
- [Symbol.iterator](): MapIterator<[K, V]>;
65
- readonly [Symbol.toStringTag]: "KindaWeakMap";
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;
66
104
  }
67
105
 
68
- export { AutoStack, ImmutableLinkedList, KindaWeakMap };
106
+ export { AutoStack, ImmutableLinkedList, KindaWeakMap, MultiKeyWeakMap };
@@ -81,67 +81,146 @@ 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 {
85
- #map = new Map;
86
- #registry = new FinalizationRegistry(({ key, ref }) => {
87
- if (this.#map.get(key) === ref) {
88
- this.#map.delete(key);
89
- }
99
+ #weak = new WeakMap;
100
+ #weakSize = 0;
101
+ #collected = new FinalizationRegistry(() => {
102
+ this.#weakSize--;
90
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
+ }
91
123
  set(key, value) {
92
- const held = this.#map.get(key);
93
- if (held) {
94
- this.#registry.unregister(held);
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);
95
132
  }
96
- const ref = new WeakRef(value);
97
- this.#registry.register(value, { key, ref }, ref);
98
- this.#map.set(key, ref);
99
133
  return this;
100
134
  }
101
135
  delete(key) {
102
- const held = this.#map.get(key);
103
- if (!held) {
104
- return false;
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;
105
143
  }
106
- this.#map.delete(key);
107
- this.#registry.unregister(held);
108
- return held.deref() !== undefined;
144
+ return this.#strong?.delete(key) ?? false;
109
145
  }
110
- get(key) {
111
- return this.#map.get(key)?.deref();
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;
112
153
  }
113
- has(key) {
114
- return this.get(key) !== undefined;
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;
115
162
  }
116
- clear() {
117
- this.#map.forEach((ref) => this.#registry.unregister(ref));
118
- this.#map.clear();
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";
119
174
  }
120
- forEach(callbackfn, thisArg) {
121
- for (const [key, value] of this) {
122
- callbackfn.call(thisArg, value, key, this);
123
- }
175
+ #findNodeAt(keys) {
176
+ return keys.reduce((node, key) => node?.next.get(key), this._root);
124
177
  }
125
- get size() {
126
- return this.entries().reduce((count) => count + 1, 0);
178
+ #ensureNodeAt(keys) {
179
+ return keys.reduce((node, key) => node.next.getOrInsertComputed(key, () => new Node), this._root);
127
180
  }
128
- entries() {
129
- const held = this.#map.entries().map(([key, ref]) => [key, ref.deref()]);
130
- return held.filter((entry) => entry[1] !== undefined);
181
+ get(keys) {
182
+ return this.#findNodeAt(keys)?.entry?.value;
131
183
  }
132
- keys() {
133
- return this.entries().map(([key]) => key);
184
+ has(keys) {
185
+ return this.#findNodeAt(keys)?.entry !== undefined;
134
186
  }
135
- values() {
136
- return this.entries().map(([, value]) => value);
187
+ set(keys, value) {
188
+ this.#ensureNodeAt(keys).entry = { value };
189
+ return this;
137
190
  }
138
- [Symbol.iterator]() {
139
- return this.entries();
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;
140
219
  }
141
- [Symbol.toStringTag] = "KindaWeakMap";
142
220
  }
143
221
  export {
144
222
  AutoStack,
145
223
  ImmutableLinkedList,
146
- KindaWeakMap
224
+ KindaWeakMap,
225
+ MultiKeyWeakMap
147
226
  };
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.0.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).",
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