@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.
Files changed (81) hide show
  1. package/CHANGELOG.md +293 -0
  2. package/README.md +71 -7
  3. package/dist/ambient/active-container.d.ts +13 -11
  4. package/dist/ambient/active-container.js +8 -6
  5. package/dist/container/binding-builders.d.ts +25 -7
  6. package/dist/container/binding-builders.js +110 -42
  7. package/dist/container/container.d.ts +8 -8
  8. package/dist/container/container.js +244 -67
  9. package/dist/core/binding-declaration.d.ts +120 -0
  10. package/dist/core/binding-declaration.js +186 -0
  11. package/dist/core/binding-scope.d.ts +2 -2
  12. package/dist/core/binding.d.ts +61 -9
  13. package/dist/core/binding.js +49 -2
  14. package/dist/core/constraint-requirement.d.ts +1 -1
  15. package/dist/core/module.d.ts +10 -7
  16. package/dist/core/module.js +17 -3
  17. package/dist/core/registry.d.ts +17 -13
  18. package/dist/core/registry.js +155 -59
  19. package/dist/core/state-epoch.d.ts +18 -1
  20. package/dist/core/state-epoch.js +17 -0
  21. package/dist/core/tag.js +1 -1
  22. package/dist/core/token.d.ts +1 -1
  23. package/dist/core/types.d.ts +5 -5
  24. package/dist/decorators/decorator-metadata.d.ts +9 -0
  25. package/dist/decorators/decorator-metadata.js +20 -0
  26. package/dist/decorators/inject.d.ts +3 -3
  27. package/dist/decorators/inject.js +7 -6
  28. package/dist/decorators/injectable.d.ts +2 -2
  29. package/dist/decorators/injectable.js +5 -3
  30. package/dist/decorators/lifecycle-decorators.js +9 -3
  31. package/dist/errors/errors.d.ts +85 -14
  32. package/dist/errors/errors.js +100 -20
  33. package/dist/index.d.ts +37 -35
  34. package/dist/index.js +20 -19
  35. package/dist/injection/descriptor.d.ts +3 -3
  36. package/dist/injection/descriptor.js +3 -7
  37. package/dist/injection/resolve-options.d.ts +3 -3
  38. package/dist/injection/resolve-options.js +7 -5
  39. package/dist/introspection/dependency-graph.d.ts +10 -5
  40. package/dist/introspection/dependency-graph.js +51 -28
  41. package/dist/introspection/graph-adapters/cytoscape.d.ts +1 -1
  42. package/dist/introspection/graph-adapters/dot.d.ts +1 -1
  43. package/dist/introspection/graph-adapters/mermaid.d.ts +1 -1
  44. package/dist/introspection/graph-adapters/reactflow.d.ts +1 -1
  45. package/dist/introspection/graph-adapters/reactflow.js +6 -4
  46. package/dist/introspection/inspector.d.ts +4 -4
  47. package/dist/introspection/inspector.js +9 -12
  48. package/dist/lifecycle/lifecycle-manager.d.ts +4 -4
  49. package/dist/lifecycle/lifecycle-manager.js +18 -6
  50. package/dist/lifecycle/scope-manager.d.ts +2 -2
  51. package/dist/lifecycle/scope-manager.js +31 -13
  52. package/dist/metadata/metadata-reader-token.d.ts +2 -2
  53. package/dist/metadata/metadata-reader-token.js +1 -1
  54. package/dist/metadata/metadata-types.d.ts +3 -3
  55. package/dist/metadata/symbol-metadata-reader.d.ts +3 -3
  56. package/dist/metadata/symbol-metadata-reader.js +1 -1
  57. package/dist/metadata/verifying-metadata-reader.d.ts +5 -4
  58. package/dist/metadata/verifying-metadata-reader.js +30 -8
  59. package/dist/resolution/async-fan-out.d.ts +12 -0
  60. package/dist/resolution/async-fan-out.js +26 -0
  61. package/dist/resolution/cache/activation-need.d.ts +4 -5
  62. package/dist/resolution/cache/activation-need.js +11 -18
  63. package/dist/resolution/cache/binding-lookup-cache.d.ts +5 -12
  64. package/dist/resolution/cache/binding-lookup-cache.js +32 -19
  65. package/dist/resolution/cache/class-introspector.d.ts +16 -5
  66. package/dist/resolution/cache/class-introspector.js +73 -56
  67. package/dist/resolution/context.d.ts +17 -25
  68. package/dist/resolution/context.js +47 -56
  69. package/dist/resolution/path/resolution-path.d.ts +48 -13
  70. package/dist/resolution/path/resolution-path.js +90 -39
  71. package/dist/resolution/plan/instantiation-plan.d.ts +5 -5
  72. package/dist/resolution/plan/instantiation-plan.js +66 -26
  73. package/dist/resolution/plan/plan-codegen.d.ts +10 -7
  74. package/dist/resolution/plan/plan-codegen.js +62 -34
  75. package/dist/resolution/resolver.d.ts +16 -17
  76. package/dist/resolution/resolver.js +316 -273
  77. package/dist/resolution/select/binding-select.d.ts +6 -5
  78. package/dist/resolution/select/binding-select.js +5 -4
  79. package/dist/resolution/select/constraints.d.ts +3 -3
  80. package/dist/resolution/select/constraints.js +4 -4
  81. package/package.json +14 -2
@@ -1,11 +1,9 @@
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";
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
- * One container's binding store, indexed by token, binding id, and slot for fast lookup.
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 `createBinding`, which is
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
  }
@@ -1,6 +1,6 @@
1
- import { bindingSlotEquals, bindingSlotToString, writableMembership, writablePredicate } from "#/core/binding";
2
- import { getOrInsert } from "#/core/map-upsert";
3
- import { advanceStateEpoch } from "#/core/state-epoch";
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 = new Map();
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 `createBinding`, which is
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 record = this.#records?.get(key);
82
- if (record !== undefined) {
83
- return this.#addToRecord(key, record, binding);
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 lone = this.#lone.get(key);
86
- if (lone === undefined) {
87
- this.#byId?.set(binding.identifier, binding);
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.#lone.set(key, binding);
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.#lone.set(key, binding);
126
+ this.#setLone(key, binding);
127
+ this.#lastAddedRecord = undefined;
99
128
  if (this.#byId !== undefined) {
100
- this.#byId.delete(lone.identifier);
129
+ this.#byId.delete(occupant.identifier);
101
130
  this.#byId.set(binding.identifier, binding);
102
131
  }
103
- return lone;
132
+ return occupant;
104
133
  }
105
134
  // A second shape joins the token, which is what a record is for.
106
- this.#lone.delete(key);
135
+ this.#deleteLone(key);
107
136
  this.#byId?.set(binding.identifier, binding);
108
- this.#createRecord(key, [lone, binding]);
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.#lone.delete(token);
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.#lone.delete(key);
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.#lone.clear();
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.#lone.delete(key);
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
- const key = binding.token;
303
- if (this.#lone.get(key) === binding) {
304
- writableMembership(binding).isMany = true;
305
- this.#lone.delete(key);
306
- this.#createRecord(key, [binding]);
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
- // A default occupant that becomes a member frees its slot, so drop the stale index entry.
310
- const record = this.#records?.get(key);
311
- if (record?.defaultOccupant === binding) {
312
- record.defaultOccupant = undefined;
362
+ if (this.#promoteLoneToRecord(binding.token, binding)) {
363
+ return;
313
364
  }
314
- writableMembership(binding).isMany = true;
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
- const key = binding.token;
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
- if (record !== undefined) {
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 === undefined) {
380
- record.bindings.push(binding);
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.#lone.set(key, bindings[0]);
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;
@@ -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
- * How many distinct tag keys one mask bit is shared by.
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.
@@ -1,4 +1,4 @@
1
- import type { Constructor } from "#/core/constructor-type";
1
+ import type { Constructor } from "#core/constructor-type";
2
2
  declare const TOKEN_BRAND: unique symbol;
3
3
  declare const TOKEN_NAMES_BRAND: unique symbol;
4
4
  /**
@@ -1,8 +1,8 @@
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";
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 "#/core/token";
3
- import type { Constructor } from "#/core/types";
4
- import type { InjectionDescriptor, InjectOptions } from "#/injection/descriptor";
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 "#/ambient/active-container";
2
- import { MissingContainerContextError, StaticMemberDecoratorError } from "#/errors/errors";
3
- import { buildInjectionDescriptor } from "#/injection/descriptor";
4
- import { injectionSlotToResolveOptions } from "#/injection/resolve-options";
5
- import { INJECT_ACCESSOR_KEY } from "#/metadata/metadata-keys";
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.metadata;
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 "#/core/types";
2
- import type { InjectableDependency, ResolvedDependencyValue } from "#/injection/descriptor";
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 { normalizeToDescriptor } from "#/injection/descriptor";
2
- import { INJECTABLE_KEY } from "#/metadata/metadata-keys";
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
- context.metadata[INJECTABLE_KEY] = {
49
+ meta[INJECTABLE_KEY] = {
48
50
  params: parameterMetadataList,
49
51
  };
50
52
  if (options?.autoRegister !== undefined) {
@@ -1,12 +1,18 @@
1
- import { StaticMemberDecoratorError } from "#/errors/errors";
2
- import { LIFECYCLE_KEY } from "#/metadata/metadata-keys";
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
- const meta = context.metadata;
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)) {