@codefast/di 0.9.0 → 0.10.1

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 (73) hide show
  1. package/CHANGELOG.md +165 -0
  2. package/README.md +6 -5
  3. package/dist/ambient/active-container.d.ts +14 -4
  4. package/dist/ambient/active-container.js +22 -1
  5. package/dist/container/binding-builders.d.ts +35 -13
  6. package/dist/container/binding-builders.js +174 -97
  7. package/dist/container/container.d.ts +10 -10
  8. package/dist/container/container.js +53 -36
  9. package/dist/core/binding-scope.d.ts +2 -2
  10. package/dist/core/binding.d.ts +42 -49
  11. package/dist/core/binding.js +12 -38
  12. package/dist/core/constraint-requirement.d.ts +1 -1
  13. package/dist/core/module.d.ts +3 -3
  14. package/dist/core/registry.d.ts +52 -9
  15. package/dist/core/registry.js +376 -162
  16. package/dist/core/state-epoch.d.ts +16 -0
  17. package/dist/core/state-epoch.js +21 -0
  18. package/dist/core/token.d.ts +1 -1
  19. package/dist/core/types.d.ts +12 -9
  20. package/dist/decorators/inject.d.ts +3 -3
  21. package/dist/decorators/inject.js +5 -5
  22. package/dist/decorators/injectable.d.ts +2 -2
  23. package/dist/decorators/injectable.js +2 -2
  24. package/dist/decorators/lifecycle-decorators.js +2 -2
  25. package/dist/errors/diagnostics.d.ts +2 -0
  26. package/dist/errors/errors.d.ts +29 -3
  27. package/dist/errors/errors.js +34 -2
  28. package/dist/index.d.ts +35 -35
  29. package/dist/index.js +19 -19
  30. package/dist/injection/descriptor.d.ts +9 -7
  31. package/dist/injection/descriptor.js +3 -1
  32. package/dist/injection/resolve-options.d.ts +9 -3
  33. package/dist/injection/resolve-options.js +17 -1
  34. package/dist/introspection/dependency-graph.d.ts +3 -3
  35. package/dist/introspection/dependency-graph.js +15 -10
  36. package/dist/introspection/graph-adapters/cytoscape.d.ts +1 -1
  37. package/dist/introspection/graph-adapters/dot.d.ts +1 -1
  38. package/dist/introspection/graph-adapters/mermaid.d.ts +1 -1
  39. package/dist/introspection/graph-adapters/reactflow.d.ts +1 -1
  40. package/dist/introspection/inspector.d.ts +7 -5
  41. package/dist/introspection/inspector.js +13 -27
  42. package/dist/lifecycle/lifecycle-manager.d.ts +4 -4
  43. package/dist/lifecycle/lifecycle-manager.js +13 -11
  44. package/dist/lifecycle/scope-manager.d.ts +2 -2
  45. package/dist/lifecycle/scope-manager.js +4 -4
  46. package/dist/metadata/metadata-reader-token.d.ts +2 -2
  47. package/dist/metadata/metadata-reader-token.js +1 -1
  48. package/dist/metadata/metadata-types.d.ts +3 -3
  49. package/dist/metadata/symbol-metadata-reader.d.ts +3 -3
  50. package/dist/metadata/symbol-metadata-reader.js +1 -1
  51. package/dist/metadata/verifying-metadata-reader.d.ts +1 -1
  52. package/dist/metadata/verifying-metadata-reader.js +2 -2
  53. package/dist/resolution/cache/activation-need.d.ts +6 -4
  54. package/dist/resolution/cache/activation-need.js +13 -6
  55. package/dist/resolution/cache/binding-lookup-cache.d.ts +39 -6
  56. package/dist/resolution/cache/binding-lookup-cache.js +97 -13
  57. package/dist/resolution/cache/class-introspector.d.ts +6 -6
  58. package/dist/resolution/cache/class-introspector.js +82 -69
  59. package/dist/resolution/context.d.ts +11 -11
  60. package/dist/resolution/context.js +1 -1
  61. package/dist/resolution/path/resolution-path.d.ts +1 -1
  62. package/dist/resolution/path/resolution-path.js +1 -1
  63. package/dist/resolution/plan/instantiation-plan.d.ts +16 -4
  64. package/dist/resolution/plan/instantiation-plan.js +160 -69
  65. package/dist/resolution/plan/plan-codegen.d.ts +100 -0
  66. package/dist/resolution/plan/plan-codegen.js +185 -0
  67. package/dist/resolution/resolver.d.ts +26 -16
  68. package/dist/resolution/resolver.js +401 -147
  69. package/dist/resolution/select/binding-select.d.ts +6 -5
  70. package/dist/resolution/select/binding-select.js +17 -11
  71. package/dist/resolution/select/constraints.d.ts +3 -3
  72. package/dist/resolution/select/constraints.js +4 -4
  73. package/package.json +11 -3
@@ -1,21 +1,33 @@
1
- import { NO_INSTANCE } from "#/core/binding";
2
- import { NO_TAG_KEYS, slotNameCriterionOf } from "#/core/tag";
3
- import { tokenName } from "#/core/token";
4
- import { AsyncActivationError, AsyncResolutionError, CircularDependencyError, DisposedContainerError, InternalError, MissingMetadataError, MissingScopeContextError, NoMatchingBindingError, TokenNotBoundError, } from "#/errors/errors";
5
- import { resolveOptionsForSlot, singleCriterionForSlot, singleCriterionOnlyOf } from "#/injection/resolve-options";
6
- import { SCOPED_MISS } from "#/lifecycle/scope-manager";
7
- import { ActivationNeedCache } from "#/resolution/cache/activation-need";
8
- import { BindingLookupCache } from "#/resolution/cache/binding-lookup-cache";
9
- import { ClassIntrospector } from "#/resolution/cache/class-introspector";
10
- import { AsyncCascadeContext, AsyncLevelContext, buildResolutionFrame, DefaultResolutionContext, } from "#/resolution/context";
11
- import { branchDepthOf, cycleNamesOf, enterResolutionPath, extendResolutionBranch, ROOT_BRANCH, UNOWNED_BRANCH, } from "#/resolution/path/resolution-path";
12
- import { InstantiationPlanCompiler, PLAN_RETRY } from "#/resolution/plan/instantiation-plan";
13
- import { matchesSlot, requestedTagKeyMask, selectAllBindings, selectBinding } from "#/resolution/select/binding-select";
1
+ import { NO_INSTANCE } from "#core/binding";
2
+ import { NO_TAG_KEYS, slotNameCriterionOf } from "#core/tag";
3
+ import { tokenName } from "#core/token";
4
+ import { AsyncActivationError, AsyncResolutionError, CircularDependencyError, DisposedContainerError, InternalError, MissingMetadataError, MissingScopeContextError, NoMatchingBindingError, TokenNotBoundError, } from "#errors/errors";
5
+ import { loneTagBesideNameOf, resolveOptionsForSlot, singleCriterionForSlot, singleCriterionOnlyOf, } from "#injection/resolve-options";
6
+ import { SCOPED_MISS } from "#lifecycle/scope-manager";
7
+ import { ActivationNeedCache } from "#resolution/cache/activation-need";
8
+ import { BindingLookupCache } from "#resolution/cache/binding-lookup-cache";
9
+ import { ClassIntrospector } from "#resolution/cache/class-introspector";
10
+ import { AsyncCascadeContext, AsyncLevelContext, buildResolutionFrame, DefaultResolutionContext, } from "#resolution/context";
11
+ import { branchDepthOf, cycleNamesOf, enterResolutionPath, extendResolutionBranch, ROOT_BRANCH, UNOWNED_BRANCH, } from "#resolution/path/resolution-path";
12
+ import { InstantiationPlanCompiler, PLAN_RETRY } from "#resolution/plan/instantiation-plan";
13
+ import { matchesSlot, requestedTagKeyMask, selectAllBindings, selectBinding } from "#resolution/select/binding-select";
14
14
  // Where a multi-tag resolve switches from scanning the token's list to walking the tag indexes.
15
15
  const MULTI_TAG_INDEX_THRESHOLD = 8;
16
16
  const EMPTY_STRING_LIST = [];
17
17
  const EMPTY_FRAME_LIST = [];
18
18
  const EMPTY_PARAM_LIST = [];
19
+ /** Plans compiled so far, with the `null` unplannable marks left out. */
20
+ function countCompiledPlans(plans) {
21
+ let count = 0;
22
+ if (plans !== undefined) {
23
+ for (const plan of plans.values()) {
24
+ if (plan !== null) {
25
+ count += 1;
26
+ }
27
+ }
28
+ }
29
+ return count;
30
+ }
19
31
  const ROOT_CONSTRAINT_CONTEXT = {
20
32
  resolutionPath: EMPTY_STRING_LIST,
21
33
  resolutionStack: EMPTY_FRAME_LIST,
@@ -45,12 +57,13 @@ export class DependencyResolver {
45
57
  // because synchronous code does not interleave.
46
58
  #cascadeStack = [];
47
59
  #cascadeContext;
48
- // Compiled plans; `null` marks a binding as unplannable under the current cache versions.
49
- #classPlanByBindingId = new Map();
60
+ // Compiled plans; `null` marks a binding as unplannable under the current cache versions. Both
61
+ // maps are allocated by the first plan request, which only a class or resolved binding makes.
62
+ #classPlanByBindingId;
50
63
  #classPlanRegistryVersion = -1;
51
64
  #classPlanActivationVersion = -1;
52
65
  // The async lane's plans, stamped and invalidated apart so neither lane pays the other's misses.
53
- #asyncPlanByBindingId = new Map();
66
+ #asyncPlanByBindingId;
54
67
  #asyncPlanRegistryVersion = -1;
55
68
  #asyncPlanActivationVersion = -1;
56
69
  #registry;
@@ -60,6 +73,7 @@ export class DependencyResolver {
60
73
  #parent;
61
74
  #lookup;
62
75
  #classes;
76
+ // Built by the first interpreted resolve that asks; a plan-served or constant-only container never does.
63
77
  #activation;
64
78
  constructor(registry, scope, lifecycle, metadataReader, container, parent) {
65
79
  this.#registry = registry;
@@ -68,31 +82,37 @@ export class DependencyResolver {
68
82
  this.#metadataReader = metadataReader;
69
83
  this.#parent = parent;
70
84
  this.#lookup = new BindingLookupCache(registry, this, parent === undefined ? undefined : parent.#lookup);
71
- this.#classes = new ClassIntrospector(metadataReader, container);
72
- this.#activation = new ActivationNeedCache(lifecycle, this.#classes, registry);
85
+ this.#classes = new ClassIntrospector(metadataReader, container, parent === undefined ? undefined : parent.#classes);
73
86
  }
74
87
  /** The reader this resolver was built with, which is the one its container answers with. */
75
88
  get metadataReader() {
76
89
  return this.#metadataReader;
77
90
  }
78
- /** Structural counts for the {@link ResolutionDiagnostics} a container reports. */
91
+ #activationNeed() {
92
+ return (this.#activation ??= new ActivationNeedCache(this.#lifecycle, this.#classes, this.#registry));
93
+ }
94
+ /** Structural counts and the resolver-owned collaborators built so far, for the {@link ResolutionDiagnostics} a container reports. */
79
95
  describeCaches() {
80
- let compiledPlanCount = 0;
81
- for (const plan of this.#classPlanByBindingId.values()) {
82
- if (plan !== null) {
83
- compiledPlanCount += 1;
84
- }
96
+ const builtSubsystems = [];
97
+ if (this.#planCompiler !== undefined) {
98
+ builtSubsystems.push("resolver.planCompiler");
85
99
  }
86
- let compiledAsyncPlanCount = 0;
87
- for (const plan of this.#asyncPlanByBindingId.values()) {
88
- if (plan !== null) {
89
- compiledAsyncPlanCount += 1;
90
- }
100
+ if (this.#lookup.isMemoBuilt) {
101
+ builtSubsystems.push("resolver.lookupMemo");
102
+ }
103
+ if (this.#activation?.isMemoBuilt === true) {
104
+ builtSubsystems.push("resolver.activationNeedMemo");
105
+ }
106
+ const generatedPlanCount = this.#planCompiler?.generatedPlanCount ?? 0;
107
+ if (generatedPlanCount > 0) {
108
+ builtSubsystems.push("resolver.planCodegen");
91
109
  }
92
110
  return {
93
- compiledPlanCount,
94
- compiledAsyncPlanCount,
111
+ compiledPlanCount: countCompiledPlans(this.#classPlanByBindingId),
112
+ compiledAsyncPlanCount: countCompiledPlans(this.#asyncPlanByBindingId),
113
+ generatedPlanCount,
95
114
  syncContextPoolSize: this.#syncResolutionContextPool.length,
115
+ builtSubsystems,
96
116
  };
97
117
  }
98
118
  // ── Binding lookup ─────────────────────────────────────────────────────────────────────────────────────────────────
@@ -111,37 +131,67 @@ export class DependencyResolver {
111
131
  }
112
132
  else if (singleCriterion !== undefined) {
113
133
  const indexed = this.#registry.getSimpleTagged(token, singleCriterion);
114
- if (indexed !== undefined && this.#satisfiesPredicate(indexed, options, resolutionStack)) {
134
+ if (indexed === undefined) {
135
+ // A one-criterion request matches only a slot carrying exactly that criterion, and every such
136
+ // slot is in the index, so a miss here is a miss for this registry: nothing left to scan.
137
+ return this.#parent === undefined
138
+ ? undefined
139
+ : this.#parent.#findBinding(token, options, resolutionStack, singleCriterion);
140
+ }
141
+ if (this.#satisfiesPredicate(indexed, options, resolutionStack)) {
115
142
  return { binding: indexed, owner: this };
116
143
  }
117
144
  }
118
- else if (
119
- // A threshold switches the data structure, never the semantics: under it the generic scan
120
- // below beats walking the indexes, and both paths answer identically. Sized first, so a
121
- // small list pays one length read and nothing else.
122
- this.#registry.getAll(token).length > MULTI_TAG_INDEX_THRESHOLD &&
123
- requestedTagKeyMask(options) !== NO_TAG_KEYS) {
124
- // A multi-criterion request matches only slots whose every criterion it carries, and every
125
- // such slot is in the two tag indexes — their union is the whole candidate set, unscanned.
126
- const selected = this.#selectMultiTagged(token, options, resolutionStack);
127
- if (selected !== undefined) {
128
- return { binding: selected, owner: this };
129
- }
130
- return this.#parent === undefined
131
- ? undefined
132
- : this.#parent.#findBinding(token, options, resolutionStack, singleCriterion);
133
- }
134
- const bindings = this.#registry.getAll(token);
135
- if (bindings.length > 0) {
136
- // A lone candidate is its own selection: matching it is the whole decision, with no
137
- // specificity to weigh and no ambiguity to report.
138
- const selected = bindings.length === 1
139
- ? matchesSlot(bindings[0].slot, options) && this.#satisfiesPredicate(bindings[0], options, resolutionStack)
140
- ? bindings[0]
141
- : undefined
142
- : selectBinding(bindings, options, this.#makeConstraintContext(resolutionStack, options), tokenName(token));
143
- if (selected !== undefined) {
144
- return { binding: selected, owner: this };
145
+ else {
146
+ if (options.name !== undefined) {
147
+ const pairTag = loneTagBesideNameOf(options);
148
+ if (pairTag !== undefined) {
149
+ const nameCriterion = slotNameCriterionOf(options.name);
150
+ if (nameCriterion === undefined) {
151
+ // No binding anywhere has declared this name, so no slot in any registry can carry it.
152
+ return undefined;
153
+ }
154
+ // The exact two-criterion slot, memoized over the chain; a predicate or an alias declines to the scan.
155
+ const entry = this.#lookup.namedTaggedEntry(token, nameCriterion, pairTag);
156
+ if (entry !== null) {
157
+ return entry;
158
+ }
159
+ }
160
+ }
161
+ if (
162
+ // A threshold switches the data structure, never the semantics: under it the generic scan
163
+ // below beats walking the indexes, and both paths answer identically. Sized first, so a
164
+ // small list pays one length read and nothing else.
165
+ this.#registry.countBindings(token) > MULTI_TAG_INDEX_THRESHOLD &&
166
+ requestedTagKeyMask(options) !== NO_TAG_KEYS) {
167
+ // A multi-criterion request matches only slots whose every criterion it carries, and every
168
+ // such slot is in the two tag indexes — their union is the whole candidate set, unscanned.
169
+ const selected = this.#selectMultiTagged(token, options, resolutionStack);
170
+ if (selected !== undefined) {
171
+ return { binding: selected, owner: this };
172
+ }
173
+ return this.#parent === undefined
174
+ ? undefined
175
+ : this.#parent.#findBinding(token, options, resolutionStack, singleCriterion);
176
+ }
177
+ }
178
+ // A lone default-slot candidate is its own selection: the slot match is the whole decision,
179
+ // it carries no predicate, and asking for it first keeps the registry from materialising its list.
180
+ // An options-less request already probed the lone map above, so only a request with options asks again.
181
+ const lone = options === undefined ? undefined : this.#registry.getFastDefault(token);
182
+ if (lone !== undefined) {
183
+ if (matchesSlot(lone.slot, options)) {
184
+ return { binding: lone, owner: this };
185
+ }
186
+ }
187
+ else {
188
+ // The lone map has missed either way, so the record map is all that is left to read.
189
+ const bindings = this.#registry.getRecorded(token);
190
+ if (bindings.length > 0) {
191
+ const selected = this.#selectFromList(bindings, options, resolutionStack, token);
192
+ if (selected !== undefined) {
193
+ return { binding: selected, owner: this };
194
+ }
145
195
  }
146
196
  }
147
197
  if (this.#parent !== undefined) {
@@ -149,6 +199,31 @@ export class DependencyResolver {
149
199
  }
150
200
  return undefined;
151
201
  }
202
+ /**
203
+ * The scan a candidate list gets before full selection.
204
+ *
205
+ * @remarks A single slot match is the whole answer once its predicate, if any, agrees, and no
206
+ * match is a clean miss; neither needs a display name or a candidate array. Two slot matches
207
+ * hand the same list to full selection, which weighs specificity and reports ambiguity — so both
208
+ * lanes answer identically.
209
+ */
210
+ #selectFromList(bindings, options, resolutionStack, token) {
211
+ let match;
212
+ for (let index = 0; index < bindings.length; index += 1) {
213
+ const candidate = bindings[index];
214
+ if (candidate.isMany || !matchesSlot(candidate.slot, options)) {
215
+ continue;
216
+ }
217
+ if (match !== undefined) {
218
+ return selectBinding(bindings, options, this.#makeConstraintContext(resolutionStack, options), tokenName(token));
219
+ }
220
+ match = candidate;
221
+ }
222
+ if (match === undefined) {
223
+ return undefined;
224
+ }
225
+ return this.#satisfiesPredicate(match, options, resolutionStack) ? match : undefined;
226
+ }
152
227
  /**
153
228
  * The binding a token resolves to with alias hops followed, or a diagnostic throw.
154
229
  *
@@ -214,7 +289,7 @@ export class DependencyResolver {
214
289
  // Container-level hooks belong to the binding's owner — a child-registered hook must not
215
290
  // fire for a parent-owned binding, and the owner's must.
216
291
  const containerHooks = owner.#lifecycle.activationVersion === 0 ? undefined : owner.#lifecycle.activationHandlersFor(binding.token);
217
- if (binding.onActivation === undefined && (containerHooks === undefined || containerHooks.length === 0)) {
292
+ if (binding.activationHook === undefined && (containerHooks === undefined || containerHooks.length === 0)) {
218
293
  return this.#resolveTransientDynamicSyncFromContext(binding, resolutionStack);
219
294
  }
220
295
  return this.#resolveTransientDynamicActivatedSync(binding, containerHooks, resolutionStack);
@@ -268,8 +343,8 @@ export class DependencyResolver {
268
343
  throw new AsyncResolutionError(resolutionStack[0]?.tokenName ?? tokenDisplayName, tokenDisplayName);
269
344
  }
270
345
  let activated = factoryResult;
271
- if (binding.onActivation !== undefined) {
272
- const activationResult = binding.onActivation(resolutionCtx, activated);
346
+ if (binding.activationHook !== undefined) {
347
+ const activationResult = binding.activationHook(resolutionCtx, activated);
273
348
  if (activationResult instanceof Promise) {
274
349
  throw new AsyncActivationError(tokenDisplayName, "onActivation");
275
350
  }
@@ -291,8 +366,14 @@ export class DependencyResolver {
291
366
  binding.inFlight = false;
292
367
  }
293
368
  }
294
- /** Chain-summed activation version: a plan can inline a parent-owned binding, so a parent's hook registration must invalidate it. */
369
+ /**
370
+ * Chain-summed activation version: a plan can inline a parent-owned binding, so a parent's hook
371
+ * registration must invalidate it.
372
+ */
295
373
  #chainActivationVersion() {
374
+ if (this.#parent === undefined) {
375
+ return this.#lifecycle.activationVersion;
376
+ }
296
377
  let version = this.#lifecycle.activationVersion;
297
378
  for (let current = this.#parent; current !== undefined; current = current.#parent) {
298
379
  version += current.#lifecycle.activationVersion;
@@ -303,93 +384,142 @@ export class DependencyResolver {
303
384
  const registryVersion = this.#lookup.chainVersion();
304
385
  const activationVersion = this.#chainActivationVersion();
305
386
  if (registryVersion !== this.#classPlanRegistryVersion || activationVersion !== this.#classPlanActivationVersion) {
306
- this.#classPlanByBindingId.clear();
387
+ // The stamps start at -1, so the first request always lands here: allocate then, clear after.
388
+ if (this.#classPlanByBindingId === undefined) {
389
+ this.#classPlanByBindingId = new Map();
390
+ }
391
+ else {
392
+ this.#classPlanByBindingId.clear();
393
+ }
307
394
  this.#classPlanRegistryVersion = registryVersion;
308
395
  this.#classPlanActivationVersion = activationVersion;
309
396
  }
310
- const cached = this.#classPlanByBindingId.get(binding.id);
397
+ const plans = this.#classPlanByBindingId;
398
+ const cached = plans.get(binding.identifier);
311
399
  if (cached !== undefined) {
312
400
  return cached;
313
401
  }
314
- const compiled = this.#planCompiler.compile(binding);
402
+ const compiled = this.#compiler().compile(binding);
315
403
  if (compiled === PLAN_RETRY) {
316
404
  // Lifecycle metadata not discovered yet — the fallback resolve discovers it; retry then.
317
405
  return null;
318
406
  }
319
- this.#classPlanByBindingId.set(binding.id, compiled);
407
+ plans.set(binding.identifier, compiled);
320
408
  return compiled;
321
409
  }
322
- // Compiler behind #getInstantiationPlan — cold path, so the host indirection costs nothing hot.
323
- #planCompiler = new InstantiationPlanCompiler({
324
- hasActivationHandlers: (binding) => this.#ownerOf(binding).#lifecycle.hasActivationHandlers(binding.token),
325
- knownPostConstruct: (target) => this.#classes.knownPostConstruct(target),
326
- needsActiveContainer: (target) => this.#classes.needsActiveContainer(target),
327
- getConstructorMetadata: (target) => this.#classes.constructorMetadata(target),
328
- lookupDependencyEntry: (token) => {
329
- const entry = this.#lookup.defaultEntry(token);
330
- return entry === null ? null : { binding: entry.binding };
331
- },
332
- // Exactly what #findBinding's single-criterion lane accepts, minus the half that reads a path:
333
- // a predicate is the compiler's cue to leave the selection to the runtime.
334
- lookupPathIndependentEntry: (token, options) => {
335
- const singleCriterion = singleCriterionOnlyOf(options);
336
- if (singleCriterion === undefined) {
337
- return null;
338
- }
339
- const entry = this.#lookup.taggedEntry(token, singleCriterion);
340
- if (entry === null || entry.binding.predicate !== undefined || !matchesSlot(entry.binding.slot, options)) {
341
- return null;
342
- }
343
- return { binding: entry.binding };
344
- },
345
- getResolutionFrame: (binding) => this.#getResolutionFrame(binding),
346
- // Dispatches exactly as #resolveDep does, so an escaped dep is indistinguishable
347
- // from the same dep on a fully interpreted resolve.
348
- resolveEscaped: (token, options, arity, resolutionStack) => {
349
- if (arity === "all") {
350
- return this.resolveAll(token, options, resolutionStack);
351
- }
352
- if (arity === "optional") {
353
- return this.resolveOptional(token, options, resolutionStack);
354
- }
355
- if (options === undefined) {
356
- return this.resolveFromContext(token, resolutionStack);
357
- }
358
- return this.resolve(token, options, resolutionStack);
359
- },
360
- // Dispatches exactly as #resolveDepAsync does, for the async lane's escapes.
361
- resolveEscapedAsync: (token, options, arity, resolutionStack) => {
362
- if (arity === "all") {
363
- return this.resolveAllAsync(token, options, resolutionStack, UNOWNED_BRANCH);
364
- }
365
- if (arity === "optional") {
366
- return this.resolveOptionalAsync(token, options, resolutionStack, UNOWNED_BRANCH);
367
- }
368
- if (options === undefined) {
369
- return this.resolveAsyncFromContext(token, resolutionStack, UNOWNED_BRANCH);
370
- }
371
- return this.resolveAsync(token, options, resolutionStack, UNOWNED_BRANCH);
372
- },
373
- });
410
+ // Compiler behind #getInstantiationPlan — a cold path, and built on the first plan so a container
411
+ // that never resolves a class or a resolved factory never pays for the host.
412
+ #planCompiler;
413
+ #compiler() {
414
+ return (this.#planCompiler ??= new InstantiationPlanCompiler(this.#buildPlanCompilerHost()));
415
+ }
416
+ // The behaviour the plan compiler needs from this resolver — lookups, escapes, plan swaps, and the
417
+ // accessor construction path. Built once with the compiler, so each closure is allocated once.
418
+ #buildPlanCompilerHost() {
419
+ return {
420
+ hasActivationHandlers: (binding) => this.#ownerOf(binding).#lifecycle.hasActivationHandlers(binding.token),
421
+ knownPostConstruct: (target) => this.#classes.knownPostConstruct(target),
422
+ needsActiveContainer: (target) => this.#classes.needsActiveContainer(target),
423
+ // A plan runs at the top level, so the lent root stack is free when it is; a plan reached
424
+ // with the root stack held mints its own path, exactly as the interpreted lane would.
425
+ constructWithAccessors: (binding, target, deps) => {
426
+ const stack = this.rootStack.length === 0 ? this.rootStack : [];
427
+ const frame = this.#getResolutionFrame(binding);
428
+ const resolutionSet = enterResolutionPath(stack, frame);
429
+ try {
430
+ return this.#classes.instantiate(target, deps, this.#ambientResolutionFor(stack));
431
+ }
432
+ finally {
433
+ stack.pop();
434
+ resolutionSet?.delete(frame.bindingId);
435
+ }
436
+ },
437
+ getConstructorMetadata: (target) => this.#classes.constructorMetadata(target),
438
+ lookupDependencyEntry: (token) => {
439
+ const entry = this.#lookup.defaultEntry(token);
440
+ return entry === null ? null : { binding: entry.binding };
441
+ },
442
+ // Exactly what #findBinding's single-criterion lane accepts, minus the half that reads a path:
443
+ // a predicate is the compiler's cue to leave the selection to the runtime.
444
+ lookupPathIndependentEntry: (token, options) => {
445
+ const singleCriterion = singleCriterionOnlyOf(options);
446
+ if (singleCriterion === undefined) {
447
+ return null;
448
+ }
449
+ const entry = this.#lookup.taggedEntry(token, singleCriterion);
450
+ if (entry === null || entry.binding.predicate !== undefined || !matchesSlot(entry.binding.slot, options)) {
451
+ return null;
452
+ }
453
+ return { binding: entry.binding };
454
+ },
455
+ getResolutionFrame: (binding) => this.#getResolutionFrame(binding),
456
+ // Identity-guarded: a map cleared and recompiled since no longer holds the plan being replaced.
457
+ replacePlan: (binding, current, next) => {
458
+ const plans = this.#classPlanByBindingId;
459
+ if (plans !== undefined && plans.get(binding.identifier) === current) {
460
+ plans.set(binding.identifier, next);
461
+ }
462
+ },
463
+ replaceAsyncPlan: (binding, current, next) => {
464
+ const plans = this.#asyncPlanByBindingId;
465
+ if (plans !== undefined && plans.get(binding.identifier) === current) {
466
+ plans.set(binding.identifier, next);
467
+ }
468
+ },
469
+ // Dispatches exactly as #resolveDep does, so an escaped dep is indistinguishable
470
+ // from the same dep on a fully interpreted resolve.
471
+ resolveEscaped: (token, options, arity, resolutionStack) => {
472
+ if (arity === "all") {
473
+ return this.resolveAll(token, options, resolutionStack);
474
+ }
475
+ if (arity === "optional") {
476
+ return this.resolveOptional(token, options, resolutionStack);
477
+ }
478
+ if (options === undefined) {
479
+ return this.resolveFromContext(token, resolutionStack);
480
+ }
481
+ return this.resolve(token, options, resolutionStack);
482
+ },
483
+ // Dispatches exactly as #resolveDepAsync does, for the async lane's escapes.
484
+ resolveEscapedAsync: (token, options, arity, resolutionStack) => {
485
+ if (arity === "all") {
486
+ return this.resolveAllAsync(token, options, resolutionStack, UNOWNED_BRANCH);
487
+ }
488
+ if (arity === "optional") {
489
+ return this.resolveOptionalAsync(token, options, resolutionStack, UNOWNED_BRANCH);
490
+ }
491
+ if (options === undefined) {
492
+ return this.resolveAsyncFromContext(token, resolutionStack, UNOWNED_BRANCH);
493
+ }
494
+ return this.resolveAsync(token, options, resolutionStack, UNOWNED_BRANCH);
495
+ },
496
+ };
497
+ }
374
498
  /** The async lane's plan for a statically-visible transient binding, mirroring the sync getter. */
375
499
  #getAsyncInstantiationPlan(binding) {
376
500
  const registryVersion = this.#lookup.chainVersion();
377
501
  const activationVersion = this.#chainActivationVersion();
378
502
  if (registryVersion !== this.#asyncPlanRegistryVersion || activationVersion !== this.#asyncPlanActivationVersion) {
379
- this.#asyncPlanByBindingId.clear();
503
+ if (this.#asyncPlanByBindingId === undefined) {
504
+ this.#asyncPlanByBindingId = new Map();
505
+ }
506
+ else {
507
+ this.#asyncPlanByBindingId.clear();
508
+ }
380
509
  this.#asyncPlanRegistryVersion = registryVersion;
381
510
  this.#asyncPlanActivationVersion = activationVersion;
382
511
  }
383
- const cached = this.#asyncPlanByBindingId.get(binding.id);
512
+ const plans = this.#asyncPlanByBindingId;
513
+ const cached = plans.get(binding.identifier);
384
514
  if (cached !== undefined) {
385
515
  return cached;
386
516
  }
387
- const compiled = this.#planCompiler.compileAsync(binding);
517
+ const compiled = this.#compiler().compileAsync(binding);
388
518
  if (compiled === PLAN_RETRY) {
389
519
  // Lifecycle metadata not discovered yet — the fallback resolve discovers it; retry then.
390
520
  return null;
391
521
  }
392
- this.#asyncPlanByBindingId.set(binding.id, compiled);
522
+ plans.set(binding.identifier, compiled);
393
523
  return compiled;
394
524
  }
395
525
  resolve(token, options, resolutionStack, precomputedCriterion) {
@@ -430,7 +560,7 @@ export class DependencyResolver {
430
560
  return binding.instance;
431
561
  }
432
562
  // An async materialization already in flight must not be raced by a second, sync one.
433
- if (this.#scope.getInflight(binding.id) !== undefined) {
563
+ if (this.#scope.getInflight(binding.identifier) !== undefined) {
434
564
  throw new AsyncResolutionError(resolutionStack[0]?.tokenName ?? tokenName(binding.token), tokenName(binding.token));
435
565
  }
436
566
  if (this.#scope.isClosed) {
@@ -442,7 +572,7 @@ export class DependencyResolver {
442
572
  if (cachedScoped !== SCOPED_MISS) {
443
573
  return cachedScoped;
444
574
  }
445
- if (this.#scope.getInflight(binding.id) !== undefined) {
575
+ if (this.#scope.getInflight(binding.identifier) !== undefined) {
446
576
  throw new AsyncResolutionError(resolutionStack[0]?.tokenName ?? tokenName(binding.token), tokenName(binding.token));
447
577
  }
448
578
  if (this.#scope.isClosed) {
@@ -453,7 +583,7 @@ export class DependencyResolver {
453
583
  const tokenDisplayName = frame.tokenName;
454
584
  const resolutionSet = enterResolutionPath(resolutionStack, frame);
455
585
  try {
456
- const needsActivation = owner.#activation.needsActivation(binding);
586
+ const needsActivation = owner.#activationNeed().needsActivation(binding);
457
587
  if (!needsActivation && scope === "transient" && binding.kind === "dynamic") {
458
588
  const resolutionCtx = this.#acquireSyncResolutionContext(resolutionStack, options);
459
589
  const dynamicResult = binding.factory(resolutionCtx);
@@ -467,7 +597,7 @@ export class DependencyResolver {
467
597
  : undefined;
468
598
  const instance = this.#instantiateSync(binding, resolutionCtx, resolutionStack);
469
599
  this.#mirrorPostConstructFromOwner(binding, owner);
470
- const activated = owner.#activation.refreshAfterFirstInstantiation(binding, needsActivation)
600
+ const activated = owner.#activationNeed().refreshAfterFirstInstantiation(binding, needsActivation)
471
601
  ? owner.#lifecycle.runActivationSync(resolutionCtx, binding, instance, owner.#metadataReader)
472
602
  : instance;
473
603
  if (scope === "singleton") {
@@ -483,8 +613,20 @@ export class DependencyResolver {
483
613
  resolutionSet?.delete(frame.bindingId);
484
614
  }
485
615
  }
486
- /** Path-continuing resolution handed to the ambient slot while an accessor class constructs. */
616
+ /**
617
+ * Path-continuing resolution handed to the ambient slot while an accessor class constructs.
618
+ *
619
+ * @remarks The lent root stack is one array for the resolver's lifetime, so the closure pair over
620
+ * it is built once and reused.
621
+ */
622
+ #rootAmbientResolution;
487
623
  #ambientResolutionFor(resolutionStack) {
624
+ if (resolutionStack === this.rootStack) {
625
+ return (this.#rootAmbientResolution ??= this.#buildAmbientResolution(resolutionStack));
626
+ }
627
+ return this.#buildAmbientResolution(resolutionStack);
628
+ }
629
+ #buildAmbientResolution(resolutionStack) {
488
630
  return {
489
631
  resolve: (token, options) => options === undefined
490
632
  ? this.resolveFromContext(token, resolutionStack)
@@ -572,6 +714,19 @@ export class DependencyResolver {
572
714
  return this.resolve(dep.token, options, resolutionStack, singleCriterionForSlot(dep));
573
715
  }
574
716
  resolveOptional(token, options, resolutionStack, precomputedCriterion) {
717
+ if (options === undefined) {
718
+ // The lane a plain resolve takes: a lone default in this registry is the answer, predicate-free by
719
+ // construction, and a root that keeps no records has nothing else that could hold the token.
720
+ const fastBinding = this.#registry.getFastDefault(token);
721
+ if (fastBinding !== undefined) {
722
+ if (fastBinding.kind !== "alias") {
723
+ return this.#resolveDefaultEntry(fastBinding, this, resolutionStack);
724
+ }
725
+ }
726
+ else if (this.#parent === undefined && !this.#registry.isRecordMapBuilt) {
727
+ return undefined;
728
+ }
729
+ }
575
730
  const entry = this.#findBinding(token, options, resolutionStack, precomputedCriterion === undefined ? singleCriterionOnlyOf(options) : (precomputedCriterion ?? undefined));
576
731
  if (entry === undefined) {
577
732
  return undefined;
@@ -595,6 +750,80 @@ export class DependencyResolver {
595
750
  }
596
751
  return resolved;
597
752
  }
753
+ /**
754
+ * A root-level, options-less collection read through its memo: the value list when it is stable,
755
+ * the candidate list otherwise.
756
+ *
757
+ * @remarks Its own entry rather than a branch in `resolveAll`, so the options lane keeps the exact
758
+ * shape it had; the container routes a top-level read with no options here.
759
+ */
760
+ resolveRootCollection(token) {
761
+ const resolutionStack = this.rootStack;
762
+ const memo = this.#rootCollection(token, resolutionStack);
763
+ if (memo.values !== undefined && memo.activationVersion === this.#chainActivationVersion()) {
764
+ return memo.values;
765
+ }
766
+ const { candidates } = memo;
767
+ const resolved = new Array(candidates.length);
768
+ for (let index = 0; index < candidates.length; index += 1) {
769
+ resolved[index] = this.#resolveCandidateSync(candidates[index], undefined, resolutionStack);
770
+ }
771
+ // The members this read materialised may have made the list stable for the next one.
772
+ this.#settleCollectionValues(memo);
773
+ return resolved;
774
+ }
775
+ /** The async twin of `resolveRootCollection`: a stable value list settles at once, candidates fan out as usual. */
776
+ resolveRootCollectionAsync(token) {
777
+ // The async lane appends to its branch and never unwinds, so it works on a stack of its own.
778
+ const resolutionStack = [];
779
+ const memo = this.#rootCollection(token, resolutionStack);
780
+ if (memo.values !== undefined && memo.activationVersion === this.#chainActivationVersion()) {
781
+ return Promise.resolve(memo.values);
782
+ }
783
+ return Promise.all(memo.candidates.map((candidate) => this.#resolveCandidateAsync(candidate, undefined, resolutionStack, UNOWNED_BRANCH))).then((values) => {
784
+ this.#settleCollectionValues(memo);
785
+ return values;
786
+ });
787
+ }
788
+ /**
789
+ * The memoized candidate list of a root-level, options-less collection, built on its first read.
790
+ *
791
+ * @remarks Sound because a `when()` predicate is pure over its context and the root context is a
792
+ * constant: the list can only change when a registry in the chain does, which is the version the
793
+ * memo is stamped with. The value list is kept too while every member is a hook-free constant
794
+ * and no activation hook exists anywhere in the chain.
795
+ */
796
+ #rootCollection(token, resolutionStack) {
797
+ const memo = this.#lookup.collection(token);
798
+ if (memo !== undefined) {
799
+ return memo;
800
+ }
801
+ const candidates = this.#candidateBindings(token, undefined, resolutionStack);
802
+ const entry = { candidates, values: undefined, activationVersion: -1 };
803
+ this.#settleCollectionValues(entry);
804
+ this.#lookup.rememberCollection(token, entry);
805
+ return entry;
806
+ }
807
+ /**
808
+ * Fills a collection memo's value list once every member is stable: a hook-free constant, or a
809
+ * hook-free singleton whose instance is cached — anything that changes either bumps a registry
810
+ * version the memo is keyed on. A member still to be materialised leaves the list unfilled.
811
+ */
812
+ #settleCollectionValues(entry) {
813
+ if (entry.values !== undefined || this.#chainActivationVersion() !== 0) {
814
+ return;
815
+ }
816
+ const { candidates } = entry;
817
+ for (let index = 0; index < candidates.length; index += 1) {
818
+ if (!isStableCollectionMember(candidates[index])) {
819
+ return;
820
+ }
821
+ }
822
+ // Handed out as is, unfrozen: a frozen array iterates through a slow elements kind, so the
823
+ // contract's read-only return is the guard against a caller writing into the memo.
824
+ entry.values = candidates.map(stableMemberValue);
825
+ entry.activationVersion = 0;
826
+ }
598
827
  /** Every binding in the chain a `resolveAll` request matches, in chain order. */
599
828
  #candidateBindings(token, options, resolutionStack) {
600
829
  if (options !== undefined) {
@@ -648,7 +877,7 @@ export class DependencyResolver {
648
877
  }
649
878
  }
650
879
  else if (this.#scope.isChild) {
651
- const cachedScoped = this.#scope.readScoped(binding.id);
880
+ const cachedScoped = this.#scope.readScoped(binding.identifier);
652
881
  if (cachedScoped !== SCOPED_MISS) {
653
882
  return Promise.resolve(cachedScoped);
654
883
  }
@@ -685,7 +914,7 @@ export class DependencyResolver {
685
914
  return binding.instance;
686
915
  }
687
916
  // In-flight dedup: concurrent callers share the first creation.
688
- const inflight = this.#scope.getInflight(binding.id);
917
+ const inflight = this.#scope.getInflight(binding.identifier);
689
918
  if (inflight !== undefined) {
690
919
  return inflight;
691
920
  }
@@ -699,7 +928,7 @@ export class DependencyResolver {
699
928
  return cachedScoped;
700
929
  }
701
930
  // In-flight dedup, scoped flavor: one instance per scope even under concurrency.
702
- const inflight = this.#scope.getInflight(binding.id);
931
+ const inflight = this.#scope.getInflight(binding.identifier);
703
932
  if (inflight !== undefined) {
704
933
  return inflight;
705
934
  }
@@ -711,7 +940,7 @@ export class DependencyResolver {
711
940
  // This level appends to its own branch and never unwinds.
712
941
  const levelStack = extendResolutionBranch(resolutionStack, branchDepth, frame);
713
942
  const levelDepth = branchDepthOf(levelStack);
714
- const needsActivation = owner.#activation.needsActivation(binding);
943
+ const needsActivation = owner.#activationNeed().needsActivation(binding);
715
944
  if (!needsActivation && scope === "transient" && (binding.kind === "dynamic" || binding.kind === "dynamic-async")) {
716
945
  const resolutionCtx = new AsyncLevelContext(this, levelStack, options);
717
946
  if (binding.kind === "dynamic-async") {
@@ -727,26 +956,26 @@ export class DependencyResolver {
727
956
  // The promise is published before it settles, so concurrent callers dedup onto it.
728
957
  const singletonPromise = this.#instantiateAndActivateAsync(binding, resolutionCtx, levelStack, levelDepth, needsActivation, owner).then((activated) => {
729
958
  this.#scope.setSingleton(binding, activated);
730
- this.#scope.clearInflight(binding.id);
959
+ this.#scope.clearInflight(binding.identifier);
731
960
  return activated;
732
961
  }, (error) => {
733
- this.#scope.clearInflight(binding.id);
962
+ this.#scope.clearInflight(binding.identifier);
734
963
  throw error;
735
964
  });
736
- this.#scope.setInflight(binding.id, singletonPromise);
965
+ this.#scope.setInflight(binding.identifier, singletonPromise);
737
966
  return await singletonPromise;
738
967
  }
739
968
  if (scope === "scoped") {
740
969
  // Published before it settles, like the singleton lane: concurrent callers share one creation.
741
970
  const scopedPromise = this.#instantiateAndActivateAsync(binding, resolutionCtx, levelStack, levelDepth, needsActivation, owner).then((activated) => {
742
971
  this.#scope.setScoped(binding, activated);
743
- this.#scope.clearInflight(binding.id);
972
+ this.#scope.clearInflight(binding.identifier);
744
973
  return activated;
745
974
  }, (error) => {
746
- this.#scope.clearInflight(binding.id);
975
+ this.#scope.clearInflight(binding.identifier);
747
976
  throw error;
748
977
  });
749
- this.#scope.setInflight(binding.id, scopedPromise);
978
+ this.#scope.setInflight(binding.identifier, scopedPromise);
750
979
  return await scopedPromise;
751
980
  }
752
981
  return await this.#instantiateAndActivateAsync(binding, resolutionCtx, levelStack, levelDepth, needsActivation, owner);
@@ -766,7 +995,7 @@ export class DependencyResolver {
766
995
  async #instantiateAndActivateAsync(binding, ctx, resolutionStack, branchDepth, needsActivation, owner) {
767
996
  const instance = await this.#instantiateAsync(binding, ctx, resolutionStack, branchDepth);
768
997
  this.#mirrorPostConstructFromOwner(binding, owner);
769
- if (!owner.#activation.refreshAfterFirstInstantiation(binding, needsActivation)) {
998
+ if (!owner.#activationNeed().refreshAfterFirstInstantiation(binding, needsActivation)) {
770
999
  return instance;
771
1000
  }
772
1001
  return owner.#lifecycle.runActivation(ctx, binding, instance, owner.#metadataReader);
@@ -916,12 +1145,12 @@ export class DependencyResolver {
916
1145
  /** A constant with no activation anywhere resolves to its value with no pipeline at all. */
917
1146
  #isPlainConstant(binding) {
918
1147
  return (binding.kind === "constant" &&
919
- binding.onActivation === undefined &&
1148
+ binding.activationHook === undefined &&
920
1149
  (this.#lifecycle.activationVersion === 0 || !this.#lifecycle.hasActivationHandlers(binding.token)));
921
1150
  }
922
1151
  /** Whether either an own hook or a container-level hook would run for this binding. */
923
1152
  #hasAnyActivation(binding) {
924
- if (binding.onActivation !== undefined) {
1153
+ if (binding.activationHook !== undefined) {
925
1154
  return true;
926
1155
  }
927
1156
  return this.#lifecycle.activationVersion !== 0 && this.#lifecycle.hasActivationHandlers(binding.token);
@@ -936,7 +1165,7 @@ export class DependencyResolver {
936
1165
  if (!this.#scope.isChild) {
937
1166
  throw new MissingScopeContextError(tokenName(binding.token));
938
1167
  }
939
- return this.#scope.readScoped(binding.id);
1168
+ return this.#scope.readScoped(binding.identifier);
940
1169
  }
941
1170
  // The shared root context answers every top-level request; building one is the rarer half and
942
1171
  // lives outside, so what a selection inlines is the test and not the literal.
@@ -1137,7 +1366,7 @@ export class DependencyResolver {
1137
1366
  #resolveCandidateSync(binding, options, resolutionStack) {
1138
1367
  // Fan-outs are dominated by constants: with no activation hook anywhere in the chain, a
1139
1368
  // hook-free constant is plain no matter which container owns it — skip the owner probe.
1140
- if (binding.kind === "constant" && binding.onActivation === undefined && this.#chainActivationVersion() === 0) {
1369
+ if (binding.kind === "constant" && binding.activationHook === undefined && this.#chainActivationVersion() === 0) {
1141
1370
  return binding.value;
1142
1371
  }
1143
1372
  const owner = this.#ownerOf(binding);
@@ -1157,7 +1386,7 @@ export class DependencyResolver {
1157
1386
  return this.#resolveBinding(binding, options, resolutionStack, owner);
1158
1387
  }
1159
1388
  #resolveCandidateAsync(binding, options, resolutionStack, branchDepth) {
1160
- if (binding.kind === "constant" && binding.onActivation === undefined && this.#chainActivationVersion() === 0) {
1389
+ if (binding.kind === "constant" && binding.activationHook === undefined && this.#chainActivationVersion() === 0) {
1161
1390
  return Promise.resolve(binding.value);
1162
1391
  }
1163
1392
  const owner = this.#ownerOf(binding);
@@ -1173,16 +1402,25 @@ export class DependencyResolver {
1173
1402
  }
1174
1403
  return owner.#resolveBindingAsync(binding, options, resolutionStack, branchDepth, owner);
1175
1404
  }
1405
+ // The dominant collection member — a transient factory with no activation, asked with no
1406
+ // options — takes the non-async lane a single resolve takes, so a fan-out costs one factory
1407
+ // promise per member rather than a state machine on top of each.
1408
+ if (options === undefined &&
1409
+ binding.scope === "transient" &&
1410
+ (binding.kind === "dynamic" || binding.kind === "dynamic-async") &&
1411
+ !owner.#hasAnyActivation(binding)) {
1412
+ return this.#resolveTransientDynamicAsyncFromContext(binding, resolutionStack, branchDepth);
1413
+ }
1176
1414
  return this.#resolveBindingAsync(binding, options, resolutionStack, branchDepth, owner);
1177
1415
  }
1178
1416
  /** The resolver whose registry holds `binding` — `this` (the common case) when it is own. */
1179
1417
  #ownerOf(binding) {
1180
1418
  // A root resolver can only hold its own bindings, so the per-candidate id probe is chain-only.
1181
- if (this.#parent === undefined || this.#registry.getById(binding.id) !== undefined) {
1419
+ if (this.#parent === undefined || this.#registry.getById(binding.identifier) !== undefined) {
1182
1420
  return this;
1183
1421
  }
1184
1422
  for (let current = this.#parent; current !== undefined; current = current.#parent) {
1185
- if (current.#registry.getById(binding.id) !== undefined) {
1423
+ if (current.#registry.getById(binding.identifier) !== undefined) {
1186
1424
  return current;
1187
1425
  }
1188
1426
  }
@@ -1196,7 +1434,7 @@ export class DependencyResolver {
1196
1434
  if (existing !== undefined) {
1197
1435
  return existing;
1198
1436
  }
1199
- const frame = buildResolutionFrame(tokenName(binding.token), binding.scope, binding.id, binding.kind, binding.slot);
1437
+ const frame = buildResolutionFrame(tokenName(binding.token), binding.scope, binding.identifier, binding.kind, binding.slot);
1200
1438
  binding.frame = frame;
1201
1439
  return frame;
1202
1440
  }
@@ -1234,6 +1472,22 @@ export class DependencyResolver {
1234
1472
  return created;
1235
1473
  }
1236
1474
  }
1475
+ /**
1476
+ * Whether a binding answers a collection read with a fixed value: a hook-free constant, or a
1477
+ * singleton whose instance is already cached.
1478
+ *
1479
+ * @remarks A cached singleton reads like a constant until a registry change evicts it, which also
1480
+ * drops the memo.
1481
+ */
1482
+ function isStableCollectionMember(binding) {
1483
+ if (binding.kind === "alias" || binding.activationHook !== undefined) {
1484
+ return false;
1485
+ }
1486
+ return binding.kind === "constant" || (binding.scope === "singleton" && binding.instance !== NO_INSTANCE);
1487
+ }
1488
+ function stableMemberValue(binding) {
1489
+ return binding.kind === "constant" ? binding.value : binding.instance;
1490
+ }
1237
1491
  function anyPredicate(bindings) {
1238
1492
  for (let index = 0; index < bindings.length; index += 1) {
1239
1493
  if (bindings[index].predicate !== undefined) {