@codefast/di 0.5.0-canary.7 → 0.5.0-canary.8
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 +68 -0
- package/README.md +3 -1
- package/dist/binding.d.ts +47 -5
- package/dist/binding.d.ts.map +1 -1
- package/dist/binding.js +44 -0
- package/dist/binding.js.map +1 -1
- package/dist/constructor-type.d.ts +4 -5
- package/dist/constructor-type.d.ts.map +1 -1
- package/dist/container/binding-builders.d.ts +32 -11
- package/dist/container/binding-builders.d.ts.map +1 -1
- package/dist/container/binding-builders.js +140 -191
- package/dist/container/binding-builders.js.map +1 -1
- package/dist/container/container.d.ts.map +1 -1
- package/dist/container/container.js +75 -92
- package/dist/container/container.js.map +1 -1
- package/dist/decorators/inject.d.ts +2 -4
- package/dist/decorators/inject.d.ts.map +1 -1
- package/dist/decorators/inject.js.map +1 -1
- package/dist/errors.d.ts +14 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +17 -0
- package/dist/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/introspection/inspector.js +1 -1
- package/dist/introspection/inspector.js.map +1 -1
- package/dist/metadata/metadata-keys.d.ts +3 -6
- package/dist/metadata/metadata-keys.d.ts.map +1 -1
- package/dist/metadata/metadata-keys.js +3 -6
- package/dist/metadata/metadata-keys.js.map +1 -1
- package/dist/registry.d.ts +14 -2
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +35 -45
- package/dist/registry.js.map +1 -1
- package/dist/resolution/activation-need.d.ts +25 -0
- package/dist/resolution/activation-need.d.ts.map +1 -0
- package/dist/resolution/activation-need.js +64 -0
- package/dist/resolution/activation-need.js.map +1 -0
- package/dist/resolution/binding-lookup-cache.d.ts +41 -0
- package/dist/resolution/binding-lookup-cache.d.ts.map +1 -0
- package/dist/resolution/binding-lookup-cache.js +102 -0
- package/dist/resolution/binding-lookup-cache.js.map +1 -0
- package/dist/resolution/binding-select.js +1 -4
- package/dist/resolution/binding-select.js.map +1 -1
- package/dist/resolution/class-introspector.d.ts +27 -0
- package/dist/resolution/class-introspector.d.ts.map +1 -0
- package/dist/resolution/class-introspector.js +60 -0
- package/dist/resolution/class-introspector.js.map +1 -0
- package/dist/resolution/diagnostics.d.ts +41 -0
- package/dist/resolution/diagnostics.d.ts.map +1 -0
- package/dist/resolution/diagnostics.js +18 -0
- package/dist/resolution/diagnostics.js.map +1 -0
- package/dist/resolution/environment.d.ts +22 -1
- package/dist/resolution/environment.d.ts.map +1 -1
- package/dist/resolution/environment.js +27 -1
- package/dist/resolution/environment.js.map +1 -1
- package/dist/resolution/instantiation-plan.d.ts +15 -15
- package/dist/resolution/instantiation-plan.d.ts.map +1 -1
- package/dist/resolution/instantiation-plan.js +68 -48
- package/dist/resolution/instantiation-plan.js.map +1 -1
- package/dist/resolution/lifecycle.d.ts +2 -0
- package/dist/resolution/lifecycle.d.ts.map +1 -1
- package/dist/resolution/lifecycle.js +16 -11
- package/dist/resolution/lifecycle.js.map +1 -1
- package/dist/resolution/resolution-path.d.ts +4 -13
- package/dist/resolution/resolution-path.d.ts.map +1 -1
- package/dist/resolution/resolution-path.js +3 -16
- package/dist/resolution/resolution-path.js.map +1 -1
- package/dist/resolution/resolver.d.ts +7 -2
- package/dist/resolution/resolver.d.ts.map +1 -1
- package/dist/resolution/resolver.js +116 -328
- package/dist/resolution/resolver.js.map +1 -1
- package/dist/resolution/scope.d.ts +7 -15
- package/dist/resolution/scope.d.ts.map +1 -1
- package/dist/resolution/scope.js +48 -44
- package/dist/resolution/scope.js.map +1 -1
- package/package.json +7 -97
- package/src/binding.ts +104 -5
- package/src/constructor-type.ts +4 -5
- package/src/container/binding-builders.ts +180 -283
- package/src/container/container.ts +90 -103
- package/src/decorators/inject.ts +3 -5
- package/src/errors.ts +21 -0
- package/src/index.ts +1 -0
- package/src/introspection/inspector.ts +1 -1
- package/src/metadata/metadata-keys.ts +3 -6
- package/src/registry.ts +38 -59
- package/src/resolution/activation-need.ts +81 -0
- package/src/resolution/binding-lookup-cache.ts +132 -0
- package/src/resolution/binding-select.ts +1 -4
- package/src/resolution/class-introspector.ts +74 -0
- package/src/resolution/diagnostics.ts +43 -0
- package/src/resolution/environment.ts +31 -1
- package/src/resolution/instantiation-plan.ts +113 -61
- package/src/resolution/lifecycle.ts +16 -11
- package/src/resolution/resolution-path.ts +5 -20
- package/src/resolution/resolver.ts +142 -371
- package/src/resolution/scope.ts +50 -49
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Binding, BindToBuilder } from "#/binding";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { NO_INSTANCE } from "#/binding";
|
|
3
|
+
import type { BindingRegistration } from "#/container/binding-builders";
|
|
4
|
+
import { BindingChain } from "#/container/binding-builders";
|
|
4
5
|
import type { AutoRegisterRegistry } from "#/decorators/injectable";
|
|
5
6
|
import {
|
|
6
7
|
AsyncModuleLoadError,
|
|
@@ -23,6 +24,8 @@ import type { AsyncModuleBuilder } from "#/module";
|
|
|
23
24
|
import { isSyncModule, MODULE_SETUP } from "#/module";
|
|
24
25
|
import { BindingRegistry } from "#/registry";
|
|
25
26
|
import { effectiveBindingScope } from "#/resolution/binding-scope";
|
|
27
|
+
import type { ResolutionDiagnostics } from "#/resolution/diagnostics";
|
|
28
|
+
import { RESOLUTION_DIAGNOSTICS } from "#/resolution/diagnostics";
|
|
26
29
|
import { LifecycleManager } from "#/resolution/lifecycle";
|
|
27
30
|
import { injectionSlotToResolveOptions, bindingSlotToResolveOptions } from "#/resolution/resolve-options";
|
|
28
31
|
import { DependencyResolver } from "#/resolution/resolver";
|
|
@@ -38,17 +41,6 @@ import type {
|
|
|
38
41
|
ResolveOptions,
|
|
39
42
|
} from "#/types";
|
|
40
43
|
|
|
41
|
-
// True when re-adding `restored` would immediately be displaced again by `current`
|
|
42
|
-
// (both slot-based with equal slots) — in that case the replacement was legitimate.
|
|
43
|
-
function displacesRestoredBinding(current: Binding, restored: Binding): boolean {
|
|
44
|
-
const currentIsPurePredicate =
|
|
45
|
-
current.predicate !== undefined && current.slot.name === undefined && current.slot.tags.length === 0;
|
|
46
|
-
if (currentIsPurePredicate) {
|
|
47
|
-
return false;
|
|
48
|
-
}
|
|
49
|
-
return bindingSlotEquals(current.slot, restored.slot);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
44
|
// ── Container interface ────────────────────────────────────────────────────────
|
|
53
45
|
|
|
54
46
|
/**
|
|
@@ -119,23 +111,54 @@ class DefaultContainer implements Container {
|
|
|
119
111
|
readonly #scope: ScopeManager;
|
|
120
112
|
readonly #lifecycle: LifecycleManager;
|
|
121
113
|
#resolver!: DependencyResolver;
|
|
122
|
-
|
|
114
|
+
// Built on the first introspecting call — a container that only binds and resolves never needs it.
|
|
115
|
+
#inspector: Inspector | undefined;
|
|
123
116
|
readonly #parent: DefaultContainer | undefined;
|
|
124
117
|
|
|
125
|
-
// Module tracking: module -> ref count
|
|
126
|
-
|
|
118
|
+
// Module tracking: module -> ref count. Both tables stay unallocated until a module is loaded.
|
|
119
|
+
#moduleRefs: Map<object, number> | undefined;
|
|
127
120
|
// Module bindings: module -> array of binding IDs registered by it
|
|
128
|
-
|
|
121
|
+
#moduleBindingIds: Map<object, Array<BindingIdentifier>> | undefined;
|
|
122
|
+
// One shared registration for every chain this container's own `bind()` creates.
|
|
123
|
+
#registration: BindingRegistration | undefined;
|
|
129
124
|
|
|
130
125
|
constructor(parent?: DefaultContainer) {
|
|
131
126
|
this.#parent = parent;
|
|
132
127
|
this.#registry = new BindingRegistry();
|
|
133
128
|
this.#scope = new ScopeManager(parent !== undefined);
|
|
134
129
|
this.#lifecycle = new LifecycleManager();
|
|
135
|
-
this.#inspector = new Inspector(this.#registry, this.#scope, parent !== undefined, () => this.#disposed);
|
|
136
130
|
this.#initResolver();
|
|
137
131
|
}
|
|
138
132
|
|
|
133
|
+
#getInspector(): Inspector {
|
|
134
|
+
return (this.#inspector ??= new Inspector(
|
|
135
|
+
this.#registry,
|
|
136
|
+
this.#scope,
|
|
137
|
+
this.#parent !== undefined,
|
|
138
|
+
() => this.#disposed,
|
|
139
|
+
));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
[RESOLUTION_DIAGNOSTICS](): ResolutionDiagnostics {
|
|
143
|
+
const builtSubsystems: Array<string> = [];
|
|
144
|
+
if (this.#inspector !== undefined) {
|
|
145
|
+
builtSubsystems.push("container.inspector");
|
|
146
|
+
}
|
|
147
|
+
if (this.#moduleRefs !== undefined || this.#moduleBindingIds !== undefined) {
|
|
148
|
+
builtSubsystems.push("container.moduleTables");
|
|
149
|
+
}
|
|
150
|
+
if (this.#registry.isBuilt) {
|
|
151
|
+
builtSubsystems.push("registry.namedIndex");
|
|
152
|
+
}
|
|
153
|
+
if (this.#scope.isBuilt) {
|
|
154
|
+
builtSubsystems.push("scope.scoped");
|
|
155
|
+
}
|
|
156
|
+
if (this.#lifecycle.isBuilt) {
|
|
157
|
+
builtSubsystems.push("lifecycle.activationHooks");
|
|
158
|
+
}
|
|
159
|
+
return { ...this.#resolver.describeCaches(), builtSubsystems };
|
|
160
|
+
}
|
|
161
|
+
|
|
139
162
|
#initResolver(): void {
|
|
140
163
|
const metadataReader = this.#getMetadataReader();
|
|
141
164
|
const parentResolver = this.#parent === undefined ? undefined : this.#parent.#resolver;
|
|
@@ -176,63 +199,24 @@ class DefaultContainer implements Container {
|
|
|
176
199
|
return this.#createBindToBuilder(token);
|
|
177
200
|
}
|
|
178
201
|
|
|
202
|
+
/** The registration every non-module chain shares, so `bind()` allocates only the builder. */
|
|
203
|
+
#ownRegistration(): BindingRegistration {
|
|
204
|
+
return (this.#registration ??= { registry: this.#registry, moduleBindingIds: undefined });
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** One registration per module load, holding that module's id list directly. */
|
|
208
|
+
#moduleRegistration(moduleRef: object): BindingRegistration {
|
|
209
|
+
return {
|
|
210
|
+
registry: this.#registry,
|
|
211
|
+
moduleBindingIds: (this.#moduleBindingIds ??= new Map()).getOrInsert(moduleRef, []),
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
179
215
|
#createBindToBuilder<const Value>(
|
|
180
216
|
token: Token<Value> | Constructor<Value>,
|
|
181
|
-
|
|
217
|
+
registration: BindingRegistration = this.#ownRegistration(),
|
|
182
218
|
): BindToBuilder<Value> {
|
|
183
|
-
|
|
184
|
-
// Bindings displaced by this fluent chain's commits — each stays restorable
|
|
185
|
-
// until the chain settles on a shape that genuinely conflicts with it. A list,
|
|
186
|
-
// because one chain can displace several bindings (the default at an
|
|
187
|
-
// intermediate commit, then a named/tagged binding at the final one).
|
|
188
|
-
const displacedByChain: Array<Binding> = [];
|
|
189
|
-
|
|
190
|
-
const commitBinding = <BindingValue>(
|
|
191
|
-
binding: Binding<BindingValue>,
|
|
192
|
-
previousId?: BindingIdentifier,
|
|
193
|
-
): BindingIdentifier => {
|
|
194
|
-
if (previousId !== undefined) {
|
|
195
|
-
registry.removeById(previousId);
|
|
196
|
-
if (moduleRef !== undefined) {
|
|
197
|
-
const ids = this.#moduleBindingIds.get(moduleRef);
|
|
198
|
-
if (ids !== undefined) {
|
|
199
|
-
const idx = ids.indexOf(previousId);
|
|
200
|
-
if (idx !== -1) {
|
|
201
|
-
ids.splice(idx, 1);
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
// The registry stores value-erased bindings — this is the single erasure point for the builder chain.
|
|
207
|
-
const displaced = registry.add(binding as Binding);
|
|
208
|
-
if (displaced !== undefined) {
|
|
209
|
-
// A fluent chain commits eagerly on each refinement, so an intermediate
|
|
210
|
-
// commit can displace a binding that the final shape would never conflict
|
|
211
|
-
// with. Remember it so a later re-commit that morphs away (named/tagged
|
|
212
|
-
// slot, pure predicate) can restore it.
|
|
213
|
-
displacedByChain.push(displaced);
|
|
214
|
-
}
|
|
215
|
-
if (previousId !== undefined && displacedByChain.length > 0) {
|
|
216
|
-
for (let index = displacedByChain.length - 1; index >= 0; index -= 1) {
|
|
217
|
-
const candidate = displacedByChain[index]!;
|
|
218
|
-
if (!displacesRestoredBinding(binding as Binding, candidate)) {
|
|
219
|
-
registry.add(candidate);
|
|
220
|
-
displacedByChain.splice(index, 1);
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
if (moduleRef !== undefined) {
|
|
225
|
-
let ids = this.#moduleBindingIds.get(moduleRef);
|
|
226
|
-
if (ids === undefined) {
|
|
227
|
-
ids = [];
|
|
228
|
-
this.#moduleBindingIds.set(moduleRef, ids);
|
|
229
|
-
}
|
|
230
|
-
ids.push(binding.id);
|
|
231
|
-
}
|
|
232
|
-
return binding.id;
|
|
233
|
-
};
|
|
234
|
-
|
|
235
|
-
return new BindingEntry<Value>(token, commitBinding);
|
|
219
|
+
return new BindingChain<Value>(token, registration);
|
|
236
220
|
}
|
|
237
221
|
|
|
238
222
|
unbind(tokenOrId: Token<unknown> | Constructor | BindingIdentifier): void {
|
|
@@ -255,9 +239,9 @@ class DefaultContainer implements Container {
|
|
|
255
239
|
#drainSingletons(bindings: ReadonlyArray<Binding>): Array<[Binding, unknown]> {
|
|
256
240
|
const pairs: Array<[Binding, unknown]> = [];
|
|
257
241
|
for (const binding of bindings) {
|
|
258
|
-
if (
|
|
259
|
-
pairs.push([binding,
|
|
260
|
-
this.#scope.deleteSingleton(binding
|
|
242
|
+
if (binding.instance !== NO_INSTANCE) {
|
|
243
|
+
pairs.push([binding, binding.instance]);
|
|
244
|
+
this.#scope.deleteSingleton(binding);
|
|
261
245
|
}
|
|
262
246
|
}
|
|
263
247
|
return pairs;
|
|
@@ -319,12 +303,13 @@ class DefaultContainer implements Container {
|
|
|
319
303
|
throw new AsyncModuleLoadError(module.name);
|
|
320
304
|
}
|
|
321
305
|
const moduleRef = module as object;
|
|
322
|
-
const
|
|
306
|
+
const moduleRefs = (this.#moduleRefs ??= new Map());
|
|
307
|
+
const existing = moduleRefs.get(moduleRef);
|
|
323
308
|
if (existing !== undefined) {
|
|
324
|
-
|
|
309
|
+
moduleRefs.set(moduleRef, existing + 1);
|
|
325
310
|
continue;
|
|
326
311
|
}
|
|
327
|
-
|
|
312
|
+
moduleRefs.set(moduleRef, 1);
|
|
328
313
|
const builder = this.#createModuleBuilder(moduleRef);
|
|
329
314
|
module[MODULE_SETUP](builder);
|
|
330
315
|
}
|
|
@@ -359,12 +344,13 @@ class DefaultContainer implements Container {
|
|
|
359
344
|
|
|
360
345
|
async #loadOneModuleAsync(module: SyncModule | AsyncModule): Promise<void> {
|
|
361
346
|
const moduleRef = module as object;
|
|
362
|
-
const
|
|
347
|
+
const moduleRefs = (this.#moduleRefs ??= new Map());
|
|
348
|
+
const existing = moduleRefs.get(moduleRef);
|
|
363
349
|
if (existing !== undefined) {
|
|
364
|
-
|
|
350
|
+
moduleRefs.set(moduleRef, existing + 1);
|
|
365
351
|
return;
|
|
366
352
|
}
|
|
367
|
-
|
|
353
|
+
moduleRefs.set(moduleRef, 1);
|
|
368
354
|
|
|
369
355
|
if (isSyncModule(module)) {
|
|
370
356
|
const builder = this.#createModuleBuilder(moduleRef);
|
|
@@ -381,9 +367,10 @@ class DefaultContainer implements Container {
|
|
|
381
367
|
}
|
|
382
368
|
|
|
383
369
|
#createModuleBuilder(moduleRef: object): ModuleBuilder {
|
|
370
|
+
const registration = this.#moduleRegistration(moduleRef);
|
|
384
371
|
return {
|
|
385
372
|
bind: <const Value>(token: Token<Value> | Constructor<Value>): BindToBuilder<Value> =>
|
|
386
|
-
this.#createBindToBuilder(token,
|
|
373
|
+
this.#createBindToBuilder(token, registration),
|
|
387
374
|
import: (...modules: Array<SyncModule>): void => {
|
|
388
375
|
this.#loadSyncModules(modules);
|
|
389
376
|
},
|
|
@@ -391,9 +378,10 @@ class DefaultContainer implements Container {
|
|
|
391
378
|
}
|
|
392
379
|
|
|
393
380
|
#createAsyncModuleBuilder(moduleRef: object, importPromises: Array<Promise<void>>): AsyncModuleBuilder {
|
|
381
|
+
const registration = this.#moduleRegistration(moduleRef);
|
|
394
382
|
return {
|
|
395
383
|
bind: <const Value>(token: Token<Value> | Constructor<Value>): BindToBuilder<Value> =>
|
|
396
|
-
this.#createBindToBuilder(token,
|
|
384
|
+
this.#createBindToBuilder(token, registration),
|
|
397
385
|
import: (...modules: Array<SyncModule | AsyncModule>): void => {
|
|
398
386
|
for (const module of modules) {
|
|
399
387
|
importPromises.push(this.#loadOneModuleAsync(module));
|
|
@@ -411,17 +399,17 @@ class DefaultContainer implements Container {
|
|
|
411
399
|
|
|
412
400
|
/** Unregister module bindings and collect [binding, instance] pairs for deactivation. */
|
|
413
401
|
#removeModuleBindings(ref: object): Array<[Binding, unknown]> {
|
|
414
|
-
this.#moduleRefs
|
|
415
|
-
const ids = this.#moduleBindingIds
|
|
416
|
-
this.#moduleBindingIds
|
|
402
|
+
this.#moduleRefs?.delete(ref);
|
|
403
|
+
const ids = this.#moduleBindingIds?.get(ref) ?? [];
|
|
404
|
+
this.#moduleBindingIds?.delete(ref);
|
|
417
405
|
const pairs: Array<[Binding, unknown]> = [];
|
|
418
406
|
for (const id of ids) {
|
|
419
407
|
const binding = this.#registry.getById(id);
|
|
420
408
|
if (binding !== undefined) {
|
|
421
409
|
this.#registry.removeById(id);
|
|
422
|
-
if (
|
|
423
|
-
pairs.push([binding,
|
|
424
|
-
this.#scope.deleteSingleton(
|
|
410
|
+
if (binding.instance !== NO_INSTANCE) {
|
|
411
|
+
pairs.push([binding, binding.instance]);
|
|
412
|
+
this.#scope.deleteSingleton(binding);
|
|
425
413
|
}
|
|
426
414
|
}
|
|
427
415
|
}
|
|
@@ -429,14 +417,14 @@ class DefaultContainer implements Container {
|
|
|
429
417
|
}
|
|
430
418
|
|
|
431
419
|
#unloadModuleSync(ref: object): void {
|
|
432
|
-
const count = this.#moduleRefs
|
|
420
|
+
const count = this.#moduleRefs?.get(ref) ?? 0;
|
|
433
421
|
if (count <= 1) {
|
|
434
422
|
const reader = this.#getMetadataReader();
|
|
435
423
|
for (const [binding, instance] of this.#removeModuleBindings(ref)) {
|
|
436
424
|
this.#lifecycle.runDeactivationSync(binding, instance, reader);
|
|
437
425
|
}
|
|
438
426
|
} else {
|
|
439
|
-
this.#moduleRefs
|
|
427
|
+
this.#moduleRefs!.set(ref, count - 1);
|
|
440
428
|
}
|
|
441
429
|
}
|
|
442
430
|
|
|
@@ -448,14 +436,14 @@ class DefaultContainer implements Container {
|
|
|
448
436
|
}
|
|
449
437
|
|
|
450
438
|
async #unloadModuleAsync(ref: object): Promise<void> {
|
|
451
|
-
const count = this.#moduleRefs
|
|
439
|
+
const count = this.#moduleRefs?.get(ref) ?? 0;
|
|
452
440
|
if (count <= 1) {
|
|
453
441
|
const reader = this.#getMetadataReader();
|
|
454
442
|
for (const [binding, instance] of this.#removeModuleBindings(ref)) {
|
|
455
443
|
await this.#lifecycle.runDeactivation(binding, instance, reader);
|
|
456
444
|
}
|
|
457
445
|
} else {
|
|
458
|
-
this.#moduleRefs
|
|
446
|
+
this.#moduleRefs!.set(ref, count - 1);
|
|
459
447
|
}
|
|
460
448
|
}
|
|
461
449
|
|
|
@@ -549,11 +537,10 @@ class DefaultContainer implements Container {
|
|
|
549
537
|
|
|
550
538
|
// Deactivate all singletons in this container (own only)
|
|
551
539
|
const reader = this.#getMetadataReader();
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
}
|
|
540
|
+
// Iterate a copy: a deactivation handler is user code, and the live list is what
|
|
541
|
+
// materializing or dropping a singleton mutates.
|
|
542
|
+
for (const binding of this.#scope.cachedSingletons().slice()) {
|
|
543
|
+
await this.#lifecycle.runDeactivation(binding, binding.instance, reader);
|
|
557
544
|
}
|
|
558
545
|
|
|
559
546
|
this.#scope.clearAll();
|
|
@@ -577,7 +564,7 @@ class DefaultContainer implements Container {
|
|
|
577
564
|
continue;
|
|
578
565
|
}
|
|
579
566
|
const scope = effectiveBindingScope(binding);
|
|
580
|
-
if (scope === "singleton" &&
|
|
567
|
+
if (scope === "singleton" && binding.instance === NO_INSTANCE) {
|
|
581
568
|
if (binding.predicate !== undefined) {
|
|
582
569
|
continue;
|
|
583
570
|
}
|
|
@@ -777,22 +764,22 @@ class DefaultContainer implements Container {
|
|
|
777
764
|
|
|
778
765
|
has(token: Token<unknown> | Constructor, options?: ResolveOptions): boolean {
|
|
779
766
|
this.#assertNotDisposed();
|
|
780
|
-
return this.#
|
|
767
|
+
return this.#getInspector().has(token, options, () => this.#parent?.has(token, options) ?? false);
|
|
781
768
|
}
|
|
782
769
|
|
|
783
770
|
hasOwn(token: Token<unknown> | Constructor, options?: ResolveOptions): boolean {
|
|
784
771
|
this.#assertNotDisposed();
|
|
785
|
-
return this.#
|
|
772
|
+
return this.#getInspector().hasOwn(token, options);
|
|
786
773
|
}
|
|
787
774
|
|
|
788
775
|
lookupBindings<const Value>(token: Token<Value> | Constructor<Value>): ReadonlyArray<BindingSnapshot> {
|
|
789
776
|
this.#assertNotDisposed();
|
|
790
|
-
return this.#
|
|
777
|
+
return this.#getInspector().lookupBindings(token);
|
|
791
778
|
}
|
|
792
779
|
|
|
793
780
|
inspect(): ContainerSnapshot {
|
|
794
781
|
this.#assertNotDisposed();
|
|
795
|
-
return this.#
|
|
782
|
+
return this.#getInspector().inspect();
|
|
796
783
|
}
|
|
797
784
|
|
|
798
785
|
generateDependencyGraph(options?: GraphOptions): ContainerGraphJson {
|
package/src/decorators/inject.ts
CHANGED
|
@@ -34,10 +34,8 @@ export type InjectableDependency<Value = unknown> = Token<Value> | Constructor<V
|
|
|
34
34
|
/**
|
|
35
35
|
* The value a factory receives for one declared dependency.
|
|
36
36
|
*
|
|
37
|
-
* @remarks `optional()` and `injectAll()`
|
|
38
|
-
*
|
|
39
|
-
* — so reading that parameter back is enough; bare tokens and constructors fall through to
|
|
40
|
-
* {@link TokenValue}.
|
|
37
|
+
* @remarks `optional()` and `injectAll()` fold their effect into the descriptor's own type
|
|
38
|
+
* parameter, so reading it back is enough; bare tokens fall through to {@link TokenValue}.
|
|
41
39
|
*
|
|
42
40
|
* @since 0.5.0-canary.7
|
|
43
41
|
*/
|
|
@@ -195,7 +193,7 @@ export function inject<const Value>(
|
|
|
195
193
|
}
|
|
196
194
|
Object.defineProperties(decoratorFn, props);
|
|
197
195
|
|
|
198
|
-
return decoratorFn as
|
|
196
|
+
return decoratorFn as InjectionDescriptor<Value> & ClassAccessorDecorator<unknown, Value>;
|
|
199
197
|
}
|
|
200
198
|
|
|
201
199
|
// ── optional() ────────────────────────────────────────────────────────────────
|
package/src/errors.ts
CHANGED
|
@@ -212,6 +212,27 @@ export class MissingContainerContextError extends DiError {
|
|
|
212
212
|
}
|
|
213
213
|
}
|
|
214
214
|
|
|
215
|
+
/**
|
|
216
|
+
* A fluent chain was refined before a `to*()` call gave it a binding to refine.
|
|
217
|
+
*
|
|
218
|
+
* @remarks The builder types make this unreachable from TypeScript — `bind()` returns
|
|
219
|
+
* `BindToBuilder`, which exposes only `to*()`. It exists for JavaScript callers and for anyone who
|
|
220
|
+
* casts past the types, so the misuse fails loudly instead of mutating nothing.
|
|
221
|
+
*
|
|
222
|
+
* @since 0.5.0-canary.8
|
|
223
|
+
*/
|
|
224
|
+
export class ChainNotRegisteredError extends DiError {
|
|
225
|
+
readonly code = "CHAIN_NOT_REGISTERED";
|
|
226
|
+
readonly tokenName: string;
|
|
227
|
+
|
|
228
|
+
constructor(tokenName: string) {
|
|
229
|
+
super(
|
|
230
|
+
`Cannot refine the binding for token '${tokenName}' before choosing a target. Call a to*() method first — for example .to(SomeClass), .toConstantValue(value) or .toDynamic(factory).`,
|
|
231
|
+
);
|
|
232
|
+
this.tokenName = tokenName;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
215
236
|
/**
|
|
216
237
|
* @since 0.3.16-canary.0
|
|
217
238
|
*/
|
package/src/index.ts
CHANGED
|
@@ -63,7 +63,7 @@ export class Inspector {
|
|
|
63
63
|
const snapshots = this.allBindingSnapshots();
|
|
64
64
|
return {
|
|
65
65
|
ownBindings: snapshots,
|
|
66
|
-
cachedSingletonCount: this.#scope.
|
|
66
|
+
cachedSingletonCount: this.#scope.cachedSingletons().length,
|
|
67
67
|
hasParent: this.#hasParent,
|
|
68
68
|
isDisposed: this.#isDisposed(),
|
|
69
69
|
};
|
|
@@ -12,13 +12,10 @@ export const LIFECYCLE_KEY: unique symbol = Symbol("di:lifecycle");
|
|
|
12
12
|
export const INJECT_ACCESSOR_KEY: unique symbol = Symbol("di:inject-accessor");
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
|
-
* The
|
|
15
|
+
* The symbol TC39 Stage 3 decorator transforms store class metadata under.
|
|
16
16
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* `Symbol.for("Symbol.metadata")` — a global-registry symbol with the same string key.
|
|
20
|
-
* Resolving it here once keeps the reader and the decorator transforms in sync regardless
|
|
21
|
-
* of which path is taken.
|
|
17
|
+
* @remarks Falls back to the global-registry symbol, which is what Babel and esbuild emit until
|
|
18
|
+
* a runtime ships `Symbol.metadata` natively.
|
|
22
19
|
*
|
|
23
20
|
* @since 0.3.16-canary.0
|
|
24
21
|
*/
|
package/src/registry.ts
CHANGED
|
@@ -3,44 +3,6 @@ import { bindingSlotEquals, bindingSlotToString } from "#/binding";
|
|
|
3
3
|
import type { Token } from "#/token";
|
|
4
4
|
import type { BindingIdentifier, Constructor, DependencyKey } from "#/types";
|
|
5
5
|
|
|
6
|
-
// Superset of every binding kind's fields, used to normalize hidden classes below.
|
|
7
|
-
type BindingFieldSuperset = Binding & {
|
|
8
|
-
readonly scope?: unknown;
|
|
9
|
-
readonly target?: unknown;
|
|
10
|
-
readonly factory?: unknown;
|
|
11
|
-
readonly deps?: unknown;
|
|
12
|
-
readonly value?: unknown;
|
|
13
|
-
readonly onActivation?: unknown;
|
|
14
|
-
readonly onDeactivation?: unknown;
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Rebuilds a binding with every kind's fields in one fixed key order, so all stored
|
|
19
|
-
* bindings share a single V8 hidden class. Mixed binding kinds otherwise turn the
|
|
20
|
-
* resolver's hot property reads (kind/scope/factory/...) megamorphic, which costs
|
|
21
|
-
* ~30% throughput in processes that exercise many kinds. The value-erasing cast is
|
|
22
|
-
* safe: every declared field is copied verbatim and `kind` stays the discriminant.
|
|
23
|
-
*/
|
|
24
|
-
function normalizeBindingShape(binding: Binding): Binding {
|
|
25
|
-
const source = binding as BindingFieldSuperset;
|
|
26
|
-
return {
|
|
27
|
-
kind: source.kind,
|
|
28
|
-
id: source.id,
|
|
29
|
-
inFlight: false,
|
|
30
|
-
frame: undefined,
|
|
31
|
-
token: source.token,
|
|
32
|
-
slot: source.slot,
|
|
33
|
-
predicate: source.predicate,
|
|
34
|
-
scope: source.scope,
|
|
35
|
-
target: source.target,
|
|
36
|
-
factory: source.factory,
|
|
37
|
-
deps: source.deps,
|
|
38
|
-
value: source.value,
|
|
39
|
-
onActivation: source.onActivation,
|
|
40
|
-
onDeactivation: source.onDeactivation,
|
|
41
|
-
} as Binding;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
6
|
/**
|
|
45
7
|
* @since 0.3.16-canary.0
|
|
46
8
|
*/
|
|
@@ -51,22 +13,35 @@ export class BindingRegistry {
|
|
|
51
13
|
readonly #bindings = new Map<DependencyKey, Array<Binding>>();
|
|
52
14
|
// Fast lookup by binding ID
|
|
53
15
|
readonly #byId = new Map<BindingIdentifier, Binding>();
|
|
54
|
-
// Fast lookup for slot { name, tags: [] }
|
|
55
|
-
|
|
16
|
+
// Fast lookup for slot { name, tags: [] } — unallocated until a named binding is registered.
|
|
17
|
+
#simpleNamed: Map<DependencyKey, Map<string, Binding>> | undefined;
|
|
56
18
|
// Fast path for one default slot binding with no predicate
|
|
57
19
|
readonly #fastDefault = new Map<DependencyKey, Binding>();
|
|
58
|
-
// Fast lookup for slot { name: undefined, tags: [[key, value]] } with no predicate
|
|
59
|
-
|
|
20
|
+
// Fast lookup for slot { name: undefined, tags: [[key, value]] } with no predicate — likewise
|
|
21
|
+
// unallocated until a tagged binding is registered.
|
|
22
|
+
#simpleTagged: Map<DependencyKey, Map<string, Map<unknown, Binding>>> | undefined;
|
|
60
23
|
|
|
61
24
|
/** Monotonic version — increments on every mutation. */
|
|
62
25
|
get version(): number {
|
|
63
26
|
return this.#version;
|
|
64
27
|
}
|
|
65
28
|
|
|
66
|
-
/**
|
|
67
|
-
|
|
29
|
+
/**
|
|
30
|
+
* Registers a mutation the indexes don't care about (a fluent chain refining scope or an
|
|
31
|
+
* activation hook in place), so version-stamped resolver caches still invalidate.
|
|
32
|
+
*/
|
|
33
|
+
touch(): void {
|
|
34
|
+
this.#version += 1;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Add or replace binding using slot-aware last-wins. Returns the displaced binding, if any.
|
|
39
|
+
*
|
|
40
|
+
* @remarks The binding is stored by reference — it must come from `createBinding`, which is
|
|
41
|
+
* what guarantees the single hidden class the resolver's hot reads depend on.
|
|
42
|
+
*/
|
|
43
|
+
add(binding: Binding): Binding | undefined {
|
|
68
44
|
this.#version += 1;
|
|
69
|
-
const binding = normalizeBindingShape(uncommittedBinding);
|
|
70
45
|
const key = binding.token as DependencyKey;
|
|
71
46
|
// ✓ TS6.0: Map.getOrInsert (ES2025) replaces the manual get+check+set upsert
|
|
72
47
|
const bindingsForToken = this.#bindings.getOrInsert(key, []);
|
|
@@ -100,8 +75,8 @@ export class BindingRegistry {
|
|
|
100
75
|
const key = token as DependencyKey;
|
|
101
76
|
const bindingsForToken = this.#bindings.get(key) ?? [];
|
|
102
77
|
this.#bindings.delete(key);
|
|
103
|
-
this.#simpleNamed
|
|
104
|
-
this.#simpleTagged
|
|
78
|
+
this.#simpleNamed?.delete(key);
|
|
79
|
+
this.#simpleTagged?.delete(key);
|
|
105
80
|
this.#fastDefault.delete(key);
|
|
106
81
|
for (const binding of bindingsForToken) {
|
|
107
82
|
this.#byId.delete(binding.id);
|
|
@@ -128,8 +103,8 @@ export class BindingRegistry {
|
|
|
128
103
|
this.#deindexSimpleTaggedBinding(key, binding);
|
|
129
104
|
if (bindingsForToken.length === 0) {
|
|
130
105
|
this.#bindings.delete(key);
|
|
131
|
-
this.#simpleNamed
|
|
132
|
-
this.#simpleTagged
|
|
106
|
+
this.#simpleNamed?.delete(key);
|
|
107
|
+
this.#simpleTagged?.delete(key);
|
|
133
108
|
this.#fastDefault.delete(key);
|
|
134
109
|
} else {
|
|
135
110
|
this.#refreshFastDefaultForToken(key);
|
|
@@ -170,19 +145,19 @@ export class BindingRegistry {
|
|
|
170
145
|
const all = this.allBindings();
|
|
171
146
|
this.#bindings.clear();
|
|
172
147
|
this.#byId.clear();
|
|
173
|
-
this.#simpleNamed
|
|
174
|
-
this.#simpleTagged
|
|
148
|
+
this.#simpleNamed?.clear();
|
|
149
|
+
this.#simpleTagged?.clear();
|
|
175
150
|
this.#fastDefault.clear();
|
|
176
151
|
return all;
|
|
177
152
|
}
|
|
178
153
|
|
|
179
154
|
getSimpleNamed(token: Token<unknown> | Constructor, name: string): Binding | undefined {
|
|
180
|
-
return this.#simpleNamed
|
|
155
|
+
return this.#simpleNamed?.get(token as DependencyKey)?.get(name);
|
|
181
156
|
}
|
|
182
157
|
|
|
183
158
|
getSimpleTagged(token: Token<unknown> | Constructor, tagKey: string, tagValue: unknown): Binding | undefined {
|
|
184
159
|
return this.#simpleTagged
|
|
185
|
-
|
|
160
|
+
?.get(token as DependencyKey)
|
|
186
161
|
?.get(tagKey)
|
|
187
162
|
?.get(tagValue);
|
|
188
163
|
}
|
|
@@ -203,7 +178,7 @@ export class BindingRegistry {
|
|
|
203
178
|
return;
|
|
204
179
|
}
|
|
205
180
|
const [tagKey, tagValue] = slot.tags[0]!;
|
|
206
|
-
const byTagKey = this.#simpleTagged.getOrInsert(tokenKey, new Map<string, Map<unknown, Binding>>());
|
|
181
|
+
const byTagKey = (this.#simpleTagged ??= new Map()).getOrInsert(tokenKey, new Map<string, Map<unknown, Binding>>());
|
|
207
182
|
const byTagValue = byTagKey.getOrInsert(tagKey, new Map<unknown, Binding>());
|
|
208
183
|
byTagValue.set(tagValue, binding);
|
|
209
184
|
}
|
|
@@ -214,7 +189,7 @@ export class BindingRegistry {
|
|
|
214
189
|
return;
|
|
215
190
|
}
|
|
216
191
|
const [tagKey, tagValue] = slot.tags[0]!;
|
|
217
|
-
const byTagKey = this.#simpleTagged
|
|
192
|
+
const byTagKey = this.#simpleTagged?.get(tokenKey);
|
|
218
193
|
if (byTagKey === undefined) {
|
|
219
194
|
return;
|
|
220
195
|
}
|
|
@@ -228,7 +203,7 @@ export class BindingRegistry {
|
|
|
228
203
|
if (byTagValue.size === 0) {
|
|
229
204
|
byTagKey.delete(tagKey);
|
|
230
205
|
if (byTagKey.size === 0) {
|
|
231
|
-
this.#simpleTagged
|
|
206
|
+
this.#simpleTagged!.delete(tokenKey);
|
|
232
207
|
}
|
|
233
208
|
}
|
|
234
209
|
}
|
|
@@ -247,7 +222,7 @@ export class BindingRegistry {
|
|
|
247
222
|
if (slot.name === undefined || slot.tags.length > 0) {
|
|
248
223
|
return;
|
|
249
224
|
}
|
|
250
|
-
const bindingsByName = this.#simpleNamed.getOrInsert(tokenKey, new Map<string, Binding>());
|
|
225
|
+
const bindingsByName = (this.#simpleNamed ??= new Map()).getOrInsert(tokenKey, new Map<string, Binding>());
|
|
251
226
|
bindingsByName.set(slot.name, binding);
|
|
252
227
|
}
|
|
253
228
|
|
|
@@ -256,7 +231,7 @@ export class BindingRegistry {
|
|
|
256
231
|
if (slot.name === undefined || slot.tags.length > 0) {
|
|
257
232
|
return;
|
|
258
233
|
}
|
|
259
|
-
const bindingsByName = this.#simpleNamed
|
|
234
|
+
const bindingsByName = this.#simpleNamed?.get(tokenKey);
|
|
260
235
|
if (bindingsByName === undefined) {
|
|
261
236
|
return;
|
|
262
237
|
}
|
|
@@ -264,7 +239,7 @@ export class BindingRegistry {
|
|
|
264
239
|
if (currentBinding?.id === binding.id) {
|
|
265
240
|
bindingsByName.delete(slot.name);
|
|
266
241
|
if (bindingsByName.size === 0) {
|
|
267
|
-
this.#simpleNamed
|
|
242
|
+
this.#simpleNamed!.delete(tokenKey);
|
|
268
243
|
}
|
|
269
244
|
}
|
|
270
245
|
}
|
|
@@ -283,4 +258,8 @@ export class BindingRegistry {
|
|
|
283
258
|
}
|
|
284
259
|
this.#fastDefault.set(tokenKey, onlyBinding);
|
|
285
260
|
}
|
|
261
|
+
/** Whether the deferred table behind `#simpleNamed` has had to be built. */
|
|
262
|
+
get isBuilt(): boolean {
|
|
263
|
+
return this.#simpleNamed !== undefined;
|
|
264
|
+
}
|
|
286
265
|
}
|