@fnioc/di 1.0.0 → 2.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
@@ -6,16 +6,18 @@ No decorators. No `reflect-metadata`. No runtime type introspection. Feed it str
6
6
 
7
7
  ---
8
8
 
9
- ## `DiBuilder<Scopes>`
9
+ ## `DiBuilder<Root, Children>`
10
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.
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
  ```typescript
14
14
  import { DiBuilder } from "@fnioc/di";
15
15
 
16
- const services = new DiBuilder<"singleton" | "request">();
16
+ const services = new DiBuilder<"singleton", "request">();
17
17
  ```
18
18
 
19
+ Registration is append-only: each token holds a **list** of registrations in registration order, and resolution picks the most-recent (last) one. A later `add` for the same token therefore overrides an earlier one without deleting it.
20
+
19
21
  ### `.add<Interface>(Concrete).as<"scope">()`
20
22
 
21
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,7 +26,7 @@ Register a concrete implementation against an interface token. The transformer r
24
26
  // With transformer (author form):
25
27
  services.add<ILogger>(ConsoleLogger).as<"singleton">();
26
28
  services.add<IUserRepo>(SqlUserRepo).as<"request">();
27
- services.add<IRequestId>(UuidRequestId>(); // no .as() → transient
29
+ services.add<IRequestId>(UuidRequestId); // no .as() → transient
28
30
 
29
31
  // Without transformer (lowered form, or plugin-less):
30
32
  services.add("pkg:ILogger", ConsoleLogger).as("singleton");
@@ -36,39 +38,43 @@ The type constraint on `Concrete` is `new (...args: any[]) => Interface` — pla
36
38
 
37
39
  `.as<S>()` checks at compile time that `S` is a declared scope name. Passing an undeclared string is a type error.
38
40
 
39
- ### `useFactory` and `useValue`
41
+ ### `add(token, { useFactory })` and `add(token, { useValue })`
40
42
 
41
- Override paths that bypass the dep-metadata system entirely. Recommended for test doubles, third-party instances, and plugin-less consumers.
43
+ 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.
42
44
 
43
45
  ```typescript
44
- // Factory: receives the scope's container, returns the instance
45
- container.register("pkg:IDb", {
46
+ // Factory: receives the scope, returns the instance. An optional `scope`
47
+ // caches the result at the matching ancestor (singleton-style).
48
+ services.add("pkg:IDb", {
46
49
  useFactory: (c) => new PostgresDb(c.resolve<IConfig>("pkg:IConfig")),
50
+ scope: "singleton",
47
51
  });
48
52
 
49
- // Value: a pre-constructed instance (always singleton-like; re-used as-is)
50
- container.register("pkg:ICache", {
53
+ // Value: a pre-constructed instance (re-used as-is, no lifetime)
54
+ services.add("pkg:ICache", {
51
55
  useValue: new NullCache(),
52
56
  });
53
57
  ```
54
58
 
55
- `useFactory` with `.as("singleton")` on the parent builder makes the factory run once and cache the result. `useValue` is always cached.
59
+ A `useFactory` with `scope: "singleton"` runs once and caches the result; without a `scope` it runs on every resolve (transient). `useValue` is always the same reference.
60
+
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.
56
62
 
57
63
  ---
58
64
 
59
65
  ## Scope model
60
66
 
61
- Scopes form a parent-linked chain. The root scope is a real, app-lifetime object — never auto-created by the container.
67
+ 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`.
62
68
 
63
69
  ```typescript
64
- const root = services.createScope("singleton"); // app lifetime
65
- const req = root.createScope("request"); // per HTTP request
70
+ const root = services.build(); // app lifetime — named by Root
71
+ const req = root.createScope("request"); // per HTTP request
66
72
  ```
67
73
 
68
74
  **Resolution walks the parent chain** for two purposes:
69
75
 
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.
76
+ 1. **Registration lookup** — walks up until the token is found, taking the most-recent registration at the nearest scope. A child scope can shadow a parent registration.
77
+ 2. **Instance ownership** — the lifetime scope names which ancestor scope caches the instance. Walks up to the nearest matching ancestor and caches there.
72
78
 
73
79
  **Lifetime rules:**
74
80
 
@@ -77,20 +83,20 @@ const req = root.createScope("request"); // per HTTP request
77
83
  | No `.as()` (transient) | Fresh instance on every resolve. Never cached. |
78
84
  | `.as("singleton")` | Owned and cached by the nearest `"singleton"` ancestor in the chain. |
79
85
  | `.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. |
86
+ | Scope with no matching ancestor | **Throws.** The missing-ancestor error is intentional — see captive-dependency protection below. |
81
87
 
82
88
  ### Captive-dependency protection
83
89
 
84
90
  The critical correctness rule: deps are resolved **relative to the scope that will own the instance**, not the scope that triggered the resolve.
85
91
 
86
92
  ```typescript
87
- const services = new DiBuilder<"singleton" | "request">();
93
+ const services = new DiBuilder<"singleton", "request">();
88
94
  services.add<ICache>(RedisCache).as<"singleton">();
89
95
  services.add<IUserContext>(HttpUserContext).as<"request">();
90
96
  services.add<IUserService>(UserService).as<"singleton">();
91
97
  // UserService constructor: (cache: ICache, ctx: IUserContext)
92
98
 
93
- const root = services.createScope("singleton");
99
+ const root = services.build();
94
100
  const req = root.createScope("request");
95
101
 
96
102
  req.resolve<IUserService>("pkg:IUserService");
@@ -164,11 +170,12 @@ The container never awaits. Async is expressed as `Promise<T>` values through th
164
170
 
165
171
  ```typescript
166
172
  // Register an async factory
167
- container.register("pkg:IDb", {
173
+ services.add("pkg:IDb", {
168
174
  useFactory: async (c) => {
169
175
  const pool = c.resolve<IConnectionPool>("pkg:IConnectionPool");
170
176
  return new PostgresDb(await pool.connect());
171
177
  },
178
+ scope: "singleton",
172
179
  });
173
180
 
174
181
  // Consume it — declare the dep as Promise<IDb>
@@ -258,21 +265,23 @@ Note: `FactoryTargetError` is thrown when the factory callable is constructed (a
258
265
 
259
266
  ## API reference
260
267
 
261
- ### `DiBuilder<Scopes>`
268
+ ### `DiBuilder<Root, Children>`
262
269
 
263
270
  | Member | Signature | Description |
264
271
  |---|---|---|
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. |
272
+ | `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. |
274
+ | `add(token, ctor)` | `(token: string, ctor) => AddBuilder` | Class registration (lowered form). |
275
+ | `add(token, spec)` | `(token: string, { useFactory, scope? } \| { useValue }) => this` | Factory / value registration. No dep metadata required. |
276
+ | `build()` | `() => Scope<Root \| Children>` | Mint the root scope (named `Root`). No argument. |
269
277
 
270
278
  ### `Scope<Scopes>`
271
279
 
272
280
  | Member | Signature | Description |
273
281
  |---|---|---|
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. |
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
+ | `resolve<T>(token)` | `(token: string) => T` | Resolve an instance. Throws on captive-dep violation, missing scope ancestor, or cycle. |
276
285
  | `dispose()` | `() => void` | Sync close. Throws if any owned instance has async-only disposal. |
277
286
  | `disposeAsync()` | `() => Promise<void>` | Async close. |
278
287
  | `[Symbol.dispose]()` | — | Native `using` support. |