@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,17 +1,17 @@
1
- import { clearBindingFrame, NO_ACTIVATION_STAMP, DEFAULT_BINDING_SLOT, generateBindingId, NO_INSTANCE, UNREGISTERED_ORDER, withSlotCriterion, } from "#core/binding";
1
+ import { clearBindingFrame, NO_ACTIVATION_STAMP, DEFAULT_BINDING_SLOT, generateBindingId, NO_INSTANCE, withSlotCriterion, } from "#core/binding";
2
2
  import { mergingConstraintRequirements } from "#core/constraint-requirement";
3
3
  import { slotName } from "#core/tag";
4
4
  import { tokenName } from "#core/token";
5
- import { ChainAlreadyRegisteredError, ChainNotRegisteredError, ManyBindingSlotError, SelfBindingRequiresClassError, } from "#errors/errors";
5
+ import { ChainAlreadyRegisteredError, ChainNotRegisteredError, ManyBindingSlotError, SelfBindingRequiresClassError, } from "#errors";
6
6
  import { normalizeToDescriptor } from "#injection/descriptor";
7
7
  // ── BindingChain ─────────────────────────────────────────────────────────────────────────────────────────────────────
8
8
  /**
9
9
  * The one builder behind `bind()` and every `to*()` return type — and the binding it registers.
10
10
  *
11
11
  * @remarks The binding fields come first, in the order every binding shares, and the chain's own
12
- * bookkeeping follows as private fields; `to*()` fills the fields in place and hands this object to
13
- * the registry, so a plain bind is one allocation. Refinements write the registered object: a scope
14
- * or hook in place, a slot or predicate through the registry so its indexes follow.
12
+ * bookkeeping follows as a private field. The slot steps write the fields before anything registers;
13
+ * `to*()` fills the strategy in place and hands this object to the registry once, so a bind is one
14
+ * allocation and one add. A scope or hook afterwards writes the registered object in place.
15
15
  *
16
16
  * @since 0.5.0-canary.8
17
17
  */
@@ -23,8 +23,8 @@ export class BindingChain {
23
23
  frame = undefined;
24
24
  rootContext = undefined;
25
25
  activationStamp = NO_ACTIVATION_STAMP;
26
- registrationOrder = UNREGISTERED_ORDER;
27
26
  instance = NO_INSTANCE;
27
+ scopedCacheKey = this.identifier;
28
28
  token;
29
29
  slot = DEFAULT_BINDING_SLOT;
30
30
  predicate = undefined;
@@ -38,10 +38,6 @@ export class BindingChain {
38
38
  deactivationHook = undefined;
39
39
  // The chain. Set by the first `to*()`, which is the only one a chain accepts.
40
40
  #isRegistered = false;
41
- // Allocated only by a chain that actually displaces something — most never do.
42
- #displacedByChain;
43
- // Registry version after this chain's last write — a mismatch means someone else wrote in between.
44
- #versionAfterLastWrite = -1;
45
41
  #registration;
46
42
  constructor(token, registration) {
47
43
  this.token = token;
@@ -51,13 +47,13 @@ export class BindingChain {
51
47
  get #binding() {
52
48
  return this;
53
49
  }
54
- /** Loud failure for a refinement before `to*()`. */
50
+ /** Loud failure for a scope, a hook or `id()` before `to*()`. */
55
51
  #requireRegistered() {
56
52
  if (!this.#isRegistered) {
57
53
  throw new ChainNotRegisteredError(tokenName(this.token));
58
54
  }
59
55
  }
60
- /** Loud failure for a second `to*()`, raised before the first one's fields can be overwritten. */
56
+ /** Loud failure for a slot step or a second `to*()` once registered, raised before a field is overwritten. */
61
57
  #requireUnregistered() {
62
58
  if (this.#isRegistered) {
63
59
  throw new ChainAlreadyRegisteredError(tokenName(this.token));
@@ -67,32 +63,22 @@ export class BindingChain {
67
63
  this.kind = kind;
68
64
  this.scope = scope;
69
65
  this.#isRegistered = true;
70
- // A fresh registration parks nothing yet and restores nothing: one add, then the version.
71
66
  const registration = this.#registration;
72
- const registry = registration.registry;
73
- const displaced = registry.add(this.#binding);
67
+ const displaced = registration.registry.add(this.#binding);
74
68
  if (displaced !== undefined) {
75
69
  if (registration.deactivateDisplaced !== undefined) {
76
70
  registration.deactivateDisplaced(displaced);
77
71
  }
78
72
  else {
79
- this.#displacedByChain = [displaced];
80
73
  registration.onDisplaced?.(displaced);
81
74
  }
82
75
  }
83
76
  if (registration.moduleBindingIds !== undefined) {
84
77
  registration.moduleBindingIds.push(this.identifier);
85
78
  }
86
- this.#versionAfterLastWrite = registry.version;
87
79
  return this;
88
80
  }
89
- /**
90
- * Registers a declared module's bindings in list order, each a chain of its own that no step
91
- * will ever refine.
92
- *
93
- * @remarks A displaced binding is handled as `to*()` handles it, never parked: nothing refines the
94
- * chain, so nothing could restore it.
95
- */
81
+ /** Registers a declared module's bindings in list order, each a chain of its own, handled as `to*()` handles one. */
96
82
  static registerDeclared(declarations, registration) {
97
83
  const { registry, moduleBindingIds, deactivateDisplaced, onDisplaced } = registration;
98
84
  for (let index = 0; index < declarations.length; index += 1) {
@@ -177,102 +163,67 @@ export class BindingChain {
177
163
  this.target = target;
178
164
  return this.#register("alias", "transient");
179
165
  }
180
- // ── Refinement ─────────────────────────────────────────────────────────────────────────────────────────────────────
181
- /**
182
- * Whether this chain provably still owns the registry's last write with nothing parked.
183
- *
184
- * @remarks When it does, a refinement rewrites the live binding in place; otherwise `#reslot`
185
- * re-checks liveness and restores what the new shape frees.
186
- */
187
- get #isProvablyLive() {
188
- return this.#registration.registry.version === this.#versionAfterLastWrite && this.#displacedByChain === undefined;
189
- }
190
- /** Stamps this chain's last-write version after a mutation it made in place. */
191
- #recordWrite() {
192
- this.#versionAfterLastWrite = this.#registration.registry.version;
193
- }
194
- /** Bumps the registry for an index-neutral mutation (scope, hook) and records this chain's write. */
195
- #touchAndRecordWrite() {
196
- this.#registration.registry.touch();
197
- this.#recordWrite();
198
- }
199
- // Slot and predicate are what the registry indexes on, so a re-slot takes the binding out of the
200
- // registry, rewrites the two fields while it is out, and registers it again — same object, same id.
201
- #reslot(slot, predicate) {
202
- this.#requireRegistered();
203
- this.#commit(() => {
204
- this.slot = slot;
205
- this.predicate = predicate;
206
- // The frame reports the slot, so a resolve before this call memoized the previous one.
207
- clearBindingFrame(this.#binding);
208
- });
209
- return this;
210
- }
211
- #withScope(scope) {
212
- this.#requireRegistered();
213
- if (this.scope !== scope) {
214
- // An instance cached under the old scope must not survive the change — a later flip back
215
- // to that scope would resurrect it.
216
- this.#registration.scope.deleteSingleton(this.#binding);
217
- this.#registration.scope.deleteScoped(this.identifier);
218
- this.scope = scope;
219
- }
220
- // The frame reports the scope, so a resolve before this call memoized the previous one.
221
- clearBindingFrame(this.#binding);
222
- this.#touchAndRecordWrite();
223
- return this;
224
- }
225
- // SPEC calls a candidate a binding that passes *all* of a chain's predicates, and the chain type
226
- // reads as refinement, so a second `when()` narrows rather than replaces.
166
+ // ── Slot ───────────────────────────────────────────────────────────────────────────────────────────────────────────
167
+ // SPEC calls a candidate a binding that passes *all* of a chain's predicates, so a second `when()`
168
+ // narrows rather than replaces.
227
169
  when(predicate) {
228
- this.#requireRegistered();
170
+ this.#requireUnregistered();
229
171
  const previous = this.predicate;
230
172
  // The composite carries both sides' requirements, so validate() still sees them.
231
- const narrowed = previous === undefined
232
- ? predicate
233
- : mergingConstraintRequirements((ctx) => previous(ctx) && predicate(ctx), previous, predicate);
234
- // The slot is unchanged, so a provably-live binding just takes the predicate in place.
235
- if (this.#isProvablyLive) {
236
- this.#registration.registry.setPredicate(this.#binding, narrowed);
237
- this.#recordWrite();
238
- return this;
239
- }
240
- return this.#reslot(this.slot, narrowed);
173
+ this.predicate =
174
+ previous === undefined
175
+ ? predicate
176
+ : mergingConstraintRequirements((ctx) => previous(ctx) && predicate(ctx), previous, predicate);
177
+ return this;
241
178
  }
242
179
  whenNamed(name) {
243
180
  return this.whenTagged(slotName.of(name));
244
181
  }
245
182
  whenTagged(criterion) {
246
- this.#requireRegistered();
183
+ this.#requireUnregistered();
247
184
  if (this.isMany) {
248
185
  throw new ManyBindingSlotError(tokenName(this.token));
249
186
  }
250
- return this.#reslot(withSlotCriterion(this.slot, criterion), this.predicate);
187
+ this.slot = withSlotCriterion(this.slot, criterion);
188
+ return this;
251
189
  }
252
190
  many() {
253
- this.#requireRegistered();
191
+ this.#requireUnregistered();
254
192
  if (this.slot.tags.length !== 0) {
255
193
  throw new ManyBindingSlotError(tokenName(this.token));
256
194
  }
257
- if (this.isMany) {
258
- return this;
259
- }
260
- // A provably-live binding takes membership in place; the registry only moves it out of the lone
261
- // map. Otherwise `#commit` re-checks liveness and restores what the freed slot lets back in.
262
- if (this.#isProvablyLive) {
263
- this.#registration.registry.setMany(this.#binding);
264
- this.#recordWrite();
265
- return this;
266
- }
267
- this.#commit(() => {
268
- this.isMany = true;
269
- });
195
+ this.isMany = true;
270
196
  return this;
271
197
  }
272
198
  whenDefault() {
273
- // The default slot is what a fresh registration already has, so there is nothing to re-slot —
274
- // but an unregistered chain must fail here exactly as it does in every other refinement.
199
+ // The default slot is what a fresh chain already has; the check keeps it a slot step like the others.
200
+ this.#requireUnregistered();
201
+ return this;
202
+ }
203
+ // ── Scope and lifecycle ────────────────────────────────────────────────────────────────────────────────────────────
204
+ #withScope(scope) {
275
205
  this.#requireRegistered();
206
+ // Nothing a cache, a frame or the version answers for depends on a scope that did not change.
207
+ if (this.scope === scope) {
208
+ return this;
209
+ }
210
+ // An instance cached under the old scope must not survive the change — a later flip back to that
211
+ // scope would resurrect it. A child's scoped cache is out of reach here, so leaving `scoped` retires
212
+ // the key every child filed its instance under.
213
+ const registration = this.#registration;
214
+ if (this.instance !== NO_INSTANCE) {
215
+ registration.scope.deleteSingleton(this.#binding);
216
+ }
217
+ if (this.scope === "scoped") {
218
+ registration.scope.deleteScoped(this.scopedCacheKey);
219
+ this.scopedCacheKey = generateBindingId();
220
+ }
221
+ this.scope = scope;
222
+ // The frame reports the scope, so a resolve before this call memoized the previous one.
223
+ if (this.frame !== undefined || this.rootContext !== undefined) {
224
+ clearBindingFrame(this.#binding);
225
+ }
226
+ registration.registry.touch();
276
227
  return this;
277
228
  }
278
229
  singleton() {
@@ -287,78 +238,17 @@ export class BindingChain {
287
238
  onActivation(fn) {
288
239
  this.#requireRegistered();
289
240
  this.activationHook = fn;
290
- this.#touchAndRecordWrite();
241
+ this.#registration.registry.touch();
291
242
  return this;
292
243
  }
293
244
  onDeactivation(fn) {
294
245
  this.#requireRegistered();
295
246
  this.deactivationHook = fn;
296
- this.#touchAndRecordWrite();
247
+ this.#registration.registry.touch();
297
248
  return this;
298
249
  }
299
250
  id() {
300
251
  this.#requireRegistered();
301
252
  return this.identifier;
302
253
  }
303
- // ── Registry ───────────────────────────────────────────────────────────────────────────────────────────────────────
304
- /**
305
- * Registers this binding; with `rewrite` given, first takes the live binding out, rewrites it, and
306
- * registers it again.
307
- *
308
- * @remarks A `when*()` that follows `to*()` re-slots an already-live binding and can displace one
309
- * the final shape would never conflict with. Those stay parked in `#displacedByChain` until the
310
- * chain settles, then get restored.
311
- */
312
- #commit(rewrite) {
313
- const { registry, moduleBindingIds } = this.#registration;
314
- const registered = this.#binding;
315
- if (rewrite !== undefined) {
316
- // A registry someone else wrote since this chain's last write invalidates the parked
317
- // snapshot: restoring it could undo an unbind or shadow a newer binding.
318
- if (registry.version !== this.#versionAfterLastWrite) {
319
- this.#displacedByChain = undefined;
320
- }
321
- if (!registry.reslot(registered, rewrite)) {
322
- // The chain's binding is no longer live (unbound or displaced) — a refinement must not
323
- // resurrect it, so the chain goes inert against the registry.
324
- this.#displacedByChain = undefined;
325
- this.#versionAfterLastWrite = registry.version;
326
- return;
327
- }
328
- }
329
- else {
330
- // Each `to*()` starts its own registration, so anything a previous one displaced is not this
331
- // registration's to restore.
332
- this.#displacedByChain = undefined;
333
- }
334
- const displaced = registry.add(registered);
335
- if (displaced !== undefined) {
336
- if (this.#registration.deactivateDisplaced !== undefined && rewrite === undefined) {
337
- this.#registration.deactivateDisplaced(displaced);
338
- }
339
- else {
340
- (this.#displacedByChain ??= []).push(displaced);
341
- this.#registration.onDisplaced?.(displaced);
342
- }
343
- }
344
- if (rewrite !== undefined && this.#displacedByChain !== undefined) {
345
- this.#restoreNonConflicting(this.#displacedByChain);
346
- }
347
- if (rewrite === undefined && moduleBindingIds !== undefined) {
348
- moduleBindingIds.push(this.identifier);
349
- }
350
- this.#versionAfterLastWrite = registry.version;
351
- }
352
- #restoreNonConflicting(displaced) {
353
- for (let index = displaced.length - 1; index >= 0; index -= 1) {
354
- const candidate = displaced[index];
355
- // A restore must never displace: a slot that has been re-occupied keeps its occupant, and
356
- // the candidate stays parked.
357
- if (!this.#registration.registry.hasSlotOccupant(candidate)) {
358
- this.#registration.registry.add(candidate);
359
- this.#registration.onRestored?.(candidate);
360
- displaced.splice(index, 1);
361
- }
362
- }
363
- }
364
254
  }
@@ -1,11 +1,12 @@
1
- import type { BindToBuilder } from "#core/binding";
1
+ import type { BindToBuilder } from "#core/binding-builders";
2
2
  import type { AsyncModule, SyncModule } from "#core/module";
3
3
  import type { Token } from "#core/token";
4
4
  import type { ActivationHandler, BindingIdentifier, Constructor, DeactivationHandler, ResolveOptions } from "#core/types";
5
5
  import type { AutoRegisterRegistry } from "#decorators/injectable";
6
6
  import type { ContainerGraphJson, GraphOptions } from "#introspection/dependency-graph";
7
+ import type { ExplainOptions, ResolutionExplanation } from "#introspection/explanation";
7
8
  import type { BindingSnapshot, ContainerSnapshot } from "#introspection/inspector";
8
- import type { MetadataReader } from "#metadata/metadata-types";
9
+ import type { MetadataReader } from "#metadata/types";
9
10
  /**
10
11
  * The public surface: binding, resolving, modules, lifecycle hooks, child scopes, and disposal.
11
12
  *
@@ -35,13 +36,20 @@ export interface Container {
35
36
  createChild(): Container;
36
37
  dispose(): Promise<void>;
37
38
  [Symbol.asyncDispose](): Promise<void>;
38
- [Symbol.dispose](): never;
39
39
  initializeAsync(): Promise<void>;
40
40
  validate(): void;
41
41
  has<Names extends string = string>(token: Token<unknown, Names> | Constructor, options?: NoInfer<ResolveOptions<Names>>): boolean;
42
42
  hasOwn<Names extends string = string>(token: Token<unknown, Names> | Constructor, options?: NoInfer<ResolveOptions<Names>>): boolean;
43
43
  lookupBindings<Value>(token: Token<Value> | Constructor<Value>): ReadonlyArray<BindingSnapshot>;
44
44
  inspect(): ContainerSnapshot;
45
+ /**
46
+ * Explains why a request selects the binding it does, reading each registry the way `resolve` does and deciding
47
+ * by the same rules, without instantiating anything.
48
+ *
49
+ * @remarks A `when()` predicate runs here as it would in `resolve`. `options.ancestors` names the resolutions the
50
+ * request is nested in, so a predicate that reads the parent sees the one it would.
51
+ */
52
+ explain<Names extends string = string>(token: Token<unknown, Names> | Constructor, options?: NoInfer<ExplainOptions<Names>>): ResolutionExplanation;
45
53
  generateDependencyGraph(options?: GraphOptions): ContainerGraphJson;
46
54
  }
47
55
  /**