di-bag 0.5.0 → 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 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 ships in `node_modules/di-bag/`. Every call, with one way per task and an example: [docs/agent/api-card.md](docs/agent/api-card.md).
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
- TypeScript dependency composition and resource ownership for modular codebases.
4
- Built for coding agents that ship one feature at a time.
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
+ [![CI](https://github.com/dany-fedorov/di-bag/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/dany-fedorov/di-bag/actions/workflows/ci.yml)
8
+ [![Runtime dependencies: 0](https://img.shields.io/badge/runtime_dependencies-0-2563eb)](package.json)
9
+ [![License: MIT](https://img.shields.io/badge/license-MIT-16a34a)](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
- Compose ordinary TypeScript factories into reusable features. DI Bag checks
11
- declared dependencies, keeps module internals private, and manages resource
12
- creation and disposal. 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.
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.
19
- - **[Compile-time wiring checks](docs/guides/examples-type-checking.md).**
20
- Catch missing dependencies and incompatible replacements before starting the app.
21
- - **[Metadata inspection without creating services](docs/guides/examples-extensibility.md).**
22
- Build capability catalogs without running factories or opening clients.
23
- - **[Inject anything with a simple factory function](docs/guides/examples-plain-services.md).**
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
- The API is pre-1.0 and includes breaking changes. Run the 0.4-to-0.5 codemod
35
- before upgrading, then review the changelog and the
36
- [0.5 migration guide](docs/guides/migrating-to-0.5.md).
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 **service** can be a configuration object, a database client, or a function.
45
- A **factory** creates a service. A **container** resolves its factories' declared
46
- dependencies. Services are created when needed. The container disposes owned
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
- `withServices({...})` adds factories to an immutable builder. `buildContainer()`
72
- checks the declared graph and creates a container. `resolve('greeter')` creates
73
- the greeter and its config on first use. The default scoped lifetime returns the
74
- same greeter on later resolutions in this container. Provider order does not matter.
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
- TypeScript knows that `greeter` has a `greet(name: string): string` method.
77
- Removing the `config` factory makes `buildContainer()` a compile-time error. Changing
78
- `greeting` to a number also fails the type check because the greeter needs a string.
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 {#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
- [integration recipes](docs/guides/enterprise-integration.md).
255
+ [Integration patterns](docs/guides/enterprise-integration.md).
177
256
 
178
- The [tutorial](docs/guides/tutorial.md) also covers modules with private services,
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 DI Bag **module** is the unit of work that one person or one coding agent can
185
- own: a directory with a small exported contract, private services, and its own
186
- tests. The composition is checked when the modules meet, so several modules can
187
- be developed in parallel and merged with confidence.
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 })` seals a feature and
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 without collision.
192
- - **Verification without the whole application.** A module declares what its host
193
- must supply. A small check file installs the module with typed fixtures and calls
194
- `verifyGraphAtCompileTime()`. Tests can use `createIndependentContainer(keys, providers)`
195
- for checked replacements without live clients.
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, an incompatible
198
- replacement, or a contract that no longer matches its consumers fails at
199
- `buildContainer()` or `verifyGraphAtCompileTime()`. The `di-bag-graph` tool exports the declared
200
- edges and cycles for review.
201
-
202
- Install modules with `withInstalledModules([...])`. If two modules require the
203
- same name for different contracts, rename each requirement before installation
204
- with `withRenamedRequirement({ currentRequirementKey, newRequirementKey })`. Use
205
- `withRenamedExport({ currentExportKey, newExportKey })` for colliding exports.
206
-
207
- For discovery, the directory layout is the map: one directory per module, the
208
- contract first. The [modularity guide](docs/guides/examples-modularity.md)
209
- describes the recommended layout and shows separately owned features, isolated
210
- tests, and contributed tools in three runnable programs.
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 tested recipes for both.
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
- | [Radical modularity](docs/guides/examples-modularity.md) | The recommended module layout, separately owned features, isolated tests, and contributed tools. |
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](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. |
286
- | [Runnable examples](examples) | Modules, tokens, composition, collections, plugins, observers, child containers, and provider metadata. |
287
- | [Integration guide](docs/guides/enterprise-integration.md) | Tested recipes for request ownership, substitutions, and dynamic features. |
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
- [![CI](https://github.com/dany-fedorov/di-bag/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/dany-fedorov/di-bag/actions/workflows/ci.yml)
295
- [![Runtime dependencies: 0](https://img.shields.io/badge/runtime_dependencies-0-2563eb)](package.json)
296
- [![License: MIT](https://img.shields.io/badge/license-MIT-16a34a)](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
- /** Aborted when the acquisition's owning container begins closing. */
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
- * Own a resource acquired during this factory call; pushed disposers run once in reverse order.
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 container = DiBag.createBuilder().withServices({ greeting: () => 'hello' }).buildContainer();
112
- * const labels = container.graphSnapshot().bindings.map(binding => binding.bindingLabel);
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 container = DiBag.createBuilder().withServices({ greeting: () => 'hello' }).buildContainer();
133
- * const labels = container.graphSnapshot().bindings.map(binding => binding.bindingLabel);
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/index.d.ts CHANGED
@@ -18,7 +18,7 @@ export type { ReboundProviders, ReboundSelection } from './types';
18
18
  export type { CanonicalLifetime, ChildReplacementAdmission, CheckedChildContainerLifetimes, CheckedLifetimes, LifetimeObligation, Reach } from './lifetime-types';
19
19
  export type { DiBagPolicy } from './types';
20
20
  export type { CheckDependencyCompatibility, CheckDependencyCompleteness, RegistrationEntries, OverrideFactoryContext, RegistrationsFromEntries, OverrideRegistrations, Overrides, ServicesOf, SelectedRegistrations, Selection } from './types';
21
- export type { Presence, AcquisitionMetadataPresence, AcquisitionSnapshot, RegistrationSnapshot, BindingSnapshot, GraphSnapshot } from './inspection';
21
+ export type { Presence, AcquisitionMetadataPresence, AcquisitionSnapshot, RegistrationSnapshot, BindingSnapshot, ModuleInstallationSnapshot, GraphSnapshot } from './inspection';
22
22
  export type { PositionalFactoryArguments, PositionalFactoryFunction } from './composition';
23
23
  export type { OptionalDependency, LazyDependency, DependencyReference } from './dependency-references';
24
24
  export type { PluginOutputValidator, PluginProvider } from './plugins';
@@ -53,6 +53,8 @@ export interface RegistrationSnapshot<M = Readonly<{}>, A extends readonly unkno
53
53
  * @see https://dany-fedorov.github.io/di-bag/guides/tutorial.html#attach-metadata-and-inspect-without-resolving
54
54
  */
55
55
  export interface BindingSnapshot<M = Readonly<{}>, A extends readonly unknown[] = readonly []> extends RegistrationSnapshot<M, A> {
56
+ /** Innermost module installation that introduced this binding; absent for host declarations and host replacements. */
57
+ readonly moduleInstallationId: symbol | undefined;
56
58
  /** Public names or token symbols that select this binding, in service key order; empty for a private module binding. */
57
59
  readonly serviceKeys: readonly (string | symbol)[];
58
60
  readonly lifetime: Lifetime;
@@ -65,6 +67,15 @@ export interface BindingSnapshot<M = Readonly<{}>, A extends readonly unknown[]
65
67
  readonly dependencyKind: 'required' | 'optional' | 'lazy';
66
68
  }[];
67
69
  }
70
+ /** One occurrence of a sealed module in a container graph. */
71
+ export interface ModuleInstallationSnapshot {
72
+ /** Stable identity for this occurrence in the graph. */
73
+ readonly installationId: symbol;
74
+ /** Exact label supplied when the module was sealed, if any. */
75
+ readonly moduleLabel: string | undefined;
76
+ /** Enclosing installation, if this module was installed inside another module. */
77
+ readonly parentInstallationId: symbol | undefined;
78
+ }
68
79
  /**
69
80
  * A frozen description of every binding a container can resolve, plus the edges observed so far.
70
81
  * Named dependencies read from a factory's object parameter are not knowable until the factory
@@ -73,6 +84,8 @@ export interface BindingSnapshot<M = Readonly<{}>, A extends readonly unknown[]
73
84
  */
74
85
  export interface GraphSnapshot {
75
86
  readonly containerId: symbol;
87
+ /** Module occurrences in installation order, with each parent preceding its contiguous descendants. */
88
+ readonly moduleInstallations: readonly ModuleInstallationSnapshot[];
76
89
  /** Public bindings in service key order, then contributions in group order, then remaining private bindings. */
77
90
  readonly bindings: readonly BindingSnapshot<object, readonly unknown[]>[];
78
91
  readonly contributions: readonly {
package/dist/module.d.ts CHANGED
@@ -39,6 +39,16 @@ 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
+ /**
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
+ */
51
+ get moduleLabel(): string | undefined;
42
52
  /**
43
53
  * Return a module view with one string-named export renamed through an options object.
44
54
  * @param options - The current export and its noncolliding new name.
@@ -49,6 +59,9 @@ declare class Module<ExportedServices extends object, RequiredServices extends o
49
59
  * const feature = DiBag.createBuilder().withServices({ service: () => 1 })
50
60
  * .buildModule({ exportedServiceKeys: ['service'] });
51
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();
52
65
  * ```
53
66
  */
54
67
  withRenamedExport<const CurrentExportKey extends string, const NewExportKey extends string>(options: {
package/dist/module.js CHANGED
@@ -27,6 +27,18 @@ class Module {
27
27
  });
28
28
  Object.freeze(this);
29
29
  }
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
+ */
39
+ get moduleLabel() {
40
+ return descriptions.get(this).label;
41
+ }
30
42
  /**
31
43
  * Return a module view with one string-named export renamed through an options object.
32
44
  * @param options - The current export and its noncolliding new name.
@@ -37,6 +49,9 @@ class Module {
37
49
  * const feature = DiBag.createBuilder().withServices({ service: () => 1 })
38
50
  * .buildModule({ exportedServiceKeys: ['service'] });
39
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();
40
55
  * ```
41
56
  */
42
57
  withRenamedExport(options) {
@@ -171,6 +186,19 @@ function moduleGraph(value, index) {
171
186
  throw (0, errors_1.libraryError)('DI_BAG_INVALID_MODULE', `withInstalledModules requires genuine modules: element ${index} is not one`, { operation: 'withInstalledModules', index });
172
187
  }
173
188
  const { graph, exports, label, requirementRenames } = description;
189
+ const installationId = Symbol('module installation');
190
+ const remapped = new Map();
191
+ for (const nested of graph.moduleInstallations ?? [])
192
+ remapped.set(nested.installationId, Symbol('module installation'));
193
+ const moduleInstallations = [
194
+ { installationId, moduleLabel: label, parentInstallationId: undefined },
195
+ ...(graph.moduleInstallations ?? []).map(nested => ({
196
+ installationId: remapped.get(nested.installationId),
197
+ moduleLabel: nested.moduleLabel,
198
+ parentInstallationId: nested.parentInstallationId === undefined
199
+ ? installationId : remapped.get(nested.parentInstallationId),
200
+ })),
201
+ ];
174
202
  const exported = new Set();
175
203
  for (const localKey of exports.values())
176
204
  exported.add(graph.publicSlots.get(localKey));
@@ -219,6 +247,8 @@ function moduleGraph(value, index) {
219
247
  bindings.set(fresh, {
220
248
  id: fresh, label: labelOf(id, binding), registration: binding.registration,
221
249
  localNames: localNamesFor(binding),
250
+ moduleInstallationId: binding.moduleInstallationId === undefined
251
+ ? installationId : remapped.get(binding.moduleInstallationId),
222
252
  });
223
253
  }
224
254
  const publicSlots = new Map();
@@ -232,5 +262,6 @@ function moduleGraph(value, index) {
232
262
  publicSlots,
233
263
  contributions,
234
264
  tokenKinds: new Map(graph.tokenKinds ?? []),
265
+ moduleInstallations,
235
266
  };
236
267
  }
package/dist/runtime.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { ScopeAcquisitions } from './acquisition';
2
2
  import { normalize } from './registration';
3
3
  import type { ProviderOrFactory, Registrations } from './registration';
4
- import type { GraphSnapshot, RegistrationSnapshot } from './inspection';
4
+ import type { GraphSnapshot, ModuleInstallationSnapshot, RegistrationSnapshot } from './inspection';
5
5
  import type { RuntimeContext } from './acquisition-mode';
6
6
  import { type TokenKind } from './tokens';
7
7
  import type { LifetimeKind } from './lifetime';
@@ -19,12 +19,14 @@ export interface BindingDescription {
19
19
  readonly label: string;
20
20
  readonly registration: ProviderOrFactory;
21
21
  readonly localNames: ReadonlyMap<BindingKey, BindingRef>;
22
+ readonly moduleInstallationId?: symbol;
22
23
  }
23
24
  export interface GraphDescription {
24
25
  readonly bindings: ReadonlyMap<BindingId, BindingDescription>;
25
26
  readonly publicSlots: ReadonlyMap<BindingKey, BindingId>;
26
27
  readonly contributions?: ReadonlyMap<symbol, readonly BindingId[]>;
27
28
  readonly tokenKinds?: ReadonlyMap<symbol, TokenKind>;
29
+ readonly moduleInstallations?: readonly ModuleInstallationSnapshot[];
28
30
  }
29
31
  type Normalized = Readonly<ReturnType<typeof normalize>>;
30
32
  /** Immutable descriptions and path-copied lookup storage. Retained maps never escape. */
@@ -60,6 +62,8 @@ export declare class BindingGraph {
60
62
  registration(id: BindingId): Normalized;
61
63
  private requireRegistration;
62
64
  label(id: BindingId): string;
65
+ moduleInstallationId(id: BindingId): symbol | undefined;
66
+ moduleInstallations(): readonly ModuleInstallationSnapshot[];
63
67
  withPublicRegistrations(registrations: Registrations, operation?: string): BindingGraph;
64
68
  /** Replace ordered slots and prune only unreferenced public replacement history. */
65
69
  withPublicBindings(entries: readonly (readonly [BindingKey, ProviderOrFactory])[], operation?: string): BindingGraph;
package/dist/runtime.js CHANGED
@@ -12,6 +12,7 @@ const tokens_1 = require("./tokens");
12
12
  const lifetime_1 = require("./lifetime");
13
13
  const emptyContributions = Object.freeze([]);
14
14
  const emptyNames = new Map();
15
+ const emptyModuleInstallations = Object.freeze([]);
15
16
  /** Immutable descriptions and path-copied lookup storage. Retained maps never escape. */
16
17
  class BindingGraph {
17
18
  #bindings = new persistent_map_1.PersistentMap();
@@ -32,12 +33,16 @@ class BindingGraph {
32
33
  // First public registration order by key, for module snapshots. Keys are
33
34
  // never unregistered, so this retains nothing a graph would otherwise drop.
34
35
  #publicOrder;
36
+ #moduleInstallations;
35
37
  #bindingCache = new Map();
36
38
  #registrationCache = new Map();
37
39
  #publicCache = new Map();
38
40
  #contributionCache = new Map();
39
41
  #explicitlyClassified = false;
40
42
  constructor(description = { bindings: new Map(), publicSlots: new Map() }) {
43
+ if (description.moduleInstallations?.length) {
44
+ this.#moduleInstallations = { values: Object.freeze(description.moduleInstallations.map(record => Object.freeze({ ...record }))) };
45
+ }
41
46
  const declaredKinds = description.tokenKinds ?? new Map();
42
47
  const lexicalSnapshots = new Map();
43
48
  for (const [id, binding] of description.bindings) {
@@ -59,7 +64,10 @@ class BindingGraph {
59
64
  this.#lexicalUsers = this.#lexicalUsers.set(lexical.id, (this.#lexicalUsers.get(lexical.id) ?? 0) + 1);
60
65
  const entry = {
61
66
  lexical,
62
- description: Object.freeze({ id: binding.id, label: binding.label, registration: binding.registration, localNames: lexical.names }),
67
+ description: Object.freeze({
68
+ id: binding.id, label: binding.label, registration: binding.registration, localNames: lexical.names,
69
+ ...(binding.moduleInstallationId === undefined ? {} : { moduleInstallationId: binding.moduleInstallationId }),
70
+ }),
63
71
  normalized: Object.freeze((0, registration_1.normalize)(binding.registration)),
64
72
  };
65
73
  this.retainBindingTokenKinds(entry, 'withInstalledModules');
@@ -98,6 +106,7 @@ class BindingGraph {
98
106
  graph.#tokenKindOwners = this.#tokenKindOwners;
99
107
  graph.#directTokenKinds = this.#directTokenKinds;
100
108
  graph.#publicOrder = this.#publicOrder;
109
+ graph.#moduleInstallations = this.#moduleInstallations;
101
110
  return graph;
102
111
  }
103
112
  entry(id) {
@@ -253,6 +262,12 @@ class BindingGraph {
253
262
  return registration;
254
263
  }
255
264
  label(id) { return (this.#bindingCache.get(id) ?? this.entry(id)?.description)?.label ?? String(id); }
265
+ moduleInstallationId(id) {
266
+ return (this.#bindingCache.get(id) ?? this.entry(id)?.description)?.moduleInstallationId;
267
+ }
268
+ moduleInstallations() {
269
+ return this.#moduleInstallations ? (0, persistent_sequence_1.materialize)(this.#moduleInstallations) : emptyModuleInstallations;
270
+ }
256
271
  withPublicRegistrations(registrations, operation = 'register') {
257
272
  return this.withPublicBindings(Object.keys(registrations).map(key => [key, registrations[key]]), operation);
258
273
  }
@@ -360,7 +375,7 @@ class BindingGraph {
360
375
  for (const [key, kind] of this.#tokenKinds) {
361
376
  tokenKinds.set(key, kind);
362
377
  }
363
- return { bindings, publicSlots, contributions, tokenKinds };
378
+ return { bindings, publicSlots, contributions, tokenKinds, moduleInstallations: this.moduleInstallations() };
364
379
  }
365
380
  /** Every retained binding in `describe()` order, with the public keys that select it. */
366
381
  bindingSummaries() {
@@ -396,6 +411,9 @@ class BindingGraph {
396
411
  this.assertTokenKind(key, kind, operation);
397
412
  }
398
413
  const graph = this.copy();
414
+ if (description.moduleInstallations?.length) {
415
+ graph.#moduleInstallations = (0, persistent_sequence_1.append)(graph.#moduleInstallations, installation.#moduleInstallations);
416
+ }
399
417
  const pending = [];
400
418
  // Publish all incoming protection before releasing overwritten descriptions.
401
419
  for (const [id, count] of installation.#lexicalUsers)
@@ -485,6 +503,7 @@ class BagRuntime {
485
503
  const description = this.graph.registration(id);
486
504
  return Object.freeze({
487
505
  ...this.inspectBinding(id),
506
+ moduleInstallationId: this.graph.moduleInstallationId(id),
488
507
  serviceKeys: keys,
489
508
  lifetime: (0, lifetime_1.publicLifetime)(description.lifetime.kind),
490
509
  factoryReturnKind: description.factoryReturnKind,
@@ -494,6 +513,7 @@ class BagRuntime {
494
513
  });
495
514
  return Object.freeze({
496
515
  containerId: this.acquisitions.ownerId,
516
+ moduleInstallations: this.graph.moduleInstallations(),
497
517
  bindings: Object.freeze(bindings),
498
518
  contributions: this.graph.contributionGroups(),
499
519
  observedEdges: this.acquisitions.observedEdges(),
@@ -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 container = DiBag.createBuilder().withServices({ greeting: () => 'hello' }).buildContainer();
249
- const labels = container.graphSnapshot().bindings.map(binding => binding.bindingLabel);
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}
@@ -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](../guides/migrating-to-0.5.md).
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "di-bag",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
4
4
  "description": "DI container for agent-built codebases: the compiler checks the wiring, modules stay private, resources get cleaned up.",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",