@codefast/di 0.7.1 → 0.8.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.
- package/CHANGELOG.md +27 -0
- package/dist/container/binding-builders.d.ts.map +1 -1
- package/dist/container/binding-builders.js +4 -5
- package/dist/container/binding-builders.js.map +1 -1
- package/dist/container/container.d.ts.map +1 -1
- package/dist/container/container.js +3 -4
- package/dist/container/container.js.map +1 -1
- package/dist/core/binding.d.ts +15 -2
- package/dist/core/binding.d.ts.map +1 -1
- package/dist/core/binding.js +29 -4
- package/dist/core/binding.js.map +1 -1
- package/dist/core/registry.d.ts +0 -3
- package/dist/core/registry.d.ts.map +1 -1
- package/dist/core/registry.js +14 -61
- package/dist/core/registry.js.map +1 -1
- package/dist/core/tag.d.ts +26 -0
- package/dist/core/tag.d.ts.map +1 -1
- package/dist/core/tag.js +54 -11
- package/dist/core/tag.js.map +1 -1
- package/dist/errors/errors.d.ts.map +1 -1
- package/dist/errors/errors.js +28 -3
- package/dist/errors/errors.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/injection/resolve-options.d.ts +17 -12
- package/dist/injection/resolve-options.d.ts.map +1 -1
- package/dist/injection/resolve-options.js +69 -15
- package/dist/injection/resolve-options.js.map +1 -1
- package/dist/introspection/dependency-graph.d.ts.map +1 -1
- package/dist/introspection/dependency-graph.js +22 -9
- package/dist/introspection/dependency-graph.js.map +1 -1
- package/dist/resolution/cache/binding-lookup-cache.d.ts +1 -3
- package/dist/resolution/cache/binding-lookup-cache.d.ts.map +1 -1
- package/dist/resolution/cache/binding-lookup-cache.js +1 -38
- package/dist/resolution/cache/binding-lookup-cache.js.map +1 -1
- package/dist/resolution/plan/instantiation-plan.d.ts +4 -12
- package/dist/resolution/plan/instantiation-plan.d.ts.map +1 -1
- package/dist/resolution/plan/instantiation-plan.js +9 -27
- package/dist/resolution/plan/instantiation-plan.js.map +1 -1
- package/dist/resolution/resolver.d.ts +12 -5
- package/dist/resolution/resolver.d.ts.map +1 -1
- package/dist/resolution/resolver.js +106 -141
- package/dist/resolution/resolver.js.map +1 -1
- package/dist/resolution/select/binding-select.d.ts +4 -5
- package/dist/resolution/select/binding-select.d.ts.map +1 -1
- package/dist/resolution/select/binding-select.js +18 -20
- package/dist/resolution/select/binding-select.js.map +1 -1
- package/package.json +2 -2
- package/src/container/binding-builders.ts +10 -5
- package/src/container/container.ts +3 -4
- package/src/core/binding.ts +33 -5
- package/src/core/registry.ts +14 -66
- package/src/core/tag.ts +71 -17
- package/src/errors/errors.ts +28 -5
- package/src/index.ts +1 -1
- package/src/injection/resolve-options.ts +75 -18
- package/src/introspection/dependency-graph.ts +23 -9
- package/src/resolution/cache/binding-lookup-cache.ts +1 -41
- package/src/resolution/plan/instantiation-plan.ts +13 -38
- package/src/resolution/resolver.ts +120 -141
- package/src/resolution/select/binding-select.ts +18 -21
package/src/core/registry.ts
CHANGED
|
@@ -17,15 +17,13 @@ export class BindingRegistry {
|
|
|
17
17
|
readonly #bindings = new Map<DependencyKey, Array<Binding>>();
|
|
18
18
|
// Fast lookup by binding ID
|
|
19
19
|
readonly #byId = new Map<BindingIdentifier, Binding>();
|
|
20
|
-
// Fast lookup for slot { name, tags: [] } — unallocated until a named binding is registered.
|
|
21
|
-
#simpleNamed: Map<DependencyKey, Map<string, Binding>> | undefined;
|
|
22
20
|
// Fast path for one default slot binding with no predicate
|
|
23
21
|
readonly #fastDefault = new Map<DependencyKey, Binding>();
|
|
24
|
-
// Fast lookup for a slot carrying exactly one
|
|
25
|
-
// the
|
|
22
|
+
// Fast lookup for a slot carrying exactly one criterion (a lone name folds here too) — keyed by
|
|
23
|
+
// the interned criterion itself, so the pair is one hash. Unallocated until such a slot lands.
|
|
26
24
|
#simpleTagged: Map<DependencyKey, Map<BindingTag, Binding>> | undefined;
|
|
27
|
-
//
|
|
28
|
-
//
|
|
25
|
+
// Slots with two or more criteria, bucketed by their FIRST criterion. A matching slot's every
|
|
26
|
+
// criterion is in the request, so walking the request's buckets finds each candidate exactly once.
|
|
29
27
|
#multiTagged: Map<DependencyKey, Map<BindingTag, Array<Binding>>> | undefined;
|
|
30
28
|
|
|
31
29
|
// Set on the first constant registered and never cleared. Teardown only needs the negative answer
|
|
@@ -79,7 +77,6 @@ export class BindingRegistry {
|
|
|
79
77
|
if (existingIndex !== -1) {
|
|
80
78
|
displacedBinding = bindingsForToken[existingIndex]!;
|
|
81
79
|
this.#byId.delete(displacedBinding.id);
|
|
82
|
-
this.#deindexSimpleNamedBinding(key, displacedBinding);
|
|
83
80
|
this.#deindexSimpleTaggedBinding(key, displacedBinding);
|
|
84
81
|
this.#deindexMultiTaggedBinding(key, displacedBinding);
|
|
85
82
|
}
|
|
@@ -92,7 +89,6 @@ export class BindingRegistry {
|
|
|
92
89
|
|
|
93
90
|
this.#bindings.set(key, nextBindings);
|
|
94
91
|
this.#byId.set(binding.id, binding);
|
|
95
|
-
this.#indexSimpleNamedBinding(key, binding);
|
|
96
92
|
this.#indexSimpleTaggedBinding(key, binding);
|
|
97
93
|
this.#indexMultiTaggedBinding(key, binding);
|
|
98
94
|
this.#refreshFastDefaultForToken(key);
|
|
@@ -105,7 +101,6 @@ export class BindingRegistry {
|
|
|
105
101
|
const key: DependencyKey = token;
|
|
106
102
|
const bindingsForToken = this.#bindings.get(key) ?? [];
|
|
107
103
|
this.#bindings.delete(key);
|
|
108
|
-
this.#simpleNamed?.delete(key);
|
|
109
104
|
this.#simpleTagged?.delete(key);
|
|
110
105
|
this.#multiTagged?.delete(key);
|
|
111
106
|
this.#fastDefault.delete(key);
|
|
@@ -129,12 +124,10 @@ export class BindingRegistry {
|
|
|
129
124
|
const bindingIndex = bindingsForToken.findIndex((candidate) => candidate.id === id);
|
|
130
125
|
// Copy-on-write, like `add`: a walk holding the current array must not lose its place.
|
|
131
126
|
const remaining = bindingIndex === -1 ? bindingsForToken : bindingsForToken.toSpliced(bindingIndex, 1);
|
|
132
|
-
this.#deindexSimpleNamedBinding(key, binding);
|
|
133
127
|
this.#deindexSimpleTaggedBinding(key, binding);
|
|
134
128
|
this.#deindexMultiTaggedBinding(key, binding);
|
|
135
129
|
if (remaining.length === 0) {
|
|
136
130
|
this.#bindings.delete(key);
|
|
137
|
-
this.#simpleNamed?.delete(key);
|
|
138
131
|
this.#simpleTagged?.delete(key);
|
|
139
132
|
this.#multiTagged?.delete(key);
|
|
140
133
|
this.#fastDefault.delete(key);
|
|
@@ -178,7 +171,6 @@ export class BindingRegistry {
|
|
|
178
171
|
const all = this.allBindings();
|
|
179
172
|
this.#bindings.clear();
|
|
180
173
|
this.#byId.clear();
|
|
181
|
-
this.#simpleNamed?.clear();
|
|
182
174
|
this.#simpleTagged?.clear();
|
|
183
175
|
this.#multiTagged?.clear();
|
|
184
176
|
this.#fastDefault.clear();
|
|
@@ -199,10 +191,6 @@ export class BindingRegistry {
|
|
|
199
191
|
);
|
|
200
192
|
}
|
|
201
193
|
|
|
202
|
-
getSimpleNamed(token: Token<unknown> | Constructor, name: string): Binding | undefined {
|
|
203
|
-
return this.#simpleNamed?.get(token)?.get(name);
|
|
204
|
-
}
|
|
205
|
-
|
|
206
194
|
/**
|
|
207
195
|
* The binding indexed under one criterion.
|
|
208
196
|
*
|
|
@@ -287,33 +275,6 @@ export class BindingRegistry {
|
|
|
287
275
|
}
|
|
288
276
|
}
|
|
289
277
|
|
|
290
|
-
#indexSimpleNamedBinding(tokenKey: DependencyKey, binding: Binding): void {
|
|
291
|
-
const name = simpleNameOf(binding);
|
|
292
|
-
if (name === undefined) {
|
|
293
|
-
return;
|
|
294
|
-
}
|
|
295
|
-
this.#simpleNamed ??= new Map();
|
|
296
|
-
const bindingsByName = getOrInsert(this.#simpleNamed, tokenKey, new Map<string, Binding>());
|
|
297
|
-
bindingsByName.set(name, binding);
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
#deindexSimpleNamedBinding(tokenKey: DependencyKey, binding: Binding): void {
|
|
301
|
-
const name = simpleNameOf(binding);
|
|
302
|
-
if (name === undefined) {
|
|
303
|
-
return;
|
|
304
|
-
}
|
|
305
|
-
const bindingsByName = this.#simpleNamed?.get(tokenKey);
|
|
306
|
-
if (bindingsByName === undefined) {
|
|
307
|
-
return;
|
|
308
|
-
}
|
|
309
|
-
if (bindingsByName.get(name)?.id === binding.id) {
|
|
310
|
-
bindingsByName.delete(name);
|
|
311
|
-
if (bindingsByName.size === 0) {
|
|
312
|
-
this.#simpleNamed!.delete(tokenKey);
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
|
|
317
278
|
#refreshFastDefaultForToken(tokenKey: DependencyKey): void {
|
|
318
279
|
const bindingsForToken = this.#bindings.get(tokenKey);
|
|
319
280
|
const onlyBinding = bindingsForToken?.length === 1 ? bindingsForToken[0]! : undefined;
|
|
@@ -324,48 +285,35 @@ export class BindingRegistry {
|
|
|
324
285
|
this.#fastDefault.delete(tokenKey);
|
|
325
286
|
}
|
|
326
287
|
|
|
327
|
-
/** Whether the deferred named-slot index has had to be built. */
|
|
328
|
-
get isNamedIndexBuilt(): boolean {
|
|
329
|
-
return this.#simpleNamed !== undefined;
|
|
330
|
-
}
|
|
331
|
-
|
|
332
288
|
/** Whether the deferred tagged-slot index has had to be built. */
|
|
333
289
|
get isTaggedIndexBuilt(): boolean {
|
|
334
290
|
return this.#simpleTagged !== undefined;
|
|
335
291
|
}
|
|
336
292
|
}
|
|
337
293
|
|
|
338
|
-
/** The name a binding is indexed under, or `undefined` when its slot is more than a plain name. */
|
|
339
|
-
function simpleNameOf(binding: Binding): string | undefined {
|
|
340
|
-
const { name, tags } = binding.slot;
|
|
341
|
-
return name !== undefined && tags.length === 0 ? name : undefined;
|
|
342
|
-
}
|
|
343
|
-
|
|
344
294
|
/**
|
|
345
|
-
* The
|
|
295
|
+
* The criterion a binding is indexed under, or `undefined` when its slot carries more than one.
|
|
346
296
|
*
|
|
347
|
-
* @remarks Carries predicate-bearing bindings too
|
|
348
|
-
*
|
|
297
|
+
* @remarks Carries predicate-bearing bindings too: every lane that reads this index already
|
|
298
|
+
* re-checks what it finds, so an indexed hit was never unconditional.
|
|
349
299
|
*/
|
|
350
300
|
function simpleTagOf(binding: Binding): BindingTag | undefined {
|
|
351
|
-
const {
|
|
352
|
-
return
|
|
301
|
+
const { tags } = binding.slot;
|
|
302
|
+
return tags.length === 1 ? tags[0] : undefined;
|
|
353
303
|
}
|
|
354
304
|
|
|
355
|
-
/** The first criterion a multi-
|
|
305
|
+
/** The first criterion a multi-criterion slot is bucketed under, or `undefined` for any other shape. */
|
|
356
306
|
function multiTagFirstOf(binding: Binding): BindingTag | undefined {
|
|
357
|
-
const {
|
|
358
|
-
return
|
|
307
|
+
const { tags } = binding.slot;
|
|
308
|
+
return tags.length >= 2 ? tags[0] : undefined;
|
|
359
309
|
}
|
|
360
310
|
|
|
361
311
|
/** A binding nothing has to be matched against: the default slot, no predicate. */
|
|
362
312
|
function isDefaultSlotBinding(binding: Binding): boolean {
|
|
363
|
-
|
|
364
|
-
return name === undefined && tags.length === 0 && binding.predicate === undefined;
|
|
313
|
+
return binding.slot.tags.length === 0 && binding.predicate === undefined;
|
|
365
314
|
}
|
|
366
315
|
|
|
367
316
|
/** A predicate with no slot constraint: last-wins does not apply to it. */
|
|
368
317
|
function isPurePredicateBinding(binding: Binding): boolean {
|
|
369
|
-
|
|
370
|
-
return binding.predicate !== undefined && name === undefined && tags.length === 0;
|
|
318
|
+
return binding.predicate !== undefined && binding.slot.tags.length === 0;
|
|
371
319
|
}
|
package/src/core/tag.ts
CHANGED
|
@@ -58,6 +58,14 @@ export interface TagKey<Value = unknown> {
|
|
|
58
58
|
readonly mask: TagKeyMask;
|
|
59
59
|
/** The criterion for one value, interned: the same value always yields the same object. */
|
|
60
60
|
of(value: Value): BindingTag<Value>;
|
|
61
|
+
/**
|
|
62
|
+
* The interned criterion for a value, or `undefined` when none was ever minted.
|
|
63
|
+
*
|
|
64
|
+
* @remarks Reading without minting is what keeps a request-side value from being retained for
|
|
65
|
+
* the process lifetime: a value no binding ever declared has no criterion, so a lookup can
|
|
66
|
+
* answer "no match" without inserting one.
|
|
67
|
+
*/
|
|
68
|
+
peek(value: Value): BindingTag<Value> | undefined;
|
|
61
69
|
}
|
|
62
70
|
|
|
63
71
|
let tagKeyCounter = -1;
|
|
@@ -101,6 +109,28 @@ export function tag<Value = unknown>(name: string): TagKey<Value> {
|
|
|
101
109
|
let lastValue: Value | undefined;
|
|
102
110
|
let lastPair: BindingTag<Value> | undefined;
|
|
103
111
|
|
|
112
|
+
// The miss paths live outside `of()`/`peek()` so the hot wrappers stay small enough to inline.
|
|
113
|
+
const internPair = (value: Value): BindingTag<Value> => {
|
|
114
|
+
const cacheKey = internKeyFor(value);
|
|
115
|
+
const existing = interned.get(cacheKey);
|
|
116
|
+
|
|
117
|
+
if (existing !== undefined) {
|
|
118
|
+
lastValue = value;
|
|
119
|
+
lastPair = existing;
|
|
120
|
+
|
|
121
|
+
return existing;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const pair = { key, value, mask } as BindingTag<Value>;
|
|
125
|
+
|
|
126
|
+
interned.set(cacheKey, pair);
|
|
127
|
+
lastValue = value;
|
|
128
|
+
lastPair = pair;
|
|
129
|
+
|
|
130
|
+
return pair;
|
|
131
|
+
};
|
|
132
|
+
const peekInterned = (value: Value): BindingTag<Value> | undefined => interned.get(internKeyFor(value));
|
|
133
|
+
|
|
104
134
|
const key: TagKey<Value> = {
|
|
105
135
|
name,
|
|
106
136
|
id,
|
|
@@ -109,30 +139,54 @@ export function tag<Value = unknown>(name: string): TagKey<Value> {
|
|
|
109
139
|
if (lastPair !== undefined && Object.is(value, lastValue)) {
|
|
110
140
|
return lastPair;
|
|
111
141
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
lastValue = value;
|
|
118
|
-
lastPair = existing;
|
|
119
|
-
|
|
120
|
-
return existing;
|
|
142
|
+
return internPair(value);
|
|
143
|
+
},
|
|
144
|
+
peek(value: Value): BindingTag<Value> | undefined {
|
|
145
|
+
if (lastPair !== undefined && Object.is(value, lastValue)) {
|
|
146
|
+
return lastPair;
|
|
121
147
|
}
|
|
122
|
-
|
|
123
|
-
const pair = { key, value, mask } as BindingTag<Value>;
|
|
124
|
-
|
|
125
|
-
interned.set(cacheKey, pair);
|
|
126
|
-
lastValue = value;
|
|
127
|
-
lastPair = pair;
|
|
128
|
-
|
|
129
|
-
return pair;
|
|
148
|
+
return peekInterned(value);
|
|
130
149
|
},
|
|
131
150
|
};
|
|
132
151
|
|
|
133
152
|
return key;
|
|
134
153
|
}
|
|
135
154
|
|
|
155
|
+
/**
|
|
156
|
+
* The reserved key a slot's name is a criterion of.
|
|
157
|
+
*
|
|
158
|
+
* @remarks `whenNamed(n)` and a request's `name` are sugar for `slotName.of(n)`, so one selection
|
|
159
|
+
* model serves both spellings — a name takes part in key masks, indexes and specificity like any
|
|
160
|
+
* criterion. What reserves the key is its identity; diagnostics render its criteria as `name:<value>`.
|
|
161
|
+
*
|
|
162
|
+
* @since 0.8.0
|
|
163
|
+
*/
|
|
164
|
+
export const slotName: TagKey<string> = tag<string>("di:name");
|
|
165
|
+
|
|
166
|
+
// One-entry front for the reserved key's read: a name's criterion never changes once minted, so a
|
|
167
|
+
// hit is sound forever, and a miss is never cached so a later `whenNamed` bind is still seen.
|
|
168
|
+
let lastPeekedName: string | undefined;
|
|
169
|
+
let lastPeekedCriterion: BindingTag<string> | undefined;
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* The reserved criterion for a name, or `undefined` while no binding has declared it.
|
|
173
|
+
*
|
|
174
|
+
* @remarks Reads without minting, so a request-side name no binding declared is never retained.
|
|
175
|
+
*
|
|
176
|
+
* @since 0.8.0
|
|
177
|
+
*/
|
|
178
|
+
export function slotNameCriterionOf(name: string): BindingTag<string> | undefined {
|
|
179
|
+
if (name === lastPeekedName) {
|
|
180
|
+
return lastPeekedCriterion;
|
|
181
|
+
}
|
|
182
|
+
const criterion = slotName.peek(name);
|
|
183
|
+
if (criterion !== undefined) {
|
|
184
|
+
lastPeekedName = name;
|
|
185
|
+
lastPeekedCriterion = criterion;
|
|
186
|
+
}
|
|
187
|
+
return criterion;
|
|
188
|
+
}
|
|
189
|
+
|
|
136
190
|
/**
|
|
137
191
|
* The key set a list of criteria covers.
|
|
138
192
|
*
|
package/src/errors/errors.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { BindingTag } from "#/core/tag";
|
|
2
|
+
import { slotName } from "#/core/tag";
|
|
1
3
|
import type { BindingIdentifier, BindingScope, ResolveOptions } from "#/core/types";
|
|
2
4
|
|
|
3
5
|
/**
|
|
@@ -43,13 +45,34 @@ export class TokenNotBoundError extends DiError {
|
|
|
43
45
|
}
|
|
44
46
|
|
|
45
47
|
// Options carry caller values a tag may hold — a bigint or a circular object must not make the
|
|
46
|
-
// diagnostic itself throw and mask the real error.
|
|
48
|
+
// diagnostic itself throw and mask the real error. The reserved criterion renders as `name`.
|
|
47
49
|
function describeResolveOptions(options: ResolveOptions): string {
|
|
48
50
|
try {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
51
|
+
let name = options.name;
|
|
52
|
+
const criteria: Array<string> = [];
|
|
53
|
+
const add = (criterion: BindingTag): void => {
|
|
54
|
+
if (criterion.key === slotName) {
|
|
55
|
+
name ??= String(criterion.value);
|
|
56
|
+
} else {
|
|
57
|
+
criteria.push(`${criterion.key.name}=${String(criterion.value)}`);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
if (options.tag !== undefined) {
|
|
61
|
+
add(options.tag);
|
|
62
|
+
}
|
|
63
|
+
if (options.tags !== undefined) {
|
|
64
|
+
for (const criterion of options.tags) {
|
|
65
|
+
add(criterion);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const display: { name?: string; tags?: Array<string> } = {};
|
|
69
|
+
if (name !== undefined) {
|
|
70
|
+
display.name = name;
|
|
71
|
+
}
|
|
72
|
+
if (criteria.length > 0) {
|
|
73
|
+
display.tags = criteria;
|
|
74
|
+
}
|
|
75
|
+
return JSON.stringify(display) ?? "undefined";
|
|
53
76
|
} catch {
|
|
54
77
|
return "[unserializable options]";
|
|
55
78
|
}
|
package/src/index.ts
CHANGED
|
@@ -21,7 +21,7 @@ export { token, tokenName } from "#/core/token";
|
|
|
21
21
|
export type { Token } from "#/core/token";
|
|
22
22
|
|
|
23
23
|
// Tag — the interned slot criteria a `whenTagged` and a resolve both take
|
|
24
|
-
export { coversTagKeys, NO_TAG_KEYS, tag, tagKeyMaskOf } from "#/core/tag";
|
|
24
|
+
export { coversTagKeys, NO_TAG_KEYS, slotName, tag, tagKeyMaskOf } from "#/core/tag";
|
|
25
25
|
export type { TagKey, TagKeyMask } from "#/core/tag";
|
|
26
26
|
|
|
27
27
|
// Binding builders — types only
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { BindingTag } from "#/core/tag";
|
|
2
|
+
import { slotName, slotNameCriterionOf } from "#/core/tag";
|
|
2
3
|
import type { Token } from "#/core/token";
|
|
3
4
|
import type { Constructor, ResolveOptions } from "#/core/types";
|
|
4
5
|
|
|
@@ -19,37 +20,39 @@ export interface DependencySlot {
|
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
/**
|
|
22
|
-
*
|
|
23
|
+
* The lone criterion of a request that carries exactly one, whatever its spelling — the shape the
|
|
24
|
+
* registry has a direct index for.
|
|
23
25
|
*
|
|
24
|
-
* @
|
|
25
|
-
|
|
26
|
-
export function isNameOnlyOptions(options: ResolveOptions): options is ResolveOptions & { name: string } {
|
|
27
|
-
return (
|
|
28
|
-
options.name !== undefined && options.tag === undefined && (options.tags === undefined || options.tags.length === 0)
|
|
29
|
-
);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* The lone tag of a request that asks for exactly one, written either way — the shape the registry
|
|
34
|
-
* has a direct tag index for.
|
|
35
|
-
*
|
|
36
|
-
* @remarks Both spellings answer here, so the index is not something one of them silently misses.
|
|
26
|
+
* @remarks Every spelling answers here, so the index is not something one of them silently misses;
|
|
27
|
+
* a name folds to the reserved `slotName` criterion, making the name lane the tag lane.
|
|
37
28
|
*
|
|
38
29
|
* @since 0.5.0-canary.9
|
|
39
30
|
*/
|
|
40
|
-
export function
|
|
41
|
-
if (options
|
|
31
|
+
export function singleCriterionOnlyOf(options: ResolveOptions | undefined): BindingTag | undefined {
|
|
32
|
+
if (options === undefined) {
|
|
42
33
|
return undefined;
|
|
43
34
|
}
|
|
35
|
+
if (options.name !== undefined) {
|
|
36
|
+
return loneNameCriterionOf(options);
|
|
37
|
+
}
|
|
44
38
|
const listed = options.tags;
|
|
45
39
|
const shorthand = options.tag;
|
|
46
40
|
if (shorthand !== undefined) {
|
|
47
|
-
// Both sources present means the request carries two tags, which no single-tag index can answer.
|
|
48
41
|
return listed === undefined || listed.length === 0 ? shorthand : undefined;
|
|
49
42
|
}
|
|
50
43
|
return listed !== undefined && listed.length === 1 ? listed[0] : undefined;
|
|
51
44
|
}
|
|
52
45
|
|
|
46
|
+
/** The name spelling's half of the fold, kept apart so the common body stays small enough to inline. */
|
|
47
|
+
function loneNameCriterionOf(options: ResolveOptions): BindingTag | undefined {
|
|
48
|
+
// A name next to any tag means the request carries two criteria, which no single index answers.
|
|
49
|
+
if (options.tag !== undefined || (options.tags !== undefined && options.tags.length > 0)) {
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
// Read, not minted: a request must not retain a name no binding ever declared.
|
|
53
|
+
return slotNameCriterionOf(options.name as string);
|
|
54
|
+
}
|
|
55
|
+
|
|
53
56
|
/** Shared core: build a ResolveOptions from already-normalised name + tags. */
|
|
54
57
|
function buildOptions(
|
|
55
58
|
name: string | undefined,
|
|
@@ -83,8 +86,12 @@ export function injectionSlotToResolveOptions(
|
|
|
83
86
|
/** Where a slot's derived options are memoized, so the same object is handed out every resolve. */
|
|
84
87
|
const MEMOIZED_RESOLVE_OPTIONS: unique symbol = Symbol("di:resolve-options");
|
|
85
88
|
|
|
89
|
+
/** Where a slot's folded lone criterion is memoized — `null` records "computed: none". */
|
|
90
|
+
const MEMOIZED_SINGLE_CRITERION: unique symbol = Symbol("di:single-criterion");
|
|
91
|
+
|
|
86
92
|
interface SlotWithMemoizedOptions {
|
|
87
93
|
[MEMOIZED_RESOLVE_OPTIONS]?: ResolveOptions;
|
|
94
|
+
[MEMOIZED_SINGLE_CRITERION]?: BindingTag | null;
|
|
88
95
|
}
|
|
89
96
|
|
|
90
97
|
/**
|
|
@@ -126,6 +133,44 @@ function memoizeResolveOptions(
|
|
|
126
133
|
return built;
|
|
127
134
|
}
|
|
128
135
|
|
|
136
|
+
/**
|
|
137
|
+
* The lone criterion a dependency's fixed criteria fold to, memoized on the slot like its options —
|
|
138
|
+
* `null` means the fold answered "none".
|
|
139
|
+
*
|
|
140
|
+
* @remarks A slot's criteria never change after declaration, so the fold happens once per slot
|
|
141
|
+
* rather than per hop — which is what keeps a named dependency's resolve off the intern map. A
|
|
142
|
+
* lone name whose criterion is not interned yet is left unmemoized: a later `whenNamed` binding
|
|
143
|
+
* mints it, and the next fold must see that.
|
|
144
|
+
*
|
|
145
|
+
* @since 0.8.0
|
|
146
|
+
*/
|
|
147
|
+
export function singleCriterionForSlot(injectionSlot: DependencySlot): BindingTag | null {
|
|
148
|
+
const slot = injectionSlot as SlotWithMemoizedOptions;
|
|
149
|
+
const memoized = slot[MEMOIZED_SINGLE_CRITERION];
|
|
150
|
+
if (memoized !== undefined) {
|
|
151
|
+
return memoized;
|
|
152
|
+
}
|
|
153
|
+
const options = resolveOptionsForSlot(injectionSlot);
|
|
154
|
+
const criterion = singleCriterionOnlyOf(options);
|
|
155
|
+
const folded = criterion ?? null;
|
|
156
|
+
if (criterion !== undefined || !isLoneNameOptions(options)) {
|
|
157
|
+
try {
|
|
158
|
+
slot[MEMOIZED_SINGLE_CRITERION] = folded;
|
|
159
|
+
} catch {
|
|
160
|
+
// A frozen slot re-folds on every hop rather than throwing.
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return folded;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** A slot request whose only criterion is a name — the one shape whose fold can change after a bind. */
|
|
167
|
+
function isLoneNameOptions(options: ResolveOptions | undefined): boolean {
|
|
168
|
+
// Slot-derived options never carry the `tag` shorthand — `buildOptions` folds it into `tags`.
|
|
169
|
+
return (
|
|
170
|
+
options !== undefined && options.name !== undefined && (options.tags === undefined || options.tags.length === 0)
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
129
174
|
/**
|
|
130
175
|
* Resolve options derived from a binding slot (tags may be empty; omits when nothing to match).
|
|
131
176
|
*
|
|
@@ -140,5 +185,17 @@ export function bindingSlotToResolveOptions(bindingSlot: {
|
|
|
140
185
|
readonly tags?: ReadonlyArray<BindingTag> | undefined;
|
|
141
186
|
}): ResolveOptions | undefined {
|
|
142
187
|
const tags = bindingSlot.tags;
|
|
143
|
-
|
|
188
|
+
let name = bindingSlot.name;
|
|
189
|
+
let criteria: ReadonlyArray<BindingTag> | undefined = tags !== undefined && tags.length > 0 ? tags : undefined;
|
|
190
|
+
if (criteria !== undefined) {
|
|
191
|
+
// A reserved criterion folds into `name` — carried in `tags` it would be said twice or, on a
|
|
192
|
+
// descriptor that spells its name as a tag, not at all.
|
|
193
|
+
const reserved = criteria.find((criterion) => criterion.key === slotName);
|
|
194
|
+
if (reserved !== undefined) {
|
|
195
|
+
name ??= reserved.value as string;
|
|
196
|
+
const rest = criteria.filter((criterion) => criterion.key !== slotName);
|
|
197
|
+
criteria = rest.length > 0 ? rest : undefined;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return buildOptions(name, criteria);
|
|
144
201
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Binding } from "#/core/binding";
|
|
2
2
|
import { effectiveBindingScope } from "#/core/binding-scope";
|
|
3
3
|
import type { BindingRegistry } from "#/core/registry";
|
|
4
|
+
import { slotName } from "#/core/tag";
|
|
4
5
|
import type { Token } from "#/core/token";
|
|
5
6
|
import { tokenName } from "#/core/token";
|
|
6
7
|
import type { BindingKind, BindingScope, Constructor } from "#/core/types";
|
|
@@ -96,12 +97,23 @@ function matchingTargets(candidates: ReadonlyArray<Binding>, ref: DependencySlot
|
|
|
96
97
|
return candidates.filter((candidate) => matchesSlot(candidate.slot, criterion));
|
|
97
98
|
}
|
|
98
99
|
|
|
100
|
+
/** The name a dependency asks for, whichever spelling carries it. */
|
|
101
|
+
function refSlotName(ref: DependencySlot): string | undefined {
|
|
102
|
+
if (ref.name !== undefined) {
|
|
103
|
+
return ref.name;
|
|
104
|
+
}
|
|
105
|
+
const reserved = ref.tags?.find((criterion) => criterion.key === slotName);
|
|
106
|
+
return reserved === undefined ? undefined : String(reserved.value);
|
|
107
|
+
}
|
|
108
|
+
|
|
99
109
|
function edgeLabel(ref: DependencySlot, index: number): string {
|
|
110
|
+
const name = refSlotName(ref);
|
|
111
|
+
const firstPlainTag = ref.tags?.find((criterion) => criterion.key !== slotName);
|
|
100
112
|
const criterion =
|
|
101
|
-
|
|
102
|
-
? `name:${
|
|
103
|
-
:
|
|
104
|
-
? `tag:${
|
|
113
|
+
name !== undefined
|
|
114
|
+
? `name:${name}`
|
|
115
|
+
: firstPlainTag !== undefined
|
|
116
|
+
? `tag:${firstPlainTag.key.name}=${String(firstPlainTag.value)}`
|
|
105
117
|
: `[${index}]`;
|
|
106
118
|
|
|
107
119
|
return ref.optional ? `${criterion} optional` : criterion;
|
|
@@ -170,23 +182,25 @@ function addDependencyEdges(
|
|
|
170
182
|
return;
|
|
171
183
|
}
|
|
172
184
|
|
|
185
|
+
const unboundSlotName = refSlotName(ref);
|
|
173
186
|
accumulator.edges.push({
|
|
174
187
|
from,
|
|
175
188
|
to: unboundNodeIdFor(accumulator, ref.token),
|
|
176
189
|
label,
|
|
177
190
|
optional: true,
|
|
178
|
-
...(
|
|
191
|
+
...(unboundSlotName !== undefined ? { slotName: unboundSlotName } : {}),
|
|
179
192
|
});
|
|
180
193
|
|
|
181
194
|
return;
|
|
182
195
|
}
|
|
183
196
|
|
|
197
|
+
const requestedName = refSlotName(ref);
|
|
184
198
|
for (const target of targets) {
|
|
185
199
|
// A multi dep with no criterion of its own fans out — each edge names the slot it hits.
|
|
186
|
-
const
|
|
200
|
+
const edgeSlotName = target.slot.name ?? requestedName;
|
|
187
201
|
const perTargetLabel =
|
|
188
|
-
ref.multi &&
|
|
189
|
-
? edgeLabel({ ...ref, name:
|
|
202
|
+
ref.multi && requestedName === undefined && edgeSlotName !== undefined
|
|
203
|
+
? edgeLabel({ ...ref, name: edgeSlotName }, index)
|
|
190
204
|
: label;
|
|
191
205
|
|
|
192
206
|
accumulator.edges.push({
|
|
@@ -194,7 +208,7 @@ function addDependencyEdges(
|
|
|
194
208
|
to: target.id,
|
|
195
209
|
label: perTargetLabel,
|
|
196
210
|
optional: ref.optional,
|
|
197
|
-
...(
|
|
211
|
+
...(edgeSlotName !== undefined ? { slotName: edgeSlotName } : {}),
|
|
198
212
|
});
|
|
199
213
|
}
|
|
200
214
|
}
|
|
@@ -31,15 +31,10 @@ export interface DefaultLookupEntry<Owner> {
|
|
|
31
31
|
*/
|
|
32
32
|
export const ALIAS_HOP_LIMIT = 32;
|
|
33
33
|
|
|
34
|
-
/**
|
|
35
|
-
* @since 0.5.0-canary.8
|
|
36
|
-
*/
|
|
37
|
-
const newNameToEntryMap = <Owner>(): Map<string, DefaultLookupEntry<Owner> | null> => new Map();
|
|
38
|
-
|
|
39
34
|
const newTagToEntryMap = <Owner>(): Map<BindingTag, DefaultLookupEntry<Owner> | null> => new Map();
|
|
40
35
|
|
|
41
36
|
/**
|
|
42
|
-
* A version-stamped cache of binding lookups by token
|
|
37
|
+
* A version-stamped cache of binding lookups by token and criterion across the container chain.
|
|
43
38
|
*
|
|
44
39
|
* @since 0.5.0-canary.9
|
|
45
40
|
*/
|
|
@@ -51,8 +46,6 @@ export class BindingLookupCache<Owner> {
|
|
|
51
46
|
// is tracked by the token slot rather than by the entry.
|
|
52
47
|
#lastToken: Token<unknown> | Constructor | undefined;
|
|
53
48
|
#lastEntry: DefaultLookupEntry<Owner> | null = null;
|
|
54
|
-
readonly #byTokenAndName = new Map<Token<unknown> | Constructor, Map<string, DefaultLookupEntry<Owner> | null>>();
|
|
55
|
-
#namedVersion = -1;
|
|
56
49
|
readonly #byTokenAndTag = new Map<Token<unknown> | Constructor, Map<BindingTag, DefaultLookupEntry<Owner> | null>>();
|
|
57
50
|
#taggedVersion = -1;
|
|
58
51
|
// One entry in front of the tag map, and the map is not written until a second distinct request
|
|
@@ -101,24 +94,6 @@ export class BindingLookupCache<Owner> {
|
|
|
101
94
|
return entry;
|
|
102
95
|
}
|
|
103
96
|
|
|
104
|
-
/** `null` when the name's shape needs the full selection path. */
|
|
105
|
-
namedEntry(token: Token<unknown> | Constructor, name: string): DefaultLookupEntry<Owner> | null {
|
|
106
|
-
const version = this.chainVersion();
|
|
107
|
-
if (version !== this.#namedVersion) {
|
|
108
|
-
this.#byTokenAndName.clear();
|
|
109
|
-
this.#namedVersion = version;
|
|
110
|
-
}
|
|
111
|
-
// Computed, not eager: this runs on every named resolve, and the eager form would allocate a
|
|
112
|
-
// Map per call only to discard it on the hit that follows.
|
|
113
|
-
const byName = getOrInsertComputed(this.#byTokenAndName, token, newNameToEntryMap);
|
|
114
|
-
let entry = byName.get(name);
|
|
115
|
-
if (entry === undefined) {
|
|
116
|
-
entry = this.#findNamedInChain(token, name);
|
|
117
|
-
byName.set(name, entry);
|
|
118
|
-
}
|
|
119
|
-
return entry;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
97
|
/** `null` when the tag's shape needs the full selection path. */
|
|
123
98
|
taggedEntry(token: Token<unknown> | Constructor, tag: BindingTag): DefaultLookupEntry<Owner> | null {
|
|
124
99
|
const version = this.chainVersion();
|
|
@@ -177,21 +152,6 @@ export class BindingLookupCache<Owner> {
|
|
|
177
152
|
return this.#parent === undefined ? null : this.#parent.#findDefaultInChain(token);
|
|
178
153
|
}
|
|
179
154
|
|
|
180
|
-
#findNamedInChain(token: Token<unknown> | Constructor, name: string): DefaultLookupEntry<Owner> | null {
|
|
181
|
-
const named = this.#registry.getSimpleNamed(token, name);
|
|
182
|
-
if (named !== undefined) {
|
|
183
|
-
// Predicates need a live context; aliases carry options through the full path.
|
|
184
|
-
if (named.predicate !== undefined || named.kind === "alias") {
|
|
185
|
-
return null;
|
|
186
|
-
}
|
|
187
|
-
return { binding: named, owner: this.#owner };
|
|
188
|
-
}
|
|
189
|
-
if (this.#registry.has(token)) {
|
|
190
|
-
return null;
|
|
191
|
-
}
|
|
192
|
-
return this.#parent === undefined ? null : this.#parent.#findNamedInChain(token, name);
|
|
193
|
-
}
|
|
194
|
-
|
|
195
155
|
#findTaggedInChain(token: Token<unknown> | Constructor, tag: BindingTag): DefaultLookupEntry<Owner> | null {
|
|
196
156
|
const tagged = this.#registry.getSimpleTagged(token, tag);
|
|
197
157
|
if (tagged !== undefined) {
|