@codefast/di 0.10.0 → 0.11.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.
- package/CHANGELOG.md +293 -0
- package/README.md +71 -7
- package/dist/ambient/active-container.d.ts +13 -11
- package/dist/ambient/active-container.js +8 -6
- package/dist/container/binding-builders.d.ts +25 -7
- package/dist/container/binding-builders.js +110 -42
- package/dist/container/container.d.ts +8 -8
- package/dist/container/container.js +244 -67
- package/dist/core/binding-declaration.d.ts +120 -0
- package/dist/core/binding-declaration.js +186 -0
- package/dist/core/binding-scope.d.ts +2 -2
- package/dist/core/binding.d.ts +61 -9
- package/dist/core/binding.js +49 -2
- package/dist/core/constraint-requirement.d.ts +1 -1
- package/dist/core/module.d.ts +10 -7
- package/dist/core/module.js +17 -3
- package/dist/core/registry.d.ts +17 -13
- package/dist/core/registry.js +155 -59
- package/dist/core/state-epoch.d.ts +18 -1
- package/dist/core/state-epoch.js +17 -0
- package/dist/core/tag.js +1 -1
- package/dist/core/token.d.ts +1 -1
- package/dist/core/types.d.ts +5 -5
- package/dist/decorators/decorator-metadata.d.ts +9 -0
- package/dist/decorators/decorator-metadata.js +20 -0
- package/dist/decorators/inject.d.ts +3 -3
- package/dist/decorators/inject.js +7 -6
- package/dist/decorators/injectable.d.ts +2 -2
- package/dist/decorators/injectable.js +5 -3
- package/dist/decorators/lifecycle-decorators.js +9 -3
- package/dist/errors/errors.d.ts +85 -14
- package/dist/errors/errors.js +100 -20
- package/dist/index.d.ts +37 -35
- package/dist/index.js +20 -19
- package/dist/injection/descriptor.d.ts +3 -3
- package/dist/injection/descriptor.js +3 -7
- package/dist/injection/resolve-options.d.ts +3 -3
- package/dist/injection/resolve-options.js +7 -5
- package/dist/introspection/dependency-graph.d.ts +10 -5
- package/dist/introspection/dependency-graph.js +51 -28
- 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/graph-adapters/reactflow.js +6 -4
- package/dist/introspection/inspector.d.ts +4 -4
- package/dist/introspection/inspector.js +9 -12
- package/dist/lifecycle/lifecycle-manager.d.ts +4 -4
- package/dist/lifecycle/lifecycle-manager.js +18 -6
- package/dist/lifecycle/scope-manager.d.ts +2 -2
- package/dist/lifecycle/scope-manager.js +31 -13
- 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 +5 -4
- package/dist/metadata/verifying-metadata-reader.js +30 -8
- package/dist/resolution/async-fan-out.d.ts +12 -0
- package/dist/resolution/async-fan-out.js +26 -0
- package/dist/resolution/cache/activation-need.d.ts +4 -5
- package/dist/resolution/cache/activation-need.js +11 -18
- package/dist/resolution/cache/binding-lookup-cache.d.ts +5 -12
- package/dist/resolution/cache/binding-lookup-cache.js +32 -19
- package/dist/resolution/cache/class-introspector.d.ts +16 -5
- package/dist/resolution/cache/class-introspector.js +73 -56
- package/dist/resolution/context.d.ts +17 -25
- package/dist/resolution/context.js +47 -56
- package/dist/resolution/path/resolution-path.d.ts +48 -13
- package/dist/resolution/path/resolution-path.js +90 -39
- package/dist/resolution/plan/instantiation-plan.d.ts +5 -5
- package/dist/resolution/plan/instantiation-plan.js +66 -26
- package/dist/resolution/plan/plan-codegen.d.ts +10 -7
- package/dist/resolution/plan/plan-codegen.js +62 -34
- package/dist/resolution/resolver.d.ts +16 -17
- package/dist/resolution/resolver.js +316 -273
- package/dist/resolution/select/binding-select.d.ts +6 -5
- package/dist/resolution/select/binding-select.js +5 -4
- package/dist/resolution/select/constraints.d.ts +3 -3
- package/dist/resolution/select/constraints.js +4 -4
- package/package.json +14 -2
package/dist/core/registry.d.ts
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
|
-
import type { Binding } from "
|
|
2
|
-
import type { BindingTag } from "
|
|
3
|
-
import type { Token } from "
|
|
4
|
-
import type { BindingConstraint, BindingIdentifier, Constructor } from "
|
|
1
|
+
import type { Binding } from "#core/binding";
|
|
2
|
+
import type { BindingTag } from "#core/tag";
|
|
3
|
+
import type { Token } from "#core/token";
|
|
4
|
+
import type { BindingConstraint, BindingIdentifier, Constructor } from "#core/types";
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* @since 0.3.16-canary.0
|
|
6
|
+
* @since 0.11.0
|
|
9
7
|
*/
|
|
10
8
|
export declare class BindingRegistry {
|
|
11
9
|
#private;
|
|
@@ -27,8 +25,8 @@ export declare class BindingRegistry {
|
|
|
27
25
|
/**
|
|
28
26
|
* Adds or replaces a binding using slot-aware last-wins. Returns the displaced binding, if any.
|
|
29
27
|
*
|
|
30
|
-
* @remarks The binding is stored by reference — it must come from
|
|
31
|
-
* what guarantees the single hidden class the resolver's hot reads depend on.
|
|
28
|
+
* @remarks The binding is stored by reference — it must come from the one binding builder, which
|
|
29
|
+
* is what guarantees the single hidden class the resolver's hot reads depend on.
|
|
32
30
|
*/
|
|
33
31
|
add(binding: Binding): Binding | undefined;
|
|
34
32
|
/** Remove all bindings for a token. Returns removed bindings. */
|
|
@@ -49,8 +47,6 @@ export declare class BindingRegistry {
|
|
|
49
47
|
* to ask, and a lone binding's one-element list is never materialised here.
|
|
50
48
|
*/
|
|
51
49
|
getRecorded(token: Token<unknown> | Constructor): ReadonlyArray<Binding>;
|
|
52
|
-
/** How many bindings a token holds, without materialising a lone binding's list. */
|
|
53
|
-
countBindings(token: Token<unknown> | Constructor): number;
|
|
54
50
|
/** Get binding by ID. */
|
|
55
51
|
getById(id: BindingIdentifier): Binding | undefined;
|
|
56
52
|
/** Check if any binding exists for token. */
|
|
@@ -77,6 +73,16 @@ export declare class BindingRegistry {
|
|
|
77
73
|
* against the request — first-criterion bucketing only guarantees each candidate appears once.
|
|
78
74
|
*/
|
|
79
75
|
getMultiTagged(token: Token<unknown> | Constructor, criterion: BindingTag): ReadonlyArray<Binding> | undefined;
|
|
76
|
+
/**
|
|
77
|
+
* Whether any binding for the token carries a `when()` predicate.
|
|
78
|
+
*
|
|
79
|
+
* @remarks Only a token that keeps a record can hold a predicate — a lone binding is default-slot
|
|
80
|
+
* with none — so an index fast lane consults this to decline to full selection when the
|
|
81
|
+
* more-specific rule's predicate step could apply.
|
|
82
|
+
*/
|
|
83
|
+
hasPredicateCandidate(token: Token<unknown> | Constructor): boolean;
|
|
84
|
+
/** The binding holding a token's default slot — lone or recorded — or `undefined` when none does. */
|
|
85
|
+
getDefaultSlotBinding(token: Token<unknown> | Constructor): Binding | undefined;
|
|
80
86
|
/** A token's lone default-slot binding — the first read of every synchronous resolve. */
|
|
81
87
|
getFastDefault(token: Token<unknown> | Constructor): Binding | undefined;
|
|
82
88
|
/**
|
|
@@ -101,6 +107,4 @@ export declare class BindingRegistry {
|
|
|
101
107
|
* binding moves to a record because the lone map holds default-slot bindings with no predicate.
|
|
102
108
|
*/
|
|
103
109
|
setPredicate(binding: Binding, predicate: BindingConstraint): void;
|
|
104
|
-
/** Summarize available slot strings for a token (for error messages). */
|
|
105
|
-
availableSlotStrings(token: Token<unknown> | Constructor): Array<string>;
|
|
106
110
|
}
|
package/dist/core/registry.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { bindingSlotEquals,
|
|
2
|
-
import { getOrInsert } from "
|
|
3
|
-
import { advanceStateEpoch } from "
|
|
1
|
+
import { bindingSlotEquals, UNREGISTERED_ORDER, writableMembership, writablePredicate } from "#core/binding";
|
|
2
|
+
import { getOrInsert } from "#core/map-upsert";
|
|
3
|
+
import { advanceStateEpoch } from "#core/state-epoch";
|
|
4
4
|
const NO_BINDINGS = Object.freeze([]);
|
|
5
5
|
/** One construction site, so every record shares a hidden class. */
|
|
6
6
|
function createTokenRecord(bindings) {
|
|
@@ -16,15 +16,29 @@ function createTokenRecord(bindings) {
|
|
|
16
16
|
*
|
|
17
17
|
* @since 0.3.16-canary.0
|
|
18
18
|
*/
|
|
19
|
+
/**
|
|
20
|
+
* One shared empty map answers every read of a registry nothing was bound into, so a container
|
|
21
|
+
* that only ever resolves through its parent — every per-request child — allocates no map.
|
|
22
|
+
*/
|
|
23
|
+
const EMPTY_LONE = new Map();
|
|
24
|
+
// Process-wide, so registration order compares across every registry a binding could be restored into.
|
|
25
|
+
let registrationCounter = 0;
|
|
26
|
+
/**
|
|
27
|
+
* @since 0.11.0
|
|
28
|
+
*/
|
|
19
29
|
export class BindingRegistry {
|
|
20
30
|
// Monotonic mutation counter — lets resolvers version-stamp lookup caches across a container chain.
|
|
21
31
|
#version = 0;
|
|
22
32
|
// The common token lives here and nowhere else: exactly one default-slot binding, so the hot read
|
|
23
33
|
// of every resolve is a bare `Map.get` and a plain bind is one map write.
|
|
24
|
-
#lone =
|
|
34
|
+
#lone = EMPTY_LONE;
|
|
25
35
|
// Every other token — several bindings, a tagged slot, a predicate — has a record here, and a
|
|
26
36
|
// token is in exactly one of the two maps. Allocated by the first token that needs a record.
|
|
27
37
|
#records;
|
|
38
|
+
// The binding the last `add` placed and where it landed: the fluent chain refines what it just
|
|
39
|
+
// registered, so a refinement that follows its own add re-slots without a map probe.
|
|
40
|
+
#lastAdded;
|
|
41
|
+
#lastAddedRecord;
|
|
28
42
|
// Built on the first id-keyed read and maintained from then on: a bind-and-resolve container
|
|
29
43
|
// never asks by id, so it never pays for the second map.
|
|
30
44
|
#byId;
|
|
@@ -69,51 +83,67 @@ export class BindingRegistry {
|
|
|
69
83
|
/**
|
|
70
84
|
* Adds or replaces a binding using slot-aware last-wins. Returns the displaced binding, if any.
|
|
71
85
|
*
|
|
72
|
-
* @remarks The binding is stored by reference — it must come from
|
|
73
|
-
* what guarantees the single hidden class the resolver's hot reads depend on.
|
|
86
|
+
* @remarks The binding is stored by reference — it must come from the one binding builder, which
|
|
87
|
+
* is what guarantees the single hidden class the resolver's hot reads depend on.
|
|
74
88
|
*/
|
|
75
89
|
add(binding) {
|
|
76
90
|
this.#bump();
|
|
91
|
+
if (binding.registrationOrder === UNREGISTERED_ORDER) {
|
|
92
|
+
registrationCounter += 1;
|
|
93
|
+
binding.registrationOrder = registrationCounter;
|
|
94
|
+
}
|
|
77
95
|
if (binding.kind === "constant") {
|
|
78
96
|
this.#heldConstantBinding = true;
|
|
79
97
|
}
|
|
80
98
|
const key = binding.token;
|
|
81
|
-
const
|
|
82
|
-
if (
|
|
83
|
-
|
|
99
|
+
const records = this.#records;
|
|
100
|
+
if (records !== undefined) {
|
|
101
|
+
const record = records.get(key);
|
|
102
|
+
if (record !== undefined) {
|
|
103
|
+
this.#lastAdded = binding;
|
|
104
|
+
this.#lastAddedRecord = record;
|
|
105
|
+
return this.#addToRecord(key, record, binding);
|
|
106
|
+
}
|
|
84
107
|
}
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
108
|
+
const occupant = this.#lone.get(key);
|
|
109
|
+
this.#lastAdded = binding;
|
|
110
|
+
if (occupant === undefined) {
|
|
111
|
+
// The common bind: a fresh token taking the lone seat, one probe and one write.
|
|
112
|
+
if (this.#byId !== undefined) {
|
|
113
|
+
this.#byId.set(binding.identifier, binding);
|
|
114
|
+
}
|
|
88
115
|
if (isDefaultSlotBinding(binding)) {
|
|
89
|
-
this.#
|
|
116
|
+
this.#setLone(key, binding);
|
|
117
|
+
this.#lastAddedRecord = undefined;
|
|
90
118
|
}
|
|
91
119
|
else {
|
|
92
|
-
this.#createRecord(key, [binding]);
|
|
120
|
+
this.#lastAddedRecord = this.#createRecord(key, [binding]);
|
|
93
121
|
}
|
|
94
122
|
return undefined;
|
|
95
123
|
}
|
|
96
124
|
// Same slot, last wins: the newcomer takes the lone seat and nothing else moves.
|
|
97
125
|
if (isDefaultSlotBinding(binding)) {
|
|
98
|
-
this.#
|
|
126
|
+
this.#setLone(key, binding);
|
|
127
|
+
this.#lastAddedRecord = undefined;
|
|
99
128
|
if (this.#byId !== undefined) {
|
|
100
|
-
this.#byId.delete(
|
|
129
|
+
this.#byId.delete(occupant.identifier);
|
|
101
130
|
this.#byId.set(binding.identifier, binding);
|
|
102
131
|
}
|
|
103
|
-
return
|
|
132
|
+
return occupant;
|
|
104
133
|
}
|
|
105
134
|
// A second shape joins the token, which is what a record is for.
|
|
106
|
-
this.#
|
|
135
|
+
this.#deleteLone(key);
|
|
107
136
|
this.#byId?.set(binding.identifier, binding);
|
|
108
|
-
this.#createRecord(key, [
|
|
137
|
+
this.#lastAddedRecord = this.#createRecord(key, occupant.registrationOrder < binding.registrationOrder ? [occupant, binding] : [binding, occupant]);
|
|
109
138
|
return undefined;
|
|
110
139
|
}
|
|
111
140
|
/** Remove all bindings for a token. Returns removed bindings. */
|
|
112
141
|
removeByToken(token) {
|
|
113
142
|
this.#bump();
|
|
143
|
+
this.#lastAdded = undefined;
|
|
114
144
|
const lone = this.#lone.get(token);
|
|
115
145
|
if (lone !== undefined) {
|
|
116
|
-
this.#
|
|
146
|
+
this.#deleteLone(token);
|
|
117
147
|
this.#byId?.delete(lone.identifier);
|
|
118
148
|
return [lone];
|
|
119
149
|
}
|
|
@@ -138,10 +168,11 @@ export class BindingRegistry {
|
|
|
138
168
|
return undefined;
|
|
139
169
|
}
|
|
140
170
|
this.#bump();
|
|
171
|
+
this.#lastAdded = undefined;
|
|
141
172
|
byId.delete(id);
|
|
142
173
|
const key = binding.token;
|
|
143
174
|
if (this.#lone.get(key)?.identifier === id) {
|
|
144
|
-
this.#
|
|
175
|
+
this.#deleteLone(key);
|
|
145
176
|
return binding;
|
|
146
177
|
}
|
|
147
178
|
const record = this.#records?.get(key);
|
|
@@ -179,14 +210,6 @@ export class BindingRegistry {
|
|
|
179
210
|
getRecorded(token) {
|
|
180
211
|
return this.#records?.get(token)?.bindings ?? NO_BINDINGS;
|
|
181
212
|
}
|
|
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;
|
|
189
|
-
}
|
|
190
213
|
/** Get binding by ID. */
|
|
191
214
|
getById(id) {
|
|
192
215
|
return this.#ensureById().get(id);
|
|
@@ -213,8 +236,9 @@ export class BindingRegistry {
|
|
|
213
236
|
/** Remove all bindings. Returns all removed. */
|
|
214
237
|
clear() {
|
|
215
238
|
this.#bump();
|
|
239
|
+
this.#lastAdded = undefined;
|
|
216
240
|
const all = this.allBindings();
|
|
217
|
-
this.#
|
|
241
|
+
this.#clearLone();
|
|
218
242
|
this.#records?.clear();
|
|
219
243
|
this.#byId?.clear();
|
|
220
244
|
return all;
|
|
@@ -260,6 +284,26 @@ export class BindingRegistry {
|
|
|
260
284
|
getMultiTagged(token, criterion) {
|
|
261
285
|
return this.#records?.get(token)?.multi?.get(criterion);
|
|
262
286
|
}
|
|
287
|
+
/**
|
|
288
|
+
* Whether any binding for the token carries a `when()` predicate.
|
|
289
|
+
*
|
|
290
|
+
* @remarks Only a token that keeps a record can hold a predicate — a lone binding is default-slot
|
|
291
|
+
* with none — so an index fast lane consults this to decline to full selection when the
|
|
292
|
+
* more-specific rule's predicate step could apply.
|
|
293
|
+
*/
|
|
294
|
+
hasPredicateCandidate(token) {
|
|
295
|
+
const bindings = this.getRecorded(token);
|
|
296
|
+
for (let index = 0; index < bindings.length; index += 1) {
|
|
297
|
+
if (bindings[index].predicate !== undefined) {
|
|
298
|
+
return true;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
return false;
|
|
302
|
+
}
|
|
303
|
+
/** The binding holding a token's default slot — lone or recorded — or `undefined` when none does. */
|
|
304
|
+
getDefaultSlotBinding(token) {
|
|
305
|
+
return this.#lone.get(token) ?? this.#records?.get(token)?.defaultOccupant;
|
|
306
|
+
}
|
|
263
307
|
/** A token's lone default-slot binding — the first read of every synchronous resolve. */
|
|
264
308
|
getFastDefault(token) {
|
|
265
309
|
return this.#lone.get(token);
|
|
@@ -276,7 +320,8 @@ export class BindingRegistry {
|
|
|
276
320
|
const key = binding.token;
|
|
277
321
|
if (this.#lone.get(key) === binding) {
|
|
278
322
|
this.#bump();
|
|
279
|
-
this.#
|
|
323
|
+
this.#lastAdded = undefined;
|
|
324
|
+
this.#deleteLone(key);
|
|
280
325
|
}
|
|
281
326
|
else {
|
|
282
327
|
const record = this.#records?.get(key);
|
|
@@ -285,6 +330,7 @@ export class BindingRegistry {
|
|
|
285
330
|
return false;
|
|
286
331
|
}
|
|
287
332
|
this.#bump();
|
|
333
|
+
this.#lastAdded = undefined;
|
|
288
334
|
// Replaced, never spliced: a walk holding the current array must not lose its place.
|
|
289
335
|
record.bindings = record.bindings.toSpliced(index, 1);
|
|
290
336
|
this.#deindexSlot(record, binding);
|
|
@@ -299,19 +345,26 @@ export class BindingRegistry {
|
|
|
299
345
|
*/
|
|
300
346
|
setMany(binding) {
|
|
301
347
|
this.#bump();
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
this.#
|
|
348
|
+
// Set before any (re)indexing so `#indexSlot` sees a member and leaves it out of every slot.
|
|
349
|
+
writableMembership(binding).isMany = true;
|
|
350
|
+
if (binding === this.#lastAdded) {
|
|
351
|
+
// The chain refining what it just added: where the binding sits is known without a probe.
|
|
352
|
+
const record = this.#lastAddedRecord;
|
|
353
|
+
if (record === undefined) {
|
|
354
|
+
this.#deleteLone(binding.token);
|
|
355
|
+
this.#lastAddedRecord = this.#createRecord(binding.token, [binding]);
|
|
356
|
+
}
|
|
357
|
+
else if (record.defaultOccupant === binding) {
|
|
358
|
+
record.defaultOccupant = undefined;
|
|
359
|
+
}
|
|
307
360
|
return;
|
|
308
361
|
}
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
if (record?.defaultOccupant === binding) {
|
|
312
|
-
record.defaultOccupant = undefined;
|
|
362
|
+
if (this.#promoteLoneToRecord(binding.token, binding)) {
|
|
363
|
+
return;
|
|
313
364
|
}
|
|
314
|
-
|
|
365
|
+
// A default occupant that becomes a member frees its slot, so drop the stale index entry. A
|
|
366
|
+
// member cannot collapse a record back to lone, so this path does not settle.
|
|
367
|
+
this.#clearDefaultOccupant(binding.token, binding);
|
|
315
368
|
}
|
|
316
369
|
/**
|
|
317
370
|
* Adds a predicate to a live binding in place.
|
|
@@ -322,25 +375,38 @@ export class BindingRegistry {
|
|
|
322
375
|
*/
|
|
323
376
|
setPredicate(binding, predicate) {
|
|
324
377
|
this.#bump();
|
|
378
|
+
// Set before any (re)indexing: a predicate-only binding holds no default slot.
|
|
325
379
|
writablePredicate(binding).predicate = predicate;
|
|
326
|
-
|
|
327
|
-
if (this.#lone.get(key) === binding) {
|
|
328
|
-
this.#lone.delete(key);
|
|
329
|
-
this.#createRecord(key, [binding]);
|
|
380
|
+
if (this.#promoteLoneToRecord(binding.token, binding)) {
|
|
330
381
|
return;
|
|
331
382
|
}
|
|
383
|
+
// The binding may have vacated the default slot, and a narrowed record can collapse back to lone.
|
|
384
|
+
const record = this.#clearDefaultOccupant(binding.token, binding);
|
|
385
|
+
if (record !== undefined) {
|
|
386
|
+
this.#settle(binding.token, record);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* Moves a binding still holding the lone seat into a fresh one-binding record, returning whether it did.
|
|
391
|
+
*
|
|
392
|
+
* @remarks Its field (`isMany` or `predicate`) is written before this call, so the founding
|
|
393
|
+
* `#indexSlot` files it under the slot it now holds.
|
|
394
|
+
*/
|
|
395
|
+
#promoteLoneToRecord(key, binding) {
|
|
396
|
+
if (this.#lone.get(key) !== binding) {
|
|
397
|
+
return false;
|
|
398
|
+
}
|
|
399
|
+
this.#deleteLone(key);
|
|
400
|
+
this.#createRecord(key, [binding]);
|
|
401
|
+
return true;
|
|
402
|
+
}
|
|
403
|
+
/** Clears the default-slot index entry a binding has vacated, returning its record if one exists. */
|
|
404
|
+
#clearDefaultOccupant(key, binding) {
|
|
332
405
|
const record = this.#records?.get(key);
|
|
333
|
-
// The binding is now predicate-only, so it no longer holds the default slot it may have held.
|
|
334
406
|
if (record?.defaultOccupant === binding) {
|
|
335
407
|
record.defaultOccupant = undefined;
|
|
336
408
|
}
|
|
337
|
-
|
|
338
|
-
this.#settle(key, record);
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
/** Summarize available slot strings for a token (for error messages). */
|
|
342
|
-
availableSlotStrings(token) {
|
|
343
|
-
return this.getAll(token).map((binding) => bindingSlotToString(binding.slot));
|
|
409
|
+
return record;
|
|
344
410
|
}
|
|
345
411
|
#createRecord(key, bindings) {
|
|
346
412
|
const record = createTokenRecord(bindings);
|
|
@@ -376,12 +442,10 @@ export class BindingRegistry {
|
|
|
376
442
|
}
|
|
377
443
|
// A selection may be walking this list inside a `when()` predicate. An append past the length it
|
|
378
444
|
// read cannot shift it, so it lands in place; a displacement replaces the array instead.
|
|
379
|
-
if (displacedBinding
|
|
380
|
-
record.bindings.
|
|
381
|
-
}
|
|
382
|
-
else {
|
|
383
|
-
record.bindings = [...record.bindings.filter((candidate) => candidate !== displacedBinding), binding];
|
|
445
|
+
if (displacedBinding !== undefined) {
|
|
446
|
+
record.bindings = record.bindings.filter((candidate) => candidate !== displacedBinding);
|
|
384
447
|
}
|
|
448
|
+
insertInRegistrationOrder(record, binding);
|
|
385
449
|
this.#byId?.set(binding.identifier, binding);
|
|
386
450
|
this.#indexSlot(record, binding);
|
|
387
451
|
this.#settle(key, record);
|
|
@@ -395,8 +459,22 @@ export class BindingRegistry {
|
|
|
395
459
|
}
|
|
396
460
|
else if (bindings.length === 1 && isDefaultSlotBinding(bindings[0])) {
|
|
397
461
|
this.#records.delete(key);
|
|
398
|
-
this.#
|
|
462
|
+
this.#setLone(key, bindings[0]);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
// Every write to the lone map goes through these three, so a cache of its entries has one place to follow them.
|
|
466
|
+
#setLone(key, binding) {
|
|
467
|
+
let lone = this.#lone;
|
|
468
|
+
if (lone === EMPTY_LONE) {
|
|
469
|
+
lone = this.#lone = new Map();
|
|
399
470
|
}
|
|
471
|
+
lone.set(key, binding);
|
|
472
|
+
}
|
|
473
|
+
#deleteLone(key) {
|
|
474
|
+
this.#lone.delete(key);
|
|
475
|
+
}
|
|
476
|
+
#clearLone() {
|
|
477
|
+
this.#lone.clear();
|
|
400
478
|
}
|
|
401
479
|
#ensureById() {
|
|
402
480
|
if (this.#byId === undefined) {
|
|
@@ -462,6 +540,24 @@ export class BindingRegistry {
|
|
|
462
540
|
}
|
|
463
541
|
}
|
|
464
542
|
}
|
|
543
|
+
/**
|
|
544
|
+
* Places a binding in its record by registration order: a fresh registration appends, and a binding a
|
|
545
|
+
* chain took out and put back returns to its place.
|
|
546
|
+
*/
|
|
547
|
+
function insertInRegistrationOrder(record, binding) {
|
|
548
|
+
const { bindings } = record;
|
|
549
|
+
const order = binding.registrationOrder;
|
|
550
|
+
if (bindings.length === 0 || bindings.at(-1).registrationOrder < order) {
|
|
551
|
+
bindings.push(binding);
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
let index = bindings.length - 1;
|
|
555
|
+
while (index > 0 && bindings[index - 1].registrationOrder > order) {
|
|
556
|
+
index -= 1;
|
|
557
|
+
}
|
|
558
|
+
// Replaced, never spliced: a walk holding the current array must not lose its place.
|
|
559
|
+
record.bindings = bindings.toSpliced(index, 0, binding);
|
|
560
|
+
}
|
|
465
561
|
/** A binding nothing has to be matched against: the default slot, no predicate, not a collection member. */
|
|
466
562
|
function isDefaultSlotBinding(binding) {
|
|
467
563
|
return binding.slot.tags.length === 0 && binding.predicate === undefined && !binding.isMany;
|
|
@@ -13,4 +13,21 @@ export declare function stateEpoch(): number;
|
|
|
13
13
|
*
|
|
14
14
|
* @since 0.10.0
|
|
15
15
|
*/
|
|
16
|
-
export declare function advanceStateEpoch(): void;
|
|
16
|
+
export declare function advanceStateEpoch(): void;
|
|
17
|
+
/**
|
|
18
|
+
* The container-disposal counter, held in a cell so a hot reader dereferences a field rather than
|
|
19
|
+
* paying a cross-module call — apart from {@link stateEpoch} so a per-request child's `dispose()`
|
|
20
|
+
* does not invalidate the chain-version memo that keeps deep resolves cheap. A child compares it to
|
|
21
|
+
* learn whether an ancestor was disposed, re-walking the chain only after a disposal somewhere.
|
|
22
|
+
*
|
|
23
|
+
* @since 0.11.0
|
|
24
|
+
*/
|
|
25
|
+
export declare const disposeEpochRef: {
|
|
26
|
+
value: number;
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Advances the dispose epoch; a container's teardown calls it so descendants re-check the chain.
|
|
30
|
+
*
|
|
31
|
+
* @since 0.11.0
|
|
32
|
+
*/
|
|
33
|
+
export declare function advanceDisposeEpoch(): void;
|
package/dist/core/state-epoch.js
CHANGED
|
@@ -18,4 +18,21 @@ export function stateEpoch() {
|
|
|
18
18
|
*/
|
|
19
19
|
export function advanceStateEpoch() {
|
|
20
20
|
epoch += 1;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* The container-disposal counter, held in a cell so a hot reader dereferences a field rather than
|
|
24
|
+
* paying a cross-module call — apart from {@link stateEpoch} so a per-request child's `dispose()`
|
|
25
|
+
* does not invalidate the chain-version memo that keeps deep resolves cheap. A child compares it to
|
|
26
|
+
* learn whether an ancestor was disposed, re-walking the chain only after a disposal somewhere.
|
|
27
|
+
*
|
|
28
|
+
* @since 0.11.0
|
|
29
|
+
*/
|
|
30
|
+
export const disposeEpochRef = { value: 0 };
|
|
31
|
+
/**
|
|
32
|
+
* Advances the dispose epoch; a container's teardown calls it so descendants re-check the chain.
|
|
33
|
+
*
|
|
34
|
+
* @since 0.11.0
|
|
35
|
+
*/
|
|
36
|
+
export function advanceDisposeEpoch() {
|
|
37
|
+
disposeEpochRef.value += 1;
|
|
21
38
|
}
|
package/dist/core/tag.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** Tag keys and the interned pairs they mint — the one way a slot criterion is built. */
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* The bits a tag-key mask holds: the width of a 32-bit integer, a constant of the machine.
|
|
4
4
|
*
|
|
5
5
|
* @remarks Ids past this wrap, so two keys can share a bit. The mask is a prefilter only — a shared
|
|
6
6
|
* bit costs a false positive that the identity comparison then rejects, never a false negative.
|
package/dist/core/token.d.ts
CHANGED
package/dist/core/types.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import type { Constructor } from "
|
|
2
|
-
import type { BindingTag, TagKeyMask } from "
|
|
3
|
-
import type { Token } from "
|
|
4
|
-
export type { Constructor } from "
|
|
5
|
-
export type { BindingTag, TagKey, TagKeyMask } from "
|
|
1
|
+
import type { Constructor } from "#core/constructor-type";
|
|
2
|
+
import type { BindingTag, TagKeyMask } from "#core/tag";
|
|
3
|
+
import type { Token } from "#core/token";
|
|
4
|
+
export type { Constructor } from "#core/constructor-type";
|
|
5
|
+
export type { BindingTag, TagKey, TagKeyMask } from "#core/tag";
|
|
6
6
|
/**
|
|
7
7
|
* Token or class constructor used as a binding / injection / resolve key.
|
|
8
8
|
*
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns the metadata record a decorator context carries.
|
|
3
|
+
*
|
|
4
|
+
* @throws `MissingDecoratorMetadataError` when the transpiler handed none, which is how a runtime
|
|
5
|
+
* without `Symbol.metadata` shows up.
|
|
6
|
+
*
|
|
7
|
+
* @since 0.11.0
|
|
8
|
+
*/
|
|
9
|
+
export declare function decoratorMetadataOf(context: DecoratorContext, decoratorName: string): Record<string | symbol, unknown>;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The metadata record a decorator writes into, checked once for every decorator the library ships.
|
|
3
|
+
*/
|
|
4
|
+
import { MissingDecoratorMetadataError } from "#errors/errors";
|
|
5
|
+
/**
|
|
6
|
+
* Returns the metadata record a decorator context carries.
|
|
7
|
+
*
|
|
8
|
+
* @throws `MissingDecoratorMetadataError` when the transpiler handed none, which is how a runtime
|
|
9
|
+
* without `Symbol.metadata` shows up.
|
|
10
|
+
*
|
|
11
|
+
* @since 0.11.0
|
|
12
|
+
*/
|
|
13
|
+
export function decoratorMetadataOf(context, decoratorName) {
|
|
14
|
+
// Widened, not asserted: the lib types promise a record that a runtime without `Symbol.metadata` never hands over.
|
|
15
|
+
const metadata = context.metadata;
|
|
16
|
+
if (metadata === undefined || metadata === null) {
|
|
17
|
+
throw new MissingDecoratorMetadataError(decoratorName);
|
|
18
|
+
}
|
|
19
|
+
return metadata;
|
|
20
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** `@inject` — the accessor-decorator channel, resolving from the ambient container. */
|
|
2
|
-
import type { Token } from "
|
|
3
|
-
import type { Constructor } from "
|
|
4
|
-
import type { InjectionDescriptor, InjectOptions } from "
|
|
2
|
+
import type { Token } from "#core/token";
|
|
3
|
+
import type { Constructor } from "#core/types";
|
|
4
|
+
import type { InjectionDescriptor, InjectOptions } from "#injection/descriptor";
|
|
5
5
|
type ClassAccessorDecorator<This, Value> = (target: ClassAccessorDecoratorTarget<This, Value>, context: ClassAccessorDecoratorContext<This, Value>) => ClassAccessorDecoratorResult<This, Value> | void;
|
|
6
6
|
/**
|
|
7
7
|
* Creates a dual-role value: an injection descriptor that also works as a class accessor decorator.
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { getActiveContainer, getAmbientResolution } from "
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
1
|
+
import { getActiveContainer, getAmbientResolution } from "#ambient/active-container";
|
|
2
|
+
import { decoratorMetadataOf } from "#decorators/decorator-metadata";
|
|
3
|
+
import { MissingContainerContextError, StaticMemberDecoratorError } from "#errors/errors";
|
|
4
|
+
import { buildInjectionDescriptor } from "#injection/descriptor";
|
|
5
|
+
import { injectionSlotToResolveOptions } from "#injection/resolve-options";
|
|
6
|
+
import { INJECT_ACCESSOR_KEY } from "#metadata/metadata-keys";
|
|
6
7
|
/**
|
|
7
8
|
* The name of the class being constructed, or `undefined` when there is none to report.
|
|
8
9
|
*
|
|
@@ -36,7 +37,7 @@ export function inject(token, options) {
|
|
|
36
37
|
if (context.static) {
|
|
37
38
|
throw new StaticMemberDecoratorError("inject", String(context.name));
|
|
38
39
|
}
|
|
39
|
-
const meta = context
|
|
40
|
+
const meta = decoratorMetadataOf(context, "inject");
|
|
40
41
|
// Own bucket only: the metadata record inherits the base class's, and pushing into an inherited
|
|
41
42
|
// array would register this accessor on the base class instead.
|
|
42
43
|
if (!Object.hasOwn(meta, INJECT_ACCESSOR_KEY) || !Array.isArray(meta[INJECT_ACCESSOR_KEY])) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { BindingScope, Constructor } from "
|
|
2
|
-
import type { InjectableDependency, ResolvedDependencyValue } from "
|
|
1
|
+
import type { BindingScope, Constructor } from "#core/types";
|
|
2
|
+
import type { InjectableDependency, ResolvedDependencyValue } from "#injection/descriptor";
|
|
3
3
|
/**
|
|
4
4
|
* The collector `@injectable` registers a class into, for a container to bind later.
|
|
5
5
|
*
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { decoratorMetadataOf } from "#decorators/decorator-metadata";
|
|
2
|
+
import { normalizeToDescriptor } from "#injection/descriptor";
|
|
3
|
+
import { INJECTABLE_KEY } from "#metadata/metadata-keys";
|
|
3
4
|
/**
|
|
4
5
|
* Creates an empty auto-register registry.
|
|
5
6
|
*
|
|
@@ -23,6 +24,7 @@ export function createAutoRegisterRegistry() {
|
|
|
23
24
|
*/
|
|
24
25
|
export function injectable(deps, options) {
|
|
25
26
|
return function (target, context) {
|
|
27
|
+
const meta = decoratorMetadataOf(context, "injectable");
|
|
26
28
|
const parameterMetadataList = (deps ?? []).map((dependency, index) => {
|
|
27
29
|
const descriptor = normalizeToDescriptor(dependency);
|
|
28
30
|
const baseParameterMetadata = {
|
|
@@ -44,7 +46,7 @@ export function injectable(deps, options) {
|
|
|
44
46
|
});
|
|
45
47
|
// Field decorators run before the class decorator — accessor @inject entries are
|
|
46
48
|
// already on context.metadata by the time this runs.
|
|
47
|
-
|
|
49
|
+
meta[INJECTABLE_KEY] = {
|
|
48
50
|
params: parameterMetadataList,
|
|
49
51
|
};
|
|
50
52
|
if (options?.autoRegister !== undefined) {
|
|
@@ -1,12 +1,18 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { decoratorMetadataOf } from "#decorators/decorator-metadata";
|
|
2
|
+
import { StaticMemberDecoratorError, SymbolKeyedLifecycleError } from "#errors/errors";
|
|
3
|
+
import { LIFECYCLE_KEY } from "#metadata/metadata-keys";
|
|
3
4
|
/** Records the decorated method under one lifecycle phase; both decorators differ only in that phase. */
|
|
4
5
|
function recordLifecycleMethod(phase) {
|
|
5
6
|
return function (target, context) {
|
|
6
7
|
if (context.static) {
|
|
7
8
|
throw new StaticMemberDecoratorError(phase, String(context.name));
|
|
8
9
|
}
|
|
9
|
-
|
|
10
|
+
// The lifecycle reader keys methods by their string name, so a symbol-keyed method can never be
|
|
11
|
+
// found again — fail here, where the declaration is, rather than at resolve.
|
|
12
|
+
if (typeof context.name === "symbol") {
|
|
13
|
+
throw new SymbolKeyedLifecycleError(phase, String(context.name));
|
|
14
|
+
}
|
|
15
|
+
const meta = decoratorMetadataOf(context, phase);
|
|
10
16
|
// Own bucket only: `context.metadata` inherits the base class's record, and writing through an
|
|
11
17
|
// inherited bucket would register this hook on the base class instead.
|
|
12
18
|
if (!Object.hasOwn(meta, LIFECYCLE_KEY)) {
|