@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 +76 -28
- package/dist/index.d.ts +297 -242
- package/dist/index.js +203 -131
- package/package.json +1 -1
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
|
-
```
|
|
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
|
-
```
|
|
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
|
-
```
|
|
46
|
-
// Factory: receives the scope, returns the instance.
|
|
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
|
-
|
|
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
|
-
```
|
|
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
|
-
```
|
|
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
|
|
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
|
-
```
|
|
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
|
-
```
|
|
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
|
-
```
|
|
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
|
-
```
|
|
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
|
|
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
|
-
```
|
|
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
|
|
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
|
-
```
|
|
252
|
+
```ts
|
|
226
253
|
// IUserRepo concrete: constructor(log: ILogger, tableName: string, db: IDb)
|
|
227
|
-
// ILogger and IDb are registered; tableName is not (
|
|
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
|
|
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
|
|
249
|
-
| **Parameterized** (caller args fill
|
|
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
|
|
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
|
-
|
|
|
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
|
-
| `
|
|
276
|
-
| `
|
|
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
|
-
|
|
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. |
|