-
Notifications
You must be signed in to change notification settings - Fork 0
/
Composite.mjs
220 lines (220 loc) · 9.1 KB
/
Composite.mjs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
/**
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* @license MPL-2.0
* @author Alexander J. Vincent <ajvincent@gmail.com>
* @copyright © 2021-2022 Alexander J. Vincent
* @file
* This transforms multiple object keys into a "weak key" object. The weak arguments
* in WeakKeyComposer.prototype.getKey() are the only guarantees that the weak key
* will continue to exist: if one of the weak arguments is no longer reachable,
* the weak key is subject to garbage collection.
*/
import KeyHasher from "./Hasher.mjs";
class WeakKey {
constructor() {
Object.freeze(this);
}
}
class FinalizerKey {
constructor() {
Object.freeze(this);
}
}
/**
* @private
* @classdesc
* Internally, there are two levels of keys:
* finalizerKey is a frozen object which never leaves this module.
* weakKey is the actual key we give to the caller.
*/
class WeakKeyPropertyBag {
finalizerKeyRef;
weakKeyRef;
hash;
strongRefSet;
strongRef;
/**
* @param {FinalizerKey} finalizerKey The finalizer key for deleting the weak key object.
* @param {WeakKey} weakKey The weak key object.
* @param {hash} hash A hash of all the arguments from a KeyHasher.
* @param {*[]} strongArguments The list of strong arguments.
*/
constructor(finalizerKey, weakKey, hash, strongArguments) {
this.finalizerKeyRef = new WeakRef(finalizerKey);
this.weakKeyRef = new WeakRef(weakKey);
this.hash = hash;
if (strongArguments.length > 1) {
this.strongRefSet = new Set(strongArguments);
}
else if (strongArguments.length === 1) {
this.strongRef = strongArguments[0];
}
}
}
/**
* The weak key composer.
*
* @public
* @classdesc
*
* Each weak argument, through #keyFinalizer, holds a strong reference to finalizerKey.
* #finalizerToPublic maps from finalizerKey to weakKey.
* #weakKeyPropertyMap maps from weakKey to an instance of WeakKeyPropertyBag,
* which holds weak references to finalizerKey and weakKey.
* #hashToPropertyMap maps from a hash to the same instance of WeakKeyPropertyBag.
*
* Shorthand:
* weakArg => finalizerKey => weakKey => WeakKeyPropertyBag -> the same weakKey, the same finalizerKey, hash -> the same WeakKeyPropertyBag
*/
export default class WeakKeyComposer {
/** @type {string[]} @constant */
#weakArgList;
/** @type {string[]} @constant */
#strongArgList;
/** @type {KeyHasher} @constant */
#keyHasher;
/** @type {FinalizationRegistry} @constant */
#keyFinalizer = new FinalizationRegistry(finalizerKey => this.#deleteByFinalizerKey(finalizerKey));
/** @type {WeakMap<FinalizerKey, WeakKey>} */
#finalizerToPublic = new WeakMap;
/** @type {WeakMap<WeakKey, WeakKeyPropertyBag>} @constant */
#weakKeyPropertyMap = new WeakMap;
/** @type {Map<hash, WeakKeyPropertyBag>} */
#hashToPropertyMap = new Map;
/**
* @param {string[]} weakArgList The list of weak argument names.
* @param {string[]} strongArgList The list of strong argument names.
*/
constructor(weakArgList, strongArgList = []) {
if (new.target !== WeakKeyComposer)
throw new Error("You cannot subclass WeakKeyComposer!");
if (!Array.isArray(weakArgList) || (weakArgList.length === 0))
throw new Error("weakArgList must be a string array of at least one argument!");
if (!Array.isArray(strongArgList))
throw new Error("strongArgList must be a string array!");
// require all arguments be unique strings
{
const allArgs = weakArgList.concat(strongArgList);
if (!allArgs.every(arg => (typeof arg === "string") && (arg.length > 0)))
throw new Error("weakArgList and strongArgList can only contain non-empty strings!");
const argSet = new Set(allArgs);
if (argSet.size !== allArgs.length)
throw new Error("There is a duplicate argument among weakArgList and strongArgList!");
}
this.#weakArgList = weakArgList.slice();
this.#strongArgList = strongArgList.slice();
this.#keyHasher = new KeyHasher();
Object.freeze(this);
}
/**
* Get an unique key for an ordered set of weak and strong arguments. Create it if there isn't one.
*
* @param {object[]} weakArguments The list of weak arguments.
* @param {any[]} strongArguments The list of strong arguments.
* @returns {WeakKey} The key.
* @public
*/
getKey(weakArguments, strongArguments) {
if (!this.isValidForKey(weakArguments, strongArguments))
throw new Error("Argument lists do not form a valid key!");
const hash = this.#keyHasher.getHash(...weakArguments.concat(strongArguments));
let properties = this.#hashToPropertyMap.get(hash);
if (properties) {
// Each weak argument indirectly holds a strong reference on the weak key.
return properties.weakKeyRef.deref();
}
const finalizerKey = new FinalizerKey;
const weakKey = new WeakKey;
properties = new WeakKeyPropertyBag(finalizerKey, weakKey, hash, strongArguments);
weakArguments.forEach(weakArg => this.#keyFinalizer.register(weakArg, finalizerKey, finalizerKey));
this.#finalizerToPublic.set(finalizerKey, weakKey);
this.#weakKeyPropertyMap.set(weakKey, properties);
this.#hashToPropertyMap.set(hash, properties);
return weakKey;
}
/**
* Determine if an unique key for an ordered set of weak and strong arguments exists.
*
* @param {object[]} weakArguments The list of weak arguments.
* @param {any[]} strongArguments The list of strong arguments.
* @returns {boolean} True if the key exists.
* @public
*/
hasKey(weakArguments, strongArguments) {
const fullArgList = weakArguments.concat(strongArguments);
const hash = this.#keyHasher.getHashIfExists(...fullArgList);
return hash ? this.#hashToPropertyMap.has(hash) : false;
}
/**
* Get the unique key for an ordered set of weak and strong arguments if it exists.
*
* @param {object[]} weakArguments The list of weak arguments.
* @param {any[]} strongArguments The list of strong arguments.
* @returns {WeakKey?} The WeakKey, or null if there isn't one already.
* @public
*/
getKeyIfExists(weakArguments, strongArguments) {
const hash = this.#keyHasher.getHashIfExists(...weakArguments, ...strongArguments);
if (!hash)
return null;
const properties = this.#hashToPropertyMap.get(hash);
// Each weak argument indirectly holds a strong reference on the weak key.
return properties ? properties.weakKeyRef.deref() : null;
}
/**
* Delete an unique key for an ordered set of weak and strong arguments.
*
* @param {object[]} weakArguments The list of weak arguments.
* @param {any[]} strongArguments The list of strong arguments.
* @returns {boolean} True if the key was deleted, false if the key wasn't found.
* @public
*/
deleteKey(weakArguments, strongArguments) {
void weakArguments;
void strongArguments;
const fullArgList = weakArguments.concat(strongArguments);
const hash = this.#keyHasher.getHashIfExists(...fullArgList);
if (!hash)
return false;
const properties = this.#hashToPropertyMap.get(hash);
if (!properties)
return false;
// Each weak argument indirectly holds a strong reference on the finalizer key.
const finalizerKey = properties.finalizerKeyRef.deref();
return this.#deleteByFinalizerKey(finalizerKey);
}
#deleteByFinalizerKey(finalizerKey) {
const weakKey = this.#finalizerToPublic.get(finalizerKey);
if (!weakKey)
return false;
// Each weak argument indirectly holds a strong reference on the property bag.
const properties = this.#weakKeyPropertyMap.get(weakKey);
this.#keyFinalizer.unregister(finalizerKey);
this.#finalizerToPublic.delete(finalizerKey);
this.#weakKeyPropertyMap.delete(weakKey);
this.#hashToPropertyMap.delete(properties.hash);
return true;
}
/**
* Determine if the set of arguments is valid to form a key.
*
* @param {object[]} weakArguments The list of weak arguments.
* @param {any[]} strongArguments The list of strong arguments.
* @returns {boolean} True if the arguments may lead to a WeakKey.
*/
isValidForKey(weakArguments, strongArguments) {
if (weakArguments.length !== this.#weakArgList.length)
return false;
if (weakArguments.some(arg => Object(arg) !== arg))
return false;
if (strongArguments.length !== this.#strongArgList.length)
return false;
return true;
}
}
Object.freeze(WeakKeyComposer);
Object.freeze(WeakKeyComposer.prototype);
//# sourceMappingURL=Composite.mjs.map