di-bag 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +6 -5
- package/README.md +14 -10
- package/dist/acquisition-context.d.ts +73 -4
- package/dist/acquisition-context.js +30 -2
- package/dist/acquisition-mode.d.ts +10 -2
- package/dist/acquisition-mode.js +13 -5
- package/dist/acquisition.d.ts +11 -2
- package/dist/acquisition.js +36 -9
- package/dist/di-bag.d.ts +25 -2
- package/dist/di-bag.js +2 -2
- package/dist/index.d.ts +1 -1
- package/dist/provider-execution.d.ts +38 -2
- package/dist/provider-execution.js +145 -17
- package/dist/runtime.d.ts +1 -0
- package/dist/runtime.js +14 -3
- package/dist/startup.js +11 -2
- package/dist/types.d.ts +1 -1
- package/docs/agent/api-card.md +17 -0
- package/docs/agent/errors.md +108 -12
- package/docs/agent/recipes.md +113 -0
- package/package.json +8 -1
package/AGENTS.md
CHANGED
|
@@ -12,9 +12,9 @@ Every compiler and runtime message: [docs/agent/errors.md](docs/agent/errors.md)
|
|
|
12
12
|
|
|
13
13
|
1. **Import from `di-bag`:** `import { DiBag } from 'di-bag';`. It configures
|
|
14
14
|
itself on Node, Bun, and Deno; `di-bag/node` is the same API in explicit form.
|
|
15
|
-
|
|
16
|
-
[
|
|
17
|
-
|
|
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
18
|
2. **A factory declares its dependencies in the type of its one object
|
|
19
19
|
parameter; destructure it** (`({ clock }: { clock: Clock }) => ...`) or read
|
|
20
20
|
`deps.clock` directly. The object is a Proxy that resolves each property when
|
|
@@ -31,8 +31,9 @@ Every compiler and runtime message: [docs/agent/errors.md](docs/agent/errors.md)
|
|
|
31
31
|
method (query builders) is [rejected](docs/agent/errors.md#structural-thenable).
|
|
32
32
|
Return `Promise.resolve(builder)` or use `DiBag.fromFactory(create, { acquisitionMode: 'raw' })`.
|
|
33
33
|
6. **Ownership.** `DiBag.withDisposal(factory, dispose)` makes the bag own the
|
|
34
|
-
value; `close()` runs disposers, dependents first. Close every scope and
|
|
35
|
-
you create
|
|
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'`.
|
|
36
37
|
7. **Replace dependencies in tests with `fork(keys, overrides)`**; each
|
|
37
38
|
override must satisfy the original contract.
|
|
38
39
|
8. **Modules.** Register a feature's factories, then `buildModule(['exported'])`.
|
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# DI Bag
|
|
2
2
|
|
|
3
|
-
TypeScript dependency composition and resource ownership for modular codebases
|
|
4
|
-
|
|
3
|
+
TypeScript dependency composition and resource ownership for modular codebases.
|
|
4
|
+
Built for coding agents that ship one feature at a time.
|
|
5
5
|
|
|
6
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)
|
|
7
7
|
|
|
@@ -225,9 +225,9 @@ The package has **zero runtime dependencies** and two entry points:
|
|
|
225
225
|
| `di-bag/node` | The same API with detection configured explicitly at import, for Node and Bun. |
|
|
226
226
|
|
|
227
227
|
On hosts without `process.getBuiltinModule` (browsers, workers), `build()`
|
|
228
|
-
rejects automatic acquisition stages
|
|
229
|
-
|
|
230
|
-
[portable mode](docs/guides/tutorial.md#portable-mode)
|
|
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).
|
|
231
231
|
|
|
232
232
|
## Tradeoffs and limits
|
|
233
233
|
|
|
@@ -242,13 +242,16 @@ or give each stage an explicit acquisition mode. See
|
|
|
242
242
|
- **Type safety follows the declared graph.** Casts, unchecked JavaScript, and
|
|
243
243
|
unknown plugins need appropriate runtime checks. Dependency cycles are detected
|
|
244
244
|
at runtime, or before running by [`di-bag-graph`](tools/graph/README.md).
|
|
245
|
-
- **Graph types have a compiler cost.**
|
|
246
|
-
compiler
|
|
247
|
-
|
|
248
|
-
|
|
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).
|
|
249
251
|
- **Framework integration belongs to the application.** DI Bag provides the
|
|
250
252
|
composition and ownership primitives; the host connects request, job, or UI
|
|
251
|
-
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.
|
|
252
255
|
|
|
253
256
|
## Explore further
|
|
254
257
|
|
|
@@ -257,6 +260,7 @@ or give each stage an explicit acquisition mode. See
|
|
|
257
260
|
| [Complete tutorial](docs/guides/tutorial.md) | Learn every public API through examples, from first composition to advanced ownership. |
|
|
258
261
|
| [API reference](docs/guides/api-reference.md) | Exact generated signatures, overloads, type parameters, and API inventories. |
|
|
259
262
|
| [Server guide](docs/guides/server-integration.md) | Node HTTP, Express, Fastify, Bun, and Deno: shared services, request scopes, startup, and shutdown. |
|
|
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`. |
|
|
260
264
|
| [Radical modularity](docs/guides/examples-modularity.md) | The recommended module layout, separately owned features, isolated tests, and contributed tools. |
|
|
261
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. |
|
|
262
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. |
|
|
@@ -1,16 +1,39 @@
|
|
|
1
1
|
import type { Provider } from './provider';
|
|
2
2
|
import type { Factory } from './registration';
|
|
3
|
-
import type { Acquired, AcquisitionMode, AutoOutput, NativeOutput, ModeOptions } from './acquisition-mode';
|
|
3
|
+
import type { Acquired, AcquisitionMode, AsyncOutput, AutoOutput, NativeOutput, ModeOptions, SyncOutput } from './acquisition-mode';
|
|
4
4
|
import type { TokenDependencyContract } from './token-types';
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
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.
|
|
7
22
|
* @see https://dany-fedorov.github.io/di-bag/guides/tutorial.html#start-selected-services-and-cancel-cooperatively
|
|
8
23
|
*/
|
|
9
24
|
export interface AcquisitionContext {
|
|
10
25
|
/** Aborted when the acquisition's owning scope begins closing. */
|
|
11
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;
|
|
12
35
|
}
|
|
13
|
-
type ContextFactory = (this: void, deps: never,
|
|
36
|
+
type ContextFactory = (this: void, deps: never, factoryCtx: AcquisitionContext) => unknown;
|
|
14
37
|
/**
|
|
15
38
|
* The named-dependency factory contract retained by an acquisition-context callback.
|
|
16
39
|
* @see https://dany-fedorov.github.io/di-bag/guides/tutorial.html#start-selected-services-and-cancel-cooperatively
|
|
@@ -32,7 +55,7 @@ type FactoryOptions<M extends AcquisitionMode> = 'auto' extends M ? [options?: {
|
|
|
32
55
|
* @typeParam F - The complete callback signature, retaining dependency and output inference.
|
|
33
56
|
* @typeParam M - The raw, nativePromise, or configured auto acquisition policy.
|
|
34
57
|
*/
|
|
35
|
-
export declare function fromFactory<F extends (this: void, deps: never,
|
|
58
|
+
export declare function fromFactory<F extends (this: void, deps: never, factoryCtx: AcquisitionContext) => ('nativePromise' extends M ? Promise<unknown> : unknown), M extends AcquisitionMode = 'auto'>(callback: F & AutoOutput<ReturnType<NoInfer<F>>, NoInfer<M>>, options: {
|
|
36
59
|
readonly context: 'acquisition';
|
|
37
60
|
} & ModeOptions<M>): Provider<ContextualFactory<F>, Readonly<{}>, readonly [], TokenDependencyContract, Acquired<ReturnType<F>, M>>;
|
|
38
61
|
/**
|
|
@@ -45,4 +68,50 @@ export declare function fromFactory<F extends (this: void, deps: never, context:
|
|
|
45
68
|
* @typeParam M - The raw, nativePromise, or configured auto acquisition policy.
|
|
46
69
|
*/
|
|
47
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>>>;
|
|
48
117
|
export {};
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.fromFactory = fromFactory;
|
|
4
|
+
exports.fromSyncFactory = fromSyncFactory;
|
|
5
|
+
exports.fromAsyncFactory = fromAsyncFactory;
|
|
4
6
|
const errors_1 = require("./errors");
|
|
5
7
|
const provider_1 = require("./provider");
|
|
6
8
|
const provider_operations_1 = require("./provider-operations");
|
|
@@ -11,9 +13,35 @@ function fromFactory(callback, options) {
|
|
|
11
13
|
const mode = (0, acquisition_mode_1.acquisitionMode)(options);
|
|
12
14
|
if (options?.context !== undefined && options.context !== 'acquisition')
|
|
13
15
|
throw (0, errors_1.libraryError)('DI_BAG_INVALID_FACTORY', 'fromFactory context must be acquisition', { operation: 'fromFactory' });
|
|
14
|
-
|
|
16
|
+
return factoryProvider(callback, mode, options?.context === 'acquisition');
|
|
17
|
+
}
|
|
18
|
+
/** Build the provider for one validated factory; `fromFactory` and both portable helpers end here. */
|
|
19
|
+
function factoryProvider(callback, mode, contextual) {
|
|
15
20
|
const handle = (0, provider_1.createProvider)();
|
|
16
|
-
const create = contextual ? ((deps,
|
|
21
|
+
const create = contextual ? ((deps, factoryCtx) => callback(deps, factoryCtx)) : callback;
|
|
17
22
|
(0, provider_operations_1.retainDescription)(handle, (0, provider_operations_1.sourceDescription)(create, undefined, [], mode, contextual));
|
|
18
23
|
return handle;
|
|
19
24
|
}
|
|
25
|
+
/** Validate a portable helper's options; the mode is the helper's, so `acquisitionMode` is refused. */
|
|
26
|
+
function portableContext(operation, options) {
|
|
27
|
+
if (options === undefined)
|
|
28
|
+
return false;
|
|
29
|
+
if (typeof options !== 'object' || options === null)
|
|
30
|
+
throw (0, errors_1.libraryError)('DI_BAG_INVALID_FACTORY', `${operation} options must be an object`, { operation });
|
|
31
|
+
if ('acquisitionMode' in options)
|
|
32
|
+
throw (0, errors_1.libraryError)('DI_BAG_INVALID_FACTORY', `${operation} selects its acquisitionMode itself`, { operation });
|
|
33
|
+
const context = options.context;
|
|
34
|
+
if (context !== undefined && context !== 'acquisition')
|
|
35
|
+
throw (0, errors_1.libraryError)('DI_BAG_INVALID_FACTORY', `${operation} context must be acquisition`, { operation });
|
|
36
|
+
return context === 'acquisition';
|
|
37
|
+
}
|
|
38
|
+
function fromSyncFactory(callback, options) {
|
|
39
|
+
if (typeof callback !== 'function')
|
|
40
|
+
throw (0, errors_1.libraryError)('DI_BAG_INVALID_FACTORY', 'fromSyncFactory requires a function', { operation: 'fromSyncFactory' });
|
|
41
|
+
return factoryProvider(callback, 'raw', portableContext('fromSyncFactory', options));
|
|
42
|
+
}
|
|
43
|
+
function fromAsyncFactory(callback, options) {
|
|
44
|
+
if (typeof callback !== 'function')
|
|
45
|
+
throw (0, errors_1.libraryError)('DI_BAG_INVALID_FACTORY', 'fromAsyncFactory requires a function', { operation: 'fromAsyncFactory' });
|
|
46
|
+
return factoryProvider(callback, 'nativePromise', portableContext('fromAsyncFactory', options));
|
|
47
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { LifecycleObservers } from './observers';
|
|
2
|
-
import type { SeeErrors, 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.
|
|
@@ -34,8 +34,16 @@ export type StageOptions<M extends AcquisitionMode> = 'auto' extends M ? [option
|
|
|
34
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
35
|
/** Reject a structural thenable output when the stage would classify it automatically. */
|
|
36
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'>}`, {}>;
|
|
37
42
|
export declare function acquisitionMode(options: {
|
|
38
43
|
readonly acquisitionMode?: AcquisitionMode;
|
|
39
44
|
} | undefined, fallback?: AcquisitionMode): AcquisitionMode;
|
|
40
45
|
/** Resolve the classifier when a graph first needs one; a configured classifier always wins. */
|
|
41
|
-
export declare function
|
|
46
|
+
export declare function resolveClassifier(context: RuntimeContext): RuntimeContext | undefined;
|
|
47
|
+
/** The graph has automatic stages and no classifier; `bindings` labels every such registration. */
|
|
48
|
+
export declare function classifierRequired(bindings: readonly string[]): Error;
|
|
49
|
+
export {};
|
package/dist/acquisition-mode.js
CHANGED
|
@@ -3,7 +3,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.unconfigured = void 0;
|
|
4
4
|
exports.runtimeContext = runtimeContext;
|
|
5
5
|
exports.acquisitionMode = acquisitionMode;
|
|
6
|
-
exports.
|
|
6
|
+
exports.resolveClassifier = resolveClassifier;
|
|
7
|
+
exports.classifierRequired = classifierRequired;
|
|
7
8
|
const errors_1 = require("./errors");
|
|
8
9
|
exports.unconfigured = Object.freeze({});
|
|
9
10
|
function runtimeContext(options, previous = exports.unconfigured) {
|
|
@@ -43,11 +44,18 @@ function hostClassifier() {
|
|
|
43
44
|
return typeof isPromise === 'function' ? isPromise : undefined;
|
|
44
45
|
}
|
|
45
46
|
/** Resolve the classifier when a graph first needs one; a configured classifier always wins. */
|
|
46
|
-
function
|
|
47
|
+
function resolveClassifier(context) {
|
|
47
48
|
if (context.isNativePromise)
|
|
48
49
|
return context;
|
|
49
50
|
const isNativePromise = hostClassifier();
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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) });
|
|
53
61
|
}
|
package/dist/acquisition.d.ts
CHANGED
|
@@ -16,7 +16,6 @@ export declare class ScopeAcquisitions {
|
|
|
16
16
|
private state;
|
|
17
17
|
private closing;
|
|
18
18
|
private controller;
|
|
19
|
-
private acquisitionContext;
|
|
20
19
|
private cancellationStarted;
|
|
21
20
|
private cancellationCause;
|
|
22
21
|
private readonly shared;
|
|
@@ -41,7 +40,17 @@ export declare class ScopeAcquisitions {
|
|
|
41
40
|
close(beforeDispose?: Promise<void>, cause?: unknown): Promise<void>;
|
|
42
41
|
/** Labels of this scope's running disposers and of acquisitions close is still draining. */
|
|
43
42
|
collectProgress(pending: string[], acquiring: string[]): void;
|
|
44
|
-
|
|
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;
|
|
45
54
|
private resolveBinding;
|
|
46
55
|
private eventFields;
|
|
47
56
|
private observeAttempt;
|
package/dist/acquisition.js
CHANGED
|
@@ -5,6 +5,19 @@ const errors_1 = require("./errors");
|
|
|
5
5
|
const errors_2 = require("./errors");
|
|
6
6
|
const provider_execution_1 = require("./provider-execution");
|
|
7
7
|
const acquisition_family_1 = require("./acquisition-family");
|
|
8
|
+
/**
|
|
9
|
+
* The reason a close() without a cause aborts with. Built once at load: an error
|
|
10
|
+
* created inside close() keeps an unformatted stack whose frames retain the
|
|
11
|
+
* closing callbacks, and through them the scope and its graph, on every signal an
|
|
12
|
+
* application kept after the bag closed. It stays a plain `AbortError`, so its
|
|
13
|
+
* legacy numeric `code` is what an automatic abort reason had; the message names
|
|
14
|
+
* the diagnostic.
|
|
15
|
+
*/
|
|
16
|
+
const closingReason = (() => {
|
|
17
|
+
const reason = new DOMException((0, errors_1.diagnosticMessage)('DI_BAG_CLOSING', 'bag is closing'), 'AbortError');
|
|
18
|
+
void reason.stack; // format the load-time frames now, so nothing is retained lazily
|
|
19
|
+
return Object.freeze(reason);
|
|
20
|
+
})();
|
|
8
21
|
/** Mutable, runtime-local attempts. Binding descriptions never carry ownership. */
|
|
9
22
|
class ScopeAcquisitions {
|
|
10
23
|
graph;
|
|
@@ -21,7 +34,6 @@ class ScopeAcquisitions {
|
|
|
21
34
|
state = 'open';
|
|
22
35
|
closing;
|
|
23
36
|
controller;
|
|
24
|
-
acquisitionContext;
|
|
25
37
|
cancellationStarted = false;
|
|
26
38
|
cancellationCause;
|
|
27
39
|
shared;
|
|
@@ -132,8 +144,8 @@ class ScopeAcquisitions {
|
|
|
132
144
|
this.closing = Promise.resolve().then(() => {
|
|
133
145
|
// Every descendant admission gate is closed before abort listeners run.
|
|
134
146
|
this.cancellationStarted = true;
|
|
135
|
-
this.cancellationCause = cause;
|
|
136
|
-
this.controller?.abort(
|
|
147
|
+
this.cancellationCause = cause === undefined ? closingReason : cause;
|
|
148
|
+
this.controller?.abort(this.cancellationCause);
|
|
137
149
|
return this.disposeAll(beforeDispose);
|
|
138
150
|
});
|
|
139
151
|
return this.closing;
|
|
@@ -141,20 +153,34 @@ class ScopeAcquisitions {
|
|
|
141
153
|
/** Labels of this scope's running disposers and of acquisitions close is still draining. */
|
|
142
154
|
collectProgress(pending, acquiring) {
|
|
143
155
|
for (const attempt of this.attempts.values()) {
|
|
144
|
-
if (attempt.state === 'disposing' || this.retired.has(attempt.id))
|
|
156
|
+
if (attempt.state === 'disposing' || this.retired.has(attempt.id) || attempt.execution.rollingBack)
|
|
145
157
|
pending.push(attempt.label);
|
|
146
158
|
else if (attempt.state === 'creating' || attempt.state === 'pending')
|
|
147
159
|
acquiring.push(attempt.label);
|
|
148
160
|
}
|
|
149
161
|
}
|
|
150
|
-
|
|
151
|
-
|
|
162
|
+
/** One controller per scope; every acquisition observes the same cancellation. */
|
|
163
|
+
cancellationSignal() {
|
|
164
|
+
if (!this.controller) {
|
|
152
165
|
this.controller = new AbortController();
|
|
153
166
|
if (this.cancellationStarted)
|
|
154
167
|
this.controller.abort(this.cancellationCause);
|
|
155
|
-
this.acquisitionContext = Object.freeze({ signal: this.controller.signal });
|
|
156
168
|
}
|
|
157
|
-
return this.
|
|
169
|
+
return this.controller.signal;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* The signal is scope-wide; deferred cleanup is local to this attempt, so each
|
|
173
|
+
* contextual acquisition receives its own frozen context. The context captures
|
|
174
|
+
* only its disposer stack, never the execution or this scope, so an
|
|
175
|
+
* application that retains it past `close()` retains nothing else. Built here
|
|
176
|
+
* rather than in `resolveBinding` for the same reason: every closure of a
|
|
177
|
+
* function shares one scope, and a factory can retain the dependency proxy.
|
|
178
|
+
*/
|
|
179
|
+
acquisitionContext(disposers) {
|
|
180
|
+
return Object.freeze({
|
|
181
|
+
signal: this.cancellationSignal(),
|
|
182
|
+
pushDisposer: (disposer) => { disposers.push(disposer); },
|
|
183
|
+
});
|
|
158
184
|
}
|
|
159
185
|
resolveBinding(bindingId, from, path = []) {
|
|
160
186
|
// Sharing an alias borrows its lexical parent graph before following targets.
|
|
@@ -271,6 +297,7 @@ class ScopeAcquisitions {
|
|
|
271
297
|
this.observeAttempt(attempt, 'acquisition-started');
|
|
272
298
|
this.family.enter(attempt);
|
|
273
299
|
const directSource = !description.contextual && !description.operations.length;
|
|
300
|
+
const { disposers } = execution;
|
|
274
301
|
try {
|
|
275
302
|
let value;
|
|
276
303
|
if (directSource) {
|
|
@@ -279,7 +306,7 @@ class ScopeAcquisitions {
|
|
|
279
306
|
execution.publishSource(value, description);
|
|
280
307
|
}
|
|
281
308
|
else {
|
|
282
|
-
value = execution.evaluate(description, deps,
|
|
309
|
+
value = execution.evaluate(description, deps, disposers && this.acquisitionContext(disposers));
|
|
283
310
|
}
|
|
284
311
|
attempt.exposed = value;
|
|
285
312
|
attempt.state = attempt.execution.state;
|
package/dist/di-bag.d.ts
CHANGED
|
@@ -10,7 +10,7 @@ import type { CompositionReport } from './composition-report';
|
|
|
10
10
|
import type { CheckedConstraints, CompleteConstraints, ExternalRequirements, IncrementalConstraints, ModulePublicProviders, ModuleSealedConstraints, NeedConstraint } from './module-types';
|
|
11
11
|
import type { CheckedLifetimes, SealAdmission, WithoutExportObligations } from './lifetime-types';
|
|
12
12
|
import { withLifetime } from './lifetime';
|
|
13
|
-
import { fromFactory } from './acquisition-context';
|
|
13
|
+
import { fromFactory, fromSyncFactory, fromAsyncFactory } from './acquisition-context';
|
|
14
14
|
import type { ScopeOptions, DisjointScopeSelection, UnsharedAliases, ScopedAliases } from './scope-types';
|
|
15
15
|
import type { CheckedScopeLifetimes } from './lifetime-types';
|
|
16
16
|
import type { CloseOptions, StartupOptions } from './startup';
|
|
@@ -399,6 +399,29 @@ export interface DiBagApi {
|
|
|
399
399
|
* ```
|
|
400
400
|
*/
|
|
401
401
|
fromFactory: typeof fromFactory;
|
|
402
|
+
/**
|
|
403
|
+
* Describe a synchronous factory that runs on every host: the exact return value is the service and `then` is never read.
|
|
404
|
+
* A Promise or thenable output is rejected at compile time; use `fromAsyncFactory`, or `fromFactory` with `acquisitionMode: 'raw'` when the Promise object itself is the service.
|
|
405
|
+
* @throws `DI_BAG_INVALID_FACTORY` for a non-function, an unknown `context`, or an `acquisitionMode` option.
|
|
406
|
+
* @example
|
|
407
|
+
* ```ts
|
|
408
|
+
* const config = DiBag.fromSyncFactory(() => ({ url: 'memory:' }));
|
|
409
|
+
* ```
|
|
410
|
+
*/
|
|
411
|
+
fromSyncFactory: typeof fromSyncFactory;
|
|
412
|
+
/**
|
|
413
|
+
* Describe an asynchronous factory that runs on every host: the service is the returned native Promise and `withDisposal` receives its fulfilled value.
|
|
414
|
+
* A non-Promise output is rejected at compile time; a thenable that is not a native Promise fails the acquisition with a `TypeError`.
|
|
415
|
+
* @throws `DI_BAG_INVALID_FACTORY` for a non-function, an unknown `context`, or an `acquisitionMode` option.
|
|
416
|
+
* @example
|
|
417
|
+
* ```ts
|
|
418
|
+
* const db = DiBag.withDisposal(
|
|
419
|
+
* DiBag.fromAsyncFactory(async ({ config }: { config: { url: string } }) => ({ url: config.url, end: async () => {} })),
|
|
420
|
+
* db => db.end(),
|
|
421
|
+
* );
|
|
422
|
+
* ```
|
|
423
|
+
*/
|
|
424
|
+
fromAsyncFactory: typeof fromAsyncFactory;
|
|
402
425
|
/**
|
|
403
426
|
* Create a typed token from a unique symbol; `.of<Service>()` fixes its service type.
|
|
404
427
|
* @throws `DI_BAG_INVALID_TOKEN` when the key is not a symbol.
|
|
@@ -536,6 +559,6 @@ export interface DiBagApi {
|
|
|
536
559
|
}
|
|
537
560
|
/**
|
|
538
561
|
* The immutable DI Bag facade. `auto` acquisition uses the host classifier where `process.getBuiltinModule`
|
|
539
|
-
* exists; elsewhere
|
|
562
|
+
* exists; elsewhere register with `fromSyncFactory` and `fromAsyncFactory`, use explicit modes, or configure a classifier.
|
|
540
563
|
*/
|
|
541
564
|
export declare const DiBag: DiBagApi;
|
package/dist/di-bag.js
CHANGED
|
@@ -279,13 +279,13 @@ function facade(context) {
|
|
|
279
279
|
}
|
|
280
280
|
return facade(configured);
|
|
281
281
|
},
|
|
282
|
-
fromFactory: acquisition_context_1.fromFactory, token: tokens_1.token, optional: dependency_references_1.optional, lazy: dependency_references_1.lazy, all: dependency_references_1.all, fromPlugin: plugins_1.fromPlugin, fromFunction: composition_1.fromFunction, fromClass: composition_1.fromClass,
|
|
282
|
+
fromFactory: acquisition_context_1.fromFactory, fromSyncFactory: acquisition_context_1.fromSyncFactory, fromAsyncFactory: acquisition_context_1.fromAsyncFactory, token: tokens_1.token, optional: dependency_references_1.optional, lazy: dependency_references_1.lazy, all: dependency_references_1.all, fromPlugin: plugins_1.fromPlugin, fromFunction: composition_1.fromFunction, fromClass: composition_1.fromClass,
|
|
283
283
|
createBuilder: () => new Builder(new runtime_1.BindingGraph(), context),
|
|
284
284
|
withDisposal: registration_1.withDisposal, withLifetime: lifetime_1.withLifetime, withMetadata: provider_1.withMetadata, transformService: provider_1.transformService,
|
|
285
285
|
});
|
|
286
286
|
}
|
|
287
287
|
/**
|
|
288
288
|
* The immutable DI Bag facade. `auto` acquisition uses the host classifier where `process.getBuiltinModule`
|
|
289
|
-
* exists; elsewhere
|
|
289
|
+
* exists; elsewhere register with `fromSyncFactory` and `fromAsyncFactory`, use explicit modes, or configure a classifier.
|
|
290
290
|
*/
|
|
291
291
|
exports.DiBag = facade(acquisition_mode_1.unconfigured);
|
package/dist/index.d.ts
CHANGED
|
@@ -9,7 +9,7 @@ export type { FactoryWithDisposal, Registration } from './registration';
|
|
|
9
9
|
export type { Provider, ProviderFactory, ProviderGraphContract, ProviderOutput, ProviderAcquiredValue, ProviderNamedDependencies, ProviderRegistrationMetadata, ProviderAcquisitionMetadata, ProviderRequiredTokens, ProviderOptionalTokens, ProviderCollectionTokens } from './provider';
|
|
10
10
|
export type { AcquisitionMode, RuntimeOptions } from './acquisition-mode';
|
|
11
11
|
export type { Lifetime } from './lifetime';
|
|
12
|
-
export type { AcquisitionContext, ContextualFactory } from './acquisition-context';
|
|
12
|
+
export type { AcquisitionContext, ContextualFactory, DisposerContext } from './acquisition-context';
|
|
13
13
|
export type { CloseOptions, StartupOptions } from './startup';
|
|
14
14
|
export type { ScopeOptions, DisjointScopeSelection, UnsharedAliases, ScopedAliases, SharedAliasProviders } from './scope-types';
|
|
15
15
|
export type { Token, TokenBase, TokenKey, TokenService } from './tokens';
|
|
@@ -1,8 +1,26 @@
|
|
|
1
1
|
import type { normalize } from './provider-operations';
|
|
2
2
|
import type { AcquisitionMetadataPresence } from './inspection';
|
|
3
3
|
import type { RuntimeContext } from './acquisition-mode';
|
|
4
|
-
import type { AcquisitionContext } from './acquisition-context';
|
|
4
|
+
import type { AcquisitionContext, DisposerContext } from './acquisition-context';
|
|
5
5
|
type RegistrationDescription = ReturnType<typeof normalize>;
|
|
6
|
+
type PushedDisposer = (this: void, disposerCtx: DisposerContext) => void | Promise<void>;
|
|
7
|
+
/**
|
|
8
|
+
* One factory's pushed disposers. It is held by the frozen acquisition context
|
|
9
|
+
* handed to that factory, so it deliberately references neither the execution
|
|
10
|
+
* nor its scope: a context the application retains must keep nothing but its
|
|
11
|
+
* own registrations alive.
|
|
12
|
+
*/
|
|
13
|
+
export declare class DisposerStack {
|
|
14
|
+
private readonly disposers;
|
|
15
|
+
private settled;
|
|
16
|
+
/** Own a resource the running factory already holds. */
|
|
17
|
+
push(disposer: PushedDisposer): void;
|
|
18
|
+
/** The factory settled; nothing more can be pushed. */
|
|
19
|
+
settle(): void;
|
|
20
|
+
get pending(): boolean;
|
|
21
|
+
/** Take every pushed disposer, last pushed first, and close registration for good. */
|
|
22
|
+
drain(): readonly PushedDisposer[];
|
|
23
|
+
}
|
|
6
24
|
interface ExecutionEvents {
|
|
7
25
|
accepted(): void;
|
|
8
26
|
settled(): void;
|
|
@@ -18,6 +36,8 @@ export declare class CompletedExecution {
|
|
|
18
36
|
readonly state = "ready";
|
|
19
37
|
readonly sourceInFlight = false;
|
|
20
38
|
readonly hasOwnership = false;
|
|
39
|
+
readonly rollingBack = false;
|
|
40
|
+
readonly disposers: undefined;
|
|
21
41
|
readonly error: undefined;
|
|
22
42
|
readonly work: readonly Promise<void>[];
|
|
23
43
|
constructor(frames: AcquisitionMetadataPresence<readonly unknown[]>);
|
|
@@ -32,6 +52,10 @@ export declare class ProviderExecution {
|
|
|
32
52
|
private readonly context;
|
|
33
53
|
private readonly frames;
|
|
34
54
|
private readonly stages;
|
|
55
|
+
/** Allocated only for a context-aware factory, which is the only source that can push disposers. */
|
|
56
|
+
readonly disposers: DisposerStack | undefined;
|
|
57
|
+
private disposersOwned;
|
|
58
|
+
private rollback;
|
|
35
59
|
private readonly pending;
|
|
36
60
|
private result;
|
|
37
61
|
private cleaning;
|
|
@@ -41,13 +65,25 @@ export declare class ProviderExecution {
|
|
|
41
65
|
get state(): 'pending' | 'ready' | 'failed';
|
|
42
66
|
get error(): unknown;
|
|
43
67
|
get hasOwnership(): boolean;
|
|
68
|
+
get rollingBack(): boolean;
|
|
44
69
|
get work(): readonly Promise<void>[];
|
|
70
|
+
/**
|
|
71
|
+
* The source stage settles the stack. A factory that returned hands what it
|
|
72
|
+
* pushed to the bag; a failed one releases it at once, as pending work of this
|
|
73
|
+
* execution. Anchoring on the source rather than on the attempt's result covers
|
|
74
|
+
* a direct projection that is already ready while its source is still running,
|
|
75
|
+
* which no retirement reaches.
|
|
76
|
+
*/
|
|
77
|
+
private settleDisposers;
|
|
78
|
+
private rollbackDisposers;
|
|
79
|
+
/** Run every pushed disposer, last pushed first, all attempted; true when one threw. */
|
|
80
|
+
private runDisposers;
|
|
45
81
|
/** Observe the selected stage, retaining its failure even after retirement. */
|
|
46
82
|
ready(): Promise<void>;
|
|
47
83
|
compact(): ProviderExecution | CompletedExecution;
|
|
48
84
|
/** Classify/own only after the direct operation-free source call has returned. */
|
|
49
85
|
publishSource(value: unknown, description: RegistrationDescription): void;
|
|
50
|
-
evaluate(description: RegistrationDescription, deps: unknown,
|
|
86
|
+
evaluate(description: RegistrationDescription, deps: unknown, context: AcquisitionContext | undefined): unknown;
|
|
51
87
|
private consume;
|
|
52
88
|
private capture;
|
|
53
89
|
private own;
|