@fnioc/di 2.0.0 → 3.0.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/README.md CHANGED
@@ -58,7 +58,7 @@ services.add("pkg:ICache", {
58
58
 
59
59
  A `useFactory` with `scope: "singleton"` runs once and caches the result; without a `scope` it runs on every resolve (transient). `useValue` is always the same reference.
60
60
 
61
- The same two specs are available scope-locally via `scope.add(token, spec)`, so a single scope (e.g. a test scope) can swap an implementation without rebuilding the builder.
61
+ To override a registration for a specific context (e.g. a test double), register a later spec for the same token on the `DiBuilder` before calling `build()`. The registration map is append-only and last-registration-wins, so a later `.add(token, ...)` / `.addFactory(token, ...)` / `.addValue(token, ...)` call shadows the earlier one without deleting it. The map seals at `build()` — no post-build mutation is possible.
62
62
 
63
63
  ---
64
64
 
@@ -272,16 +272,19 @@ Note: `FactoryTargetError` is thrown when the factory callable is constructed (a
272
272
  | `add<I>(Concrete)` | `(ctor: new (...) => I) => AddBuilder` | Register a concrete class against interface `I`. |
273
273
  | `.as<S>()` | `(scope: S) → void` | Set the lifetime scope. No call → transient. |
274
274
  | `add(token, ctor)` | `(token: string, ctor) => AddBuilder` | Class registration (lowered form). |
275
- | `add(token, spec)` | `(token: string, { useFactory, scope? } \| { useValue }) => this` | Factory / value registration. No dep metadata required. |
276
- | `build()` | `() => Scope<Root \| Children>` | Mint the root scope (named `Root`). No argument. |
275
+ | `addFactory(token, factory)` | `(token: string, factory: (sp: Resolver) => T) => AddBuilder` | Factory registration. No dep metadata required — the factory receives the live `Resolver`. |
276
+ | `addValue(token, value)` | `(token: string, value: unknown) => void` | Value registration. A pre-built instance, re-used as-is. |
277
+ | `build()` | `() => ServiceProvider<Root \| Children>` | Seal the registration map and mint the root `ServiceProvider`. No post-build mutation is possible. |
277
278
 
278
- ### `Scope<Scopes>`
279
+ ### `ServiceProvider<Scopes>`
280
+
281
+ Implements `Resolver` + `ScopeFactory` + `Disposable` / `AsyncDisposable`.
279
282
 
280
283
  | Member | Signature | Description |
281
284
  |---|---|---|
282
- | `createScope(name)` | `(name: Scopes) => Scope<Scopes>` | Create a nested child scope. |
283
- | `add(token, spec)` | `(token, { useFactory, scope? } \| { useValue }) => this` | Scope-local override registration. |
284
285
  | `resolve<T>(token)` | `(token: string) => T` | Resolve an instance. Throws on captive-dep violation, missing scope ancestor, or cycle. |
286
+ | `resolveFactory(token)` | `(token: string) => (...args) => T` | Resolve a factory callable for the token rather than an instance. |
287
+ | `createScope(name)` | `(name: Scopes) => ServiceProvider<Scopes>` | Create a nested child scope. |
285
288
  | `dispose()` | `() => void` | Sync close. Throws if any owned instance has async-only disposal. |
286
289
  | `disposeAsync()` | `() => Promise<void>` | Async close. |
287
290
  | `[Symbol.dispose]()` | — | Native `using` support. |
package/dist/index.d.ts CHANGED
@@ -1,3 +1,5 @@
1
+ type Func$1<in Args extends readonly any[] = any[], out Return = any> = (...args: Args) => Return;
2
+
1
3
  interface Ctor$1<in Args extends readonly any[] = any[], out Instance = any> {
2
4
  new(...args: Args): Instance;
3
5
  prototype: Instance;
@@ -28,7 +30,7 @@ declare const hole: null;
28
30
  * `never[]` rest keeps any concrete function assignable here regardless of its
29
31
  * own parameter list.
30
32
  */
31
- type DepTarget = Ctor$1 | ((...args: never[]) => unknown);
33
+ type DepTarget = Ctor$1 | Func$1<never[], unknown>;
32
34
  /**
33
35
  * A stable string identifying an interface — the DI key.
34
36
  *
@@ -115,7 +117,7 @@ declare function defineDeps(target: DepTarget, signatures: readonly (readonly De
115
117
  * }
116
118
  * ```
117
119
  */
118
- declare function signature(...tokens: readonly DepSlot[]): (value: Ctor$1, _context: ClassDecoratorContext) => void;
120
+ declare function signature(...tokens: readonly DepSlot[]): Func$1<[Ctor$1, ClassDecoratorContext], void>;
119
121
 
120
122
  /**
121
123
  * The builder returned by `forCtor`. Chainable — each `.signature()` call
@@ -145,6 +147,8 @@ interface ForCtorBuilder {
145
147
  */
146
148
  declare function forCtor(ctor: Ctor$1): ForCtorBuilder;
147
149
 
150
+ type Func<in Args extends readonly any[] = any[], out Return = any> = (...args: Args) => Return;
151
+
148
152
  interface Ctor<in Args extends readonly any[] = any[], out Instance = any> {
149
153
  new(...args: Args): Instance;
150
154
  prototype: Instance;
@@ -154,15 +158,15 @@ interface Ctor<in Args extends readonly any[] = any[], out Instance = any> {
154
158
  * A registration-level factory function. Its parameters are filled by the
155
159
  * engine at resolve time, the same way a class constructor's are: a factory
156
160
  * WITH a `defineDeps` record has each parameter resolved by its slot (token →
157
- * resolved instance, `ScopeRef` → the live scope, hole → caller-supplied); a
161
+ * resolved instance, `ScopeRef` → the live provider, hole → caller-supplied); a
158
162
  * factory WITHOUT a record is the plugin-less escape hatch and is called with
159
- * the live scope as its single argument (`(scope) => …`).
163
+ * the live provider as its single argument (`(sp) => …`).
160
164
  *
161
165
  * May be async — it can return a `Promise<T>`. The container never awaits; the
162
166
  * Promise flows through the sync resolution channel as a value (§"Async as
163
167
  * values"). A consumer that depends on it declares `Promise<T>` and awaits.
164
168
  */
165
- type Factory = (...args: any[]) => unknown;
169
+ type Factory = Func<any[], unknown>;
166
170
  /** A class registration: a token bound to a concrete constructor. */
167
171
  interface ClassRegistration {
168
172
  readonly kind: "class";
@@ -192,18 +196,24 @@ interface ValueRegistration {
192
196
  /** Any registration the engine can resolve. */
193
197
  type Registration = ClassRegistration | FactoryRegistration | ValueRegistration;
194
198
  /**
195
- * The resolution surface a factory receives either as an injected `ScopeRef`
196
- * parameter, or (plugin-less escape hatch) as the sole argument of a
197
- * record-less factory. A structural subset of `Scope`: resolve further tokens
198
- * and open child scopes.
199
+ * The named lifetime tag for a registration. `"singleton"` and `"transient"`
200
+ * are the built-in names; `U` is the user-declared scope-name union (defaults
201
+ * to `"scoped"`). Transient is represented by the ABSENCE of a lifetime tag
202
+ * (`undefined` on the registration), not by the string `"transient"`.
203
+ */
204
+ type Lifetime<U extends string = "scoped"> = "singleton" | "transient" | U;
205
+ /**
206
+ * The minimal resolution surface — resolve tokens and get factories. Injected
207
+ * into factory parameters typed `Resolver` (and for the plugin-less escape
208
+ * hatch as the sole argument of a record-less factory).
199
209
  *
200
- * `resolve` has three shapes:
201
- * - `resolve<T>()` tokenless; the transformer lowers it to a token.
210
+ * `resolve` has two published shapes (the tokenless authoring form
211
+ * `resolve<T>()` is a PURE TYPING contributed by the `@fnioc/transformer`
212
+ * augmentation, not part of di's published surface):
202
213
  * - `resolve<T>(token)` — explicit token, typed return.
203
214
  * - `resolve(token)` — explicit token, `unknown` return (dynamic).
204
215
  */
205
- interface ResolveScope {
206
- resolve<T>(): T;
216
+ interface Resolver {
207
217
  resolve<T>(token: Token): T;
208
218
  resolve(token: Token): unknown;
209
219
  /**
@@ -212,76 +222,94 @@ interface ResolveScope {
212
222
  * (a function-typed type arg) lowers to this.
213
223
  */
214
224
  resolveFactory(token: Token): unknown;
215
- createScope(name: string): ResolveScope;
225
+ }
226
+ /**
227
+ * The scope-creation surface. Injected into factory parameters typed
228
+ * `ScopeFactory`, and implemented by `ServiceProvider`.
229
+ */
230
+ interface ScopeFactory<S extends string = string> {
231
+ createScope(...args: "scoped" extends S ? [name?: S] : [name: S]): ServiceProvider<S>;
232
+ }
233
+ /**
234
+ * @deprecated Use `Resolver` instead. Kept for backwards compatibility.
235
+ *
236
+ * The resolution surface a factory receives — either as an injected `ScopeRef`
237
+ * parameter, or (plugin-less escape hatch) as the sole argument of a
238
+ * record-less factory.
239
+ */
240
+ interface ResolveScope extends Resolver {
241
+ createScope(name: string): ServiceProvider;
216
242
  }
217
243
 
218
244
  /**
219
- * A node in the scope chain. Created from a `DiBuilder` (the root) or from a
220
- * parent scope (`.createScope`). Holds the instances it owns and any local
221
- * override registrations.
245
+ * A scope frame — a node in the parent-linked chain. Holds this scope's name,
246
+ * its instance cache, an ordered list for disposal, and an optional parent.
247
+ * It does NOT hold registrations (those live sealed on the ServiceProvider).
222
248
  *
223
- * The generic `Scopes` is the user's scope-name union, threaded so
224
- * `.createScope` only accepts declared names.
249
+ * The special "no frame" on the root ServiceProvider means transient-only /
250
+ * unscoped resolution attempting to resolve a scoped registration from the
251
+ * root will throw MissingScopeError.
225
252
  */
226
- declare class Scope<Scopes extends string = string> implements ResolveScope {
227
- /** This scope's name. The root scope's name is its lifetime. */
228
- readonly name: Scopes;
229
- /** The parent scope, or `undefined` for the root. */
230
- private readonly parent;
231
- /** The builder's base registration map (shared, walked last). */
232
- private readonly baseRegistrations;
233
- /**
234
- * Local override registrations held at this scope (shadow ancestors). Each
235
- * token maps to a LIST in registration order; the most-recent (last) entry
236
- * wins, mirroring the builder's service collection.
237
- */
238
- private readonly localRegistrations;
253
+ declare class Scope {
254
+ /** This scope's name must match the registration's lifetime tag. */
255
+ readonly name: string;
256
+ /** The parent scope, or omitted for the topmost frame. */
257
+ readonly parent?: Scope | undefined;
239
258
  /** Instances this scope owns and caches, keyed by token. */
240
- private readonly instances;
259
+ readonly cache: Map<Token, unknown>;
241
260
  /** Owned instances in construction order — disposed in reverse. */
242
- private readonly ownedOrder;
243
- private disposed;
261
+ readonly owned: unknown[];
244
262
  constructor(
245
- /** This scope's name. The root scope's name is its lifetime. */
246
- name: Scopes,
247
- /** The parent scope, or `undefined` for the root. */
248
- parent: Scope<Scopes> | undefined,
249
- /** The builder's base registration map (shared, walked last). */
250
- baseRegistrations: ReadonlyMap<Token, Registration[]>);
251
- /** Appends a registration to a token's list in the given map. */
252
- private static appendTo;
253
- /**
254
- * Creates a parent-linked child scope with the given (declared) name. Scopes
255
- * MUST nest this parent chain IS the lifetime hierarchy. The root is minted
256
- * by `DiBuilder.build()`; every other scope descends from one via this call.
257
- */
258
- createScope(childName: Scopes): Scope<Scopes>;
263
+ /** This scope's name must match the registration's lifetime tag. */
264
+ name: string,
265
+ /** The parent scope, or omitted for the topmost frame. */
266
+ parent?: Scope | undefined);
267
+ }
268
+ /**
269
+ * The public container surface. Implements `Resolver` (resolve + resolveFactory)
270
+ * and `ScopeFactory` (createScope), plus native `Disposable`/`AsyncDisposable`.
271
+ *
272
+ * `S` is the user-declared scope-name union. The root (`DiBuilder.build()`)
273
+ * has an EMPTY scope slot it acts as the unscoped root that owns singletons
274
+ * when the root name is "singleton", reached by the first `createScope("singleton")`.
275
+ * Wait — actually the root SP from build() DOES have a scope frame (named after
276
+ * the builder's rootName), exactly as before: `build()` creates a root SP
277
+ * with `new Scope(rootName)` as its frame.
278
+ */
279
+ declare class ServiceProvider<S extends string = string> implements Resolver, ScopeFactory<S>, Disposable, AsyncDisposable {
280
+ /** The sealed registration map (shared across all providers in the tree). */
281
+ private readonly registrations;
282
+ private disposed;
259
283
  /**
260
- * Appends a scopeless `class`/`factory` LOCAL override and returns the
261
- * `.as(scope?)` continuation (mirrors `DiBuilder.appendScoped`). Local
262
- * overrides shadow any ancestor or base registration for the same token, for
263
- * this scope and its descendants only, most-recent-wins.
284
+ * The scope frame for this provider. `undefined` means this is the "unscoped"
285
+ * root a sentinel that exists only for transient-only trees (no build()
286
+ * call sets this to undefined in normal usage; build() always sets a root name).
264
287
  */
265
- private appendScopedLocal;
288
+ private readonly frame;
289
+ constructor(
290
+ /** The sealed registration map (shared across all providers in the tree). */
291
+ registrations: ReadonlyMap<Token, Registration[]>,
292
+ /** This provider's scope frame, if any. */
293
+ frame?: Scope);
266
294
  /**
267
- * Registers a scope-local CLASS override shadows ancestors for this scope
268
- * and its descendants. Returns the `.as(scope?)` continuation.
295
+ * The name of this provider's scope frame. Throws if the provider has no
296
+ * frame (unscoped root). Kept for backwards-compatibility with tests that
297
+ * inspect `root.name`.
269
298
  */
270
- add(token: Token, ctor: Ctor): AddBuilder<Scopes>;
299
+ get name(): S;
271
300
  /**
272
- * Registers a scope-local FACTORY override. Parameter injection follows the
273
- * same metadata rule as a builder factory (record inject; record-less
274
- * called with the live scope). Returns the `.as(scope?)` continuation.
301
+ * Creates a child `ServiceProvider` whose scope frame is a new `Scope` named
302
+ * `name`, parented to this provider's frame (or a top-level frame if this
303
+ * provider is unscoped).
304
+ *
305
+ * Default name `"scoped"` is accepted only when `"scoped"` ∈ S (the
306
+ * conditional-rest-param type ensures this at the call site).
275
307
  */
276
- addFactory(token: Token, factory: (scope: ResolveScope) => unknown): AddBuilder<Scopes>;
277
- /** Registers a scope-local VALUE override — the instance itself, no lifetime. */
278
- addValue(token: Token, value: unknown): this;
308
+ createScope(...args: "scoped" extends S ? [name?: S] : [name: S]): ServiceProvider<S>;
279
309
  /**
280
- * Resolves a token to an instance, walking the parent chain for both the
281
- * registration and the owning scope. The public entry point starts a fresh
282
- * cycle-detection stack.
310
+ * Resolves a token to an instance, walking the scope chain for the owning
311
+ * frame. The public entry point starts a fresh cycle-detection stack.
283
312
  */
284
- resolve<T>(): T;
285
313
  resolve<T>(token: Token): T;
286
314
  resolve(token: Token): unknown;
287
315
  /**
@@ -294,63 +322,54 @@ declare class Scope<Scopes extends string = string> implements ResolveScope {
294
322
  */
295
323
  resolveFactory(token: Token): unknown;
296
324
  /**
297
- * Walks UP the chain (this scope's locals ancestors' locals → base map),
298
- * returning the nearest registration for `token`. Child shadows parent, and
299
- * within any one scope's list the most-recent (last) registration wins — so a
300
- * later `.add()`/`.add(...)` override beats an earlier one without deletion.
325
+ * Returns the most-recent registration for `token` from the sealed map.
326
+ * The sealed map is shared across all providers in the tree; local overrides
327
+ * are not supported in the new model (scope-local registration is deleted).
301
328
  */
302
329
  private lookup;
303
330
  /**
304
- * Finds the nearest ancestor scope (inclusive of this one) whose name matches
305
- * `scope`, walking UP the chain. Returns `undefined` when none matches.
331
+ * Finds the nearest ancestor scope frame (inclusive) whose name matches
332
+ * `scopeName`, walking UP the chain. Returns `undefined` when none matches.
306
333
  */
307
- private findOwner;
308
- /** The chain of scope names from this scope up to the root, for diagnostics. */
309
- private chainNames;
334
+ private static findOwner;
335
+ /** The chain of scope names from `vantage` up to the root, for diagnostics. */
336
+ private static chainNames;
310
337
  /**
311
- * The internal resolver. `stack` is the active resolution path (for cycle
312
- * detection); it is shared across the whole `resolve()` call but never across
313
- * separate calls.
338
+ * The internal resolver. `vantage` is the scope frame the walk starts from.
339
+ * `stack` is the active resolution path (for cycle detection); it is shared
340
+ * across the whole `resolve()` call but never across separate calls.
314
341
  */
315
342
  private resolveWith;
316
343
  /**
317
- * Builds an instance for `registration`. `owningScope` is the scope whose
318
- * chain the dependencies are resolved against — THE critical rule. For a
319
- * factory override that is the scope passed to the closure; for a class it is
320
- * the scope its ctor deps resolve relative to.
344
+ * Builds an instance for `registration`. `owningFrame` is the scope frame
345
+ * whose chain the dependencies are resolved against — THE critical rule.
321
346
  */
322
347
  private instantiate;
323
348
  /**
324
- * The resolution view a factory receives `resolve` (overloaded; a tokenless
325
- * authored `resolve<T>()` is lowered to a token before runtime),
326
- * `resolveFactory`, and `createScope`, all continuing the active cycle
327
- * `stack` and resolving relative to THIS (the owning) scope.
349
+ * The resolution view injected for a `ScopeRef` parameter (`Resolver` or
350
+ * `ScopeFactory` typed param). Produces a ServiceProvider-like view that
351
+ * continues the active cycle `stack` and resolves relative to `owningFrame`.
328
352
  */
329
- private makeScopeView;
353
+ private makeProviderView;
330
354
  /**
331
355
  * Invokes a factory registration under the metadata-vs-scope rule:
332
356
  * - factory WITH a `defineDeps` record → resolve each slot (token →
333
- * resolved instance, `ScopeRef` → the live scope, `FactoryRef` → an
334
- * injected callable) and call `factory(...args)`;
357
+ * resolved instance, `ScopeRef` → the live provider view, `FactoryRef` →
358
+ * an injected callable) and call `factory(...args)`;
335
359
  * - factory WITHOUT a record (the plugin-less escape hatch) → call
336
- * `factory(scopeView)` with the live scope as its sole argument.
337
- * Deps resolve relative to `this` (the owning scope) — §5.4.
360
+ * `factory(providerView)` with the live provider view as its sole argument.
361
+ * Deps resolve relative to `owningFrame` (the owning scope) — §5.4.
338
362
  */
339
363
  private invokeFactory;
340
364
  /**
341
- * Constructs a class instance on a DIRECT resolve, resolving its constructor
342
- * dependencies relative to THIS scope (the owning scope). Performs greedy
343
- * signature selection over the ctor's DepRecord, then fills each slot:
365
+ * Constructs a class instance, resolving its constructor dependencies
366
+ * relative to `owningFrame`. Performs greedy signature selection over the
367
+ * ctor's DepRecord, then fills each slot:
344
368
  *
345
- * - a string token → resolved through this scope's chain (selection
369
+ * - a string token → resolved through the owning frame's chain (selection
346
370
  * guarantees every string-token slot here is resolvable);
347
371
  * - a `FactoryRef` → injected as a callable (see `makeFactory`);
348
- * - a `ScopeRef` → the live scope.
349
- *
350
- * A hole never reaches here on a direct resolve: it is an unresolvable token,
351
- * so selection rejects any signature carrying one (falling to a shorter
352
- * optional-overload, or throwing). Holes are filled by the caller only when
353
- * the class is a factory target — see `buildPartitioned`.
372
+ * - a `ScopeRef` → the live provider view.
354
373
  */
355
374
  private construct;
356
375
  /**
@@ -360,62 +379,41 @@ declare class Scope<Scopes extends string = string> implements ResolveScope {
360
379
  * registration map: each slot that is a registered token is resolved; each
361
380
  * slot that is an unregistered token or a `hole` takes the next
362
381
  * caller-supplied argument, positionally. The injected callable therefore
363
- * exposes only the target's unregistered parameters, in their relative order
364
- * — no Ramda-style placeholders, no leaked constructor arity.
382
+ * exposes only the target's unregistered parameters, in their relative order.
365
383
  *
366
384
  * Lifetime semantics:
367
- * - A ZERO-ARG factory (the target ctor has no holes / unregistered params)
368
- * routes the build through the normal `resolve` path, so it RESPECTS the
369
- * target's registered lifetime: a singleton target yields the same
370
- * instance on every call; a transient target yields a fresh one.
371
- * - A PARAMETERIZED factory (the target has holes / unregistered params
372
- * filled per call) constructs a FRESH instance on every call and BYPASSES
373
- * the instance cache. Caller args differ per call, so caching would be
374
- * wrong — two calls with different arguments must not collapse to one
375
- * cached instance.
385
+ * - A ZERO-ARG factory routes through the normal `resolve` path, so it
386
+ * RESPECTS the target's registered lifetime.
387
+ * - A PARAMETERIZED factory constructs a FRESH instance on every call and
388
+ * BYPASSES the instance cache. Caller args differ per call, so caching
389
+ * would be wrong.
376
390
  *
377
- * The closure captures `this` as the owning scope. §5.4 holds at call time:
378
- * the target's deps resolve relative to the scope that owns the
379
- * factory-holding instance, exactly as a direct resolve would — so a factory
380
- * captured by a singleton that tries to build a request-scoped target still
381
- * throws `MissingScopeError` when invoked.
391
+ * The closure captures `owningFrame`. §5.4 holds at call time: the target's
392
+ * deps resolve relative to the scope that owns the factory-holding instance.
382
393
  */
383
394
  private makeFactory;
384
395
  /**
385
396
  * Builds a factory target, partitioning its already-selected signature
386
397
  * against the live registration map: a registered token is resolved; a
387
- * `ScopeRef` is the live scope; a `FactoryRef` is injected; an unregistered
388
- * token or a `hole` takes the next caller-supplied argument positionally. A
389
- * class target is `new`ed, a factory target is called. Always a fresh result
390
- * — a parameterized factory bypasses the instance cache (caller args differ
391
- * per call). Runs on a fresh cycle stack since the factory is invoked outside
392
- * the original resolve.
398
+ * `ScopeRef` is the live provider view; a `FactoryRef` is injected; an
399
+ * unregistered token or a `hole` takes the next caller-supplied argument
400
+ * positionally. A class target is `new`ed, a factory target is called.
401
+ * Always a fresh result — a parameterized factory bypasses the instance cache.
402
+ * Runs on a fresh cycle stack since the factory is invoked outside the
403
+ * original resolve.
393
404
  */
394
405
  private buildPartitioned;
395
406
  /**
396
407
  * Greedy signature selection. Scans signatures longest → shortest and returns
397
408
  * the first SATISFIABLE one. A slot is satisfiable when it is:
398
409
  *
399
- * - a `FactoryRef` — always satisfiable; injected as a callable. The
400
- * factory's target need not be resolvable for the slot to count (an
401
- * unregistered target surfaces a `FactoryTargetError` when the factory is
402
- * built / called, not here);
403
- * - a `ScopeRef` — always satisfiable; filled with the live scope; or
404
- * - a string token whose registration is resolvable in this (the owning)
405
- * scope's chain.
406
- *
407
- * A `hole` (`null`) is NOT special — it is an unregistered token, so it is
408
- * unsatisfiable on a direct resolve. A class with an optional/defaulted param
409
- * carries a shorter "without that arg" overload (emitted by the transformer);
410
- * greedy selection falls to it when the longer one can't be satisfied, so the
411
- * default / `undefined` applies. A required unfilled param has no such shorter
412
- * overload and surfaces a `NoSatisfiableSignatureError` (build it via a
413
- * factory instead). A signature is satisfiable iff every string-token slot is
414
- * resolvable.
410
+ * - a `FactoryRef` — always satisfiable; injected as a callable;
411
+ * - a `ScopeRef` always satisfiable; filled with the live provider view; or
412
+ * - a string token whose registration exists in the sealed map.
415
413
  *
416
- * - Equal-arity ties break by registration order (the order signatures appear
417
- * in the DepRecord), which `sort`'s stability preserves.
418
- * - None satisfiable ⇒ throw naming the unsatisfiable tokens.
414
+ * A `hole` (`null`) is NOT satisfiable on a direct resolve. An unregistered
415
+ * string token is also not satisfiable. Equal-arity ties break by registration
416
+ * order. None satisfiable ⇒ throw naming the unsatisfiable tokens.
419
417
  */
420
418
  private selectSignature;
421
419
  /**
@@ -423,19 +421,19 @@ declare class Scope<Scopes extends string = string> implements ResolveScope {
423
421
  * there is no resolvability gate: a target's unregistered tokens are not
424
422
  * unsatisfiable — they are the factory's caller-supplied parameters. So the
425
423
  * choice is purely the longest signature, equal-arity ties broken by
426
- * registration order (`sort` stability). Always returns a signature (the
427
- * caller has already checked `signatures.length > 0`).
424
+ * registration order.
428
425
  */
429
426
  private selectTargetSignature;
430
427
  /**
431
- * True when `slot` is a registered string token somewhere in this scope's
432
- * chain. A hole (`null`), `FactoryRef`, or `ScopeRef` is not a registered
433
- * token, so it is never "resolvable" in this sense.
428
+ * True when `slot` is a registered string token in the sealed map. A hole
429
+ * (`null`), `FactoryRef`, or `ScopeRef` is not a registered token, so it is
430
+ * never "resolvable" in this sense.
434
431
  */
435
432
  private isResolvable;
436
433
  /**
437
- * Closes this scope synchronously, disposing the instances it owns in REVERSE
438
- * construction order. Only native `Disposable` instances are disposed.
434
+ * Closes this provider synchronously, disposing the instances its scope frame
435
+ * owns in REVERSE construction order. Only native `Disposable` instances are
436
+ * disposed. NO cascade to child scopes.
439
437
  *
440
438
  * Throws `AsyncDisposalRequiredError` if any owned instance is a Promise
441
439
  * (thenable) — a pending Promise cannot be disposed synchronously; the caller
@@ -443,9 +441,9 @@ declare class Scope<Scopes extends string = string> implements ResolveScope {
443
441
  */
444
442
  dispose(): void;
445
443
  /**
446
- * Closes this scope asynchronously. Awaits each owned Promise-valued instance
447
- * first (so an async factory's result settles before teardown), then disposes
448
- * owned instances in REVERSE construction order — honoring both
444
+ * Closes this provider asynchronously. Awaits each owned Promise-valued
445
+ * instance first (so an async factory's result settles before teardown), then
446
+ * disposes owned instances in REVERSE construction order — honoring both
449
447
  * `Symbol.asyncDispose` and `Symbol.dispose`. Idempotent.
450
448
  */
451
449
  disposeAsync(): Promise<void>;
@@ -467,23 +465,17 @@ declare class Scope<Scopes extends string = string> implements ResolveScope {
467
465
  */
468
466
  interface AddBuilder<Scopes extends string> {
469
467
  /**
470
- * Attaches the lifetime. Must name a declared scope.
468
+ * Attaches the lifetime — the RUNTIME (lowered) form. Must name a declared
469
+ * scope.
471
470
  *
472
- * Two call shapes, by design (PRD §7):
473
- * - AUTHORED `.as<"singleton">()` the scope name is a TYPE argument; the
474
- * `S extends Scopes` bound is the compile-time captive-misconfiguration
475
- * guard. No value argument is passed; this form is never executed (the
476
- * transformer rewrites it before it runs).
477
- * - LOWERED `.as("singleton")` the transformer rewrites the type
478
- * argument to a value argument. This is the form the engine executes; the
479
- * runtime reads the scope from the value arg.
480
- *
481
- * `scope` is therefore OPTIONAL at the type level: the authored form supplies
482
- * it as a type arg only, the lowered form as a value. A bare `.as()` with no
483
- * type arg leaves `S = Scopes` and is a degenerate (scopeless) call — use the
484
- * type arg.
485
- */
486
- as<S extends Scopes>(scope?: S): void;
471
+ * `.as("singleton")` is what the engine executes: the transformer rewrites the
472
+ * authored type-arg form (`.as<"singleton">()`) to this value-arg form before
473
+ * runtime, and a plugin-less caller writes it directly. The AUTHORED type-arg
474
+ * form (`.as<S extends Scopes>(): void`) is a PURE TYPING contributed by the
475
+ * `@fnioc/transformer` augmentation it is not part of di's published surface,
476
+ * so it only type-checks when the transformer's types are in the program.
477
+ */
478
+ as(scope: Scopes): void;
487
479
  }
488
480
  /**
489
481
  * The registration builder.
@@ -527,18 +519,6 @@ declare class DiBuilder<Root extends string = "singleton", Children extends stri
527
519
  * trailing `.as()` leaves the base (transient) registration in place.
528
520
  */
529
521
  private appendScoped;
530
- /**
531
- * Type-only authoring overloads — the forms the transformer rewrites FROM:
532
- * - `add<I>(C)` → `add("token", C)` (class)
533
- * - `add<I>(fn)` → `addFactory("token", fn)` (factory; the transformer
534
- * knows the arg is a function and routes it to `addFactory`).
535
- * The ctor is typed `Ctor<any[], I>` (plain construct signature, so an
536
- * abstract class is rejected); the factory is any `(...args) => I`. Neither
537
- * runs post-transform — they exist purely so type-driven authoring
538
- * type-checks before lowering.
539
- */
540
- add<I>(ctor: Ctor<any[], I>): AddBuilder<Root | Children>;
541
- add<I>(factory: (...args: any[]) => I): AddBuilder<Root | Children>;
542
522
  /**
543
523
  * Class registration — a string token bound to a concrete constructor. The
544
524
  * runtime form: what the transformer emits for a class, and what a
@@ -550,29 +530,30 @@ declare class DiBuilder<Root extends string = "singleton", Children extends stri
550
530
  * runtime form the transformer emits for an authored `add<I>(fn)`, and what a
551
531
  * plugin-less caller writes directly.
552
532
  *
553
- * Parameter injection follows the metadata rule (see `Scope.instantiate`): a
533
+ * Parameter injection follows the metadata rule (see `ServiceProvider`): a
554
534
  * factory WITH a `defineDeps` record (emitted by the transformer) has each
555
535
  * parameter injected by its slot; a record-less factory (the plugin-less
556
- * escape hatch) is called with the live scope — type it `(scope: ResolveScope)
557
- * => T` and `scope.resolve(...)` its own deps. Returns the `.as(scope?)`
536
+ * escape hatch) is called with the live provider — type it `(sp: Resolver)
537
+ * => T` and `sp.resolve(...)` its own deps. Returns the `.as(scope?)`
558
538
  * continuation so a factory caches at a named scope exactly like a class.
559
539
  */
560
- addFactory(token: Token, factory: (scope: ResolveScope) => unknown): AddBuilder<Root | Children>;
540
+ addFactory(token: Token, factory: Func<[Resolver], unknown>): AddBuilder<Root | Children>;
561
541
  /**
562
542
  * Value registration — an already-built instance, no deps and no lifetime.
563
543
  * Separate from `add` because a value may itself be a function (a callable
564
544
  * service), which is structurally indistinguishable from a factory inside one
565
- * overload. Authoring `addValue<I>(v)` lowers to `addValue("token", v)`.
545
+ * overload. The authoring form `addValue<I>(v)` (which lowers to
546
+ * `addValue("token", v)`) is a PURE TYPING contributed by the
547
+ * `@fnioc/transformer` augmentation, not part of di's published surface.
566
548
  */
567
- addValue<I>(value: I): void;
568
549
  addValue(token: Token, value: unknown): void;
569
550
  /**
570
- * Mints the root scope. The root is a real, app-lifetime object its name is
571
- * `Root` (the lifetime that singletons, or whatever the app's longest
572
- * lifetime is, bind to). No argument: `build()` owns the root name via the
573
- * `Root` type parameter.
551
+ * Mints the root ServiceProvider with a SEALED copy of the registration map.
552
+ * Sealing (deep-freezing the map and each per-token list) ensures that any
553
+ * `.add()` call on the builder after `build()` cannot mutate what the root
554
+ * and its descendants see — the container's view is fixed at construction time.
574
555
  */
575
- build(): Scope<Root | Children>;
556
+ build(): ServiceProvider<Root | Children>;
576
557
  }
577
558
 
578
559
  /** Base class for every error the container raises. */
@@ -648,5 +629,5 @@ declare class AsyncDisposalRequiredError extends DiError {
648
629
  constructor();
649
630
  }
650
631
 
651
- export { AsyncDisposalRequiredError, CircularDependencyError, DiBuilder, DiError, FactoryTargetError, MissingMetadataError, MissingScopeError, NoSatisfiableSignatureError, Scope, UnregisteredTokenError, defineDeps, forCtor, hole, signature };
652
- export type { AddBuilder, ClassRegistration, Ctor, DepRecord, Factory, FactoryRegistration, ForCtorBuilder, Registration, ResolveScope, Token, ValueRegistration };
632
+ export { AsyncDisposalRequiredError, CircularDependencyError, DiBuilder, DiError, FactoryTargetError, MissingMetadataError, MissingScopeError, NoSatisfiableSignatureError, Scope, ServiceProvider, UnregisteredTokenError, defineDeps, forCtor, hole, signature };
633
+ export type { AddBuilder, ClassRegistration, Ctor, DepRecord, Factory, FactoryRegistration, ForCtorBuilder, Lifetime, Registration, ResolveScope, Resolver, ScopeFactory, Token, ValueRegistration };
package/dist/index.js CHANGED
@@ -160,98 +160,65 @@ function isScopeRef2(slot) {
160
160
  class Scope {
161
161
  name;
162
162
  parent;
163
- baseRegistrations;
164
- localRegistrations = new Map;
165
- instances = new Map;
166
- ownedOrder = [];
167
- disposed = false;
168
- constructor(name, parent, baseRegistrations) {
163
+ cache = new Map;
164
+ owned = [];
165
+ constructor(name, parent) {
169
166
  this.name = name;
170
167
  this.parent = parent;
171
- this.baseRegistrations = baseRegistrations;
172
168
  }
173
- static appendTo(map, token, registration) {
174
- const existing = map.get(token);
175
- if (existing === undefined) {
176
- map.set(token, [registration]);
177
- } else {
178
- existing.push(registration);
169
+ }
170
+
171
+ class ServiceProvider {
172
+ registrations;
173
+ disposed = false;
174
+ frame;
175
+ constructor(registrations, frame) {
176
+ this.registrations = registrations;
177
+ this.frame = frame;
178
+ }
179
+ get name() {
180
+ if (this.frame === undefined) {
181
+ throw new TypeError("This ServiceProvider has no scope frame (unscoped root).");
179
182
  }
183
+ return this.frame.name;
180
184
  }
181
- createScope(childName) {
182
- return new Scope(childName, this, this.baseRegistrations);
183
- }
184
- appendScopedLocal(token, base) {
185
- Scope.appendTo(this.localRegistrations, token, base);
186
- const map = this.localRegistrations;
187
- return {
188
- as(scope) {
189
- if (scope === undefined)
190
- return;
191
- Scope.appendTo(map, token, { ...base, scope });
192
- }
193
- };
194
- }
195
- add(token, ctor) {
196
- return this.appendScopedLocal(token, {
197
- kind: "class",
198
- ctor,
199
- scope: undefined
200
- });
201
- }
202
- addFactory(token, factory) {
203
- return this.appendScopedLocal(token, {
204
- kind: "factory",
205
- factory,
206
- scope: undefined
207
- });
208
- }
209
- addValue(token, value) {
210
- Scope.appendTo(this.localRegistrations, token, {
211
- kind: "value",
212
- useValue: value
213
- });
214
- return this;
185
+ createScope(...args) {
186
+ const name = args[0] ?? "scoped";
187
+ const childFrame = new Scope(name, this.frame);
188
+ return new ServiceProvider(this.registrations, childFrame);
215
189
  }
216
190
  resolve(token) {
217
191
  if (token === undefined) {
218
192
  throw new TypeError("resolve<T>() requires the @fnioc/transformer plugin (no token at " + "runtime). Without it, resolve with an explicit token: " + 'resolve<T>("my:token").');
219
193
  }
220
- return this.resolveWith(token, []);
194
+ return this.resolveWith(token, this.frame, []);
221
195
  }
222
196
  resolveFactory(token) {
223
- return this.makeFactory({ factory: token });
197
+ return this.makeFactory({ factory: token }, this.frame);
224
198
  }
225
199
  lookup(token) {
226
- let node = this;
227
- while (node !== undefined) {
228
- const local = node.localRegistrations.get(token);
229
- if (local !== undefined && local.length > 0)
230
- return local[local.length - 1];
231
- node = node.parent;
232
- }
233
- const base = this.baseRegistrations.get(token);
234
- return base !== undefined && base.length > 0 ? base[base.length - 1] : undefined;
200
+ const list = this.registrations.get(token);
201
+ return list !== undefined && list.length > 0 ? list[list.length - 1] : undefined;
235
202
  }
236
- findOwner(scope) {
237
- let node = this;
203
+ static findOwner(vantage, scopeName) {
204
+ let node = vantage;
238
205
  while (node !== undefined) {
239
- if (node.name === scope)
206
+ if (node.name === scopeName)
240
207
  return node;
241
208
  node = node.parent;
242
209
  }
243
210
  return;
244
211
  }
245
- chainNames() {
212
+ static chainNames(vantage) {
246
213
  const names = [];
247
- let node = this;
214
+ let node = vantage;
248
215
  while (node !== undefined) {
249
216
  names.push(node.name);
250
217
  node = node.parent;
251
218
  }
252
219
  return names;
253
220
  }
254
- resolveWith(token, stack) {
221
+ resolveWith(token, vantage, stack) {
255
222
  if (stack.includes(token)) {
256
223
  throw new CircularDependencyError([...stack, token]);
257
224
  }
@@ -265,65 +232,68 @@ class Scope {
265
232
  if (registration.scope === undefined) {
266
233
  stack.push(token);
267
234
  try {
268
- return this.instantiate(token, registration, this, stack);
235
+ return this.instantiate(token, registration, vantage, stack);
269
236
  } finally {
270
237
  stack.pop();
271
238
  }
272
239
  }
273
- const owner = this.findOwner(registration.scope);
240
+ const owner = ServiceProvider.findOwner(vantage, registration.scope);
274
241
  if (owner === undefined) {
275
- throw new MissingScopeError(token, registration.scope, this.chainNames());
242
+ throw new MissingScopeError(token, registration.scope, ServiceProvider.chainNames(vantage));
276
243
  }
277
- if (owner.instances.has(token)) {
278
- return owner.instances.get(token);
244
+ if (owner.cache.has(token)) {
245
+ return owner.cache.get(token);
279
246
  }
280
247
  stack.push(token);
281
248
  try {
282
- const instance = owner.instantiate(token, registration, owner, stack);
283
- owner.instances.set(token, instance);
284
- owner.ownedOrder.push(instance);
249
+ const instance = this.instantiate(token, registration, owner, stack);
250
+ owner.cache.set(token, instance);
251
+ owner.owned.push(instance);
285
252
  return instance;
286
253
  } finally {
287
254
  stack.pop();
288
255
  }
289
256
  }
290
- instantiate(token, registration, owningScope, stack) {
257
+ instantiate(token, registration, owningFrame, stack) {
291
258
  if (registration.kind === "factory") {
292
- return owningScope.invokeFactory(token, registration.factory, stack);
259
+ return this.invokeFactory(token, registration.factory, owningFrame, stack);
293
260
  }
294
- return owningScope.construct(token, registration.ctor, stack);
261
+ return this.construct(token, registration.ctor, owningFrame, stack);
295
262
  }
296
- makeScopeView(stack) {
297
- const owner = this;
298
- const view = {
263
+ makeProviderView(owningFrame, stack) {
264
+ const sp = this;
265
+ return {
299
266
  resolve: (depToken) => {
300
267
  if (depToken === undefined) {
301
268
  throw new TypeError("resolve<T>() requires the @fnioc/transformer plugin (no token at " + "runtime).");
302
269
  }
303
- return owner.resolveWith(depToken, stack);
270
+ return sp.resolveWith(depToken, owningFrame, stack);
304
271
  },
305
- resolveFactory: (depToken) => owner.makeFactory({ factory: depToken }),
306
- createScope: (name) => owner.createScope(name)
272
+ resolveFactory: (depToken) => sp.makeFactory({ factory: depToken }, owningFrame),
273
+ createScope: (...args) => {
274
+ const name = args[0] ?? "scoped";
275
+ const childFrame = new Scope(name, owningFrame);
276
+ return new ServiceProvider(sp.registrations, childFrame);
277
+ }
307
278
  };
308
- return view;
309
279
  }
310
- invokeFactory(token, factory, stack) {
311
- const scopeView = this.makeScopeView(stack);
280
+ invokeFactory(token, factory, owningFrame, stack) {
281
+ const providerView = this.makeProviderView(owningFrame, stack);
312
282
  const record = getDeps(factory);
313
283
  if (record === undefined || record.signatures.length === 0) {
314
- return factory(scopeView);
284
+ return factory(providerView);
315
285
  }
316
- const signature2 = this.selectSignature(token, factory.name, record.signatures);
286
+ const signature2 = this.selectSignature(token, factory.name, record.signatures, owningFrame);
317
287
  const args = signature2.map((slot) => {
318
288
  if (isScopeRef2(slot))
319
- return scopeView;
289
+ return providerView;
320
290
  if (isFactoryRef2(slot))
321
- return this.makeFactory(slot);
322
- return this.resolveWith(slot, stack);
291
+ return this.makeFactory(slot, owningFrame);
292
+ return this.resolveWith(slot, owningFrame, stack);
323
293
  });
324
294
  return factory(...args);
325
295
  }
326
- construct(token, ctor, stack) {
296
+ construct(token, ctor, owningFrame, stack) {
327
297
  const record = getDeps(ctor);
328
298
  if (record === undefined || record.signatures.length === 0) {
329
299
  if (ctor.length > 0) {
@@ -331,52 +301,54 @@ class Scope {
331
301
  }
332
302
  return new ctor;
333
303
  }
334
- const signature2 = this.selectSignature(token, ctor.name, record.signatures);
304
+ const signature2 = this.selectSignature(token, ctor.name, record.signatures, owningFrame);
305
+ const providerView = this.makeProviderView(owningFrame, stack);
335
306
  const args = signature2.map((slot) => {
336
307
  if (isScopeRef2(slot)) {
337
- return this.makeScopeView(stack);
308
+ return providerView;
338
309
  }
339
310
  if (isFactoryRef2(slot)) {
340
- return this.makeFactory(slot);
311
+ return this.makeFactory(slot, owningFrame);
341
312
  }
342
- return this.resolveWith(slot, stack);
313
+ return this.resolveWith(slot, owningFrame, stack);
343
314
  });
344
315
  return new ctor(...args);
345
316
  }
346
- makeFactory(ref) {
347
- const owningScope = this;
317
+ makeFactory(ref, owningFrame) {
318
+ const sp = this;
348
319
  const target = this.lookup(ref.factory);
349
320
  if (target === undefined) {
350
321
  throw new FactoryTargetError(ref.factory, "unregistered");
351
322
  }
352
323
  if (target.kind === "value") {
353
- return () => owningScope.resolveWith(ref.factory, []);
324
+ return () => sp.resolveWith(ref.factory, owningFrame, []);
354
325
  }
355
326
  const depTarget = target.kind === "class" ? target.ctor : target.factory;
356
327
  const record = getDeps(depTarget);
357
- const targetSignature = record === undefined || record.signatures.length === 0 ? undefined : owningScope.selectTargetSignature(record.signatures);
358
- const parameterized = targetSignature !== undefined && targetSignature.some((slot) => !isFactoryRef2(slot) && !isScopeRef2(slot) && !owningScope.isResolvable(slot));
328
+ const targetSignature = record === undefined || record.signatures.length === 0 ? undefined : sp.selectTargetSignature(record.signatures);
329
+ const parameterized = targetSignature !== undefined && targetSignature.some((slot) => !isFactoryRef2(slot) && !isScopeRef2(slot) && !sp.isResolvable(slot));
359
330
  if (!parameterized) {
360
- return () => owningScope.resolveWith(ref.factory, []);
331
+ return () => sp.resolveWith(ref.factory, owningFrame, []);
361
332
  }
362
- return (...callArgs) => owningScope.buildPartitioned(target, targetSignature, callArgs);
333
+ return (...callArgs) => sp.buildPartitioned(target, targetSignature, callArgs, owningFrame);
363
334
  }
364
- buildPartitioned(target, signature2, callerArgs) {
335
+ buildPartitioned(target, signature2, callerArgs, owningFrame) {
365
336
  const stack = [];
337
+ const providerView = this.makeProviderView(owningFrame, stack);
366
338
  let nextCallerArg = 0;
367
339
  const args = signature2.map((slot) => {
368
340
  if (isScopeRef2(slot))
369
- return this.makeScopeView(stack);
341
+ return providerView;
370
342
  if (isFactoryRef2(slot))
371
- return this.makeFactory(slot);
343
+ return this.makeFactory(slot, owningFrame);
372
344
  if (!this.isResolvable(slot)) {
373
345
  return callerArgs[nextCallerArg++];
374
346
  }
375
- return this.resolveWith(slot, stack);
347
+ return this.resolveWith(slot, owningFrame, stack);
376
348
  });
377
349
  return target.kind === "class" ? new target.ctor(...args) : target.factory(...args);
378
350
  }
379
- selectSignature(token, targetName, signatures) {
351
+ selectSignature(token, targetName, signatures, _owningFrame) {
380
352
  const ordered = signatures.map((sig, index) => ({ sig, index })).sort((a, b) => b.sig.length !== a.sig.length ? b.sig.length - a.sig.length : a.index - b.index);
381
353
  const unsatisfiable = new Set;
382
354
  for (const { sig } of ordered) {
@@ -404,14 +376,15 @@ class Scope {
404
376
  dispose() {
405
377
  if (this.disposed)
406
378
  return;
407
- for (const instance of this.ownedOrder) {
379
+ const owned = this.frame?.owned ?? [];
380
+ for (const instance of owned) {
408
381
  if (isThenable(instance)) {
409
382
  throw new AsyncDisposalRequiredError;
410
383
  }
411
384
  }
412
385
  this.disposed = true;
413
- for (let i = this.ownedOrder.length - 1;i >= 0; i--) {
414
- const instance = this.ownedOrder[i];
386
+ for (let i = owned.length - 1;i >= 0; i--) {
387
+ const instance = owned[i];
415
388
  if (isDisposable(instance)) {
416
389
  instance[Symbol.dispose]();
417
390
  }
@@ -422,8 +395,9 @@ class Scope {
422
395
  if (this.disposed)
423
396
  return;
424
397
  this.disposed = true;
398
+ const owned = this.frame?.owned ?? [];
425
399
  const settled = [];
426
- for (const instance of this.ownedOrder) {
400
+ for (const instance of owned) {
427
401
  settled.push(isThenable(instance) ? await instance : instance);
428
402
  }
429
403
  for (let i = settled.length - 1;i >= 0; i--) {
@@ -437,8 +411,10 @@ class Scope {
437
411
  this.clear();
438
412
  }
439
413
  clear() {
440
- this.instances.clear();
441
- this.ownedOrder.length = 0;
414
+ if (this.frame) {
415
+ this.frame.cache.clear();
416
+ this.frame.owned.length = 0;
417
+ }
442
418
  }
443
419
  [Symbol.dispose]() {
444
420
  this.dispose();
@@ -500,7 +476,13 @@ class DiBuilder {
500
476
  this.append(token, { kind: "value", useValue: value });
501
477
  }
502
478
  build() {
503
- return new Scope(this.rootName, undefined, this.registrations);
479
+ const sealed = new Map;
480
+ for (const [token, list] of this.registrations) {
481
+ sealed.set(token, Object.freeze([...list]));
482
+ }
483
+ Object.freeze(sealed);
484
+ const rootFrame = new Scope(this.rootName);
485
+ return new ServiceProvider(sealed, rootFrame);
504
486
  }
505
487
  }
506
488
  export {
@@ -509,6 +491,7 @@ export {
509
491
  forCtor,
510
492
  defineDeps,
511
493
  UnregisteredTokenError,
494
+ ServiceProvider,
512
495
  Scope,
513
496
  NoSatisfiableSignatureError,
514
497
  MissingScopeError,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fnioc/di",
3
- "version": "2.0.0",
3
+ "version": "3.0.0",
4
4
  "description": "The ioc runtime engine: DiBuilder, scopes, resolution, captive-dependency protection, factories, and native disposal.",
5
5
  "keywords": [
6
6
  "dependency-injection",