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
|
@@ -1,7 +1,35 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.ProviderExecution = exports.CompletedExecution = void 0;
|
|
3
|
+
exports.ProviderExecution = exports.CompletedExecution = exports.DisposerStack = void 0;
|
|
4
4
|
const errors_1 = require("./errors");
|
|
5
|
+
/**
|
|
6
|
+
* One factory's pushed disposers. It is held by the frozen acquisition context
|
|
7
|
+
* handed to that factory, so it deliberately references neither the execution
|
|
8
|
+
* nor its scope: a context the application retains must keep nothing but its
|
|
9
|
+
* own registrations alive.
|
|
10
|
+
*/
|
|
11
|
+
class DisposerStack {
|
|
12
|
+
disposers = [];
|
|
13
|
+
settled = false;
|
|
14
|
+
/** Own a resource the running factory already holds. */
|
|
15
|
+
push(disposer) {
|
|
16
|
+
if (typeof disposer !== 'function')
|
|
17
|
+
throw (0, errors_1.libraryError)('DI_BAG_INVALID_CLEANUP', 'pushDisposer requires a function', { operation: 'pushDisposer', provided: typeof disposer });
|
|
18
|
+
// A retained context is a leak, not a stack: registration closes with the factory.
|
|
19
|
+
if (this.settled)
|
|
20
|
+
throw (0, errors_1.libraryError)('DI_BAG_CLEANUP_AFTER_FACTORY', 'pushDisposer is only available while its factory is running', { operation: 'pushDisposer' });
|
|
21
|
+
this.disposers.push(disposer);
|
|
22
|
+
}
|
|
23
|
+
/** The factory settled; nothing more can be pushed. */
|
|
24
|
+
settle() { this.settled = true; }
|
|
25
|
+
get pending() { return this.disposers.length > 0; }
|
|
26
|
+
/** Take every pushed disposer, last pushed first, and close registration for good. */
|
|
27
|
+
drain() {
|
|
28
|
+
this.settled = true;
|
|
29
|
+
return this.disposers.splice(0).reverse();
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
exports.DisposerStack = DisposerStack;
|
|
5
33
|
const observePromise = (Promise.prototype.then);
|
|
6
34
|
const emptyFrames = Object.freeze([]);
|
|
7
35
|
const emptyWork = Object.freeze([]);
|
|
@@ -11,6 +39,8 @@ class CompletedExecution {
|
|
|
11
39
|
state = 'ready';
|
|
12
40
|
sourceInFlight = false;
|
|
13
41
|
hasOwnership = false;
|
|
42
|
+
rollingBack = false;
|
|
43
|
+
disposers = undefined;
|
|
14
44
|
error = undefined;
|
|
15
45
|
work = emptyWork;
|
|
16
46
|
constructor(frames) {
|
|
@@ -34,6 +64,12 @@ class ProviderExecution {
|
|
|
34
64
|
context;
|
|
35
65
|
frames;
|
|
36
66
|
stages = [];
|
|
67
|
+
/** Allocated only for a context-aware factory, which is the only source that can push disposers. */
|
|
68
|
+
disposers;
|
|
69
|
+
// The factory returned, so the bag owns what it pushed, below every accepted stage.
|
|
70
|
+
disposersOwned = false;
|
|
71
|
+
// The failure-path run of the stack, while it is in flight.
|
|
72
|
+
rollback;
|
|
37
73
|
pending = new Set();
|
|
38
74
|
result;
|
|
39
75
|
cleaning;
|
|
@@ -45,12 +81,68 @@ class ProviderExecution {
|
|
|
45
81
|
// Reserve every metadata frame before the source can reenter inspection.
|
|
46
82
|
this.frames = description.operations.filter(operation => operation.kind === 'frame-sync' || operation.kind === 'frame-async')
|
|
47
83
|
.map(() => Object.freeze({ present: false }));
|
|
84
|
+
if (description.contextual)
|
|
85
|
+
this.disposers = new DisposerStack();
|
|
48
86
|
}
|
|
49
87
|
inspectFrames() { return Object.freeze([...this.frames]); }
|
|
50
88
|
get state() { return this.result?.state ?? 'failed'; }
|
|
51
89
|
get error() { return this.result?.error; }
|
|
52
|
-
get hasOwnership() { return this.stages.length > 0; }
|
|
90
|
+
get hasOwnership() { return this.stages.length > 0 || this.disposersOwned; }
|
|
91
|
+
get rollingBack() { return this.rollback !== undefined; }
|
|
53
92
|
get work() { return [...this.pending]; }
|
|
93
|
+
/**
|
|
94
|
+
* The source stage settles the stack. A factory that returned hands what it
|
|
95
|
+
* pushed to the bag; a failed one releases it at once, as pending work of this
|
|
96
|
+
* execution. Anchoring on the source rather than on the attempt's result covers
|
|
97
|
+
* a direct projection that is already ready while its source is still running,
|
|
98
|
+
* which no retirement reaches.
|
|
99
|
+
*/
|
|
100
|
+
settleDisposers(state) {
|
|
101
|
+
if (!this.disposers)
|
|
102
|
+
return;
|
|
103
|
+
this.disposers.settle();
|
|
104
|
+
if (!this.disposers.pending)
|
|
105
|
+
return;
|
|
106
|
+
if (state === 'ready') {
|
|
107
|
+
this.disposersOwned = true;
|
|
108
|
+
this.events.accepted();
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const work = this.rollbackDisposers().then(() => {
|
|
112
|
+
this.rollback = undefined;
|
|
113
|
+
this.pending.delete(work);
|
|
114
|
+
if (!this.pending.size)
|
|
115
|
+
this.events.drained();
|
|
116
|
+
});
|
|
117
|
+
this.rollback = work;
|
|
118
|
+
this.pending.add(work);
|
|
119
|
+
}
|
|
120
|
+
async rollbackDisposers() {
|
|
121
|
+
// One microtask after the source settles: a synchronous failure never runs
|
|
122
|
+
// cleanup inline, and an unprojected attempt has already reported
|
|
123
|
+
// acquisition-failed. Under a projection the result settles later, so this
|
|
124
|
+
// run can precede both acquisition-failed and the consumer's rejection.
|
|
125
|
+
await undefined;
|
|
126
|
+
this.events.cleanupStarted?.();
|
|
127
|
+
const failed = await this.runDisposers('factory-failed');
|
|
128
|
+
this.events.cleanupCompleted?.(failed ? 'failure' : 'success');
|
|
129
|
+
}
|
|
130
|
+
/** Run every pushed disposer, last pushed first, all attempted; true when one threw. */
|
|
131
|
+
async runDisposers(reason) {
|
|
132
|
+
const disposerCtx = Object.freeze({ reason });
|
|
133
|
+
let failed = false;
|
|
134
|
+
for (const disposer of this.disposers?.drain() ?? []) {
|
|
135
|
+
const sequence = this.events.invoking();
|
|
136
|
+
try {
|
|
137
|
+
await disposer(disposerCtx);
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
failed = true;
|
|
141
|
+
this.events.cleanupFailed(sequence, error);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return failed;
|
|
145
|
+
}
|
|
54
146
|
/** Observe the selected stage, retaining its failure even after retirement. */
|
|
55
147
|
async ready() {
|
|
56
148
|
const result = this.result;
|
|
@@ -64,7 +156,13 @@ class ProviderExecution {
|
|
|
64
156
|
compact() {
|
|
65
157
|
if (this.state !== 'ready' || this.pending.size || this.hasOwnership)
|
|
66
158
|
return this;
|
|
67
|
-
|
|
159
|
+
if (!this.frames.length)
|
|
160
|
+
return completedWithoutFrames;
|
|
161
|
+
// The copy owns the frames from here; a retained reference to this record
|
|
162
|
+
// must not keep the application's frame payloads alive.
|
|
163
|
+
const completed = new CompletedExecution(this.inspectFrames());
|
|
164
|
+
this.frames.length = 0;
|
|
165
|
+
return completed;
|
|
68
166
|
}
|
|
69
167
|
/** Classify/own only after the direct operation-free source call has returned. */
|
|
70
168
|
publishSource(value, description) {
|
|
@@ -72,32 +170,39 @@ class ProviderExecution {
|
|
|
72
170
|
this.sourceInFlight = false;
|
|
73
171
|
this.result = { exposed: undefined, consumed: true, state: 'ready', value: undefined, error: undefined, owners: [] };
|
|
74
172
|
if (description.dispose)
|
|
75
|
-
this.accept(0, value, description.dispose);
|
|
173
|
+
this.accept(0, value, description.dispose, true);
|
|
76
174
|
return;
|
|
77
175
|
}
|
|
78
176
|
const stage = this.capture(() => value, true, description.acquisitionMode);
|
|
79
177
|
if (description.dispose)
|
|
80
|
-
this.own(stage, 0, description.dispose);
|
|
178
|
+
this.own(stage, 0, description.dispose, true);
|
|
81
179
|
this.result = stage;
|
|
82
180
|
this.consume(stage);
|
|
83
181
|
if (stage.state === 'failed')
|
|
84
182
|
throw stage.error;
|
|
85
183
|
}
|
|
86
|
-
evaluate(description, deps,
|
|
184
|
+
evaluate(description, deps, context) {
|
|
87
185
|
const { create, dispose } = description;
|
|
88
186
|
let current = this.capture(() => description.contextual
|
|
89
|
-
? Reflect.apply(create, undefined, [deps,
|
|
187
|
+
? Reflect.apply(create, undefined, [deps, context])
|
|
90
188
|
: create(deps), true, description.acquisitionMode);
|
|
189
|
+
// A pending source settles its own rollback list from the promise handler.
|
|
190
|
+
if (current.state !== 'pending')
|
|
191
|
+
this.settleDisposers(current.state);
|
|
91
192
|
let nextFrame = 0;
|
|
193
|
+
// Ownership attached before the first transform owns the value the factory
|
|
194
|
+
// returned; metadata frames preserve the value, so they do not end it.
|
|
195
|
+
let returned = true;
|
|
92
196
|
if (dispose)
|
|
93
|
-
this.own(current, 0, dispose);
|
|
197
|
+
this.own(current, 0, dispose, true);
|
|
94
198
|
description.operations.forEach((operation, offset) => {
|
|
95
199
|
const inputStage = current;
|
|
96
200
|
const index = offset + 1;
|
|
97
201
|
if (operation.kind === 'owned') {
|
|
98
|
-
this.own(current, index, operation.dispose);
|
|
202
|
+
this.own(current, index, operation.dispose, returned);
|
|
99
203
|
}
|
|
100
204
|
else if (operation.kind === 'map-sync') {
|
|
205
|
+
returned = false;
|
|
101
206
|
if (current.state !== 'failed') {
|
|
102
207
|
const input = current.exposed;
|
|
103
208
|
const { project } = operation;
|
|
@@ -105,6 +210,7 @@ class ProviderExecution {
|
|
|
105
210
|
}
|
|
106
211
|
}
|
|
107
212
|
else if (operation.kind === 'map-async') {
|
|
213
|
+
returned = false;
|
|
108
214
|
const input = current;
|
|
109
215
|
const { project } = operation;
|
|
110
216
|
current = this.capture(async () => {
|
|
@@ -179,7 +285,7 @@ class ProviderExecution {
|
|
|
179
285
|
if (!stage.consumed)
|
|
180
286
|
stage.value = value;
|
|
181
287
|
for (const owner of stage.owners)
|
|
182
|
-
this.accept(owner.index, value, owner.dispose);
|
|
288
|
+
this.accept(owner.index, value, owner.dispose, owner.returned);
|
|
183
289
|
stage.owners.length = 0;
|
|
184
290
|
finish();
|
|
185
291
|
}, error => {
|
|
@@ -193,8 +299,10 @@ class ProviderExecution {
|
|
|
193
299
|
stage.settled = barrier;
|
|
194
300
|
this.pending.add(barrier);
|
|
195
301
|
const finish = () => {
|
|
196
|
-
if (source)
|
|
302
|
+
if (source) {
|
|
197
303
|
this.sourceInFlight = false;
|
|
304
|
+
this.settleDisposers(stage.state === 'ready' ? 'ready' : 'failed');
|
|
305
|
+
}
|
|
198
306
|
this.pending.delete(barrier);
|
|
199
307
|
settled();
|
|
200
308
|
if (this.result === stage)
|
|
@@ -214,14 +322,14 @@ class ProviderExecution {
|
|
|
214
322
|
return { exposed: undefined, consumed: true, state: 'failed', value: undefined, error, owners: [] };
|
|
215
323
|
}
|
|
216
324
|
}
|
|
217
|
-
own(stage, index, dispose) {
|
|
325
|
+
own(stage, index, dispose, returned) {
|
|
218
326
|
if (stage.state === 'ready')
|
|
219
|
-
this.accept(index, stage.value, dispose);
|
|
327
|
+
this.accept(index, stage.value, dispose, returned);
|
|
220
328
|
else if (stage.state === 'pending')
|
|
221
|
-
stage.owners.push({ index, dispose });
|
|
329
|
+
stage.owners.push({ index, dispose, returned });
|
|
222
330
|
}
|
|
223
|
-
accept(index, value, dispose) {
|
|
224
|
-
this.stages.push({ index, value, dispose, state: 'accepted' });
|
|
331
|
+
accept(index, value, dispose, returned) {
|
|
332
|
+
this.stages.push({ index, value, dispose, returned, state: 'accepted' });
|
|
225
333
|
this.events.accepted();
|
|
226
334
|
}
|
|
227
335
|
dispose() {
|
|
@@ -237,12 +345,16 @@ class ProviderExecution {
|
|
|
237
345
|
// All later acceptances must be known before reversing stable stage indices.
|
|
238
346
|
while (this.pending.size)
|
|
239
347
|
await Promise.all(this.pending);
|
|
240
|
-
const owned = this.
|
|
348
|
+
const owned = this.hasOwnership;
|
|
241
349
|
let failed = false;
|
|
350
|
+
let returnedOwned = false;
|
|
351
|
+
let returnedFailed = false;
|
|
242
352
|
if (owned)
|
|
243
353
|
this.events.cleanupStarted?.();
|
|
244
354
|
for (const stage of this.stages.sort((a, b) => b.index - a.index)) {
|
|
245
355
|
stage.state = 'disposing';
|
|
356
|
+
if (stage.returned)
|
|
357
|
+
returnedOwned = true;
|
|
246
358
|
const sequence = this.events.invoking();
|
|
247
359
|
const { dispose, value } = stage;
|
|
248
360
|
try {
|
|
@@ -250,20 +362,36 @@ class ProviderExecution {
|
|
|
250
362
|
}
|
|
251
363
|
catch (error) {
|
|
252
364
|
failed = true;
|
|
365
|
+
if (stage.returned)
|
|
366
|
+
returnedFailed = true;
|
|
253
367
|
this.events.cleanupFailed(sequence, error);
|
|
254
368
|
}
|
|
255
369
|
finally {
|
|
256
370
|
stage.state = 'disposed';
|
|
257
371
|
}
|
|
258
372
|
}
|
|
373
|
+
// Pushed disposers are the bottom of the ownership stack: they run after every
|
|
374
|
+
// accepted stage and are told how the returned value's own disposer went. A
|
|
375
|
+
// projection owner belongs to whoever transformed the value; its outcome is
|
|
376
|
+
// reported through cleanup-failed, not through the reason.
|
|
377
|
+
if (this.disposersOwned) {
|
|
378
|
+
const reason = !returnedOwned ? 'no-service-disposer' : returnedFailed ? 'service-disposal-failed' : 'service-disposed';
|
|
379
|
+
if (await this.runDisposers(reason))
|
|
380
|
+
failed = true;
|
|
381
|
+
this.disposersOwned = false;
|
|
382
|
+
}
|
|
259
383
|
if (owned)
|
|
260
384
|
this.events.cleanupCompleted?.(failed ? 'failure' : 'success');
|
|
261
385
|
this.stages.length = 0;
|
|
262
386
|
this.result = undefined;
|
|
263
387
|
}
|
|
388
|
+
// Reached only after every barrier and rollback has drained; draining the stack
|
|
389
|
+
// here is for a context the application retained, never for a running one.
|
|
264
390
|
release() {
|
|
265
391
|
this.frames.length = 0;
|
|
266
392
|
this.stages.length = 0;
|
|
393
|
+
this.disposers?.drain();
|
|
394
|
+
this.disposersOwned = false;
|
|
267
395
|
this.pending.clear();
|
|
268
396
|
this.result = undefined;
|
|
269
397
|
}
|
package/dist/runtime.d.ts
CHANGED
|
@@ -40,6 +40,7 @@ export declare class BindingGraph {
|
|
|
40
40
|
/**
|
|
41
41
|
* Return the context acquisitions use, resolving the host classifier before any factory runs.
|
|
42
42
|
* Immutable graphs need explicit-mode validation only once; configured forks are O(1).
|
|
43
|
+
* A host without a classifier gets every automatic registration named, so the fix is one pass.
|
|
43
44
|
*/
|
|
44
45
|
preflight(context: RuntimeContext): RuntimeContext;
|
|
45
46
|
publicBinding(key: BindingKey): BindingId;
|
package/dist/runtime.js
CHANGED
|
@@ -126,14 +126,25 @@ class BindingGraph {
|
|
|
126
126
|
/**
|
|
127
127
|
* Return the context acquisitions use, resolving the host classifier before any factory runs.
|
|
128
128
|
* Immutable graphs need explicit-mode validation only once; configured forks are O(1).
|
|
129
|
+
* A host without a classifier gets every automatic registration named, so the fix is one pass.
|
|
129
130
|
*/
|
|
130
131
|
preflight(context) {
|
|
131
132
|
if (context.isNativePromise || this.#explicitlyClassified)
|
|
132
133
|
return context;
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
134
|
+
const automatic = [];
|
|
135
|
+
for (const [, { description, normalized }] of this.#bindings) {
|
|
136
|
+
if (normalized.acquisitionMode === 'auto' || normalized.operations.some(operation => 'acquisitionMode' in operation && operation.acquisitionMode === 'auto')) {
|
|
137
|
+
// The host answers once for the whole graph; only a host without a classifier needs the full list.
|
|
138
|
+
if (!automatic.length) {
|
|
139
|
+
const resolved = (0, acquisition_mode_1.resolveClassifier)(context);
|
|
140
|
+
if (resolved)
|
|
141
|
+
return resolved;
|
|
142
|
+
}
|
|
143
|
+
automatic.push(description.label);
|
|
144
|
+
}
|
|
136
145
|
}
|
|
146
|
+
if (automatic.length)
|
|
147
|
+
throw (0, acquisition_mode_1.classifierRequired)(automatic);
|
|
137
148
|
this.#explicitlyClassified = true;
|
|
138
149
|
return context;
|
|
139
150
|
}
|
package/dist/startup.js
CHANGED
|
@@ -7,6 +7,15 @@ const runtime_1 = require("./runtime");
|
|
|
7
7
|
const tokens_1 = require("./tokens");
|
|
8
8
|
const errors_2 = require("./errors");
|
|
9
9
|
/** Snapshot own cancellation options once, so getters and prototypes cannot change them later. */
|
|
10
|
+
/**
|
|
11
|
+
* Format a timeout error's stack before handing it on. An unformatted stack keeps
|
|
12
|
+
* the frames that created it alive, and these are closures over the runtime; a
|
|
13
|
+
* startup timeout also becomes the reason on every signal the bag handed out.
|
|
14
|
+
*/
|
|
15
|
+
function formatted(error) {
|
|
16
|
+
void error.stack;
|
|
17
|
+
return error;
|
|
18
|
+
}
|
|
10
19
|
function snapshotOptions(options, operation, code, supported) {
|
|
11
20
|
if (options === undefined)
|
|
12
21
|
return {};
|
|
@@ -79,7 +88,7 @@ function closeRuntime(runtime, options) {
|
|
|
79
88
|
if (settled || timeoutMs === undefined)
|
|
80
89
|
return;
|
|
81
90
|
if (performance.now() - began >= timeoutMs) {
|
|
82
|
-
cancel('timeout', (0, errors_1.diagnostic)(new DOMException((0, errors_1.diagnosticMessage)('DI_BAG_CLOSE_TIMEOUT', 'Bag close timed out'), 'TimeoutError'), 'DI_BAG_CLOSE_TIMEOUT', { operation: 'close', timeoutMs }));
|
|
91
|
+
cancel('timeout', formatted((0, errors_1.diagnostic)(new DOMException((0, errors_1.diagnosticMessage)('DI_BAG_CLOSE_TIMEOUT', 'Bag close timed out'), 'TimeoutError'), 'DI_BAG_CLOSE_TIMEOUT', { operation: 'close', timeoutMs })));
|
|
83
92
|
return;
|
|
84
93
|
}
|
|
85
94
|
// Long deadlines must not wrap into an immediate timer on Node/Bun.
|
|
@@ -131,7 +140,7 @@ function startRuntime(graph, context, keys, options) {
|
|
|
131
140
|
const checkCancellation = () => {
|
|
132
141
|
aborted();
|
|
133
142
|
if (!settled && timeoutMs !== undefined && performance.now() - began >= timeoutMs) {
|
|
134
|
-
cancel('timeout', (0, errors_1.diagnostic)(new DOMException((0, errors_1.diagnosticMessage)('DI_BAG_STARTUP_TIMEOUT', 'Bag startup timed out'), 'TimeoutError'), 'DI_BAG_STARTUP_TIMEOUT', { operation: 'buildAndStart', timeoutMs }));
|
|
143
|
+
cancel('timeout', formatted((0, errors_1.diagnostic)(new DOMException((0, errors_1.diagnosticMessage)('DI_BAG_STARTUP_TIMEOUT', 'Bag startup timed out'), 'TimeoutError'), 'DI_BAG_STARTUP_TIMEOUT', { operation: 'buildAndStart', timeoutMs })));
|
|
135
144
|
}
|
|
136
145
|
return settled;
|
|
137
146
|
};
|
package/dist/types.d.ts
CHANGED
|
@@ -46,7 +46,7 @@ export interface DiBagPolicy {
|
|
|
46
46
|
type StructuralThenablesAllowed = DiBagPolicy extends {
|
|
47
47
|
readonly structuralThenables: 'allow';
|
|
48
48
|
} ? true : false;
|
|
49
|
-
type IsAny<T> = 0 extends 1 & T ? true : false;
|
|
49
|
+
export type IsAny<T> = 0 extends 1 & T ? true : false;
|
|
50
50
|
/** True for a declared output with a callable `then` that is not a native Promise; `any` is exempt. */
|
|
51
51
|
export type StructuralThenable<O> = StructuralThenablesAllowed extends true ? false : IsAny<O> extends true ? false : O extends infer T & {} ? T extends Promise<unknown> ? false : T extends {
|
|
52
52
|
then(...args: never[]): unknown;
|
package/docs/agent/api-card.md
CHANGED
|
@@ -12,6 +12,8 @@ An example without an import line uses `import { DiBag } from 'di-bag';`. The ru
|
|
|
12
12
|
| --- | --- |
|
|
13
13
|
| Start a graph | [`DiBag.createBuilder()`](#dibag-createbuilder) |
|
|
14
14
|
| Register a service | [`builder.register(more)`](#builder-register) |
|
|
15
|
+
| Register a synchronous factory for a browser or worker | [`DiBag.fromSyncFactory(callback, options)`](#dibag-fromsyncfactory) |
|
|
16
|
+
| Register an async factory for a browser or worker | [`DiBag.fromAsyncFactory(callback, options)`](#dibag-fromasyncfactory) |
|
|
15
17
|
| Attach cleanup | [`DiBag.withDisposal(create, dispose)`](#dibag-withdisposal) |
|
|
16
18
|
| Choose a lifetime | [`DiBag.withLifetime(registration, lifetime)`](#dibag-withlifetime) |
|
|
17
19
|
| Seal a module | [`builder.buildModule(keys, options?)`](#builder-buildmodule) |
|
|
@@ -40,6 +42,21 @@ type Query = { then(done: (rows: string[]) => void): void };
|
|
|
40
42
|
const query = DiBag.fromFactory((): Query => ({ then: done => done([]) }), { acquisitionMode: 'raw' });
|
|
41
43
|
```
|
|
42
44
|
|
|
45
|
+
### `DiBag.fromSyncFactory(callback, options)` {#dibag-fromsyncfactory}
|
|
46
|
+
Describe a synchronous factory that runs on every host: the exact return value is the service and `then` is never read. Throws: [`DI_BAG_INVALID_FACTORY`](errors.md#di-bag-invalid-factory).
|
|
47
|
+
```ts
|
|
48
|
+
const config = DiBag.fromSyncFactory(() => ({ url: 'memory:' }));
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### `DiBag.fromAsyncFactory(callback, options)` {#dibag-fromasyncfactory}
|
|
52
|
+
Describe an asynchronous factory that runs on every host: the service is the returned native Promise and `withDisposal` receives its fulfilled value. Throws: [`DI_BAG_INVALID_FACTORY`](errors.md#di-bag-invalid-factory).
|
|
53
|
+
```ts
|
|
54
|
+
const db = DiBag.withDisposal(
|
|
55
|
+
DiBag.fromAsyncFactory(async ({ config }: { config: { url: string } }) => ({ url: config.url, end: async () => {} })),
|
|
56
|
+
db => db.end(),
|
|
57
|
+
);
|
|
58
|
+
```
|
|
59
|
+
|
|
43
60
|
### `DiBag.token(key)` {#dibag-token}
|
|
44
61
|
Create a typed token from a unique symbol; `.of<Service>()` fixes its service type. Throws: [`DI_BAG_INVALID_TOKEN`](errors.md#di-bag-invalid-token).
|
|
45
62
|
```ts
|
package/docs/agent/errors.md
CHANGED
|
@@ -190,6 +190,37 @@ DiBag.createBuilder()
|
|
|
190
190
|
|
|
191
191
|
**Recipe:** [add and consume an async client](recipes.md#async-client).
|
|
192
192
|
|
|
193
|
+
### Portable factory output {#portable-factory-output}
|
|
194
|
+
|
|
195
|
+
**When:** `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; see https://dany-fedorov.github.io/di-bag/agent/errors.html#portable-factory-output`,
|
|
196
|
+
or `fromAsyncFactory requires a Promise output; use fromSyncFactory for a synchronous value; see https://dany-fedorov.github.io/di-bag/agent/errors.html#portable-factory-output`.
|
|
197
|
+
|
|
198
|
+
**Cause:** the helper fixes the acquisition mode from its name, so the factory's
|
|
199
|
+
declared output must agree with it. `fromSyncFactory` is a `raw` stage that never
|
|
200
|
+
reads `then`: an `async` function, a `Promise`-returning function, a union with a
|
|
201
|
+
Promise member, or a thenable such as a query builder cannot be its service.
|
|
202
|
+
`fromAsyncFactory` is a `nativePromise` stage: a plain value, a union, or a
|
|
203
|
+
`PromiseLike` cannot be its service.
|
|
204
|
+
|
|
205
|
+
**Fix:** pick the helper that matches the output. When the Promise object itself
|
|
206
|
+
is the service, use `DiBag.fromFactory(create, { acquisitionMode: 'raw' })`.
|
|
207
|
+
|
|
208
|
+
```ts
|
|
209
|
+
// expect-error: fromSyncFactory output must not be a Promise or thenable
|
|
210
|
+
import { DiBag } from 'di-bag';
|
|
211
|
+
|
|
212
|
+
const config = DiBag.fromSyncFactory(async () => ({ url: 'memory:' }));
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
```ts
|
|
216
|
+
import { DiBag } from 'di-bag';
|
|
217
|
+
|
|
218
|
+
const config = DiBag.fromAsyncFactory(async () => ({ url: 'memory:' }));
|
|
219
|
+
const ownedPromise = DiBag.fromFactory(() => Promise.resolve({ url: 'memory:' }), { acquisitionMode: 'raw' });
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
**Recipe:** [make a graph portable to browsers and workers](recipes.md#portable-graph).
|
|
223
|
+
|
|
193
224
|
### Wrong shape at a call {#wrong-shape}
|
|
194
225
|
|
|
195
226
|
**When:** `register`, `installModule`, or `replace` reports
|
|
@@ -244,22 +275,59 @@ app.fork(['port'], { port: () => 'eighty' });
|
|
|
244
275
|
`process.getBuiltinModule`: browsers, Web Workers, and other non-Node runtimes.
|
|
245
276
|
Node, Bun, and Deno never raise it.
|
|
246
277
|
|
|
247
|
-
**Cause:** a
|
|
248
|
-
is configured, and the host offers none.
|
|
278
|
+
**Cause:** a registration uses automatic acquisition, no native-Promise classifier
|
|
279
|
+
is configured, and the host offers none. The message and `details.bindings` name
|
|
280
|
+
every such registration, sorted, with private module services as `<label>/<key>`;
|
|
281
|
+
a direct `transformService` without an `acquisitionMode` counts under its
|
|
282
|
+
registration's name.
|
|
249
283
|
|
|
250
|
-
**Fix:**
|
|
251
|
-
`
|
|
252
|
-
|
|
284
|
+
**Fix:** register each named service with `DiBag.fromSyncFactory` or
|
|
285
|
+
`DiBag.fromAsyncFactory`; give `fromFunction`, `fromClass`, and direct
|
|
286
|
+
`transformService` an explicit `acquisitionMode`; or configure a trusted
|
|
287
|
+
classifier with `withConfiguration({ runtime: { isNativePromise } })`.
|
|
253
288
|
|
|
254
289
|
```ts
|
|
255
290
|
import { DiBag } from 'di-bag';
|
|
256
291
|
|
|
257
292
|
const app = DiBag.createBuilder()
|
|
258
|
-
.register({
|
|
293
|
+
.register({
|
|
294
|
+
answer: DiBag.fromSyncFactory(() => 42),
|
|
295
|
+
later: DiBag.fromAsyncFactory(async ({ answer }: { answer: number }) => answer * 2),
|
|
296
|
+
})
|
|
259
297
|
.build();
|
|
260
298
|
```
|
|
261
299
|
|
|
262
|
-
**Recipe:**
|
|
300
|
+
**Recipe:** [make a graph portable to browsers and workers](recipes.md#portable-graph).
|
|
301
|
+
|
|
302
|
+
### DI_BAG_CLEANUP_AFTER_FACTORY {#di-bag-cleanup-after-factory}
|
|
303
|
+
|
|
304
|
+
**When:** `factoryCtx.pushDisposer(disposer)` throws because the factory that
|
|
305
|
+
owns the context has already returned or failed. Its projections may still be
|
|
306
|
+
running; the factory is the boundary, not the whole acquisition.
|
|
307
|
+
|
|
308
|
+
**Cause:** the acquisition context escaped its factory and was called later —
|
|
309
|
+
from the service it produced, from a projection of the registration, or from
|
|
310
|
+
inside a pushed disposer already running. A context belongs to one running
|
|
311
|
+
factory, not to the service it produced.
|
|
312
|
+
|
|
313
|
+
**Fix:** push inside the factory, immediately after acquiring the resource; own
|
|
314
|
+
the returned value with `DiBag.withDisposal`, and give a pushed disposer for that
|
|
315
|
+
same value a `reason` check.
|
|
316
|
+
|
|
317
|
+
```ts
|
|
318
|
+
import { DiBag } from 'di-bag';
|
|
319
|
+
|
|
320
|
+
const handle = DiBag.withDisposal(
|
|
321
|
+
DiBag.fromFactory(async (_deps: {}, factoryCtx) => {
|
|
322
|
+
const socket = { close: async () => {} };
|
|
323
|
+
factoryCtx.pushDisposer(disposerCtx => { if (disposerCtx.reason !== 'service-disposed') return socket.close(); });
|
|
324
|
+
return socket;
|
|
325
|
+
}, { context: 'acquisition' }),
|
|
326
|
+
socket => socket.close(),
|
|
327
|
+
);
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
**Recipe:** [own a resource a factory acquires on the way](recipes.md#partial-acquisition).
|
|
263
331
|
|
|
264
332
|
### DI_BAG_CLEANUP_FAILED {#di-bag-cleanup-failed}
|
|
265
333
|
|
|
@@ -390,7 +458,9 @@ await app.close();
|
|
|
390
458
|
### DI_BAG_CLOSING {#di-bag-closing}
|
|
391
459
|
|
|
392
460
|
**When:** the same operations as [`DI_BAG_CLOSED`](#di-bag-closed), while
|
|
393
|
-
`close()` is still in progress.
|
|
461
|
+
`close()` is still in progress. Also the message of `factoryCtx.signal.reason`
|
|
462
|
+
after `close()`: an `AbortError` that is the same object for every bag. A
|
|
463
|
+
cancelled or failed startup aborts with its own cause instead.
|
|
394
464
|
|
|
395
465
|
**Cause:** a request, timer, or factory started new resolution after shutdown
|
|
396
466
|
began. Only a factory already running when `close()` started may still read its
|
|
@@ -543,6 +613,29 @@ const DiBag = CoreDiBag.withConfiguration({
|
|
|
543
613
|
|
|
544
614
|
**Recipe:** none.
|
|
545
615
|
|
|
616
|
+
### DI_BAG_INVALID_CLEANUP {#di-bag-invalid-cleanup}
|
|
617
|
+
|
|
618
|
+
**When:** `factoryCtx.pushDisposer(disposer)` throws because `disposer` is not a
|
|
619
|
+
function.
|
|
620
|
+
|
|
621
|
+
**Cause:** a value was passed where a disposer callback belongs, usually the
|
|
622
|
+
result of calling the release instead of passing it.
|
|
623
|
+
|
|
624
|
+
**Fix:** pass a function: `factoryCtx.pushDisposer(() => socket.close())`, not
|
|
625
|
+
`factoryCtx.pushDisposer(socket.close())`.
|
|
626
|
+
|
|
627
|
+
```ts
|
|
628
|
+
import { DiBag } from 'di-bag';
|
|
629
|
+
|
|
630
|
+
const socket = DiBag.fromFactory(async (_deps: {}, factoryCtx) => {
|
|
631
|
+
const handle = { close: async () => {} };
|
|
632
|
+
factoryCtx.pushDisposer(() => handle.close());
|
|
633
|
+
return handle;
|
|
634
|
+
}, { context: 'acquisition' });
|
|
635
|
+
```
|
|
636
|
+
|
|
637
|
+
**Recipe:** [own a resource a factory acquires on the way](recipes.md#partial-acquisition).
|
|
638
|
+
|
|
546
639
|
### DI_BAG_INVALID_CLOSE {#di-bag-invalid-close}
|
|
547
640
|
|
|
548
641
|
**When:** `close(options)` rejects because options are not
|
|
@@ -650,13 +743,16 @@ const east = reports.renameExport('service', 'eastReports');
|
|
|
650
743
|
|
|
651
744
|
### DI_BAG_INVALID_FACTORY {#di-bag-invalid-factory}
|
|
652
745
|
|
|
653
|
-
**When:** `DiBag.fromFactory
|
|
654
|
-
`context` option other than `'acquisition'
|
|
746
|
+
**When:** `DiBag.fromFactory`, `fromSyncFactory`, or `fromAsyncFactory` receives a
|
|
747
|
+
non-function, or a `context` option other than `'acquisition'`; the two portable
|
|
748
|
+
helpers also refuse an `acquisitionMode` option, because they fix it themselves.
|
|
655
749
|
|
|
656
|
-
**Cause:** a value passed where a factory is expected
|
|
750
|
+
**Cause:** a value passed where a factory is expected, or a mode passed to a
|
|
751
|
+
helper whose name already selects it.
|
|
657
752
|
|
|
658
753
|
**Fix:** pass a function; use `{ context: 'acquisition' }` to receive the
|
|
659
|
-
acquisition
|
|
754
|
+
acquisition context as the second argument; choose `fromSyncFactory` or
|
|
755
|
+
`fromAsyncFactory` instead of passing a mode to them.
|
|
660
756
|
|
|
661
757
|
```ts
|
|
662
758
|
import { DiBag } from 'di-bag';
|