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.
Files changed (46) hide show
  1. package/AGENTS.md +150 -0
  2. package/README.md +68 -51
  3. package/dist/acquisition-context.d.ts +80 -5
  4. package/dist/acquisition-context.js +30 -2
  5. package/dist/acquisition-mode.d.ts +17 -4
  6. package/dist/acquisition-mode.js +33 -7
  7. package/dist/acquisition.d.ts +13 -2
  8. package/dist/acquisition.js +44 -8
  9. package/dist/alias-types.d.ts +12 -3
  10. package/dist/composition-report.d.ts +3 -2
  11. package/dist/composition.d.ts +8 -2
  12. package/dist/contribution-types.d.ts +26 -18
  13. package/dist/dependency-references.d.ts +16 -4
  14. package/dist/di-bag.d.ts +344 -42
  15. package/dist/di-bag.js +87 -15
  16. package/dist/errors.d.ts +115 -8
  17. package/dist/errors.js +113 -12
  18. package/dist/index.d.ts +6 -6
  19. package/dist/index.js +2 -1
  20. package/dist/inspection.d.ts +21 -5
  21. package/dist/lifetime-types.d.ts +281 -180
  22. package/dist/lifetime.d.ts +4 -1
  23. package/dist/module-types.d.ts +87 -30
  24. package/dist/module.d.ts +18 -4
  25. package/dist/module.js +31 -7
  26. package/dist/observers.d.ts +25 -6
  27. package/dist/plugins.d.ts +17 -4
  28. package/dist/provider-execution.d.ts +38 -2
  29. package/dist/provider-execution.js +145 -17
  30. package/dist/provider.d.ts +41 -10
  31. package/dist/provider.js +1 -0
  32. package/dist/registration.d.ts +8 -2
  33. package/dist/registration.js +4 -1
  34. package/dist/runtime.d.ts +13 -3
  35. package/dist/runtime.js +37 -9
  36. package/dist/scope-types.d.ts +20 -5
  37. package/dist/startup.d.ts +19 -1
  38. package/dist/startup.js +81 -11
  39. package/dist/token-types.d.ts +26 -8
  40. package/dist/tokens.d.ts +10 -2
  41. package/dist/tokens.js +2 -0
  42. package/dist/types.d.ts +69 -23
  43. package/docs/agent/api-card.md +354 -0
  44. package/docs/agent/errors.md +1138 -0
  45. package/docs/agent/recipes.md +403 -0
  46. 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 agentic development, LLM harnesses, and agent graphs.
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) · [Agent harnesses](docs/guides/agent-harnesses-and-graphs.md) · [Comparison](#how-it-compares) · [Tutorial](docs/guides/tutorial.md) · [API reference](docs/guides/api-reference.md)
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 services, tools, or graph nodes against explicit
12
- contracts, then replace their dependencies for tests.
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
- - **[Reusable modules with private bindings](docs/guides/examples-modularity.md).**
15
- Compose independently developed features without exposing their internals.
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
- Use `di-bag/node` in Node or Bun. Here, `greeter` needs `config`. Its parameter
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/node';
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/node';
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/node';
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
- ## LLM harnesses and agent graphs
165
-
166
- Compose an **LLM harness** from model clients, tools, context sources, and
167
- ordinary functions that serve as **agent graph** nodes.
168
-
169
- - **Explicit feature boundaries:** give each node or tool a small contract
170
- and private implementation. Keep feature contracts and focused tests together;
171
- select tools and context for the LLM in the harness.
172
- - **Contract checks and fixture tests:** check declared wiring before a model call,
173
- then fork the composition with typed model and tool fixtures for deterministic
174
- behavioral tests. Keep live-model evals for quality and task success.
175
- - **Inspectable capability descriptions:** describe public nodes and tools next
176
- to their factories. Inspect that metadata without creating services, and use
177
- it in application-defined catalogs, diagnostics, or dispatch policies.
178
-
179
- DI Bag's dependency graph describes how services are supplied. The agent graph
180
- describes execution: which node runs next and what state it receives. Your
181
- harness or graph framework owns routing, retries, persistence, and execution;
182
- DI Bag supplies checked composition and resource ownership.
183
-
184
- The [agent harness and graph guide](docs/guides/agent-harnesses-and-graphs.md)
185
- combines private feature modules, an LLM-backed node, metadata inspection, and
186
- fork-based fixture tests in one runnable example. `inspectGraph()` lists every
187
- binding and the edges observed at runtime; declared edges come from the static
188
- graph tool. Observers track acquisition, not ordinary node calls. Use your graph
189
- framework for workflow checkpoints.
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/node` | Ready-to-use factory composition in Node and Bun, with native Promise detection. |
217
- | `di-bag` | Portable core for other hosts, including Deno and bundled browsers. Use explicit acquisition modes or configure a trusted native Promise predicate. |
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
- The portable entry rejects automatic acquisition stages unless you configure a
220
- trusted classifier. See [portable mode](docs/guides/tutorial.md#portable-mode)
221
- for both setup options.
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 factory or
230
- disposer that never settles can keep `close()` pending.
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.** Very long fluent expressions can exceed
235
- compiler limits. Classic TypeScript still fails the recorded 1,000-call named
236
- registration and replacement cases; use bulk registration or smaller groups.
237
- See the [compiler evidence](docs/benchmarks/typescript.md) for tested forms and limits.
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
- | [Agent harnesses and graphs](docs/guides/agent-harnesses-and-graphs.md) | Compose model and tool dependencies, inspect metadata, and test nodes with typed fixtures. |
250
- | [Static dependency graph](docs/guides/agent-harnesses-and-graphs.md#export-the-declared-dependency-graph) | Export every builder chain, declared edge, and cycle to JSON with `di-bag-graph`. |
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
- /** Cooperative cancellation information supplied to a context-aware acquisition. */
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, context: AcquisitionContext) => unknown;
11
- /** The named-dependency factory contract retained by an acquisition-context callback. */
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, context: AcquisitionContext) => ('nativePromise' extends M ? Promise<unknown> : unknown), M extends AcquisitionMode = 'auto'>(callback: F & AutoOutput<ReturnType<NoInfer<F>>, NoInfer<M>>, options: {
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
- const contextual = options?.context === 'acquisition';
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, context) => callback(deps, context)) : callback;
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
- /** Portable facade configuration for `auto` acquisition stages. */
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<'factory output is a structural thenable; return a native Promise or select acquisitionMode raw or nativePromise', {}> : unknown : unknown;
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
- export declare function requireClassificationCapability(modes: Iterable<AcquisitionMode>, context: RuntimeContext): void;
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 {};
@@ -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.requireClassificationCapability = requireClassificationCapability;
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
- function requireClassificationCapability(modes, context) {
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
- for (const mode of modes)
32
- if (mode === 'auto') {
33
- throw (0, errors_1.libraryError)('DI_BAG_CLASSIFIER_REQUIRED', 'Automatic acquisition classification requires DiBag.withConfiguration({ runtime }), di-bag/node, or explicit acquisitionMode options', { option: 'runtime.isNativePromise' });
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
  }
@@ -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
- private getContext;
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;