@codefast/di 0.11.0 → 0.13.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 (79) hide show
  1. package/CHANGELOG.md +151 -0
  2. package/README.md +128 -31
  3. package/dist/container/{binding-builders.d.ts → binding-chain.d.ts} +21 -28
  4. package/dist/container/{binding-builders.js → binding-chain.js} +53 -163
  5. package/dist/container/container.d.ts +11 -3
  6. package/dist/container/container.js +176 -102
  7. package/dist/core/binding-builders.d.ts +117 -0
  8. package/dist/core/binding-declaration.d.ts +9 -2
  9. package/dist/core/binding-declaration.js +5 -4
  10. package/dist/core/binding.d.ts +21 -149
  11. package/dist/core/binding.js +15 -25
  12. package/dist/core/module.d.ts +1 -1
  13. package/dist/core/registry.d.ts +9 -24
  14. package/dist/core/registry.js +30 -155
  15. package/dist/core/tag.js +1 -1
  16. package/dist/decorators/inject.d.ts +3 -1
  17. package/dist/decorators/inject.js +5 -5
  18. package/dist/decorators/injectable.js +2 -2
  19. package/dist/decorators/lifecycle.d.ts +20 -0
  20. package/dist/decorators/{lifecycle-decorators.js → lifecycle.js} +7 -3
  21. package/dist/decorators/{decorator-metadata.js → metadata-record.js} +1 -1
  22. package/dist/{errors/errors.d.ts → errors.d.ts} +29 -19
  23. package/dist/{errors/errors.js → errors.js} +43 -25
  24. package/dist/index.d.ts +12 -10
  25. package/dist/index.js +6 -6
  26. package/dist/injection/descriptor.d.ts +1 -1
  27. package/dist/introspection/dependency-graph.d.ts +1 -1
  28. package/dist/introspection/dependency-graph.js +3 -4
  29. package/dist/introspection/explanation.d.ts +101 -0
  30. package/dist/introspection/explanation.js +169 -0
  31. package/dist/introspection/inspector.d.ts +9 -2
  32. package/dist/introspection/inspector.js +25 -20
  33. package/dist/lifecycle/{lifecycle-manager.d.ts → hooks.d.ts} +11 -2
  34. package/dist/lifecycle/{lifecycle-manager.js → hooks.js} +51 -28
  35. package/dist/lifecycle/{scope-manager.d.ts → scopes.d.ts} +23 -6
  36. package/dist/lifecycle/{scope-manager.js → scopes.js} +93 -20
  37. package/dist/metadata/{metadata-keys.d.ts → keys.d.ts} +1 -1
  38. package/dist/metadata/{metadata-keys.js → keys.js} +1 -1
  39. package/dist/metadata/{metadata-reader-token.d.ts → reader-token.d.ts} +1 -1
  40. package/dist/metadata/{symbol-metadata-reader.d.ts → symbol-reader.d.ts} +1 -1
  41. package/dist/metadata/{symbol-metadata-reader.js → symbol-reader.js} +1 -1
  42. package/dist/metadata/{metadata-types.d.ts → types.d.ts} +1 -1
  43. package/dist/metadata/types.js +1 -0
  44. package/dist/metadata/verify.d.ts +43 -0
  45. package/dist/metadata/verify.js +133 -0
  46. package/dist/metadata/{verifying-metadata-reader.d.ts → verifying-reader.d.ts} +1 -1
  47. package/dist/metadata/{verifying-metadata-reader.js → verifying-reader.js} +2 -2
  48. package/dist/resolution/cache/activation-need.d.ts +1 -1
  49. package/dist/resolution/cache/activation-need.js +5 -1
  50. package/dist/resolution/cache/class-introspector.d.ts +27 -36
  51. package/dist/resolution/cache/class-introspector.js +61 -159
  52. package/dist/resolution/cache/{binding-lookup-cache.d.ts → lookup.d.ts} +7 -2
  53. package/dist/resolution/cache/{binding-lookup-cache.js → lookup.js} +40 -8
  54. package/dist/resolution/context.d.ts +1 -1
  55. package/dist/resolution/context.js +1 -1
  56. package/dist/resolution/{path/resolution-path.d.ts → path.d.ts} +12 -2
  57. package/dist/resolution/{path/resolution-path.js → path.js} +23 -3
  58. package/dist/resolution/plan/{plan-codegen.d.ts → codegen.d.ts} +2 -4
  59. package/dist/resolution/plan/{plan-codegen.js → codegen.js} +2 -4
  60. package/dist/resolution/plan/{instantiation-plan.d.ts → compiler.d.ts} +1 -1
  61. package/dist/resolution/plan/{instantiation-plan.js → compiler.js} +4 -4
  62. package/dist/resolution/resolver.d.ts +19 -7
  63. package/dist/resolution/resolver.js +343 -125
  64. package/dist/resolution/select/{binding-select.d.ts → candidates.d.ts} +30 -0
  65. package/dist/resolution/select/{binding-select.js → candidates.js} +51 -10
  66. package/dist/resolution/select/constraints.js +1 -1
  67. package/package.json +80 -72
  68. package/dist/core/binding-scope.d.ts +0 -12
  69. package/dist/core/binding-scope.js +0 -12
  70. package/dist/decorators/lifecycle-decorators.d.ts +0 -14
  71. /package/dist/{ambient/active-container.d.ts → ambient-container.d.ts} +0 -0
  72. /package/dist/{ambient/active-container.js → ambient-container.js} +0 -0
  73. /package/dist/{metadata/metadata-types.js → core/binding-builders.js} +0 -0
  74. /package/dist/decorators/{decorator-metadata.d.ts → metadata-record.d.ts} +0 -0
  75. /package/dist/injection/{resolve-options.d.ts → dependency-slot.d.ts} +0 -0
  76. /package/dist/injection/{resolve-options.js → dependency-slot.js} +0 -0
  77. /package/dist/{errors → introspection}/diagnostics.d.ts +0 -0
  78. /package/dist/{errors → introspection}/diagnostics.js +0 -0
  79. /package/dist/metadata/{metadata-reader-token.js → reader-token.js} +0 -0
@@ -1,4 +1,4 @@
1
- import { bindingSlotEquals, UNREGISTERED_ORDER, 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([]);
@@ -21,8 +21,8 @@ function createTokenRecord(bindings) {
21
21
  * that only ever resolves through its parent — every per-request child — allocates no map.
22
22
  */
23
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;
24
+ const HELD_CONSTANT = 1;
25
+ const HELD_ALIAS = 2;
26
26
  /**
27
27
  * @since 0.11.0
28
28
  */
@@ -35,18 +35,14 @@ export class BindingRegistry {
35
35
  // Every other token — several bindings, a tagged slot, a predicate — has a record here, and a
36
36
  // token is in exactly one of the two maps. Allocated by the first token that needs a record.
37
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;
42
38
  // Built on the first id-keyed read and maintained from then on: a bind-and-resolve container
43
39
  // never asks by id, so it never pays for the second map.
44
40
  #byId;
45
41
  // Set when the first tagged slot lands and never cleared, like the tagged maps it stands for.
46
42
  #taggedIndexBuilt = false;
47
- // Set on the first constant registered and never cleared. Teardown only needs the negative answer
48
- // to be exact, and that is what lets a container holding no constant skip its sweep entirely.
49
- #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;
50
46
  /** Monotonic version — increments on every mutation. */
51
47
  get version() {
52
48
  return this.#version;
@@ -59,7 +55,11 @@ export class BindingRegistry {
59
55
  }
60
56
  /** Whether a constant has ever been registered here, and so whether teardown has anything to sweep. */
61
57
  get hasHeldConstantBinding() {
62
- 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;
63
63
  }
64
64
  /** Whether the deferred tagged-slot index has had to be built. */
65
65
  get isTaggedIndexBuilt() {
@@ -88,25 +88,21 @@ export class BindingRegistry {
88
88
  */
89
89
  add(binding) {
90
90
  this.#bump();
91
- if (binding.registrationOrder === UNREGISTERED_ORDER) {
92
- registrationCounter += 1;
93
- binding.registrationOrder = registrationCounter;
94
- }
95
91
  if (binding.kind === "constant") {
96
- this.#heldConstantBinding = true;
92
+ this.#heldKinds |= HELD_CONSTANT;
93
+ }
94
+ else if (binding.kind === "alias") {
95
+ this.#heldKinds |= HELD_ALIAS;
97
96
  }
98
97
  const key = binding.token;
99
98
  const records = this.#records;
100
99
  if (records !== undefined) {
101
100
  const record = records.get(key);
102
101
  if (record !== undefined) {
103
- this.#lastAdded = binding;
104
- this.#lastAddedRecord = record;
105
102
  return this.#addToRecord(key, record, binding);
106
103
  }
107
104
  }
108
105
  const occupant = this.#lone.get(key);
109
- this.#lastAdded = binding;
110
106
  if (occupant === undefined) {
111
107
  // The common bind: a fresh token taking the lone seat, one probe and one write.
112
108
  if (this.#byId !== undefined) {
@@ -114,33 +110,30 @@ export class BindingRegistry {
114
110
  }
115
111
  if (isDefaultSlotBinding(binding)) {
116
112
  this.#setLone(key, binding);
117
- this.#lastAddedRecord = undefined;
118
113
  }
119
114
  else {
120
- this.#lastAddedRecord = this.#createRecord(key, [binding]);
115
+ this.#createRecord(key, [binding]);
121
116
  }
122
117
  return undefined;
123
118
  }
124
119
  // Same slot, last wins: the newcomer takes the lone seat and nothing else moves.
125
120
  if (isDefaultSlotBinding(binding)) {
126
121
  this.#setLone(key, binding);
127
- this.#lastAddedRecord = undefined;
128
122
  if (this.#byId !== undefined) {
129
123
  this.#byId.delete(occupant.identifier);
130
124
  this.#byId.set(binding.identifier, binding);
131
125
  }
132
126
  return occupant;
133
127
  }
134
- // A second shape joins the token, which is what a record is for.
128
+ // A second shape joins the token, which is what a record is for; every add is the newest, so it goes last.
135
129
  this.#deleteLone(key);
136
130
  this.#byId?.set(binding.identifier, binding);
137
- this.#lastAddedRecord = this.#createRecord(key, occupant.registrationOrder < binding.registrationOrder ? [occupant, binding] : [binding, occupant]);
131
+ this.#createRecord(key, [occupant, binding]);
138
132
  return undefined;
139
133
  }
140
134
  /** Remove all bindings for a token. Returns removed bindings. */
141
135
  removeByToken(token) {
142
136
  this.#bump();
143
- this.#lastAdded = undefined;
144
137
  const lone = this.#lone.get(token);
145
138
  if (lone !== undefined) {
146
139
  this.#deleteLone(token);
@@ -168,7 +161,6 @@ export class BindingRegistry {
168
161
  return undefined;
169
162
  }
170
163
  this.#bump();
171
- this.#lastAdded = undefined;
172
164
  byId.delete(id);
173
165
  const key = binding.token;
174
166
  if (this.#lone.get(key)?.identifier === id) {
@@ -236,28 +228,12 @@ export class BindingRegistry {
236
228
  /** Remove all bindings. Returns all removed. */
237
229
  clear() {
238
230
  this.#bump();
239
- this.#lastAdded = undefined;
240
231
  const all = this.allBindings();
241
232
  this.#clearLone();
242
233
  this.#records?.clear();
243
234
  this.#byId?.clear();
244
235
  return all;
245
236
  }
246
- /** Whether a slot-based binding currently occupies `binding`'s slot, so adding it would displace. */
247
- hasSlotOccupant(binding) {
248
- if (isPurePredicateBinding(binding)) {
249
- return false;
250
- }
251
- if (this.#lone.has(binding.token)) {
252
- // The lone seat is the default slot, so only a default-slot newcomer collides with it.
253
- return binding.slot.tags.length === 0;
254
- }
255
- const record = this.#records?.get(binding.token);
256
- if (record === undefined) {
257
- return false;
258
- }
259
- return this.#slotOccupant(record, binding.slot) !== undefined;
260
- }
261
237
  /**
262
238
  * The binding indexed under one criterion.
263
239
  *
@@ -304,109 +280,22 @@ export class BindingRegistry {
304
280
  getDefaultSlotBinding(token) {
305
281
  return this.#lone.get(token) ?? this.#records?.get(token)?.defaultOccupant;
306
282
  }
307
- /** A token's lone default-slot binding — the first read of every synchronous resolve. */
308
- getFastDefault(token) {
309
- return this.#lone.get(token);
310
- }
311
- /**
312
- * Takes a live binding out of every index, lets `rewrite` change its slot or predicate, and reports
313
- * whether it was live; the caller registers it again with `add`.
314
- *
315
- * @remarks The binding keeps its object and its id, so the id index needs no touch and nothing
316
- * that holds the object has to be told. `false` means the binding was unbound or displaced since
317
- * it registered, and a refinement must not resurrect it.
318
- */
319
- reslot(binding, rewrite) {
320
- const key = binding.token;
321
- if (this.#lone.get(key) === binding) {
322
- this.#bump();
323
- this.#lastAdded = undefined;
324
- this.#deleteLone(key);
325
- }
326
- else {
327
- const record = this.#records?.get(key);
328
- const index = record === undefined ? -1 : record.bindings.indexOf(binding);
329
- if (record === undefined || index === -1) {
330
- return false;
331
- }
332
- this.#bump();
333
- this.#lastAdded = undefined;
334
- // Replaced, never spliced: a walk holding the current array must not lose its place.
335
- record.bindings = record.bindings.toSpliced(index, 1);
336
- this.#deindexSlot(record, binding);
337
- this.#settle(key, record);
338
- }
339
- rewrite();
340
- return true;
341
- }
342
- /**
343
- * Marks a live binding as a collection member in place, moving it out of the lone map: a member is
344
- * never the token's lone default answer, and nothing else indexes on membership.
345
- */
346
- setMany(binding) {
347
- this.#bump();
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
- }
360
- return;
361
- }
362
- if (this.#promoteLoneToRecord(binding.token, binding)) {
363
- return;
364
- }
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);
368
- }
369
283
  /**
370
- * 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`.
371
285
  *
372
- * @remarks Only ever narrows — `when()` composes with any existing predicate — so the argument is
373
- * never absent. Nothing indexes on the predicate, so the binding object and its id stay; a lone
374
- * 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.
375
288
  */
376
- setPredicate(binding, predicate) {
377
- this.#bump();
378
- // Set before any (re)indexing: a predicate-only binding holds no default slot.
379
- writablePredicate(binding).predicate = predicate;
380
- if (this.#promoteLoneToRecord(binding.token, binding)) {
381
- return;
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;
289
+ defaultSlotAlias(token) {
290
+ if ((this.#heldKinds & HELD_ALIAS) === 0) {
291
+ return undefined;
398
292
  }
399
- this.#deleteLone(key);
400
- this.#createRecord(key, [binding]);
401
- return true;
293
+ const occupant = this.getDefaultSlotBinding(token);
294
+ return occupant !== undefined && occupant.kind === "alias" ? occupant : undefined;
402
295
  }
403
- /** Clears the default-slot index entry a binding has vacated, returning its record if one exists. */
404
- #clearDefaultOccupant(key, binding) {
405
- const record = this.#records?.get(key);
406
- if (record?.defaultOccupant === binding) {
407
- record.defaultOccupant = undefined;
408
- }
409
- return record;
296
+ /** A token's lone default-slot binding — the first read of every synchronous resolve. */
297
+ getFastDefault(token) {
298
+ return this.#lone.get(token);
410
299
  }
411
300
  #createRecord(key, bindings) {
412
301
  const record = createTokenRecord(bindings);
@@ -445,7 +334,7 @@ export class BindingRegistry {
445
334
  if (displacedBinding !== undefined) {
446
335
  record.bindings = record.bindings.filter((candidate) => candidate !== displacedBinding);
447
336
  }
448
- insertInRegistrationOrder(record, binding);
337
+ record.bindings.push(binding);
449
338
  this.#byId?.set(binding.identifier, binding);
450
339
  this.#indexSlot(record, binding);
451
340
  this.#settle(key, record);
@@ -544,20 +433,6 @@ export class BindingRegistry {
544
433
  * Places a binding in its record by registration order: a fresh registration appends, and a binding a
545
434
  * chain took out and put back returns to its place.
546
435
  */
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
- }
561
436
  /** A binding nothing has to be matched against: the default slot, no predicate, not a collection member. */
562
437
  function isDefaultSlotBinding(binding) {
563
438
  return binding.slot.tags.length === 0 && binding.predicate === undefined && !binding.isMany;
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
- * The bits a tag-key mask holds: the width of a 32-bit integer, a constant of the machine.
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.
@@ -2,7 +2,9 @@
2
2
  import type { Token } from "#core/token";
3
3
  import type { Constructor } from "#core/types";
4
4
  import type { InjectionDescriptor, InjectOptions } from "#injection/descriptor";
5
- type ClassAccessorDecorator<This, Value> = (target: ClassAccessorDecoratorTarget<This, Value>, context: ClassAccessorDecoratorContext<This, Value>) => ClassAccessorDecoratorResult<This, Value> | void;
5
+ type ClassAccessorDecorator<This, Value> = (target: ClassAccessorDecoratorTarget<This, Value>, context: ClassAccessorDecoratorContext<This, Value> & {
6
+ readonly static: false;
7
+ }) => ClassAccessorDecoratorResult<This, Value> | void;
6
8
  /**
7
9
  * Creates a dual-role value: an injection descriptor that also works as a class accessor decorator.
8
10
  *
@@ -1,9 +1,9 @@
1
- import { getActiveContainer, getAmbientResolution } from "#ambient/active-container";
2
- import { decoratorMetadataOf } from "#decorators/decorator-metadata";
3
- 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";
4
5
  import { buildInjectionDescriptor } from "#injection/descriptor";
5
- import { injectionSlotToResolveOptions } from "#injection/resolve-options";
6
- import { INJECT_ACCESSOR_KEY } from "#metadata/metadata-keys";
6
+ import { INJECT_ACCESSOR_KEY } from "#metadata/keys";
7
7
  /**
8
8
  * The name of the class being constructed, or `undefined` when there is none to report.
9
9
  *
@@ -1,6 +1,6 @@
1
- import { decoratorMetadataOf } from "#decorators/decorator-metadata";
1
+ import { decoratorMetadataOf } from "#decorators/metadata-record";
2
2
  import { normalizeToDescriptor } from "#injection/descriptor";
3
- import { INJECTABLE_KEY } from "#metadata/metadata-keys";
3
+ import { INJECTABLE_KEY } from "#metadata/keys";
4
4
  /**
5
5
  * Creates an empty auto-register registry.
6
6
  *
@@ -0,0 +1,20 @@
1
+ /** The only method a hook can be: the lifecycle reader calls it on the instance by its string name. */
2
+ type LifecycleMethodContext = ClassMethodDecoratorContext & {
3
+ readonly static: false;
4
+ readonly private: false;
5
+ readonly name: string;
6
+ };
7
+ type LifecycleMethodDecorator = (target: unknown, context: LifecycleMethodContext) => void;
8
+ /**
9
+ * Marks an instance method to run after the container constructs and wires the instance.
10
+ *
11
+ * @since 0.3.16-canary.0
12
+ */
13
+ export declare function postConstruct(): LifecycleMethodDecorator;
14
+ /**
15
+ * Marks an instance method to run when the instance's container or scope is disposed.
16
+ *
17
+ * @since 0.3.16-canary.0
18
+ */
19
+ export declare function preDestroy(): LifecycleMethodDecorator;
20
+ export {};
@@ -1,12 +1,16 @@
1
- import { decoratorMetadataOf } from "#decorators/decorator-metadata";
2
- import { StaticMemberDecoratorError, SymbolKeyedLifecycleError } from "#errors/errors";
3
- import { LIFECYCLE_KEY } from "#metadata/metadata-keys";
1
+ import { decoratorMetadataOf } from "#decorators/metadata-record";
2
+ import { PrivateLifecycleMethodError, StaticMemberDecoratorError, SymbolKeyedLifecycleError } from "#errors";
3
+ import { LIFECYCLE_KEY } from "#metadata/keys";
4
4
  /** Records the decorated method under one lifecycle phase; both decorators differ only in that phase. */
5
5
  function recordLifecycleMethod(phase) {
6
+ // Typed wide on purpose: the checks below are what an untyped caller meets instead of the compiler.
6
7
  return function (target, context) {
7
8
  if (context.static) {
8
9
  throw new StaticMemberDecoratorError(phase, String(context.name));
9
10
  }
11
+ if (context.private) {
12
+ throw new PrivateLifecycleMethodError(phase, String(context.name));
13
+ }
10
14
  // The lifecycle reader keys methods by their string name, so a symbol-keyed method can never be
11
15
  // found again — fail here, where the declaration is, rather than at resolve.
12
16
  if (typeof context.name === "symbol") {
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * The metadata record a decorator writes into, checked once for every decorator the library ships.
3
3
  */
4
- import { MissingDecoratorMetadataError } from "#errors/errors";
4
+ import { MissingDecoratorMetadataError } from "#errors";
5
5
  /**
6
6
  * Returns the metadata record a decorator context carries.
7
7
  *
@@ -28,7 +28,9 @@ export declare class TokenNotBoundError extends DiError {
28
28
  readonly name = "TokenNotBoundError";
29
29
  readonly code = "TOKEN_NOT_BOUND";
30
30
  readonly tokenName: string;
31
- constructor(tokenName: string);
31
+ /** The token names from the outermost request down to the unbound one, which is last. */
32
+ readonly path: ReadonlyArray<string>;
33
+ constructor(tokenName: string, path?: ReadonlyArray<string>);
32
34
  }
33
35
  /**
34
36
  * A token that has bindings, none of whose slots match the given `ResolveOptions`.
@@ -41,7 +43,9 @@ export declare class NoMatchingBindingError extends DiError {
41
43
  readonly tokenName: string;
42
44
  readonly options: ResolveOptions;
43
45
  readonly availableSlots: Array<string>;
44
- constructor(tokenName: string, options: ResolveOptions, availableSlots: Array<string>);
46
+ /** The token names from the outermost request down to the unmatched one, which is last. */
47
+ readonly path: ReadonlyArray<string>;
48
+ constructor(tokenName: string, options: ResolveOptions, availableSlots: Array<string>, path?: ReadonlyArray<string>);
45
49
  }
46
50
  /**
47
51
  * Two or more candidate bindings that matched without the more-specific rule deciding a winner.
@@ -162,7 +166,7 @@ export declare class UnreachableLifecycleHookError extends DiError {
162
166
  readonly code = "UNREACHABLE_LIFECYCLE_HOOK";
163
167
  readonly tokenName: string;
164
168
  readonly phase: "onActivation" | "onDeactivation";
165
- /** Nothing binds the token, or — for a deactivation hook — every binding it has is transient or scoped. */
169
+ /** Nothing binds the token, or — for a deactivation hook — every binding it has is transient. */
166
170
  readonly reason: "unbound" | "no-deactivatable-binding";
167
171
  constructor(tokenName: string, phase: "onActivation" | "onDeactivation", reason?: "unbound" | "no-deactivatable-binding");
168
172
  }
@@ -207,16 +211,6 @@ export declare class AsyncModuleLoadError extends DiError {
207
211
  readonly moduleName: string;
208
212
  constructor(moduleName: string);
209
213
  }
210
- /**
211
- * A synchronous disposal attempt on a container whose `onDeactivation` handlers may be async.
212
- *
213
- * @since 0.3.16-canary.0
214
- */
215
- export declare class SyncDisposalNotSupportedError extends DiError {
216
- readonly name = "SyncDisposalNotSupportedError";
217
- readonly code = "SYNC_DISPOSAL_NOT_SUPPORTED";
218
- constructor();
219
- }
220
214
  /**
221
215
  * A `scoped` binding resolved from a container with no child scope context.
222
216
  *
@@ -246,10 +240,11 @@ export declare class MissingContainerContextError extends DiError {
246
240
  constructor(className: string | undefined, accessorName: string | symbol);
247
241
  }
248
242
  /**
249
- * A second `to*()` on a chain that already registered its binding.
243
+ * A slot step or a second `to*()` on a chain that already registered its binding.
250
244
  *
251
- * @remarks A chain is its binding, so it registers exactly once; a token bound twice is two `bind()`
252
- * calls, the second of which displaces the first under slot last-wins.
245
+ * @remarks A chain is its binding and registers exactly once, in its final shape, so its slot is
246
+ * settled by `to*()`; a token bound twice is two `bind()` calls, the second of which displaces the
247
+ * first under slot last-wins.
253
248
  *
254
249
  * @since 0.10.0
255
250
  */
@@ -274,11 +269,11 @@ export declare class ManyBindingSlotError extends DiError {
274
269
  constructor(tokenName: string);
275
270
  }
276
271
  /**
277
- * A fluent chain was refined before a `to*()` call gave it a binding to refine.
272
+ * A scope, a lifecycle hook or `id()` asked of a fluent chain before a `to*()` call registered its binding.
278
273
  *
279
274
  * @remarks The builder types make this unreachable from TypeScript — `bind()` returns
280
- * `BindToBuilder`, which exposes only `to*()`. It exists for JavaScript callers and for anyone who
281
- * casts past the types, so the misuse fails loudly instead of mutating nothing.
275
+ * `BindToBuilder`, which exposes only the slot steps and `to*()`. It exists for JavaScript callers and
276
+ * for anyone who casts past the types, so the misuse fails loudly instead of mutating nothing.
282
277
  *
283
278
  * @since 0.10.0
284
279
  */
@@ -341,6 +336,21 @@ export declare class SymbolKeyedLifecycleError extends DiError {
341
336
  readonly memberName: string;
342
337
  constructor(decoratorName: string, memberName: string);
343
338
  }
339
+ /**
340
+ * A `@postConstruct` or `@preDestroy` on a private method, which the lifecycle reader cannot call by name.
341
+ *
342
+ * @remarks Reported at decoration rather than at resolve, so the error points at the declaration
343
+ * instead of a `MetadataReader` the caller never configured.
344
+ *
345
+ * @since 0.13.0
346
+ */
347
+ export declare class PrivateLifecycleMethodError extends DiError {
348
+ readonly name = "PrivateLifecycleMethodError";
349
+ readonly code = "PRIVATE_LIFECYCLE_METHOD";
350
+ readonly decoratorName: string;
351
+ readonly memberName: string;
352
+ constructor(decoratorName: string, memberName: string);
353
+ }
344
354
  /**
345
355
  * A decorator handed no metadata object, because the runtime has no `Symbol.metadata` to key one by.
346
356
  *
@@ -18,6 +18,10 @@ export class InternalError extends DiError {
18
18
  super(message);
19
19
  }
20
20
  }
21
+ // A path of one token only repeats the name the message already opens with.
22
+ function pathSuffix(path) {
23
+ return path.length > 1 ? ` Path: ${path.join(" → ")}` : "";
24
+ }
21
25
  /**
22
26
  * A token with no binding at all, even after walking the parent container chain.
23
27
  *
@@ -27,9 +31,12 @@ export class TokenNotBoundError extends DiError {
27
31
  name = "TokenNotBoundError";
28
32
  code = "TOKEN_NOT_BOUND";
29
33
  tokenName;
30
- constructor(tokenName) {
31
- super(`No binding found for token '${tokenName}'. Did you forget container.bind(${tokenName})?`);
34
+ /** The token names from the outermost request down to the unbound one, which is last. */
35
+ path;
36
+ constructor(tokenName, path = [tokenName]) {
37
+ super(`No binding found for token '${tokenName}'. Did you forget container.bind(${tokenName})?${pathSuffix(path)}`);
32
38
  this.tokenName = tokenName;
39
+ this.path = path;
33
40
  }
34
41
  }
35
42
  // Options carry caller values a tag may hold — a bigint or a circular object must not make the
@@ -78,13 +85,16 @@ export class NoMatchingBindingError extends DiError {
78
85
  tokenName;
79
86
  options;
80
87
  availableSlots;
81
- constructor(tokenName, options, availableSlots) {
88
+ /** The token names from the outermost request down to the unmatched one, which is last. */
89
+ path;
90
+ constructor(tokenName, options, availableSlots, path = [tokenName]) {
82
91
  const optionsString = describeResolveOptions(options);
83
92
  const slotsStr = availableSlots.join(", ");
84
- super(`No binding for '${tokenName}' matching ${optionsString}. Available slots: [${slotsStr}].`);
93
+ super(`No binding for '${tokenName}' matching ${optionsString}. Available slots: [${slotsStr}].${pathSuffix(path)}`);
85
94
  this.tokenName = tokenName;
86
95
  this.options = options;
87
96
  this.availableSlots = availableSlots;
97
+ this.path = path;
88
98
  }
89
99
  }
90
100
  /**
@@ -225,12 +235,12 @@ export class UnreachableLifecycleHookError extends DiError {
225
235
  code = "UNREACHABLE_LIFECYCLE_HOOK";
226
236
  tokenName;
227
237
  phase;
228
- /** Nothing binds the token, or — for a deactivation hook — every binding it has is transient or scoped. */
238
+ /** Nothing binds the token, or — for a deactivation hook — every binding it has is transient. */
229
239
  reason;
230
240
  constructor(tokenName, phase, reason = "unbound") {
231
241
  super(reason === "unbound"
232
242
  ? `${phase}() is registered for '${tokenName}', which nothing is bound to in this container or its ancestors, so the hook can never run. Bind the token, or — if '${tokenName}' is a class you bound as an implementation via .to(${tokenName}) — register the hook against the token you bound instead.`
233
- : `${phase}() is registered for '${tokenName}', whose every binding in this container and its ancestors is transient or scoped, so the hook can never run: only a singleton or a constant is deactivated. Make one of its bindings a singleton, or drop the hook.`);
243
+ : `${phase}() is registered for '${tokenName}', whose every binding in this container and its ancestors is transient, so the hook can never run: a transient instance is never deactivated. Make one of its bindings a singleton or scoped, or drop the hook.`);
234
244
  this.tokenName = tokenName;
235
245
  this.phase = phase;
236
246
  this.reason = reason;
@@ -286,18 +296,6 @@ export class AsyncModuleLoadError extends DiError {
286
296
  this.moduleName = moduleName;
287
297
  }
288
298
  }
289
- /**
290
- * A synchronous disposal attempt on a container whose `onDeactivation` handlers may be async.
291
- *
292
- * @since 0.3.16-canary.0
293
- */
294
- export class SyncDisposalNotSupportedError extends DiError {
295
- name = "SyncDisposalNotSupportedError";
296
- code = "SYNC_DISPOSAL_NOT_SUPPORTED";
297
- constructor() {
298
- super("Container cannot be disposed synchronously because onDeactivation handlers may be async. Use `await using` or call container.dispose() explicitly.");
299
- }
300
- }
301
299
  /**
302
300
  * A `scoped` binding resolved from a container with no child scope context.
303
301
  *
@@ -337,10 +335,11 @@ export class MissingContainerContextError extends DiError {
337
335
  }
338
336
  }
339
337
  /**
340
- * A second `to*()` on a chain that already registered its binding.
338
+ * A slot step or a second `to*()` on a chain that already registered its binding.
341
339
  *
342
- * @remarks A chain is its binding, so it registers exactly once; a token bound twice is two `bind()`
343
- * calls, the second of which displaces the first under slot last-wins.
340
+ * @remarks A chain is its binding and registers exactly once, in its final shape, so its slot is
341
+ * settled by `to*()`; a token bound twice is two `bind()` calls, the second of which displaces the
342
+ * first under slot last-wins.
344
343
  *
345
344
  * @since 0.10.0
346
345
  */
@@ -349,7 +348,7 @@ export class ChainAlreadyRegisteredError extends DiError {
349
348
  code = "CHAIN_ALREADY_REGISTERED";
350
349
  tokenName;
351
350
  constructor(tokenName) {
352
- super(`The binding for token '${tokenName}' is already registered on this chain. Call bind() again to register another binding for the token.`);
351
+ super(`The binding for token '${tokenName}' is already registered on this chain, so its slot is final. Declare when*() and many() before to*(), or call bind() again to register another binding for the token.`);
353
352
  this.tokenName = tokenName;
354
353
  }
355
354
  }
@@ -371,11 +370,11 @@ export class ManyBindingSlotError extends DiError {
371
370
  }
372
371
  }
373
372
  /**
374
- * A fluent chain was refined before a `to*()` call gave it a binding to refine.
373
+ * A scope, a lifecycle hook or `id()` asked of a fluent chain before a `to*()` call registered its binding.
375
374
  *
376
375
  * @remarks The builder types make this unreachable from TypeScript — `bind()` returns
377
- * `BindToBuilder`, which exposes only `to*()`. It exists for JavaScript callers and for anyone who
378
- * casts past the types, so the misuse fails loudly instead of mutating nothing.
376
+ * `BindToBuilder`, which exposes only the slot steps and `to*()`. It exists for JavaScript callers and
377
+ * for anyone who casts past the types, so the misuse fails loudly instead of mutating nothing.
379
378
  *
380
379
  * @since 0.10.0
381
380
  */
@@ -455,6 +454,25 @@ export class SymbolKeyedLifecycleError extends DiError {
455
454
  this.memberName = memberName;
456
455
  }
457
456
  }
457
+ /**
458
+ * A `@postConstruct` or `@preDestroy` on a private method, which the lifecycle reader cannot call by name.
459
+ *
460
+ * @remarks Reported at decoration rather than at resolve, so the error points at the declaration
461
+ * instead of a `MetadataReader` the caller never configured.
462
+ *
463
+ * @since 0.13.0
464
+ */
465
+ export class PrivateLifecycleMethodError extends DiError {
466
+ name = "PrivateLifecycleMethodError";
467
+ code = "PRIVATE_LIFECYCLE_METHOD";
468
+ decoratorName;
469
+ memberName;
470
+ constructor(decoratorName, memberName) {
471
+ super(`@${decoratorName}() does not support a private method ('${memberName}'). Make the lifecycle method public, and call the private code from it.`);
472
+ this.decoratorName = decoratorName;
473
+ this.memberName = memberName;
474
+ }
475
+ }
458
476
  /**
459
477
  * A decorator handed no metadata object, because the runtime has no `Symbol.metadata` to key one by.
460
478
  *