@codefast/di 0.9.0 → 0.10.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 (44) hide show
  1. package/CHANGELOG.md +137 -0
  2. package/README.md +3 -3
  3. package/dist/ambient/active-container.d.ts +13 -0
  4. package/dist/ambient/active-container.js +24 -0
  5. package/dist/container/binding-builders.d.ts +31 -9
  6. package/dist/container/binding-builders.js +155 -87
  7. package/dist/container/container.d.ts +2 -2
  8. package/dist/container/container.js +33 -16
  9. package/dist/core/binding.d.ts +36 -43
  10. package/dist/core/binding.js +11 -37
  11. package/dist/core/registry.d.ts +47 -4
  12. package/dist/core/registry.js +361 -159
  13. package/dist/core/state-epoch.d.ts +16 -0
  14. package/dist/core/state-epoch.js +21 -0
  15. package/dist/core/types.d.ts +7 -4
  16. package/dist/errors/diagnostics.d.ts +2 -0
  17. package/dist/errors/errors.d.ts +29 -0
  18. package/dist/errors/errors.js +35 -0
  19. package/dist/index.d.ts +1 -1
  20. package/dist/index.js +1 -1
  21. package/dist/injection/descriptor.d.ts +6 -4
  22. package/dist/injection/descriptor.js +3 -1
  23. package/dist/injection/resolve-options.d.ts +6 -0
  24. package/dist/injection/resolve-options.js +16 -0
  25. package/dist/introspection/dependency-graph.js +10 -5
  26. package/dist/introspection/inspector.d.ts +3 -1
  27. package/dist/introspection/inspector.js +10 -24
  28. package/dist/lifecycle/lifecycle-manager.js +10 -8
  29. package/dist/lifecycle/scope-manager.js +1 -1
  30. package/dist/resolution/cache/activation-need.d.ts +2 -0
  31. package/dist/resolution/cache/activation-need.js +13 -6
  32. package/dist/resolution/cache/binding-lookup-cache.d.ts +34 -1
  33. package/dist/resolution/cache/binding-lookup-cache.js +96 -12
  34. package/dist/resolution/cache/class-introspector.d.ts +1 -1
  35. package/dist/resolution/cache/class-introspector.js +28 -14
  36. package/dist/resolution/context.d.ts +8 -8
  37. package/dist/resolution/plan/instantiation-plan.d.ts +12 -0
  38. package/dist/resolution/plan/instantiation-plan.js +156 -65
  39. package/dist/resolution/plan/plan-codegen.d.ts +100 -0
  40. package/dist/resolution/plan/plan-codegen.js +185 -0
  41. package/dist/resolution/resolver.d.ts +14 -4
  42. package/dist/resolution/resolver.js +375 -134
  43. package/dist/resolution/select/binding-select.js +12 -7
  44. package/package.json +9 -1
@@ -1,8 +1,8 @@
1
- import { clearBindingFrame, createBinding, createBindingSlot, DEFAULT_BINDING_SLOT, refinableFields, } from "#/core/binding";
1
+ import { clearBindingFrame, createBindingSlot, DEFAULT_BINDING_SLOT, generateBindingId, NO_INSTANCE, } 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 { ChainNotRegisteredError, SelfBindingRequiresClassError } from "#/errors/errors";
5
+ import { ChainAlreadyRegisteredError, ChainNotRegisteredError, ManyBindingSlotError, SelfBindingRequiresClassError, } from "#/errors/errors";
6
6
  import { normalizeToDescriptor } from "#/injection/descriptor";
7
7
  /** One criterion per key: re-tagging the same key replaces it rather than asking for both values. */
8
8
  function updateSlotTag(slot, criterion) {
@@ -16,112 +16,139 @@ function updateSlotTag(slot, criterion) {
16
16
  }
17
17
  return createBindingSlot(tags);
18
18
  }
19
- /** Record a module's binding id, dropping the id the chain re-slotted away from. */
20
- function trackBindingForModule(ids, id, previousId) {
21
- if (previousId !== undefined) {
22
- const previousIndex = ids.indexOf(previousId);
23
- if (previousIndex !== -1) {
24
- ids.splice(previousIndex, 1);
25
- }
26
- }
27
- ids.push(id);
28
- }
29
19
  // ── BindingChain ─────────────────────────────────────────────────────────────────────────────────────────────────────
30
20
  /**
31
- * The one builder behind `bind()` and every `to*()` return type. Each interface exposes only the
32
- * calls that are legal at that point in the chain; the runtime object is shared because every
33
- * refinement is the same operation — narrow the registered binding, keep its id.
21
+ * The one builder behind `bind()` and every `to*()` return type — and the binding it registers.
22
+ *
23
+ * @remarks The binding fields come first, in the order every binding shares, and the chain's own
24
+ * bookkeeping follows as private fields; `to*()` fills the fields in place and hands this object to
25
+ * the registry, so a plain bind is one allocation. Refinements write the registered object: a scope
26
+ * or hook in place, a slot or predicate through the registry so its indexes follow.
34
27
  *
35
28
  * @since 0.5.0-canary.8
36
29
  */
37
30
  export class BindingChain {
38
- // Undefined until a `to*()` call registers the binding.
39
- #binding;
31
+ // The binding. Every field, every kind, written in this order and nowhere else: one hidden class.
32
+ kind = "constant";
33
+ identifier = generateBindingId();
34
+ inFlight = false;
35
+ frame = undefined;
36
+ instance = NO_INSTANCE;
37
+ token;
38
+ slot = DEFAULT_BINDING_SLOT;
39
+ predicate = undefined;
40
+ isMany = false;
41
+ scope = "singleton";
42
+ target = undefined;
43
+ factory = undefined;
44
+ deps = undefined;
45
+ value = undefined;
46
+ activationHook = undefined;
47
+ deactivationHook = undefined;
48
+ // The chain. Set by the first `to*()`, which is the only one a chain accepts.
49
+ #isRegistered = false;
40
50
  // Allocated only by a chain that actually displaces something — most never do.
41
51
  #displacedByChain;
42
52
  // Registry version after this chain's last write — a mismatch means someone else wrote in between.
43
53
  #versionAfterLastWrite = -1;
44
- #token;
45
54
  #registration;
46
55
  constructor(token, registration) {
47
- this.#token = token;
56
+ this.token = token;
48
57
  this.#registration = registration;
49
58
  }
50
- /** The registered binding, or a loud failure if no `to*()` has run yet. */
51
- #registered() {
52
- if (this.#binding === undefined) {
53
- throw new ChainNotRegisteredError(tokenName(this.#token));
59
+ /** This object as the registry and the resolver see it: the value type erased once, here. */
60
+ get #binding() {
61
+ return this;
62
+ }
63
+ /** Loud failure for a refinement before `to*()`. */
64
+ #requireRegistered() {
65
+ if (!this.#isRegistered) {
66
+ throw new ChainNotRegisteredError(tokenName(this.token));
54
67
  }
55
- return this.#binding;
56
- }
57
- #register(partial) {
58
- // Each `to*()` starts its own registration, so anything a previous one displaced is not this
59
- // registration's to restore.
60
- this.#displacedByChain = undefined;
61
- this.#binding = createBinding(partial, this.#token, DEFAULT_BINDING_SLOT, undefined);
62
- this.#commit(this.#binding, undefined);
68
+ }
69
+ /** Loud failure for a second `to*()`, raised before the first one's fields can be overwritten. */
70
+ #requireUnregistered() {
71
+ if (this.#isRegistered) {
72
+ throw new ChainAlreadyRegisteredError(tokenName(this.token));
73
+ }
74
+ }
75
+ #register(kind, scope) {
76
+ this.kind = kind;
77
+ this.scope = scope;
78
+ this.#isRegistered = true;
79
+ this.#commit(undefined);
63
80
  return this;
64
81
  }
65
82
  // ── Registration ───────────────────────────────────────────────────────────────────────────────────────────────────
66
83
  to(type) {
67
- return this.#register({ kind: "class", target: type, scope: "transient" });
84
+ this.#requireUnregistered();
85
+ this.target = type;
86
+ return this.#register("class", "transient");
68
87
  }
69
88
  toSelf() {
70
- if (typeof this.#token !== "function") {
71
- throw new SelfBindingRequiresClassError(tokenName(this.#token));
89
+ this.#requireUnregistered();
90
+ if (typeof this.token !== "function") {
91
+ throw new SelfBindingRequiresClassError(tokenName(this.token));
72
92
  }
73
- return this.#register({ kind: "class", target: this.#token, scope: "transient" });
93
+ this.target = this.token;
94
+ return this.#register("class", "transient");
74
95
  }
75
96
  toConstantValue(value) {
76
- return this.#register({ kind: "constant", scope: "singleton", value });
97
+ this.#requireUnregistered();
98
+ this.value = value;
99
+ return this.#register("constant", "singleton");
77
100
  }
78
101
  toDynamic(factory) {
79
- return this.#register({ kind: "dynamic", factory, scope: "transient" });
102
+ this.#requireUnregistered();
103
+ this.factory = factory;
104
+ return this.#register("dynamic", "transient");
80
105
  }
81
106
  toDynamicAsync(factory) {
82
- return this.#register({ kind: "dynamic-async", factory, scope: "transient" });
107
+ this.#requireUnregistered();
108
+ this.factory = factory;
109
+ return this.#register("dynamic-async", "transient");
83
110
  }
84
111
  toResolved(factory, deps) {
85
- return this.#register({
86
- kind: "resolved",
87
- deps: deps.map((dependency) => normalizeToDescriptor(dependency)),
88
- factory: factory,
89
- scope: "transient",
90
- });
112
+ this.#requireUnregistered();
113
+ this.factory = factory;
114
+ this.deps = deps.map((dependency) => normalizeToDescriptor(dependency));
115
+ return this.#register("resolved", "transient");
91
116
  }
92
117
  toResolvedAsync(factory, deps) {
93
- return this.#register({
94
- kind: "resolved-async",
95
- deps: deps.map((dependency) => normalizeToDescriptor(dependency)),
96
- factory: factory,
97
- scope: "transient",
98
- });
118
+ this.#requireUnregistered();
119
+ this.factory = factory;
120
+ this.deps = deps.map((dependency) => normalizeToDescriptor(dependency));
121
+ return this.#register("resolved-async", "transient");
99
122
  }
100
123
  toAlias(target) {
101
- return this.#register({ kind: "alias", scope: "transient", target });
124
+ this.#requireUnregistered();
125
+ this.target = target;
126
+ return this.#register("alias", "transient");
102
127
  }
103
128
  // ── Refinement ─────────────────────────────────────────────────────────────────────────────────────────────────────
104
- // Slot and predicate are what the registry indexes on, so a re-slot rebuilds the binding and
105
- // re-registers it — under the original id, keeping `id()` stable for the whole chain.
129
+ // Slot and predicate are what the registry indexes on, so a re-slot takes the binding out of the
130
+ // registry, rewrites the two fields while it is out, and registers it again — same object, same id.
106
131
  #reslot(slot, predicate) {
107
- const previous = this.#registered();
108
- this.#binding = createBinding(previous, previous.token, slot, predicate, previous.id);
109
- this.#commit(this.#binding, previous.id);
110
- // The tracked-singleton list holds object references, and the re-slot just replaced the object.
111
- this.#registration.scope.replaceSingleton(previous, this.#binding);
132
+ this.#requireRegistered();
133
+ this.#commit(() => {
134
+ this.slot = slot;
135
+ this.predicate = predicate;
136
+ // The frame reports the slot, so a resolve before this call memoized the previous one.
137
+ clearBindingFrame(this.#binding);
138
+ });
112
139
  return this;
113
140
  }
114
141
  #withScope(scope) {
115
- const binding = this.#registered();
116
- if (binding.scope !== scope) {
142
+ this.#requireRegistered();
143
+ if (this.scope !== scope) {
117
144
  // An instance cached under the old scope must not survive the change — a later flip back
118
145
  // to that scope would resurrect it.
119
- this.#registration.scope.deleteSingleton(binding);
120
- this.#registration.scope.deleteScoped(binding.id);
121
- refinableFields(binding).scope = scope;
146
+ this.#registration.scope.deleteSingleton(this.#binding);
147
+ this.#registration.scope.deleteScoped(this.identifier);
148
+ this.scope = scope;
122
149
  }
123
150
  // The frame reports the scope, so a resolve before this call memoized the previous one.
124
- clearBindingFrame(binding);
151
+ clearBindingFrame(this.#binding);
125
152
  this.#registration.registry.touch();
126
153
  this.#versionAfterLastWrite = this.#registration.registry.version;
127
154
  return this;
@@ -129,26 +156,60 @@ export class BindingChain {
129
156
  // SPEC calls a candidate a binding that passes *all* of a chain's predicates, and the chain type
130
157
  // reads as refinement, so a second `when()` narrows rather than replaces.
131
158
  when(predicate) {
132
- const binding = this.#registered();
133
- const previous = binding.predicate;
134
- if (previous === undefined) {
135
- return this.#reslot(binding.slot, predicate);
136
- }
159
+ this.#requireRegistered();
160
+ const previous = this.predicate;
137
161
  // The composite carries both sides' requirements, so validate() still sees them.
138
- const composed = mergingConstraintRequirements((ctx) => previous(ctx) && predicate(ctx), previous, predicate);
139
- return this.#reslot(binding.slot, composed);
162
+ const narrowed = previous === undefined
163
+ ? predicate
164
+ : mergingConstraintRequirements((ctx) => previous(ctx) && predicate(ctx), previous, predicate);
165
+ const { registry } = this.#registration;
166
+ // The slot is unchanged, so nothing has to be re-indexed or displaced. With the last registry
167
+ // write this chain's own and nothing parked, the binding is provably live and is rewritten in
168
+ // place; otherwise the re-slot path re-checks liveness and restores what the shape frees.
169
+ if (registry.version === this.#versionAfterLastWrite && this.#displacedByChain === undefined) {
170
+ registry.setPredicate(this.#binding, narrowed);
171
+ this.#versionAfterLastWrite = registry.version;
172
+ return this;
173
+ }
174
+ return this.#reslot(this.slot, narrowed);
140
175
  }
141
176
  whenNamed(name) {
142
177
  return this.whenTagged(slotName.of(name));
143
178
  }
144
179
  whenTagged(criterion) {
145
- const binding = this.#registered();
146
- return this.#reslot(updateSlotTag(binding.slot, criterion), binding.predicate);
180
+ this.#requireRegistered();
181
+ if (this.isMany) {
182
+ throw new ManyBindingSlotError(tokenName(this.token));
183
+ }
184
+ return this.#reslot(updateSlotTag(this.slot, criterion), this.predicate);
185
+ }
186
+ many() {
187
+ this.#requireRegistered();
188
+ if (this.slot.tags.length !== 0) {
189
+ throw new ManyBindingSlotError(tokenName(this.token));
190
+ }
191
+ if (this.isMany) {
192
+ return this;
193
+ }
194
+ const { registry } = this.#registration;
195
+ // With the last registry write this chain's own and nothing parked, the binding is provably live
196
+ // and displaced nobody, so membership is written in place and the registry only moves it out of
197
+ // the lone map. Otherwise it goes through the re-slot path, which re-checks liveness and restores
198
+ // an ordinary binding this chain's `to*()` displaced, now that the member frees its slot.
199
+ if (registry.version === this.#versionAfterLastWrite && this.#displacedByChain === undefined) {
200
+ registry.setMany(this.#binding);
201
+ this.#versionAfterLastWrite = registry.version;
202
+ return this;
203
+ }
204
+ this.#commit(() => {
205
+ this.isMany = true;
206
+ });
207
+ return this;
147
208
  }
148
209
  whenDefault() {
149
210
  // The default slot is what a fresh registration already has, so there is nothing to re-slot —
150
211
  // but an unregistered chain must fail here exactly as it does in every other refinement.
151
- this.#registered();
212
+ this.#requireRegistered();
152
213
  return this;
153
214
  }
154
215
  singleton() {
@@ -161,40 +222,42 @@ export class BindingChain {
161
222
  return this.#withScope("scoped");
162
223
  }
163
224
  onActivation(fn) {
164
- refinableFields(this.#registered()).onActivation = fn;
225
+ this.#requireRegistered();
226
+ this.activationHook = fn;
165
227
  this.#registration.registry.touch();
166
228
  this.#versionAfterLastWrite = this.#registration.registry.version;
167
229
  return this;
168
230
  }
169
231
  onDeactivation(fn) {
170
- refinableFields(this.#registered()).onDeactivation = fn;
232
+ this.#requireRegistered();
233
+ this.deactivationHook = fn;
171
234
  this.#registration.registry.touch();
172
235
  this.#versionAfterLastWrite = this.#registration.registry.version;
173
236
  return this;
174
237
  }
175
238
  id() {
176
- return this.#registered().id;
239
+ this.#requireRegistered();
240
+ return this.identifier;
177
241
  }
178
242
  // ── Registry ───────────────────────────────────────────────────────────────────────────────────────────────────────
179
243
  /**
180
- * Registers `binding`, first removing `previousId` when the chain is re-slotting.
244
+ * Registers this binding; with `rewrite` given, first takes the live binding out, rewrites it, and
245
+ * registers it again.
181
246
  *
182
247
  * @remarks A `when*()` that follows `to*()` re-slots an already-live binding and can displace one
183
248
  * the final shape would never conflict with. Those stay parked in `#displacedByChain` until the
184
249
  * chain settles, then get restored.
185
250
  */
186
- #commit(binding, previousId) {
251
+ #commit(rewrite) {
187
252
  const { registry, moduleBindingIds } = this.#registration;
188
- // The registry is heterogeneous by design — one instance holds every value type — so the
189
- // chain's `Binding<Value>` is erased once, here, rather than at each call site.
190
- const registered = binding;
191
- if (previousId !== undefined) {
253
+ const registered = this.#binding;
254
+ if (rewrite !== undefined) {
192
255
  // A registry someone else wrote since this chain's last write invalidates the parked
193
256
  // snapshot: restoring it could undo an unbind or shadow a newer binding.
194
257
  if (registry.version !== this.#versionAfterLastWrite) {
195
258
  this.#displacedByChain = undefined;
196
259
  }
197
- if (registry.removeById(previousId) === undefined) {
260
+ if (!registry.reslot(registered, rewrite)) {
198
261
  // The chain's binding is no longer live (unbound or displaced) — a refinement must not
199
262
  // resurrect it, so the chain goes inert against the registry.
200
263
  this.#displacedByChain = undefined;
@@ -202,15 +265,20 @@ export class BindingChain {
202
265
  return;
203
266
  }
204
267
  }
268
+ else {
269
+ // Each `to*()` starts its own registration, so anything a previous one displaced is not this
270
+ // registration's to restore.
271
+ this.#displacedByChain = undefined;
272
+ }
205
273
  const displaced = registry.add(registered);
206
274
  if (displaced !== undefined) {
207
275
  (this.#displacedByChain ??= []).push(displaced);
208
276
  }
209
- if (previousId !== undefined && this.#displacedByChain !== undefined) {
277
+ if (rewrite !== undefined && this.#displacedByChain !== undefined) {
210
278
  this.#restoreNonConflicting(this.#displacedByChain);
211
279
  }
212
- if (moduleBindingIds !== undefined) {
213
- trackBindingForModule(moduleBindingIds, registered.id, previousId);
280
+ if (rewrite === undefined && moduleBindingIds !== undefined) {
281
+ moduleBindingIds.push(this.identifier);
214
282
  }
215
283
  this.#versionAfterLastWrite = registry.version;
216
284
  }
@@ -30,8 +30,8 @@ export interface Container {
30
30
  resolveAsync<Value, Names extends string = string>(token: Token<Value, Names> | Constructor<Value>, options?: NoInfer<ResolveOptions<Names>>): Promise<Value>;
31
31
  resolveOptional<Value, Names extends string = string>(token: Token<Value, Names> | Constructor<Value>, options?: NoInfer<ResolveOptions<Names>>): Value | undefined;
32
32
  resolveOptionalAsync<Value, Names extends string = string>(token: Token<Value, Names> | Constructor<Value>, options?: NoInfer<ResolveOptions<Names>>): Promise<Value | undefined>;
33
- resolveAll<Value, Names extends string = string>(token: Token<Value, Names> | Constructor<Value>, options?: NoInfer<ResolveOptions<Names>>): Array<Value>;
34
- resolveAllAsync<Value, Names extends string = string>(token: Token<Value, Names> | Constructor<Value>, options?: NoInfer<ResolveOptions<Names>>): Promise<Array<Value>>;
33
+ resolveAll<Value, Names extends string = string>(token: Token<Value, Names> | Constructor<Value>, options?: NoInfer<ResolveOptions<Names>>): ReadonlyArray<Value>;
34
+ resolveAllAsync<Value, Names extends string = string>(token: Token<Value, Names> | Constructor<Value>, options?: NoInfer<ResolveOptions<Names>>): Promise<ReadonlyArray<Value>>;
35
35
  createChild(): Container;
36
36
  dispose(): Promise<void>;
37
37
  [Symbol.asyncDispose](): Promise<void>;
@@ -45,6 +45,7 @@ const APPLY_BINDING_SCOPE = {
45
45
  builder.transient();
46
46
  },
47
47
  };
48
+ const NO_DEACTIVATION_PAIRS = Object.freeze([]);
48
49
  // ── DefaultContainer ─────────────────────────────────────────────────────────────────────────────────────────────────
49
50
  class DefaultContainer {
50
51
  #disposed = false;
@@ -74,6 +75,7 @@ class DefaultContainer {
74
75
  return (this.#inspector ??= new Inspector(this.#registry, this.#scope, this.#parent !== undefined, () => this.#disposed));
75
76
  }
76
77
  [RESOLUTION_DIAGNOSTICS]() {
78
+ const resolverCaches = this.#resolver.describeCaches();
77
79
  const builtSubsystems = [];
78
80
  if (this.#inspector !== undefined) {
79
81
  builtSubsystems.push("container.inspector");
@@ -81,6 +83,12 @@ class DefaultContainer {
81
83
  if (this.#moduleRefs !== undefined || this.#moduleBindingIds !== undefined) {
82
84
  builtSubsystems.push("container.moduleTables");
83
85
  }
86
+ if (this.#registry.isRecordMapBuilt) {
87
+ builtSubsystems.push("registry.records");
88
+ }
89
+ if (this.#registry.isIdIndexBuilt) {
90
+ builtSubsystems.push("registry.idIndex");
91
+ }
84
92
  if (this.#registry.isTaggedIndexBuilt) {
85
93
  builtSubsystems.push("registry.taggedIndex");
86
94
  }
@@ -90,7 +98,8 @@ class DefaultContainer {
90
98
  if (this.#lifecycle.isActivationTableBuilt) {
91
99
  builtSubsystems.push("lifecycle.activationHooks");
92
100
  }
93
- return { ...this.#resolver.describeCaches(), scopedInstanceCount: this.#scope.scopedCount, builtSubsystems };
101
+ builtSubsystems.push(...resolverCaches.builtSubsystems);
102
+ return { ...resolverCaches, scopedInstanceCount: this.#scope.scopedCount, builtSubsystems };
94
103
  }
95
104
  #initResolver(configuredReader) {
96
105
  const parent = this.#parent;
@@ -100,7 +109,7 @@ class DefaultContainer {
100
109
  }
101
110
  /** What a container being constructed under this one inherits: a reader bound here, else this one's. */
102
111
  #readerForChild() {
103
- if (this.#registry.getAll(MetadataReaderToken).length > 0) {
112
+ if (this.#registry.has(MetadataReaderToken)) {
104
113
  try {
105
114
  return this.#resolver.resolve(MetadataReaderToken, undefined, []);
106
115
  }
@@ -144,9 +153,9 @@ class DefaultContainer {
144
153
  }
145
154
  /** Remove bindings from registry + scope and collect [binding, instance] pairs for deactivation. */
146
155
  #collectDeactivationPairs(tokenOrId) {
147
- if (typeof tokenOrId === "string") {
156
+ if (typeof tokenOrId === "number") {
148
157
  const binding = this.#registry.removeById(tokenOrId);
149
- return binding === undefined ? [] : this.#drainSingletons([binding]);
158
+ return binding === undefined ? NO_DEACTIVATION_PAIRS : this.#drainSingletons([binding]);
150
159
  }
151
160
  // Dropping the whole token in one pass: removing each binding by id instead would re-scan and
152
161
  // re-index the token's binding list once per binding.
@@ -154,18 +163,20 @@ class DefaultContainer {
154
163
  }
155
164
  /** Drain scope entries for already-removed bindings, and pair each one that still owes a deactivation. */
156
165
  #drainSingletons(bindings) {
157
- const pairs = [];
166
+ // Allocated by the first pair owed: an unbind or rebind of a binding nothing ever cached — the
167
+ // hot-swap shape — owes no deactivation and hands the shared empty list back.
168
+ let pairs;
158
169
  for (const binding of bindings) {
159
170
  if (binding.instance !== NO_INSTANCE) {
160
- pairs.push([binding, binding.instance]);
171
+ (pairs ??= []).push([binding, binding.instance]);
161
172
  this.#scope.deleteSingleton(binding);
162
173
  }
163
174
  else if (this.#owesConstantDeactivation(binding)) {
164
- pairs.push([binding, binding.value]);
175
+ (pairs ??= []).push([binding, binding.value]);
165
176
  }
166
- this.#scope.deleteScoped(binding.id);
177
+ this.#scope.deleteScoped(binding.identifier);
167
178
  }
168
- return pairs;
179
+ return pairs ?? NO_DEACTIVATION_PAIRS;
169
180
  }
170
181
  /**
171
182
  * Whether a constant still owes its deactivation.
@@ -176,7 +187,7 @@ class DefaultContainer {
176
187
  */
177
188
  #owesConstantDeactivation(binding) {
178
189
  return (binding.kind === "constant" &&
179
- (binding.onDeactivation !== undefined || this.#lifecycle.hasDeactivationHandlers(binding.token)));
190
+ (binding.deactivationHook !== undefined || this.#lifecycle.hasDeactivationHandlers(binding.token)));
180
191
  }
181
192
  /** Runs every pair's deactivation even when one throws, then reports what threw. */
182
193
  #deactivatePairsSync(pairs) {
@@ -445,12 +456,18 @@ class DefaultContainer {
445
456
  resolveAll(token, options) {
446
457
  this.#assertNotDisposed();
447
458
  const rootStack = this.#resolver.rootStack;
459
+ if (options === undefined && rootStack.length === 0) {
460
+ return this.#resolver.resolveRootCollection(token);
461
+ }
448
462
  return rootStack.length === 0
449
463
  ? this.#resolver.resolveAll(token, options, rootStack)
450
464
  : this.#resolver.resolveAll(token, options, []);
451
465
  }
452
466
  resolveAllAsync(token, options) {
453
467
  this.#assertNotDisposed();
468
+ if (options === undefined) {
469
+ return this.#resolver.resolveRootCollectionAsync(token);
470
+ }
454
471
  return this.#resolver.resolveAllAsync(token, options, [], ROOT_BRANCH);
455
472
  }
456
473
  // ── Child ──────────────────────────────────────────────────────────────────────────────────────────────────────────
@@ -523,7 +540,7 @@ class DefaultContainer {
523
540
  // A constant is already its own instance, so only an activation hook gives the warm-up
524
541
  // something to run — and a container-level hook counts as one just as a per-binding hook does.
525
542
  if (binding.kind === "constant" &&
526
- binding.onActivation === undefined &&
543
+ binding.activationHook === undefined &&
527
544
  !this.#lifecycle.hasActivationHandlers(binding.token)) {
528
545
  continue;
529
546
  }
@@ -607,11 +624,11 @@ class DefaultContainer {
607
624
  #validateSingletonBindingGraph(root, reader) {
608
625
  const rootName = tokenName(root.token);
609
626
  const dfs = (current, pathNames, pathBindingIds) => {
610
- if (pathBindingIds.has(current.id)) {
627
+ if (pathBindingIds.has(current.identifier)) {
611
628
  return;
612
629
  }
613
630
  const extendedPathIds = new Set(pathBindingIds);
614
- extendedPathIds.add(current.id);
631
+ extendedPathIds.add(current.identifier);
615
632
  for (const edge of this.#collectStaticDependencyEdges(current, reader)) {
616
633
  const { terminal, depTokenName } = edge;
617
634
  const depScope = this.#validationScopeFromTerminal(terminal);
@@ -649,10 +666,10 @@ class DefaultContainer {
649
666
  const seenAliasIds = new Set();
650
667
  let current = binding;
651
668
  while (current !== undefined && current.kind === "alias") {
652
- if (seenAliasIds.has(current.id)) {
669
+ if (seenAliasIds.has(current.identifier)) {
653
670
  throw new CircularDependencyError(cyclePath);
654
671
  }
655
- seenAliasIds.add(current.id);
672
+ seenAliasIds.add(current.identifier);
656
673
  cyclePath.push(tokenName(current.token));
657
674
  const nextToken = current.target;
658
675
  const next = this.#resolver.peekBindingForValidate(nextToken, options);
@@ -701,7 +718,7 @@ class DefaultContainer {
701
718
  // ── Introspection ──────────────────────────────────────────────────────────────────────────────────────────────────
702
719
  has(token, options) {
703
720
  this.#assertNotDisposed();
704
- return this.#getInspector().has(token, options, () => this.#parent?.has(token, options) ?? false);
721
+ return this.#getInspector().hasOwn(token, options) || (this.#parent?.has(token, options) ?? false);
705
722
  }
706
723
  hasOwn(token, options) {
707
724
  this.#assertNotDisposed();