@fnioc/di 3.0.0 → 4.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -10,7 +10,7 @@ No decorators. No `reflect-metadata`. No runtime type introspection. Feed it str
10
10
 
11
11
  The entry point. `Root` is the root scope's name (the app-lifetime scope singletons bind to, default `"singleton"`); `Children` is the union of declarable child-scope names. Scopes are `Root | Children`. Transient (no cache, fresh instance on every resolve) is the default — there is no `"transient"` scope; the absence of `.as()` is what makes a registration transient.
12
12
 
13
- ```typescript
13
+ ```ts
14
14
  import { DiBuilder } from "@fnioc/di";
15
15
 
16
16
  const services = new DiBuilder<"singleton", "request">();
@@ -22,7 +22,7 @@ Registration is append-only: each token holds a **list** of registrations in reg
22
22
 
23
23
  Register a concrete implementation against an interface token. The transformer rewrites `add<IFoo>(Foo)` to `add("pkg:IFoo", Foo)` at build time. Hand-fed consumers pass the token string directly.
24
24
 
25
- ```typescript
25
+ ```ts
26
26
  // With transformer (author form):
27
27
  services.add<ILogger>(ConsoleLogger).as<"singleton">();
28
28
  services.add<IUserRepo>(SqlUserRepo).as<"request">();
@@ -38,13 +38,24 @@ The type constraint on `Concrete` is `new (...args: any[]) => Interface` — pla
38
38
 
39
39
  `.as<S>()` checks at compile time that `S` is a declared scope name. Passing an undeclared string is a type error.
40
40
 
41
+ ### `add<I>(Concrete, sig)` — registration-time signature override
42
+
43
+ For third-party classes (ctor not editable) or generic instantiations the transformer cannot infer, supply a positional override array alongside the class:
44
+
45
+ ```ts
46
+ add<ICache>(RedisCache, ["pkg:IRedisClient", undefined, "pkg:ILogger"]);
47
+ ```
48
+
49
+ `sig` is `readonly (DepSlot | undefined)[]` — a positional sparse override over the transformer-generated signature. A `DepSlot` at a position overrides the generated token there; `undefined` keeps the generated token. Use explicit `undefined` rather than sparse elision (no-sparse-arrays).
50
+
51
+ Pure token users (no transformer) supply a complete signature via `forCtor(C).signature(...)` instead.
52
+
41
53
  ### `add(token, { useFactory })` and `add(token, { useValue })`
42
54
 
43
55
  The same `add` surface also takes factory and value specs — registration paths that bypass the dep-metadata system entirely. Recommended for test doubles, third-party instances, and plugin-less consumers. Both return the builder for chaining.
44
56
 
45
- ```typescript
46
- // Factory: receives the scope, returns the instance. An optional `scope`
47
- // caches the result at the matching ancestor (singleton-style).
57
+ ```ts
58
+ // Factory: receives the scope, returns the instance.
48
59
  services.add("pkg:IDb", {
49
60
  useFactory: (c) => new PostgresDb(c.resolve<IConfig>("pkg:IConfig")),
50
61
  scope: "singleton",
@@ -58,7 +69,7 @@ services.add("pkg:ICache", {
58
69
 
59
70
  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
71
 
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.
72
+ 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. The map seals at `build()` — no post-build mutation is possible.
62
73
 
63
74
  ---
64
75
 
@@ -66,7 +77,7 @@ To override a registration for a specific context (e.g. a test double), register
66
77
 
67
78
  Scopes form a parent-linked chain. The root scope is a real, app-lifetime object — minted by `build()`, never auto-created by the container. Child scopes nest from a scope via `createScope`.
68
79
 
69
- ```typescript
80
+ ```ts
70
81
  const root = services.build(); // app lifetime — named by Root
71
82
  const req = root.createScope("request"); // per HTTP request
72
83
  ```
@@ -89,7 +100,7 @@ const req = root.createScope("request"); // per HTTP request
89
100
 
90
101
  The critical correctness rule: deps are resolved **relative to the scope that will own the instance**, not the scope that triggered the resolve.
91
102
 
92
- ```typescript
103
+ ```ts
93
104
  const services = new DiBuilder<"singleton", "request">();
94
105
  services.add<ICache>(RedisCache).as<"singleton">();
95
106
  services.add<IUserContext>(HttpUserContext).as<"request">();
@@ -114,9 +125,9 @@ This mirrors `Microsoft.Extensions.DependencyInjection`'s scope-validation disci
114
125
 
115
126
  ## Greedy overload selection
116
127
 
117
- When a constructor has multiple registered signatures (stacked `@signature` decorators or chained `forCtor.signature()` calls), the engine selects by scanning **longest → shortest** and picking the first signature where every non-hole parameter token is satisfiable (registered in the container). Equal-arity ties break by registration order.
128
+ When a constructor has multiple registered signatures (stacked `@signature` decorators or chained `forCtor.signature()` calls), the engine selects by scanning **longest → shortest** and picking the first signature where every resolvable parameter token is satisfiable (registered in the container). Equal-arity ties break by registration order.
118
129
 
119
- ```typescript
130
+ ```ts
120
131
  // Two overloads: prefer the one with ILogger if available
121
132
  @signature("pkg:ILogger", "pkg:IDb")
122
133
  @signature("pkg:IDb")
@@ -144,7 +155,7 @@ Circular dependency detected:
144
155
 
145
156
  Closing a scope disposes the instances it owns in **reverse construction order**. Only instances implementing the native TC39 disposal contract are disposed.
146
157
 
147
- ```typescript
158
+ ```ts
148
159
  // Sync disposal
149
160
  scope.dispose(): void
150
161
 
@@ -168,7 +179,7 @@ Instances owned by ancestor scopes are disposed when those scopes close, not whe
168
179
 
169
180
  The container never awaits. Async is expressed as `Promise<T>` values through the sync channel.
170
181
 
171
- ```typescript
182
+ ```ts
172
183
  // Register an async factory
173
184
  services.add("pkg:IDb", {
174
185
  useFactory: async (c) => {
@@ -197,11 +208,11 @@ The transformer unwraps `Promise<X>` at dep-extraction: a parameter typed `Promi
197
208
 
198
209
  A constructor parameter whose type annotation is an inline function type returning a registered interface is injected as a **factory** — a callable that builds the target on demand — rather than a resolved instance.
199
210
 
200
- ```typescript
211
+ ```ts
201
212
  // IDb is a registered class. This parameter receives a callable:
202
213
  constructor(makeDb: () => IDb) { ... }
203
214
 
204
- // Partial factory — the target ctor has holes the caller fills:
215
+ // Partial factory — the caller fills caller-supplied params:
205
216
  constructor(makeRepo: (tableName: string) => IUserRepo) { ... }
206
217
  ```
207
218
 
@@ -209,7 +220,7 @@ constructor(makeRepo: (tableName: string) => IUserRepo) { ... }
209
220
 
210
221
  A **named** callable interface is NOT treated as a factory — it resolves as a normal service keyed on that interface's own token:
211
222
 
212
- ```typescript
223
+ ```ts
213
224
  interface IDbFactory { (): IDb }
214
225
 
215
226
  // Resolves as the "pkg:IDbFactory" token, not a factory for IDb
@@ -218,13 +229,29 @@ constructor(dbFactory: IDbFactory) { ... }
218
229
 
219
230
  Name the interface to opt out of factory interpretation whenever your function-typed service should itself be a registered dep.
220
231
 
232
+ ### `resolveFactory(type, params?)`
233
+
234
+ Resolve a factory callable for the token rather than an instance:
235
+
236
+ ```ts
237
+ // Without params → strict zero-arg () => T; every slot must resolve from the container
238
+ const makeDb = scope.resolveFactory("pkg:IDb");
239
+ const db = makeDb(); // all deps resolved from container
240
+
241
+ // With params → factory (...params) => T; named tokens filled by caller, rest from container
242
+ const makeRepo = scope.resolveFactory("pkg:IUserRepo", ["app:tableName"]);
243
+ const repo = makeRepo("users"); // tableName filled by caller; ILogger, IDb from container
244
+ ```
245
+
246
+ `params` is the complete authored-order list of caller-supplied token strings, matched by token (first-occurrence, left-to-right). Passing `params` pins the factory's shape — it no longer drifts as registration state changes.
247
+
221
248
  ### Partial / positional factories
222
249
 
223
- The injected callable exposes **only the target constructor's unregistered parameters**, in their relative order. Registered deps are resolved by the container at call time; holes and unregistered params are filled positionally by caller-supplied arguments.
250
+ The injected callable exposes **only the target constructor's caller-supplied parameters**, in their relative order. Registered deps are resolved by the container at call time.
224
251
 
225
- ```typescript
252
+ ```ts
226
253
  // IUserRepo concrete: constructor(log: ILogger, tableName: string, db: IDb)
227
- // ILogger and IDb are registered; tableName is not (a hole).
254
+ // ILogger and IDb are registered; tableName is not registered (caller-supplied).
228
255
  // Injected factory type: (tableName: string) => IUserRepo
229
256
 
230
257
  class RequestHandler {
@@ -237,7 +264,7 @@ class RequestHandler {
237
264
  }
238
265
  ```
239
266
 
240
- There are no Ramda-style placeholders. The factory's call arity is exactly the count of unregistered parameters; the caller never sees the full constructor shape.
267
+ There are no Ramda-style placeholders. The factory's call arity is exactly the count of caller-supplied parameters; the caller never sees the full constructor shape.
241
268
 
242
269
  ### Lifetime semantics
243
270
 
@@ -245,10 +272,10 @@ The injected factory is a closure captured at injection time, referencing the ow
245
272
 
246
273
  | Factory kind | Lifetime behavior |
247
274
  |---|---|
248
- | **Zero-arg** (`() => IFoo`, no holes or unregistered params) | Routes through normal `resolve` — respects the target's registered lifetime. A singleton target returns the same instance on every call; a transient target yields a fresh one. |
249
- | **Parameterized** (caller args fill holes or unregistered params) | Builds a **fresh instance on every call**, bypassing the instance cache. Caller args differ per invocation, so caching would be wrong — two calls with different arguments must not collapse to one cached instance. |
275
+ | **Zero-arg** (`() => IFoo`, no caller-supplied params) | Routes through normal `resolve` — respects the target's registered lifetime. A singleton target returns the same instance on every call; a transient target yields a fresh one. |
276
+ | **Parameterized** (caller args fill caller-supplied params) | Builds a **fresh instance on every call**, bypassing the instance cache. Caller args differ per invocation, so caching would be wrong — two calls with different arguments must not collapse to one cached instance. |
250
277
 
251
- The captive-dependency rule (§5.4) holds at call time: the target's own deps are resolved relative to the scope that owns the factory-holding instance. A factory captured by a singleton that tries to build a request-scoped target still throws `MissingScopeError` when invoked.
278
+ The captive-dependency rule holds at call time: the target's own deps are resolved relative to the scope that owns the factory-holding instance. A factory captured by a singleton that tries to build a request-scoped target still throws `MissingScopeError` when invoked.
252
279
 
253
280
  ### `FactoryTargetError`
254
281
 
@@ -263,6 +290,23 @@ Note: `FactoryTargetError` is thrown when the factory callable is constructed (a
263
290
 
264
291
  ---
265
292
 
293
+ ## Union slots
294
+
295
+ A `Union` dep slot tries each member in declaration order and resolves to the first registered one. Throw if none resolves.
296
+
297
+ ```ts
298
+ import { union } from "@fnioc/di";
299
+
300
+ forCtor(Handler).signature(
301
+ union("pkg:IRedis", "pkg:IMemoryCache"),
302
+ "pkg:ILogger",
303
+ );
304
+ ```
305
+
306
+ Token users construct `Union` slots with `union(...)`. Transformer users write an inline `A | B` annotation and the transformer lowers it automatically. See [`@fnioc/transformer`](../transformer/README.md) for the named-vs-inline distinction.
307
+
308
+ ---
309
+
266
310
  ## API reference
267
311
 
268
312
  ### `DiBuilder<Root, Children>`
@@ -270,7 +314,8 @@ Note: `FactoryTargetError` is thrown when the factory callable is constructed (a
270
314
  | Member | Signature | Description |
271
315
  |---|---|---|
272
316
  | `add<I>(Concrete)` | `(ctor: new (...) => I) => AddBuilder` | Register a concrete class against interface `I`. |
273
- | `.as<S>()` | `(scope: S) void` | Set the lifetime scope. No call → transient. |
317
+ | `add<I>(Concrete, sig)` | `(ctor, sig: readonly (DepSlot \| undefined)[]) => AddBuilder` | Register with a positional signature override. |
318
+ | `.as<S>()` | `(scope: S) => void` | Set the lifetime scope. No call → transient. |
274
319
  | `add(token, ctor)` | `(token: string, ctor) => AddBuilder` | Class registration (lowered form). |
275
320
  | `addFactory(token, factory)` | `(token: string, factory: (sp: Resolver) => T) => AddBuilder` | Factory registration. No dep metadata required — the factory receives the live `Resolver`. |
276
321
  | `addValue(token, value)` | `(token: string, value: unknown) => void` | Value registration. A pre-built instance, re-used as-is. |
@@ -283,7 +328,7 @@ Implements `Resolver` + `ScopeFactory` + `Disposable` / `AsyncDisposable`.
283
328
  | Member | Signature | Description |
284
329
  |---|---|---|
285
330
  | `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. |
331
+ | `resolveFactory(type, params?)` | `(type: string, params?: readonly string[]) => (...args) => T` | Resolve a factory callable. Without `params`, strict zero-arg `() => T`; with `params`, `(...params) => T` matched by token. |
287
332
  | `createScope(name)` | `(name: Scopes) => ServiceProvider<Scopes>` | Create a nested child scope. |
288
333
  | `dispose()` | `() => void` | Sync close. Throws if any owned instance has async-only disposal. |
289
334
  | `disposeAsync()` | `() => Promise<void>` | Async close. |
package/dist/index.d.ts CHANGED
@@ -5,24 +5,6 @@ interface Ctor$1<in Args extends readonly any[] = any[], out Instance = any> {
5
5
  prototype: Instance;
6
6
  }
7
7
 
8
- /**
9
- * The hole sentinel: marks a constructor parameter as caller-supplied rather
10
- * than container-resolved.
11
- *
12
- * Used in signatures to communicate "this position is a hole — the factory
13
- * caller supplies this argument, not the DI container." Detected by identity
14
- * (`slot === hole`), and `null` is the sentinel value: it is exactly what the
15
- * transformer emits for a hole slot, so the authoring surface and the lowered
16
- * output agree on one representation.
17
- *
18
- * @example
19
- * ```ts
20
- * @signature("pkg:ILogger", hole, "pkg:IDb")
21
- * class SqlRepo { constructor(log: ILogger, tableName: string, db: IDb) { ... } }
22
- * ```
23
- */
24
- declare const hole: null;
25
-
26
8
  /**
27
9
  * Anything dependency metadata can be attached to: a class constructor (its
28
10
  * deps are the ctor parameters) or a factory function (its deps are the call
@@ -40,12 +22,16 @@ type DepTarget = Ctor$1 | Func$1<never[], unknown>;
40
22
  type Token = string;
41
23
  /**
42
24
  * Marks a constructor parameter to be injected as a *factory* producing the
43
- * registered token `factory`, rather than a resolved instance. The factory's
44
- * own call signature is the target ctor's unregistered params, partitioned at
45
- * resolve time.
25
+ * registered type token, rather than a resolved instance. The factory's own
26
+ * call signature is determined by the caller-supplied `params` list.
27
+ *
28
+ * `type` is the token of the produced type T (replaces the former `.factory` field).
29
+ * `params` is the complete, authored-order list of caller-supplied parameter tokens;
30
+ * when present it pins the factory shape so it no longer drifts with registration state.
46
31
  */
47
32
  interface FactoryRef {
48
- readonly factory: Token;
33
+ readonly type: Token;
34
+ readonly params?: readonly Token[];
49
35
  }
50
36
  /**
51
37
  * Marks a parameter to be injected with the live resolution scope itself,
@@ -58,20 +44,28 @@ interface FactoryRef {
58
44
  interface ScopeRef {
59
45
  readonly scope: true;
60
46
  }
47
+ /**
48
+ * A set of alternative dependency slots tried in declaration order (first
49
+ * resolvable member wins). If no member is resolvable, resolution throws.
50
+ * Each member is itself a `DepSlot` — nesting is allowed.
51
+ */
52
+ interface Union {
53
+ readonly union: readonly DepSlot[];
54
+ }
61
55
  /**
62
56
  * One positional slot in a constructor / factory signature:
63
57
  * - a `Token` string — a container-resolved dependency,
64
- * - the `hole` sentinel — a caller-supplied parameter,
65
- * - a `FactoryRef` a factory-injected parameter (see `FactoryRef`), or
66
- * - a `ScopeRef` the live resolution scope (see `ScopeRef`).
58
+ * - a `FactoryRef` — a factory-injected parameter (see `FactoryRef`),
59
+ * - a `ScopeRef` the live resolution scope (see `ScopeRef`), or
60
+ * - a `Union` member-level alternatives tried in order.
67
61
  */
68
- type DepSlot = Token | typeof hole | FactoryRef | ScopeRef;
62
+ type DepSlot = Token | FactoryRef | ScopeRef | Union;
69
63
  /**
70
64
  * Per-constructor dependency metadata stored in the global WeakMap.
71
65
  *
72
66
  * `signatures` is an array of arrays: each element is one constructor signature
73
- * (for overload support). `signatures[i][j]` is the `DepSlot` — a token, the
74
- * `hole` sentinel, or a `FactoryRef` — for constructor parameter `j` of
67
+ * (for overload support). `signatures[i][j]` is the `DepSlot` — a token, a
68
+ * `FactoryRef`, a `ScopeRef`, or a `Union` — for constructor parameter `j` of
75
69
  * overload `i`.
76
70
  */
77
71
  interface DepRecord {
@@ -92,7 +86,7 @@ interface DepRecord {
92
86
  * prototype-chain walk — subclasses do NOT inherit the
93
87
  * parent's record).
94
88
  * @param signatures An array of signatures; each signature is a positional array
95
- * of DepSlot (Token | hole | FactoryRef | ScopeRef) parallel to
89
+ * of DepSlot (Token | FactoryRef | ScopeRef | Union) parallel to
96
90
  * the target's parameter list.
97
91
  */
98
92
  declare function defineDeps(target: DepTarget, signatures: readonly (readonly DepSlot[])[]): void;
@@ -126,8 +120,8 @@ declare function signature(...tokens: readonly DepSlot[]): Func$1<[Ctor$1, Class
126
120
  interface ForCtorBuilder {
127
121
  /**
128
122
  * Appends one constructor signature (a positional array of DepSlot —
129
- * Token | hole | FactoryRef) to the ctor's dependency metadata. Returns
130
- * `this` for chaining.
123
+ * Token | FactoryRef | ScopeRef | Union) to the ctor's dependency metadata.
124
+ * Returns `this` for chaining.
131
125
  *
132
126
  * Each `.signature(...)` call is one overload. Chaining two calls is
133
127
  * equivalent to stacking two `@signature` decorators.
@@ -147,6 +141,44 @@ interface ForCtorBuilder {
147
141
  */
148
142
  declare function forCtor(ctor: Ctor$1): ForCtorBuilder;
149
143
 
144
+ /**
145
+ * @fnioc/core — the immutable substrate and dependency-metadata format.
146
+ *
147
+ * Exports:
148
+ * - `Token` — string alias for a DI key
149
+ * - `FactoryRef` — marks a signature slot as a factory-injected parameter
150
+ * - `ScopeRef` — marks a signature slot as the live resolution scope
151
+ * - `Union` — member-level alternative slots tried in declaration order
152
+ * - `DepSlot` — one positional slot: Token | FactoryRef | ScopeRef | Union
153
+ * - `Inject` — compile-time brand that pins a token for one arg
154
+ * - `DepTarget` — a ctor or factory function metadata attaches to
155
+ * - `DepRecord` — shape of per-constructor metadata in the WeakMap
156
+ * - `defineDeps` — the single write path into the global WeakMap
157
+ * - `getDeps` — the read path (consumed by @fnioc/di)
158
+ * - `union` — runtime helper: constructs a Union slot from member slots
159
+ * - `isFactoryRef` — type guard for FactoryRef slots
160
+ * - `isScopeRef` — type guard for ScopeRef slots
161
+ * - `isUnionSlot` — type guard for Union slots
162
+ * - `signature` — TC39 class decorator factory
163
+ * - `ForCtorBuilder` — return type of `forCtor`
164
+ * - `forCtor` — fluent free-function for third-party classes
165
+ */
166
+
167
+ /**
168
+ * Constructs a `Union` slot — a set of alternative dependency slots tried in
169
+ * declaration order. The first resolvable member wins; if none is resolvable,
170
+ * resolution throws.
171
+ *
172
+ * @example
173
+ * ```ts
174
+ * forCtor(Handler).signature(
175
+ * union("pkg:IRedis", "pkg:IMemoryCache"),
176
+ * "pkg:ILogger",
177
+ * );
178
+ * ```
179
+ */
180
+ declare function union(...slots: DepSlot[]): Union;
181
+
150
182
  type Func<in Args extends readonly any[] = any[], out Return = any> = (...args: Args) => Return;
151
183
 
152
184
  interface Ctor<in Args extends readonly any[] = any[], out Instance = any> {
@@ -217,11 +249,14 @@ interface Resolver {
217
249
  resolve<T>(token: Token): T;
218
250
  resolve(token: Token): unknown;
219
251
  /**
220
- * Returns a FACTORY for the token rather than an instance the resolve-site
221
- * mirror of a `FactoryRef` ctor param. The authored `resolve<(a: A) => T>()`
222
- * (a function-typed type arg) lowers to this.
252
+ * Returns a FACTORY for `type` rather than an instance. When `params` is
253
+ * absent or empty, returns a strict zero-arg `() => T` — every ctor slot must
254
+ * resolve from the container. When `params` is present, it is the complete
255
+ * authored-order list of caller-supplied parameter tokens; the returned factory
256
+ * has shape `(...params) => T`. The authored `resolve<(a: A) => T>()` lowers
257
+ * to `resolveFactory("pkg:T", ["pkg:A"])`.
223
258
  */
224
- resolveFactory(token: Token): unknown;
259
+ resolveFactory(type: Token, params?: readonly Token[]): unknown;
225
260
  }
226
261
  /**
227
262
  * The scope-creation surface. Injected into factory parameters typed
@@ -313,14 +348,14 @@ declare class ServiceProvider<S extends string = string> implements Resolver, Sc
313
348
  resolve<T>(token: Token): T;
314
349
  resolve(token: Token): unknown;
315
350
  /**
316
- * Returns a FACTORY for `token` rather than an instance the resolve-site
317
- * mirror of a `FactoryRef` ctor param. The authored `resolve<(a: A) => T>()`
318
- * lowers here. The returned callable exposes the target's UNREGISTERED /
319
- * caller-supplied parameters in order (PRD §7 "Partial / positional
320
- * factories"); a fully-resolvable target yields a zero-arg lazy factory that
321
- * respects the target's registered lifetime.
351
+ * Returns a FACTORY for `type` rather than an instance. When `params` is
352
+ * absent or empty, returns a strict zero-arg `() => T` — every ctor slot must
353
+ * resolve from the container (an unresolvable slot throws). When `params` is
354
+ * present, it is the complete authored-order list of caller-supplied parameter
355
+ * tokens; the returned factory has shape `(...params) => T`. The authored
356
+ * `resolve<(a: A) => T>()` lowers to `resolveFactory("pkg:T", ["pkg:A"])`.
322
357
  */
323
- resolveFactory(token: Token): unknown;
358
+ resolveFactory(type: Token, params?: readonly Token[]): unknown;
324
359
  /**
325
360
  * Returns the most-recent registration for `token` from the sealed map.
326
361
  * The sealed map is shared across all providers in the tree; local overrides
@@ -375,32 +410,45 @@ declare class ServiceProvider<S extends string = string> implements Resolver, Sc
375
410
  /**
376
411
  * Builds the callable injected for a `FactoryRef` parameter.
377
412
  *
378
- * The target ctor's signature is partitioned at CALL time against the live
379
- * registration map: each slot that is a registered token is resolved; each
380
- * slot that is an unregistered token or a `hole` takes the next
381
- * caller-supplied argument, positionally. The injected callable therefore
382
- * exposes only the target's unregistered parameters, in their relative order.
413
+ * When `ref.params` is absent or empty, the factory is STRICT: every ctor slot
414
+ * of the target must resolve from the container. An unresolvable slot throws at
415
+ * build time (via `selectSignature`). The result is a zero-arg `() => T` that
416
+ * respects the target's registered lifetime.
417
+ *
418
+ * When `ref.params` is present, it is the COMPLETE authored-order list of
419
+ * caller-supplied parameter tokens. The caller-supplied set is pinned to those
420
+ * tokens (by first-occurrence left-to-right matching against ctor slots). A
421
+ * slot token that appears in `params` is caller-supplied even if it is also
422
+ * registered (caller wins). A slot that is neither claimed by `params` nor
423
+ * resolvable from the container → error. The factory shape is exactly
424
+ * `(...params) => T`; a fresh instance is built on every call (bypassing the
425
+ * instance cache — caller args differ per call so caching would be wrong).
383
426
  *
384
427
  * Lifetime semantics:
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.
428
+ * - A ZERO-ARG (no-params) factory routes through the normal `resolve` path
429
+ * and RESPECTS the target's registered lifetime.
430
+ * - A PARAMETERIZED factory constructs a FRESH instance every call.
390
431
  *
391
432
  * The closure captures `owningFrame`. §5.4 holds at call time: the target's
392
433
  * deps resolve relative to the scope that owns the factory-holding instance.
393
434
  */
394
435
  private makeFactory;
395
436
  /**
396
- * Builds a factory target, partitioning its already-selected signature
397
- * against the live registration map: a registered token is resolved; a
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
437
+ * Builds a factory target with the params-driven caller-supplied partition.
438
+ *
439
+ * `callerParams` is the authored-order list of tokens whose values are
440
+ * supplied by the caller (from the `FactoryRef.params` list). Each ctor slot
441
+ * whose token appears in `callerParams` (first-occurrence left-to-right match)
442
+ * takes the corresponding `callArgs` value; every other slot resolves from the
443
+ * container. A slot that is neither claimed nor resolvable error (the factory
444
+ * cannot be built). A claimed slot that is also registered → caller wins.
445
+ *
446
+ * Always builds a fresh result — a parameterized factory bypasses the instance
447
+ * cache. Runs on a fresh cycle stack since the factory is invoked outside the
403
448
  * original resolve.
449
+ *
450
+ * `signature` may be `undefined` when the target has no DepRecord (zero-arg
451
+ * ctor or record-less factory) — in that case args is empty.
404
452
  */
405
453
  private buildPartitioned;
406
454
  /**
@@ -408,12 +456,12 @@ declare class ServiceProvider<S extends string = string> implements Resolver, Sc
408
456
  * the first SATISFIABLE one. A slot is satisfiable when it is:
409
457
  *
410
458
  * - a `FactoryRef` — always satisfiable; injected as a callable;
411
- * - a `ScopeRef` — always satisfiable; filled with the live provider view; or
459
+ * - a `ScopeRef` — always satisfiable; filled with the live provider view;
460
+ * - a `Union` — satisfiable iff at least one member is resolvable; or
412
461
  * - a string token whose registration exists in the sealed map.
413
462
  *
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.
463
+ * An unregistered string token is not satisfiable. Equal-arity ties break by
464
+ * registration order. None satisfiable throw naming the unsatisfiable tokens.
417
465
  */
418
466
  private selectSignature;
419
467
  /**
@@ -425,11 +473,29 @@ declare class ServiceProvider<S extends string = string> implements Resolver, Sc
425
473
  */
426
474
  private selectTargetSignature;
427
475
  /**
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.
476
+ * True when `slot` is a registered string token in the sealed map. A
477
+ * `FactoryRef`, `ScopeRef`, or `Union` is not tested here use
478
+ * `isResolvableSlot` for a full slot check.
431
479
  */
432
480
  private isResolvable;
481
+ /**
482
+ * True when a slot is resolvable in ANY form:
483
+ * - `FactoryRef` / `ScopeRef` — always satisfiable (injected);
484
+ * - `Union` — satisfiable iff at least one member is resolvable (recursive);
485
+ * - string token — registered in the sealed map.
486
+ */
487
+ private isResolvableSlot;
488
+ /**
489
+ * Resolves a `Union` slot: tries each member in declaration order and returns
490
+ * the first resolvable one. If no member is resolvable, throws
491
+ * `NoSatisfiableUnionError`.
492
+ */
493
+ private resolveUnion;
494
+ /**
495
+ * Resolves a single `DepSlot` to its value, dispatching on slot kind.
496
+ * Used by `resolveUnion` to recurse into members.
497
+ */
498
+ private resolveSlot;
433
499
  /**
434
500
  * Closes this provider synchronously, disposing the instances its scope frame
435
501
  * owns in REVERSE construction order. Only native `Disposable` instances are
@@ -620,6 +686,14 @@ declare class FactoryTargetError extends DiError {
620
686
  readonly reason: "unregistered" | "not-a-class";
621
687
  constructor(factoryToken: Token, reason: "unregistered" | "not-a-class");
622
688
  }
689
+ /**
690
+ * A `Union` slot was encountered during resolution but none of its member slots
691
+ * was resolvable. Resolution cannot proceed without at least one registered member.
692
+ */
693
+ declare class NoSatisfiableUnionError extends DiError {
694
+ readonly members: readonly DepSlot[];
695
+ constructor(members: readonly DepSlot[]);
696
+ }
623
697
  /**
624
698
  * Sync `dispose()` was called on a scope that owns a Promise-valued (thenable)
625
699
  * cached instance. A pending Promise cannot be disposed synchronously — the
@@ -629,5 +703,5 @@ declare class AsyncDisposalRequiredError extends DiError {
629
703
  constructor();
630
704
  }
631
705
 
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 };
706
+ export { AsyncDisposalRequiredError, CircularDependencyError, DiBuilder, DiError, FactoryTargetError, MissingMetadataError, MissingScopeError, NoSatisfiableSignatureError, NoSatisfiableUnionError, Scope, ServiceProvider, UnregisteredTokenError, defineDeps, forCtor, signature, union };
707
+ export type { AddBuilder, ClassRegistration, Ctor, DepRecord, Factory, FactoryRegistration, ForCtorBuilder, Lifetime, Registration, ResolveScope, Resolver, ScopeFactory, Token, Union, ValueRegistration };
package/dist/index.js CHANGED
@@ -1,26 +1,54 @@
1
1
  // ../core/src/store.ts
2
- var hole = null;
3
2
  var GLOBAL_KEY = Symbol.for("fnioc:deps");
4
3
  var globals = globalThis;
5
4
  var store = globals[GLOBAL_KEY] ??= new Map;
5
+
6
6
  // ../core/src/defineDeps.ts
7
7
  function isFactoryRef(slot) {
8
- return typeof slot === "object" && slot !== null && typeof slot.factory === "string";
8
+ return typeof slot === "object" && slot !== null && typeof slot.type === "string";
9
9
  }
10
10
  function isScopeRef(slot) {
11
11
  return typeof slot === "object" && slot !== null && slot.scope === true;
12
12
  }
13
+ function isUnionSlot(slot) {
14
+ return typeof slot === "object" && slot !== null && Array.isArray(slot.union);
15
+ }
13
16
  function slotsEqual(a, b) {
14
17
  const aIsRef = isFactoryRef(a);
15
18
  const bIsRef = isFactoryRef(b);
16
19
  if (aIsRef || bIsRef) {
17
- return aIsRef && bIsRef && a.factory === b.factory;
20
+ if (!aIsRef || !bIsRef)
21
+ return false;
22
+ if (a.type !== b.type)
23
+ return false;
24
+ const aParams = a.params ?? [];
25
+ const bParams = b.params ?? [];
26
+ if (aParams.length !== bParams.length)
27
+ return false;
28
+ for (let i = 0;i < aParams.length; i++) {
29
+ if (aParams[i] !== bParams[i])
30
+ return false;
31
+ }
32
+ return true;
18
33
  }
19
34
  const aIsScope = isScopeRef(a);
20
35
  const bIsScope = isScopeRef(b);
21
36
  if (aIsScope || bIsScope) {
22
37
  return aIsScope && bIsScope;
23
38
  }
39
+ const aIsUnion = isUnionSlot(a);
40
+ const bIsUnion = isUnionSlot(b);
41
+ if (aIsUnion || bIsUnion) {
42
+ if (!aIsUnion || !bIsUnion)
43
+ return false;
44
+ if (a.union.length !== b.union.length)
45
+ return false;
46
+ for (let i = 0;i < a.union.length; i++) {
47
+ if (!slotsEqual(a.union[i], b.union[i]))
48
+ return false;
49
+ }
50
+ return true;
51
+ }
24
52
  return a === b;
25
53
  }
26
54
  function signaturesEqual(a, b) {
@@ -65,6 +93,12 @@ function forCtor(ctor) {
65
93
  };
66
94
  return builder;
67
95
  }
96
+
97
+ // ../core/src/index.ts
98
+ function union(...slots) {
99
+ return { union: slots };
100
+ }
101
+
68
102
  // src/errors.ts
69
103
  class DiError extends Error {
70
104
  constructor(message) {
@@ -134,6 +168,15 @@ class FactoryTargetError extends DiError {
134
168
  }
135
169
  }
136
170
 
171
+ class NoSatisfiableUnionError extends DiError {
172
+ members;
173
+ constructor(members) {
174
+ const memberList = members.map((m) => typeof m === "string" ? `"${m}"` : JSON.stringify(m)).join(", ");
175
+ super(`No satisfiable union member found. Tried: [${memberList}]. ` + `Register at least one of the union members before resolving.`);
176
+ this.members = members;
177
+ }
178
+ }
179
+
137
180
  class AsyncDisposalRequiredError extends DiError {
138
181
  constructor() {
139
182
  super(`Cannot dispose synchronously: this scope owns a Promise-valued ` + `instance (an async useFactory result). Awaiting it is required ` + `before disposal — call disposeAsync() instead of dispose().`);
@@ -150,12 +193,9 @@ function isAsyncDisposable(value) {
150
193
  function isThenable(value) {
151
194
  return value != null && (typeof value === "object" || typeof value === "function") && typeof value.then === "function";
152
195
  }
153
- function isFactoryRef2(slot) {
154
- return slot !== null && typeof slot === "object" && typeof slot.factory === "string";
155
- }
156
- function isScopeRef2(slot) {
157
- return slot !== null && typeof slot === "object" && slot.scope === true;
158
- }
196
+ var isFactoryRef2 = isFactoryRef;
197
+ var isScopeRef2 = isScopeRef;
198
+ var isUnion = isUnionSlot;
159
199
 
160
200
  class Scope {
161
201
  name;
@@ -193,8 +233,8 @@ class ServiceProvider {
193
233
  }
194
234
  return this.resolveWith(token, this.frame, []);
195
235
  }
196
- resolveFactory(token) {
197
- return this.makeFactory({ factory: token }, this.frame);
236
+ resolveFactory(type, params) {
237
+ return this.makeFactory({ type, params }, this.frame);
198
238
  }
199
239
  lookup(token) {
200
240
  const list = this.registrations.get(token);
@@ -269,7 +309,7 @@ class ServiceProvider {
269
309
  }
270
310
  return sp.resolveWith(depToken, owningFrame, stack);
271
311
  },
272
- resolveFactory: (depToken) => sp.makeFactory({ factory: depToken }, owningFrame),
312
+ resolveFactory: (depToken, depParams) => sp.makeFactory({ type: depToken, params: depParams }, owningFrame),
273
313
  createScope: (...args) => {
274
314
  const name = args[0] ?? "scoped";
275
315
  const childFrame = new Scope(name, owningFrame);
@@ -289,6 +329,8 @@ class ServiceProvider {
289
329
  return providerView;
290
330
  if (isFactoryRef2(slot))
291
331
  return this.makeFactory(slot, owningFrame);
332
+ if (isUnion(slot))
333
+ return this.resolveUnion(slot, owningFrame, stack);
292
334
  return this.resolveWith(slot, owningFrame, stack);
293
335
  });
294
336
  return factory(...args);
@@ -310,41 +352,56 @@ class ServiceProvider {
310
352
  if (isFactoryRef2(slot)) {
311
353
  return this.makeFactory(slot, owningFrame);
312
354
  }
355
+ if (isUnion(slot)) {
356
+ return this.resolveUnion(slot, owningFrame, stack);
357
+ }
313
358
  return this.resolveWith(slot, owningFrame, stack);
314
359
  });
315
360
  return new ctor(...args);
316
361
  }
317
362
  makeFactory(ref, owningFrame) {
318
363
  const sp = this;
319
- const target = this.lookup(ref.factory);
364
+ const target = this.lookup(ref.type);
320
365
  if (target === undefined) {
321
- throw new FactoryTargetError(ref.factory, "unregistered");
366
+ throw new FactoryTargetError(ref.type, "unregistered");
322
367
  }
323
368
  if (target.kind === "value") {
324
- return () => sp.resolveWith(ref.factory, owningFrame, []);
369
+ return () => sp.resolveWith(ref.type, owningFrame, []);
370
+ }
371
+ const callerParams = ref.params !== undefined && ref.params.length > 0 ? ref.params : undefined;
372
+ if (callerParams === undefined) {
373
+ return () => sp.resolveWith(ref.type, owningFrame, []);
325
374
  }
326
375
  const depTarget = target.kind === "class" ? target.ctor : target.factory;
327
376
  const record = getDeps(depTarget);
328
377
  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));
330
- if (!parameterized) {
331
- return () => sp.resolveWith(ref.factory, owningFrame, []);
332
- }
333
- return (...callArgs) => sp.buildPartitioned(target, targetSignature, callArgs, owningFrame);
378
+ return (...callArgs) => sp.buildPartitioned(target, targetSignature, callerParams, callArgs, owningFrame);
334
379
  }
335
- buildPartitioned(target, signature2, callerArgs, owningFrame) {
380
+ buildPartitioned(target, signature2, callerParams, callArgs, owningFrame) {
336
381
  const stack = [];
337
382
  const providerView = this.makeProviderView(owningFrame, stack);
338
- let nextCallerArg = 0;
383
+ if (signature2 === undefined || signature2.length === 0) {
384
+ return target.kind === "class" ? new target.ctor : target.factory(providerView);
385
+ }
386
+ const remainingParamIndices = callerParams.map((_, i) => i);
339
387
  const args = signature2.map((slot) => {
340
388
  if (isScopeRef2(slot))
341
389
  return providerView;
342
390
  if (isFactoryRef2(slot))
343
391
  return this.makeFactory(slot, owningFrame);
344
- if (!this.isResolvable(slot)) {
345
- return callerArgs[nextCallerArg++];
392
+ if (isUnion(slot))
393
+ return this.resolveUnion(slot, owningFrame, stack);
394
+ const token = slot;
395
+ const matchIdx = remainingParamIndices.findIndex((pi) => callerParams[pi] === token);
396
+ if (matchIdx !== -1) {
397
+ const paramIdx = remainingParamIndices[matchIdx];
398
+ remainingParamIndices.splice(matchIdx, 1);
399
+ return callArgs[paramIdx];
346
400
  }
347
- return this.resolveWith(slot, owningFrame, stack);
401
+ if (!this.isResolvable(token)) {
402
+ throw new NoSatisfiableSignatureError(token, token, [token]);
403
+ }
404
+ return this.resolveWith(token, owningFrame, stack);
348
405
  });
349
406
  return target.kind === "class" ? new target.ctor(...args) : target.factory(...args);
350
407
  }
@@ -356,6 +413,12 @@ class ServiceProvider {
356
413
  for (const slot of sig) {
357
414
  if (isFactoryRef2(slot) || isScopeRef2(slot))
358
415
  continue;
416
+ if (isUnion(slot)) {
417
+ if (!this.isResolvableSlot(slot)) {
418
+ satisfiable = false;
419
+ }
420
+ continue;
421
+ }
359
422
  if (!this.isResolvable(slot)) {
360
423
  satisfiable = false;
361
424
  if (typeof slot === "string")
@@ -373,6 +436,31 @@ class ServiceProvider {
373
436
  isResolvable(slot) {
374
437
  return typeof slot === "string" && this.lookup(slot) !== undefined;
375
438
  }
439
+ isResolvableSlot(slot) {
440
+ if (isFactoryRef2(slot) || isScopeRef2(slot))
441
+ return true;
442
+ if (isUnion(slot)) {
443
+ return slot.union.some((member) => this.isResolvableSlot(member));
444
+ }
445
+ return this.isResolvable(slot);
446
+ }
447
+ resolveUnion(slot, owningFrame, stack) {
448
+ for (const member of slot.union) {
449
+ if (this.isResolvableSlot(member)) {
450
+ return this.resolveSlot(member, owningFrame, stack);
451
+ }
452
+ }
453
+ throw new NoSatisfiableUnionError(slot.union);
454
+ }
455
+ resolveSlot(slot, owningFrame, stack) {
456
+ if (isScopeRef2(slot))
457
+ return this.makeProviderView(owningFrame, stack);
458
+ if (isFactoryRef2(slot))
459
+ return this.makeFactory(slot, owningFrame);
460
+ if (isUnion(slot))
461
+ return this.resolveUnion(slot, owningFrame, stack);
462
+ return this.resolveWith(slot, owningFrame, stack);
463
+ }
376
464
  dispose() {
377
465
  if (this.disposed)
378
466
  return;
@@ -486,13 +574,14 @@ class DiBuilder {
486
574
  }
487
575
  }
488
576
  export {
577
+ union,
489
578
  signature,
490
- hole,
491
579
  forCtor,
492
580
  defineDeps,
493
581
  UnregisteredTokenError,
494
582
  ServiceProvider,
495
583
  Scope,
584
+ NoSatisfiableUnionError,
496
585
  NoSatisfiableSignatureError,
497
586
  MissingScopeError,
498
587
  MissingMetadataError,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fnioc/di",
3
- "version": "3.0.0",
3
+ "version": "4.0.1",
4
4
  "description": "The ioc runtime engine: DiBuilder, scopes, resolution, captive-dependency protection, factories, and native disposal.",
5
5
  "keywords": [
6
6
  "dependency-injection",
@@ -21,6 +21,7 @@
21
21
  "types": "./dist/index.d.ts",
22
22
  "exports": {
23
23
  ".": {
24
+ "source": "./src/index.ts",
24
25
  "types": "./dist/index.d.ts",
25
26
  "import": "./dist/index.js",
26
27
  "default": "./dist/index.js"