@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.
- package/CHANGELOG.md +97 -0
- package/dist/container/binding-builders.d.ts.map +1 -1
- package/dist/container/binding-builders.js +7 -1
- package/dist/container/binding-builders.js.map +1 -1
- package/dist/container/container.d.ts.map +1 -1
- package/dist/container/container.js +7 -5
- package/dist/container/container.js.map +1 -1
- package/dist/core/constraint-requirement.d.ts +15 -0
- package/dist/core/constraint-requirement.d.ts.map +1 -1
- package/dist/core/constraint-requirement.js +35 -1
- package/dist/core/constraint-requirement.js.map +1 -1
- package/dist/core/tag.d.ts.map +1 -1
- package/dist/core/tag.js +12 -0
- package/dist/core/tag.js.map +1 -1
- package/dist/resolution/cache/binding-lookup-cache.d.ts +3 -0
- package/dist/resolution/cache/binding-lookup-cache.d.ts.map +1 -1
- package/dist/resolution/cache/binding-lookup-cache.js +55 -0
- package/dist/resolution/cache/binding-lookup-cache.js.map +1 -1
- package/dist/resolution/plan/instantiation-plan.d.ts +7 -0
- package/dist/resolution/plan/instantiation-plan.d.ts.map +1 -1
- package/dist/resolution/plan/instantiation-plan.js +32 -3
- package/dist/resolution/plan/instantiation-plan.js.map +1 -1
- package/dist/resolution/resolver.d.ts.map +1 -1
- package/dist/resolution/resolver.js +29 -0
- package/dist/resolution/resolver.js.map +1 -1
- package/package.json +2 -2
- package/src/container/binding-builders.ts +8 -1
- package/src/container/container.ts +7 -5
- package/src/core/constraint-requirement.ts +44 -1
- package/src/core/tag.ts +14 -0
- package/src/resolution/cache/binding-lookup-cache.ts +57 -0
- package/src/resolution/plan/instantiation-plan.ts +39 -3
- 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
|
|
173
|
-
*
|
|
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
|
-
|
|
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);
|