di-bag 0.2.0 → 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.
Files changed (43) hide show
  1. package/AGENTS.md +149 -0
  2. package/README.md +59 -46
  3. package/dist/acquisition-context.d.ts +8 -2
  4. package/dist/acquisition-mode.d.ts +9 -4
  5. package/dist/acquisition-mode.js +25 -7
  6. package/dist/acquisition.d.ts +2 -0
  7. package/dist/acquisition.js +9 -0
  8. package/dist/alias-types.d.ts +12 -3
  9. package/dist/composition-report.d.ts +3 -2
  10. package/dist/composition.d.ts +8 -2
  11. package/dist/contribution-types.d.ts +26 -18
  12. package/dist/dependency-references.d.ts +16 -4
  13. package/dist/di-bag.d.ts +320 -41
  14. package/dist/di-bag.js +86 -14
  15. package/dist/errors.d.ts +115 -8
  16. package/dist/errors.js +113 -12
  17. package/dist/index.d.ts +5 -5
  18. package/dist/index.js +2 -1
  19. package/dist/inspection.d.ts +21 -5
  20. package/dist/lifetime-types.d.ts +281 -180
  21. package/dist/lifetime.d.ts +4 -1
  22. package/dist/module-types.d.ts +87 -30
  23. package/dist/module.d.ts +18 -4
  24. package/dist/module.js +31 -7
  25. package/dist/observers.d.ts +25 -6
  26. package/dist/plugins.d.ts +17 -4
  27. package/dist/provider.d.ts +41 -10
  28. package/dist/provider.js +1 -0
  29. package/dist/registration.d.ts +8 -2
  30. package/dist/registration.js +4 -1
  31. package/dist/runtime.d.ts +12 -3
  32. package/dist/runtime.js +25 -8
  33. package/dist/scope-types.d.ts +20 -5
  34. package/dist/startup.d.ts +19 -1
  35. package/dist/startup.js +72 -11
  36. package/dist/token-types.d.ts +26 -8
  37. package/dist/tokens.d.ts +10 -2
  38. package/dist/tokens.js +2 -0
  39. package/dist/types.d.ts +68 -22
  40. package/docs/agent/api-card.md +337 -0
  41. package/docs/agent/errors.md +1042 -0
  42. package/docs/agent/recipes.md +290 -0
  43. package/package.json +11 -5
package/dist/di-bag.js CHANGED
@@ -23,15 +23,17 @@ const plugins_1 = require("./plugins");
23
23
  *
24
24
  * Create bags through {@link DiBagApi.createBuilder} followed by {@link Builder.build} or
25
25
  * {@link Builder.buildAndStart}; the class is exported as a type and has no public constructor.
26
+ * @see https://dany-fedorov.github.io/di-bag/agent/api-card.html#bag
26
27
  */
27
28
  class Bag {
28
- context;
29
29
  #graph;
30
30
  #runtime;
31
+ context;
31
32
  constructor(graph, context, runtime) {
32
- this.context = context;
33
33
  this.#graph = graph;
34
34
  this.#runtime = runtime ?? new runtime_1.BagRuntime(graph, context);
35
+ // Scopes and forks reuse the classifier the runtime resolved, so detection runs once per build.
36
+ this.context = this.#runtime.context;
35
37
  }
36
38
  resolve(token) {
37
39
  return this.#runtime.resolve(typeof token === 'string' ? token : (0, tokens_1.readTokenKey)(token));
@@ -46,6 +48,11 @@ class Bag {
46
48
  * Nothing is acquired. Named dependencies declared on factory parameters are not visible
47
49
  * until the factory runs; the static graph tool reports them from source.
48
50
  * @returns A frozen point-in-time snapshot; application-owned metadata payloads are not frozen.
51
+ * @example
52
+ * ```ts
53
+ * const bag = DiBag.createBuilder().register({ greeting: () => 'hello' }).build();
54
+ * const labels = bag.inspectGraph().bindings.map(binding => binding.label);
55
+ * ```
49
56
  */
50
57
  inspectGraph() { return this.#runtime.inspectGraph(); }
51
58
  createScope(...args) {
@@ -92,12 +99,24 @@ class Bag {
92
99
  /**
93
100
  * Close this bag, drain in-flight work, and dispose owned resources once.
94
101
  * Dependents are disposed before dependencies; remaining independent acquisitions use
95
- * reverse acquisition order. Repeated calls return the same promise.
96
- * @returns The shared shutdown promise.
97
- * @throws {@link DiBagCleanupError} when one or more disposers fail after all cleanup is attempted.
102
+ * reverse acquisition order. Without options the promise waits for cleanup however long it
103
+ * takes, and repeated calls return the same promise. With `timeoutMs` or `signal`, cleanup
104
+ * starts the same way but the returned promise stops waiting when either fires; scopes and
105
+ * forks accept the same options. Close every scope and fork you create; a parent closes its live scopes, never forks.
106
+ * @param options - An optional deadline and abort signal bounding the wait, not the cleanup.
107
+ * @returns The shared shutdown promise, or a bounded wait on it when options are given.
108
+ * @throws {@link DiBagCleanupError} (`DI_BAG_CLEANUP_FAILED`) when one or more disposers fail after all cleanup is attempted;
109
+ * `DI_BAG_CLOSE_FAILED` for other shutdown failures;
110
+ * {@link DiBagCloseCancelledError} (`DI_BAG_CLOSE_TIMEOUT` or `DI_BAG_CLOSE_ABORTED`) when the wait stops first,
111
+ * naming unfinished disposers in `details.pending`; `DI_BAG_INVALID_CLOSE` for malformed options.
112
+ * @example
113
+ * ```ts
114
+ * const bag = DiBag.createBuilder().register({ value: () => 1 }).build();
115
+ * await bag.close({ timeoutMs: 10_000, signal: AbortSignal.timeout(15_000) });
116
+ * ```
98
117
  */
99
- close() {
100
- return this.#runtime.close();
118
+ close(options) {
119
+ return (0, startup_1.closeRuntime)(this.#runtime, options);
101
120
  }
102
121
  }
103
122
  /**
@@ -106,6 +125,7 @@ class Bag {
106
125
  * {@link Builder.build} a bag once its graph is complete, or
107
126
  * {@link Builder.buildModule} a reusable module whose unmet dependencies become
108
127
  * requirements the installing host must satisfy.
128
+ * @see https://dany-fedorov.github.io/di-bag/agent/api-card.html#builder
109
129
  */
110
130
  class Builder {
111
131
  context;
@@ -129,6 +149,12 @@ class Builder {
129
149
  * @param destination - A new string name or typed token.
130
150
  * @param target - The existing name or token whose canonical acquisition is reused.
131
151
  * @returns A new builder; aliases add no cache or ownership of their own.
152
+ * @throws `DI_BAG_INVALID_TOKEN` for a bad token; `DI_BAG_DUPLICATE_REGISTRATION` when the destination exists;
153
+ * `DI_BAG_INVALID_ALIAS` for an absent named target.
154
+ * @example
155
+ * ```ts
156
+ * const builder = DiBag.createBuilder().register({ clock: () => Date.now() }).alias('now', 'clock');
157
+ * ```
132
158
  */
133
159
  alias(destination, target, ...invalid) {
134
160
  const [key, registration] = (0, aliases_1.aliasEntry)(destination, target, key => this.#graph.hasPublic(key));
@@ -139,6 +165,13 @@ class Builder {
139
165
  * @param token - The collection's typed token.
140
166
  * @param registration - A registration whose output satisfies the token service type.
141
167
  * @returns A new builder preserving contribution order.
168
+ * @throws `DI_BAG_INVALID_TOKEN` for a bad token; `DI_BAG_INVALID_REGISTRATION` for an invalid registration.
169
+ * @example
170
+ * ```ts
171
+ * const toolsKey = Symbol('tools');
172
+ * const tools = DiBag.token(toolsKey).of<string>();
173
+ * const builder = DiBag.createBuilder().contribute(tools, () => 'search').contribute(tools, () => 'fetch');
174
+ * ```
142
175
  */
143
176
  // A named callable keeps extracted generic methods nameable in consumer declarations.
144
177
  contribute = ((token, registration) => {
@@ -155,8 +188,17 @@ class Builder {
155
188
  }
156
189
  /**
157
190
  * Install a sealed module, allocating fresh private bindings for this installation.
191
+ * The installing host must register every requirement the module does not register itself.
158
192
  * @param module - A module whose public names do not collide and whose external requirements remain checkable.
159
193
  * @returns A new builder exposing only the module's selected exports.
194
+ * @throws `DI_BAG_INVALID_MODULE` for a value not made by `buildModule`; `DI_BAG_DUPLICATE_REGISTRATION` when an export name is already registered.
195
+ * @example
196
+ * ```ts
197
+ * const greeting = DiBag.createBuilder()
198
+ * .register({ greet: ({ name }: { name: string }) => `hello, ${name}` })
199
+ * .buildModule(['greet']);
200
+ * const bag = DiBag.createBuilder().installModule(greeting).register({ name: () => 'Ada' }).build();
201
+ * ```
160
202
  */
161
203
  installModule(module) {
162
204
  return new Builder(this.#graph.withInstallation((0, module_1.moduleGraph)(module)), this.context);
@@ -168,16 +210,35 @@ class Builder {
168
210
  * become requirements of the module. Installed modules nest: their private
169
211
  * bindings and retained constraints are re-scoped inside this module.
170
212
  * @param keys - A finite tuple of existing names or tokens; an empty tuple is allowed.
213
+ * @param options - An optional `label`; each installation names its private bindings `<label>/<key>` in
214
+ * error messages, cycle paths, `inspectGraph()`, and observer events, and nested labels compose as `outer/inner/key`.
171
215
  * @returns An immutable module that can be renamed or installed in another builder.
172
- * @throws If the selection is not a tuple or contains an absent name or token.
216
+ * @throws `DI_BAG_INVALID_EXPORT` if the selection is not a tuple, contains an absent name or token, or the label is not a non-empty string;
217
+ * `DI_BAG_INVALID_TOKEN` for a value that is not a genuine token.
218
+ * @example
219
+ * ```ts
220
+ * const orders = DiBag.createBuilder()
221
+ * .register({ repository: () => new Map<string, number>() })
222
+ * .register({ placeOrder: ({ repository }: { repository: Map<string, number> }) => (id: string) => repository.set(id, 1) })
223
+ * .buildModule(['placeOrder'], { label: 'orders' });
224
+ * // Errors and inspectGraph() name the private binding 'orders/repository'.
225
+ * const app = DiBag.createBuilder().installModule(orders).build();
226
+ * ```
173
227
  */
174
- buildModule(keys) {
175
- return (0, module_1.sealModule)(this.#graph, keys);
228
+ buildModule(keys, options) {
229
+ return (0, module_1.sealModule)(this.#graph, keys, options);
176
230
  }
177
231
  /**
178
232
  * Finish a complete graph as a lazy bag.
233
+ * The bag owns what it acquires; close it when done.
179
234
  * @returns A fresh bag that owns the acquisitions it creates.
180
- * @throws At runtime if automatic acquisition is used without a configured Promise classifier.
235
+ * @throws `DI_BAG_CLASSIFIER_REQUIRED` when a registration uses `auto` acquisition, the facade has no Promise
236
+ * classifier, and the host has no `process.getBuiltinModule`.
237
+ * @example
238
+ * ```ts
239
+ * const bag = DiBag.createBuilder().register({ greeting: () => 'hello' }).build();
240
+ * await bag.close();
241
+ * ```
181
242
  */
182
243
  build() {
183
244
  return new Bag(this.#graph, this.context);
@@ -187,8 +248,16 @@ class Builder {
187
248
  * @param keys - A finite tuple of existing names or typed tokens to make ready.
188
249
  * @param options - Optional cancellation signal, positive timeout, and parallel, sequential, or positive safe integer bounded scheduling.
189
250
  * @returns A promise for the new bag after every selected final stage is ready.
190
- * @throws {@link DiBagStartupError} after rollback on acquisition failure, or
191
- * {@link DiBagStartupCancelledError} promptly on abort or timeout.
251
+ * @throws {@link DiBagStartupError} (`DI_BAG_STARTUP_FAILED`) after rollback on acquisition failure;
252
+ * {@link DiBagStartupCancelledError} (`DI_BAG_STARTUP_CANCELLED`) promptly on abort or timeout;
253
+ * `DI_BAG_INVALID_STARTUP` for malformed keys or options; `DI_BAG_INVALID_TOKEN` for a bad token;
254
+ * `DI_BAG_CLASSIFIER_REQUIRED` as for {@link Builder.build}. Each arrives as a rejection.
255
+ * @example
256
+ * ```ts
257
+ * const bag = await DiBag.createBuilder()
258
+ * .register({ db: async () => ({ ping: () => true }) })
259
+ * .buildAndStart(['db'], { timeoutMs: 5_000 });
260
+ * ```
192
261
  */
193
262
  async buildAndStart(keys, options) {
194
263
  const runtime = await (0, startup_1.startRuntime)(this.#graph, this.context, keys, options);
@@ -215,5 +284,8 @@ function facade(context) {
215
284
  withDisposal: registration_1.withDisposal, withLifetime: lifetime_1.withLifetime, withMetadata: provider_1.withMetadata, transformService: provider_1.transformService,
216
285
  });
217
286
  }
218
- /** The portable, immutable DI Bag facade. Configure `auto` acquisition or use explicit modes. */
287
+ /**
288
+ * The immutable DI Bag facade. `auto` acquisition uses the host classifier where `process.getBuiltinModule`
289
+ * exists; elsewhere configure one or use explicit modes.
290
+ */
219
291
  exports.DiBag = facade(acquisition_mode_1.unconfigured);
package/dist/errors.d.ts CHANGED
@@ -1,22 +1,45 @@
1
- /** Stable category for a diagnostic created by DI Bag itself. */
1
+ /**
2
+ * Stable category for a diagnostic created by DI Bag itself.
3
+ * @see https://dany-fedorov.github.io/di-bag/agent/errors.html#runtime-codes
4
+ */
2
5
  export type DiBagErrorCode = `DI_BAG_${string}`;
3
- /** Structured library diagnostics. Application-owned payloads retain their identity. */
6
+ /**
7
+ * Structured library diagnostics. Application-owned payloads retain their identity.
8
+ * @see https://dany-fedorov.github.io/di-bag/agent/errors.html#runtime-codes
9
+ */
4
10
  export interface DiBagDiagnostic {
5
11
  readonly code: DiBagErrorCode;
6
12
  readonly details: Readonly<Record<string, unknown>>;
7
13
  }
8
- /** Attach only at library-owned error creation sites; never modify application errors. */
14
+ /** Prefix the code and append the errors-page section so a stack trace names both. */
15
+ export declare function diagnosticMessage(code: DiBagErrorCode, message: string): string;
16
+ /** Attach only at library-owned error creation sites whose message came from {@link diagnosticMessage}; never modify application errors. */
9
17
  export declare function diagnostic<E extends Error>(error: E, code: DiBagErrorCode, details?: Readonly<Record<string, unknown>>): E & DiBagDiagnostic;
10
18
  export declare function libraryError(code: DiBagErrorCode, message: string, details?: Readonly<Record<string, unknown>>): Error & DiBagDiagnostic;
11
19
  export declare function libraryTypeError(code: DiBagErrorCode, message: string, details?: Readonly<Record<string, unknown>>): TypeError & DiBagDiagnostic;
12
- /** One disposer failure, associated with the acquisition that owned it. */
20
+ /**
21
+ * One disposer failure, associated with the acquisition that owned it.
22
+ * @see https://dany-fedorov.github.io/di-bag/agent/errors.html#di-bag-cleanup-failed
23
+ */
13
24
  export interface CleanupFailure {
14
25
  readonly acquisitionId: symbol;
15
26
  readonly bindingId: symbol;
16
27
  readonly label: string;
17
28
  readonly error: unknown;
18
29
  }
19
- /** A plugin descriptor or produced value crossed the checked plugin boundary. */
30
+ /**
31
+ * A plugin descriptor or its acquired output failed validation at the checked plugin boundary.
32
+ * @example
33
+ * ```ts
34
+ * import { DiBag, DiBagPluginValidationError } from 'di-bag';
35
+ *
36
+ * try {
37
+ * DiBag.fromPlugin([], { apiVersion: 2 }, { acquisitionMode: 'raw', validate: (value): value is string => typeof value === 'string' });
38
+ * } catch (error) {
39
+ * if (error instanceof DiBagPluginValidationError) console.error(error.phase, error.reason);
40
+ * }
41
+ * ```
42
+ */
20
43
  export declare class DiBagPluginValidationError extends Error {
21
44
  readonly phase: 'descriptor' | 'output';
22
45
  readonly reason: string;
@@ -28,7 +51,19 @@ export declare class DiBagPluginValidationError extends Error {
28
51
  */
29
52
  constructor(phase: 'descriptor' | 'output', reason: string);
30
53
  }
31
- /** Original cleanup causes and detached acquisition diagnostics, in attempt order. */
54
+ /**
55
+ * One or more disposers failed during `close()`; every cleanup was still attempted.
56
+ * `failures` lists each original error with the label of the service it belonged to, in attempt order.
57
+ * @example
58
+ * ```ts
59
+ * import { DiBag, DiBagCleanupError } from 'di-bag';
60
+ *
61
+ * const bag = DiBag.createBuilder().register({ value: () => 1 }).build();
62
+ * await bag.close().catch((error: unknown) => {
63
+ * if (error instanceof DiBagCleanupError) for (const failure of error.failures) console.error(failure.label, failure.error);
64
+ * });
65
+ * ```
66
+ */
32
67
  export declare class DiBagCleanupError extends AggregateError {
33
68
  readonly code: 'DI_BAG_CLEANUP_FAILED';
34
69
  readonly details: Readonly<Record<string, unknown>>;
@@ -37,7 +72,21 @@ export declare class DiBagCleanupError extends AggregateError {
37
72
  /** @param failures - Structured failures whose original errors also populate `AggregateError.errors`. */
38
73
  constructor(failures: readonly CleanupFailure[]);
39
74
  }
40
- /** Acquisition failure after the new bag has finished releasing its resources. */
75
+ /**
76
+ * `buildAndStart` failed to acquire a selected service; the new bag has already released its resources.
77
+ * `cause` is the original failure and `cleanupFailures` lists disposers that failed during rollback.
78
+ * @example
79
+ * ```ts
80
+ * import { DiBag, DiBagStartupError } from 'di-bag';
81
+ *
82
+ * const builder = DiBag.createBuilder().register({ db: async (): Promise<number> => { throw new Error('offline'); } });
83
+ * try {
84
+ * await builder.buildAndStart(['db']);
85
+ * } catch (error) {
86
+ * if (error instanceof DiBagStartupError) console.error(error.cause, error.cleanupFailures);
87
+ * }
88
+ * ```
89
+ */
41
90
  export declare class DiBagStartupError extends Error {
42
91
  readonly cleanupError?: unknown | undefined;
43
92
  readonly code: 'DI_BAG_STARTUP_FAILED';
@@ -51,7 +100,21 @@ export declare class DiBagStartupError extends Error {
51
100
  */
52
101
  constructor(cause: unknown, cleanupFailures: readonly CleanupFailure[], cleanupError?: unknown | undefined);
53
102
  }
54
- /** Prompt cancellation; cleanup remains awaitable for uncooperative factories. */
103
+ /**
104
+ * `buildAndStart` stopped waiting on abort or timeout; `cleanupPromise` settles when the partial bag is released.
105
+ * On timeout, `cause` carries `DI_BAG_STARTUP_TIMEOUT`.
106
+ * @example
107
+ * ```ts
108
+ * import { DiBag, DiBagStartupCancelledError } from 'di-bag';
109
+ *
110
+ * const builder = DiBag.createBuilder().register({ db: () => new Promise<number>(() => {}) });
111
+ * try {
112
+ * await builder.buildAndStart(['db'], { timeoutMs: 1_000 });
113
+ * } catch (error) {
114
+ * if (error instanceof DiBagStartupCancelledError) await error.cleanupPromise;
115
+ * }
116
+ * ```
117
+ */
55
118
  export declare class DiBagStartupCancelledError extends Error {
56
119
  readonly reason: 'aborted' | 'timeout';
57
120
  readonly cleanupPromise: Promise<void>;
@@ -64,3 +127,47 @@ export declare class DiBagStartupCancelledError extends Error {
64
127
  */
65
128
  constructor(reason: 'aborted' | 'timeout', cause: unknown, cleanupPromise: Promise<void>);
66
129
  }
130
+ /**
131
+ * What a close deadline or abort interrupted: labels still in progress when the wait stopped.
132
+ * @see https://dany-fedorov.github.io/di-bag/agent/errors.html#di-bag-close-timeout
133
+ */
134
+ export interface CloseProgress {
135
+ /** Labels of disposers that started and had not completed. */
136
+ readonly pending: readonly string[];
137
+ /** Labels of acquisitions close was still draining before running disposers. */
138
+ readonly acquiring: readonly string[];
139
+ }
140
+ /**
141
+ * A `close({ timeoutMs, signal })` wait stopped before cleanup finished; cleanup keeps running.
142
+ * `code` is `DI_BAG_CLOSE_TIMEOUT` for the deadline and `DI_BAG_CLOSE_ABORTED` for the signal.
143
+ * @example
144
+ * ```ts
145
+ * import { DiBag, DiBagCloseCancelledError } from 'di-bag';
146
+ *
147
+ * const bag = DiBag.createBuilder().register({ value: () => 1 }).build();
148
+ * try {
149
+ * await bag.close({ timeoutMs: 5_000 });
150
+ * } catch (error) {
151
+ * if (error instanceof DiBagCloseCancelledError) console.error(error.details.pending);
152
+ * throw error;
153
+ * }
154
+ * ```
155
+ */
156
+ export declare class DiBagCloseCancelledError extends Error {
157
+ readonly reason: 'aborted' | 'timeout';
158
+ readonly cleanupPromise: Promise<void>;
159
+ readonly code: 'DI_BAG_CLOSE_TIMEOUT' | 'DI_BAG_CLOSE_ABORTED';
160
+ readonly details: Readonly<{
161
+ operation: 'close';
162
+ reason: 'aborted' | 'timeout';
163
+ timeoutMs?: number;
164
+ } & CloseProgress>;
165
+ /**
166
+ * @param reason - Whether an external abort or the close deadline stopped the wait.
167
+ * @param cause - The abort reason, or a `TimeoutError` DOMException for the deadline.
168
+ * @param cleanupPromise - The bag's shared shutdown promise; it settles when cleanup eventually finishes.
169
+ * @param progress - Labels still in progress when the wait stopped.
170
+ * @param timeoutMs - The deadline that elapsed, for `reason: 'timeout'`.
171
+ */
172
+ constructor(reason: 'aborted' | 'timeout', cause: unknown, cleanupPromise: Promise<void>, progress: CloseProgress, timeoutMs?: number);
173
+ }
package/dist/errors.js CHANGED
@@ -1,10 +1,17 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.DiBagStartupCancelledError = exports.DiBagStartupError = exports.DiBagCleanupError = exports.DiBagPluginValidationError = void 0;
3
+ exports.DiBagCloseCancelledError = exports.DiBagStartupCancelledError = exports.DiBagStartupError = exports.DiBagCleanupError = exports.DiBagPluginValidationError = void 0;
4
+ exports.diagnosticMessage = diagnosticMessage;
4
5
  exports.diagnostic = diagnostic;
5
6
  exports.libraryError = libraryError;
6
7
  exports.libraryTypeError = libraryTypeError;
7
- /** Attach only at library-owned error creation sites; never modify application errors. */
8
+ // The one place the errors page URL lives; fragments follow the site's heading slugs.
9
+ const errorsPage = 'https://dany-fedorov.github.io/di-bag/agent/errors.html';
10
+ /** Prefix the code and append the errors-page section so a stack trace names both. */
11
+ function diagnosticMessage(code, message) {
12
+ return `${code}: ${message}; see ${errorsPage}#${code.toLowerCase().replaceAll('_', '-')}`;
13
+ }
14
+ /** Attach only at library-owned error creation sites whose message came from {@link diagnosticMessage}; never modify application errors. */
8
15
  function diagnostic(error, code, details = {}) {
9
16
  Object.defineProperties(error, {
10
17
  code: { value: code, enumerable: true },
@@ -13,12 +20,24 @@ function diagnostic(error, code, details = {}) {
13
20
  return error;
14
21
  }
15
22
  function libraryError(code, message, details = {}) {
16
- return diagnostic(new Error(message), code, details);
23
+ return diagnostic(new Error(diagnosticMessage(code, message)), code, details);
17
24
  }
18
25
  function libraryTypeError(code, message, details = {}) {
19
- return diagnostic(new TypeError(message), code, details);
26
+ return diagnostic(new TypeError(diagnosticMessage(code, message)), code, details);
20
27
  }
21
- /** A plugin descriptor or produced value crossed the checked plugin boundary. */
28
+ /**
29
+ * A plugin descriptor or its acquired output failed validation at the checked plugin boundary.
30
+ * @example
31
+ * ```ts
32
+ * import { DiBag, DiBagPluginValidationError } from 'di-bag';
33
+ *
34
+ * try {
35
+ * DiBag.fromPlugin([], { apiVersion: 2 }, { acquisitionMode: 'raw', validate: (value): value is string => typeof value === 'string' });
36
+ * } catch (error) {
37
+ * if (error instanceof DiBagPluginValidationError) console.error(error.phase, error.reason);
38
+ * }
39
+ * ```
40
+ */
22
41
  class DiBagPluginValidationError extends Error {
23
42
  phase;
24
43
  reason;
@@ -27,7 +46,7 @@ class DiBagPluginValidationError extends Error {
27
46
  * @param reason - A stable description of the rejected boundary condition.
28
47
  */
29
48
  constructor(phase, reason) {
30
- super(`Invalid plugin ${phase}: ${reason}`);
49
+ super(diagnosticMessage('DI_BAG_PLUGIN_VALIDATION', `Invalid plugin ${phase}: ${reason}`));
31
50
  this.phase = phase;
32
51
  this.reason = reason;
33
52
  this.name = 'DiBagPluginValidationError';
@@ -35,21 +54,47 @@ class DiBagPluginValidationError extends Error {
35
54
  }
36
55
  }
37
56
  exports.DiBagPluginValidationError = DiBagPluginValidationError;
38
- /** Original cleanup causes and detached acquisition diagnostics, in attempt order. */
57
+ /**
58
+ * One or more disposers failed during `close()`; every cleanup was still attempted.
59
+ * `failures` lists each original error with the label of the service it belonged to, in attempt order.
60
+ * @example
61
+ * ```ts
62
+ * import { DiBag, DiBagCleanupError } from 'di-bag';
63
+ *
64
+ * const bag = DiBag.createBuilder().register({ value: () => 1 }).build();
65
+ * await bag.close().catch((error: unknown) => {
66
+ * if (error instanceof DiBagCleanupError) for (const failure of error.failures) console.error(failure.label, failure.error);
67
+ * });
68
+ * ```
69
+ */
39
70
  class DiBagCleanupError extends AggregateError {
40
71
  /** Frozen cleanup failures in finalizer invocation order. */
41
72
  failures;
42
73
  /** @param failures - Structured failures whose original errors also populate `AggregateError.errors`. */
43
74
  constructor(failures) {
44
75
  const snapshot = Object.freeze(failures.map(item => Object.freeze({ ...item })));
45
- super(snapshot.map(item => item.error), `Failed to run ${snapshot.length} disposal callback(s)`);
76
+ super(snapshot.map(item => item.error), diagnosticMessage('DI_BAG_CLEANUP_FAILED', `Failed to run ${snapshot.length} disposal callback(s)`));
46
77
  this.name = 'DiBagCleanupError';
47
78
  diagnostic(this, 'DI_BAG_CLEANUP_FAILED', { operation: 'close', failedCallbacks: snapshot.length, failures: snapshot });
48
79
  this.failures = snapshot;
49
80
  }
50
81
  }
51
82
  exports.DiBagCleanupError = DiBagCleanupError;
52
- /** Acquisition failure after the new bag has finished releasing its resources. */
83
+ /**
84
+ * `buildAndStart` failed to acquire a selected service; the new bag has already released its resources.
85
+ * `cause` is the original failure and `cleanupFailures` lists disposers that failed during rollback.
86
+ * @example
87
+ * ```ts
88
+ * import { DiBag, DiBagStartupError } from 'di-bag';
89
+ *
90
+ * const builder = DiBag.createBuilder().register({ db: async (): Promise<number> => { throw new Error('offline'); } });
91
+ * try {
92
+ * await builder.buildAndStart(['db']);
93
+ * } catch (error) {
94
+ * if (error instanceof DiBagStartupError) console.error(error.cause, error.cleanupFailures);
95
+ * }
96
+ * ```
97
+ */
53
98
  class DiBagStartupError extends Error {
54
99
  cleanupError;
55
100
  /** Frozen rollback disposal failures in invocation order. */
@@ -60,7 +105,7 @@ class DiBagStartupError extends Error {
60
105
  * @param cleanupError - The complete shutdown error, when rollback itself rejected.
61
106
  */
62
107
  constructor(cause, cleanupFailures, cleanupError) {
63
- super('Failed to start bag', { cause });
108
+ super(diagnosticMessage('DI_BAG_STARTUP_FAILED', 'Failed to start bag'), { cause });
64
109
  this.cleanupError = cleanupError;
65
110
  this.name = 'DiBagStartupError';
66
111
  this.cleanupFailures = Object.freeze(cleanupFailures.map(item => Object.freeze({ ...item })));
@@ -68,7 +113,21 @@ class DiBagStartupError extends Error {
68
113
  }
69
114
  }
70
115
  exports.DiBagStartupError = DiBagStartupError;
71
- /** Prompt cancellation; cleanup remains awaitable for uncooperative factories. */
116
+ /**
117
+ * `buildAndStart` stopped waiting on abort or timeout; `cleanupPromise` settles when the partial bag is released.
118
+ * On timeout, `cause` carries `DI_BAG_STARTUP_TIMEOUT`.
119
+ * @example
120
+ * ```ts
121
+ * import { DiBag, DiBagStartupCancelledError } from 'di-bag';
122
+ *
123
+ * const builder = DiBag.createBuilder().register({ db: () => new Promise<number>(() => {}) });
124
+ * try {
125
+ * await builder.buildAndStart(['db'], { timeoutMs: 1_000 });
126
+ * } catch (error) {
127
+ * if (error instanceof DiBagStartupCancelledError) await error.cleanupPromise;
128
+ * }
129
+ * ```
130
+ */
72
131
  class DiBagStartupCancelledError extends Error {
73
132
  reason;
74
133
  cleanupPromise;
@@ -78,7 +137,7 @@ class DiBagStartupCancelledError extends Error {
78
137
  * @param cleanupPromise - Eventual shutdown of the partially started bag; cancellation does not await it.
79
138
  */
80
139
  constructor(reason, cause, cleanupPromise) {
81
- super(`Bag startup ${reason}`, { cause });
140
+ super(diagnosticMessage('DI_BAG_STARTUP_CANCELLED', `Bag startup ${reason}`), { cause });
82
141
  this.reason = reason;
83
142
  this.cleanupPromise = cleanupPromise;
84
143
  this.name = 'DiBagStartupCancelledError';
@@ -87,3 +146,45 @@ class DiBagStartupCancelledError extends Error {
87
146
  }
88
147
  }
89
148
  exports.DiBagStartupCancelledError = DiBagStartupCancelledError;
149
+ /**
150
+ * A `close({ timeoutMs, signal })` wait stopped before cleanup finished; cleanup keeps running.
151
+ * `code` is `DI_BAG_CLOSE_TIMEOUT` for the deadline and `DI_BAG_CLOSE_ABORTED` for the signal.
152
+ * @example
153
+ * ```ts
154
+ * import { DiBag, DiBagCloseCancelledError } from 'di-bag';
155
+ *
156
+ * const bag = DiBag.createBuilder().register({ value: () => 1 }).build();
157
+ * try {
158
+ * await bag.close({ timeoutMs: 5_000 });
159
+ * } catch (error) {
160
+ * if (error instanceof DiBagCloseCancelledError) console.error(error.details.pending);
161
+ * throw error;
162
+ * }
163
+ * ```
164
+ */
165
+ class DiBagCloseCancelledError extends Error {
166
+ reason;
167
+ cleanupPromise;
168
+ /**
169
+ * @param reason - Whether an external abort or the close deadline stopped the wait.
170
+ * @param cause - The abort reason, or a `TimeoutError` DOMException for the deadline.
171
+ * @param cleanupPromise - The bag's shared shutdown promise; it settles when cleanup eventually finishes.
172
+ * @param progress - Labels still in progress when the wait stopped.
173
+ * @param timeoutMs - The deadline that elapsed, for `reason: 'timeout'`.
174
+ */
175
+ constructor(reason, cause, cleanupPromise, progress, timeoutMs) {
176
+ const code = reason === 'timeout' ? 'DI_BAG_CLOSE_TIMEOUT' : 'DI_BAG_CLOSE_ABORTED';
177
+ const waiting = progress.pending.length ? `; disposers still running: ${progress.pending.join(', ')}`
178
+ : progress.acquiring.length ? `; acquisitions still pending: ${progress.acquiring.join(', ')}` : '';
179
+ super(diagnosticMessage(code, `Bag close ${reason === 'timeout' ? `timed out after ${timeoutMs}ms` : 'aborted'}${waiting}`), { cause });
180
+ this.reason = reason;
181
+ this.cleanupPromise = cleanupPromise;
182
+ this.name = 'DiBagCloseCancelledError';
183
+ diagnostic(this, code, {
184
+ operation: 'close', reason, ...(timeoutMs === undefined ? {} : { timeoutMs }),
185
+ pending: Object.freeze([...progress.pending]), acquiring: Object.freeze([...progress.acquiring]),
186
+ });
187
+ void cleanupPromise.catch(() => { });
188
+ }
189
+ }
190
+ exports.DiBagCloseCancelledError = DiBagCloseCancelledError;
package/dist/index.d.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  export { DiBag } from './di-bag';
2
- export { DiBagCleanupError, DiBagPluginValidationError, DiBagStartupError, DiBagStartupCancelledError } from './errors';
3
- export type { CleanupFailure, DiBagErrorCode, DiBagDiagnostic } from './errors';
2
+ export { DiBagCleanupError, DiBagCloseCancelledError, DiBagPluginValidationError, DiBagStartupError, DiBagStartupCancelledError } from './errors';
3
+ export type { CleanupFailure, CloseProgress, DiBagErrorCode, DiBagDiagnostic } from './errors';
4
4
  export type { Bag, Builder, DiBagApi, ConfigurationOptions } from './di-bag';
5
- export type { Module } from './module';
5
+ export type { Module, ModuleOptions } from './module';
6
6
  export type { CompositionReport } from './composition-report';
7
7
  export type { ModuleExportedServices, ModuleRequiredServices, ModuleConstraints, ModuleSealedConstraints, SealedConstraints, PublicProviders, ModulePublicProviders, Renamed } from './module-types';
8
8
  export type { FactoryWithDisposal, Registration } from './registration';
@@ -10,11 +10,11 @@ export type { Provider, ProviderFactory, ProviderGraphContract, ProviderOutput,
10
10
  export type { AcquisitionMode, RuntimeOptions } from './acquisition-mode';
11
11
  export type { Lifetime } from './lifetime';
12
12
  export type { AcquisitionContext, ContextualFactory } from './acquisition-context';
13
- export type { StartupOptions } from './startup';
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';
16
16
  export type { TokenBinding, TokenMember, TokenDependencyContract, ReboundProviders, ReboundSelection, SelectionKey } from './token-types';
17
- export type { CheckedLifetimes, CheckedScopeLifetimes, LexicalContext, ModuleScope, Enclosed, RenamedContext, RenamedLifetimeObligation, EnclosedLifetimeObligation, RenamedLifetimeProviders } from './lifetime-types';
17
+ export type { CheckedLifetimes, CheckedScopeLifetimes, LifetimeObligation, Reach } from './lifetime-types';
18
18
  export type { DiBagPolicy } from './types';
19
19
  export type { CheckDependencyCompatibility, CheckDependencyCompleteness, RegistrationEntries, OverrideFactoryContext, RegistrationsFromEntries, OverrideRegistrations, Overrides, ServicesOf, SelectedRegistrations, Selection } from './types';
20
20
  export type { Presence, AcquisitionMetadataPresence, AcquisitionSnapshot, RegistrationSnapshot, BindingSnapshot, GraphSnapshot } from './inspection';
package/dist/index.js CHANGED
@@ -1,10 +1,11 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.DiBagStartupCancelledError = exports.DiBagStartupError = exports.DiBagPluginValidationError = exports.DiBagCleanupError = exports.DiBag = void 0;
3
+ exports.DiBagStartupCancelledError = exports.DiBagStartupError = exports.DiBagPluginValidationError = exports.DiBagCloseCancelledError = exports.DiBagCleanupError = exports.DiBag = void 0;
4
4
  var di_bag_1 = require("./di-bag");
5
5
  Object.defineProperty(exports, "DiBag", { enumerable: true, get: function () { return di_bag_1.DiBag; } });
6
6
  var errors_1 = require("./errors");
7
7
  Object.defineProperty(exports, "DiBagCleanupError", { enumerable: true, get: function () { return errors_1.DiBagCleanupError; } });
8
+ Object.defineProperty(exports, "DiBagCloseCancelledError", { enumerable: true, get: function () { return errors_1.DiBagCloseCancelledError; } });
8
9
  Object.defineProperty(exports, "DiBagPluginValidationError", { enumerable: true, get: function () { return errors_1.DiBagPluginValidationError; } });
9
10
  Object.defineProperty(exports, "DiBagStartupError", { enumerable: true, get: function () { return errors_1.DiBagStartupError; } });
10
11
  Object.defineProperty(exports, "DiBagStartupCancelledError", { enumerable: true, get: function () { return errors_1.DiBagStartupCancelledError; } });
@@ -1,17 +1,26 @@
1
1
  import type { AcquisitionMode } from './acquisition-mode';
2
2
  import type { Lifetime } from './lifetime';
3
- /** Structural optional presence; payloads are application-owned and not frozen. */
3
+ /**
4
+ * Structural optional presence; payloads are application-owned and not frozen.
5
+ * @see https://dany-fedorov.github.io/di-bag/guides/tutorial.html#attach-metadata-and-inspect-without-resolving
6
+ */
4
7
  export type Presence<T> = {
5
8
  readonly present: false;
6
9
  } | {
7
10
  readonly present: true;
8
11
  readonly value: T;
9
12
  };
10
- /** A readonly tuple indicating whether each acquisition-stage frame is available. */
13
+ /**
14
+ * A readonly tuple indicating whether each acquisition-stage frame is available.
15
+ * @see https://dany-fedorov.github.io/di-bag/guides/tutorial.html#attach-metadata-and-inspect-without-resolving
16
+ */
11
17
  export type AcquisitionMetadataPresence<A extends readonly unknown[]> = {
12
18
  readonly [I in keyof A]: Presence<A[I]>;
13
19
  };
14
- /** A frozen point-in-time view of one acquisition attempt. */
20
+ /**
21
+ * A frozen point-in-time view of one acquisition attempt.
22
+ * @see https://dany-fedorov.github.io/di-bag/guides/tutorial.html#attach-metadata-and-inspect-without-resolving
23
+ */
15
24
  export interface AcquisitionSnapshot<A extends readonly unknown[] = readonly []> {
16
25
  /** Stable identity for this attempt; retries receive a new symbol. */
17
26
  readonly acquisitionId: symbol;
@@ -20,7 +29,10 @@ export interface AcquisitionSnapshot<A extends readonly unknown[] = readonly []>
20
29
  /** Ordered presence records for metadata captured during acquisition. */
21
30
  readonly acquisitionMetadata: AcquisitionMetadataPresence<A>;
22
31
  }
23
- /** A frozen registration description and copied acquisition state returned by bag inspection. */
32
+ /**
33
+ * A frozen registration description and copied acquisition state returned by bag inspection.
34
+ * @see https://dany-fedorov.github.io/di-bag/guides/tutorial.html#attach-metadata-and-inspect-without-resolving
35
+ */
24
36
  export interface RegistrationSnapshot<M = Readonly<{}>, A extends readonly unknown[] = readonly []> {
25
37
  /** Stable identity for the canonical graph binding. */
26
38
  readonly bindingId: symbol;
@@ -36,7 +48,10 @@ export interface RegistrationSnapshot<M = Readonly<{}>, A extends readonly unkno
36
48
  /** Point-in-time attempts; inspection does not retain failed-attempt history. */
37
49
  readonly acquisitions: readonly AcquisitionSnapshot<A>[];
38
50
  }
39
- /** One binding of a bag's graph, described without acquiring it. */
51
+ /**
52
+ * One binding of a bag's graph, described without acquiring it.
53
+ * @see https://dany-fedorov.github.io/di-bag/guides/tutorial.html#attach-metadata-and-inspect-without-resolving
54
+ */
40
55
  export interface BindingSnapshot<M = Readonly<{}>, A extends readonly unknown[] = readonly []> extends RegistrationSnapshot<M, A> {
41
56
  /** Public names or token symbols that select this binding, in registration order; empty for a private module binding. */
42
57
  readonly keys: readonly (string | symbol)[];
@@ -54,6 +69,7 @@ export interface BindingSnapshot<M = Readonly<{}>, A extends readonly unknown[]
54
69
  * A frozen description of every binding a bag can resolve, plus the edges observed so far.
55
70
  * Named dependencies read from a factory's object parameter are not knowable until the factory
56
71
  * runs; `observedEdges` records them after acquisition. Use the static graph tool for declared edges.
72
+ * @see https://dany-fedorov.github.io/di-bag/guides/tutorial.html#attach-metadata-and-inspect-without-resolving
57
73
  */
58
74
  export interface GraphSnapshot {
59
75
  readonly scopeId: symbol;