di-bag 0.5.1 → 0.5.2
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 +2 -2
- package/README.md +163 -83
- package/dist/acquisition-context.d.ts +45 -2
- package/dist/di-bag.d.ts +7 -2
- package/dist/di-bag.js +6 -2
- package/dist/module.d.ts +12 -1
- package/dist/module.js +12 -1
- package/docs/agent/api-card.md +67 -2
- package/docs/agent/errors.md +1 -1
- package/package.json +1 -1
package/AGENTS.md
CHANGED
|
@@ -4,8 +4,8 @@ DI Bag composes TypeScript factories into a dependency graph that the compiler
|
|
|
4
4
|
checks. Modules keep a feature's services private behind exported keys; a container
|
|
5
5
|
creates services on first use and releases what it owns when closed.
|
|
6
6
|
|
|
7
|
-
This file
|
|
8
|
-
Task recipes: [docs/agent/recipes.md](docs/agent/recipes.md). Every compiler and runtime message: [docs/agent/errors.md](docs/agent/errors.md).
|
|
7
|
+
This file and `docs/agent/` ship in `node_modules/di-bag/`; the guides live at https://dany-fedorov.github.io/di-bag/, where `llms.txt` indexes these agent pages.
|
|
8
|
+
Every call, one way per task, with an example: [docs/agent/api-card.md](docs/agent/api-card.md). Task recipes: [docs/agent/recipes.md](docs/agent/recipes.md). Every compiler and runtime message: [docs/agent/errors.md](docs/agent/errors.md).
|
|
9
9
|
|
|
10
10
|
## Rules
|
|
11
11
|
|
package/README.md
CHANGED
|
@@ -1,53 +1,66 @@
|
|
|
1
1
|
# DI Bag
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
Dependency injection for TypeScript where the compiler checks the wiring.
|
|
4
|
+
A module keeps a feature's services private behind named exports; a container
|
|
5
|
+
creates services on first use and releases what it owns when closed.
|
|
6
|
+
|
|
7
|
+
[](https://github.com/dany-fedorov/di-bag/actions/workflows/ci.yml)
|
|
8
|
+
[](package.json)
|
|
9
|
+
[](LICENSE)
|
|
5
10
|
|
|
6
11
|
[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)
|
|
7
12
|
|
|
13
|
+
Using a coding agent? The rules, the API card, the recipes, and every error
|
|
14
|
+
message ship in the package. Paste this into your agent:
|
|
15
|
+
|
|
16
|
+
```text
|
|
17
|
+
If di-bag is installed: read node_modules/di-bag/AGENTS.md first and follow its rules.
|
|
18
|
+
Use node_modules/di-bag/docs/agent/api-card.md for a call's signature and example,
|
|
19
|
+
node_modules/di-bag/docs/agent/recipes.md for a task, and
|
|
20
|
+
node_modules/di-bag/docs/agent/errors.md for a compiler or runtime message.
|
|
21
|
+
|
|
22
|
+
If di-bag is not installed: evaluate it from these pages before deciding.
|
|
23
|
+
- https://dany-fedorov.github.io/di-bag/ : what it does, the compiler check, a module in one screen
|
|
24
|
+
- https://dany-fedorov.github.io/di-bag/llms-full.txt : AGENTS.md, the API card, recipes, and errors in one file
|
|
25
|
+
- https://dany-fedorov.github.io/di-bag/guides/examples-modularity.html : the module layout and three runnable programs
|
|
26
|
+
- https://dany-fedorov.github.io/di-bag/guides/comparison.html : how it differs from Awilix, InversifyJS, TSyringe, Typed Inject, Effect, and NestJS
|
|
27
|
+
To install: npm install di-bag. It needs TypeScript 6.0.3 or later with "strict": true and has no
|
|
28
|
+
runtime dependencies. Node, Bun, and Deno need nothing else; in browsers and workers, pass
|
|
29
|
+
factoryReturnKind to each factory. After installing, read node_modules/di-bag/AGENTS.md.
|
|
30
|
+
```
|
|
31
|
+
|
|
8
32
|
## Why DI Bag?
|
|
9
33
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
Supply functions, objects, clients, or promises without decorators or base classes.
|
|
34
|
+
- **A missing dependency is a type error, not a crash on the first request.**
|
|
35
|
+
Delete a factory and `tsc` names the missing service. Change a contract and
|
|
36
|
+
`tsc` rejects the consumer that no longer fits. [See it](#what-the-compiler-catches).
|
|
37
|
+
- **One feature is one directory that type-checks alone.** A module exports
|
|
38
|
+
only the services it names; everything else stays private, so modules written
|
|
39
|
+
separately can reuse the same internal names without colliding. A one-statement
|
|
40
|
+
check file and a per-module `tsconfig.json` verify the module without the rest
|
|
41
|
+
of the application. [See a module](#a-module-in-one-screen).
|
|
42
|
+
- **Resources have one owner.** Attach a disposer to a factory and `close()`
|
|
43
|
+
runs it, dependents first. A child container per request releases what the
|
|
44
|
+
request acquired. [See ownership](#give-resources-a-clear-owner).
|
|
45
|
+
|
|
46
|
+
No decorators, no reflection metadata, no base classes, and zero runtime
|
|
47
|
+
dependencies. Runs on Node, Bun, Deno, browsers, and workers.
|
|
25
48
|
|
|
26
49
|
## Install
|
|
27
50
|
|
|
28
|
-
Install [di-bag from npm](https://www.npmjs.com/package/di-bag):
|
|
29
|
-
|
|
30
51
|
```sh
|
|
31
52
|
npm install di-bag
|
|
32
53
|
```
|
|
33
54
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
The minimum supported TypeScript version is **6.0.3**; enable `strict` in your
|
|
39
|
-
`tsconfig.json`. The repository checks classic TypeScript 6.0.3 and native 7.0.2.
|
|
40
|
-
For browsers and workers, see [runtime support](#runtime-support).
|
|
55
|
+
TypeScript 6.0.3 or later with `strict` enabled. Browsers and workers need one
|
|
56
|
+
extra option per factory; see [runtime support](#runtime-support). Coming from
|
|
57
|
+
0.4? See [upgrading](#upgrading).
|
|
41
58
|
|
|
42
59
|
## Quickstart
|
|
43
60
|
|
|
44
|
-
A **
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
resources when you close it.
|
|
48
|
-
|
|
49
|
-
Import from `di-bag`. Here, `greeter` needs `config`. Its parameter
|
|
50
|
-
type describes that dependency, and its return value is the service it provides:
|
|
61
|
+
A **factory** creates a **service**. The type of a factory's one object
|
|
62
|
+
parameter declares what it depends on. A **container** calls factories on first
|
|
63
|
+
use:
|
|
51
64
|
|
|
52
65
|
```ts
|
|
53
66
|
import { DiBag } from 'di-bag';
|
|
@@ -68,14 +81,80 @@ console.log(greeter.greet('Ada')); // Hello, Ada!
|
|
|
68
81
|
await app.close();
|
|
69
82
|
```
|
|
70
83
|
|
|
71
|
-
`
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
84
|
+
`resolve('greeter')` creates the greeter and its config the first time, and
|
|
85
|
+
returns the same greeter afterwards. TypeScript knows that `greeter` has a
|
|
86
|
+
`greet(name: string): string` method, with no cast and no token.
|
|
87
|
+
|
|
88
|
+
## What the compiler catches
|
|
89
|
+
|
|
90
|
+
Remove the `config` factory. `buildContainer()` no longer type-checks, and the
|
|
91
|
+
message names the missing service:
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
import { DiBag } from 'di-bag';
|
|
95
|
+
|
|
96
|
+
const incomplete = DiBag.createBuilder().withServices({
|
|
97
|
+
greeter: ({ config }: { config: { greeting: string } }) => ({
|
|
98
|
+
greet(name: string) {
|
|
99
|
+
return `${config.greeting}, ${name}!`;
|
|
100
|
+
},
|
|
101
|
+
}),
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// @ts-expect-error required services are missing: config
|
|
105
|
+
incomplete.buildContainer();
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
```text
|
|
109
|
+
required services are missing: config; see https://dany-fedorov.github.io/di-bag/agent/errors.html#missing-service
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Change `greeting` to a number and, with default settings, the message is
|
|
113
|
+
`provided service does not satisfy its consumer dependency`. With
|
|
114
|
+
`"noErrorTruncation": true` in `tsconfig.json`, the details name the consumer,
|
|
115
|
+
the dependency, the expected type, and the provided type. The
|
|
116
|
+
[error index](https://dany-fedorov.github.io/di-bag/agent/errors.html#compile-time) lists every message with its
|
|
117
|
+
cause and fix. The directive above is not decoration: this repository compiles
|
|
118
|
+
the block in CI, and an unused `@ts-expect-error` fails the build.
|
|
119
|
+
|
|
120
|
+
## A module in one screen
|
|
121
|
+
|
|
122
|
+
A module is a builder sealed with the names it exports. The host supplies what
|
|
123
|
+
the module requires. In a project each comment below is a file; the layout and
|
|
124
|
+
the per-module check command are in [AGENTS.md](AGENTS.md#module-layout).
|
|
75
125
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
126
|
+
```ts
|
|
127
|
+
import { DiBag } from 'di-bag';
|
|
128
|
+
|
|
129
|
+
// src/features/greeting/contract.ts: what the module exports and what it needs
|
|
130
|
+
type Greeter = { greet(name: string): string };
|
|
131
|
+
type GreetingConfig = { greeting: string };
|
|
132
|
+
|
|
133
|
+
// src/features/greeting/module.ts: private factories, named exports
|
|
134
|
+
const greetingModule = DiBag.createBuilder()
|
|
135
|
+
.withServices({
|
|
136
|
+
template: ({ config }: { config: GreetingConfig }) => config.greeting,
|
|
137
|
+
greeter: ({ template }: { template: string }): Greeter => ({
|
|
138
|
+
greet: name => `${template}, ${name}!`,
|
|
139
|
+
}),
|
|
140
|
+
})
|
|
141
|
+
.buildModule({ exportedServiceKeys: ['greeter'], moduleLabel: 'greeting' });
|
|
142
|
+
|
|
143
|
+
// src/app.ts: the host supplies config; template stays private
|
|
144
|
+
const app = DiBag.createBuilder()
|
|
145
|
+
.withInstalledModules([greetingModule])
|
|
146
|
+
.withServices({ config: (): GreetingConfig => ({ greeting: 'Hello' }) })
|
|
147
|
+
.buildContainer();
|
|
148
|
+
|
|
149
|
+
console.log(app.resolve('greeter').greet('Ada')); // Hello, Ada!
|
|
150
|
+
await app.close();
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
`app.resolve('template')` is a type error: the module did not export it. Leave
|
|
154
|
+
out the `config` factory and the host fails with
|
|
155
|
+
`required services are missing: config`. Another module may also call a private
|
|
156
|
+
service `template`; the names never meet. [Modules as units of work](#modules-as-units-of-work)
|
|
157
|
+
explains the layout, the check file, and the merge check.
|
|
79
158
|
|
|
80
159
|
## Swap a dependency for a test
|
|
81
160
|
|
|
@@ -160,7 +239,7 @@ be asynchronous. Dependents close before their dependencies, and resources that
|
|
|
160
239
|
were never created need no disposal. Ordinary factories return borrowed values.
|
|
161
240
|
Having a `close()` method alone does not transfer ownership to the container.
|
|
162
241
|
|
|
163
|
-
## Child and independent containers
|
|
242
|
+
## Child and independent containers
|
|
164
243
|
|
|
165
244
|
| Operation | What it creates | Who closes it? |
|
|
166
245
|
| --- | --- | --- |
|
|
@@ -173,48 +252,42 @@ An unmarked provider is scoped. Choose singleton explicitly when child container
|
|
|
173
252
|
|
|
174
253
|
For request handling, checked test replacements, and loading dynamic features,
|
|
175
254
|
see the [server guide](docs/guides/server-integration.md) and
|
|
176
|
-
[
|
|
255
|
+
[Integration patterns](docs/guides/enterprise-integration.md).
|
|
177
256
|
|
|
178
|
-
The [tutorial](docs/guides/tutorial.md) also covers
|
|
257
|
+
The [tutorial](docs/guides/tutorial.md) also covers
|
|
179
258
|
typed tokens, class and function adapters, optional and lazy dependencies,
|
|
180
259
|
collections, service readiness, metadata, observers, and plugin validation.
|
|
181
260
|
|
|
182
261
|
## Modules as units of work
|
|
183
262
|
|
|
184
|
-
A
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
263
|
+
A module is the unit of work one person or one coding agent can own: a
|
|
264
|
+
directory with a small exported contract, private services, and its own tests.
|
|
265
|
+
Composition is checked when the modules meet, so several can be developed in
|
|
266
|
+
parallel and merged with confidence.
|
|
188
267
|
|
|
189
|
-
- **A boundary an owner can hold.** `buildModule({ exportedServiceKeys })`
|
|
268
|
+
- **A boundary an owner can hold.** `buildModule({ exportedServiceKeys })`
|
|
190
269
|
exports only the named services. Private services and their types stay
|
|
191
|
-
inside, and two modules can use the same private names
|
|
192
|
-
- **Verification without the whole application.** A module declares what its
|
|
193
|
-
must supply. A
|
|
194
|
-
`verifyGraphAtCompileTime()
|
|
195
|
-
for checked replacements
|
|
270
|
+
inside, and two modules can use the same private names.
|
|
271
|
+
- **Verification without the whole application.** A module declares what its
|
|
272
|
+
host must supply. A one-statement check file installs the module with typed
|
|
273
|
+
fixtures and calls `verifyGraphAtCompileTime()`; tests use
|
|
274
|
+
`createIndependentContainer(keys, providers)` for checked replacements.
|
|
196
275
|
- **Checks at merge time.** Installing every module into one builder is where
|
|
197
|
-
independently developed work meets. A missing requirement
|
|
198
|
-
|
|
199
|
-
`
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
`withRenamedExport({ currentExportKey, newExportKey })
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
DI Bag's dependency graph describes how services are supplied, not what runs
|
|
213
|
-
next. LLM harnesses and agent graphs are one application of the module pattern:
|
|
214
|
-
model clients, tools, and context sources become modules, and the harness or
|
|
215
|
-
graph framework owns routing, retries, persistence, and execution. The
|
|
216
|
-
[agent harness and graph guide](docs/guides/agent-harnesses-and-graphs.md) is a
|
|
217
|
-
complete example.
|
|
276
|
+
independently developed work meets. A missing requirement or a contract that
|
|
277
|
+
no longer matches its consumers fails at `buildContainer()` or
|
|
278
|
+
`verifyGraphAtCompileTime()`; `di-bag-graph` exports the declared edges and
|
|
279
|
+
cycles for review.
|
|
280
|
+
|
|
281
|
+
If two modules require the same name for different contracts, rename each
|
|
282
|
+
requirement at the install site with
|
|
283
|
+
`withRenamedRequirement({ currentRequirementKey, newRequirementKey })`, and
|
|
284
|
+
colliding exports with `withRenamedExport({ currentExportKey, newExportKey })`.
|
|
285
|
+
|
|
286
|
+
The directory layout is the map: one directory per module, the contract first.
|
|
287
|
+
The [modules guide](docs/guides/examples-modularity.md) shows the layout and
|
|
288
|
+
three runnable programs. LLM harnesses and agent graphs are one application of
|
|
289
|
+
the pattern; the [agent harness guide](docs/guides/agent-harnesses-and-graphs.md)
|
|
290
|
+
is a complete example.
|
|
218
291
|
|
|
219
292
|
## How it compares
|
|
220
293
|
|
|
@@ -269,7 +342,7 @@ Promise factories with `'native-promise'`. See
|
|
|
269
342
|
- **Framework integration belongs to the application.** DI Bag provides the
|
|
270
343
|
composition and ownership primitives; the host connects request, job, or UI
|
|
271
344
|
lifecycles. The [server guide](docs/guides/server-integration.md) and the
|
|
272
|
-
[React guide](docs/guides/react-integration.md) are
|
|
345
|
+
[React guide](docs/guides/react-integration.md) are recipes for both; the React recipe runs in CI, the server recipes compile where they import only `di-bag` and `node:*`.
|
|
273
346
|
|
|
274
347
|
## Explore further
|
|
275
348
|
|
|
@@ -279,22 +352,18 @@ Promise factories with `'native-promise'`. See
|
|
|
279
352
|
| [API reference](docs/guides/api-reference.md) | Exact generated signatures, overloads, type parameters, and API inventories. |
|
|
280
353
|
| [Server guide](docs/guides/server-integration.md) | Node HTTP, Express, Fastify, Bun, and Deno: shared services, request containers, service readiness, and shutdown. |
|
|
281
354
|
| [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`. |
|
|
282
|
-
| [
|
|
355
|
+
| [Modules as units of work](docs/guides/examples-modularity.md) | The recommended module layout, separately owned features, isolated tests, and contributed tools. |
|
|
283
356
|
| [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. |
|
|
284
357
|
| [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. |
|
|
285
|
-
| [Static dependency graph](
|
|
286
|
-
| [Runnable examples](examples) |
|
|
287
|
-
| [Integration
|
|
358
|
+
| [Static dependency graph](https://dany-fedorov.github.io/di-bag/agent/recipes.html#review-merge) | Export every builder chain, declared edge, and cycle to JSON with `di-bag-graph` for merge review and CI. |
|
|
359
|
+
| [Runnable examples](examples/README.md) | One program per topic: modules, tokens, composition, collections, plugins, observers, child containers, and provider metadata. |
|
|
360
|
+
| [Integration patterns](docs/guides/enterprise-integration.md) | Tested recipes for request ownership, substitutions, and dynamic features. |
|
|
288
361
|
| [Comparison with alternatives](docs/guides/comparison.md) | When DI Bag or another approach may be a better fit, with primary sources. |
|
|
289
362
|
| [Development and verification](docs/guides/development.md) | Full checks, portable runtime testing, compiler scale, and performance evidence. |
|
|
290
363
|
| [Documentation map](docs/README.md) | Every guide, the generated reference, and the contributor documents. |
|
|
291
364
|
|
|
292
365
|
## Working on DI Bag
|
|
293
366
|
|
|
294
|
-
[](https://github.com/dany-fedorov/di-bag/actions/workflows/ci.yml)
|
|
295
|
-
[](package.json)
|
|
296
|
-
[](LICENSE)
|
|
297
|
-
|
|
298
367
|
After `npm ci`, run the main checks with Node and Bun installed:
|
|
299
368
|
|
|
300
369
|
```sh
|
|
@@ -307,6 +376,17 @@ Run an example with `bun run examples/composition.ts`. The
|
|
|
307
376
|
[development guide](docs/guides/development.md) covers the remaining compiler,
|
|
308
377
|
platform, and packaging checks and their tool requirements.
|
|
309
378
|
|
|
379
|
+
## Upgrading
|
|
380
|
+
|
|
381
|
+
The API is pre-1.0 and includes breaking changes. Run the 0.4-to-0.5 codemod
|
|
382
|
+
before upgrading, while the 0.4 types are still installed, then read the
|
|
383
|
+
[0.5 migration guide](docs/guides/migrating-to-0.5.md) and the
|
|
384
|
+
[changelog](CHANGELOG.md):
|
|
385
|
+
|
|
386
|
+
```sh
|
|
387
|
+
npx di-bag-codemod --project tsconfig.json --write
|
|
388
|
+
```
|
|
389
|
+
|
|
310
390
|
## License
|
|
311
391
|
|
|
312
392
|
[MIT](LICENSE) © Dany Fedorov
|
|
@@ -7,6 +7,22 @@ import type { TokenDependencyContract } from './token-types';
|
|
|
7
7
|
* service disposer — the `withDisposal` on the value this factory returned — has
|
|
8
8
|
* just run. Ownership a consumer attaches to a transformed value does not count.
|
|
9
9
|
* @see https://dany-fedorov.github.io/di-bag/guides/tutorial.html#release-partial-acquisition
|
|
10
|
+
* @example
|
|
11
|
+
* ```ts
|
|
12
|
+
* type Socket = { close(): Promise<void> };
|
|
13
|
+
* declare function open(url: string): Promise<Socket>;
|
|
14
|
+
* declare function authenticate(connection: Socket): Promise<void>;
|
|
15
|
+
* const socket = DiBag.providerWithDisposal({
|
|
16
|
+
* provider: DiBag.createProvider(async ({ url }: { url: string }, factoryContext) => {
|
|
17
|
+
* const connection = await open(url);
|
|
18
|
+
* // Runs at once if authenticate() rejects; at close() it sees 'service-disposed' and does nothing.
|
|
19
|
+
* factoryContext.pushDisposer(async disposerContext => { if (disposerContext.reason !== 'service-disposed') await connection.close(); });
|
|
20
|
+
* await authenticate(connection);
|
|
21
|
+
* return connection;
|
|
22
|
+
* }, { factoryReceivesContext: true }),
|
|
23
|
+
* disposeService: connection => connection.close(),
|
|
24
|
+
* });
|
|
25
|
+
* ```
|
|
10
26
|
*/
|
|
11
27
|
export interface DisposerContext {
|
|
12
28
|
/**
|
|
@@ -22,12 +38,39 @@ export interface DisposerContext {
|
|
|
22
38
|
* @see https://dany-fedorov.github.io/di-bag/guides/tutorial.html#make-selected-services-ready
|
|
23
39
|
*/
|
|
24
40
|
export interface FactoryContext {
|
|
25
|
-
/**
|
|
41
|
+
/**
|
|
42
|
+
* Return the signal that aborts when the acquisition's owning container begins closing.
|
|
43
|
+
* @example
|
|
44
|
+
* ```ts
|
|
45
|
+
* declare function fetchCatalog(signal: AbortSignal): Promise<string[]>;
|
|
46
|
+
* const catalog = DiBag.createProvider(async (_dependencies: {}, factoryContext) => fetchCatalog(factoryContext.abortSignal), {
|
|
47
|
+
* factoryReceivesContext: true, factoryReturnKind: 'native-promise',
|
|
48
|
+
* });
|
|
49
|
+
* const container = DiBag.createBuilder().withServices({ catalog }).buildContainer();
|
|
50
|
+
* const names: Promise<string[]> = container.resolve('catalog'); // close() aborts the signal while fetchCatalog is pending
|
|
51
|
+
* await container.close();
|
|
52
|
+
* ```
|
|
53
|
+
*/
|
|
26
54
|
readonly abortSignal: AbortSignal;
|
|
27
55
|
/**
|
|
28
|
-
*
|
|
56
|
+
* Attach a disposer for a resource acquired during this factory call; pushed disposers run once in reverse order.
|
|
29
57
|
* Pass the release callback itself, such as `() => socket.close()`, rather than calling it here.
|
|
30
58
|
* @param disposer - Releases the acquired resource when the factory fails or the container closes.
|
|
59
|
+
* @throws `DI_BAG_DISPOSER_PUSHED_AFTER_FACTORY` when called after the factory settled.
|
|
60
|
+
* @example
|
|
61
|
+
* ```ts
|
|
62
|
+
* declare function openLock(name: string): Promise<{ release(): Promise<void> }>;
|
|
63
|
+
* declare function verify(name: string): Promise<void>;
|
|
64
|
+
* const worker = DiBag.createProvider(async ({ name }: { name: string }, factoryContext) => {
|
|
65
|
+
* const lock = await openLock(name);
|
|
66
|
+
* factoryContext.pushDisposer(() => lock.release()); // released at close(), or at once if verify() rejects
|
|
67
|
+
* await verify(name);
|
|
68
|
+
* return { name };
|
|
69
|
+
* }, { factoryReceivesContext: true });
|
|
70
|
+
* const container = DiBag.createBuilder().withServices({ name: () => 'nightly', worker }).buildContainer();
|
|
71
|
+
* const started: Promise<{ name: string }> = container.resolve('worker');
|
|
72
|
+
* await container.close(); // runs lock.release() after the worker settled
|
|
73
|
+
* ```
|
|
31
74
|
*/
|
|
32
75
|
pushDisposer(this: void, disposer: (this: void, disposerContext: DisposerContext) => void | Promise<void>): void;
|
|
33
76
|
}
|
package/dist/di-bag.d.ts
CHANGED
|
@@ -79,6 +79,7 @@ declare class Container<ServiceRegistrations extends Registrations, Constraints
|
|
|
79
79
|
* ```ts
|
|
80
80
|
* const container = DiBag.createBuilder().withServices({ greeting: () => 'hello' }).buildContainer();
|
|
81
81
|
* const snapshot = container.serviceSnapshot('greeting');
|
|
82
|
+
* console.log(snapshot.bindingLabel, snapshot.acquisitions.length); // greeting 0
|
|
82
83
|
* await container.close();
|
|
83
84
|
* ```
|
|
84
85
|
*/
|
|
@@ -108,8 +109,12 @@ declare class Container<ServiceRegistrations extends Registrations, Constraints
|
|
|
108
109
|
* @returns A frozen point-in-time graph snapshot without acquiring services.
|
|
109
110
|
* @example
|
|
110
111
|
* ```ts
|
|
111
|
-
* const
|
|
112
|
-
*
|
|
112
|
+
* const billing = DiBag.createBuilder().withServices({ ledger: () => new Map<string, number>() })
|
|
113
|
+
* .buildModule({ exportedServiceKeys: ['ledger'], moduleLabel: 'billing' });
|
|
114
|
+
* const container = DiBag.createBuilder().withInstalledModules([billing]).buildContainer();
|
|
115
|
+
* const graph = container.graphSnapshot();
|
|
116
|
+
* const installation = graph.moduleInstallations[0]!; // { installationId, moduleLabel: 'billing', parentInstallationId: undefined }
|
|
117
|
+
* const owned = graph.bindings.filter(binding => binding.moduleInstallationId === installation.installationId).map(binding => binding.bindingLabel); // ['ledger']
|
|
113
118
|
* await container.close();
|
|
114
119
|
* ```
|
|
115
120
|
*/
|
package/dist/di-bag.js
CHANGED
|
@@ -129,8 +129,12 @@ class Container {
|
|
|
129
129
|
* @returns A frozen point-in-time graph snapshot without acquiring services.
|
|
130
130
|
* @example
|
|
131
131
|
* ```ts
|
|
132
|
-
* const
|
|
133
|
-
*
|
|
132
|
+
* const billing = DiBag.createBuilder().withServices({ ledger: () => new Map<string, number>() })
|
|
133
|
+
* .buildModule({ exportedServiceKeys: ['ledger'], moduleLabel: 'billing' });
|
|
134
|
+
* const container = DiBag.createBuilder().withInstalledModules([billing]).buildContainer();
|
|
135
|
+
* const graph = container.graphSnapshot();
|
|
136
|
+
* const installation = graph.moduleInstallations[0]!; // { installationId, moduleLabel: 'billing', parentInstallationId: undefined }
|
|
137
|
+
* const owned = graph.bindings.filter(binding => binding.moduleInstallationId === installation.installationId).map(binding => binding.bindingLabel); // ['ledger']
|
|
134
138
|
* await container.close();
|
|
135
139
|
* ```
|
|
136
140
|
*/
|
package/dist/module.d.ts
CHANGED
|
@@ -39,7 +39,15 @@ declare class Module<ExportedServices extends object, RequiredServices extends o
|
|
|
39
39
|
/** @internal */
|
|
40
40
|
readonly [moduleInvariant]: (value: [ExportedServices, RequiredServices, Constraints, PublicProviders]) => [ExportedServices, RequiredServices, Constraints, PublicProviders];
|
|
41
41
|
constructor(description: ModuleDescription);
|
|
42
|
-
/**
|
|
42
|
+
/**
|
|
43
|
+
* Return the exact label supplied when this module was sealed, if any. Reading it never acquires a service.
|
|
44
|
+
* @example
|
|
45
|
+
* ```ts
|
|
46
|
+
* const feature = DiBag.createBuilder().withServices({ answer: () => 42 })
|
|
47
|
+
* .buildModule({ exportedServiceKeys: ['answer'], moduleLabel: 'answers' });
|
|
48
|
+
* const label: string | undefined = feature.moduleLabel; // 'answers'
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
43
51
|
get moduleLabel(): string | undefined;
|
|
44
52
|
/**
|
|
45
53
|
* Return a module view with one string-named export renamed through an options object.
|
|
@@ -51,6 +59,9 @@ declare class Module<ExportedServices extends object, RequiredServices extends o
|
|
|
51
59
|
* const feature = DiBag.createBuilder().withServices({ service: () => 1 })
|
|
52
60
|
* .buildModule({ exportedServiceKeys: ['service'] });
|
|
53
61
|
* const renamed = feature.withRenamedExport({ currentExportKey: 'service', newExportKey: 'featureService' });
|
|
62
|
+
* const app = DiBag.createBuilder().withInstalledModules([renamed]).buildContainer();
|
|
63
|
+
* const value: number = app.resolve('featureService');
|
|
64
|
+
* await app.close();
|
|
54
65
|
* ```
|
|
55
66
|
*/
|
|
56
67
|
withRenamedExport<const CurrentExportKey extends string, const NewExportKey extends string>(options: {
|
package/dist/module.js
CHANGED
|
@@ -27,7 +27,15 @@ class Module {
|
|
|
27
27
|
});
|
|
28
28
|
Object.freeze(this);
|
|
29
29
|
}
|
|
30
|
-
/**
|
|
30
|
+
/**
|
|
31
|
+
* Return the exact label supplied when this module was sealed, if any. Reading it never acquires a service.
|
|
32
|
+
* @example
|
|
33
|
+
* ```ts
|
|
34
|
+
* const feature = DiBag.createBuilder().withServices({ answer: () => 42 })
|
|
35
|
+
* .buildModule({ exportedServiceKeys: ['answer'], moduleLabel: 'answers' });
|
|
36
|
+
* const label: string | undefined = feature.moduleLabel; // 'answers'
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
31
39
|
get moduleLabel() {
|
|
32
40
|
return descriptions.get(this).label;
|
|
33
41
|
}
|
|
@@ -41,6 +49,9 @@ class Module {
|
|
|
41
49
|
* const feature = DiBag.createBuilder().withServices({ service: () => 1 })
|
|
42
50
|
* .buildModule({ exportedServiceKeys: ['service'] });
|
|
43
51
|
* const renamed = feature.withRenamedExport({ currentExportKey: 'service', newExportKey: 'featureService' });
|
|
52
|
+
* const app = DiBag.createBuilder().withInstalledModules([renamed]).buildContainer();
|
|
53
|
+
* const value: number = app.resolve('featureService');
|
|
54
|
+
* await app.close();
|
|
44
55
|
* ```
|
|
45
56
|
*/
|
|
46
57
|
withRenamedExport(options) {
|
package/docs/agent/api-card.md
CHANGED
|
@@ -12,9 +12,14 @@ An example without an import line uses `import { DiBag } from 'di-bag';`. The ru
|
|
|
12
12
|
| --- | --- |
|
|
13
13
|
| Start a graph | [`DiBag.createBuilder()`](#dibag-createbuilder) |
|
|
14
14
|
| Register services by name | [`builder.withServices(providersByName)`](#builder-withservices) |
|
|
15
|
+
| Register the service of a typed token | [`builder.withTokenService(token, provider)`](#builder-withtokenservice) |
|
|
15
16
|
| Create a collection token | [`DiBag.createToken(symbol)`](#dibag-createtoken) |
|
|
16
17
|
| Contribute a collection member | [`builder.withCollectionContribution(options)`](#builder-withcollectioncontribution) |
|
|
18
|
+
| Give a service a second name | [`builder.withServiceAlias(options)`](#builder-withservicealias) |
|
|
19
|
+
| Replace a provider in a builder | [`builder.withReplacedService(serviceKey, provider)`](#builder-withreplacedservice) |
|
|
17
20
|
| Register a portable factory | [`DiBag.createProvider(factory, options)`](#dibag-createprovider) |
|
|
21
|
+
| Adapt a class to positional dependencies | [`DiBag.createProviderFromClass(options)`](#dibag-createproviderfromclass) |
|
|
22
|
+
| Adapt a positional function | [`DiBag.createProviderFromFunction(options)`](#dibag-createproviderfromfunction) |
|
|
18
23
|
| Attach disposal | [`DiBag.providerWithDisposal(options)`](#dibag-providerwithdisposal) |
|
|
19
24
|
| Choose a lifetime | [`DiBag.providerWithLifetime(options)`](#dibag-providerwithlifetime) |
|
|
20
25
|
| Attach registration metadata | [`DiBag.providerWithRegistrationMetadata(options)`](#dibag-providerwithregistrationmetadata) |
|
|
@@ -23,6 +28,8 @@ An example without an import line uses `import { DiBag } from 'di-bag';`. The ru
|
|
|
23
28
|
| Seal a module | [`builder.buildModule(options)`](#builder-buildmodule) |
|
|
24
29
|
| Install modules | [`builder.withInstalledModules(modules)`](#builder-withinstalledmodules) |
|
|
25
30
|
| Rename a module requirement | [`module.withRenamedRequirement(options)`](#module-withrenamedrequirement) |
|
|
31
|
+
| Rename a module export | [`module.withRenamedExport(options)`](#module-withrenamedexport) |
|
|
32
|
+
| Read the label a module was sealed with | [`module.moduleLabel`](#module-modulelabel) |
|
|
26
33
|
| Check the graph on its own line | [`builder.verifyGraphAtCompileTime()`](#builder-verifygraphatcompiletime) |
|
|
27
34
|
| Build a container | [`builder.buildContainer()`](#builder-buildcontainer) |
|
|
28
35
|
| Wait for services before accepting work | [`container.ensureServicesReady(serviceKeys, options?)`](#container-ensureservicesready) |
|
|
@@ -30,7 +37,11 @@ An example without an import line uses `import { DiBag } from 'di-bag';`. The ru
|
|
|
30
37
|
| Open a child container | [`container.createChildContainer(options?)`](#container-createchildcontainer) |
|
|
31
38
|
| Resolve | [`container.resolve(token)`](#container-resolve) |
|
|
32
39
|
| Resolve a collection | [`container.resolveCollection(token)`](#container-resolvecollection) |
|
|
40
|
+
| Inspect one service without resolving it | [`container.serviceSnapshot(serviceKey)`](#container-servicesnapshot) |
|
|
41
|
+
| Inspect every binding and module installation | [`container.graphSnapshot()`](#container-graphsnapshot) |
|
|
33
42
|
| Close | [`container.close(options?)`](#container-close) |
|
|
43
|
+
| Own a resource acquired inside a factory | [`factoryContext.pushDisposer(disposer)`](#factorycontext-pushdisposer) |
|
|
44
|
+
| Stop factory work when the container closes | [`factoryContext.abortSignal`](#factorycontext-abortsignal) |
|
|
34
45
|
|
|
35
46
|
## DiBag facade {#dibag-facade}
|
|
36
47
|
|
|
@@ -203,6 +214,25 @@ await app.close();
|
|
|
203
214
|
|
|
204
215
|
## Module {#module}
|
|
205
216
|
|
|
217
|
+
### `module.moduleLabel` {#module-modulelabel}
|
|
218
|
+
Return the exact label supplied when this module was sealed, if any.
|
|
219
|
+
```ts
|
|
220
|
+
const feature = DiBag.createBuilder().withServices({ answer: () => 42 })
|
|
221
|
+
.buildModule({ exportedServiceKeys: ['answer'], moduleLabel: 'answers' });
|
|
222
|
+
const label: string | undefined = feature.moduleLabel; // 'answers'
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### `module.withRenamedExport(options)` {#module-withrenamedexport}
|
|
226
|
+
Return a module view with one string-named export renamed through an options object. Throws: [`DI_BAG_INVALID_ARGUMENT`](errors.md#di-bag-invalid-argument), [`DI_BAG_UNKNOWN_SERVICE_KEY`](errors.md#di-bag-unknown-service-key).
|
|
227
|
+
```ts
|
|
228
|
+
const feature = DiBag.createBuilder().withServices({ service: () => 1 })
|
|
229
|
+
.buildModule({ exportedServiceKeys: ['service'] });
|
|
230
|
+
const renamed = feature.withRenamedExport({ currentExportKey: 'service', newExportKey: 'featureService' });
|
|
231
|
+
const app = DiBag.createBuilder().withInstalledModules([renamed]).buildContainer();
|
|
232
|
+
const value: number = app.resolve('featureService');
|
|
233
|
+
await app.close();
|
|
234
|
+
```
|
|
235
|
+
|
|
206
236
|
### `module.withRenamedRequirement(options)` {#module-withrenamedrequirement}
|
|
207
237
|
Return a module view that asks its host for a requirement under a new name. Throws: [`DI_BAG_INVALID_ARGUMENT`](errors.md#di-bag-invalid-argument), [`DI_BAG_UNKNOWN_SERVICE_KEY`](errors.md#di-bag-unknown-service-key), [`DI_BAG_DUPLICATE_SERVICE_KEY`](errors.md#di-bag-duplicate-service-key).
|
|
208
238
|
```ts
|
|
@@ -239,14 +269,19 @@ Inspect a service binding through any supported public key without resolving it.
|
|
|
239
269
|
```ts
|
|
240
270
|
const container = DiBag.createBuilder().withServices({ greeting: () => 'hello' }).buildContainer();
|
|
241
271
|
const snapshot = container.serviceSnapshot('greeting');
|
|
272
|
+
console.log(snapshot.bindingLabel, snapshot.acquisitions.length); // greeting 0
|
|
242
273
|
await container.close();
|
|
243
274
|
```
|
|
244
275
|
|
|
245
276
|
### `container.graphSnapshot()` {#container-graphsnapshot}
|
|
246
277
|
Describe every resolvable binding and the dependency edges observed so far.
|
|
247
278
|
```ts
|
|
248
|
-
const
|
|
249
|
-
|
|
279
|
+
const billing = DiBag.createBuilder().withServices({ ledger: () => new Map<string, number>() })
|
|
280
|
+
.buildModule({ exportedServiceKeys: ['ledger'], moduleLabel: 'billing' });
|
|
281
|
+
const container = DiBag.createBuilder().withInstalledModules([billing]).buildContainer();
|
|
282
|
+
const graph = container.graphSnapshot();
|
|
283
|
+
const installation = graph.moduleInstallations[0]!; // { installationId, moduleLabel: 'billing', parentInstallationId: undefined }
|
|
284
|
+
const owned = graph.bindings.filter(binding => binding.moduleInstallationId === installation.installationId).map(binding => binding.bindingLabel); // ['ledger']
|
|
250
285
|
await container.close();
|
|
251
286
|
```
|
|
252
287
|
|
|
@@ -288,6 +323,36 @@ const container = DiBag.createBuilder().withServices({ value: () => 1 }).buildCo
|
|
|
288
323
|
await container.close({ waitTimeoutMs: 10_000, abortSignal: AbortSignal.timeout(15_000) });
|
|
289
324
|
```
|
|
290
325
|
|
|
326
|
+
## Factory context {#factory-context}
|
|
327
|
+
|
|
328
|
+
### `factoryContext.abortSignal` {#factorycontext-abortsignal}
|
|
329
|
+
Return the signal that aborts when the acquisition's owning container begins closing.
|
|
330
|
+
```ts
|
|
331
|
+
declare function fetchCatalog(signal: AbortSignal): Promise<string[]>;
|
|
332
|
+
const catalog = DiBag.createProvider(async (_dependencies: {}, factoryContext) => fetchCatalog(factoryContext.abortSignal), {
|
|
333
|
+
factoryReceivesContext: true, factoryReturnKind: 'native-promise',
|
|
334
|
+
});
|
|
335
|
+
const container = DiBag.createBuilder().withServices({ catalog }).buildContainer();
|
|
336
|
+
const names: Promise<string[]> = container.resolve('catalog'); // close() aborts the signal while fetchCatalog is pending
|
|
337
|
+
await container.close();
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
### `factoryContext.pushDisposer(disposer)` {#factorycontext-pushdisposer}
|
|
341
|
+
Attach a disposer for a resource acquired during this factory call; pushed disposers run once in reverse order. Throws: [`DI_BAG_DISPOSER_PUSHED_AFTER_FACTORY`](errors.md#di-bag-disposer-pushed-after-factory).
|
|
342
|
+
```ts
|
|
343
|
+
declare function openLock(name: string): Promise<{ release(): Promise<void> }>;
|
|
344
|
+
declare function verify(name: string): Promise<void>;
|
|
345
|
+
const worker = DiBag.createProvider(async ({ name }: { name: string }, factoryContext) => {
|
|
346
|
+
const lock = await openLock(name);
|
|
347
|
+
factoryContext.pushDisposer(() => lock.release()); // released at close(), or at once if verify() rejects
|
|
348
|
+
await verify(name);
|
|
349
|
+
return { name };
|
|
350
|
+
}, { factoryReceivesContext: true });
|
|
351
|
+
const container = DiBag.createBuilder().withServices({ name: () => 'nightly', worker }).buildContainer();
|
|
352
|
+
const started: Promise<{ name: string }> = container.resolve('worker');
|
|
353
|
+
await container.close(); // runs lock.release() after the worker settled
|
|
354
|
+
```
|
|
355
|
+
|
|
291
356
|
## Errors {#errors}
|
|
292
357
|
|
|
293
358
|
### `DiBagPluginValidationError` {#dibagpluginvalidationerror}
|
package/docs/agent/errors.md
CHANGED
|
@@ -828,7 +828,7 @@ functions that only throw. `details.removed` names the old call and
|
|
|
828
828
|
|
|
829
829
|
**Fix:** write the replacement. For a whole project, run `npx di-bag-codemod`
|
|
830
830
|
BEFORE upgrading, while the 0.4.0 types are still installed; see the
|
|
831
|
-
[migration guide](
|
|
831
|
+
[migration guide](https://dany-fedorov.github.io/di-bag/guides/migrating-to-0.5.html).
|
|
832
832
|
|
|
833
833
|
```ts
|
|
834
834
|
import { DiBag } from 'di-bag';
|
package/package.json
CHANGED