@fnioc/di 1.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 +294 -0
- package/dist/builder.d.ts +81 -0
- package/dist/builder.d.ts.map +1 -0
- package/dist/builder.js +85 -0
- package/dist/builder.js.map +1 -0
- package/dist/errors.d.ts +73 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +139 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +23 -0
- package/dist/index.js.map +1 -0
- package/dist/scope.d.ts +180 -0
- package/dist/scope.d.ts.map +1 -0
- package/dist/scope.js +466 -0
- package/dist/scope.js.map +1 -0
- package/dist/types.d.ts +57 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +4 -0
- package/dist/types.js.map +1 -0
- package/package.json +47 -0
package/README.md
ADDED
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
# @fnioc/di
|
|
2
|
+
|
|
3
|
+
The runtime engine for `ioc`. Resolves dependency graphs, manages scope lifetimes, enforces captive-dependency correctness, and drives native disposal.
|
|
4
|
+
|
|
5
|
+
No decorators. No `reflect-metadata`. No runtime type introspection. Feed it string tokens and dep arrays (generated by `@fnioc/transformer` or written by hand via `@fnioc/core`'s authoring surfaces) and it handles the rest.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## `DiBuilder<Scopes>`
|
|
10
|
+
|
|
11
|
+
The entry point. `Scopes` is your string union of scope names. Transient (no cache, fresh instance on every resolve) is the default — there is no `"transient"` tag; the absence of `.as()` is what makes a registration transient.
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import { DiBuilder } from "@fnioc/di";
|
|
15
|
+
|
|
16
|
+
const services = new DiBuilder<"singleton" | "request">();
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
### `.add<Interface>(Concrete).as<"scope">()`
|
|
20
|
+
|
|
21
|
+
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.
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
// With transformer (author form):
|
|
25
|
+
services.add<ILogger>(ConsoleLogger).as<"singleton">();
|
|
26
|
+
services.add<IUserRepo>(SqlUserRepo).as<"request">();
|
|
27
|
+
services.add<IRequestId>(UuidRequestId>(); // no .as() → transient
|
|
28
|
+
|
|
29
|
+
// Without transformer (lowered form, or plugin-less):
|
|
30
|
+
services.add("pkg:ILogger", ConsoleLogger).as("singleton");
|
|
31
|
+
services.add("pkg:IUserRepo", SqlUserRepo).as("request");
|
|
32
|
+
services.add("pkg:IRequestId", UuidRequestId);
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
The type constraint on `Concrete` is `new (...args: any[]) => Interface` — plain `new`, not `abstract new`. Abstract classes are correctly rejected because the container instantiates the concrete.
|
|
36
|
+
|
|
37
|
+
`.as<S>()` checks at compile time that `S` is a declared scope name. Passing an undeclared string is a type error.
|
|
38
|
+
|
|
39
|
+
### `useFactory` and `useValue`
|
|
40
|
+
|
|
41
|
+
Override paths that bypass the dep-metadata system entirely. Recommended for test doubles, third-party instances, and plugin-less consumers.
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
// Factory: receives the scope's container, returns the instance
|
|
45
|
+
container.register("pkg:IDb", {
|
|
46
|
+
useFactory: (c) => new PostgresDb(c.resolve<IConfig>("pkg:IConfig")),
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// Value: a pre-constructed instance (always singleton-like; re-used as-is)
|
|
50
|
+
container.register("pkg:ICache", {
|
|
51
|
+
useValue: new NullCache(),
|
|
52
|
+
});
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
`useFactory` with `.as("singleton")` on the parent builder makes the factory run once and cache the result. `useValue` is always cached.
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## Scope model
|
|
60
|
+
|
|
61
|
+
Scopes form a parent-linked chain. The root scope is a real, app-lifetime object — never auto-created by the container.
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
const root = services.createScope("singleton"); // app lifetime
|
|
65
|
+
const req = root.createScope("request"); // per HTTP request
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
**Resolution walks the parent chain** for two purposes:
|
|
69
|
+
|
|
70
|
+
1. **Registration lookup** — walks up until the token is found. A child scope can shadow a parent registration.
|
|
71
|
+
2. **Instance ownership** — the lifetime tag names which ancestor scope caches the instance. Walks up to the nearest matching ancestor and caches there.
|
|
72
|
+
|
|
73
|
+
**Lifetime rules:**
|
|
74
|
+
|
|
75
|
+
| Registration | Behavior |
|
|
76
|
+
|---|---|
|
|
77
|
+
| No `.as()` (transient) | Fresh instance on every resolve. Never cached. |
|
|
78
|
+
| `.as("singleton")` | Owned and cached by the nearest `"singleton"` ancestor in the chain. |
|
|
79
|
+
| `.as("request")` | Owned and cached by the nearest `"request"` ancestor in the chain. |
|
|
80
|
+
| Tag with no matching ancestor | **Throws.** The missing-ancestor error is intentional — see captive-dependency protection below. |
|
|
81
|
+
|
|
82
|
+
### Captive-dependency protection
|
|
83
|
+
|
|
84
|
+
The critical correctness rule: deps are resolved **relative to the scope that will own the instance**, not the scope that triggered the resolve.
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
const services = new DiBuilder<"singleton" | "request">();
|
|
88
|
+
services.add<ICache>(RedisCache).as<"singleton">();
|
|
89
|
+
services.add<IUserContext>(HttpUserContext).as<"request">();
|
|
90
|
+
services.add<IUserService>(UserService).as<"singleton">();
|
|
91
|
+
// UserService constructor: (cache: ICache, ctx: IUserContext)
|
|
92
|
+
|
|
93
|
+
const root = services.createScope("singleton");
|
|
94
|
+
const req = root.createScope("request");
|
|
95
|
+
|
|
96
|
+
req.resolve<IUserService>("pkg:IUserService");
|
|
97
|
+
// Throws: UserService is singleton-owned.
|
|
98
|
+
// Its deps are resolved from the singleton scope's chain.
|
|
99
|
+
// That chain has no "request" ancestor → IUserContext cannot be resolved → throws.
|
|
100
|
+
//
|
|
101
|
+
// Without this rule, UserService would silently capture one request's
|
|
102
|
+
// IUserContext and hold it across every subsequent request.
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
This mirrors `Microsoft.Extensions.DependencyInjection`'s scope-validation discipline. The throw is the feature.
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## Greedy overload selection
|
|
110
|
+
|
|
111
|
+
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.
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
// Two overloads: prefer the one with ILogger if available
|
|
115
|
+
@signature("pkg:ILogger", "pkg:IDb")
|
|
116
|
+
@signature("pkg:IDb")
|
|
117
|
+
class MyService {
|
|
118
|
+
constructor(logOrDb: ILogger | IDb, db?: IDb) { ... }
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
If ILogger is registered, the two-parameter signature wins. If not, the one-parameter signature is used.
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## Cycle detection
|
|
127
|
+
|
|
128
|
+
The engine maintains a resolution stack per `resolve()` call. If a token appears on the stack when it is about to be pushed again, it throws with the full path:
|
|
129
|
+
|
|
130
|
+
```
|
|
131
|
+
Circular dependency detected:
|
|
132
|
+
pkg:IUserRepo → pkg:IDb → pkg:IConnectionPool → pkg:IDb
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## Disposal
|
|
138
|
+
|
|
139
|
+
Closing a scope disposes the instances it owns in **reverse construction order**. Only instances implementing the native TC39 disposal contract are disposed.
|
|
140
|
+
|
|
141
|
+
```typescript
|
|
142
|
+
// Sync disposal
|
|
143
|
+
scope.dispose(): void
|
|
144
|
+
|
|
145
|
+
// Async disposal
|
|
146
|
+
scope.disposeAsync(): Promise<void>
|
|
147
|
+
|
|
148
|
+
// Native using / await using (TypeScript 5.2+, requires "ESNext.Disposable" in lib)
|
|
149
|
+
{
|
|
150
|
+
await using req = root.createScope("request");
|
|
151
|
+
// req.disposeAsync() called automatically on block exit
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
`Symbol.dispose` and `Symbol.asyncDispose` only — no custom `dispose()` interface. Sync `dispose()` throws if the scope owns a `Promise`-valued disposable, directing you to `disposeAsync()`. Async teardown is never silently skipped.
|
|
156
|
+
|
|
157
|
+
Instances owned by ancestor scopes are disposed when those scopes close, not when child scopes close.
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
## Async as values
|
|
162
|
+
|
|
163
|
+
The container never awaits. Async is expressed as `Promise<T>` values through the sync channel.
|
|
164
|
+
|
|
165
|
+
```typescript
|
|
166
|
+
// Register an async factory
|
|
167
|
+
container.register("pkg:IDb", {
|
|
168
|
+
useFactory: async (c) => {
|
|
169
|
+
const pool = c.resolve<IConnectionPool>("pkg:IConnectionPool");
|
|
170
|
+
return new PostgresDb(await pool.connect());
|
|
171
|
+
},
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
// Consume it — declare the dep as Promise<IDb>
|
|
175
|
+
class UserRepo {
|
|
176
|
+
constructor(private db: Promise<IDb>) {}
|
|
177
|
+
async findUser(id: string) {
|
|
178
|
+
return (await this.db).query(`SELECT * FROM users WHERE id = $1`, [id]);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
The container caches the factory's return verbatim — the `Promise` itself. Every resolve of `"pkg:IDb"` gets the same `Promise`; the async factory runs exactly once. `Promise<T>` at the dep site is the honest contract: the container doesn't hide asynchrony.
|
|
184
|
+
|
|
185
|
+
The transformer unwraps `Promise<X>` at dep-extraction: a parameter typed `Promise<IDb>` maps to the same token as `IDb` (`"pkg:IDb"`). Promise-ness lives in the factory's return, not in a separate token.
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
## Factory injection
|
|
190
|
+
|
|
191
|
+
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.
|
|
192
|
+
|
|
193
|
+
```typescript
|
|
194
|
+
// IDb is a registered class. This parameter receives a callable:
|
|
195
|
+
constructor(makeDb: () => IDb) { ... }
|
|
196
|
+
|
|
197
|
+
// Partial factory — the target ctor has holes the caller fills:
|
|
198
|
+
constructor(makeRepo: (tableName: string) => IUserRepo) { ... }
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### Named function-interface opt-out
|
|
202
|
+
|
|
203
|
+
A **named** callable interface is NOT treated as a factory — it resolves as a normal service keyed on that interface's own token:
|
|
204
|
+
|
|
205
|
+
```typescript
|
|
206
|
+
interface IDbFactory { (): IDb }
|
|
207
|
+
|
|
208
|
+
// Resolves as the "pkg:IDbFactory" token, not a factory for IDb
|
|
209
|
+
constructor(dbFactory: IDbFactory) { ... }
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
Name the interface to opt out of factory interpretation whenever your function-typed service should itself be a registered dep.
|
|
213
|
+
|
|
214
|
+
### Partial / positional factories
|
|
215
|
+
|
|
216
|
+
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.
|
|
217
|
+
|
|
218
|
+
```typescript
|
|
219
|
+
// IUserRepo concrete: constructor(log: ILogger, tableName: string, db: IDb)
|
|
220
|
+
// ILogger and IDb are registered; tableName is not (a hole).
|
|
221
|
+
// Injected factory type: (tableName: string) => IUserRepo
|
|
222
|
+
|
|
223
|
+
class RequestHandler {
|
|
224
|
+
constructor(private makeRepo: (tableName: string) => IUserRepo) {}
|
|
225
|
+
|
|
226
|
+
handle() {
|
|
227
|
+
const repo = this.makeRepo("users");
|
|
228
|
+
// At call time: new UserRepo(resolve(ILogger), "users", resolve(IDb))
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
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.
|
|
234
|
+
|
|
235
|
+
### Lifetime semantics
|
|
236
|
+
|
|
237
|
+
The injected factory is a closure captured at injection time, referencing the owning scope. How the target's instance is managed depends on whether the factory is parameterized:
|
|
238
|
+
|
|
239
|
+
| Factory kind | Lifetime behavior |
|
|
240
|
+
|---|---|
|
|
241
|
+
| **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. |
|
|
242
|
+
| **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. |
|
|
243
|
+
|
|
244
|
+
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.
|
|
245
|
+
|
|
246
|
+
### `FactoryTargetError`
|
|
247
|
+
|
|
248
|
+
Thrown when the container tries to build the factory callable and cannot. Two reasons:
|
|
249
|
+
|
|
250
|
+
| Reason | Meaning |
|
|
251
|
+
|---|---|
|
|
252
|
+
| `"unregistered"` | The factory's target token has no registration. A factory parameter needs the target registered with `services.add(...)`. |
|
|
253
|
+
| `"not-a-class"` | The target is registered as a `useValue` or `useFactory` override, not a class. A factory builds its target with `new`; only class registrations qualify. Resolve it directly or change the registration. |
|
|
254
|
+
|
|
255
|
+
Note: `FactoryTargetError` is thrown when the factory callable is constructed (at owning-class resolution time), not when the callable is invoked.
|
|
256
|
+
|
|
257
|
+
---
|
|
258
|
+
|
|
259
|
+
## API reference
|
|
260
|
+
|
|
261
|
+
### `DiBuilder<Scopes>`
|
|
262
|
+
|
|
263
|
+
| Member | Signature | Description |
|
|
264
|
+
|---|---|---|
|
|
265
|
+
| `add<I>(Concrete)` | `(ctor: new (...) => I) => RegistrationBuilder` | Register a concrete class against interface `I`. |
|
|
266
|
+
| `.as<S>()`| `(tag: S) → void` | Set the lifetime tag. No call → transient. |
|
|
267
|
+
| `register(token, opts)` | `(token: string, { useFactory? useValue? }) => void` | Override path. No dep metadata required. |
|
|
268
|
+
| `createScope(tag)` | `(tag: Scopes) => Scope<Scopes>` | Create the root scope. |
|
|
269
|
+
|
|
270
|
+
### `Scope<Scopes>`
|
|
271
|
+
|
|
272
|
+
| Member | Signature | Description |
|
|
273
|
+
|---|---|---|
|
|
274
|
+
| `createScope(tag)` | `(tag: Scopes) => Scope<Scopes>` | Create a child scope. |
|
|
275
|
+
| `resolve<T>(token)` | `(token: string) => T` | Resolve an instance. Throws on captive-dep violation, missing tag ancestor, or cycle. |
|
|
276
|
+
| `dispose()` | `() => void` | Sync close. Throws if any owned instance has async-only disposal. |
|
|
277
|
+
| `disposeAsync()` | `() => Promise<void>` | Async close. |
|
|
278
|
+
| `[Symbol.dispose]()` | — | Native `using` support. |
|
|
279
|
+
| `[Symbol.asyncDispose]()` | — | Native `await using` support. |
|
|
280
|
+
|
|
281
|
+
---
|
|
282
|
+
|
|
283
|
+
## TypeScript configuration
|
|
284
|
+
|
|
285
|
+
Disposal support requires `"ESNext.Disposable"` in your `lib` array. `"ES2022"` alone does not include the disposal symbols.
|
|
286
|
+
|
|
287
|
+
```jsonc
|
|
288
|
+
{
|
|
289
|
+
"compilerOptions": {
|
|
290
|
+
"target": "ES2022",
|
|
291
|
+
"lib": ["ES2022", "ESNext.Disposable"]
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
```
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { Token } from "@fnioc/core";
|
|
2
|
+
import { Scope } from "./scope.js";
|
|
3
|
+
import type { Ctor, OverrideSpec } from "./types.js";
|
|
4
|
+
/**
|
|
5
|
+
* The continuation returned by `DiBuilder.add`. Carries the just-added
|
|
6
|
+
* registration so `.as()` can attach its lifetime tag in place. An `.add()`
|
|
7
|
+
* with no trailing `.as()` leaves the registration untagged ⇒ transient.
|
|
8
|
+
*
|
|
9
|
+
* `Scopes` is threaded so `.as()` only accepts a declared scope name —
|
|
10
|
+
* compile-time captive-misconfiguration guard at the registration site.
|
|
11
|
+
*/
|
|
12
|
+
export interface AddBuilder<Scopes extends string> {
|
|
13
|
+
/**
|
|
14
|
+
* Attaches the lifetime tag. Must name a declared scope.
|
|
15
|
+
*
|
|
16
|
+
* Two call shapes, by design (PRD §7):
|
|
17
|
+
* - AUTHORED `.as<"singleton">()` — the scope name is a TYPE argument; the
|
|
18
|
+
* `S extends Scopes` bound is the compile-time captive-misconfiguration
|
|
19
|
+
* guard. No value argument is passed; this form is never executed (the
|
|
20
|
+
* transformer rewrites it before it runs).
|
|
21
|
+
* - LOWERED `.as("singleton")` — the transformer rewrites the type
|
|
22
|
+
* argument to a value argument. This is the form the engine executes; the
|
|
23
|
+
* runtime reads the tag from the value arg.
|
|
24
|
+
*
|
|
25
|
+
* `scope` is therefore OPTIONAL at the type level: the authored form supplies
|
|
26
|
+
* it as a type arg only, the lowered form as a value. A bare `.as()` with no
|
|
27
|
+
* type arg leaves `S = Scopes` and is a degenerate (untagged) call — use the
|
|
28
|
+
* type arg.
|
|
29
|
+
*/
|
|
30
|
+
as<S extends Scopes>(scope?: S): void;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* The registration builder.
|
|
34
|
+
*
|
|
35
|
+
* `Scopes` is the user-supplied scope-name union (e.g.
|
|
36
|
+
* `"singleton" | "request"`). `"transient"` is NOT a member — transient is the
|
|
37
|
+
* absence of a tag, not a scope.
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* ```ts
|
|
41
|
+
* const services = new DiBuilder<"singleton" | "request">();
|
|
42
|
+
* services.add("pkg:ILogger", ConsoleLogger).as("singleton"); // lowered form
|
|
43
|
+
* const root = services.createScope("singleton");
|
|
44
|
+
* const logger = root.resolve<ILogger>("pkg:ILogger");
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
export declare class DiBuilder<Scopes extends string = string> {
|
|
48
|
+
private readonly registrations;
|
|
49
|
+
/**
|
|
50
|
+
* Type-only authoring overload — the form the transformer rewrites FROM. The
|
|
51
|
+
* concrete is typed `new (...args: any[]) => I` (plain `new`, so an abstract
|
|
52
|
+
* class is rejected). At runtime the engine only ever receives the
|
|
53
|
+
* string-token form below; this signature exists purely so type-driven
|
|
54
|
+
* authoring type-checks before the transformer lowers it.
|
|
55
|
+
*/
|
|
56
|
+
add<I>(ctor: new (...args: any[]) => I): AddBuilder<Scopes>;
|
|
57
|
+
/**
|
|
58
|
+
* The runtime reality — a string token bound to a concrete constructor. This
|
|
59
|
+
* is what the transformer emits and what the engine actually executes.
|
|
60
|
+
*/
|
|
61
|
+
add(token: Token, ctor: Ctor): AddBuilder<Scopes>;
|
|
62
|
+
/**
|
|
63
|
+
* The plugin-less override path. Registers a token against either a
|
|
64
|
+
* `useFactory` closure (which resolves its own deps from the scope passed to
|
|
65
|
+
* it) or a `useValue` instance. The recommended mechanism for test doubles,
|
|
66
|
+
* third-party instances, and plugin-less wiring.
|
|
67
|
+
*
|
|
68
|
+
* A `useFactory` may carry an optional `tag` so the factory's result is
|
|
69
|
+
* cached at a matching ancestor scope (singleton-style); without a tag it
|
|
70
|
+
* runs on every resolve. A `useValue` is the instance itself — always the
|
|
71
|
+
* same value, no lifetime.
|
|
72
|
+
*/
|
|
73
|
+
register<T>(token: Token, spec: OverrideSpec<T>): this;
|
|
74
|
+
/**
|
|
75
|
+
* Mints the root scope. The root must be a real, app-lifetime object — its
|
|
76
|
+
* name is the lifetime tag that singletons (or whatever the app's longest
|
|
77
|
+
* lifetime is) bind to.
|
|
78
|
+
*/
|
|
79
|
+
createScope(rootScopeName: Scopes): Scope<Scopes>;
|
|
80
|
+
}
|
|
81
|
+
//# sourceMappingURL=builder.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"builder.d.ts","sourceRoot":"","sources":["../src/builder.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AAEzC,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,OAAO,KAAK,EAEV,IAAI,EACJ,YAAY,EAGb,MAAM,YAAY,CAAC;AAEpB;;;;;;;GAOG;AACH,MAAM,WAAW,UAAU,CAAC,MAAM,SAAS,MAAM;IAC/C;;;;;;;;;;;;;;;;OAgBG;IACH,EAAE,CAAC,CAAC,SAAS,MAAM,EAAE,KAAK,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC;CACvC;AAED;;;;;;;;;;;;;;GAcG;AACH,qBAAa,SAAS,CAAC,MAAM,SAAS,MAAM,GAAG,MAAM;IACnD,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAkC;IAEhE;;;;;;OAMG;IACI,GAAG,CAAC,CAAC,EAAE,IAAI,EAAE,KAAK,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,CAAC,GAAG,UAAU,CAAC,MAAM,CAAC;IAClE;;;OAGG;IACI,GAAG,CAAC,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,GAAG,UAAU,CAAC,MAAM,CAAC;IAsCxD;;;;;;;;;;OAUG;IACI,QAAQ,CAAC,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,YAAY,CAAC,CAAC,CAAC,GAAG,IAAI;IAa7D;;;;OAIG;IACI,WAAW,CAAC,aAAa,EAAE,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC;CAGzD"}
|
package/dist/builder.js
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// The registration builder. Holds the base token → registration map and hands
|
|
2
|
+
// out a root Scope. `.add()` is the surface the transformer lowers to; the
|
|
3
|
+
// override paths (`.register`) are the recommended plugin-less mechanism.
|
|
4
|
+
import { Scope } from "./scope.js";
|
|
5
|
+
/**
|
|
6
|
+
* The registration builder.
|
|
7
|
+
*
|
|
8
|
+
* `Scopes` is the user-supplied scope-name union (e.g.
|
|
9
|
+
* `"singleton" | "request"`). `"transient"` is NOT a member — transient is the
|
|
10
|
+
* absence of a tag, not a scope.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```ts
|
|
14
|
+
* const services = new DiBuilder<"singleton" | "request">();
|
|
15
|
+
* services.add("pkg:ILogger", ConsoleLogger).as("singleton"); // lowered form
|
|
16
|
+
* const root = services.createScope("singleton");
|
|
17
|
+
* const logger = root.resolve<ILogger>("pkg:ILogger");
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
export class DiBuilder {
|
|
21
|
+
registrations = new Map();
|
|
22
|
+
add(tokenOrCtor, maybeCtor) {
|
|
23
|
+
// Only the string-token form reaches the engine at runtime. The type-only
|
|
24
|
+
// overload is never actually invoked post-transform; guard defensively so a
|
|
25
|
+
// hand-written type-form call fails loud rather than registering garbage.
|
|
26
|
+
if (typeof tokenOrCtor !== "string" || maybeCtor === undefined) {
|
|
27
|
+
throw new TypeError('add<I>(ctor) requires the @fnioc/transformer plugin. Without it, ' +
|
|
28
|
+
'register with an explicit token: add("my:token", MyClass).');
|
|
29
|
+
}
|
|
30
|
+
const registration = {
|
|
31
|
+
kind: "class",
|
|
32
|
+
ctor: maybeCtor,
|
|
33
|
+
tag: undefined,
|
|
34
|
+
};
|
|
35
|
+
this.registrations.set(tokenOrCtor, registration);
|
|
36
|
+
// `.as()` rebinds the registration with its tag. The map holds an immutable
|
|
37
|
+
// record, so swap in a fresh one rather than mutating the readonly field.
|
|
38
|
+
const token = tokenOrCtor;
|
|
39
|
+
const registrations = this.registrations;
|
|
40
|
+
return {
|
|
41
|
+
as(scope) {
|
|
42
|
+
// The lowered form always passes a value arg; the authored type-arg-only
|
|
43
|
+
// form never executes (the transformer rewrites it first). A no-arg call
|
|
44
|
+
// at runtime would leave the registration transient — guard so it is a
|
|
45
|
+
// no-op rather than overwriting the tag with `undefined`.
|
|
46
|
+
if (scope === undefined)
|
|
47
|
+
return;
|
|
48
|
+
registrations.set(token, { ...registration, tag: scope });
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* The plugin-less override path. Registers a token against either a
|
|
54
|
+
* `useFactory` closure (which resolves its own deps from the scope passed to
|
|
55
|
+
* it) or a `useValue` instance. The recommended mechanism for test doubles,
|
|
56
|
+
* third-party instances, and plugin-less wiring.
|
|
57
|
+
*
|
|
58
|
+
* A `useFactory` may carry an optional `tag` so the factory's result is
|
|
59
|
+
* cached at a matching ancestor scope (singleton-style); without a tag it
|
|
60
|
+
* runs on every resolve. A `useValue` is the instance itself — always the
|
|
61
|
+
* same value, no lifetime.
|
|
62
|
+
*/
|
|
63
|
+
register(token, spec) {
|
|
64
|
+
if ("useValue" in spec) {
|
|
65
|
+
this.registrations.set(token, { kind: "value", useValue: spec.useValue });
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
this.registrations.set(token, {
|
|
69
|
+
kind: "factory",
|
|
70
|
+
useFactory: spec.useFactory,
|
|
71
|
+
tag: spec.tag,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
return this;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Mints the root scope. The root must be a real, app-lifetime object — its
|
|
78
|
+
* name is the lifetime tag that singletons (or whatever the app's longest
|
|
79
|
+
* lifetime is) bind to.
|
|
80
|
+
*/
|
|
81
|
+
createScope(rootScopeName) {
|
|
82
|
+
return new Scope(rootScopeName, undefined, this.registrations);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
//# sourceMappingURL=builder.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"builder.js","sourceRoot":"","sources":["../src/builder.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,2EAA2E;AAC3E,0EAA0E;AAI1E,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAsCnC;;;;;;;;;;;;;;GAcG;AACH,MAAM,OAAO,SAAS;IACH,aAAa,GAAG,IAAI,GAAG,EAAuB,CAAC;IAezD,GAAG,CACR,WAAsD,EACtD,SAAgB;QAEhB,0EAA0E;QAC1E,4EAA4E;QAC5E,0EAA0E;QAC1E,IAAI,OAAO,WAAW,KAAK,QAAQ,IAAI,SAAS,KAAK,SAAS,EAAE,CAAC;YAC/D,MAAM,IAAI,SAAS,CACjB,mEAAmE;gBACjE,4DAA4D,CAC/D,CAAC;QACJ,CAAC;QAED,MAAM,YAAY,GAAsB;YACtC,IAAI,EAAE,OAAO;YACb,IAAI,EAAE,SAAS;YACf,GAAG,EAAE,SAAS;SACf,CAAC;QACF,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,WAAW,EAAE,YAAY,CAAC,CAAC;QAElD,4EAA4E;QAC5E,0EAA0E;QAC1E,MAAM,KAAK,GAAG,WAAW,CAAC;QAC1B,MAAM,aAAa,GAAG,IAAI,CAAC,aAAa,CAAC;QACzC,OAAO;YACL,EAAE,CAAmB,KAAS;gBAC5B,yEAAyE;gBACzE,yEAAyE;gBACzE,uEAAuE;gBACvE,0DAA0D;gBAC1D,IAAI,KAAK,KAAK,SAAS;oBAAE,OAAO;gBAChC,aAAa,CAAC,GAAG,CAAC,KAAK,EAAE,EAAE,GAAG,YAAY,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC,CAAC;YAC5D,CAAC;SACF,CAAC;IACJ,CAAC;IAED;;;;;;;;;;OAUG;IACI,QAAQ,CAAI,KAAY,EAAE,IAAqB;QACpD,IAAI,UAAU,IAAI,IAAI,EAAE,CAAC;YACvB,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,KAAK,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE,IAAI,CAAC,QAAQ,EAAE,CAAC,CAAC;QAC5E,CAAC;aAAM,CAAC;YACN,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,KAAK,EAAE;gBAC5B,IAAI,EAAE,SAAS;gBACf,UAAU,EAAE,IAAI,CAAC,UAA8C;gBAC/D,GAAG,EAAE,IAAI,CAAC,GAAG;aACd,CAAC,CAAC;QACL,CAAC;QACD,OAAO,IAAI,CAAC;IACd,CAAC;IAED;;;;OAIG;IACI,WAAW,CAAC,aAAqB;QACtC,OAAO,IAAI,KAAK,CAAS,aAAa,EAAE,SAAS,EAAE,IAAI,CAAC,aAAa,CAAC,CAAC;IACzE,CAAC;CACF"}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { Token } from "@fnioc/core";
|
|
2
|
+
/** Base class for every error the container raises. */
|
|
3
|
+
export declare class DiError extends Error {
|
|
4
|
+
constructor(message: string);
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* A token was requested but no registration exists for it anywhere in the
|
|
8
|
+
* resolving scope's chain (nor on the builder's base map).
|
|
9
|
+
*/
|
|
10
|
+
export declare class UnregisteredTokenError extends DiError {
|
|
11
|
+
readonly token: Token;
|
|
12
|
+
constructor(token: Token);
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* A registration carries a lifetime tag, but no ancestor scope in the resolving
|
|
16
|
+
* chain has a matching name. This is the captive-dependency / misconfiguration
|
|
17
|
+
* detector — the engine never auto-creates a scope to satisfy the tag.
|
|
18
|
+
*/
|
|
19
|
+
export declare class MissingScopeError extends DiError {
|
|
20
|
+
readonly token: Token;
|
|
21
|
+
readonly tag: string;
|
|
22
|
+
readonly availableScopes: readonly string[];
|
|
23
|
+
constructor(token: Token, tag: string, availableScopes: readonly string[]);
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* A constructor with parameters has no DepRecord in the WeakMap — the
|
|
27
|
+
* transformer never saw it and it was never hand-annotated.
|
|
28
|
+
*/
|
|
29
|
+
export declare class MissingMetadataError extends DiError {
|
|
30
|
+
readonly token: Token;
|
|
31
|
+
readonly ctorName: string;
|
|
32
|
+
constructor(token: Token, ctorName: string);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* A constructor has DepRecord signatures, but none of them is directly
|
|
36
|
+
* satisfiable in the owning scope (every signature names at least one token
|
|
37
|
+
* that is not registered, or contains a hole this phase cannot fill).
|
|
38
|
+
*/
|
|
39
|
+
export declare class NoSatisfiableSignatureError extends DiError {
|
|
40
|
+
readonly token: Token;
|
|
41
|
+
readonly ctorName: string;
|
|
42
|
+
readonly unsatisfiable: readonly Token[];
|
|
43
|
+
constructor(token: Token, ctorName: string, unsatisfiable: readonly Token[]);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* A token reappeared on the active resolution stack — the dependency graph has
|
|
47
|
+
* a cycle. The message includes the full path that closed the loop.
|
|
48
|
+
*/
|
|
49
|
+
export declare class CircularDependencyError extends DiError {
|
|
50
|
+
readonly path: readonly Token[];
|
|
51
|
+
constructor(path: readonly Token[]);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* A constructor parameter is typed as a factory of some token (a `FactoryRef`),
|
|
55
|
+
* but that token cannot be turned into a factory: either it is not registered,
|
|
56
|
+
* or it is registered as a `useValue` / `useFactory` override rather than a
|
|
57
|
+
* class. A factory injects a callable that constructs the target class on
|
|
58
|
+
* demand, so the target must be a class registration.
|
|
59
|
+
*/
|
|
60
|
+
export declare class FactoryTargetError extends DiError {
|
|
61
|
+
readonly factoryToken: Token;
|
|
62
|
+
readonly reason: "unregistered" | "not-a-class";
|
|
63
|
+
constructor(factoryToken: Token, reason: "unregistered" | "not-a-class");
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Sync `dispose()` was called on a scope that owns a Promise-valued (thenable)
|
|
67
|
+
* cached instance. A pending Promise cannot be disposed synchronously — the
|
|
68
|
+
* caller must use `disposeAsync()`.
|
|
69
|
+
*/
|
|
70
|
+
export declare class AsyncDisposalRequiredError extends DiError {
|
|
71
|
+
constructor();
|
|
72
|
+
}
|
|
73
|
+
//# sourceMappingURL=errors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AAEzC,uDAAuD;AACvD,qBAAa,OAAQ,SAAQ,KAAK;gBACb,OAAO,EAAE,MAAM;CAInC;AAED;;;GAGG;AACH,qBAAa,sBAAuB,SAAQ,OAAO;aACd,KAAK,EAAE,KAAK;gBAAZ,KAAK,EAAE,KAAK;CAMhD;AAED;;;;GAIG;AACH,qBAAa,iBAAkB,SAAQ,OAAO;aAE1B,KAAK,EAAE,KAAK;aACZ,GAAG,EAAE,MAAM;aACX,eAAe,EAAE,SAAS,MAAM,EAAE;gBAFlC,KAAK,EAAE,KAAK,EACZ,GAAG,EAAE,MAAM,EACX,eAAe,EAAE,SAAS,MAAM,EAAE;CAerD;AAED;;;GAGG;AACH,qBAAa,oBAAqB,SAAQ,OAAO;aAE7B,KAAK,EAAE,KAAK;aACZ,QAAQ,EAAE,MAAM;gBADhB,KAAK,EAAE,KAAK,EACZ,QAAQ,EAAE,MAAM;CAUnC;AAED;;;;GAIG;AACH,qBAAa,2BAA4B,SAAQ,OAAO;aAEpC,KAAK,EAAE,KAAK;aACZ,QAAQ,EAAE,MAAM;aAChB,aAAa,EAAE,SAAS,KAAK,EAAE;gBAF/B,KAAK,EAAE,KAAK,EACZ,QAAQ,EAAE,MAAM,EAChB,aAAa,EAAE,SAAS,KAAK,EAAE;CAelD;AAED;;;GAGG;AACH,qBAAa,uBAAwB,SAAQ,OAAO;aACf,IAAI,EAAE,SAAS,KAAK,EAAE;gBAAtB,IAAI,EAAE,SAAS,KAAK,EAAE;CAG1D;AAED;;;;;;GAMG;AACH,qBAAa,kBAAmB,SAAQ,OAAO;aAE3B,YAAY,EAAE,KAAK;aACnB,MAAM,EAAE,cAAc,GAAG,aAAa;gBADtC,YAAY,EAAE,KAAK,EACnB,MAAM,EAAE,cAAc,GAAG,aAAa;CAezD;AAED;;;;GAIG;AACH,qBAAa,0BAA2B,SAAQ,OAAO;;CAQtD"}
|