@codefast/di 0.10.1 → 0.12.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 (88) hide show
  1. package/CHANGELOG.md +376 -0
  2. package/README.md +94 -17
  3. package/dist/{ambient/active-container.d.ts → ambient-container.d.ts} +7 -2
  4. package/dist/{ambient/active-container.js → ambient-container.js} +6 -1
  5. package/dist/container/{binding-builders.d.ts → binding-chain.d.ts} +27 -16
  6. package/dist/container/binding-chain.js +254 -0
  7. package/dist/container/container.d.ts +11 -2
  8. package/dist/container/container.js +293 -73
  9. package/dist/core/binding-builders.d.ts +114 -0
  10. package/dist/core/binding-declaration.d.ts +127 -0
  11. package/dist/core/binding-declaration.js +187 -0
  12. package/dist/core/binding.d.ts +62 -138
  13. package/dist/core/binding.js +54 -17
  14. package/dist/core/module.d.ts +8 -5
  15. package/dist/core/module.js +17 -3
  16. package/dist/core/registry.d.ts +17 -28
  17. package/dist/core/registry.js +92 -133
  18. package/dist/core/state-epoch.d.ts +18 -1
  19. package/dist/core/state-epoch.js +17 -0
  20. package/dist/core/tag.js +1 -1
  21. package/dist/decorators/inject.js +6 -5
  22. package/dist/decorators/injectable.js +4 -2
  23. package/dist/decorators/{lifecycle-decorators.js → lifecycle.js} +9 -3
  24. package/dist/decorators/metadata-record.d.ts +9 -0
  25. package/dist/decorators/metadata-record.js +20 -0
  26. package/dist/{errors/errors.d.ts → errors.d.ts} +84 -9
  27. package/dist/{errors/errors.js → errors.js} +101 -17
  28. package/dist/index.d.ts +14 -10
  29. package/dist/index.js +7 -6
  30. package/dist/injection/{resolve-options.js → dependency-slot.js} +6 -4
  31. package/dist/injection/descriptor.d.ts +1 -1
  32. package/dist/injection/descriptor.js +3 -7
  33. package/dist/introspection/dependency-graph.d.ts +8 -3
  34. package/dist/introspection/dependency-graph.js +48 -26
  35. package/dist/introspection/explanation.d.ts +101 -0
  36. package/dist/introspection/explanation.js +151 -0
  37. package/dist/introspection/graph-adapters/reactflow.js +6 -4
  38. package/dist/introspection/inspector.d.ts +9 -2
  39. package/dist/introspection/inspector.js +30 -28
  40. package/dist/lifecycle/{lifecycle-manager.d.ts → hooks.d.ts} +9 -2
  41. package/dist/lifecycle/{lifecycle-manager.js → hooks.js} +59 -30
  42. package/dist/lifecycle/{scope-manager.d.ts → scopes.d.ts} +3 -5
  43. package/dist/lifecycle/{scope-manager.js → scopes.js} +36 -29
  44. package/dist/metadata/{metadata-reader-token.d.ts → reader-token.d.ts} +1 -1
  45. package/dist/metadata/{symbol-metadata-reader.d.ts → symbol-reader.d.ts} +1 -1
  46. package/dist/metadata/{symbol-metadata-reader.js → symbol-reader.js} +1 -1
  47. package/dist/metadata/{metadata-types.d.ts → types.d.ts} +1 -1
  48. package/dist/metadata/types.js +1 -0
  49. package/dist/metadata/verify.d.ts +43 -0
  50. package/dist/metadata/verify.js +133 -0
  51. package/dist/metadata/{verifying-metadata-reader.d.ts → verifying-reader.d.ts} +5 -4
  52. package/dist/metadata/verifying-reader.js +53 -0
  53. package/dist/resolution/async-fan-out.d.ts +12 -0
  54. package/dist/resolution/async-fan-out.js +26 -0
  55. package/dist/resolution/cache/activation-need.d.ts +1 -2
  56. package/dist/resolution/cache/activation-need.js +16 -19
  57. package/dist/resolution/cache/class-introspector.d.ts +38 -36
  58. package/dist/resolution/cache/class-introspector.js +79 -159
  59. package/dist/resolution/cache/{binding-lookup-cache.d.ts → lookup.d.ts} +7 -9
  60. package/dist/resolution/cache/{binding-lookup-cache.js → lookup.js} +70 -25
  61. package/dist/resolution/context.d.ts +16 -24
  62. package/dist/resolution/context.js +47 -56
  63. package/dist/resolution/path.d.ts +128 -0
  64. package/dist/resolution/path.js +167 -0
  65. package/dist/resolution/plan/{plan-codegen.d.ts → codegen.d.ts} +5 -4
  66. package/dist/resolution/plan/{plan-codegen.js → codegen.js} +58 -32
  67. package/dist/resolution/plan/{instantiation-plan.d.ts → compiler.d.ts} +1 -1
  68. package/dist/resolution/plan/{instantiation-plan.js → compiler.js} +64 -24
  69. package/dist/resolution/resolver.d.ts +11 -12
  70. package/dist/resolution/resolver.js +573 -356
  71. package/dist/resolution/select/{binding-select.d.ts → candidates.d.ts} +30 -0
  72. package/dist/resolution/select/{binding-select.js → candidates.js} +51 -10
  73. package/dist/resolution/select/constraints.js +1 -1
  74. package/package.json +87 -67
  75. package/dist/container/binding-builders.js +0 -305
  76. package/dist/core/binding-scope.d.ts +0 -12
  77. package/dist/core/binding-scope.js +0 -12
  78. package/dist/metadata/verifying-metadata-reader.js +0 -31
  79. package/dist/resolution/path/resolution-path.d.ts +0 -93
  80. package/dist/resolution/path/resolution-path.js +0 -116
  81. /package/dist/{metadata/metadata-types.js → core/binding-builders.js} +0 -0
  82. /package/dist/decorators/{lifecycle-decorators.d.ts → lifecycle.d.ts} +0 -0
  83. /package/dist/injection/{resolve-options.d.ts → dependency-slot.d.ts} +0 -0
  84. /package/dist/{errors → introspection}/diagnostics.d.ts +0 -0
  85. /package/dist/{errors → introspection}/diagnostics.js +0 -0
  86. /package/dist/metadata/{metadata-keys.d.ts → keys.d.ts} +0 -0
  87. /package/dist/metadata/{metadata-keys.js → keys.js} +0 -0
  88. /package/dist/metadata/{metadata-reader-token.js → reader-token.js} +0 -0
@@ -1,4 +1,4 @@
1
- import { bindingSlotEquals, bindingSlotToString, writableMembership, writablePredicate } from "#core/binding";
1
+ import { bindingSlotEquals } from "#core/binding";
2
2
  import { getOrInsert } from "#core/map-upsert";
3
3
  import { advanceStateEpoch } from "#core/state-epoch";
4
4
  const NO_BINDINGS = Object.freeze([]);
@@ -16,12 +16,22 @@ 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
+ const HELD_CONSTANT = 1;
25
+ const HELD_ALIAS = 2;
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;
@@ -30,9 +40,9 @@ export class BindingRegistry {
30
40
  #byId;
31
41
  // Set when the first tagged slot lands and never cleared, like the tagged maps it stands for.
32
42
  #taggedIndexBuilt = false;
33
- // Set on the first constant registered and never cleared. Teardown only needs the negative answer
34
- // to be exact, and that is what lets a container holding no constant skip its sweep entirely.
35
- #heldConstantBinding = false;
43
+ // Kinds registered here at least once, never cleared: only the negative answers must be exact, so a
44
+ // container holding no constant skips its teardown sweep and one holding no alias skips its forwarding probe.
45
+ #heldKinds = 0;
36
46
  /** Monotonic version — increments on every mutation. */
37
47
  get version() {
38
48
  return this.#version;
@@ -45,7 +55,11 @@ export class BindingRegistry {
45
55
  }
46
56
  /** Whether a constant has ever been registered here, and so whether teardown has anything to sweep. */
47
57
  get hasHeldConstantBinding() {
48
- return this.#heldConstantBinding;
58
+ return (this.#heldKinds & HELD_CONSTANT) !== 0;
59
+ }
60
+ /** Whether an alias has ever been registered here, and so whether a candidate list can hold one. */
61
+ get hasHeldAlias() {
62
+ return (this.#heldKinds & HELD_ALIAS) !== 0;
49
63
  }
50
64
  /** Whether the deferred tagged-slot index has had to be built. */
51
65
  get isTaggedIndexBuilt() {
@@ -75,18 +89,27 @@ export class BindingRegistry {
75
89
  add(binding) {
76
90
  this.#bump();
77
91
  if (binding.kind === "constant") {
78
- this.#heldConstantBinding = true;
92
+ this.#heldKinds |= HELD_CONSTANT;
93
+ }
94
+ else if (binding.kind === "alias") {
95
+ this.#heldKinds |= HELD_ALIAS;
79
96
  }
80
97
  const key = binding.token;
81
- const record = this.#records?.get(key);
82
- if (record !== undefined) {
83
- return this.#addToRecord(key, record, binding);
98
+ const records = this.#records;
99
+ if (records !== undefined) {
100
+ const record = records.get(key);
101
+ if (record !== undefined) {
102
+ return this.#addToRecord(key, record, binding);
103
+ }
84
104
  }
85
- const lone = this.#lone.get(key);
86
- if (lone === undefined) {
87
- this.#byId?.set(binding.identifier, binding);
105
+ const occupant = this.#lone.get(key);
106
+ if (occupant === undefined) {
107
+ // The common bind: a fresh token taking the lone seat, one probe and one write.
108
+ if (this.#byId !== undefined) {
109
+ this.#byId.set(binding.identifier, binding);
110
+ }
88
111
  if (isDefaultSlotBinding(binding)) {
89
- this.#lone.set(key, binding);
112
+ this.#setLone(key, binding);
90
113
  }
91
114
  else {
92
115
  this.#createRecord(key, [binding]);
@@ -95,17 +118,17 @@ export class BindingRegistry {
95
118
  }
96
119
  // Same slot, last wins: the newcomer takes the lone seat and nothing else moves.
97
120
  if (isDefaultSlotBinding(binding)) {
98
- this.#lone.set(key, binding);
121
+ this.#setLone(key, binding);
99
122
  if (this.#byId !== undefined) {
100
- this.#byId.delete(lone.identifier);
123
+ this.#byId.delete(occupant.identifier);
101
124
  this.#byId.set(binding.identifier, binding);
102
125
  }
103
- return lone;
126
+ return occupant;
104
127
  }
105
- // A second shape joins the token, which is what a record is for.
106
- this.#lone.delete(key);
128
+ // A second shape joins the token, which is what a record is for; every add is the newest, so it goes last.
129
+ this.#deleteLone(key);
107
130
  this.#byId?.set(binding.identifier, binding);
108
- this.#createRecord(key, [lone, binding]);
131
+ this.#createRecord(key, [occupant, binding]);
109
132
  return undefined;
110
133
  }
111
134
  /** Remove all bindings for a token. Returns removed bindings. */
@@ -113,7 +136,7 @@ export class BindingRegistry {
113
136
  this.#bump();
114
137
  const lone = this.#lone.get(token);
115
138
  if (lone !== undefined) {
116
- this.#lone.delete(token);
139
+ this.#deleteLone(token);
117
140
  this.#byId?.delete(lone.identifier);
118
141
  return [lone];
119
142
  }
@@ -141,7 +164,7 @@ export class BindingRegistry {
141
164
  byId.delete(id);
142
165
  const key = binding.token;
143
166
  if (this.#lone.get(key)?.identifier === id) {
144
- this.#lone.delete(key);
167
+ this.#deleteLone(key);
145
168
  return binding;
146
169
  }
147
170
  const record = this.#records?.get(key);
@@ -179,14 +202,6 @@ export class BindingRegistry {
179
202
  getRecorded(token) {
180
203
  return this.#records?.get(token)?.bindings ?? NO_BINDINGS;
181
204
  }
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
205
  /** Get binding by ID. */
191
206
  getById(id) {
192
207
  return this.#ensureById().get(id);
@@ -214,26 +229,11 @@ export class BindingRegistry {
214
229
  clear() {
215
230
  this.#bump();
216
231
  const all = this.allBindings();
217
- this.#lone.clear();
232
+ this.#clearLone();
218
233
  this.#records?.clear();
219
234
  this.#byId?.clear();
220
235
  return all;
221
236
  }
222
- /** Whether a slot-based binding currently occupies `binding`'s slot, so adding it would displace. */
223
- hasSlotOccupant(binding) {
224
- if (isPurePredicateBinding(binding)) {
225
- return false;
226
- }
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) {
233
- return false;
234
- }
235
- return this.#slotOccupant(record, binding.slot) !== undefined;
236
- }
237
237
  /**
238
238
  * The binding indexed under one criterion.
239
239
  *
@@ -260,99 +260,42 @@ export class BindingRegistry {
260
260
  getMultiTagged(token, criterion) {
261
261
  return this.#records?.get(token)?.multi?.get(criterion);
262
262
  }
263
- /** A token's lone default-slot binding — the first read of every synchronous resolve. */
264
- getFastDefault(token) {
265
- return this.#lone.get(token);
266
- }
267
263
  /**
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`.
264
+ * Whether any binding for the token carries a `when()` predicate.
270
265
  *
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.
266
+ * @remarks Only a token that keeps a record can hold a predicate — a lone binding is default-slot
267
+ * with none — so an index fast lane consults this to decline to full selection when the
268
+ * more-specific rule's predicate step could apply.
274
269
  */
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;
270
+ hasPredicateCandidate(token) {
271
+ const bindings = this.getRecorded(token);
272
+ for (let index = 0; index < bindings.length; index += 1) {
273
+ if (bindings[index].predicate !== undefined) {
274
+ return true;
286
275
  }
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
276
  }
293
- rewrite();
294
- return true;
277
+ return false;
295
278
  }
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)) {
305
- return;
306
- }
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);
279
+ /** The binding holding a token's default slot — lone or recorded — or `undefined` when none does. */
280
+ getDefaultSlotBinding(token) {
281
+ return this.#lone.get(token) ?? this.#records?.get(token)?.defaultOccupant;
310
282
  }
311
283
  /**
312
- * Adds a predicate to a live binding in place.
284
+ * The token's default-slot alias, which forwards a request's criteria to its target, or `undefined`.
313
285
  *
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.
286
+ * @remarks A registry that never held an alias answers without probing, so a request missing every slot of a
287
+ * token pays for forwarding only where forwarding can happen.
317
288
  */
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)) {
323
- return;
324
- }
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;
289
+ defaultSlotAlias(token) {
290
+ if ((this.#heldKinds & HELD_ALIAS) === 0) {
291
+ return undefined;
350
292
  }
351
- return record;
293
+ const occupant = this.getDefaultSlotBinding(token);
294
+ return occupant !== undefined && occupant.kind === "alias" ? occupant : undefined;
352
295
  }
353
- /** Summarize available slot strings for a token (for error messages). */
354
- availableSlotStrings(token) {
355
- return this.getAll(token).map((binding) => bindingSlotToString(binding.slot));
296
+ /** A token's lone default-slot binding — the first read of every synchronous resolve. */
297
+ getFastDefault(token) {
298
+ return this.#lone.get(token);
356
299
  }
357
300
  #createRecord(key, bindings) {
358
301
  const record = createTokenRecord(bindings);
@@ -388,12 +331,10 @@ export class BindingRegistry {
388
331
  }
389
332
  // A selection may be walking this list inside a `when()` predicate. An append past the length it
390
333
  // 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];
334
+ if (displacedBinding !== undefined) {
335
+ record.bindings = record.bindings.filter((candidate) => candidate !== displacedBinding);
396
336
  }
337
+ record.bindings.push(binding);
397
338
  this.#byId?.set(binding.identifier, binding);
398
339
  this.#indexSlot(record, binding);
399
340
  this.#settle(key, record);
@@ -407,9 +348,23 @@ export class BindingRegistry {
407
348
  }
408
349
  else if (bindings.length === 1 && isDefaultSlotBinding(bindings[0])) {
409
350
  this.#records.delete(key);
410
- this.#lone.set(key, bindings[0]);
351
+ this.#setLone(key, bindings[0]);
411
352
  }
412
353
  }
354
+ // Every write to the lone map goes through these three, so a cache of its entries has one place to follow them.
355
+ #setLone(key, binding) {
356
+ let lone = this.#lone;
357
+ if (lone === EMPTY_LONE) {
358
+ lone = this.#lone = new Map();
359
+ }
360
+ lone.set(key, binding);
361
+ }
362
+ #deleteLone(key) {
363
+ this.#lone.delete(key);
364
+ }
365
+ #clearLone() {
366
+ this.#lone.clear();
367
+ }
413
368
  #ensureById() {
414
369
  if (this.#byId === undefined) {
415
370
  const byId = new Map();
@@ -474,6 +429,10 @@ export class BindingRegistry {
474
429
  }
475
430
  }
476
431
  }
432
+ /**
433
+ * Places a binding in its record by registration order: a fresh registration appends, and a binding a
434
+ * chain took out and put back returns to its place.
435
+ */
477
436
  /** A binding nothing has to be matched against: the default slot, no predicate, not a collection member. */
478
437
  function isDefaultSlotBinding(binding) {
479
438
  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.
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,8 +1,9 @@
1
- import { getActiveContainer, getAmbientResolution } from "#ambient/active-container";
2
- import { MissingContainerContextError, StaticMemberDecoratorError } from "#errors/errors";
1
+ import { getActiveContainer, getAmbientResolution } from "#ambient-container";
2
+ import { decoratorMetadataOf } from "#decorators/metadata-record";
3
+ import { MissingContainerContextError, StaticMemberDecoratorError } from "#errors";
4
+ import { injectionSlotToResolveOptions } from "#injection/dependency-slot";
3
5
  import { buildInjectionDescriptor } from "#injection/descriptor";
4
- import { injectionSlotToResolveOptions } from "#injection/resolve-options";
5
- import { INJECT_ACCESSOR_KEY } from "#metadata/metadata-keys";
6
+ import { INJECT_ACCESSOR_KEY } from "#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,6 @@
1
+ import { decoratorMetadataOf } from "#decorators/metadata-record";
1
2
  import { normalizeToDescriptor } from "#injection/descriptor";
2
- import { INJECTABLE_KEY } from "#metadata/metadata-keys";
3
+ import { INJECTABLE_KEY } from "#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/metadata-record";
2
+ import { StaticMemberDecoratorError, SymbolKeyedLifecycleError } from "#errors";
3
+ import { LIFECYCLE_KEY } from "#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)) {
@@ -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";
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
+ }