di-bag 0.2.0 → 0.4.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/AGENTS.md +150 -0
- package/README.md +68 -51
- package/dist/acquisition-context.d.ts +80 -5
- package/dist/acquisition-context.js +30 -2
- package/dist/acquisition-mode.d.ts +17 -4
- package/dist/acquisition-mode.js +33 -7
- package/dist/acquisition.d.ts +13 -2
- package/dist/acquisition.js +44 -8
- package/dist/alias-types.d.ts +12 -3
- package/dist/composition-report.d.ts +3 -2
- package/dist/composition.d.ts +8 -2
- package/dist/contribution-types.d.ts +26 -18
- package/dist/dependency-references.d.ts +16 -4
- package/dist/di-bag.d.ts +344 -42
- package/dist/di-bag.js +87 -15
- package/dist/errors.d.ts +115 -8
- package/dist/errors.js +113 -12
- package/dist/index.d.ts +6 -6
- package/dist/index.js +2 -1
- package/dist/inspection.d.ts +21 -5
- package/dist/lifetime-types.d.ts +281 -180
- package/dist/lifetime.d.ts +4 -1
- package/dist/module-types.d.ts +87 -30
- package/dist/module.d.ts +18 -4
- package/dist/module.js +31 -7
- package/dist/observers.d.ts +25 -6
- package/dist/plugins.d.ts +17 -4
- package/dist/provider-execution.d.ts +38 -2
- package/dist/provider-execution.js +145 -17
- package/dist/provider.d.ts +41 -10
- package/dist/provider.js +1 -0
- package/dist/registration.d.ts +8 -2
- package/dist/registration.js +4 -1
- package/dist/runtime.d.ts +13 -3
- package/dist/runtime.js +37 -9
- package/dist/scope-types.d.ts +20 -5
- package/dist/startup.d.ts +19 -1
- package/dist/startup.js +81 -11
- package/dist/token-types.d.ts +26 -8
- package/dist/tokens.d.ts +10 -2
- package/dist/tokens.js +2 -0
- package/dist/types.d.ts +69 -23
- package/docs/agent/api-card.md +354 -0
- package/docs/agent/errors.md +1138 -0
- package/docs/agent/recipes.md +403 -0
- package/package.json +18 -5
package/AGENTS.md
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# DI Bag: notes for coding agents
|
|
2
|
+
|
|
3
|
+
DI Bag composes TypeScript factories into a dependency graph that the compiler
|
|
4
|
+
checks. Modules keep a feature's services private behind exported keys; a bag
|
|
5
|
+
creates services on first use and releases what it owns when closed.
|
|
6
|
+
|
|
7
|
+
This file ships in `node_modules/di-bag/`. Every call, with one way per task and an example:
|
|
8
|
+
[docs/agent/api-card.md](docs/agent/api-card.md). Task recipes: [docs/agent/recipes.md](docs/agent/recipes.md).
|
|
9
|
+
Every compiler and runtime message: [docs/agent/errors.md](docs/agent/errors.md).
|
|
10
|
+
|
|
11
|
+
## Rules
|
|
12
|
+
|
|
13
|
+
1. **Import from `di-bag`:** `import { DiBag } from 'di-bag';`. It configures
|
|
14
|
+
itself on Node, Bun, and Deno; `di-bag/node` is the same API in explicit form.
|
|
15
|
+
For browsers and workers register with `DiBag.fromSyncFactory` / `fromAsyncFactory`
|
|
16
|
+
([portable recipe](docs/agent/recipes.md#portable-graph)); a plain factory there fails
|
|
17
|
+
`build()` with [`DI_BAG_CLASSIFIER_REQUIRED`](docs/agent/errors.md#di-bag-classifier-required), which names it.
|
|
18
|
+
2. **A factory declares its dependencies in the type of its one object
|
|
19
|
+
parameter; destructure it** (`({ clock }: { clock: Clock }) => ...`) or read
|
|
20
|
+
`deps.clock` directly. The object is a Proxy that resolves each property when
|
|
21
|
+
read: spreading it, `Object.keys`, `in`, and `JSON.stringify` throw
|
|
22
|
+
[`DI_BAG_INVALID_DEPENDENCY_ACCESS`](docs/agent/errors.md#di-bag-invalid-dependency-access).
|
|
23
|
+
3. **Lifetimes.** The default is `scoped`: one instance per bag or child scope.
|
|
24
|
+
Mark a shared client `DiBag.withLifetime(factory, 'root')` only when nothing
|
|
25
|
+
it depends on is scoped; otherwise the compiler reports a
|
|
26
|
+
[root capture](docs/agent/errors.md#root-capture) naming both keys.
|
|
27
|
+
`'transient'` creates an instance on every read.
|
|
28
|
+
4. **Async is explicit.** An async factory's service is its Promise. A consumer
|
|
29
|
+
declares `{ db: Promise<Db> }` and awaits it; nothing is awaited for you.
|
|
30
|
+
5. **No thenables.** A factory that returns a non-Promise object with a `then`
|
|
31
|
+
method (query builders) is [rejected](docs/agent/errors.md#structural-thenable).
|
|
32
|
+
Return `Promise.resolve(builder)` or use `DiBag.fromFactory(create, { acquisitionMode: 'raw' })`.
|
|
33
|
+
6. **Ownership.** `DiBag.withDisposal(factory, dispose)` makes the bag own the
|
|
34
|
+
returned value; `close()` runs disposers, dependents first. Close every scope and
|
|
35
|
+
fork you create; a parent closes its live scopes, never forks. Inside a factory,
|
|
36
|
+
[`factoryCtx.pushDisposer`](docs/agent/recipes.md#partial-acquisition) owns what it acquires on the way; if that is also the returned value, act only when `disposerCtx.reason !== 'service-disposed'`.
|
|
37
|
+
7. **Replace dependencies in tests with `fork(keys, overrides)`**; each
|
|
38
|
+
override must satisfy the original contract.
|
|
39
|
+
8. **Modules.** Register a feature's factories, then `buildModule(['exported'])`.
|
|
40
|
+
What its factories need and the module does not register becomes a
|
|
41
|
+
requirement: the host that calls `installModule(module)` must register it.
|
|
42
|
+
Pass `buildModule(keys, { label: 'billing' })` so messages name private
|
|
43
|
+
services `billing/store`.
|
|
44
|
+
9. **Read a rejection at its name.** A graph error is an assignability error
|
|
45
|
+
whose type is `Unsatisfied<"message", details>`, reported where the builder
|
|
46
|
+
expression starts. `builder.verifyGraph() satisfies void;` reports the same
|
|
47
|
+
message on its own line; `"noErrorTruncation": true` prints the details.
|
|
48
|
+
Runtime errors carry `code` and `details`: branch on `code`, never on message
|
|
49
|
+
text. The section for a code is `docs/agent/errors.md#<code>`, lower-cased
|
|
50
|
+
with `_` replaced by `-`.
|
|
51
|
+
|
|
52
|
+
## Module layout
|
|
53
|
+
|
|
54
|
+
```text
|
|
55
|
+
src/features/invoicing/
|
|
56
|
+
contract.ts # exported service types and the requirements the host must supply
|
|
57
|
+
module.ts # buildModule([...]) over the private factories
|
|
58
|
+
store.ts # private services; free to use names other modules also use
|
|
59
|
+
check.ts # type-checks this module alone; never imported, not built
|
|
60
|
+
tsconfig.json # extends the root tsconfig and includes only this directory
|
|
61
|
+
invoicing.test.ts
|
|
62
|
+
src/app.ts # installs every module, one installModule call per line
|
|
63
|
+
src/app.check.ts # verifyGraph() on the application builder: the merge check
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Inside a file, keep the same order: contract types, private factories, the
|
|
67
|
+
sealed module, then the host. Exclude `check.ts` files and `src/app.check.ts`
|
|
68
|
+
from an emitting build; they have no runtime purpose.
|
|
69
|
+
|
|
70
|
+
A complete module:
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
// src/features/greeting/contract.ts
|
|
74
|
+
export type Greeter = { greet(name: string): string };
|
|
75
|
+
export type GreetingConfig = { greeting: string };
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
// src/features/greeting/module.ts
|
|
80
|
+
import { DiBag } from 'di-bag';
|
|
81
|
+
import type { Greeter, GreetingConfig } from './contract.js';
|
|
82
|
+
|
|
83
|
+
export const greetingModule = DiBag.createBuilder()
|
|
84
|
+
.register({
|
|
85
|
+
greeter: ({ config }: { config: GreetingConfig }): Greeter => ({
|
|
86
|
+
greet: name => `${config.greeting}, ${name}!`,
|
|
87
|
+
}),
|
|
88
|
+
})
|
|
89
|
+
.buildModule(['greeter']);
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
`check.ts` is one statement: install the module, register a typed fixture for
|
|
93
|
+
each requirement, and verify.
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
// src/features/greeting/check.ts
|
|
97
|
+
import { DiBag } from 'di-bag';
|
|
98
|
+
import type { GreetingConfig } from './contract.js';
|
|
99
|
+
import { greetingModule } from './module.js';
|
|
100
|
+
|
|
101
|
+
DiBag.createBuilder()
|
|
102
|
+
.installModule(greetingModule)
|
|
103
|
+
.register({ config: (): GreetingConfig => ({ greeting: 'Hello' }) })
|
|
104
|
+
.verifyGraph() satisfies void;
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Check one module
|
|
108
|
+
|
|
109
|
+
`src/features/<name>/tsconfig.json`:
|
|
110
|
+
|
|
111
|
+
```json
|
|
112
|
+
{
|
|
113
|
+
"extends": "../../../tsconfig.json",
|
|
114
|
+
"include": ["."]
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Type-check the module and its `check.ts` without the rest of the application:
|
|
119
|
+
|
|
120
|
+
```sh
|
|
121
|
+
npx tsc --noEmit -p src/features/<name>/tsconfig.json
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
A missing requirement fails with its key:
|
|
125
|
+
`required service registrations are missing: config`.
|
|
126
|
+
|
|
127
|
+
## Fast check
|
|
128
|
+
|
|
129
|
+
Define `check:fast` in the consumer's `package.json` as the per-module
|
|
130
|
+
type-check plus that module's tests, and run it after every edit:
|
|
131
|
+
|
|
132
|
+
```json
|
|
133
|
+
"check:fast": "tsc --noEmit -p src/features/$MODULE/tsconfig.json && tsx --test src/features/$MODULE/*.test.ts"
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
```sh
|
|
137
|
+
MODULE=greeting npm run check:fast
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Replace `tsx --test` with the project's test runner. Run the full type-check and
|
|
141
|
+
test suite before merging: [review a merge](docs/agent/recipes.md#review-merge).
|
|
142
|
+
|
|
143
|
+
## Recipes
|
|
144
|
+
|
|
145
|
+
- [Add a request-scoped service with cleanup](docs/agent/recipes.md#add-scoped-service)
|
|
146
|
+
- [Write a fixture test with `fork`](docs/agent/recipes.md#fixture-test)
|
|
147
|
+
- [Split a feature into a module with private services](docs/agent/recipes.md#split-module)
|
|
148
|
+
- [Debug a missing-dependency rejection](docs/agent/recipes.md#debug-missing-dependency)
|
|
149
|
+
- [Add and consume an async client](docs/agent/recipes.md#async-client)
|
|
150
|
+
- [Review a merge](docs/agent/recipes.md#review-merge)
|
package/README.md
CHANGED
|
@@ -1,18 +1,21 @@
|
|
|
1
1
|
# DI Bag
|
|
2
2
|
|
|
3
|
-
TypeScript dependency composition and resource ownership for
|
|
3
|
+
TypeScript dependency composition and resource ownership for modular codebases.
|
|
4
|
+
Built for coding agents that ship one feature at a time.
|
|
4
5
|
|
|
5
|
-
[Documentation](https://dany-fedorov.github.io/di-bag/) · [Quickstart](#quickstart) · [
|
|
6
|
+
[Documentation](https://dany-fedorov.github.io/di-bag/) · [Quickstart](#quickstart) · [Modules as units of work](#modules-as-units-of-work) · [Comparison](#how-it-compares) · [Tutorial](docs/guides/tutorial.md) · [API reference](docs/guides/api-reference.md)
|
|
6
7
|
|
|
7
8
|
## Why DI Bag?
|
|
8
9
|
|
|
9
10
|
Compose ordinary TypeScript factories into reusable features. DI Bag checks
|
|
10
11
|
declared dependencies, keeps module internals private, and manages resource
|
|
11
|
-
creation and cleanup. Build
|
|
12
|
-
|
|
12
|
+
creation and cleanup. Build each feature against an explicit contract, test it
|
|
13
|
+
with replaced dependencies, and let the compiler check the composition when
|
|
14
|
+
independently developed features come together.
|
|
13
15
|
|
|
14
|
-
- **[
|
|
15
|
-
|
|
16
|
+
- **[Modules a single owner can build in isolation](docs/guides/examples-modularity.md).**
|
|
17
|
+
A person or a coding agent implements one feature against its contract
|
|
18
|
+
without exposing its internals or reading another feature's source.
|
|
16
19
|
- **[Compile-time wiring checks](docs/guides/examples-type-checking.md).**
|
|
17
20
|
Catch missing dependencies and incompatible replacements before starting the app.
|
|
18
21
|
- **[Metadata inspection without service startup](docs/guides/examples-extensibility.md).**
|
|
@@ -42,11 +45,11 @@ A **factory** creates a service. A **bag** holds those factories and gives each
|
|
|
42
45
|
one access to the services it needs. Services are created when needed, and
|
|
43
46
|
resources are cleaned up when you provide a disposer and close their bag.
|
|
44
47
|
|
|
45
|
-
|
|
48
|
+
Import from `di-bag`. Here, `greeter` needs `config`. Its parameter
|
|
46
49
|
type describes that dependency, and its return value is the service it provides:
|
|
47
50
|
|
|
48
51
|
```ts
|
|
49
|
-
import { DiBag } from 'di-bag
|
|
52
|
+
import { DiBag } from 'di-bag';
|
|
50
53
|
|
|
51
54
|
const app = DiBag.createBuilder()
|
|
52
55
|
.register({
|
|
@@ -100,7 +103,7 @@ An async factory provides a promise. Declare that promise in any dependent
|
|
|
100
103
|
factory and await it where you need the value:
|
|
101
104
|
|
|
102
105
|
```ts
|
|
103
|
-
import { DiBag } from 'di-bag
|
|
106
|
+
import { DiBag } from 'di-bag';
|
|
104
107
|
|
|
105
108
|
const app = DiBag.createBuilder()
|
|
106
109
|
.register({
|
|
@@ -122,7 +125,7 @@ factories keep returning ordinary values. See
|
|
|
122
125
|
Wrap a factory with `withDisposal` to tell the bag how to release its result:
|
|
123
126
|
|
|
124
127
|
```ts
|
|
125
|
-
import { DiBag } from 'di-bag
|
|
128
|
+
import { DiBag } from 'di-bag';
|
|
126
129
|
|
|
127
130
|
const resources = DiBag.createBuilder()
|
|
128
131
|
.register({
|
|
@@ -161,32 +164,37 @@ The [tutorial](docs/guides/tutorial.md) also covers modules with private service
|
|
|
161
164
|
typed tokens, class and function adapters, optional and lazy dependencies,
|
|
162
165
|
collections, startup, metadata, observers, and plugin validation.
|
|
163
166
|
|
|
164
|
-
##
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
167
|
+
## Modules as units of work
|
|
168
|
+
|
|
169
|
+
A DI Bag **module** is the unit of work that one person or one coding agent can
|
|
170
|
+
own: a directory with a small exported contract, private services, and its own
|
|
171
|
+
tests. The composition is checked when the modules meet, so several modules can
|
|
172
|
+
be developed in parallel and merged with confidence.
|
|
173
|
+
|
|
174
|
+
- **A boundary an owner can hold.** `buildModule(keys)` seals a feature and
|
|
175
|
+
exports only the named services. Private services and their types stay
|
|
176
|
+
inside, and two modules can use the same private names without collision.
|
|
177
|
+
- **Verification without the whole application.** A module type-checks
|
|
178
|
+
against the contracts it declares. `fork()` replaces its external
|
|
179
|
+
dependencies with typed fixtures for deterministic tests, so a module's tests
|
|
180
|
+
need neither the other modules nor live clients.
|
|
181
|
+
- **Checks at merge time.** Installing every module into one builder is where
|
|
182
|
+
independently developed work meets. A missing requirement, an incompatible
|
|
183
|
+
replacement, or a contract that no longer matches its consumers fails at
|
|
184
|
+
`build()` or `verifyGraph()`. The `di-bag-graph` tool exports the declared
|
|
185
|
+
edges and cycles for review.
|
|
186
|
+
|
|
187
|
+
For discovery, the directory layout is the map: one directory per module, the
|
|
188
|
+
contract first. The [modularity guide](docs/guides/examples-modularity.md)
|
|
189
|
+
describes the recommended layout and shows separately owned features, isolated
|
|
190
|
+
tests, and contributed tools in three runnable programs.
|
|
191
|
+
|
|
192
|
+
DI Bag's dependency graph describes how services are supplied, not what runs
|
|
193
|
+
next. LLM harnesses and agent graphs are one application of the module pattern:
|
|
194
|
+
model clients, tools, and context sources become modules, and the harness or
|
|
195
|
+
graph framework owns routing, retries, persistence, and execution. The
|
|
196
|
+
[agent harness and graph guide](docs/guides/agent-harnesses-and-graphs.md) is a
|
|
197
|
+
complete example.
|
|
190
198
|
|
|
191
199
|
## How it compares
|
|
192
200
|
|
|
@@ -213,12 +221,13 @@ The package has **zero runtime dependencies** and two entry points:
|
|
|
213
221
|
|
|
214
222
|
| Import | Purpose |
|
|
215
223
|
| --- | --- |
|
|
216
|
-
| `di-bag
|
|
217
|
-
| `di-bag` |
|
|
224
|
+
| `di-bag` | The entry to use. Configures native Promise detection itself on Node, Bun, and Deno through `process.getBuiltinModule`; has no `node:` imports, so it also bundles for browsers. |
|
|
225
|
+
| `di-bag/node` | The same API with detection configured explicitly at import, for Node and Bun. |
|
|
218
226
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
227
|
+
On hosts without `process.getBuiltinModule` (browsers, workers), `build()`
|
|
228
|
+
rejects automatic acquisition stages and names them. Register with
|
|
229
|
+
`DiBag.fromSyncFactory` / `DiBag.fromAsyncFactory` there, or configure a trusted
|
|
230
|
+
classifier. See [portable mode](docs/guides/tutorial.md#portable-mode).
|
|
222
231
|
|
|
223
232
|
## Tradeoffs and limits
|
|
224
233
|
|
|
@@ -226,18 +235,23 @@ for both setup options.
|
|
|
226
235
|
boundaries, manage an agent's context window, or replace behavioral tests.
|
|
227
236
|
- **Async dependencies are explicit.** A factory returning `Promise<T>` exposes
|
|
228
237
|
that promise. Consumers declare and await it themselves.
|
|
229
|
-
- **Cleanup waits for your work.** Cancellation is cooperative; a
|
|
230
|
-
disposer that never settles
|
|
238
|
+
- **Cleanup waits for your work by default.** Cancellation is cooperative; a
|
|
239
|
+
factory or disposer that never settles keeps `close()` pending. Pass
|
|
240
|
+
`close({ timeoutMs, signal })` to stop waiting: the rejection names the
|
|
241
|
+
disposers still running and cleanup continues in the background.
|
|
231
242
|
- **Type safety follows the declared graph.** Casts, unchecked JavaScript, and
|
|
232
243
|
unknown plugins need appropriate runtime checks. Dependency cycles are detected
|
|
233
|
-
at runtime.
|
|
234
|
-
- **Graph types have a compiler cost.**
|
|
235
|
-
compiler
|
|
236
|
-
|
|
237
|
-
|
|
244
|
+
at runtime, or before running by [`di-bag-graph`](tools/graph/README.md).
|
|
245
|
+
- **Graph types have a compiler cost.** One fluent expression is bounded by the
|
|
246
|
+
compiler's recursion budget: classic TypeScript 6.0.3 accepts about 1,000
|
|
247
|
+
chained calls and overflows beyond that (about 950 for a bulk map followed by
|
|
248
|
+
individual replacements); native 7.0.2 has no such ceiling. Keep an
|
|
249
|
+
expression to 500 calls or fewer and use bulk registration, groups, or named
|
|
250
|
+
modules beyond that. See the [compiler evidence](docs/benchmarks/typescript.md).
|
|
238
251
|
- **Framework integration belongs to the application.** DI Bag provides the
|
|
239
252
|
composition and ownership primitives; the host connects request, job, or UI
|
|
240
|
-
lifecycles.
|
|
253
|
+
lifecycles. The [server guide](docs/guides/server-integration.md) and the
|
|
254
|
+
[React guide](docs/guides/react-integration.md) are tested recipes for both.
|
|
241
255
|
|
|
242
256
|
## Explore further
|
|
243
257
|
|
|
@@ -246,8 +260,11 @@ for both setup options.
|
|
|
246
260
|
| [Complete tutorial](docs/guides/tutorial.md) | Learn every public API through examples, from first composition to advanced ownership. |
|
|
247
261
|
| [API reference](docs/guides/api-reference.md) | Exact generated signatures, overloads, type parameters, and API inventories. |
|
|
248
262
|
| [Server guide](docs/guides/server-integration.md) | Node HTTP, Express, Fastify, Bun, and Deno: shared services, request scopes, startup, and shutdown. |
|
|
249
|
-
| [
|
|
250
|
-
| [
|
|
263
|
+
| [React guide](docs/guides/react-integration.md) | Browser applications: one app runtime at bootstrap, project runtimes owned from effects, Strict Mode, cancellation, bounded teardown, and `useSyncExternalStore`. |
|
|
264
|
+
| [Radical modularity](docs/guides/examples-modularity.md) | The recommended module layout, separately owned features, isolated tests, and contributed tools. |
|
|
265
|
+
| [Agent docs](AGENTS.md) | Rules, module layout, and check commands for coding agents, with [recipes](docs/agent/recipes.md) and [errors](docs/agent/errors.md). Shipped in the package. |
|
|
266
|
+
| [Agent harnesses and graphs](docs/guides/agent-harnesses-and-graphs.md) | One worked application: model and tool modules, metadata inspection, and node tests with typed fixtures. |
|
|
267
|
+
| [Static dependency graph](docs/agent/recipes.md#review-merge) | Export every builder chain, declared edge, and cycle to JSON with `di-bag-graph` for merge review and CI. |
|
|
251
268
|
| [Runnable examples](examples) | Modules, tokens, composition, collections, plugins, observers, scopes, and provider metadata. |
|
|
252
269
|
| [Integration guide](docs/guides/enterprise-integration.md) | Tested recipes for request ownership, substitutions, and dynamic features. |
|
|
253
270
|
| [Comparison with alternatives](docs/guides/comparison.md) | When DI Bag or another approach may be a better fit, with primary sources. |
|
|
@@ -1,14 +1,43 @@
|
|
|
1
1
|
import type { Provider } from './provider';
|
|
2
2
|
import type { Factory } from './registration';
|
|
3
|
-
import type { Acquired, AcquisitionMode, AutoOutput, NativeOutput, ModeOptions } from './acquisition-mode';
|
|
3
|
+
import type { Acquired, AcquisitionMode, AsyncOutput, AutoOutput, NativeOutput, ModeOptions, SyncOutput } from './acquisition-mode';
|
|
4
4
|
import type { TokenDependencyContract } from './token-types';
|
|
5
|
-
/**
|
|
5
|
+
/**
|
|
6
|
+
* Why a pushed disposer is running: the factory never returned, or it did and the
|
|
7
|
+
* service disposer — the `withDisposal` on the value this factory returned — has
|
|
8
|
+
* just run. Ownership a consumer attaches to a transformed value does not count.
|
|
9
|
+
* @see https://dany-fedorov.github.io/di-bag/guides/tutorial.html#release-partial-acquisition
|
|
10
|
+
*/
|
|
11
|
+
export interface DisposerContext {
|
|
12
|
+
/**
|
|
13
|
+
* `'factory-failed'`: the factory threw, rejected, or was cancelled; no service exists.
|
|
14
|
+
* `'no-service-disposer'`: the factory returned and no `withDisposal` owns that value.
|
|
15
|
+
* `'service-disposed'`: the service disposer ran without throwing.
|
|
16
|
+
* `'service-disposal-failed'`: the service disposer threw; pushed disposers still run.
|
|
17
|
+
*/
|
|
18
|
+
readonly reason: 'factory-failed' | 'no-service-disposer' | 'service-disposed' | 'service-disposal-failed';
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Cooperative cancellation and acquisition-local ownership supplied to a context-aware factory.
|
|
22
|
+
* @see https://dany-fedorov.github.io/di-bag/guides/tutorial.html#start-selected-services-and-cancel-cooperatively
|
|
23
|
+
*/
|
|
6
24
|
export interface AcquisitionContext {
|
|
7
25
|
/** Aborted when the acquisition's owning scope begins closing. */
|
|
8
26
|
readonly signal: AbortSignal;
|
|
27
|
+
/**
|
|
28
|
+
* Own a resource this factory has already acquired. Pushed disposers run exactly once, last
|
|
29
|
+
* pushed first: at once if the factory fails, otherwise at `close()` after every disposer of the
|
|
30
|
+
* service, with `disposerCtx.reason` saying which. `withDisposal` owns the returned value; push
|
|
31
|
+
* what is acquired on the way, and test `reason` before releasing the returned value itself.
|
|
32
|
+
* @param disposer - Releases the resource acquired immediately before this call.
|
|
33
|
+
*/
|
|
34
|
+
pushDisposer(this: void, disposer: (this: void, disposerCtx: DisposerContext) => void | Promise<void>): void;
|
|
9
35
|
}
|
|
10
|
-
type ContextFactory = (this: void, deps: never,
|
|
11
|
-
/**
|
|
36
|
+
type ContextFactory = (this: void, deps: never, factoryCtx: AcquisitionContext) => unknown;
|
|
37
|
+
/**
|
|
38
|
+
* The named-dependency factory contract retained by an acquisition-context callback.
|
|
39
|
+
* @see https://dany-fedorov.github.io/di-bag/guides/tutorial.html#start-selected-services-and-cancel-cooperatively
|
|
40
|
+
*/
|
|
12
41
|
export type ContextualFactory<F extends ContextFactory> = (this: void, deps: Parameters<F> extends [] ? {} : Parameters<F>[0]) => ReturnType<F>;
|
|
13
42
|
type FactoryOptions<M extends AcquisitionMode> = 'auto' extends M ? [options?: {
|
|
14
43
|
readonly context?: never;
|
|
@@ -26,7 +55,7 @@ type FactoryOptions<M extends AcquisitionMode> = 'auto' extends M ? [options?: {
|
|
|
26
55
|
* @typeParam F - The complete callback signature, retaining dependency and output inference.
|
|
27
56
|
* @typeParam M - The raw, nativePromise, or configured auto acquisition policy.
|
|
28
57
|
*/
|
|
29
|
-
export declare function fromFactory<F extends (this: void, deps: never,
|
|
58
|
+
export declare function fromFactory<F extends (this: void, deps: never, factoryCtx: AcquisitionContext) => ('nativePromise' extends M ? Promise<unknown> : unknown), M extends AcquisitionMode = 'auto'>(callback: F & AutoOutput<ReturnType<NoInfer<F>>, NoInfer<M>>, options: {
|
|
30
59
|
readonly context: 'acquisition';
|
|
31
60
|
} & ModeOptions<M>): Provider<ContextualFactory<F>, Readonly<{}>, readonly [], TokenDependencyContract, Acquired<ReturnType<F>, M>>;
|
|
32
61
|
/**
|
|
@@ -39,4 +68,50 @@ export declare function fromFactory<F extends (this: void, deps: never, context:
|
|
|
39
68
|
* @typeParam M - The raw, nativePromise, or configured auto acquisition policy.
|
|
40
69
|
*/
|
|
41
70
|
export declare function fromFactory<F extends Factory, M extends AcquisitionMode = 'auto'>(callback: F & NativeOutput<ReturnType<NoInfer<F>>, NoInfer<M>> & AutoOutput<ReturnType<NoInfer<F>>, NoInfer<M>>, ...options: FactoryOptions<M>): Provider<F, Readonly<{}>, readonly [], TokenDependencyContract, Acquired<ReturnType<F>, M>>;
|
|
71
|
+
type PortableFactoryOptions = {
|
|
72
|
+
readonly context?: never;
|
|
73
|
+
readonly acquisitionMode?: never;
|
|
74
|
+
};
|
|
75
|
+
type ContextualPortableFactoryOptions = {
|
|
76
|
+
readonly context: 'acquisition';
|
|
77
|
+
readonly acquisitionMode?: never;
|
|
78
|
+
};
|
|
79
|
+
/**
|
|
80
|
+
* Describe a synchronous named-dependency factory that runs on every host: a `raw` stage whose
|
|
81
|
+
* exact return value is the service, so `then` is never read and no Promise classifier is needed.
|
|
82
|
+
* @param callback - A receiver-free factory taking its named dependency object and the acquisition context.
|
|
83
|
+
* @param options - `context: 'acquisition'`; the acquisition mode is fixed and `acquisitionMode` is rejected.
|
|
84
|
+
* @returns A lazy provider preserving exact output and named dependencies; adds no ownership.
|
|
85
|
+
* @typeParam F - The complete callback signature, retaining dependency and output inference.
|
|
86
|
+
*/
|
|
87
|
+
export declare function fromSyncFactory<F extends ContextFactory>(callback: F & SyncOutput<ReturnType<NoInfer<F>>>, options: ContextualPortableFactoryOptions): Provider<ContextualFactory<F>, Readonly<{}>, readonly [], TokenDependencyContract, ReturnType<F>>;
|
|
88
|
+
/**
|
|
89
|
+
* Describe a synchronous named-dependency factory that runs on every host: a `raw` stage whose
|
|
90
|
+
* exact return value is the service. A Promise or thenable output is rejected at compile time;
|
|
91
|
+
* use `fromAsyncFactory`, or `fromFactory` with `acquisitionMode: 'raw'` when the Promise object is the service.
|
|
92
|
+
* @param callback - A receiver-free factory taking its named dependency object.
|
|
93
|
+
* @param options - Optional; `acquisitionMode` is rejected because the helper fixes it.
|
|
94
|
+
* @returns A lazy provider retaining exact output and dependency types without adding ownership.
|
|
95
|
+
* @typeParam F - The exact factory signature and exposed result.
|
|
96
|
+
*/
|
|
97
|
+
export declare function fromSyncFactory<F extends Factory>(callback: F & SyncOutput<ReturnType<NoInfer<F>>>, options?: PortableFactoryOptions): Provider<F, Readonly<{}>, readonly [], TokenDependencyContract, ReturnType<F>>;
|
|
98
|
+
/**
|
|
99
|
+
* Describe an asynchronous named-dependency factory that runs on every host: a `nativePromise`
|
|
100
|
+
* stage whose service is the returned Promise and whose owners receive the fulfilled value.
|
|
101
|
+
* @param callback - A receiver-free async factory taking its named dependency object and the acquisition context.
|
|
102
|
+
* @param options - `context: 'acquisition'`; the acquisition mode is fixed and `acquisitionMode` is rejected.
|
|
103
|
+
* @returns A lazy provider exposing the factory's own Promise; adds no ownership.
|
|
104
|
+
* @typeParam F - The complete callback signature, retaining dependency and output inference.
|
|
105
|
+
*/
|
|
106
|
+
export declare function fromAsyncFactory<F extends (this: void, deps: never, factoryCtx: AcquisitionContext) => Promise<unknown>>(callback: F, options: ContextualPortableFactoryOptions): Provider<ContextualFactory<F>, Readonly<{}>, readonly [], TokenDependencyContract, Awaited<ReturnType<F>>>;
|
|
107
|
+
/**
|
|
108
|
+
* Describe an asynchronous named-dependency factory that runs on every host: a `nativePromise`
|
|
109
|
+
* stage whose service is the returned Promise and whose owners receive the fulfilled value.
|
|
110
|
+
* A non-Promise output is rejected at compile time; a thenable that is not a native Promise fails the acquisition with a `TypeError`.
|
|
111
|
+
* @param callback - A receiver-free factory returning a native Promise.
|
|
112
|
+
* @param options - Optional; `acquisitionMode` is rejected because the helper fixes it.
|
|
113
|
+
* @returns A lazy provider exposing the factory's own Promise; `withDisposal` receives its fulfilled value.
|
|
114
|
+
* @typeParam F - The exact factory signature and exposed Promise.
|
|
115
|
+
*/
|
|
116
|
+
export declare function fromAsyncFactory<F extends Factory>(callback: F & AsyncOutput<ReturnType<NoInfer<F>>>, options?: PortableFactoryOptions): Provider<F, Readonly<{}>, readonly [], TokenDependencyContract, Awaited<ReturnType<F>>>;
|
|
42
117
|
export {};
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.fromFactory = fromFactory;
|
|
4
|
+
exports.fromSyncFactory = fromSyncFactory;
|
|
5
|
+
exports.fromAsyncFactory = fromAsyncFactory;
|
|
4
6
|
const errors_1 = require("./errors");
|
|
5
7
|
const provider_1 = require("./provider");
|
|
6
8
|
const provider_operations_1 = require("./provider-operations");
|
|
@@ -11,9 +13,35 @@ function fromFactory(callback, options) {
|
|
|
11
13
|
const mode = (0, acquisition_mode_1.acquisitionMode)(options);
|
|
12
14
|
if (options?.context !== undefined && options.context !== 'acquisition')
|
|
13
15
|
throw (0, errors_1.libraryError)('DI_BAG_INVALID_FACTORY', 'fromFactory context must be acquisition', { operation: 'fromFactory' });
|
|
14
|
-
|
|
16
|
+
return factoryProvider(callback, mode, options?.context === 'acquisition');
|
|
17
|
+
}
|
|
18
|
+
/** Build the provider for one validated factory; `fromFactory` and both portable helpers end here. */
|
|
19
|
+
function factoryProvider(callback, mode, contextual) {
|
|
15
20
|
const handle = (0, provider_1.createProvider)();
|
|
16
|
-
const create = contextual ? ((deps,
|
|
21
|
+
const create = contextual ? ((deps, factoryCtx) => callback(deps, factoryCtx)) : callback;
|
|
17
22
|
(0, provider_operations_1.retainDescription)(handle, (0, provider_operations_1.sourceDescription)(create, undefined, [], mode, contextual));
|
|
18
23
|
return handle;
|
|
19
24
|
}
|
|
25
|
+
/** Validate a portable helper's options; the mode is the helper's, so `acquisitionMode` is refused. */
|
|
26
|
+
function portableContext(operation, options) {
|
|
27
|
+
if (options === undefined)
|
|
28
|
+
return false;
|
|
29
|
+
if (typeof options !== 'object' || options === null)
|
|
30
|
+
throw (0, errors_1.libraryError)('DI_BAG_INVALID_FACTORY', `${operation} options must be an object`, { operation });
|
|
31
|
+
if ('acquisitionMode' in options)
|
|
32
|
+
throw (0, errors_1.libraryError)('DI_BAG_INVALID_FACTORY', `${operation} selects its acquisitionMode itself`, { operation });
|
|
33
|
+
const context = options.context;
|
|
34
|
+
if (context !== undefined && context !== 'acquisition')
|
|
35
|
+
throw (0, errors_1.libraryError)('DI_BAG_INVALID_FACTORY', `${operation} context must be acquisition`, { operation });
|
|
36
|
+
return context === 'acquisition';
|
|
37
|
+
}
|
|
38
|
+
function fromSyncFactory(callback, options) {
|
|
39
|
+
if (typeof callback !== 'function')
|
|
40
|
+
throw (0, errors_1.libraryError)('DI_BAG_INVALID_FACTORY', 'fromSyncFactory requires a function', { operation: 'fromSyncFactory' });
|
|
41
|
+
return factoryProvider(callback, 'raw', portableContext('fromSyncFactory', options));
|
|
42
|
+
}
|
|
43
|
+
function fromAsyncFactory(callback, options) {
|
|
44
|
+
if (typeof callback !== 'function')
|
|
45
|
+
throw (0, errors_1.libraryError)('DI_BAG_INVALID_FACTORY', 'fromAsyncFactory requires a function', { operation: 'fromAsyncFactory' });
|
|
46
|
+
return factoryProvider(callback, 'nativePromise', portableContext('fromAsyncFactory', options));
|
|
47
|
+
}
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import type { LifecycleObservers } from './observers';
|
|
2
|
-
import type { StructuralThenable, Unsatisfied } from './types';
|
|
2
|
+
import type { IsAny, SeeErrors, StructuralThenable, Unsatisfied } from './types';
|
|
3
3
|
/**
|
|
4
4
|
* How an acquisition stage treats its returned value: configured classification,
|
|
5
5
|
* the exact raw value, or an observed native Promise fulfillment.
|
|
6
|
+
* @see https://dany-fedorov.github.io/di-bag/guides/tutorial.html#portable-mode
|
|
6
7
|
*/
|
|
7
8
|
export type AcquisitionMode = 'auto' | 'raw' | 'nativePromise';
|
|
8
|
-
/**
|
|
9
|
+
/**
|
|
10
|
+
* Portable facade configuration for `auto` acquisition stages.
|
|
11
|
+
* @see https://dany-fedorov.github.io/di-bag/guides/tutorial.html#portable-mode
|
|
12
|
+
*/
|
|
9
13
|
export interface RuntimeOptions {
|
|
10
14
|
/** Return true only for native Promises the host can observe without thenable assimilation. */
|
|
11
15
|
readonly isNativePromise: (this: void, value: unknown) => boolean;
|
|
@@ -29,8 +33,17 @@ export type StageOptions<M extends AcquisitionMode> = 'auto' extends M ? [option
|
|
|
29
33
|
}];
|
|
30
34
|
export type NativeOutput<O, M extends AcquisitionMode> = 'nativePromise' extends M ? [O] extends [Promise<unknown>] ? unknown : Unsatisfied<'nativePromise acquisition requires a Promise output', {}> : unknown;
|
|
31
35
|
/** Reject a structural thenable output when the stage would classify it automatically. */
|
|
32
|
-
export type AutoOutput<O, M extends AcquisitionMode> = 'auto' extends M ? true extends StructuralThenable<O> ? Unsatisfied
|
|
36
|
+
export type AutoOutput<O, M extends AcquisitionMode> = 'auto' extends M ? true extends StructuralThenable<O> ? Unsatisfied<`factory output is a structural thenable; return a native Promise or select acquisitionMode raw or nativePromise${SeeErrors<'structural-thenable'>}`, {}> : unknown : unknown;
|
|
37
|
+
type PromiseOutput<O> = O extends infer T & {} ? T extends Promise<unknown> ? true : false : false;
|
|
38
|
+
/** Reject a Promise or thenable output where the helper declares the stage synchronous; `any` is exempt. */
|
|
39
|
+
export type SyncOutput<O> = IsAny<O> extends true ? unknown : true extends PromiseOutput<O> | StructuralThenable<O> ? Unsatisfied<`fromSyncFactory output must not be a Promise or thenable; use fromAsyncFactory for a Promise, or fromFactory with acquisitionMode raw to make the Promise object the service${SeeErrors<'portable-factory-output'>}`, {}> : unknown;
|
|
40
|
+
/** Require a Promise output where the helper declares the stage asynchronous. */
|
|
41
|
+
export type AsyncOutput<O> = [O] extends [Promise<unknown>] ? unknown : Unsatisfied<`fromAsyncFactory requires a Promise output; use fromSyncFactory for a synchronous value${SeeErrors<'portable-factory-output'>}`, {}>;
|
|
33
42
|
export declare function acquisitionMode(options: {
|
|
34
43
|
readonly acquisitionMode?: AcquisitionMode;
|
|
35
44
|
} | undefined, fallback?: AcquisitionMode): AcquisitionMode;
|
|
36
|
-
|
|
45
|
+
/** Resolve the classifier when a graph first needs one; a configured classifier always wins. */
|
|
46
|
+
export declare function resolveClassifier(context: RuntimeContext): RuntimeContext | undefined;
|
|
47
|
+
/** The graph has automatic stages and no classifier; `bindings` labels every such registration. */
|
|
48
|
+
export declare function classifierRequired(bindings: readonly string[]): Error;
|
|
49
|
+
export {};
|
package/dist/acquisition-mode.js
CHANGED
|
@@ -3,7 +3,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.unconfigured = void 0;
|
|
4
4
|
exports.runtimeContext = runtimeContext;
|
|
5
5
|
exports.acquisitionMode = acquisitionMode;
|
|
6
|
-
exports.
|
|
6
|
+
exports.resolveClassifier = resolveClassifier;
|
|
7
|
+
exports.classifierRequired = classifierRequired;
|
|
7
8
|
const errors_1 = require("./errors");
|
|
8
9
|
exports.unconfigured = Object.freeze({});
|
|
9
10
|
function runtimeContext(options, previous = exports.unconfigured) {
|
|
@@ -25,11 +26,36 @@ function acquisitionMode(options, fallback = 'auto') {
|
|
|
25
26
|
throw (0, errors_1.libraryError)('DI_BAG_INVALID_ACQUISITION_MODE', 'invalid acquisitionMode: use auto, raw, or nativePromise', { option: 'acquisitionMode' });
|
|
26
27
|
return mode;
|
|
27
28
|
}
|
|
28
|
-
|
|
29
|
+
/**
|
|
30
|
+
* Read the host classifier through `process.getBuiltinModule` (Node, Bun, Deno). A call, not an
|
|
31
|
+
* import, keeps `node:` specifiers out of the root entry's module graph for bundlers and browsers.
|
|
32
|
+
*/
|
|
33
|
+
function hostClassifier() {
|
|
34
|
+
const host = globalThis.process;
|
|
35
|
+
if (typeof host !== 'object' || host === null)
|
|
36
|
+
return undefined;
|
|
37
|
+
const load = host.getBuiltinModule;
|
|
38
|
+
if (typeof load !== 'function')
|
|
39
|
+
return undefined;
|
|
40
|
+
const types = Reflect.apply(load, host, ['node:util/types']);
|
|
41
|
+
if (typeof types !== 'object' || types === null)
|
|
42
|
+
return undefined;
|
|
43
|
+
const { isPromise } = types;
|
|
44
|
+
return typeof isPromise === 'function' ? isPromise : undefined;
|
|
45
|
+
}
|
|
46
|
+
/** Resolve the classifier when a graph first needs one; a configured classifier always wins. */
|
|
47
|
+
function resolveClassifier(context) {
|
|
29
48
|
if (context.isNativePromise)
|
|
30
|
-
return;
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
49
|
+
return context;
|
|
50
|
+
const isNativePromise = hostClassifier();
|
|
51
|
+
return isNativePromise ? Object.freeze({ ...context, isNativePromise }) : undefined;
|
|
52
|
+
}
|
|
53
|
+
const namedBindings = 8;
|
|
54
|
+
/** The graph has automatic stages and no classifier; `bindings` labels every such registration. */
|
|
55
|
+
function classifierRequired(bindings) {
|
|
56
|
+
const sorted = [...bindings].sort();
|
|
57
|
+
const shown = sorted.slice(0, namedBindings).map(label => JSON.stringify(label)).join(', ');
|
|
58
|
+
const rest = sorted.length - Math.min(sorted.length, namedBindings);
|
|
59
|
+
const count = sorted.length === 1 ? '1 registration uses' : `${sorted.length} registrations use`;
|
|
60
|
+
return (0, errors_1.libraryError)('DI_BAG_CLASSIFIER_REQUIRED', `this host has no process.getBuiltinModule; ${count} automatic acquisition: ${shown}${rest ? `, and ${rest} more` : ''}; use DiBag.fromSyncFactory or DiBag.fromAsyncFactory (or an explicit acquisitionMode) for each, or configure DiBag.withConfiguration({ runtime: { isNativePromise } })`, { option: 'runtime.isNativePromise', bindings: Object.freeze(sorted) });
|
|
35
61
|
}
|
package/dist/acquisition.d.ts
CHANGED
|
@@ -16,7 +16,6 @@ export declare class ScopeAcquisitions {
|
|
|
16
16
|
private state;
|
|
17
17
|
private closing;
|
|
18
18
|
private controller;
|
|
19
|
-
private acquisitionContext;
|
|
20
19
|
private cancellationStarted;
|
|
21
20
|
private cancellationCause;
|
|
22
21
|
private readonly shared;
|
|
@@ -39,7 +38,19 @@ export declare class ScopeAcquisitions {
|
|
|
39
38
|
private assertAliasPath;
|
|
40
39
|
assertOpen(): void;
|
|
41
40
|
close(beforeDispose?: Promise<void>, cause?: unknown): Promise<void>;
|
|
42
|
-
|
|
41
|
+
/** Labels of this scope's running disposers and of acquisitions close is still draining. */
|
|
42
|
+
collectProgress(pending: string[], acquiring: string[]): void;
|
|
43
|
+
/** One controller per scope; every acquisition observes the same cancellation. */
|
|
44
|
+
private cancellationSignal;
|
|
45
|
+
/**
|
|
46
|
+
* The signal is scope-wide; deferred cleanup is local to this attempt, so each
|
|
47
|
+
* contextual acquisition receives its own frozen context. The context captures
|
|
48
|
+
* only its disposer stack, never the execution or this scope, so an
|
|
49
|
+
* application that retains it past `close()` retains nothing else. Built here
|
|
50
|
+
* rather than in `resolveBinding` for the same reason: every closure of a
|
|
51
|
+
* function shares one scope, and a factory can retain the dependency proxy.
|
|
52
|
+
*/
|
|
53
|
+
private acquisitionContext;
|
|
43
54
|
private resolveBinding;
|
|
44
55
|
private eventFields;
|
|
45
56
|
private observeAttempt;
|