@codefast/di 0.6.0 → 0.6.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 (33) hide show
  1. package/CHANGELOG.md +97 -0
  2. package/dist/container/binding-builders.d.ts.map +1 -1
  3. package/dist/container/binding-builders.js +7 -1
  4. package/dist/container/binding-builders.js.map +1 -1
  5. package/dist/container/container.d.ts.map +1 -1
  6. package/dist/container/container.js +7 -5
  7. package/dist/container/container.js.map +1 -1
  8. package/dist/core/constraint-requirement.d.ts +15 -0
  9. package/dist/core/constraint-requirement.d.ts.map +1 -1
  10. package/dist/core/constraint-requirement.js +35 -1
  11. package/dist/core/constraint-requirement.js.map +1 -1
  12. package/dist/core/tag.d.ts.map +1 -1
  13. package/dist/core/tag.js +12 -0
  14. package/dist/core/tag.js.map +1 -1
  15. package/dist/resolution/cache/binding-lookup-cache.d.ts +3 -0
  16. package/dist/resolution/cache/binding-lookup-cache.d.ts.map +1 -1
  17. package/dist/resolution/cache/binding-lookup-cache.js +55 -0
  18. package/dist/resolution/cache/binding-lookup-cache.js.map +1 -1
  19. package/dist/resolution/plan/instantiation-plan.d.ts +7 -0
  20. package/dist/resolution/plan/instantiation-plan.d.ts.map +1 -1
  21. package/dist/resolution/plan/instantiation-plan.js +32 -3
  22. package/dist/resolution/plan/instantiation-plan.js.map +1 -1
  23. package/dist/resolution/resolver.d.ts.map +1 -1
  24. package/dist/resolution/resolver.js +29 -0
  25. package/dist/resolution/resolver.js.map +1 -1
  26. package/package.json +2 -2
  27. package/src/container/binding-builders.ts +8 -1
  28. package/src/container/container.ts +7 -5
  29. package/src/core/constraint-requirement.ts +44 -1
  30. package/src/core/tag.ts +14 -0
  31. package/src/resolution/cache/binding-lookup-cache.ts +57 -0
  32. package/src/resolution/plan/instantiation-plan.ts +39 -3
  33. package/src/resolution/resolver.ts +28 -0
@@ -6,6 +6,7 @@
6
6
  */
7
7
  import type { Binding } from "#/core/binding";
8
8
  import type { BindingRegistry } from "#/core/registry";
9
+ import type { BindingTag } from "#/core/tag";
9
10
  import type { Token } from "#/core/token";
10
11
  import type { Constructor } from "#/core/types";
11
12
 
@@ -34,6 +35,8 @@ export const ALIAS_HOP_LIMIT = 32;
34
35
  */
35
36
  const newNameToEntryMap = <Owner>(): Map<string, DefaultLookupEntry<Owner> | null> => new Map();
36
37
 
38
+ const newTagToEntryMap = <Owner>(): Map<BindingTag, DefaultLookupEntry<Owner> | null> => new Map();
39
+
37
40
  /**
38
41
  * @since 0.5.0-canary.9
39
42
  */
@@ -47,6 +50,14 @@ export class BindingLookupCache<Owner> {
47
50
  #lastEntry: DefaultLookupEntry<Owner> | null = null;
48
51
  readonly #byTokenAndName = new Map<Token<unknown> | Constructor, Map<string, DefaultLookupEntry<Owner> | null>>();
49
52
  #namedVersion = -1;
53
+ readonly #byTokenAndTag = new Map<Token<unknown> | Constructor, Map<BindingTag, DefaultLookupEntry<Owner> | null>>();
54
+ #taggedVersion = -1;
55
+ // One entry in front of the tag map, and the map is not written until a second distinct request
56
+ // shape appears: a per-request child usually asks one (token, tag) once, and the inner-map
57
+ // allocation was that shape's whole regression when this memo landed.
58
+ #lastTagToken: Token<unknown> | Constructor | undefined;
59
+ #lastTag: BindingTag | undefined;
60
+ #lastTaggedEntry: DefaultLookupEntry<Owner> | null = null;
50
61
 
51
62
  readonly #registry: BindingRegistry;
52
63
  readonly #owner: Owner;
@@ -105,6 +116,37 @@ export class BindingLookupCache<Owner> {
105
116
  return entry;
106
117
  }
107
118
 
119
+ /** `null` when the tag's shape needs the full selection path. */
120
+ taggedEntry(token: Token<unknown> | Constructor, tag: BindingTag): DefaultLookupEntry<Owner> | null {
121
+ const version = this.chainVersion();
122
+ if (version !== this.#taggedVersion) {
123
+ this.#byTokenAndTag.clear();
124
+ this.#taggedVersion = version;
125
+ this.#lastTagToken = undefined;
126
+ this.#lastTag = undefined;
127
+ } else if (token === this.#lastTagToken && tag === this.#lastTag) {
128
+ return this.#lastTaggedEntry;
129
+ }
130
+ let entry: DefaultLookupEntry<Owner> | null | undefined;
131
+ if (this.#lastTagToken === undefined) {
132
+ // First shape this cache generation sees: answer from the walk and defer the map entirely.
133
+ entry = this.#findTaggedInChain(token, tag);
134
+ } else {
135
+ // Keyed by the criterion object itself: criteria are interned, so identity is the slot
136
+ // contract's own `Object.is` — the same exactness the registry's tagged index relies on.
137
+ const byTag = this.#byTokenAndTag.getOrInsertComputed(token, newTagToEntryMap);
138
+ entry = byTag.get(tag);
139
+ if (entry === undefined) {
140
+ entry = this.#findTaggedInChain(token, tag);
141
+ byTag.set(tag, entry);
142
+ }
143
+ }
144
+ this.#lastTagToken = token;
145
+ this.#lastTag = tag;
146
+ this.#lastTaggedEntry = entry;
147
+ return entry;
148
+ }
149
+
108
150
  #foldAliases(token: Token<unknown> | Constructor): DefaultLookupEntry<Owner> | null {
109
151
  let current = token;
110
152
  for (let hop = 0; hop < ALIAS_HOP_LIMIT; hop += 1) {
@@ -146,4 +188,19 @@ export class BindingLookupCache<Owner> {
146
188
  }
147
189
  return this.#parent === undefined ? null : this.#parent.#findNamedInChain(token, name);
148
190
  }
191
+
192
+ #findTaggedInChain(token: Token<unknown> | Constructor, tag: BindingTag): DefaultLookupEntry<Owner> | null {
193
+ const tagged = this.#registry.getSimpleTagged(token, tag);
194
+ if (tagged !== undefined) {
195
+ // Predicates need a live context; aliases carry options through the full path.
196
+ if (tagged.predicate !== undefined || tagged.kind === "alias") {
197
+ return null;
198
+ }
199
+ return { binding: tagged, owner: this.#owner };
200
+ }
201
+ if (this.#registry.has(token)) {
202
+ return null;
203
+ }
204
+ return this.#parent === undefined ? null : this.#parent.#findTaggedInChain(token, tag);
205
+ }
149
206
  }
@@ -124,6 +124,16 @@ export interface InstantiationPlanHost {
124
124
  token: Token<unknown> | Constructor,
125
125
  options: ResolveOptions & { name: string },
126
126
  ): InstantiationPlanDependencyEntry | null;
127
+ /**
128
+ * The named lookup's single-tag twin, or `null` under the same rule.
129
+ *
130
+ * @remarks Optional so a host predating it stays a valid host — a compiler given none simply
131
+ * escapes the dependency, which is exactly the pre-settlement behavior.
132
+ */
133
+ lookupPathIndependentTaggedEntry?(
134
+ token: Token<unknown> | Constructor,
135
+ options: ResolveOptions,
136
+ ): InstantiationPlanDependencyEntry | null;
127
137
  /** The frame the interpreted path pushes for this binding, so escapes can replay it. */
128
138
  getResolutionFrame(binding: Binding): ResolutionFrame;
129
139
  /** Runtime resolve for an escaped dependency, seeded with the ancestor frames above it. */
@@ -169,8 +179,10 @@ export class InstantiationPlanCompiler {
169
179
  /**
170
180
  * Re-entry into the runtime resolver for a dependency the plan can't see through.
171
181
  *
172
- * The ancestors are fixed at compile time, so the seeds are built once; each call copies
173
- * them because the resolver pushes and pops on the arrays it is given.
182
+ * The ancestors are fixed at compile time, so the seed is built once and lent per call — the
183
+ * root-stack rule one level down: every sync lane pops what it pushes, so the owned array still
184
+ * holds exactly the seed when a call returns. A reentrant call finds it claimed and mints its
185
+ * own copy; a return that did not restore the length hands nothing back, so the next call mints.
174
186
  */
175
187
  #compileEscapeThunk(
176
188
  token: Token<unknown> | Constructor,
@@ -180,7 +192,19 @@ export class InstantiationPlanCompiler {
180
192
  ): () => unknown {
181
193
  const host = this.#host;
182
194
  const frames = ancestors.map((ancestor) => host.getResolutionFrame(ancestor));
183
- return () => host.resolveEscaped(token, options, arity, [...frames]);
195
+ const depth = frames.length;
196
+ let owned: Array<ResolutionFrame> | undefined = [...frames];
197
+ return () => {
198
+ const stack = owned ?? [...frames];
199
+ owned = undefined;
200
+ try {
201
+ return host.resolveEscaped(token, options, arity, stack);
202
+ } finally {
203
+ if (stack.length === depth) {
204
+ owned = stack;
205
+ }
206
+ }
207
+ };
184
208
  }
185
209
 
186
210
  // A resolved binding declares its deps as explicit descriptors — same rules as
@@ -246,6 +270,12 @@ export class InstantiationPlanCompiler {
246
270
  if (named !== null) {
247
271
  return this.#compileDepThunk(named, compileStack, depth, ancestors, options);
248
272
  }
273
+ } else {
274
+ // The host owns the whole single-tag decision, including whether the options qualify.
275
+ const tagged = this.#host.lookupPathIndependentTaggedEntry?.(token, options);
276
+ if (tagged !== null && tagged !== undefined) {
277
+ return this.#compileDepThunk(tagged, compileStack, depth, ancestors, options);
278
+ }
249
279
  }
250
280
  return this.#compileEscapeThunk(token, ancestors, "single", options);
251
281
  }
@@ -514,6 +544,12 @@ export class InstantiationPlanCompiler {
514
544
  if (named !== null) {
515
545
  return this.#compileAsyncDepThunk(named, compileStack, depth, ancestors, options);
516
546
  }
547
+ } else {
548
+ // The host owns the whole single-tag decision, including whether the options qualify.
549
+ const tagged = this.#host.lookupPathIndependentTaggedEntry?.(token, options);
550
+ if (tagged !== null && tagged !== undefined) {
551
+ return this.#compileAsyncDepThunk(tagged, compileStack, depth, ancestors, options);
552
+ }
517
553
  }
518
554
  return this.#compileAsyncEscapeThunk(token, ancestors, "single", options);
519
555
  }
@@ -436,6 +436,18 @@ export class DependencyResolver implements ResolverCallbacks {
436
436
  }
437
437
  return { binding: entry.binding };
438
438
  },
439
+ // The named rule verbatim, on the single-tag lane's memo.
440
+ lookupPathIndependentTaggedEntry: (token, options) => {
441
+ const singleTag = singleTagOnlyOf(options);
442
+ if (singleTag === undefined) {
443
+ return null;
444
+ }
445
+ const entry = this.#lookup.taggedEntry(token, singleTag);
446
+ if (entry === null || entry.binding.predicate !== undefined || !matchesSlot(entry.binding.slot, options)) {
447
+ return null;
448
+ }
449
+ return { binding: entry.binding };
450
+ },
439
451
  getResolutionFrame: (binding) => this.#getResolutionFrame(binding),
440
452
  // Dispatches exactly as #resolveDep does, so an escaped dep is indistinguishable
441
453
  // from the same dep on a fully interpreted resolve.
@@ -509,6 +521,22 @@ export class DependencyResolver implements ResolverCallbacks {
509
521
  }
510
522
  // Everything else keeps the full path (context, activation, guards).
511
523
  }
524
+ } else if (options !== undefined) {
525
+ // Single-tag fast lane: the named lane's tagged twin, memoizing the chain walk.
526
+ const singleTag = singleTagOnlyOf(options);
527
+ if (singleTag !== undefined) {
528
+ const taggedEntry = this.#lookup.taggedEntry(token, singleTag);
529
+ if (taggedEntry !== null) {
530
+ const taggedBinding = taggedEntry.binding;
531
+ if (taggedEntry.owner.#isPlainConstant(taggedBinding)) {
532
+ return taggedBinding.value as Value;
533
+ }
534
+ if (taggedBinding.scope === "singleton" && taggedBinding.instance !== NO_INSTANCE) {
535
+ return taggedBinding.instance as Value;
536
+ }
537
+ // Everything else keeps the full path (context, activation, guards).
538
+ }
539
+ }
512
540
  }
513
541
 
514
542
  const { binding, owner } = this.#requireBinding(token, options, resolutionStack);