@codefast/di 0.9.0 → 0.10.1
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/CHANGELOG.md +165 -0
- package/README.md +6 -5
- package/dist/ambient/active-container.d.ts +14 -4
- package/dist/ambient/active-container.js +22 -1
- package/dist/container/binding-builders.d.ts +35 -13
- package/dist/container/binding-builders.js +174 -97
- package/dist/container/container.d.ts +10 -10
- package/dist/container/container.js +53 -36
- package/dist/core/binding-scope.d.ts +2 -2
- package/dist/core/binding.d.ts +42 -49
- package/dist/core/binding.js +12 -38
- package/dist/core/constraint-requirement.d.ts +1 -1
- package/dist/core/module.d.ts +3 -3
- package/dist/core/registry.d.ts +52 -9
- package/dist/core/registry.js +376 -162
- package/dist/core/state-epoch.d.ts +16 -0
- package/dist/core/state-epoch.js +21 -0
- package/dist/core/token.d.ts +1 -1
- package/dist/core/types.d.ts +12 -9
- package/dist/decorators/inject.d.ts +3 -3
- package/dist/decorators/inject.js +5 -5
- package/dist/decorators/injectable.d.ts +2 -2
- package/dist/decorators/injectable.js +2 -2
- package/dist/decorators/lifecycle-decorators.js +2 -2
- package/dist/errors/diagnostics.d.ts +2 -0
- package/dist/errors/errors.d.ts +29 -3
- package/dist/errors/errors.js +34 -2
- package/dist/index.d.ts +35 -35
- package/dist/index.js +19 -19
- package/dist/injection/descriptor.d.ts +9 -7
- package/dist/injection/descriptor.js +3 -1
- package/dist/injection/resolve-options.d.ts +9 -3
- package/dist/injection/resolve-options.js +17 -1
- package/dist/introspection/dependency-graph.d.ts +3 -3
- package/dist/introspection/dependency-graph.js +15 -10
- package/dist/introspection/graph-adapters/cytoscape.d.ts +1 -1
- package/dist/introspection/graph-adapters/dot.d.ts +1 -1
- package/dist/introspection/graph-adapters/mermaid.d.ts +1 -1
- package/dist/introspection/graph-adapters/reactflow.d.ts +1 -1
- package/dist/introspection/inspector.d.ts +7 -5
- package/dist/introspection/inspector.js +13 -27
- package/dist/lifecycle/lifecycle-manager.d.ts +4 -4
- package/dist/lifecycle/lifecycle-manager.js +13 -11
- package/dist/lifecycle/scope-manager.d.ts +2 -2
- package/dist/lifecycle/scope-manager.js +4 -4
- package/dist/metadata/metadata-reader-token.d.ts +2 -2
- package/dist/metadata/metadata-reader-token.js +1 -1
- package/dist/metadata/metadata-types.d.ts +3 -3
- package/dist/metadata/symbol-metadata-reader.d.ts +3 -3
- package/dist/metadata/symbol-metadata-reader.js +1 -1
- package/dist/metadata/verifying-metadata-reader.d.ts +1 -1
- package/dist/metadata/verifying-metadata-reader.js +2 -2
- package/dist/resolution/cache/activation-need.d.ts +6 -4
- package/dist/resolution/cache/activation-need.js +13 -6
- package/dist/resolution/cache/binding-lookup-cache.d.ts +39 -6
- package/dist/resolution/cache/binding-lookup-cache.js +97 -13
- package/dist/resolution/cache/class-introspector.d.ts +6 -6
- package/dist/resolution/cache/class-introspector.js +82 -69
- package/dist/resolution/context.d.ts +11 -11
- package/dist/resolution/context.js +1 -1
- package/dist/resolution/path/resolution-path.d.ts +1 -1
- package/dist/resolution/path/resolution-path.js +1 -1
- package/dist/resolution/plan/instantiation-plan.d.ts +16 -4
- package/dist/resolution/plan/instantiation-plan.js +160 -69
- package/dist/resolution/plan/plan-codegen.d.ts +100 -0
- package/dist/resolution/plan/plan-codegen.js +185 -0
- package/dist/resolution/resolver.d.ts +26 -16
- package/dist/resolution/resolver.js +401 -147
- package/dist/resolution/select/binding-select.d.ts +6 -5
- package/dist/resolution/select/binding-select.js +17 -11
- package/dist/resolution/select/constraints.d.ts +3 -3
- package/dist/resolution/select/constraints.js +4 -4
- package/package.json +11 -3
package/dist/core/registry.js
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
|
-
import { bindingSlotEquals, bindingSlotToString } from "
|
|
2
|
-
import { getOrInsert } from "
|
|
1
|
+
import { bindingSlotEquals, bindingSlotToString, writableMembership, writablePredicate } from "#core/binding";
|
|
2
|
+
import { getOrInsert } from "#core/map-upsert";
|
|
3
|
+
import { advanceStateEpoch } from "#core/state-epoch";
|
|
4
|
+
const NO_BINDINGS = Object.freeze([]);
|
|
5
|
+
/** One construction site, so every record shares a hidden class. */
|
|
6
|
+
function createTokenRecord(bindings) {
|
|
7
|
+
return {
|
|
8
|
+
bindings,
|
|
9
|
+
defaultOccupant: undefined,
|
|
10
|
+
simple: undefined,
|
|
11
|
+
multi: undefined,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
3
14
|
/**
|
|
4
15
|
* One container's binding store, indexed by token, binding id, and slot for fast lookup.
|
|
5
16
|
*
|
|
@@ -8,18 +19,17 @@ import { getOrInsert } from "#/core/map-upsert";
|
|
|
8
19
|
export class BindingRegistry {
|
|
9
20
|
// Monotonic mutation counter — lets resolvers version-stamp lookup caches across a container chain.
|
|
10
21
|
#version = 0;
|
|
11
|
-
//
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
//
|
|
16
|
-
#
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
#
|
|
20
|
-
//
|
|
21
|
-
|
|
22
|
-
#multiTagged;
|
|
22
|
+
// The common token lives here and nowhere else: exactly one default-slot binding, so the hot read
|
|
23
|
+
// of every resolve is a bare `Map.get` and a plain bind is one map write.
|
|
24
|
+
#lone = new Map();
|
|
25
|
+
// Every other token — several bindings, a tagged slot, a predicate — has a record here, and a
|
|
26
|
+
// token is in exactly one of the two maps. Allocated by the first token that needs a record.
|
|
27
|
+
#records;
|
|
28
|
+
// Built on the first id-keyed read and maintained from then on: a bind-and-resolve container
|
|
29
|
+
// never asks by id, so it never pays for the second map.
|
|
30
|
+
#byId;
|
|
31
|
+
// Set when the first tagged slot lands and never cleared, like the tagged maps it stands for.
|
|
32
|
+
#taggedIndexBuilt = false;
|
|
23
33
|
// Set on the first constant registered and never cleared. Teardown only needs the negative answer
|
|
24
34
|
// to be exact, and that is what lets a container holding no constant skip its sweep entirely.
|
|
25
35
|
#heldConstantBinding = false;
|
|
@@ -27,134 +37,186 @@ export class BindingRegistry {
|
|
|
27
37
|
get version() {
|
|
28
38
|
return this.#version;
|
|
29
39
|
}
|
|
40
|
+
// Every mutation moves the process-wide epoch too, which is what lets a descendant's cache skip
|
|
41
|
+
// re-summing the chain while nothing anywhere has changed.
|
|
42
|
+
#bump() {
|
|
43
|
+
this.#version += 1;
|
|
44
|
+
advanceStateEpoch();
|
|
45
|
+
}
|
|
30
46
|
/** Whether a constant has ever been registered here, and so whether teardown has anything to sweep. */
|
|
31
47
|
get hasHeldConstantBinding() {
|
|
32
48
|
return this.#heldConstantBinding;
|
|
33
49
|
}
|
|
50
|
+
/** Whether the deferred tagged-slot index has had to be built. */
|
|
51
|
+
get isTaggedIndexBuilt() {
|
|
52
|
+
return this.#taggedIndexBuilt;
|
|
53
|
+
}
|
|
54
|
+
/** Whether an id-keyed operation has had to build the id index. */
|
|
55
|
+
get isIdIndexBuilt() {
|
|
56
|
+
return this.#byId !== undefined;
|
|
57
|
+
}
|
|
58
|
+
/** Whether a token carrying more than one default-slot binding has had to build the record map. */
|
|
59
|
+
get isRecordMapBuilt() {
|
|
60
|
+
return this.#records !== undefined;
|
|
61
|
+
}
|
|
34
62
|
/**
|
|
35
63
|
* Registers a mutation the indexes don't care about (a fluent chain refining scope or an
|
|
36
64
|
* activation hook in place), so version-stamped resolver caches still invalidate.
|
|
37
65
|
*/
|
|
38
66
|
touch() {
|
|
39
|
-
this.#
|
|
67
|
+
this.#bump();
|
|
40
68
|
}
|
|
41
69
|
/**
|
|
42
70
|
* Adds or replaces a binding using slot-aware last-wins. Returns the displaced binding, if any.
|
|
43
71
|
*
|
|
44
|
-
* @remarks The binding is stored by reference — it must come from
|
|
45
|
-
* what guarantees the single hidden class the resolver's hot reads depend on.
|
|
72
|
+
* @remarks The binding is stored by reference — it must come from the one binding builder, which
|
|
73
|
+
* is what guarantees the single hidden class the resolver's hot reads depend on.
|
|
46
74
|
*/
|
|
47
75
|
add(binding) {
|
|
48
|
-
this.#
|
|
76
|
+
this.#bump();
|
|
49
77
|
if (binding.kind === "constant") {
|
|
50
78
|
this.#heldConstantBinding = true;
|
|
51
79
|
}
|
|
52
80
|
const key = binding.token;
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
// Only apply last-wins for slot-based bindings (not predicate-only)
|
|
57
|
-
let displacedBinding;
|
|
58
|
-
let nextBindings;
|
|
59
|
-
if (bindingsForToken === undefined) {
|
|
60
|
-
nextBindings = [binding];
|
|
81
|
+
const record = this.#records?.get(key);
|
|
82
|
+
if (record !== undefined) {
|
|
83
|
+
return this.#addToRecord(key, record, binding);
|
|
61
84
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
this.#byId.delete(displacedBinding.id);
|
|
68
|
-
this.#deindexSimpleTaggedBinding(key, displacedBinding);
|
|
69
|
-
this.#deindexMultiTaggedBinding(key, displacedBinding);
|
|
70
|
-
}
|
|
85
|
+
const lone = this.#lone.get(key);
|
|
86
|
+
if (lone === undefined) {
|
|
87
|
+
this.#byId?.set(binding.identifier, binding);
|
|
88
|
+
if (isDefaultSlotBinding(binding)) {
|
|
89
|
+
this.#lone.set(key, binding);
|
|
71
90
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
91
|
+
else {
|
|
92
|
+
this.#createRecord(key, [binding]);
|
|
93
|
+
}
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
// Same slot, last wins: the newcomer takes the lone seat and nothing else moves.
|
|
97
|
+
if (isDefaultSlotBinding(binding)) {
|
|
98
|
+
this.#lone.set(key, binding);
|
|
99
|
+
if (this.#byId !== undefined) {
|
|
100
|
+
this.#byId.delete(lone.identifier);
|
|
101
|
+
this.#byId.set(binding.identifier, binding);
|
|
102
|
+
}
|
|
103
|
+
return lone;
|
|
104
|
+
}
|
|
105
|
+
// A second shape joins the token, which is what a record is for.
|
|
106
|
+
this.#lone.delete(key);
|
|
107
|
+
this.#byId?.set(binding.identifier, binding);
|
|
108
|
+
this.#createRecord(key, [lone, binding]);
|
|
109
|
+
return undefined;
|
|
83
110
|
}
|
|
84
111
|
/** Remove all bindings for a token. Returns removed bindings. */
|
|
85
112
|
removeByToken(token) {
|
|
86
|
-
this.#
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
113
|
+
this.#bump();
|
|
114
|
+
const lone = this.#lone.get(token);
|
|
115
|
+
if (lone !== undefined) {
|
|
116
|
+
this.#lone.delete(token);
|
|
117
|
+
this.#byId?.delete(lone.identifier);
|
|
118
|
+
return [lone];
|
|
119
|
+
}
|
|
120
|
+
const records = this.#records;
|
|
121
|
+
const record = records?.get(token);
|
|
122
|
+
if (records === undefined || record === undefined) {
|
|
123
|
+
return [];
|
|
95
124
|
}
|
|
96
|
-
|
|
125
|
+
records.delete(token);
|
|
126
|
+
if (this.#byId !== undefined) {
|
|
127
|
+
for (const binding of record.bindings) {
|
|
128
|
+
this.#byId.delete(binding.identifier);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return [...record.bindings];
|
|
97
132
|
}
|
|
98
133
|
/** Remove a specific binding by ID. Returns the removed binding or undefined. */
|
|
99
134
|
removeById(id) {
|
|
100
|
-
const
|
|
135
|
+
const byId = this.#ensureById();
|
|
136
|
+
const binding = byId.get(id);
|
|
101
137
|
if (binding === undefined) {
|
|
102
138
|
return undefined;
|
|
103
139
|
}
|
|
104
|
-
this.#
|
|
105
|
-
|
|
140
|
+
this.#bump();
|
|
141
|
+
byId.delete(id);
|
|
106
142
|
const key = binding.token;
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
this.#multiTagged?.delete(key);
|
|
118
|
-
this.#fastDefault.delete(key);
|
|
119
|
-
}
|
|
120
|
-
else {
|
|
121
|
-
this.#bindings.set(key, remaining);
|
|
122
|
-
this.#refreshFastDefaultForToken(key);
|
|
143
|
+
if (this.#lone.get(key)?.identifier === id) {
|
|
144
|
+
this.#lone.delete(key);
|
|
145
|
+
return binding;
|
|
146
|
+
}
|
|
147
|
+
const record = this.#records?.get(key);
|
|
148
|
+
if (record !== undefined) {
|
|
149
|
+
const bindingIndex = record.bindings.findIndex((candidate) => candidate.identifier === id);
|
|
150
|
+
// Replaced, never spliced: a walk holding the current array must not lose its place.
|
|
151
|
+
if (bindingIndex !== -1) {
|
|
152
|
+
record.bindings = record.bindings.toSpliced(bindingIndex, 1);
|
|
123
153
|
}
|
|
154
|
+
this.#deindexSlot(record, binding);
|
|
155
|
+
this.#settle(key, record);
|
|
124
156
|
}
|
|
125
157
|
return binding;
|
|
126
158
|
}
|
|
127
|
-
/**
|
|
159
|
+
/**
|
|
160
|
+
* Get all bindings for a token.
|
|
161
|
+
*
|
|
162
|
+
* @remarks Allocates a one-element list for a lone default-slot binding, so a hot path asks
|
|
163
|
+
* `getFastDefault()` first and reaches here only for a token that keeps a record.
|
|
164
|
+
*/
|
|
128
165
|
getAll(token) {
|
|
129
|
-
|
|
166
|
+
const record = this.#records?.get(token);
|
|
167
|
+
if (record !== undefined) {
|
|
168
|
+
return record.bindings;
|
|
169
|
+
}
|
|
170
|
+
const lone = this.#lone.get(token);
|
|
171
|
+
return lone === undefined ? NO_BINDINGS : [lone];
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* The bindings of a token that keeps a record, or none.
|
|
175
|
+
*
|
|
176
|
+
* @remarks For a caller whose lone-map probe has just missed: the record map is all that is left
|
|
177
|
+
* to ask, and a lone binding's one-element list is never materialised here.
|
|
178
|
+
*/
|
|
179
|
+
getRecorded(token) {
|
|
180
|
+
return this.#records?.get(token)?.bindings ?? NO_BINDINGS;
|
|
181
|
+
}
|
|
182
|
+
/** How many bindings a token holds, without materialising a lone binding's list. */
|
|
183
|
+
countBindings(token) {
|
|
184
|
+
const record = this.#records?.get(token);
|
|
185
|
+
if (record !== undefined) {
|
|
186
|
+
return record.bindings.length;
|
|
187
|
+
}
|
|
188
|
+
return this.#lone.has(token) ? 1 : 0;
|
|
130
189
|
}
|
|
131
190
|
/** Get binding by ID. */
|
|
132
191
|
getById(id) {
|
|
133
|
-
return this.#
|
|
192
|
+
return this.#ensureById().get(id);
|
|
134
193
|
}
|
|
135
194
|
/** Check if any binding exists for token. */
|
|
136
195
|
has(token) {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
return
|
|
196
|
+
// A record is dropped with its last binding, so presence in either map is the whole answer. The
|
|
197
|
+
// size read keeps a container that never bound anything — every per-request child — off the probe.
|
|
198
|
+
return ((this.#lone.size !== 0 && this.#lone.has(token)) || (this.#records !== undefined && this.#records.has(token)));
|
|
140
199
|
}
|
|
141
200
|
/** All bindings in the registry. */
|
|
142
201
|
allBindings() {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
202
|
+
if (this.#lone.size === 0 && this.#records === undefined) {
|
|
203
|
+
return NO_BINDINGS;
|
|
204
|
+
}
|
|
205
|
+
const allBindings = [...this.#lone.values()];
|
|
206
|
+
if (this.#records !== undefined) {
|
|
207
|
+
for (const record of this.#records.values()) {
|
|
208
|
+
allBindings.push(...record.bindings);
|
|
209
|
+
}
|
|
146
210
|
}
|
|
147
211
|
return allBindings;
|
|
148
212
|
}
|
|
149
213
|
/** Remove all bindings. Returns all removed. */
|
|
150
214
|
clear() {
|
|
151
|
-
this.#
|
|
215
|
+
this.#bump();
|
|
152
216
|
const all = this.allBindings();
|
|
153
|
-
this.#
|
|
154
|
-
this.#
|
|
155
|
-
this.#
|
|
156
|
-
this.#multiTagged?.clear();
|
|
157
|
-
this.#fastDefault.clear();
|
|
217
|
+
this.#lone.clear();
|
|
218
|
+
this.#records?.clear();
|
|
219
|
+
this.#byId?.clear();
|
|
158
220
|
return all;
|
|
159
221
|
}
|
|
160
222
|
/** Whether a slot-based binding currently occupies `binding`'s slot, so adding it would displace. */
|
|
@@ -162,11 +224,15 @@ export class BindingRegistry {
|
|
|
162
224
|
if (isPurePredicateBinding(binding)) {
|
|
163
225
|
return false;
|
|
164
226
|
}
|
|
165
|
-
|
|
166
|
-
|
|
227
|
+
if (this.#lone.has(binding.token)) {
|
|
228
|
+
// The lone seat is the default slot, so only a default-slot newcomer collides with it.
|
|
229
|
+
return binding.slot.tags.length === 0;
|
|
230
|
+
}
|
|
231
|
+
const record = this.#records?.get(binding.token);
|
|
232
|
+
if (record === undefined) {
|
|
167
233
|
return false;
|
|
168
234
|
}
|
|
169
|
-
return
|
|
235
|
+
return this.#slotOccupant(record, binding.slot) !== undefined;
|
|
170
236
|
}
|
|
171
237
|
/**
|
|
172
238
|
* The binding indexed under one criterion.
|
|
@@ -175,7 +241,15 @@ export class BindingRegistry {
|
|
|
175
241
|
* identity — where a value-keyed map answered by SameValueZero and parted from `Object.is` on ±0.
|
|
176
242
|
*/
|
|
177
243
|
getSimpleTagged(token, criterion) {
|
|
178
|
-
return this.#
|
|
244
|
+
return this.#records?.get(token)?.simple?.get(criterion);
|
|
245
|
+
}
|
|
246
|
+
/** The binding whose slot is exactly these two criteria, declared in either order, or `undefined`. */
|
|
247
|
+
getPairTagged(token, first, second) {
|
|
248
|
+
const multi = this.#records?.get(token)?.multi;
|
|
249
|
+
if (multi === undefined) {
|
|
250
|
+
return undefined;
|
|
251
|
+
}
|
|
252
|
+
return findPairIn(multi.get(first), first, second) ?? findPairIn(multi.get(second), first, second);
|
|
179
253
|
}
|
|
180
254
|
/**
|
|
181
255
|
* The multi-tag bindings whose slot's first criterion is `criterion`.
|
|
@@ -184,100 +258,240 @@ export class BindingRegistry {
|
|
|
184
258
|
* against the request — first-criterion bucketing only guarantees each candidate appears once.
|
|
185
259
|
*/
|
|
186
260
|
getMultiTagged(token, criterion) {
|
|
187
|
-
return this.#
|
|
261
|
+
return this.#records?.get(token)?.multi?.get(criterion);
|
|
188
262
|
}
|
|
263
|
+
/** A token's lone default-slot binding — the first read of every synchronous resolve. */
|
|
189
264
|
getFastDefault(token) {
|
|
190
|
-
return this.#
|
|
265
|
+
return this.#lone.get(token);
|
|
191
266
|
}
|
|
192
|
-
/**
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
267
|
+
/**
|
|
268
|
+
* Takes a live binding out of every index, lets `rewrite` change its slot or predicate, and reports
|
|
269
|
+
* whether it was live; the caller registers it again with `add`.
|
|
270
|
+
*
|
|
271
|
+
* @remarks The binding keeps its object and its id, so the id index needs no touch and nothing
|
|
272
|
+
* that holds the object has to be told. `false` means the binding was unbound or displaced since
|
|
273
|
+
* it registered, and a refinement must not resurrect it.
|
|
274
|
+
*/
|
|
275
|
+
reslot(binding, rewrite) {
|
|
276
|
+
const key = binding.token;
|
|
277
|
+
if (this.#lone.get(key) === binding) {
|
|
278
|
+
this.#bump();
|
|
279
|
+
this.#lone.delete(key);
|
|
280
|
+
}
|
|
281
|
+
else {
|
|
282
|
+
const record = this.#records?.get(key);
|
|
283
|
+
const index = record === undefined ? -1 : record.bindings.indexOf(binding);
|
|
284
|
+
if (record === undefined || index === -1) {
|
|
285
|
+
return false;
|
|
286
|
+
}
|
|
287
|
+
this.#bump();
|
|
288
|
+
// Replaced, never spliced: a walk holding the current array must not lose its place.
|
|
289
|
+
record.bindings = record.bindings.toSpliced(index, 1);
|
|
290
|
+
this.#deindexSlot(record, binding);
|
|
291
|
+
this.#settle(key, record);
|
|
292
|
+
}
|
|
293
|
+
rewrite();
|
|
294
|
+
return true;
|
|
196
295
|
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
296
|
+
/**
|
|
297
|
+
* Marks a live binding as a collection member in place, moving it out of the lone map: a member is
|
|
298
|
+
* never the token's lone default answer, and nothing else indexes on membership.
|
|
299
|
+
*/
|
|
300
|
+
setMany(binding) {
|
|
301
|
+
this.#bump();
|
|
302
|
+
// Set before any (re)indexing so `#indexSlot` sees a member and leaves it out of every slot.
|
|
303
|
+
writableMembership(binding).isMany = true;
|
|
304
|
+
if (this.#promoteLoneToRecord(binding.token, binding)) {
|
|
200
305
|
return;
|
|
201
306
|
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
307
|
+
// A default occupant that becomes a member frees its slot, so drop the stale index entry. A
|
|
308
|
+
// member cannot collapse a record back to lone, so this path does not settle.
|
|
309
|
+
this.#clearDefaultOccupant(binding.token, binding);
|
|
205
310
|
}
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
311
|
+
/**
|
|
312
|
+
* Adds a predicate to a live binding in place.
|
|
313
|
+
*
|
|
314
|
+
* @remarks Only ever narrows — `when()` composes with any existing predicate — so the argument is
|
|
315
|
+
* never absent. Nothing indexes on the predicate, so the binding object and its id stay; a lone
|
|
316
|
+
* binding moves to a record because the lone map holds default-slot bindings with no predicate.
|
|
317
|
+
*/
|
|
318
|
+
setPredicate(binding, predicate) {
|
|
319
|
+
this.#bump();
|
|
320
|
+
// Set before any (re)indexing: a predicate-only binding holds no default slot.
|
|
321
|
+
writablePredicate(binding).predicate = predicate;
|
|
322
|
+
if (this.#promoteLoneToRecord(binding.token, binding)) {
|
|
209
323
|
return;
|
|
210
324
|
}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
325
|
+
// The binding may have vacated the default slot, and a narrowed record can collapse back to lone.
|
|
326
|
+
const record = this.#clearDefaultOccupant(binding.token, binding);
|
|
327
|
+
if (record !== undefined) {
|
|
328
|
+
this.#settle(binding.token, record);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Moves a binding still holding the lone seat into a fresh one-binding record, returning whether it did.
|
|
333
|
+
*
|
|
334
|
+
* @remarks Its field (`isMany` or `predicate`) is written before this call, so the founding
|
|
335
|
+
* `#indexSlot` files it under the slot it now holds.
|
|
336
|
+
*/
|
|
337
|
+
#promoteLoneToRecord(key, binding) {
|
|
338
|
+
if (this.#lone.get(key) !== binding) {
|
|
339
|
+
return false;
|
|
340
|
+
}
|
|
341
|
+
this.#lone.delete(key);
|
|
342
|
+
this.#createRecord(key, [binding]);
|
|
343
|
+
return true;
|
|
344
|
+
}
|
|
345
|
+
/** Clears the default-slot index entry a binding has vacated, returning its record if one exists. */
|
|
346
|
+
#clearDefaultOccupant(key, binding) {
|
|
347
|
+
const record = this.#records?.get(key);
|
|
348
|
+
if (record?.defaultOccupant === binding) {
|
|
349
|
+
record.defaultOccupant = undefined;
|
|
350
|
+
}
|
|
351
|
+
return record;
|
|
352
|
+
}
|
|
353
|
+
/** Summarize available slot strings for a token (for error messages). */
|
|
354
|
+
availableSlotStrings(token) {
|
|
355
|
+
return this.getAll(token).map((binding) => bindingSlotToString(binding.slot));
|
|
356
|
+
}
|
|
357
|
+
#createRecord(key, bindings) {
|
|
358
|
+
const record = createTokenRecord(bindings);
|
|
359
|
+
(this.#records ??= new Map()).set(key, record);
|
|
360
|
+
// A promoted lone binding carries its slot into the record, so index every founding binding.
|
|
361
|
+
for (const binding of bindings) {
|
|
362
|
+
this.#indexSlot(record, binding);
|
|
214
363
|
}
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
364
|
+
return record;
|
|
365
|
+
}
|
|
366
|
+
/** The binding occupying `slot` in this record, found through the slot indexes, or `undefined`. */
|
|
367
|
+
#slotOccupant(record, slot) {
|
|
368
|
+
const { tags } = slot;
|
|
369
|
+
if (tags.length === 0) {
|
|
370
|
+
return record.defaultOccupant;
|
|
371
|
+
}
|
|
372
|
+
if (tags.length === 1) {
|
|
373
|
+
return record.simple?.get(tags[0]);
|
|
374
|
+
}
|
|
375
|
+
// Two-plus criteria are rare and can be bucketed under either criterion, so the list decides.
|
|
376
|
+
return record.bindings.find((candidate) => !isPurePredicateBinding(candidate) && bindingSlotEquals(candidate.slot, slot));
|
|
377
|
+
}
|
|
378
|
+
#addToRecord(key, record, binding) {
|
|
379
|
+
// Last-wins applies only to a slot-based newcomer, and the slot indexes name its occupant
|
|
380
|
+
// directly — so a member joining a collection of members displaces nobody without a list walk.
|
|
381
|
+
let displacedBinding;
|
|
382
|
+
if (!isPurePredicateBinding(binding)) {
|
|
383
|
+
displacedBinding = this.#slotOccupant(record, binding.slot);
|
|
384
|
+
if (displacedBinding !== undefined) {
|
|
385
|
+
this.#byId?.delete(displacedBinding.identifier);
|
|
386
|
+
this.#deindexSlot(record, displacedBinding);
|
|
219
387
|
}
|
|
220
388
|
}
|
|
389
|
+
// A selection may be walking this list inside a `when()` predicate. An append past the length it
|
|
390
|
+
// read cannot shift it, so it lands in place; a displacement replaces the array instead.
|
|
391
|
+
if (displacedBinding === undefined) {
|
|
392
|
+
record.bindings.push(binding);
|
|
393
|
+
}
|
|
394
|
+
else {
|
|
395
|
+
record.bindings = [...record.bindings.filter((candidate) => candidate !== displacedBinding), binding];
|
|
396
|
+
}
|
|
397
|
+
this.#byId?.set(binding.identifier, binding);
|
|
398
|
+
this.#indexSlot(record, binding);
|
|
399
|
+
this.#settle(key, record);
|
|
400
|
+
return displacedBinding;
|
|
221
401
|
}
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
402
|
+
// A record that shrank to one default-slot binding goes back to the lone map; an empty one goes.
|
|
403
|
+
#settle(key, record) {
|
|
404
|
+
const { bindings } = record;
|
|
405
|
+
if (bindings.length === 0) {
|
|
406
|
+
this.#records.delete(key);
|
|
407
|
+
}
|
|
408
|
+
else if (bindings.length === 1 && isDefaultSlotBinding(bindings[0])) {
|
|
409
|
+
this.#records.delete(key);
|
|
410
|
+
this.#lone.set(key, bindings[0]);
|
|
226
411
|
}
|
|
227
|
-
this.#multiTagged ??= new Map();
|
|
228
|
-
const buckets = getOrInsert(this.#multiTagged, tokenKey, new Map());
|
|
229
|
-
getOrInsert(buckets, firstCriterion, []).push(binding);
|
|
230
412
|
}
|
|
231
|
-
#
|
|
232
|
-
|
|
233
|
-
|
|
413
|
+
#ensureById() {
|
|
414
|
+
if (this.#byId === undefined) {
|
|
415
|
+
const byId = new Map();
|
|
416
|
+
for (const binding of this.#lone.values()) {
|
|
417
|
+
byId.set(binding.identifier, binding);
|
|
418
|
+
}
|
|
419
|
+
if (this.#records !== undefined) {
|
|
420
|
+
for (const record of this.#records.values()) {
|
|
421
|
+
for (const binding of record.bindings) {
|
|
422
|
+
byId.set(binding.identifier, binding);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
this.#byId = byId;
|
|
427
|
+
}
|
|
428
|
+
return this.#byId;
|
|
429
|
+
}
|
|
430
|
+
// Indexes a binding by its slot so an add finds who it displaces without walking the list: the
|
|
431
|
+
// default slot in `defaultOccupant`, one criterion in the exact map, more in the first-criterion
|
|
432
|
+
// bucket. A collection member or a predicate-only binding occupies no slot and is left unindexed.
|
|
433
|
+
#indexSlot(record, binding) {
|
|
434
|
+
if (isPurePredicateBinding(binding)) {
|
|
234
435
|
return;
|
|
235
436
|
}
|
|
236
|
-
const
|
|
237
|
-
if (
|
|
437
|
+
const { tags } = binding.slot;
|
|
438
|
+
if (tags.length === 0) {
|
|
439
|
+
record.defaultOccupant = binding;
|
|
238
440
|
return;
|
|
239
441
|
}
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
442
|
+
this.#taggedIndexBuilt = true;
|
|
443
|
+
if (tags.length === 1) {
|
|
444
|
+
(record.simple ??= new Map()).set(tags[0], binding);
|
|
445
|
+
}
|
|
446
|
+
else {
|
|
447
|
+
getOrInsert((record.multi ??= new Map()), tags[0], []).push(binding);
|
|
245
448
|
}
|
|
246
449
|
}
|
|
247
|
-
#
|
|
248
|
-
|
|
249
|
-
const onlyBinding = bindingsForToken?.length === 1 ? bindingsForToken[0] : undefined;
|
|
250
|
-
if (onlyBinding !== undefined && isDefaultSlotBinding(onlyBinding)) {
|
|
251
|
-
this.#fastDefault.set(tokenKey, onlyBinding);
|
|
450
|
+
#deindexSlot(record, binding) {
|
|
451
|
+
if (isPurePredicateBinding(binding)) {
|
|
252
452
|
return;
|
|
253
453
|
}
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
454
|
+
const { tags } = binding.slot;
|
|
455
|
+
if (tags.length === 0) {
|
|
456
|
+
if (record.defaultOccupant?.identifier === binding.identifier) {
|
|
457
|
+
record.defaultOccupant = undefined;
|
|
458
|
+
}
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
if (tags.length === 1) {
|
|
462
|
+
if (record.simple?.get(tags[0])?.identifier === binding.identifier) {
|
|
463
|
+
record.simple.delete(tags[0]);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
else if (tags.length >= 2) {
|
|
467
|
+
const bucket = record.multi?.get(tags[0]);
|
|
468
|
+
const bindingIndex = bucket?.findIndex((candidate) => candidate.identifier === binding.identifier) ?? -1;
|
|
469
|
+
// Spliced in place: nothing walks a bucket while user code runs — candidates are gathered
|
|
470
|
+
// into their own array before any predicate is evaluated.
|
|
471
|
+
if (bindingIndex !== -1) {
|
|
472
|
+
bucket.splice(bindingIndex, 1);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
259
475
|
}
|
|
260
476
|
}
|
|
261
|
-
/**
|
|
262
|
-
* The criterion a binding is indexed under, or `undefined` when its slot carries more than one.
|
|
263
|
-
*
|
|
264
|
-
* @remarks Carries predicate-bearing bindings too: every lane that reads this index already
|
|
265
|
-
* re-checks what it finds, so an indexed hit was never unconditional.
|
|
266
|
-
*/
|
|
267
|
-
function simpleTagOf(binding) {
|
|
268
|
-
const { tags } = binding.slot;
|
|
269
|
-
return tags.length === 1 ? tags[0] : undefined;
|
|
270
|
-
}
|
|
271
|
-
/** The first criterion a multi-criterion slot is bucketed under, or `undefined` for any other shape. */
|
|
272
|
-
function multiTagFirstOf(binding) {
|
|
273
|
-
const { tags } = binding.slot;
|
|
274
|
-
return tags.length >= 2 ? tags[0] : undefined;
|
|
275
|
-
}
|
|
276
|
-
/** A binding nothing has to be matched against: the default slot, no predicate. */
|
|
477
|
+
/** A binding nothing has to be matched against: the default slot, no predicate, not a collection member. */
|
|
277
478
|
function isDefaultSlotBinding(binding) {
|
|
278
|
-
return binding.slot.tags.length === 0 && binding.predicate === undefined;
|
|
479
|
+
return binding.slot.tags.length === 0 && binding.predicate === undefined && !binding.isMany;
|
|
279
480
|
}
|
|
280
|
-
/** A predicate with no slot constraint
|
|
481
|
+
/** A binding that occupies no slot — a collection member, or a predicate with no slot constraint — so last-wins does not apply to it. */
|
|
281
482
|
function isPurePredicateBinding(binding) {
|
|
282
|
-
return binding.predicate !== undefined && binding.slot.tags.length === 0;
|
|
483
|
+
return binding.isMany || (binding.predicate !== undefined && binding.slot.tags.length === 0);
|
|
484
|
+
}
|
|
485
|
+
// A bucket is keyed by its members' first criterion, so the pair is read from whichever came first.
|
|
486
|
+
function findPairIn(bucket, first, second) {
|
|
487
|
+
if (bucket === undefined) {
|
|
488
|
+
return undefined;
|
|
489
|
+
}
|
|
490
|
+
for (let index = 0; index < bucket.length; index += 1) {
|
|
491
|
+
const { tags } = bucket[index].slot;
|
|
492
|
+
if (tags.length === 2 && (tags[0] === first ? tags[1] === second : tags[0] === second && tags[1] === first)) {
|
|
493
|
+
return bucket[index];
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
return undefined;
|
|
283
497
|
}
|