di-bag 0.1.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +149 -0
- package/README.md +60 -48
- package/dist/acquisition-context.d.ts +11 -5
- package/dist/acquisition-family.d.ts +5 -0
- package/dist/acquisition-family.js +20 -0
- package/dist/acquisition-mode.d.ts +10 -3
- package/dist/acquisition-mode.js +25 -7
- package/dist/acquisition.d.ts +6 -0
- package/dist/acquisition.js +18 -0
- package/dist/alias-types.d.ts +12 -3
- package/dist/composition-report.d.ts +14 -0
- package/dist/composition-report.js +2 -0
- package/dist/composition.d.ts +12 -6
- package/dist/contribution-types.d.ts +26 -18
- package/dist/dependency-references.d.ts +16 -4
- package/dist/di-bag.d.ts +338 -44
- package/dist/di-bag.js +94 -14
- package/dist/errors.d.ts +115 -8
- package/dist/errors.js +113 -12
- package/dist/index.d.ts +8 -6
- package/dist/index.js +2 -1
- package/dist/inspection.d.ts +55 -4
- package/dist/lifetime-types.d.ts +281 -171
- package/dist/lifetime.d.ts +4 -1
- package/dist/module-types.d.ts +87 -30
- package/dist/module.d.ts +18 -4
- package/dist/module.js +31 -7
- package/dist/observers.d.ts +25 -6
- package/dist/plugins.d.ts +17 -4
- package/dist/provider.d.ts +41 -10
- package/dist/provider.js +1 -0
- package/dist/registration.d.ts +8 -2
- package/dist/registration.js +4 -1
- package/dist/runtime.d.ts +24 -4
- package/dist/runtime.js +66 -8
- package/dist/scope-types.d.ts +20 -5
- package/dist/startup.d.ts +19 -1
- package/dist/startup.js +72 -11
- package/dist/token-types.d.ts +26 -8
- package/dist/tokens.d.ts +10 -2
- package/dist/tokens.js +2 -0
- package/dist/types.d.ts +95 -22
- package/docs/agent/api-card.md +337 -0
- package/docs/agent/errors.md +1042 -0
- package/docs/agent/recipes.md +290 -0
- package/package.json +15 -6
package/AGENTS.md
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
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
|
+
In browsers and workers `build()` throws
|
|
16
|
+
[`DI_BAG_CLASSIFIER_REQUIRED`](docs/agent/errors.md#di-bag-classifier-required)
|
|
17
|
+
for factories without an explicit `acquisitionMode`.
|
|
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
|
+
value; `close()` runs disposers, dependents first. Close every scope and fork
|
|
35
|
+
you create. A parent closes its live scopes, never forks.
|
|
36
|
+
7. **Replace dependencies in tests with `fork(keys, overrides)`**; each
|
|
37
|
+
override must satisfy the original contract.
|
|
38
|
+
8. **Modules.** Register a feature's factories, then `buildModule(['exported'])`.
|
|
39
|
+
What its factories need and the module does not register becomes a
|
|
40
|
+
requirement: the host that calls `installModule(module)` must register it.
|
|
41
|
+
Pass `buildModule(keys, { label: 'billing' })` so messages name private
|
|
42
|
+
services `billing/store`.
|
|
43
|
+
9. **Read a rejection at its name.** A graph error is an assignability error
|
|
44
|
+
whose type is `Unsatisfied<"message", details>`, reported where the builder
|
|
45
|
+
expression starts. `builder.verifyGraph() satisfies void;` reports the same
|
|
46
|
+
message on its own line; `"noErrorTruncation": true` prints the details.
|
|
47
|
+
Runtime errors carry `code` and `details`: branch on `code`, never on message
|
|
48
|
+
text. The section for a code is `docs/agent/errors.md#<code>`, lower-cased
|
|
49
|
+
with `_` replaced by `-`.
|
|
50
|
+
|
|
51
|
+
## Module layout
|
|
52
|
+
|
|
53
|
+
```text
|
|
54
|
+
src/features/invoicing/
|
|
55
|
+
contract.ts # exported service types and the requirements the host must supply
|
|
56
|
+
module.ts # buildModule([...]) over the private factories
|
|
57
|
+
store.ts # private services; free to use names other modules also use
|
|
58
|
+
check.ts # type-checks this module alone; never imported, not built
|
|
59
|
+
tsconfig.json # extends the root tsconfig and includes only this directory
|
|
60
|
+
invoicing.test.ts
|
|
61
|
+
src/app.ts # installs every module, one installModule call per line
|
|
62
|
+
src/app.check.ts # verifyGraph() on the application builder: the merge check
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Inside a file, keep the same order: contract types, private factories, the
|
|
66
|
+
sealed module, then the host. Exclude `check.ts` files and `src/app.check.ts`
|
|
67
|
+
from an emitting build; they have no runtime purpose.
|
|
68
|
+
|
|
69
|
+
A complete module:
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
// src/features/greeting/contract.ts
|
|
73
|
+
export type Greeter = { greet(name: string): string };
|
|
74
|
+
export type GreetingConfig = { greeting: string };
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
// src/features/greeting/module.ts
|
|
79
|
+
import { DiBag } from 'di-bag';
|
|
80
|
+
import type { Greeter, GreetingConfig } from './contract.js';
|
|
81
|
+
|
|
82
|
+
export const greetingModule = DiBag.createBuilder()
|
|
83
|
+
.register({
|
|
84
|
+
greeter: ({ config }: { config: GreetingConfig }): Greeter => ({
|
|
85
|
+
greet: name => `${config.greeting}, ${name}!`,
|
|
86
|
+
}),
|
|
87
|
+
})
|
|
88
|
+
.buildModule(['greeter']);
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
`check.ts` is one statement: install the module, register a typed fixture for
|
|
92
|
+
each requirement, and verify.
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
// src/features/greeting/check.ts
|
|
96
|
+
import { DiBag } from 'di-bag';
|
|
97
|
+
import type { GreetingConfig } from './contract.js';
|
|
98
|
+
import { greetingModule } from './module.js';
|
|
99
|
+
|
|
100
|
+
DiBag.createBuilder()
|
|
101
|
+
.installModule(greetingModule)
|
|
102
|
+
.register({ config: (): GreetingConfig => ({ greeting: 'Hello' }) })
|
|
103
|
+
.verifyGraph() satisfies void;
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Check one module
|
|
107
|
+
|
|
108
|
+
`src/features/<name>/tsconfig.json`:
|
|
109
|
+
|
|
110
|
+
```json
|
|
111
|
+
{
|
|
112
|
+
"extends": "../../../tsconfig.json",
|
|
113
|
+
"include": ["."]
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Type-check the module and its `check.ts` without the rest of the application:
|
|
118
|
+
|
|
119
|
+
```sh
|
|
120
|
+
npx tsc --noEmit -p src/features/<name>/tsconfig.json
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
A missing requirement fails with its key:
|
|
124
|
+
`required service registrations are missing: config`.
|
|
125
|
+
|
|
126
|
+
## Fast check
|
|
127
|
+
|
|
128
|
+
Define `check:fast` in the consumer's `package.json` as the per-module
|
|
129
|
+
type-check plus that module's tests, and run it after every edit:
|
|
130
|
+
|
|
131
|
+
```json
|
|
132
|
+
"check:fast": "tsc --noEmit -p src/features/$MODULE/tsconfig.json && tsx --test src/features/$MODULE/*.test.ts"
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
```sh
|
|
136
|
+
MODULE=greeting npm run check:fast
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Replace `tsx --test` with the project's test runner. Run the full type-check and
|
|
140
|
+
test suite before merging: [review a merge](docs/agent/recipes.md#review-merge).
|
|
141
|
+
|
|
142
|
+
## Recipes
|
|
143
|
+
|
|
144
|
+
- [Add a request-scoped service with cleanup](docs/agent/recipes.md#add-scoped-service)
|
|
145
|
+
- [Write a fixture test with `fork`](docs/agent/recipes.md#fixture-test)
|
|
146
|
+
- [Split a feature into a module with private services](docs/agent/recipes.md#split-module)
|
|
147
|
+
- [Debug a missing-dependency rejection](docs/agent/recipes.md#debug-missing-dependency)
|
|
148
|
+
- [Add and consume an async client](docs/agent/recipes.md#async-client)
|
|
149
|
+
- [Review a merge](docs/agent/recipes.md#review-merge)
|
package/README.md
CHANGED
|
@@ -1,18 +1,21 @@
|
|
|
1
1
|
# DI Bag
|
|
2
2
|
|
|
3
|
-
TypeScript dependency composition and resource ownership for
|
|
3
|
+
TypeScript dependency composition and resource ownership for modular codebases,
|
|
4
|
+
including the ones coding agents build one feature at a time.
|
|
4
5
|
|
|
5
|
-
[Documentation](https://dany-fedorov.github.io/di-bag/) · [Quickstart](#quickstart) · [
|
|
6
|
+
[Documentation](https://dany-fedorov.github.io/di-bag/) · [Quickstart](#quickstart) · [Modules as units of work](#modules-as-units-of-work) · [Comparison](#how-it-compares) · [Tutorial](docs/guides/tutorial.md) · [API reference](docs/guides/api-reference.md)
|
|
6
7
|
|
|
7
8
|
## Why DI Bag?
|
|
8
9
|
|
|
9
10
|
Compose ordinary TypeScript factories into reusable features. DI Bag checks
|
|
10
11
|
declared dependencies, keeps module internals private, and manages resource
|
|
11
|
-
creation and cleanup. Build
|
|
12
|
-
|
|
12
|
+
creation and cleanup. Build each feature against an explicit contract, test it
|
|
13
|
+
with replaced dependencies, and let the compiler check the composition when
|
|
14
|
+
independently developed features come together.
|
|
13
15
|
|
|
14
|
-
- **[
|
|
15
|
-
|
|
16
|
+
- **[Modules a single owner can build in isolation](docs/guides/examples-modularity.md).**
|
|
17
|
+
A person or a coding agent implements one feature against its contract
|
|
18
|
+
without exposing its internals or reading another feature's source.
|
|
16
19
|
- **[Compile-time wiring checks](docs/guides/examples-type-checking.md).**
|
|
17
20
|
Catch missing dependencies and incompatible replacements before starting the app.
|
|
18
21
|
- **[Metadata inspection without service startup](docs/guides/examples-extensibility.md).**
|
|
@@ -35,8 +38,6 @@ The minimum supported TypeScript version is **6.0.3**; enable `strict` in your
|
|
|
35
38
|
`tsconfig.json`. The repository checks classic TypeScript 6.0.3 and native 7.0.2.
|
|
36
39
|
For browsers and Deno, see [runtime support](#runtime-support).
|
|
37
40
|
|
|
38
|
-
For an older checkout, follow the [single builder](docs/migrations/single-builder.md) and [API renaming](docs/migrations/api-renaming.md) migration guides.
|
|
39
|
-
|
|
40
41
|
## Quickstart
|
|
41
42
|
|
|
42
43
|
A **service** can be a configuration object, a database client, or a function.
|
|
@@ -44,11 +45,11 @@ A **factory** creates a service. A **bag** holds those factories and gives each
|
|
|
44
45
|
one access to the services it needs. Services are created when needed, and
|
|
45
46
|
resources are cleaned up when you provide a disposer and close their bag.
|
|
46
47
|
|
|
47
|
-
|
|
48
|
+
Import from `di-bag`. Here, `greeter` needs `config`. Its parameter
|
|
48
49
|
type describes that dependency, and its return value is the service it provides:
|
|
49
50
|
|
|
50
51
|
```ts
|
|
51
|
-
import { DiBag } from 'di-bag
|
|
52
|
+
import { DiBag } from 'di-bag';
|
|
52
53
|
|
|
53
54
|
const app = DiBag.createBuilder()
|
|
54
55
|
.register({
|
|
@@ -102,7 +103,7 @@ An async factory provides a promise. Declare that promise in any dependent
|
|
|
102
103
|
factory and await it where you need the value:
|
|
103
104
|
|
|
104
105
|
```ts
|
|
105
|
-
import { DiBag } from 'di-bag
|
|
106
|
+
import { DiBag } from 'di-bag';
|
|
106
107
|
|
|
107
108
|
const app = DiBag.createBuilder()
|
|
108
109
|
.register({
|
|
@@ -124,7 +125,7 @@ factories keep returning ordinary values. See
|
|
|
124
125
|
Wrap a factory with `withDisposal` to tell the bag how to release its result:
|
|
125
126
|
|
|
126
127
|
```ts
|
|
127
|
-
import { DiBag } from 'di-bag
|
|
128
|
+
import { DiBag } from 'di-bag';
|
|
128
129
|
|
|
129
130
|
const resources = DiBag.createBuilder()
|
|
130
131
|
.register({
|
|
@@ -163,31 +164,37 @@ The [tutorial](docs/guides/tutorial.md) also covers modules with private service
|
|
|
163
164
|
typed tokens, class and function adapters, optional and lazy dependencies,
|
|
164
165
|
collections, startup, metadata, observers, and plugin validation.
|
|
165
166
|
|
|
166
|
-
##
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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.
|
|
191
198
|
|
|
192
199
|
## How it compares
|
|
193
200
|
|
|
@@ -214,12 +221,13 @@ The package has **zero runtime dependencies** and two entry points:
|
|
|
214
221
|
|
|
215
222
|
| Import | Purpose |
|
|
216
223
|
| --- | --- |
|
|
217
|
-
| `di-bag
|
|
218
|
-
| `di-bag` |
|
|
224
|
+
| `di-bag` | The entry to use. Configures native Promise detection itself on Node, Bun, and Deno through `process.getBuiltinModule`; has no `node:` imports, so it also bundles for browsers. |
|
|
225
|
+
| `di-bag/node` | The same API with detection configured explicitly at import, for Node and Bun. |
|
|
219
226
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
227
|
+
On hosts without `process.getBuiltinModule` (browsers, workers), `build()`
|
|
228
|
+
rejects automatic acquisition stages unless you configure a trusted classifier
|
|
229
|
+
or give each stage an explicit acquisition mode. See
|
|
230
|
+
[portable mode](docs/guides/tutorial.md#portable-mode) for both options.
|
|
223
231
|
|
|
224
232
|
## Tradeoffs and limits
|
|
225
233
|
|
|
@@ -227,11 +235,13 @@ for both setup options.
|
|
|
227
235
|
boundaries, manage an agent's context window, or replace behavioral tests.
|
|
228
236
|
- **Async dependencies are explicit.** A factory returning `Promise<T>` exposes
|
|
229
237
|
that promise. Consumers declare and await it themselves.
|
|
230
|
-
- **Cleanup waits for your work.** Cancellation is cooperative; a
|
|
231
|
-
disposer that never settles
|
|
238
|
+
- **Cleanup waits for your work by default.** Cancellation is cooperative; a
|
|
239
|
+
factory or disposer that never settles keeps `close()` pending. Pass
|
|
240
|
+
`close({ timeoutMs, signal })` to stop waiting: the rejection names the
|
|
241
|
+
disposers still running and cleanup continues in the background.
|
|
232
242
|
- **Type safety follows the declared graph.** Casts, unchecked JavaScript, and
|
|
233
243
|
unknown plugins need appropriate runtime checks. Dependency cycles are detected
|
|
234
|
-
at runtime.
|
|
244
|
+
at runtime, or before running by [`di-bag-graph`](tools/graph/README.md).
|
|
235
245
|
- **Graph types have a compiler cost.** Very long fluent expressions can exceed
|
|
236
246
|
compiler limits. Classic TypeScript still fails the recorded 1,000-call named
|
|
237
247
|
registration and replacement cases; use bulk registration or smaller groups.
|
|
@@ -247,13 +257,15 @@ for both setup options.
|
|
|
247
257
|
| [Complete tutorial](docs/guides/tutorial.md) | Learn every public API through examples, from first composition to advanced ownership. |
|
|
248
258
|
| [API reference](docs/guides/api-reference.md) | Exact generated signatures, overloads, type parameters, and API inventories. |
|
|
249
259
|
| [Server guide](docs/guides/server-integration.md) | Node HTTP, Express, Fastify, Bun, and Deno: shared services, request scopes, startup, and shutdown. |
|
|
250
|
-
| [
|
|
260
|
+
| [Radical modularity](docs/guides/examples-modularity.md) | The recommended module layout, separately owned features, isolated tests, and contributed tools. |
|
|
261
|
+
| [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. |
|
|
262
|
+
| [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. |
|
|
263
|
+
| [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
264
|
| [Runnable examples](examples) | Modules, tokens, composition, collections, plugins, observers, scopes, and provider metadata. |
|
|
252
265
|
| [Integration guide](docs/guides/enterprise-integration.md) | Tested recipes for request ownership, substitutions, and dynamic features. |
|
|
253
266
|
| [Comparison with alternatives](docs/guides/comparison.md) | When DI Bag or another approach may be a better fit, with primary sources. |
|
|
254
|
-
| [Migration guides](docs/migrations/single-builder.md) | Before/after examples for the single builder and the [earlier API renaming](docs/migrations/api-renaming.md). |
|
|
255
267
|
| [Development and verification](docs/guides/development.md) | Full checks, portable runtime testing, compiler scale, and performance evidence. |
|
|
256
|
-
| [Documentation map](docs/README.md) |
|
|
268
|
+
| [Documentation map](docs/README.md) | Every guide, the generated reference, and the contributor documents. |
|
|
257
269
|
|
|
258
270
|
## Working on DI Bag
|
|
259
271
|
|
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
import type { Provider } from './provider';
|
|
2
2
|
import type { Factory } from './registration';
|
|
3
|
-
import type { Acquired, AcquisitionMode, NativeOutput, ModeOptions } from './acquisition-mode';
|
|
3
|
+
import type { Acquired, AcquisitionMode, AutoOutput, NativeOutput, ModeOptions } from './acquisition-mode';
|
|
4
4
|
import type { TokenDependencyContract } from './token-types';
|
|
5
|
-
/**
|
|
5
|
+
/**
|
|
6
|
+
* Cooperative cancellation information supplied to a context-aware acquisition.
|
|
7
|
+
* @see https://dany-fedorov.github.io/di-bag/guides/tutorial.html#start-selected-services-and-cancel-cooperatively
|
|
8
|
+
*/
|
|
6
9
|
export interface AcquisitionContext {
|
|
7
10
|
/** Aborted when the acquisition's owning scope begins closing. */
|
|
8
11
|
readonly signal: AbortSignal;
|
|
9
12
|
}
|
|
10
13
|
type ContextFactory = (this: void, deps: never, context: AcquisitionContext) => unknown;
|
|
11
|
-
/**
|
|
14
|
+
/**
|
|
15
|
+
* The named-dependency factory contract retained by an acquisition-context callback.
|
|
16
|
+
* @see https://dany-fedorov.github.io/di-bag/guides/tutorial.html#start-selected-services-and-cancel-cooperatively
|
|
17
|
+
*/
|
|
12
18
|
export type ContextualFactory<F extends ContextFactory> = (this: void, deps: Parameters<F> extends [] ? {} : Parameters<F>[0]) => ReturnType<F>;
|
|
13
19
|
type FactoryOptions<M extends AcquisitionMode> = 'auto' extends M ? [options?: {
|
|
14
20
|
readonly context?: never;
|
|
@@ -26,7 +32,7 @@ type FactoryOptions<M extends AcquisitionMode> = 'auto' extends M ? [options?: {
|
|
|
26
32
|
* @typeParam F - The complete callback signature, retaining dependency and output inference.
|
|
27
33
|
* @typeParam M - The raw, nativePromise, or configured auto acquisition policy.
|
|
28
34
|
*/
|
|
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
|
|
35
|
+
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: {
|
|
30
36
|
readonly context: 'acquisition';
|
|
31
37
|
} & ModeOptions<M>): Provider<ContextualFactory<F>, Readonly<{}>, readonly [], TokenDependencyContract, Acquired<ReturnType<F>, M>>;
|
|
32
38
|
/**
|
|
@@ -38,5 +44,5 @@ export declare function fromFactory<F extends (this: void, deps: never, context:
|
|
|
38
44
|
* @typeParam F - The exact factory signature and exposed result.
|
|
39
45
|
* @typeParam M - The raw, nativePromise, or configured auto acquisition policy.
|
|
40
46
|
*/
|
|
41
|
-
export declare function fromFactory<F extends Factory, M extends AcquisitionMode = 'auto'>(callback: F & NativeOutput<ReturnType<NoInfer<F>>, NoInfer<M>>, ...options: FactoryOptions<M>): Provider<F, Readonly<{}>, readonly [], TokenDependencyContract, Acquired<ReturnType<F>, M>>;
|
|
47
|
+
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>>;
|
|
42
48
|
export {};
|
|
@@ -26,6 +26,11 @@ export declare class AcquisitionFamily {
|
|
|
26
26
|
leave(): void;
|
|
27
27
|
ancestry(bindingId: BindingId, ownerId: symbol, label: string, from?: AttemptIdentity): AcquisitionHistory | undefined;
|
|
28
28
|
dependencyPath(from: AttemptIdentity, dependency: string): readonly string[];
|
|
29
|
+
/** Distinct consumer-to-dependency binding edges recorded by live attempts, in attempt order. */
|
|
30
|
+
observedEdges(): readonly {
|
|
31
|
+
readonly from: BindingId;
|
|
32
|
+
readonly to: BindingId;
|
|
33
|
+
}[];
|
|
29
34
|
retireIncoming(attempt: AttemptIdentity): void;
|
|
30
35
|
recordEdge(from: AttemptIdentity, to: AttemptIdentity): void;
|
|
31
36
|
private path;
|
|
@@ -78,6 +78,26 @@ class AcquisitionFamily {
|
|
|
78
78
|
history.reverse();
|
|
79
79
|
return Object.freeze([...history, from.label, dependency]);
|
|
80
80
|
}
|
|
81
|
+
/** Distinct consumer-to-dependency binding edges recorded by live attempts, in attempt order. */
|
|
82
|
+
observedEdges() {
|
|
83
|
+
const seen = new Map();
|
|
84
|
+
const edges = [];
|
|
85
|
+
for (const attempt of this.attempts.values()) {
|
|
86
|
+
for (const dependency of attempt.dependencies) {
|
|
87
|
+
const target = this.attempts.get(dependency);
|
|
88
|
+
if (!target)
|
|
89
|
+
continue;
|
|
90
|
+
let targets = seen.get(attempt.bindingId);
|
|
91
|
+
if (!targets)
|
|
92
|
+
seen.set(attempt.bindingId, targets = new Set());
|
|
93
|
+
if (targets.has(target.bindingId))
|
|
94
|
+
continue;
|
|
95
|
+
targets.add(target.bindingId);
|
|
96
|
+
edges.push(Object.freeze({ from: attempt.bindingId, to: target.bindingId }));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return Object.freeze(edges);
|
|
100
|
+
}
|
|
81
101
|
retireIncoming(attempt) {
|
|
82
102
|
const consumers = this.incoming.get(attempt.id);
|
|
83
103
|
if (!consumers)
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import type { LifecycleObservers } from './observers';
|
|
2
|
-
import type { Unsatisfied } from './types';
|
|
2
|
+
import type { SeeErrors, StructuralThenable, Unsatisfied } from './types';
|
|
3
3
|
/**
|
|
4
4
|
* How an acquisition stage treats its returned value: configured classification,
|
|
5
5
|
* the exact raw value, or an observed native Promise fulfillment.
|
|
6
|
+
* @see https://dany-fedorov.github.io/di-bag/guides/tutorial.html#portable-mode
|
|
6
7
|
*/
|
|
7
8
|
export type AcquisitionMode = 'auto' | 'raw' | 'nativePromise';
|
|
8
|
-
/**
|
|
9
|
+
/**
|
|
10
|
+
* Portable facade configuration for `auto` acquisition stages.
|
|
11
|
+
* @see https://dany-fedorov.github.io/di-bag/guides/tutorial.html#portable-mode
|
|
12
|
+
*/
|
|
9
13
|
export interface RuntimeOptions {
|
|
10
14
|
/** Return true only for native Promises the host can observe without thenable assimilation. */
|
|
11
15
|
readonly isNativePromise: (this: void, value: unknown) => boolean;
|
|
@@ -28,7 +32,10 @@ export type StageOptions<M extends AcquisitionMode> = 'auto' extends M ? [option
|
|
|
28
32
|
readonly acquisitionMode: M;
|
|
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;
|
|
35
|
+
/** Reject a structural thenable output when the stage would classify it automatically. */
|
|
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;
|
|
31
37
|
export declare function acquisitionMode(options: {
|
|
32
38
|
readonly acquisitionMode?: AcquisitionMode;
|
|
33
39
|
} | undefined, fallback?: AcquisitionMode): AcquisitionMode;
|
|
34
|
-
|
|
40
|
+
/** Resolve the classifier when a graph first needs one; a configured classifier always wins. */
|
|
41
|
+
export declare function requireClassifier(context: RuntimeContext): RuntimeContext;
|
package/dist/acquisition-mode.js
CHANGED
|
@@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.unconfigured = void 0;
|
|
4
4
|
exports.runtimeContext = runtimeContext;
|
|
5
5
|
exports.acquisitionMode = acquisitionMode;
|
|
6
|
-
exports.
|
|
6
|
+
exports.requireClassifier = requireClassifier;
|
|
7
7
|
const errors_1 = require("./errors");
|
|
8
8
|
exports.unconfigured = Object.freeze({});
|
|
9
9
|
function runtimeContext(options, previous = exports.unconfigured) {
|
|
@@ -25,11 +25,29 @@ function acquisitionMode(options, fallback = 'auto') {
|
|
|
25
25
|
throw (0, errors_1.libraryError)('DI_BAG_INVALID_ACQUISITION_MODE', 'invalid acquisitionMode: use auto, raw, or nativePromise', { option: 'acquisitionMode' });
|
|
26
26
|
return mode;
|
|
27
27
|
}
|
|
28
|
-
|
|
28
|
+
/**
|
|
29
|
+
* Read the host classifier through `process.getBuiltinModule` (Node, Bun, Deno). A call, not an
|
|
30
|
+
* import, keeps `node:` specifiers out of the root entry's module graph for bundlers and browsers.
|
|
31
|
+
*/
|
|
32
|
+
function hostClassifier() {
|
|
33
|
+
const host = globalThis.process;
|
|
34
|
+
if (typeof host !== 'object' || host === null)
|
|
35
|
+
return undefined;
|
|
36
|
+
const load = host.getBuiltinModule;
|
|
37
|
+
if (typeof load !== 'function')
|
|
38
|
+
return undefined;
|
|
39
|
+
const types = Reflect.apply(load, host, ['node:util/types']);
|
|
40
|
+
if (typeof types !== 'object' || types === null)
|
|
41
|
+
return undefined;
|
|
42
|
+
const { isPromise } = types;
|
|
43
|
+
return typeof isPromise === 'function' ? isPromise : undefined;
|
|
44
|
+
}
|
|
45
|
+
/** Resolve the classifier when a graph first needs one; a configured classifier always wins. */
|
|
46
|
+
function requireClassifier(context) {
|
|
29
47
|
if (context.isNativePromise)
|
|
30
|
-
return;
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
48
|
+
return context;
|
|
49
|
+
const isNativePromise = hostClassifier();
|
|
50
|
+
if (isNativePromise)
|
|
51
|
+
return Object.freeze({ ...context, isNativePromise });
|
|
52
|
+
throw (0, errors_1.libraryError)('DI_BAG_CLASSIFIER_REQUIRED', 'this host has no process.getBuiltinModule; configure DiBag.withConfiguration({ runtime: { isNativePromise } }) or give each automatic registration an explicit acquisitionMode', { option: 'runtime.isNativePromise' });
|
|
35
53
|
}
|
package/dist/acquisition.d.ts
CHANGED
|
@@ -32,9 +32,15 @@ export declare class ScopeAcquisitions {
|
|
|
32
32
|
isTransient(bindingId: BindingId, path?: readonly BindingId[]): boolean;
|
|
33
33
|
/** Relationship uses the effective owner graph; frames use canonical attempts. */
|
|
34
34
|
inspectDescription(bindingId: BindingId): Pick<RegistrationSnapshot<object, readonly unknown[]>, 'registrationMetadata' | 'aliasTarget'>;
|
|
35
|
+
observedEdges(): readonly {
|
|
36
|
+
readonly from: BindingId;
|
|
37
|
+
readonly to: BindingId;
|
|
38
|
+
}[];
|
|
35
39
|
private assertAliasPath;
|
|
36
40
|
assertOpen(): void;
|
|
37
41
|
close(beforeDispose?: Promise<void>, cause?: unknown): Promise<void>;
|
|
42
|
+
/** Labels of this scope's running disposers and of acquisitions close is still draining. */
|
|
43
|
+
collectProgress(pending: string[], acquiring: string[]): void;
|
|
38
44
|
private getContext;
|
|
39
45
|
private resolveBinding;
|
|
40
46
|
private eventFields;
|
package/dist/acquisition.js
CHANGED
|
@@ -115,6 +115,7 @@ class ScopeAcquisitions {
|
|
|
115
115
|
const owner = this.owner(bindingId);
|
|
116
116
|
return owner === this ? { registrationMetadata: description.metadata } : owner.inspectDescription(bindingId);
|
|
117
117
|
}
|
|
118
|
+
observedEdges() { return this.family.observedEdges(); }
|
|
118
119
|
assertAliasPath(bindingId, path) {
|
|
119
120
|
if (path.includes(bindingId))
|
|
120
121
|
throw (0, errors_1.libraryError)('DI_BAG_CYCLE', `alias cycle: ${[...path, bindingId].map(id => this.graph.label(id)).join(' -> ')}`, { path: Object.freeze([...path, bindingId].map(id => this.graph.label(id))) });
|
|
@@ -137,6 +138,15 @@ class ScopeAcquisitions {
|
|
|
137
138
|
});
|
|
138
139
|
return this.closing;
|
|
139
140
|
}
|
|
141
|
+
/** Labels of this scope's running disposers and of acquisitions close is still draining. */
|
|
142
|
+
collectProgress(pending, acquiring) {
|
|
143
|
+
for (const attempt of this.attempts.values()) {
|
|
144
|
+
if (attempt.state === 'disposing' || this.retired.has(attempt.id))
|
|
145
|
+
pending.push(attempt.label);
|
|
146
|
+
else if (attempt.state === 'creating' || attempt.state === 'pending')
|
|
147
|
+
acquiring.push(attempt.label);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
140
150
|
getContext() {
|
|
141
151
|
if (!this.acquisitionContext) {
|
|
142
152
|
this.controller = new AbortController();
|
|
@@ -239,8 +249,12 @@ class ScopeAcquisitions {
|
|
|
239
249
|
return target === undefined ? undefined : this.takeExposed(this.resolveBinding(target, attempt));
|
|
240
250
|
};
|
|
241
251
|
const references = new Map(description.references.map(reference => [reference.slot, reference]));
|
|
252
|
+
const invalidAccess = (access) => (0, errors_1.libraryError)('DI_BAG_INVALID_DEPENDENCY_ACCESS', `Cannot inspect the dependencies of ${JSON.stringify(attempt.label)}: ${access} is not supported. Read each named dependency directly; the dependency object resolves lazily.`, { operation: 'resolve', consumer: attempt.label, access });
|
|
242
253
|
const deps = new Proxy(Object.create(null), {
|
|
243
254
|
get: (_, key) => {
|
|
255
|
+
// JSON.stringify probes toJSON through get before enumerating; name the real operation.
|
|
256
|
+
if (key === 'toJSON')
|
|
257
|
+
throw invalidAccess('JSON.stringify');
|
|
244
258
|
const reference = typeof key === 'symbol' ? references.get(key) : undefined;
|
|
245
259
|
if (reference)
|
|
246
260
|
return reference.kind === 'lazy' ? () => read(reference.key)
|
|
@@ -249,6 +263,10 @@ class ScopeAcquisitions {
|
|
|
249
263
|
return undefined;
|
|
250
264
|
return read(key);
|
|
251
265
|
},
|
|
266
|
+
// Only `get` is lazy and checked; every other reflection would silently report an empty object.
|
|
267
|
+
has: (_, key) => { throw invalidAccess(`'${String(key)}' in deps`); },
|
|
268
|
+
ownKeys: () => { throw invalidAccess('enumeration (Object.keys, spread, JSON.stringify)'); },
|
|
269
|
+
getOwnPropertyDescriptor: (_, key) => { throw invalidAccess(`descriptor of '${String(key)}'`); },
|
|
252
270
|
});
|
|
253
271
|
this.observeAttempt(attempt, 'acquisition-started');
|
|
254
272
|
this.family.enter(attempt);
|
package/dist/alias-types.d.ts
CHANGED
|
@@ -6,14 +6,23 @@ import type { Singleton, Unsatisfied } from './types';
|
|
|
6
6
|
export type AliasSelection = string | TokenBase;
|
|
7
7
|
export type AliasAdmission<T> = Singleton<T> extends true ? unknown : ValidToken<T> extends true ? unknown : Unsatisfied<'alias requires one singleton name or genuine token', {}>;
|
|
8
8
|
export type AliasTarget<R extends Registrations, T> = T extends string ? T extends keyof R ? unknown : Unsatisfied<'alias requires an existing named target', {}> : [WrongToken<T, R>] extends [never] ? unknown : Unsatisfied<'token dependency has an incompatible or opaque contract', {}>;
|
|
9
|
-
/**
|
|
9
|
+
/**
|
|
10
|
+
* Resolve the service type exposed by a possible alias target.
|
|
11
|
+
* @see https://dany-fedorov.github.io/di-bag/guides/tutorial.html#give-a-dependency-another-lookup-name
|
|
12
|
+
*/
|
|
10
13
|
export type AliasOutput<R extends Registrations, T> = T extends string ? T extends keyof R ? ProviderOutput<R[T]> : never : TokenService<T>;
|
|
11
14
|
export type AliasDestination<R extends Registrations, D, T> = D extends TokenBase ? [AliasOutput<R, T>] extends [TokenService<D>] ? unknown : Unsatisfied<'alias output is not assignable to destination service', {}> : unknown;
|
|
12
|
-
/**
|
|
15
|
+
/**
|
|
16
|
+
* A provider contract that forwards a destination to a canonical target acquisition.
|
|
17
|
+
* @see https://dany-fedorov.github.io/di-bag/guides/tutorial.html#give-a-dependency-another-lookup-name
|
|
18
|
+
*/
|
|
13
19
|
export type AliasRegistration<R extends Registrations, D, T> = Provider<(this: void, deps: T extends string ? Record<T, AliasOutput<R, T>> : Record<never, never>) => AliasOutput<R, T>, Readonly<object>, readonly unknown[], TokenDependencyContract<T extends TokenBase ? readonly [T] : readonly [], D extends TokenBase ? D : never> & {
|
|
14
20
|
readonly alias: SelectionKey<T>;
|
|
15
21
|
}, unknown>;
|
|
16
|
-
/**
|
|
22
|
+
/**
|
|
23
|
+
* The single registration-map entry introduced by an alias operation.
|
|
24
|
+
* @see https://dany-fedorov.github.io/di-bag/guides/tutorial.html#give-a-dependency-another-lookup-name
|
|
25
|
+
*/
|
|
17
26
|
export type AliasEntries<R extends Registrations, D, T> = Record<SelectionKey<D>, AliasRegistration<R, D, T>>;
|
|
18
27
|
/** Reflected generic methods cannot introduce a checked singleton destination. */
|
|
19
28
|
export type AliasEntry<R extends Registrations, D, T> = unknown extends AliasAdmission<D> & AliasAdmission<T> ? {
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Builder } from './di-bag';
|
|
2
|
+
import type { CheckedLifetimes } from './lifetime-types';
|
|
3
|
+
import type { CheckedConstraints, CompleteConstraints, NeedConstraint } from './module-types';
|
|
4
|
+
import type { CheckDependencyCompatibility, CheckDependencyCompleteness, ConsumerReport, Entry, RegistrationsFromEntries } from './types';
|
|
5
|
+
type ReportOf<Check> = unknown extends Check ? never : Check;
|
|
6
|
+
type Reports<E extends Entry, C extends NeedConstraint> = ReportOf<ConsumerReport<CheckDependencyCompatibility<RegistrationsFromEntries<E>>>> | ReportOf<CheckDependencyCompleteness<RegistrationsFromEntries<E>>> | ReportOf<ConsumerReport<CheckedConstraints<C, RegistrationsFromEntries<E>>>> | ReportOf<CompleteConstraints<C, RegistrationsFromEntries<E>>> | ReportOf<CheckedLifetimes<RegistrationsFromEntries<E>, C>>;
|
|
7
|
+
/**
|
|
8
|
+
* The compile-time verdict for a builder: `void` when `build()` would be accepted,
|
|
9
|
+
* otherwise the same failure `build()` reports, including its details.
|
|
10
|
+
* Read it through `builder.verifyGraph() satisfies void;` or as `CompositionReport<typeof builder>`.
|
|
11
|
+
* @see https://dany-fedorov.github.io/di-bag/guides/tutorial.html#read-compile-time-rejections
|
|
12
|
+
*/
|
|
13
|
+
export type CompositionReport<B> = B extends Builder<infer E, infer C> ? [Reports<E, C>] extends [never] ? void : Reports<E, C> : never;
|
|
14
|
+
export {};
|