@fnioc/di 2.0.0 → 4.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
@@ -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
- 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.
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 @@ The same two specs are available scope-locally via `scope.add(token, spec)`, so
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,18 +314,22 @@ 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
- | `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. |
320
+ | `addFactory(token, factory)` | `(token: string, factory: (sp: Resolver) => T) => AddBuilder` | Factory registration. No dep metadata required — the factory receives the live `Resolver`. |
321
+ | `addValue(token, value)` | `(token: string, value: unknown) => void` | Value registration. A pre-built instance, re-used as-is. |
322
+ | `build()` | `() => ServiceProvider<Root \| Children>` | Seal the registration map and mint the root `ServiceProvider`. No post-build mutation is possible. |
323
+
324
+ ### `ServiceProvider<Scopes>`
277
325
 
278
- ### `Scope<Scopes>`
326
+ Implements `Resolver` + `ScopeFactory` + `Disposable` / `AsyncDisposable`.
279
327
 
280
328
  | Member | Signature | Description |
281
329
  |---|---|---|
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
330
  | `resolve<T>(token)` | `(token: string) => T` | Resolve an instance. Throws on captive-dep violation, missing scope ancestor, or cycle. |
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. |
332
+ | `createScope(name)` | `(name: Scopes) => ServiceProvider<Scopes>` | Create a nested child scope. |
285
333
  | `dispose()` | `() => void` | Sync close. Throws if any owned instance has async-only disposal. |
286
334
  | `disposeAsync()` | `() => Promise<void>` | Async close. |
287
335
  | `[Symbol.dispose]()` | — | Native `using` support. |