injeca 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/README.md +251 -14
- package/dist/decision-BXZu3N1O.mjs +1356 -0
- package/dist/decision-BXZu3N1O.mjs.map +1 -0
- package/dist/{global-C0Nk0pU8.mjs → global-B5N-d-TO.mjs} +1024 -1214
- package/dist/global-B5N-d-TO.mjs.map +1 -0
- package/dist/{global-B9Bk2EG2.d.mts → global-DPfe26g5.d.mts} +81 -17
- package/dist/global.d.mts +1 -1
- package/dist/global.mjs +1 -1
- package/dist/{index-DXguNx2M.d.mts → index-DTYp044J.d.mts} +139 -19
- package/dist/index.d.mts +40 -15
- package/dist/index.mjs +55 -23
- package/dist/index.mjs.map +1 -1
- package/dist/testing.d.mts +31 -1
- package/dist/testing.mjs +60 -2
- package/dist/testing.mjs.map +1 -1
- package/package.json +5 -5
- package/dist/decision-odiPb2Au.mjs +0 -194
- package/dist/decision-odiPb2Au.mjs.map +0 -1
- package/dist/global-C0Nk0pU8.mjs.map +0 -1
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { a as
|
|
2
|
-
import {
|
|
3
|
-
import { isUndefined, omitBy } from "es-toolkit";
|
|
1
|
+
import { $ as candidatesOf, A as causeOf, B as withoutUndefined, Dt as StartTimeoutError, E as ensureConsumerService, Et as StaleRegistrationError, F as resolvePlain, G as nodeOf, H as affectedContainers, I as acceptsCandidate, J as releasePresent, K as publishService, L as emitRecord, N as resolveFallback, O as createService, Ot as TransactionIncompleteError, P as resolveKey, Q as buildLost, R as recordAbortedWait, S as normalizeName, St as InvalidArgumentError, U as assertActive, V as createDeferred, W as awaitWithdrawal, X as subtreeOf, Y as releaseService, Z as blockingFailure, _ as timeoutOf, _t as BuildAfterDisposeError, a as releaseReservations, at as resolutionOf, bt as HookTimeoutError, c as cancelPending, ct as serviceFor, d as currentService, dt as removeDeclaration, et as epochOf, f as releaseRegistration, ft as errorFrom, g as timeoutHandlerOf, gt as withOwner, h as readPolicy, ht as untracked, i as releaseReservation, it as requiredWithdrawn, j as sameStatus, k as setPhase, kt as WaitTimeoutError, l as cancelSuccessor, lt as slotOf, m as maxAttemptsOf, mt as batch, n as defaultDecision, nt as requiredAvailable, o as reserveKey, ot as resolvedInstance, p as readDefinition, pt as messageOf, q as registerNode, r as validateDecision, rt as requiredChanged, s as reservedBy, st as selectProvider, t as createDecide, tt as findProvider, u as createRegistration, ut as inConflict, v as readEdges, vt as DuplicateServiceError, wt as ReplaceWhileStoppingError, x as isExclusiveKey, yt as HookOutsideBuildError } from "./decision-BXZu3N1O.mjs";
|
|
2
|
+
import { effectScope, getActiveSub, signal } from "alien-signals";
|
|
4
3
|
import ErrorStackParser from "error-stack-parser";
|
|
5
4
|
import { nanoid } from "nanoid";
|
|
6
5
|
// package.json
|
|
@@ -41,6 +40,9 @@ function createDefaultLogger() {
|
|
|
41
40
|
},
|
|
42
41
|
running: () => {
|
|
43
42
|
console.log(`[${name}] RUNNING`);
|
|
43
|
+
},
|
|
44
|
+
warn: (message) => {
|
|
45
|
+
console.warn(`[${name}] WARN ${message}`);
|
|
44
46
|
}
|
|
45
47
|
};
|
|
46
48
|
}
|
|
@@ -74,6 +76,9 @@ function createLoggLogger(logg) {
|
|
|
74
76
|
},
|
|
75
77
|
running: () => {
|
|
76
78
|
logg.log(`RUNNING`);
|
|
79
|
+
},
|
|
80
|
+
warn: (message) => {
|
|
81
|
+
logg.warn(message);
|
|
77
82
|
}
|
|
78
83
|
};
|
|
79
84
|
}
|
|
@@ -87,964 +92,180 @@ function createNoopLogger() {
|
|
|
87
92
|
hookOnStartComplete: () => {},
|
|
88
93
|
hookOnStop: () => {},
|
|
89
94
|
hookOnStopComplete: () => {},
|
|
90
|
-
running: () => {}
|
|
95
|
+
running: () => {},
|
|
96
|
+
warn: () => {}
|
|
91
97
|
};
|
|
92
98
|
}
|
|
93
|
-
// src/helpers/
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
* A promise continuation starts with no active subscriber, so it needs no extra pause.
|
|
100
|
-
*/
|
|
101
|
-
function untracked(fn) {
|
|
102
|
-
const previous = setActiveSub(void 0);
|
|
103
|
-
try {
|
|
104
|
-
return fn();
|
|
105
|
-
} finally {
|
|
106
|
-
setActiveSub(previous);
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
/** Writes signals in one batch. Effects see one consistent change. */
|
|
110
|
-
function batch(fn) {
|
|
111
|
-
startBatch();
|
|
112
|
-
try {
|
|
113
|
-
fn();
|
|
114
|
-
} finally {
|
|
115
|
-
endBatch();
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
/** Runs `fn` with `owner` as the active subscriber, so effects that `fn` creates belong to `owner`. */
|
|
119
|
-
function withOwner(owner, fn) {
|
|
120
|
-
const previous = setActiveSub(owner);
|
|
121
|
-
try {
|
|
122
|
-
return fn();
|
|
123
|
-
} finally {
|
|
124
|
-
setActiveSub(previous);
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
// src/helpers/error-from.ts
|
|
128
|
-
/**
|
|
129
|
-
* Makes one error from collected errors. One error keeps its identity. More than one error becomes an `AggregateError`.
|
|
130
|
-
*
|
|
131
|
-
* The result is `unknown`, because a thrown value is not always an `Error`.
|
|
132
|
-
*/
|
|
133
|
-
function errorFrom(errors, message) {
|
|
134
|
-
return errors.length === 1 ? errors[0] : new AggregateError(errors, message);
|
|
135
|
-
}
|
|
136
|
-
// src/graph/resolvers/exclusive.ts
|
|
137
|
-
/** Tells whether `ancestor` is `container` or an ancestor of it, with no container in between that isolates `keyName`. */
|
|
138
|
-
function reaches(container, ancestor, keyName) {
|
|
139
|
-
for (let current = container; current; current = current.parent) {
|
|
140
|
-
if (current === ancestor) return true;
|
|
141
|
-
if (current.isolated.has(keyName)) return false;
|
|
142
|
-
}
|
|
143
|
-
return false;
|
|
144
|
-
}
|
|
145
|
-
/**
|
|
146
|
-
* Tells whether two containers share a resolution area of `keyName`.
|
|
147
|
-
* A resolution area is the container that has the key, with its descendants that do not isolate the key.
|
|
148
|
-
*/
|
|
149
|
-
function sameArea(a, b, keyName) {
|
|
150
|
-
return reaches(a, b, keyName) || reaches(b, a, keyName);
|
|
151
|
-
}
|
|
152
|
-
/** The published services of `keyName` in all containers of the runtime. */
|
|
153
|
-
function declarationsOf(runtime, keyName) {
|
|
154
|
-
let declarations = runtime.declarations.get(keyName);
|
|
155
|
-
if (!declarations) {
|
|
156
|
-
declarations = signal(/* @__PURE__ */ new Set());
|
|
157
|
-
runtime.declarations.set(keyName, declarations);
|
|
158
|
-
}
|
|
159
|
-
return declarations;
|
|
160
|
-
}
|
|
161
|
-
function addDeclaration(service) {
|
|
162
|
-
const declarations = declarationsOf(service.container.runtime, service.name);
|
|
163
|
-
if (!declarations().has(service)) declarations(/* @__PURE__ */ new Set([...declarations(), service]));
|
|
164
|
-
}
|
|
165
|
-
function removeDeclaration(service) {
|
|
166
|
-
const declarations = declarationsOf(service.container.runtime, service.name);
|
|
167
|
-
if (declarations().has(service)) declarations(new Set([...declarations()].filter((other) => other !== service)));
|
|
168
|
-
}
|
|
169
|
-
function sameList(a, b) {
|
|
170
|
-
return a.length === b.length && a.every((service, index) => service === b[index]);
|
|
171
|
-
}
|
|
172
|
-
const none = Object.freeze([]);
|
|
173
|
-
/**
|
|
174
|
-
* Creates the conflict of a service: the other present declarations of its key in the same resolution area, when the key is exclusive.
|
|
175
|
-
*
|
|
176
|
-
* The result depends only on the set of declarations. The order of the registrations has no effect.
|
|
177
|
-
* Only one registration of a key exists in one container, so a rival is always in another container.
|
|
178
|
-
* A service of a `perConsumer` registration has no conflict of its own. It follows the conflict of its registration.
|
|
179
|
-
*/
|
|
180
|
-
function createRivals(service) {
|
|
181
|
-
if (service.template) return () => service.template?.signals.rivals() ?? none;
|
|
182
|
-
if (service.kind !== "service") return () => none;
|
|
183
|
-
return computed((previous) => {
|
|
184
|
-
const rivals = [...declarationsOf(service.container.runtime, service.name)()].filter((other) => other.container !== service.container && other.signals.present() && (service.exclusive || other.exclusive) && sameArea(service.container, other.container, service.name)).sort((a, b) => a.seq - b.seq);
|
|
185
|
-
return previous && sameList(previous, rivals) ? previous : rivals;
|
|
186
|
-
});
|
|
187
|
-
}
|
|
188
|
-
/** Tells whether the key of the service has more than one declaration in its resolution area. */
|
|
189
|
-
function inConflict(service) {
|
|
190
|
-
return service.signals.rivals().length > 0;
|
|
191
|
-
}
|
|
192
|
-
// src/graph/resolvers/resolvers.ts
|
|
193
|
-
/** Returns the slot signal of `keyName` in `container`. It creates the slot on first read, so that resolution can track it. */
|
|
194
|
-
function slotOf(container, keyName) {
|
|
195
|
-
let slot = container.signals.slots.get(keyName);
|
|
196
|
-
if (!slot) {
|
|
197
|
-
slot = signal(void 0);
|
|
198
|
-
container.signals.slots.set(keyName, slot);
|
|
199
|
-
}
|
|
200
|
-
return slot;
|
|
201
|
-
}
|
|
202
|
-
/**
|
|
203
|
-
* Lists the registrations of `keyName` that a consumer in `container` can see, nearest first.
|
|
204
|
-
* The walk ends at a container that isolates the key. A service does not resolve itself.
|
|
205
|
-
*/
|
|
206
|
-
function candidatesOf(container, keyName, consumer) {
|
|
207
|
-
const candidates = [];
|
|
208
|
-
for (let current = container; current; current = current.parent) {
|
|
209
|
-
const candidate = slotOf(current, keyName)();
|
|
210
|
-
if (candidate && candidate !== consumer) candidates.push(candidate);
|
|
211
|
-
if (current.isolated.has(keyName)) break;
|
|
99
|
+
// src/helpers/clock.ts
|
|
100
|
+
const systemClock = Object.freeze({
|
|
101
|
+
now: () => Date.now(),
|
|
102
|
+
setTimeout: (callback, ms) => {
|
|
103
|
+
const handle = globalThis.setTimeout(callback, ms);
|
|
104
|
+
return () => globalThis.clearTimeout(handle);
|
|
212
105
|
}
|
|
213
|
-
|
|
214
|
-
}
|
|
215
|
-
/** The service that serves `registration` for a consumer in `container`. A `perConsumer` registration has one for each consumer container, or none yet. */
|
|
216
|
-
function serviceFor(registration, container) {
|
|
217
|
-
return registration.perConsumer ? registration.signals.consumers().get(container) : registration;
|
|
218
|
-
}
|
|
106
|
+
});
|
|
219
107
|
/**
|
|
220
|
-
*
|
|
108
|
+
* Calls `callback` after `ms` milliseconds of `clock`, like `clock.setTimeout`. Returns a function that cancels the call.
|
|
221
109
|
*
|
|
222
|
-
*
|
|
223
|
-
* If no registration serves, the nearest one is selected. So shadowing never leaves a gap.
|
|
110
|
+
* With `systemClock`, the timer does not keep a Node process alive, as for `AbortSignal.timeout()`. Other clocks get a plain `setTimeout` call.
|
|
224
111
|
*/
|
|
225
|
-
function
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
registration,
|
|
231
|
-
provider
|
|
232
|
-
};
|
|
233
|
-
}
|
|
234
|
-
const [nearest] = candidates;
|
|
235
|
-
return nearest ? {
|
|
236
|
-
registration: nearest,
|
|
237
|
-
provider: serviceFor(nearest, container)
|
|
238
|
-
} : void 0;
|
|
239
|
-
}
|
|
240
|
-
/** Finds the service for `keyName` that a consumer in `container` sees. A `perConsumer` registration without a service for the container gives the registration. */
|
|
241
|
-
function findProvider(container, keyName, consumer) {
|
|
242
|
-
const selection = selectProvider(container, keyName, consumer);
|
|
243
|
-
return selection && (selection.provider ?? selection.registration);
|
|
244
|
-
}
|
|
245
|
-
/** `ready` and `degraded` count as available. A provider outside `running` has no status and is not available. */
|
|
246
|
-
function isAvailable(provider) {
|
|
247
|
-
const status = provider.signals.effective();
|
|
248
|
-
return status !== void 0 && status.status !== "unavailable";
|
|
249
|
-
}
|
|
250
|
-
/** The current resolution of the edge. */
|
|
251
|
-
function resolutionOf(service, edge) {
|
|
252
|
-
return service.signals.resolutions.get(edge.name)?.() ?? { kind: "missing" };
|
|
253
|
-
}
|
|
254
|
-
/** The instance of the edge, when it resolves. */
|
|
255
|
-
function resolvedInstance(service, edge) {
|
|
256
|
-
const resolution = resolutionOf(service, edge);
|
|
257
|
-
return resolution.kind === "resolved" ? resolution.instance : void 0;
|
|
112
|
+
function setWeakTimeout(clock, callback, ms) {
|
|
113
|
+
if (clock !== systemClock) return clock.setTimeout(callback, ms);
|
|
114
|
+
const handle = globalThis.setTimeout(callback, ms);
|
|
115
|
+
if (typeof handle === "object" && handle !== null && typeof handle.unref === "function") handle.unref();
|
|
116
|
+
return () => globalThis.clearTimeout(handle);
|
|
258
117
|
}
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
const resolution = resolutionOf(service, edge);
|
|
264
|
-
return resolution.kind === "resolved" && isAvailable(resolution.provider);
|
|
265
|
-
});
|
|
118
|
+
// src/helpers/abort.ts
|
|
119
|
+
/** The reason of a timeout signal: a `DOMException` with the name `TimeoutError`, as for `AbortSignal.timeout()`. */
|
|
120
|
+
function timeoutReason(message) {
|
|
121
|
+
return new DOMException(message, "TimeoutError");
|
|
266
122
|
}
|
|
267
|
-
/**
|
|
268
|
-
function
|
|
269
|
-
return
|
|
123
|
+
/** Returns `true` when `reason` names itself a timeout: `reason.name === 'TimeoutError'`. The reason of `AbortSignal.timeout()` and of `timeout()` passes. */
|
|
124
|
+
function isTimeoutReason(reason) {
|
|
125
|
+
return typeof reason === "object" && reason !== null && reason.name === "TimeoutError";
|
|
270
126
|
}
|
|
271
|
-
/**
|
|
272
|
-
function
|
|
273
|
-
return
|
|
274
|
-
}
|
|
275
|
-
/** A build is discarded when its target is lost, a required dependency changed during the build, or the key went into conflict. */
|
|
276
|
-
function buildLost(service, instance) {
|
|
277
|
-
return !service.signals.present() || !service.signals.active() || service.signals.rivals().length > 0 || requiredChanged(service, instance);
|
|
278
|
-
}
|
|
279
|
-
/** The identity of the dependency set: the revision and the instance id of each resolved required dependency. */
|
|
280
|
-
function epochOf(service, pick) {
|
|
281
|
-
const ids = service.edges.filter((edge) => !edge.optional).map((edge) => `${edge.name}=${pick(edge)?.id ?? ""}`);
|
|
282
|
-
return [String(service.revision), ...ids].join("|");
|
|
127
|
+
/** The rejection of a wait for a state that `reason` aborted: `wait-timeout` with the reason as `cause` for a timeout reason, else the reason itself. */
|
|
128
|
+
function waitAbortError(reason, message) {
|
|
129
|
+
return isTimeoutReason(reason) ? new WaitTimeoutError(message, { cause: reason }) : reason;
|
|
283
130
|
}
|
|
284
131
|
/**
|
|
285
|
-
*
|
|
132
|
+
* Aborts `controller` when one of `sources` aborts, with the reason of that source.
|
|
133
|
+
* Returns a function that removes the listeners. The listeners also go when the first source aborts.
|
|
286
134
|
*/
|
|
287
|
-
function
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
const failure = blockingFailure(provider, visited);
|
|
298
|
-
if (failure) return failure;
|
|
135
|
+
function forwardAbort(controller, sources) {
|
|
136
|
+
const listeners = [];
|
|
137
|
+
const release = () => {
|
|
138
|
+
for (const [source, listener] of listeners.splice(0)) source.removeEventListener("abort", listener);
|
|
139
|
+
};
|
|
140
|
+
for (const source of sources) {
|
|
141
|
+
if (source.aborted) {
|
|
142
|
+
release();
|
|
143
|
+
controller.abort(source.reason);
|
|
144
|
+
return release;
|
|
299
145
|
}
|
|
146
|
+
const listener = () => {
|
|
147
|
+
release();
|
|
148
|
+
controller.abort(source.reason);
|
|
149
|
+
};
|
|
150
|
+
source.addEventListener("abort", listener, { once: true });
|
|
151
|
+
listeners.push([source, listener]);
|
|
300
152
|
}
|
|
153
|
+
return release;
|
|
301
154
|
}
|
|
302
|
-
|
|
303
|
-
const
|
|
304
|
-
/** Connects the public container object to its node. `createContainer` calls it once. */
|
|
305
|
-
function registerNode(container, node) {
|
|
306
|
-
nodes.set(container, node);
|
|
307
|
-
}
|
|
308
|
-
/** Returns the internal node of a container from `createContainer`. */
|
|
309
|
-
function nodeOf(container) {
|
|
310
|
-
const node = typeof container === "object" && container !== null ? nodes.get(container) : void 0;
|
|
311
|
-
if (!node) throw new InvalidArgumentError("The value is not a container from createContainer.");
|
|
312
|
-
return node;
|
|
313
|
-
}
|
|
314
|
-
function assertActive(container, action) {
|
|
315
|
-
if (container.state !== "active") throw new BuildAfterDisposeError(`Cannot ${action} after the container disposal started.`);
|
|
316
|
-
}
|
|
317
|
-
/** Lists `container` and its descendants that were started, through started containers only. `start()` and `stop()` apply to them. */
|
|
318
|
-
function affectedContainers(container) {
|
|
319
|
-
const result = [container];
|
|
320
|
-
for (const child of container.children) if (child.enrolled) result.push(...affectedContainers(child));
|
|
321
|
-
return result;
|
|
322
|
-
}
|
|
323
|
-
/** Lists `container` and all its descendants. */
|
|
324
|
-
function subtreeOf(container) {
|
|
325
|
-
return [container, ...[...container.children].flatMap(subtreeOf)];
|
|
326
|
-
}
|
|
155
|
+
/** The signals that each combined signal of `anySignal` comes from. A derived signal keeps the origin of its sources. */
|
|
156
|
+
const parents = /* @__PURE__ */ new WeakMap();
|
|
327
157
|
/**
|
|
328
|
-
*
|
|
329
|
-
*
|
|
158
|
+
* Returns a signal that aborts when one of `signals` aborts, with the reason of the first one. `undefined` entries are skipped.
|
|
159
|
+
*
|
|
160
|
+
* It uses `AbortSignal.any` when the runtime has it. Otherwise it forwards the abort, and removes its listeners from all signals when it aborts.
|
|
330
161
|
*/
|
|
331
|
-
function
|
|
332
|
-
const
|
|
333
|
-
if (
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
162
|
+
function anySignal(...signals) {
|
|
163
|
+
const sources = signals.filter((signal) => signal !== void 0);
|
|
164
|
+
if (sources.length === 1) return sources[0];
|
|
165
|
+
let signal;
|
|
166
|
+
if (typeof AbortSignal.any === "function") signal = AbortSignal.any(sources);
|
|
167
|
+
else {
|
|
168
|
+
const controller = new AbortController();
|
|
169
|
+
forwardAbort(controller, sources);
|
|
170
|
+
signal = controller.signal;
|
|
171
|
+
}
|
|
172
|
+
parents.set(signal, sources);
|
|
173
|
+
return signal;
|
|
174
|
+
}
|
|
175
|
+
/** Lists `signal` and each signal that it comes from through `anySignal`, `withTimeout` included, nearest first. */
|
|
176
|
+
function signalSources(signal) {
|
|
177
|
+
const seen = /* @__PURE__ */ new Set([signal]);
|
|
178
|
+
const queue = [signal];
|
|
179
|
+
for (let index = 0; index < queue.length; index++) for (const parent of parents.get(queue[index]) ?? []) if (!seen.has(parent)) {
|
|
180
|
+
seen.add(parent);
|
|
181
|
+
queue.push(parent);
|
|
182
|
+
}
|
|
183
|
+
return queue;
|
|
340
184
|
}
|
|
341
185
|
/**
|
|
342
|
-
*
|
|
343
|
-
*
|
|
186
|
+
* Creates a signal that aborts after `ms` milliseconds of `clock`, with the result of `reason`.
|
|
187
|
+
*
|
|
188
|
+
* With `weak` and the system clock, the timer does not keep a Node process alive, as for `AbortSignal.timeout()`.
|
|
344
189
|
*/
|
|
345
|
-
function
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
const slot = slotOf(service.container, service.name);
|
|
350
|
-
if (slot() === service) slot(void 0);
|
|
351
|
-
}
|
|
352
|
-
/** Waits until `service` is `withdrawn`. Rejects with its teardown errors. */
|
|
353
|
-
async function awaitWithdrawal(service) {
|
|
354
|
-
const errors = await service.withdrawal.promise;
|
|
355
|
-
if (errors.length > 0) throw errorFrom(errors, `Withdrawal of '${service.name}' failed.`);
|
|
356
|
-
}
|
|
357
|
-
/** Withdraws a service or an invocation. Resolves `false` when it is already withdrawn. It is the `withdraw()` entry point. */
|
|
358
|
-
async function withdrawService(service) {
|
|
359
|
-
if (!untracked(() => {
|
|
360
|
-
if (!service.signals.present()) return false;
|
|
361
|
-
batch(() => releaseService(service));
|
|
362
|
-
return true;
|
|
363
|
-
})) return false;
|
|
364
|
-
await awaitWithdrawal(service);
|
|
365
|
-
return true;
|
|
366
|
-
}
|
|
367
|
-
// src/helpers/deferred.ts
|
|
368
|
-
function createDeferred() {
|
|
369
|
-
let resolve;
|
|
190
|
+
function timeoutSignal(clock, ms, reason, weak = false) {
|
|
191
|
+
const controller = new AbortController();
|
|
192
|
+
const abort = () => untracked(() => controller.abort(reason()));
|
|
193
|
+
const cancel = weak ? setWeakTimeout(clock, abort, ms) : clock.setTimeout(abort, ms);
|
|
370
194
|
return {
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
}),
|
|
374
|
-
resolve
|
|
375
|
-
};
|
|
376
|
-
}
|
|
377
|
-
// src/helpers/without-undefined.ts
|
|
378
|
-
/** Copies `fields` without the fields that are `undefined`. Spread the result to add optional fields to a record. */
|
|
379
|
-
function withoutUndefined(fields) {
|
|
380
|
-
return omitBy(fields, isUndefined);
|
|
381
|
-
}
|
|
382
|
-
// src/graph/observers.ts
|
|
383
|
-
function within(container, ancestor) {
|
|
384
|
-
for (let current = container; current; current = current.parent) if (current === ancestor) return true;
|
|
385
|
-
return false;
|
|
386
|
-
}
|
|
387
|
-
/** The active observers of `container`: the observers of the container and of its ancestors. */
|
|
388
|
-
function targetsOf$1(container, source) {
|
|
389
|
-
return [...container.runtime.observers].filter((observer) => observer !== source && observer.active && within(container, observer.container));
|
|
390
|
-
}
|
|
391
|
-
function send(container, targets, record) {
|
|
392
|
-
container.runtime.deliveries.deliver(targets.map((observer) => ({
|
|
393
|
-
run: () => {
|
|
394
|
-
if (observer.active && observer.container.state !== "disposed") observer.callback(record);
|
|
395
|
-
},
|
|
396
|
-
onError: (error) => {
|
|
397
|
-
if (record.type !== "error") reportCallbackError(observer.container, record.key, error, "An observe callback threw.", observer);
|
|
398
|
-
}
|
|
399
|
-
})));
|
|
400
|
-
}
|
|
401
|
-
/** Sends `record` to the observers of `container`. */
|
|
402
|
-
function emitRecord(container, record) {
|
|
403
|
-
const targets = targetsOf$1(container);
|
|
404
|
-
if (targets.length === 0) return;
|
|
405
|
-
send(container, targets, Object.freeze(record));
|
|
406
|
-
}
|
|
407
|
-
/** Reports a callback error as an `error` record. An error from an observe callback does not go back to that callback. */
|
|
408
|
-
function reportCallbackError(container, key, error, reason, source) {
|
|
409
|
-
const record = {
|
|
410
|
-
type: "error",
|
|
411
|
-
key,
|
|
412
|
-
owner: container.owner,
|
|
413
|
-
instanceId: void 0,
|
|
414
|
-
reason,
|
|
415
|
-
error
|
|
195
|
+
signal: controller.signal,
|
|
196
|
+
cancel
|
|
416
197
|
};
|
|
417
|
-
send(container, targetsOf$1(container, source), record);
|
|
418
|
-
}
|
|
419
|
-
// src/graph/resolvers/condition.ts
|
|
420
|
-
/**
|
|
421
|
-
* The verdict of each condition for each provider registration.
|
|
422
|
-
* The input of a condition is frozen, and a condition must be pure. So each pair runs once, and a throw is reported once.
|
|
423
|
-
*/
|
|
424
|
-
const verdicts = /* @__PURE__ */ new WeakMap();
|
|
425
|
-
/** The read-only information about a provider registration that a condition gets. */
|
|
426
|
-
function providerInfo(registration) {
|
|
427
|
-
return Object.freeze({
|
|
428
|
-
key: registration.name,
|
|
429
|
-
owner: registration.container.owner,
|
|
430
|
-
metadata: registration.metadata
|
|
431
|
-
});
|
|
432
198
|
}
|
|
433
|
-
/**
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
*/
|
|
437
|
-
function acceptsProvider(consumer, keyName, condition, registration) {
|
|
438
|
-
let byProvider = verdicts.get(condition);
|
|
439
|
-
if (!byProvider) {
|
|
440
|
-
byProvider = /* @__PURE__ */ new WeakMap();
|
|
441
|
-
verdicts.set(condition, byProvider);
|
|
442
|
-
}
|
|
443
|
-
const known = byProvider.get(registration);
|
|
444
|
-
if (known !== void 0) return known;
|
|
445
|
-
let verdict;
|
|
199
|
+
/** Reads and checks the `signal` option of `options` once. `action` names the call in errors. */
|
|
200
|
+
function readSignal(options, action) {
|
|
201
|
+
let signal;
|
|
446
202
|
try {
|
|
447
|
-
|
|
203
|
+
signal = options?.signal;
|
|
448
204
|
} catch (error) {
|
|
449
|
-
|
|
450
|
-
queueMicrotask(() => reportCallbackError(consumer.container, consumer.name, error, `A resolution condition for '${keyName}' threw. It counts as false.`));
|
|
205
|
+
throw new InvalidArgumentError(`Cannot read the ${action} options.`, { cause: error });
|
|
451
206
|
}
|
|
452
|
-
|
|
453
|
-
return
|
|
454
|
-
}
|
|
455
|
-
// src/graph/resolvers/plain.ts
|
|
456
|
-
/**
|
|
457
|
-
* Resolves one key name: the selected provider, and its instance when the provider serves.
|
|
458
|
-
* A provider in conflict, or one that `condition` rejects, does not resolve.
|
|
459
|
-
*/
|
|
460
|
-
function resolveKey(consumer, keyName, condition) {
|
|
461
|
-
const selection = selectProvider(consumer.container, keyName, consumer);
|
|
462
|
-
if (!selection) return { kind: "missing" };
|
|
463
|
-
const { registration, provider } = selection;
|
|
464
|
-
const rivals = registration.signals.rivals();
|
|
465
|
-
if (rivals.length > 0) return {
|
|
466
|
-
kind: "conflict",
|
|
467
|
-
provider: registration,
|
|
468
|
-
rivals
|
|
469
|
-
};
|
|
470
|
-
if (condition && !acceptsProvider(consumer, keyName, condition, registration)) return {
|
|
471
|
-
kind: "rejected",
|
|
472
|
-
provider: registration
|
|
473
|
-
};
|
|
474
|
-
if (!provider) return {
|
|
475
|
-
kind: "not-running",
|
|
476
|
-
provider: registration
|
|
477
|
-
};
|
|
478
|
-
const instance = provider.signals.instance();
|
|
479
|
-
if (!provider.signals.serving() || !instance) return {
|
|
480
|
-
kind: "not-running",
|
|
481
|
-
provider
|
|
482
|
-
};
|
|
483
|
-
return {
|
|
484
|
-
kind: "resolved",
|
|
485
|
-
provider,
|
|
486
|
-
instance
|
|
487
|
-
};
|
|
207
|
+
if (signal !== void 0 && !(signal instanceof AbortSignal)) throw new InvalidArgumentError(`The signal option of ${action} must be an AbortSignal.`);
|
|
208
|
+
return signal;
|
|
488
209
|
}
|
|
489
|
-
/**
|
|
490
|
-
function
|
|
491
|
-
|
|
492
|
-
|
|
210
|
+
/** Calls `callback` with the reason when `signal` aborts. Returns a function that removes the listener. The callback is an entry point, so it runs untracked. */
|
|
211
|
+
function onAbort(signal, callback) {
|
|
212
|
+
if (!signal) return () => {};
|
|
213
|
+
const listener = () => untracked(() => callback(signal.reason));
|
|
214
|
+
signal.addEventListener("abort", listener, { once: true });
|
|
215
|
+
return () => signal.removeEventListener("abort", listener);
|
|
493
216
|
}
|
|
494
|
-
// src/graph/resolvers/fallback.ts
|
|
495
217
|
/**
|
|
496
|
-
*
|
|
497
|
-
*
|
|
498
|
-
*
|
|
218
|
+
* Waits for `promise` until `signal` aborts. At the abort, it rejects with the result of `reasonOf`, and nothing waits for `promise` any more.
|
|
219
|
+
*
|
|
220
|
+
* A rejection of `promise` after the abort is not reported. The listener goes when the wait settles.
|
|
499
221
|
*/
|
|
500
|
-
function
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
if (resolution.kind === "missing") continue;
|
|
506
|
-
if (resolution.kind !== "resolved") {
|
|
507
|
-
firstUnresolved ??= resolution;
|
|
508
|
-
continue;
|
|
509
|
-
}
|
|
510
|
-
if (isAvailable(resolution.provider)) return resolution;
|
|
511
|
-
firstServing ??= resolution;
|
|
222
|
+
function abortable(promise, signal, reasonOf = (reason) => reason) {
|
|
223
|
+
if (!signal) return promise;
|
|
224
|
+
if (signal.aborted) {
|
|
225
|
+
promise.catch(() => {});
|
|
226
|
+
return Promise.reject(reasonOf(signal.reason));
|
|
512
227
|
}
|
|
513
|
-
return
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
ready: 0,
|
|
518
|
-
degraded: 1,
|
|
519
|
-
unavailable: 2
|
|
520
|
-
};
|
|
521
|
-
/** Tells whether `a` is worse than `b`. The order is `ready`, then `degraded`, then `unavailable`. */
|
|
522
|
-
function worseThan(a, b) {
|
|
523
|
-
return statusRank[a] > statusRank[b];
|
|
524
|
-
}
|
|
525
|
-
function sameCause(a, b) {
|
|
526
|
-
if (a === b) return true;
|
|
527
|
-
if (!a || !b) return false;
|
|
528
|
-
return a.key === b.key && a.owner === b.owner && a.status === b.status && a.reason === b.reason && sameCause(a.cause, b.cause);
|
|
529
|
-
}
|
|
530
|
-
/** Tells whether two status values have the same status, reason, and cause. */
|
|
531
|
-
function sameStatus(a, b) {
|
|
532
|
-
if (a === b) return true;
|
|
533
|
-
if (!a || !b) return false;
|
|
534
|
-
return a.status === b.status && a.reason === b.reason && sameCause(a.cause, b.cause);
|
|
535
|
-
}
|
|
536
|
-
function statusFields(status) {
|
|
537
|
-
return withoutUndefined({
|
|
538
|
-
reason: status.reason,
|
|
539
|
-
cause: status.cause
|
|
540
|
-
});
|
|
541
|
-
}
|
|
542
|
-
/** The cause that a dependent reports when `provider` makes its effective status worse. */
|
|
543
|
-
function causeOf(provider, status) {
|
|
544
|
-
return Object.freeze({
|
|
545
|
-
key: provider.name,
|
|
546
|
-
owner: provider.container.owner,
|
|
547
|
-
status: status.status,
|
|
548
|
-
...withoutUndefined({
|
|
549
|
-
reason: status.reason,
|
|
550
|
-
cause: status.cause
|
|
551
|
-
})
|
|
552
|
-
});
|
|
553
|
-
}
|
|
554
|
-
/**
|
|
555
|
-
* Creates the effective status of a service: the worst of the own status and the effective status of each required dependency.
|
|
556
|
-
* An optional dependency does not count. It is `undefined` outside `running`.
|
|
557
|
-
*/
|
|
558
|
-
function createEffectiveStatus(input) {
|
|
559
|
-
const { phase, instance, edges, resolutions } = input;
|
|
560
|
-
return computed((previous) => {
|
|
561
|
-
const current = instance();
|
|
562
|
-
if (phase() !== "running" || !current) return void 0;
|
|
563
|
-
let result = current.signals.own();
|
|
564
|
-
for (const edge of edges) {
|
|
565
|
-
if (edge.optional) continue;
|
|
566
|
-
const resolution = resolutions.get(edge.name)?.();
|
|
567
|
-
if (resolution?.kind !== "resolved") continue;
|
|
568
|
-
const dependency = resolution.provider.signals.effective();
|
|
569
|
-
if (dependency && worseThan(dependency.status, result.status)) result = {
|
|
570
|
-
status: dependency.status,
|
|
571
|
-
cause: causeOf(resolution.provider, dependency)
|
|
572
|
-
};
|
|
573
|
-
}
|
|
574
|
-
return previous && sameStatus(previous, result) ? previous : result;
|
|
575
|
-
});
|
|
576
|
-
}
|
|
577
|
-
/** Reports a change of the effective status. The status effect of each service calls it. */
|
|
578
|
-
function reportStatus(service, next) {
|
|
579
|
-
const previous = service.reportedStatus;
|
|
580
|
-
service.reportedStatus = next;
|
|
581
|
-
if (!next || sameStatus(previous, next)) return;
|
|
582
|
-
emitRecord(service.container, {
|
|
583
|
-
type: "status",
|
|
584
|
-
key: service.name,
|
|
585
|
-
owner: service.container.owner,
|
|
586
|
-
instanceId: service.signals.instance()?.id,
|
|
587
|
-
from: previous?.status,
|
|
588
|
-
to: next.status,
|
|
589
|
-
...statusFields(next)
|
|
590
|
-
});
|
|
591
|
-
}
|
|
592
|
-
// src/graph/nodes/service.ts
|
|
593
|
-
/** Writes the phase and sends the change to the observe stream. */
|
|
594
|
-
function setPhase(service, to, details = {}) {
|
|
595
|
-
const from = service.signals.phase();
|
|
596
|
-
if (from === to) return;
|
|
597
|
-
batch(() => {
|
|
598
|
-
service.signals.phase(to);
|
|
599
|
-
if (service.kind === "resolver") return;
|
|
600
|
-
emitRecord(service.container, {
|
|
601
|
-
type: "phase",
|
|
602
|
-
key: service.name,
|
|
603
|
-
owner: service.container.owner,
|
|
604
|
-
instanceId: service.signals.instance()?.id,
|
|
605
|
-
from,
|
|
606
|
-
to,
|
|
607
|
-
...withoutUndefined({
|
|
608
|
-
reason: details.reason,
|
|
609
|
-
decision: details.decision
|
|
610
|
-
})
|
|
228
|
+
return new Promise((resolve, reject) => {
|
|
229
|
+
const remove = onAbort(signal, (reason) => {
|
|
230
|
+
promise.catch(() => {});
|
|
231
|
+
reject(reasonOf(reason));
|
|
611
232
|
});
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
case "not-running":
|
|
619
|
-
case "rejected": return "provider" in b && a.provider === b.provider;
|
|
620
|
-
case "conflict": return b.kind === "conflict" && a.provider === b.provider && a.rivals === b.rivals;
|
|
621
|
-
case "resolved": return b.kind === "resolved" && a.provider === b.provider && a.instance === b.instance;
|
|
622
|
-
}
|
|
623
|
-
}
|
|
624
|
-
/** Creates the resolution of one declared edge. It keeps the previous value when nothing changed. */
|
|
625
|
-
function resolveEdge(consumer, edge) {
|
|
626
|
-
return computed((previous) => {
|
|
627
|
-
const next = edge.fallback ? resolveFallback(consumer, edge) : resolvePlain(consumer, edge);
|
|
628
|
-
return previous && sameResolution(previous, next) ? previous : next;
|
|
629
|
-
});
|
|
630
|
-
}
|
|
631
|
-
const emptyMetadata$2 = Object.freeze({});
|
|
632
|
-
const emptyPolicy$1 = Object.freeze({});
|
|
633
|
-
/**
|
|
634
|
-
* Creates a service with its signals, its derived values, and its effects.
|
|
635
|
-
*
|
|
636
|
-
* The main effect reads the derived values of the service and only marks the service dirty.
|
|
637
|
-
* The status effect sends each change of the effective status to the observe stream.
|
|
638
|
-
*/
|
|
639
|
-
function createService(input) {
|
|
640
|
-
const { container } = input;
|
|
641
|
-
const { runtime } = container;
|
|
642
|
-
const present = signal(true);
|
|
643
|
-
const phase = signal(input.phase ?? "pending");
|
|
644
|
-
const instance = signal(void 0);
|
|
645
|
-
const demanded = signal(input.kind === "resolver");
|
|
646
|
-
const resolutions = /* @__PURE__ */ new Map();
|
|
647
|
-
const effective = createEffectiveStatus({
|
|
648
|
-
phase,
|
|
649
|
-
instance,
|
|
650
|
-
edges: input.edges,
|
|
651
|
-
resolutions
|
|
652
|
-
});
|
|
653
|
-
let rivals = () => [];
|
|
654
|
-
const service = {
|
|
655
|
-
seq: runtime.nextSeq(),
|
|
656
|
-
kind: input.kind,
|
|
657
|
-
name: input.name,
|
|
658
|
-
container,
|
|
659
|
-
edges: input.edges,
|
|
660
|
-
build: input.build,
|
|
661
|
-
metadata: input.metadata ?? emptyMetadata$2,
|
|
662
|
-
policy: input.policy ?? emptyPolicy$1,
|
|
663
|
-
exclusive: input.exclusive ?? false,
|
|
664
|
-
perConsumer: input.perConsumer ?? false,
|
|
665
|
-
template: input.template,
|
|
666
|
-
consumer: input.consumer,
|
|
667
|
-
registration: input.registration,
|
|
668
|
-
swap: input.swap,
|
|
669
|
-
replacedBy: void 0,
|
|
670
|
-
transaction: input.transaction,
|
|
671
|
-
revision: 0,
|
|
672
|
-
signals: {
|
|
673
|
-
present,
|
|
674
|
-
phase,
|
|
675
|
-
instance,
|
|
676
|
-
demanded,
|
|
677
|
-
active: computed(() => container.signals.started() || input.kind !== "invocation" && demanded()),
|
|
678
|
-
serving: computed(() => present() && phase() === "running"),
|
|
679
|
-
resolutions,
|
|
680
|
-
effective,
|
|
681
|
-
rivals: () => rivals(),
|
|
682
|
-
consumers: signal(/* @__PURE__ */ new Map()),
|
|
683
|
-
targetLost: computed(() => !present() || !service.signals.active() || service.signals.rivals().length > 0 || requiredWithdrawn(service))
|
|
684
|
-
},
|
|
685
|
-
busy: false,
|
|
686
|
-
stopIntent: {},
|
|
687
|
-
parkedEpoch: void 0,
|
|
688
|
-
failure: void 0,
|
|
689
|
-
teardownErrors: [],
|
|
690
|
-
withdrawal: createDeferred(),
|
|
691
|
-
reportedStatus: void 0,
|
|
692
|
-
disposeEffects: () => {}
|
|
693
|
-
};
|
|
694
|
-
for (const edge of input.edges) resolutions.set(edge.name, resolveEdge(service, edge));
|
|
695
|
-
rivals = createRivals(service);
|
|
696
|
-
const stopMain = withOwner(container.reactiveOwner, () => effect(() => {
|
|
697
|
-
present();
|
|
698
|
-
phase();
|
|
699
|
-
service.signals.active();
|
|
700
|
-
service.signals.rivals();
|
|
701
|
-
instance()?.signals.holders();
|
|
702
|
-
service.swap?.target.signals.present();
|
|
703
|
-
for (const resolution of resolutions.values()) {
|
|
704
|
-
const current = resolution();
|
|
705
|
-
if (current.kind === "not-running") current.provider.signals.phase();
|
|
706
|
-
if (current.kind === "resolved") current.provider.signals.effective();
|
|
707
|
-
}
|
|
708
|
-
runtime.reconciler.markDirty(service);
|
|
709
|
-
}));
|
|
710
|
-
const stopStatus = input.kind === "resolver" ? () => {} : withOwner(container.reactiveOwner, () => effect(() => {
|
|
711
|
-
const status = effective();
|
|
712
|
-
untracked(() => reportStatus(service, status));
|
|
713
|
-
}));
|
|
714
|
-
service.disposeEffects = () => {
|
|
715
|
-
stopMain();
|
|
716
|
-
stopStatus();
|
|
717
|
-
};
|
|
718
|
-
return service;
|
|
719
|
-
}
|
|
720
|
-
// src/graph/nodes/consumer.ts
|
|
721
|
-
/**
|
|
722
|
-
* Returns the service of a `perConsumer` registration for one consumer container. It creates the service on first use.
|
|
723
|
-
*
|
|
724
|
-
* The service lives in the consumer container, so it disposes with that container. Its build context has the owner of the container as `consumer`.
|
|
725
|
-
* It returns `undefined` when the registration is withdrawn or the container disposal started.
|
|
726
|
-
*/
|
|
727
|
-
function ensureConsumerService(registration, container) {
|
|
728
|
-
const existing = registration.signals.consumers().get(container);
|
|
729
|
-
if (existing) return existing;
|
|
730
|
-
if (!registration.signals.present() || container.state !== "active") return void 0;
|
|
731
|
-
let service;
|
|
732
|
-
batch(() => {
|
|
733
|
-
service = createService({
|
|
734
|
-
container,
|
|
735
|
-
kind: "service",
|
|
736
|
-
name: registration.name,
|
|
737
|
-
edges: registration.edges,
|
|
738
|
-
build: registration.build,
|
|
739
|
-
metadata: registration.metadata,
|
|
740
|
-
policy: registration.policy,
|
|
741
|
-
exclusive: registration.exclusive,
|
|
742
|
-
template: registration,
|
|
743
|
-
consumer: container.owner
|
|
233
|
+
promise.then((value) => {
|
|
234
|
+
remove();
|
|
235
|
+
resolve(value);
|
|
236
|
+
}, (error) => {
|
|
237
|
+
remove();
|
|
238
|
+
reject(error);
|
|
744
239
|
});
|
|
745
|
-
container.services.add(service);
|
|
746
|
-
publishService(service);
|
|
747
240
|
});
|
|
748
|
-
return service;
|
|
749
|
-
}
|
|
750
|
-
// src/types/keys.ts
|
|
751
|
-
const optionalBrand = Symbol("injeca.optional");
|
|
752
|
-
const fallbackBrand = Symbol("injeca.fallback");
|
|
753
|
-
const conditionBrand = Symbol("injeca.condition");
|
|
754
|
-
// src/primitives/keys.ts
|
|
755
|
-
function assertName(name) {
|
|
756
|
-
if (typeof name !== "string" || name.length === 0 || name.trim() !== name) throw new InvalidArgumentError(`A key name must be a non-empty string without outer whitespace. Got ${JSON.stringify(name)}.`);
|
|
757
|
-
}
|
|
758
|
-
function readKeyOptions(options) {
|
|
759
|
-
if (options === void 0) return false;
|
|
760
|
-
if (typeof options !== "object" || options === null) throw new InvalidArgumentError("The options of defineKey must be an object.");
|
|
761
|
-
let exclusive;
|
|
762
|
-
try {
|
|
763
|
-
exclusive = options.exclusive;
|
|
764
|
-
} catch (error) {
|
|
765
|
-
throw new InvalidArgumentError("Cannot read the options of defineKey.", { cause: error });
|
|
766
|
-
}
|
|
767
|
-
if (exclusive !== void 0 && typeof exclusive !== "boolean") throw new InvalidArgumentError("The exclusive option of defineKey must be a boolean.");
|
|
768
|
-
return exclusive === true;
|
|
769
|
-
}
|
|
770
|
-
/**
|
|
771
|
-
* Creates a typed key without a provider. A consumer can depend on it now. `provide(key, ...)` registers a provider later.
|
|
772
|
-
*
|
|
773
|
-
* With `options.exclusive`, the key can have only one provider in its resolution area. Two or more providers all go to `conflict`.
|
|
774
|
-
*/
|
|
775
|
-
function defineKey(name, options) {
|
|
776
|
-
assertName(name);
|
|
777
|
-
return readKeyOptions(options) ? Object.freeze({
|
|
778
|
-
key: name,
|
|
779
|
-
exclusive: true
|
|
780
|
-
}) : Object.freeze({ key: name });
|
|
781
|
-
}
|
|
782
|
-
/** Tells whether a key from `provide` or `defineKey` is exclusive. A name is not. Reading a caller-owned property happens once, inside a narrow `try`. */
|
|
783
|
-
function isExclusiveKey(key) {
|
|
784
|
-
if (typeof key !== "object" || key === null) return false;
|
|
785
|
-
try {
|
|
786
|
-
return key.exclusive === true;
|
|
787
|
-
} catch (error) {
|
|
788
|
-
throw new InvalidArgumentError("Cannot read the exclusive property of the key.", { cause: error });
|
|
789
|
-
}
|
|
790
|
-
}
|
|
791
|
-
function normalizeName(nameOrProvidedKey) {
|
|
792
|
-
if (typeof nameOrProvidedKey === "object" && nameOrProvidedKey !== null && "key" in nameOrProvidedKey) return nameOrProvidedKey.key;
|
|
793
|
-
return nameOrProvidedKey;
|
|
794
|
-
}
|
|
795
|
-
const optionalMarkers = /* @__PURE__ */ new WeakSet();
|
|
796
|
-
const fallbackMarkers = /* @__PURE__ */ new WeakSet();
|
|
797
|
-
const conditionMarkers = /* @__PURE__ */ new WeakSet();
|
|
798
|
-
function isConditionalDependency(value) {
|
|
799
|
-
return typeof value === "object" && value !== null && conditionMarkers.has(value);
|
|
800
|
-
}
|
|
801
|
-
function isFallbackDependency(value) {
|
|
802
|
-
return typeof value === "object" && value !== null && fallbackMarkers.has(value);
|
|
803
241
|
}
|
|
804
242
|
/**
|
|
805
|
-
*
|
|
243
|
+
* Waits for `promise` until `signal` aborts. An aborted signal gives `aborted` at once.
|
|
806
244
|
*
|
|
807
|
-
* A
|
|
808
|
-
* It is required, unless it is wrapped in `optional()`.
|
|
809
|
-
*/
|
|
810
|
-
function firstAvailable(...keys) {
|
|
811
|
-
if (keys.length === 0) throw new InvalidArgumentError("firstAvailable() needs at least one key.");
|
|
812
|
-
const dependencies = keys.map((key) => {
|
|
813
|
-
const name = normalizeName(key);
|
|
814
|
-
assertName(name);
|
|
815
|
-
return name;
|
|
816
|
-
});
|
|
817
|
-
if (new Set(dependencies).size !== dependencies.length) throw new InvalidArgumentError(`firstAvailable() got the same key more than once: ${dependencies.join(", ")}.`);
|
|
818
|
-
const marker = Object.freeze({
|
|
819
|
-
[fallbackBrand]: true,
|
|
820
|
-
dependencies: Object.freeze(dependencies)
|
|
821
|
-
});
|
|
822
|
-
fallbackMarkers.add(marker);
|
|
823
|
-
return marker;
|
|
824
|
-
}
|
|
825
|
-
function optionalMarker(dependencies, condition) {
|
|
826
|
-
const marker = Object.freeze({
|
|
827
|
-
[optionalBrand]: true,
|
|
828
|
-
dependencies,
|
|
829
|
-
...condition ? { condition } : {}
|
|
830
|
-
});
|
|
831
|
-
optionalMarkers.add(marker);
|
|
832
|
-
return marker;
|
|
833
|
-
}
|
|
834
|
-
/**
|
|
835
|
-
* Marks an optional dependency. The dependent gets `{ value, status }`, or `undefined` when no provider runs.
|
|
836
|
-
* It also accepts a `firstAvailable` marker and a `where` marker.
|
|
837
|
-
*/
|
|
838
|
-
function optional(dependency) {
|
|
839
|
-
if (isFallbackDependency(dependency)) return optionalMarker(dependency.dependencies, void 0);
|
|
840
|
-
if (isConditionalDependency(dependency)) return optionalMarker(dependency.dependencies, dependency.condition);
|
|
841
|
-
const name = normalizeName(dependency);
|
|
842
|
-
assertName(name);
|
|
843
|
-
return optionalMarker(Object.freeze([name]), void 0);
|
|
844
|
-
}
|
|
845
|
-
function where(dependency, condition) {
|
|
846
|
-
if (typeof condition !== "function") throw new InvalidArgumentError("The condition of where() is not a function.");
|
|
847
|
-
if (isOptionalDependency(dependency)) {
|
|
848
|
-
if (dependency.condition) throw new InvalidArgumentError("The optional dependency already has a condition.");
|
|
849
|
-
return optionalMarker(dependency.dependencies, condition);
|
|
850
|
-
}
|
|
851
|
-
if (isFallbackDependency(dependency) || isConditionalDependency(dependency)) throw new InvalidArgumentError("where() accepts a key, a name, or an optional() marker.");
|
|
852
|
-
const name = normalizeName(dependency);
|
|
853
|
-
assertName(name);
|
|
854
|
-
const marker = Object.freeze({
|
|
855
|
-
[conditionBrand]: true,
|
|
856
|
-
dependencies: Object.freeze([name]),
|
|
857
|
-
condition
|
|
858
|
-
});
|
|
859
|
-
conditionMarkers.add(marker);
|
|
860
|
-
return marker;
|
|
861
|
-
}
|
|
862
|
-
function isOptionalDependency(value) {
|
|
863
|
-
return typeof value === "object" && value !== null && optionalMarkers.has(value);
|
|
864
|
-
}
|
|
865
|
-
// src/graph/nodes/edge.ts
|
|
866
|
-
function edgeOf(name, candidates, optional, fallback, condition) {
|
|
867
|
-
return Object.freeze({
|
|
868
|
-
name,
|
|
869
|
-
keyName: candidates.join(" | "),
|
|
870
|
-
candidates,
|
|
871
|
-
optional,
|
|
872
|
-
fallback,
|
|
873
|
-
...condition ? { condition } : {}
|
|
874
|
-
});
|
|
875
|
-
}
|
|
876
|
-
/**
|
|
877
|
-
* Reads a `dependsOn` declaration into edges. Reading a caller-owned property happens once, inside a narrow `try`.
|
|
245
|
+
* A rejection of `promise` before the abort rejects the result. After the abort, nothing waits for `promise`, and its rejection is not reported.
|
|
878
246
|
*/
|
|
879
|
-
function
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
} catch (error) {
|
|
886
|
-
throw new InvalidArgumentError(`Cannot read the dependsOn option of '${owner}'.`, { cause: error });
|
|
887
|
-
}
|
|
888
|
-
return Object.freeze(entries.map(([name, dependency]) => {
|
|
889
|
-
if (isOptionalDependency(dependency)) return edgeOf(name, dependency.dependencies, true, dependency.dependencies.length > 1, dependency.condition);
|
|
890
|
-
if (isConditionalDependency(dependency)) return edgeOf(name, dependency.dependencies, false, false, dependency.condition);
|
|
891
|
-
if (isFallbackDependency(dependency)) return edgeOf(name, dependency.dependencies, false, true);
|
|
892
|
-
if (typeof dependency === "string" || typeof dependency === "object" && dependency !== null && "key" in dependency) {
|
|
893
|
-
const keyName = normalizeName(dependency);
|
|
894
|
-
if (typeof keyName === "string" && keyName.length > 0) return edgeOf(name, Object.freeze([keyName]), false, false);
|
|
247
|
+
function raceAbort(promise, signal) {
|
|
248
|
+
return new Promise((resolve, reject) => {
|
|
249
|
+
if (signal.aborted) {
|
|
250
|
+
promise.catch(() => {});
|
|
251
|
+
resolve({ kind: "aborted" });
|
|
252
|
+
return;
|
|
895
253
|
}
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
}
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
function readTimeouts(owner, value) {
|
|
910
|
-
if (value === void 0) return void 0;
|
|
911
|
-
if (typeof value !== "object" || value === null) throw new InvalidArgumentError(`The timeouts option of ${owner} must be an object.`);
|
|
912
|
-
let fields;
|
|
913
|
-
try {
|
|
914
|
-
const source = value;
|
|
915
|
-
fields = {
|
|
916
|
-
start: source.start,
|
|
917
|
-
stop: source.stop
|
|
918
|
-
};
|
|
919
|
-
} catch (error) {
|
|
920
|
-
throw new InvalidArgumentError(`Cannot read the timeouts option of ${owner}.`, { cause: error });
|
|
921
|
-
}
|
|
922
|
-
const start = readTimeout$1(owner, "start", fields.start);
|
|
923
|
-
const stop = readTimeout$1(owner, "stop", fields.stop);
|
|
924
|
-
return Object.freeze({
|
|
925
|
-
...start === void 0 ? {} : { start },
|
|
926
|
-
...stop === void 0 ? {} : { stop }
|
|
927
|
-
});
|
|
928
|
-
}
|
|
929
|
-
/**
|
|
930
|
-
* Checks the retry and timeout options that a caller gave. `owner` names the caller in the messages, for example `'voice.speech'`.
|
|
931
|
-
* A field that is `undefined` takes the value of `inherit`, so that a replacement keeps it.
|
|
932
|
-
*/
|
|
933
|
-
function readPolicy(owner, fields, inherit) {
|
|
934
|
-
const maxAttempts = readMaxAttempts(owner, fields.maxAttempts) ?? inherit?.maxAttempts;
|
|
935
|
-
const timeouts = readTimeouts(owner, fields.timeouts) ?? inherit?.timeouts;
|
|
936
|
-
if (fields.onTimeout !== void 0 && typeof fields.onTimeout !== "function") throw new InvalidArgumentError(`The onTimeout option of ${owner} must be a function.`);
|
|
937
|
-
const onTimeout = fields.onTimeout ?? inherit?.onTimeout;
|
|
938
|
-
if (maxAttempts === void 0 && timeouts === void 0 && onTimeout === void 0) return emptyPolicy;
|
|
939
|
-
return Object.freeze({
|
|
940
|
-
...maxAttempts === void 0 ? {} : { maxAttempts },
|
|
941
|
-
...timeouts === void 0 ? {} : { timeouts },
|
|
942
|
-
...onTimeout === void 0 ? {} : { onTimeout }
|
|
943
|
-
});
|
|
944
|
-
}
|
|
945
|
-
/** The value of the nearest container, from `container` up to the root, that sets it. */
|
|
946
|
-
function fromContainers(container, pick) {
|
|
947
|
-
for (let current = container; current; current = current.parent) {
|
|
948
|
-
const value = pick(current.policy);
|
|
949
|
-
if (value !== void 0) return value;
|
|
950
|
-
}
|
|
951
|
-
}
|
|
952
|
-
/** The attempt limit of a requeue: the decision, then the service, then the containers. `undefined` means no limit. */
|
|
953
|
-
function maxAttemptsOf(service, fromDecision) {
|
|
954
|
-
return fromDecision ?? service.policy.maxAttempts ?? fromContainers(service.container, (policy) => policy.maxAttempts);
|
|
955
|
-
}
|
|
956
|
-
/** The timeout of a hook: the service, then the containers. `undefined` means no timeout. */
|
|
957
|
-
function timeoutOf(service, hook) {
|
|
958
|
-
return service.policy.timeouts?.[hook] ?? fromContainers(service.container, (policy) => policy.timeouts?.[hook]);
|
|
959
|
-
}
|
|
960
|
-
/** The `onTimeout` handler: the service, then the containers. */
|
|
961
|
-
function timeoutHandlerOf(service) {
|
|
962
|
-
return service.policy.onTimeout ?? fromContainers(service.container, (policy) => policy.onTimeout);
|
|
963
|
-
}
|
|
964
|
-
// src/graph/nodes/definition.ts
|
|
965
|
-
const emptyMetadata$1 = Object.freeze({});
|
|
966
|
-
function readMetadata(name, metadata, fallback) {
|
|
967
|
-
if (metadata === void 0) return fallback ?? emptyMetadata$1;
|
|
968
|
-
if (typeof metadata !== "object" || metadata === null || Array.isArray(metadata)) throw new InvalidArgumentError(`The metadata option of '${name}' must be a record.`);
|
|
969
|
-
try {
|
|
970
|
-
return Object.freeze({ ...metadata });
|
|
971
|
-
} catch (error) {
|
|
972
|
-
throw new InvalidArgumentError(`Cannot read the metadata option of '${name}'.`, { cause: error });
|
|
973
|
-
}
|
|
974
|
-
}
|
|
975
|
-
/**
|
|
976
|
-
* Reads the options of one registration once: the function shorthand, or the object form.
|
|
977
|
-
* Reading caller-owned properties happens inside a narrow `try`. `inherit` gives the metadata and each policy field when the options have none.
|
|
978
|
-
*/
|
|
979
|
-
function readDefinition(name, option, inherit) {
|
|
980
|
-
let build;
|
|
981
|
-
let dependsOn;
|
|
982
|
-
let metadata;
|
|
983
|
-
let perConsumer;
|
|
984
|
-
let policy;
|
|
985
|
-
try {
|
|
986
|
-
const object = typeof option === "function" ? { build: option } : typeof option === "object" && option !== null ? option : {};
|
|
987
|
-
build = object.build;
|
|
988
|
-
dependsOn = object.dependsOn;
|
|
989
|
-
metadata = object.metadata;
|
|
990
|
-
perConsumer = object.perConsumer;
|
|
991
|
-
policy = {
|
|
992
|
-
maxAttempts: object.maxAttempts,
|
|
993
|
-
timeouts: object.timeouts,
|
|
994
|
-
onTimeout: object.onTimeout
|
|
995
|
-
};
|
|
996
|
-
} catch (error) {
|
|
997
|
-
throw new InvalidArgumentError(`Cannot read the provide options of '${name}'.`, { cause: error });
|
|
998
|
-
}
|
|
999
|
-
if (typeof build !== "function") throw new InvalidArgumentError(`The build option of '${name}' is not a function.`);
|
|
1000
|
-
if (perConsumer !== void 0 && typeof perConsumer !== "boolean") throw new InvalidArgumentError(`The perConsumer option of '${name}' must be a boolean.`);
|
|
1001
|
-
return {
|
|
1002
|
-
edges: readEdges(name, dependsOn),
|
|
1003
|
-
build,
|
|
1004
|
-
metadata: readMetadata(name, metadata, inherit?.metadata),
|
|
1005
|
-
perConsumer: perConsumer === true,
|
|
1006
|
-
policy: readPolicy(`'${name}'`, policy, inherit?.policy)
|
|
1007
|
-
};
|
|
1008
|
-
}
|
|
1009
|
-
// src/graph/nodes/registration.ts
|
|
1010
|
-
function createRegistration(container, name, exclusive) {
|
|
1011
|
-
return {
|
|
1012
|
-
container,
|
|
1013
|
-
name,
|
|
1014
|
-
exclusive,
|
|
1015
|
-
current: void 0,
|
|
1016
|
-
pending: void 0
|
|
1017
|
-
};
|
|
1018
|
-
}
|
|
1019
|
-
/**
|
|
1020
|
-
* The published service of a registration that is still current, or `undefined` for a stale registration.
|
|
1021
|
-
* A registration is stale after its own withdrawal, or after a newer `provide` of the same key in the same container.
|
|
1022
|
-
*/
|
|
1023
|
-
function currentService(registration) {
|
|
1024
|
-
const service = registration.current;
|
|
1025
|
-
return service?.signals.present() ? service : void 0;
|
|
1026
|
-
}
|
|
1027
|
-
/** Cancels the replacement that waits for its swap. The old service does not change. */
|
|
1028
|
-
function cancelPending(registration) {
|
|
1029
|
-
const pending = registration.pending;
|
|
1030
|
-
if (!pending) return;
|
|
1031
|
-
registration.pending = void 0;
|
|
1032
|
-
pending.swap?.settle({ kind: "cancelled" });
|
|
1033
|
-
releaseService(pending);
|
|
1034
|
-
}
|
|
1035
|
-
/** The `withdraw()` of a registration. Resolves `false` for a stale registration. Rejects with the teardown errors. */
|
|
1036
|
-
async function withdrawRegistration(registration) {
|
|
1037
|
-
const service = untracked(() => {
|
|
1038
|
-
const current = currentService(registration);
|
|
1039
|
-
if (current) batch(() => {
|
|
1040
|
-
cancelPending(registration);
|
|
1041
|
-
releaseService(current);
|
|
254
|
+
const remove = onAbort(signal, () => {
|
|
255
|
+
promise.catch(() => {});
|
|
256
|
+
resolve({ kind: "aborted" });
|
|
257
|
+
});
|
|
258
|
+
promise.then((value) => {
|
|
259
|
+
remove();
|
|
260
|
+
resolve({
|
|
261
|
+
kind: "settled",
|
|
262
|
+
value
|
|
263
|
+
});
|
|
264
|
+
}, (error) => {
|
|
265
|
+
remove();
|
|
266
|
+
reject(error);
|
|
1042
267
|
});
|
|
1043
|
-
return current;
|
|
1044
268
|
});
|
|
1045
|
-
if (!service) return false;
|
|
1046
|
-
await awaitWithdrawal(service);
|
|
1047
|
-
return true;
|
|
1048
269
|
}
|
|
1049
270
|
// src/helpers/cleanup-context.ts
|
|
1050
271
|
function activeIn(frame, owner) {
|
|
@@ -1070,14 +291,6 @@ function createSynchronousContext() {
|
|
|
1070
291
|
function createCleanupContext() {
|
|
1071
292
|
return createSynchronousContext();
|
|
1072
293
|
}
|
|
1073
|
-
// src/helpers/clock.ts
|
|
1074
|
-
const systemClock = Object.freeze({
|
|
1075
|
-
now: () => Date.now(),
|
|
1076
|
-
setTimeout: (callback, ms) => {
|
|
1077
|
-
const handle = globalThis.setTimeout(callback, ms);
|
|
1078
|
-
return () => globalThis.clearTimeout(handle);
|
|
1079
|
-
}
|
|
1080
|
-
});
|
|
1081
294
|
// src/helpers/delivery-queue.ts
|
|
1082
295
|
function createDeliveryQueue() {
|
|
1083
296
|
const queue = [];
|
|
@@ -1248,9 +461,13 @@ function invokeUser(fn) {
|
|
|
1248
461
|
return Promise.reject(error);
|
|
1249
462
|
}
|
|
1250
463
|
}
|
|
1251
|
-
/**
|
|
464
|
+
/** The logger of the lifecycle trace of `container`, or `undefined` when its `logging` option is off. `warn` does not use it. */
|
|
465
|
+
function traceLoggerOf(container) {
|
|
466
|
+
return container.logging ? container.logger : void 0;
|
|
467
|
+
}
|
|
468
|
+
/** Only services with a key write the lifecycle trace. Invocations and resolvers do not. */
|
|
1252
469
|
function loggerOf(service) {
|
|
1253
|
-
return service.kind === "service" ? service.container.api
|
|
470
|
+
return service.kind === "service" ? traceLoggerOf(service.container.api) : void 0;
|
|
1254
471
|
}
|
|
1255
472
|
function callListeners(listeners, ...args) {
|
|
1256
473
|
for (const listener of [...listeners]) try {
|
|
@@ -1261,6 +478,19 @@ function callListeners(listeners, ...args) {
|
|
|
1261
478
|
});
|
|
1262
479
|
}
|
|
1263
480
|
}
|
|
481
|
+
// src/reconciler/demand.ts
|
|
482
|
+
/**
|
|
483
|
+
* An active service makes the providers of its edges converge too, before `start()`. A fallback edge demands each candidate.
|
|
484
|
+
* A `perConsumer` registration gets a service for the scope of `service`. The nearest registration of each key is demanded.
|
|
485
|
+
*
|
|
486
|
+
* A pending service demands its providers before it builds. A running service demands them too, so that an optional or a fallback edge to a key that registers later gets its consumer service.
|
|
487
|
+
*/
|
|
488
|
+
function demandProviders(service) {
|
|
489
|
+
for (const edge of service.edges) for (const keyName of edge.candidates) {
|
|
490
|
+
const [nearest] = candidatesOf(service.scope, keyName, service).map((registration) => registration.perConsumer ? ensureConsumerService(registration, service.scope, service) : registration);
|
|
491
|
+
if (nearest && !nearest.signals.serving() && !nearest.signals.demanded()) nearest.signals.demanded(true);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
1264
494
|
// src/reconciler/holds.ts
|
|
1265
495
|
function addHolder(provider, holder) {
|
|
1266
496
|
const holders = provider.signals.holders();
|
|
@@ -1286,6 +516,115 @@ function releaseExtraHolds(service, instance) {
|
|
|
1286
516
|
instance.extraHolds.clear();
|
|
1287
517
|
batch(() => syncHolds(service, instance));
|
|
1288
518
|
}
|
|
519
|
+
// src/reconciler/reentry.ts
|
|
520
|
+
/** The instance of each signal that injeca gives to a run: the build signal of an instance, and the signal of each hook run. */
|
|
521
|
+
const signalOrigins = /* @__PURE__ */ new WeakMap();
|
|
522
|
+
/** The service of each cleanup frame of a teardown. */
|
|
523
|
+
const frameServices = /* @__PURE__ */ new WeakMap();
|
|
524
|
+
/** Records that `signal` belongs to the runs of `instance`. A signal from `anySignal` or `withTimeout` on top of it keeps this origin. */
|
|
525
|
+
function recordSignalOrigin(signal, instance) {
|
|
526
|
+
signalOrigins.set(signal, instance);
|
|
527
|
+
}
|
|
528
|
+
/** Creates the cleanup frame of a teardown of `service`, inside `parent`. The teardown sets `active` to `false` when it ends. */
|
|
529
|
+
function serviceFrame(service, parent) {
|
|
530
|
+
const frame = {
|
|
531
|
+
owner: service,
|
|
532
|
+
parent,
|
|
533
|
+
active: true
|
|
534
|
+
};
|
|
535
|
+
frameServices.set(frame, service);
|
|
536
|
+
return frame;
|
|
537
|
+
}
|
|
538
|
+
function within(container, ancestor) {
|
|
539
|
+
for (let current = container; current; current = current.parent) if (current === ancestor) return true;
|
|
540
|
+
return false;
|
|
541
|
+
}
|
|
542
|
+
/** Finds a cleanup that `covers` names, by the origin of `signal`, then by the synchronous cleanup frames. The instance of a signal counts only while its teardown runs. */
|
|
543
|
+
function findCleanup(context, signal, covers) {
|
|
544
|
+
if (signal) for (const source of signalSources(signal)) {
|
|
545
|
+
const instance = signalOrigins.get(source);
|
|
546
|
+
if (instance && instance.tearingDown && !instance.disposed && covers(instance.service, instance)) return {
|
|
547
|
+
service: instance.service,
|
|
548
|
+
instance
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
for (let frame = context.current(); frame; frame = frame.parent) {
|
|
552
|
+
const service = frame.active ? frameServices.get(frame) : void 0;
|
|
553
|
+
if (service && covers(service, service.signals.instance())) return { service };
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
/**
|
|
557
|
+
* Tells whether the teardown of `instance` is part of a stop that waits for each service that `stops` names.
|
|
558
|
+
* It is when the service of `instance` is such a service, or when `instance` holds an instance of one, directly or through other instances.
|
|
559
|
+
* The teardown of a held instance waits until its holders release it, so it waits for `instance` too.
|
|
560
|
+
*/
|
|
561
|
+
function waitedFor(instance, stops) {
|
|
562
|
+
const seen = /* @__PURE__ */ new Set([instance]);
|
|
563
|
+
const queue = [instance];
|
|
564
|
+
for (let index = 0; index < queue.length; index++) {
|
|
565
|
+
const current = queue[index];
|
|
566
|
+
if (stops(current.service)) return true;
|
|
567
|
+
for (const provider of current.held) if (!seen.has(provider)) {
|
|
568
|
+
seen.add(provider);
|
|
569
|
+
queue.push(provider);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
return false;
|
|
573
|
+
}
|
|
574
|
+
/** Finds a cleanup whose teardown the wait for the services that `stops` names includes. */
|
|
575
|
+
function findWaitedCleanup(context, signal, stops) {
|
|
576
|
+
return findCleanup(context, signal, (service, instance) => stops(service) || instance !== void 0 && waitedFor(instance, stops));
|
|
577
|
+
}
|
|
578
|
+
/**
|
|
579
|
+
* Finds out whether a `stop()` or `dispose()` of `target` comes from a cleanup that the call waits for. Such a call cannot wait, because the wait includes the caller.
|
|
580
|
+
*
|
|
581
|
+
* - The cleanup is of a service of the subtree, or of a container effect of the subtree.
|
|
582
|
+
* - Or the cleanup is of a service outside the subtree that holds a service that the call stops, directly or through other instances. That service waits for its holders.
|
|
583
|
+
* `dispose()` also withdraws the consumer services of each `perConsumer` registration of the subtree, so they count for it.
|
|
584
|
+
* - With `signal`: the signal, or a signal that it comes from, belongs to such an instance whose teardown runs.
|
|
585
|
+
* The check uses the instance, not the phase of its service. So the teardown of a successor whose build failed, or of an old instance during a rebuild, counts too.
|
|
586
|
+
* - Without it: the synchronous part of such a teardown makes the call.
|
|
587
|
+
*
|
|
588
|
+
* A call without a signal after an `await` is not found. A configured `stop` timeout ends that cleanup.
|
|
589
|
+
*/
|
|
590
|
+
function findReentry(target, call, signal) {
|
|
591
|
+
const { context } = target.runtime;
|
|
592
|
+
const found = findWaitedCleanup(context, signal, call === "dispose" ? (service) => within(service.container, target) || service.template !== void 0 && within(service.template.container, target) : (service) => within(service.container, target));
|
|
593
|
+
if (found) return found;
|
|
594
|
+
return context.isActive(target) ? { service: void 0 } : void 0;
|
|
595
|
+
}
|
|
596
|
+
/**
|
|
597
|
+
* Finds out whether a `withdraw()` of `target` comes from a cleanup that the withdrawal waits for: of `target`, of a consumer service of it, or of a service that holds one of them, directly or through other instances.
|
|
598
|
+
* The rules are the same as for `findReentry`.
|
|
599
|
+
*/
|
|
600
|
+
function findWithdrawReentry(target, signal) {
|
|
601
|
+
return findWaitedCleanup(target.container.runtime.context, signal, (service) => service === target || service.template === target);
|
|
602
|
+
}
|
|
603
|
+
function describeOwner(container) {
|
|
604
|
+
return JSON.stringify(container.owner);
|
|
605
|
+
}
|
|
606
|
+
const actions = {
|
|
607
|
+
stop: "stop",
|
|
608
|
+
dispose: "disposal",
|
|
609
|
+
withdraw: "withdrawal"
|
|
610
|
+
};
|
|
611
|
+
/**
|
|
612
|
+
* Warns on the logger of `target` and sends a `reentrant-call` record. The call then resolves at once.
|
|
613
|
+
* `subject` names the target of the call. It defaults to the owner of `target`.
|
|
614
|
+
*/
|
|
615
|
+
function reportReentry(target, call, reentry, subject = describeOwner(target)) {
|
|
616
|
+
const { service } = reentry;
|
|
617
|
+
const reason = `${service ? `The cleanup of '${service.name}' in ${describeOwner(service.container)}` : `A container effect in ${describeOwner(target)}`} called ${call}() of ${subject}, and the wait includes that cleanup. injeca resolved the call at once. The ${actions[call]} continues.`;
|
|
618
|
+
target.api.logger.warn(reason);
|
|
619
|
+
emitRecord(service?.container ?? target, {
|
|
620
|
+
type: "reentrant-call",
|
|
621
|
+
key: service?.name ?? "",
|
|
622
|
+
owner: (service?.container ?? target).owner,
|
|
623
|
+
instanceId: reentry.instance?.id ?? service?.signals.instance()?.id,
|
|
624
|
+
call,
|
|
625
|
+
reason
|
|
626
|
+
});
|
|
627
|
+
}
|
|
1289
628
|
// src/reconciler/snapshot.ts
|
|
1290
629
|
/** The current snapshot of all dependencies. */
|
|
1291
630
|
function currentSnapshot(service) {
|
|
@@ -1342,37 +681,6 @@ function acceptSnapshot(service, instance, snapshot) {
|
|
|
1342
681
|
for (const [name, entry] of snapshot) if (entry) instance.bindings.set(name, entry.instance);
|
|
1343
682
|
batch(() => syncHolds(service, instance));
|
|
1344
683
|
}
|
|
1345
|
-
// src/helpers/timeout.ts
|
|
1346
|
-
/**
|
|
1347
|
-
* Waits for `promise` until `ms` milliseconds of `clock` pass.
|
|
1348
|
-
*
|
|
1349
|
-
* A rejection of `promise` before the timeout rejects the result. After the timeout, nothing waits for `promise`, and its rejection is not reported.
|
|
1350
|
-
*/
|
|
1351
|
-
function raceTimeout(clock, promise, ms) {
|
|
1352
|
-
return new Promise((resolve, reject) => {
|
|
1353
|
-
let done = false;
|
|
1354
|
-
const cancel = clock.setTimeout(() => untracked(() => {
|
|
1355
|
-
if (done) return;
|
|
1356
|
-
done = true;
|
|
1357
|
-
promise.catch(() => {});
|
|
1358
|
-
resolve({ kind: "timeout" });
|
|
1359
|
-
}), ms);
|
|
1360
|
-
promise.then((value) => {
|
|
1361
|
-
if (done) return;
|
|
1362
|
-
done = true;
|
|
1363
|
-
cancel();
|
|
1364
|
-
resolve({
|
|
1365
|
-
kind: "settled",
|
|
1366
|
-
value
|
|
1367
|
-
});
|
|
1368
|
-
}, (error) => {
|
|
1369
|
-
if (done) return;
|
|
1370
|
-
done = true;
|
|
1371
|
-
cancel();
|
|
1372
|
-
reject(error);
|
|
1373
|
-
});
|
|
1374
|
-
});
|
|
1375
|
-
}
|
|
1376
684
|
// src/reconciler/hook-run.ts
|
|
1377
685
|
function hookStateOf(instance, hook) {
|
|
1378
686
|
let state = instance.hookStates.get(hook);
|
|
@@ -1404,19 +712,34 @@ function recordDecision(service, instance, decision, ignored) {
|
|
|
1404
712
|
...withoutUndefined({ reason: decision.reason })
|
|
1405
713
|
});
|
|
1406
714
|
}
|
|
1407
|
-
/**
|
|
1408
|
-
|
|
715
|
+
/**
|
|
716
|
+
* Reports a failure of a callback that injeca stopped waiting for, after a timeout or a newer target. Nothing else gets the error, so injeca writes it with `logger.warn`.
|
|
717
|
+
*
|
|
718
|
+
* A failure with the reason of one of `signals` is not reported, because the callback only followed its abort.
|
|
719
|
+
*/
|
|
720
|
+
function reportLateFailure(service, source, error, signals) {
|
|
721
|
+
if (signals.some((signal) => signal?.aborted && signal.reason === error)) return;
|
|
722
|
+
const callback = source === "effect" ? `An effect disposer of '${service.name}'` : `The '${source}' hook of '${service.name}'`;
|
|
723
|
+
service.container.api.logger.warn(`${callback} failed after injeca stopped waiting for it: ${messageOf(error)}`);
|
|
724
|
+
}
|
|
725
|
+
/** Records the late decision of a hook run that injeca stopped waiting for. A late `fail` is reported with `reportLateFailure`. */
|
|
726
|
+
function recordLateDecision(service, instance, decision, signals) {
|
|
727
|
+
recordDecision(service, instance, decision, true);
|
|
728
|
+
if (decision.kind === "fail") reportLateFailure(service, decision.hook, decision.error, signals);
|
|
729
|
+
}
|
|
730
|
+
/**
|
|
731
|
+
* Starts a hook run. Its signal aborts when a newer target supersedes it, when the instance is disposed, or when `timeout` aborts.
|
|
732
|
+
* The signal has the reason of the first one. `end` removes the listeners.
|
|
733
|
+
*/
|
|
734
|
+
function beginHookRun(instance, timeout) {
|
|
1409
735
|
const controller = new AbortController();
|
|
1410
|
-
const
|
|
1411
|
-
|
|
1412
|
-
if (parent.aborted) forward();
|
|
1413
|
-
else parent.addEventListener("abort", forward, { once: true });
|
|
736
|
+
const release = forwardAbort(controller, timeout ? [instance.controller.signal, timeout] : [instance.controller.signal]);
|
|
737
|
+
recordSignalOrigin(controller.signal, instance);
|
|
1414
738
|
instance.hookRun = controller;
|
|
1415
739
|
return {
|
|
1416
740
|
signal: controller.signal,
|
|
1417
|
-
abort: (reason) => controller.abort(reason),
|
|
1418
741
|
end: () => {
|
|
1419
|
-
|
|
742
|
+
release();
|
|
1420
743
|
if (instance.hookRun === controller) instance.hookRun = void 0;
|
|
1421
744
|
}
|
|
1422
745
|
};
|
|
@@ -1461,7 +784,8 @@ async function runHandlers(service, instance, hook, runSignal, extras, options =
|
|
|
1461
784
|
/**
|
|
1462
785
|
* Records a timeout of a `start` or `stop` hook in the observe stream, and returns the decision of the `onTimeout` handler.
|
|
1463
786
|
*
|
|
1464
|
-
* The handler runs synchronously.
|
|
787
|
+
* The handler runs synchronously. For `start`, a value that is not a decision of this hook, a promise, or no handler gives `fail` with `hook-timeout`.
|
|
788
|
+
* For `stop`, they give `handled` with the timeout as the reason, so that the stop continues and its callers do not fail. A throw gives `fail` for both hooks.
|
|
1465
789
|
*/
|
|
1466
790
|
function decideTimeout(service, instance, hook, timeoutMs) {
|
|
1467
791
|
const state = hookStateOf(instance, hook);
|
|
@@ -1484,8 +808,9 @@ function decideTimeout(service, instance, hook, timeoutMs) {
|
|
|
1484
808
|
attempt: state.attempt,
|
|
1485
809
|
reason: error.message
|
|
1486
810
|
});
|
|
811
|
+
const fallback = () => hook === "stop" ? decide.handled({ reason: error.message }) : decide.fail(error, { reason: error.message });
|
|
1487
812
|
const handler = timeoutHandlerOf(service);
|
|
1488
|
-
if (!handler) return
|
|
813
|
+
if (!handler) return fallback();
|
|
1489
814
|
try {
|
|
1490
815
|
const value = untracked(() => handler(Object.freeze({
|
|
1491
816
|
hook,
|
|
@@ -1494,7 +819,7 @@ function decideTimeout(service, instance, hook, timeoutMs) {
|
|
|
1494
819
|
previous: state.previous,
|
|
1495
820
|
decide
|
|
1496
821
|
})));
|
|
1497
|
-
return validateDecision(run, value) ??
|
|
822
|
+
return validateDecision(run, value) ?? fallback();
|
|
1498
823
|
} catch (handlerError) {
|
|
1499
824
|
return decide.fail(handlerError, { reason: `The onTimeout handler of '${service.name}' threw.` });
|
|
1500
825
|
}
|
|
@@ -1572,29 +897,45 @@ function reconcileStopping(service) {
|
|
|
1572
897
|
if (instance.signals.holders().size > 0) return;
|
|
1573
898
|
teardown(service, instance, true);
|
|
1574
899
|
}
|
|
900
|
+
/** The warning of a `stop` timeout. During a stop or a disposal of the container, it tells the most common cause. */
|
|
901
|
+
function stopTimeoutWarning(service, timeoutMs) {
|
|
902
|
+
const warning = `The cleanups of '${service.name}' did not settle within the stop timeout of ${timeoutMs}ms. injeca stopped waiting for them, and the stop continues.`;
|
|
903
|
+
const { container } = service;
|
|
904
|
+
const action = container.state === "disposing" ? "disposing" : container.signals.started() ? void 0 : "stopping";
|
|
905
|
+
if (!action) return warning;
|
|
906
|
+
return `${warning} The container is ${action}. A cleanup can be waiting for its own container. It must pass its signal to stop() or dispose().`;
|
|
907
|
+
}
|
|
1575
908
|
/**
|
|
1576
909
|
* Runs the `stop` hooks in reverse order, then the effect disposers in reverse order.
|
|
1577
910
|
*
|
|
1578
|
-
* A configured `stop` timeout covers all of them.
|
|
1579
|
-
*
|
|
911
|
+
* A configured `stop` timeout covers all of them. It is a timeout signal on the container clock. The `stop` handlers get it in their `signal`.
|
|
912
|
+
* When it aborts, injeca records it, warns, applies the `onTimeout` decision, and stops waiting. Without `onTimeout`, the timeout counts as `handled`, so the callers of `stop()` and `dispose()` do not fail.
|
|
913
|
+
* The callback in progress is abandoned. The remaining callbacks still run, but injeca does not wait for them. So a cleanup that awaits the disposal of its own container without its signal cannot block that disposal.
|
|
914
|
+
* The late decisions of the abandoned `stop` handlers are recorded as ignored. A late failure of an abandoned callback goes to `logger.warn`, unless it is the abort reason of its signal.
|
|
1580
915
|
*/
|
|
1581
916
|
async function runTeardownCallbacks(service, instance, stopHooks, frame) {
|
|
1582
917
|
const errors = [];
|
|
1583
918
|
const { clock, context } = service.container.runtime;
|
|
1584
919
|
const call = (fn) => context.run(frame, () => invokeUser(fn));
|
|
1585
920
|
const timeoutMs = timeoutOf(service, "stop");
|
|
1586
|
-
const deadline = timeoutMs === void 0 ? void 0 : clock
|
|
921
|
+
const deadline = timeoutMs === void 0 ? void 0 : timeoutSignal(clock, timeoutMs, () => new HookTimeoutError(`The 'stop' hook of '${service.name}' timed out.`, "stop", timeoutMs));
|
|
922
|
+
const hookSignal = anySignal(instance.controller.signal, deadline?.signal);
|
|
1587
923
|
let timedOut = false;
|
|
1588
|
-
/**
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
924
|
+
/**
|
|
925
|
+
* Waits for one callback until the deadline. `undefined` means that the deadline passed.
|
|
926
|
+
* After the deadline, `late` gets the value of the callback, and a rejection is reported with `logger.warn`.
|
|
927
|
+
*/
|
|
928
|
+
const settle = async (promise, source, late) => {
|
|
929
|
+
const abandon = () => {
|
|
930
|
+
promise.then((value) => late?.(value), (error) => reportLateFailure(service, source, error, [hookSignal, instance.controller.signal]));
|
|
931
|
+
};
|
|
932
|
+
if (!deadline) return { value: await promise };
|
|
933
|
+
if (timedOut) return abandon();
|
|
934
|
+
const result = await raceAbort(promise, deadline.signal);
|
|
1596
935
|
if (result.kind === "settled") return { value: result.value };
|
|
936
|
+
abandon();
|
|
1597
937
|
timedOut = true;
|
|
938
|
+
service.container.api.logger.warn(stopTimeoutWarning(service, timeoutMs));
|
|
1598
939
|
const decision = decideTimeout(service, instance, "stop", timeoutMs);
|
|
1599
940
|
recordDecision(service, instance, decision, false);
|
|
1600
941
|
finishRun(instance, "stop", decision);
|
|
@@ -1605,7 +946,7 @@ async function runTeardownCallbacks(service, instance, stopHooks, frame) {
|
|
|
1605
946
|
logger?.hookOnStop(service.name);
|
|
1606
947
|
const startTime = performance.now();
|
|
1607
948
|
for (const handler of [...instance.hooks.stop].reverse()) {
|
|
1608
|
-
const settled = await settle(callHandler(service, instance, "stop", handler,
|
|
949
|
+
const settled = await settle(callHandler(service, instance, "stop", handler, hookSignal, {}, call), "stop", (late) => recordLateDecision(service, instance, late, [hookSignal, instance.controller.signal]));
|
|
1609
950
|
if (!settled) continue;
|
|
1610
951
|
const decision = settled.value;
|
|
1611
952
|
recordDecision(service, instance, decision, false);
|
|
@@ -1616,19 +957,22 @@ async function runTeardownCallbacks(service, instance, stopHooks, frame) {
|
|
|
1616
957
|
}
|
|
1617
958
|
const effects = instance.effects.splice(0).reverse();
|
|
1618
959
|
for (const disposer of effects) try {
|
|
1619
|
-
await settle(call(disposer));
|
|
960
|
+
await settle(call(disposer), "effect");
|
|
1620
961
|
} catch (error) {
|
|
1621
962
|
errors.push(error);
|
|
1622
963
|
}
|
|
964
|
+
deadline?.cancel();
|
|
1623
965
|
return errors;
|
|
1624
966
|
}
|
|
1625
967
|
async function teardown(service, instance, stopHooks, phase) {
|
|
1626
968
|
service.busy = true;
|
|
969
|
+
instance.tearingDown = true;
|
|
1627
970
|
cancelRequeueWait(instance);
|
|
1628
971
|
instance.extraHolds.clear();
|
|
1629
972
|
if (phase) setPhase(service, phase, { reason: service.stopIntent.reason });
|
|
1630
|
-
const frame = service.container.frame;
|
|
973
|
+
const frame = serviceFrame(service, service.container.frame);
|
|
1631
974
|
const errors = await service.container.runtime.context.run(frame, () => runTeardownCallbacks(service, instance, stopHooks, frame));
|
|
975
|
+
frame.active = false;
|
|
1632
976
|
instance.disposed = true;
|
|
1633
977
|
instance.controller.abort(/* @__PURE__ */ new Error(`The instance '${instance.id}' of '${service.name}' was disposed.`));
|
|
1634
978
|
service.busy = false;
|
|
@@ -1697,11 +1041,59 @@ function finalize(service) {
|
|
|
1697
1041
|
}
|
|
1698
1042
|
setPhase(service, "withdrawn");
|
|
1699
1043
|
});
|
|
1700
|
-
service.withdrawal.resolve([...service.teardownErrors]);
|
|
1701
|
-
if (template) service.container.runtime.reconciler.markDirty(template);
|
|
1702
|
-
service.container.runtime.reconciler.requestDrain();
|
|
1044
|
+
service.withdrawal.resolve([...service.teardownErrors]);
|
|
1045
|
+
if (template) service.container.runtime.reconciler.markDirty(template);
|
|
1046
|
+
service.container.runtime.reconciler.requestDrain();
|
|
1047
|
+
}
|
|
1048
|
+
// src/reconciler/swap.ts
|
|
1049
|
+
/**
|
|
1050
|
+
* Creates a new service that replaces `target` after it completes `start`. It is not published before the swap.
|
|
1051
|
+
* `target.successor` names it until the replacement ends. It gets the demand of `target`, so that it converges as `target` does.
|
|
1052
|
+
*/
|
|
1053
|
+
function createSuccessor(target, definition, options) {
|
|
1054
|
+
const outcome = createDeferred();
|
|
1055
|
+
let settled = false;
|
|
1056
|
+
let service;
|
|
1057
|
+
batch(() => {
|
|
1058
|
+
service = createService({
|
|
1059
|
+
container: target.container,
|
|
1060
|
+
kind: "service",
|
|
1061
|
+
name: target.name,
|
|
1062
|
+
edges: definition.edges,
|
|
1063
|
+
build: definition.build,
|
|
1064
|
+
metadata: definition.metadata,
|
|
1065
|
+
policy: definition.policy,
|
|
1066
|
+
exclusive: target.exclusive,
|
|
1067
|
+
exported: target.exported,
|
|
1068
|
+
template: options.template,
|
|
1069
|
+
consumer: target.consumer,
|
|
1070
|
+
registration: target.registration,
|
|
1071
|
+
...options.rebuild ? { seq: target.seq } : {},
|
|
1072
|
+
...options.extra,
|
|
1073
|
+
swap: {
|
|
1074
|
+
target,
|
|
1075
|
+
ready: options.ready,
|
|
1076
|
+
held: false,
|
|
1077
|
+
...options.rebuild ? { rebuild: true } : {},
|
|
1078
|
+
settle: (result) => {
|
|
1079
|
+
if (settled) return;
|
|
1080
|
+
settled = true;
|
|
1081
|
+
if (target.successor === service) target.successor = void 0;
|
|
1082
|
+
options.onSettle(service, result);
|
|
1083
|
+
outcome.resolve(result);
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
});
|
|
1087
|
+
service.signals.demanded(target.signals.demanded());
|
|
1088
|
+
service.signals.subscribers(target.signals.subscribers());
|
|
1089
|
+
target.container.services.add(service);
|
|
1090
|
+
target.successor = service;
|
|
1091
|
+
});
|
|
1092
|
+
return {
|
|
1093
|
+
service,
|
|
1094
|
+
outcome: outcome.promise
|
|
1095
|
+
};
|
|
1703
1096
|
}
|
|
1704
|
-
// src/reconciler/swap.ts
|
|
1705
1097
|
/**
|
|
1706
1098
|
* Cancels a replacement that cannot complete: the old service was withdrawn, or the container stopped.
|
|
1707
1099
|
* The new service leaves. The old service does not change.
|
|
@@ -1739,34 +1131,43 @@ function completeSwap(service) {
|
|
|
1739
1131
|
// src/reconciler/start.ts
|
|
1740
1132
|
/**
|
|
1741
1133
|
* Runs the `start` handlers of one attempt, from `instance.startFrom`, within the configured `start` timeout.
|
|
1742
|
-
*
|
|
1134
|
+
*
|
|
1135
|
+
* The run signal includes a timeout signal on the container clock. At the timeout, the run signal aborts with `hook-timeout`, later handlers do not run, and the `onTimeout` decision replaces the result.
|
|
1136
|
+
* When a newer target aborts the run, for example `stop()` or `dispose()`, injeca does not wait for the handler in progress. The service stops at once.
|
|
1137
|
+
* Late decisions are recorded as ignored.
|
|
1743
1138
|
*/
|
|
1744
1139
|
async function runAttempt(service, instance) {
|
|
1745
|
-
const
|
|
1140
|
+
const timeoutMs = timeoutOf(service, "start");
|
|
1141
|
+
const timer = timeoutMs === void 0 ? void 0 : timeoutSignal(service.container.runtime.clock, timeoutMs, () => new HookTimeoutError(`The 'start' hook of '${service.name}' timed out.`, "start", timeoutMs));
|
|
1142
|
+
const run = beginHookRun(instance, timer?.signal);
|
|
1746
1143
|
const progress = { index: instance.startFrom };
|
|
1747
|
-
let
|
|
1144
|
+
let abandoned = false;
|
|
1748
1145
|
const running = runHandlers(service, instance, "start", run.signal, {}, {
|
|
1749
1146
|
from: instance.startFrom,
|
|
1750
1147
|
progress,
|
|
1751
|
-
stopped: () =>
|
|
1148
|
+
stopped: () => abandoned
|
|
1752
1149
|
});
|
|
1753
|
-
const
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
value: await running
|
|
1757
|
-
} : await raceTimeout(service.container.runtime.clock, running, timeoutMs);
|
|
1758
|
-
instance.startFrom = progress.index;
|
|
1759
|
-
if (result.kind === "settled") {
|
|
1760
|
-
run.end();
|
|
1761
|
-
return result.value;
|
|
1762
|
-
}
|
|
1763
|
-
timedOut = true;
|
|
1764
|
-
run.abort(new HookTimeoutError(`The 'start' hook of '${service.name}' timed out.`, "start", timeoutMs));
|
|
1150
|
+
const result = await raceAbort(running, run.signal);
|
|
1151
|
+
timer?.cancel();
|
|
1152
|
+
const timedOut = timer !== void 0 && timer.signal.aborted && run.signal.reason === timer.signal.reason;
|
|
1765
1153
|
run.end();
|
|
1154
|
+
instance.startFrom = progress.index;
|
|
1155
|
+
if (result.kind === "settled") return {
|
|
1156
|
+
decisions: result.value,
|
|
1157
|
+
superseded: false
|
|
1158
|
+
};
|
|
1159
|
+
abandoned = true;
|
|
1766
1160
|
running.then((late) => {
|
|
1767
|
-
for (const decision of late)
|
|
1161
|
+
for (const decision of late) recordLateDecision(service, instance, decision, [run.signal]);
|
|
1768
1162
|
});
|
|
1769
|
-
|
|
1163
|
+
if (!timedOut) return {
|
|
1164
|
+
decisions: [],
|
|
1165
|
+
superseded: true
|
|
1166
|
+
};
|
|
1167
|
+
return {
|
|
1168
|
+
decisions: [decideTimeout(service, instance, "start", timeoutMs)],
|
|
1169
|
+
superseded: false
|
|
1170
|
+
};
|
|
1770
1171
|
}
|
|
1771
1172
|
/**
|
|
1772
1173
|
* Runs the `start` hooks of a built instance. The service moves to `running`, waits in `retrying` for a requeue, or stops.
|
|
@@ -1779,9 +1180,13 @@ async function runStart(service, instance) {
|
|
|
1779
1180
|
const logger = handlers.length > 0 ? loggerOf(service) : void 0;
|
|
1780
1181
|
if (instance.startFrom === 0) logger?.hookOnStart(service.name);
|
|
1781
1182
|
const startTime = performance.now();
|
|
1782
|
-
const
|
|
1183
|
+
const attempt = handlers.length > instance.startFrom ? await runAttempt(service, instance) : {
|
|
1184
|
+
decisions: [],
|
|
1185
|
+
superseded: false
|
|
1186
|
+
};
|
|
1187
|
+
const { decisions } = attempt;
|
|
1783
1188
|
service.busy = false;
|
|
1784
|
-
const ignored = service.signals.targetLost();
|
|
1189
|
+
const ignored = attempt.superseded || service.signals.targetLost();
|
|
1785
1190
|
for (const decision of decisions) recordDecision(service, instance, decision, ignored);
|
|
1786
1191
|
if (ignored) {
|
|
1787
1192
|
enterStopping(service, { reason: "A newer target superseded the start." });
|
|
@@ -1789,9 +1194,13 @@ async function runStart(service, instance) {
|
|
|
1789
1194
|
}
|
|
1790
1195
|
const decision = decisions.at(-1);
|
|
1791
1196
|
if (!decision || decision.kind === "handled") {
|
|
1197
|
+
service.demanders.clear();
|
|
1792
1198
|
if (decision) finishRun(instance, "start", decision);
|
|
1793
1199
|
logger?.hookOnStartComplete(service.name, performance.now() - startTime);
|
|
1794
|
-
if (service.swap)
|
|
1200
|
+
if (service.swap?.ready) {
|
|
1201
|
+
service.swap.held = true;
|
|
1202
|
+
service.swap.ready(service);
|
|
1203
|
+
} else if (service.swap) completeSwap(service);
|
|
1795
1204
|
else setPhase(service, "running");
|
|
1796
1205
|
return;
|
|
1797
1206
|
}
|
|
@@ -1827,16 +1236,6 @@ async function runStart(service, instance) {
|
|
|
1827
1236
|
}
|
|
1828
1237
|
// src/reconciler/build.ts
|
|
1829
1238
|
/**
|
|
1830
|
-
* An active service makes the providers of its edges converge too, before `start()`. A fallback edge demands each candidate.
|
|
1831
|
-
* A `perConsumer` registration gets a service for the container of `service`. The nearest registration of each key is demanded.
|
|
1832
|
-
*/
|
|
1833
|
-
function demandProviders(service) {
|
|
1834
|
-
for (const edge of service.edges) for (const keyName of edge.candidates) {
|
|
1835
|
-
const [nearest] = candidatesOf(service.container, keyName, service).map((registration) => registration.perConsumer ? ensureConsumerService(registration, service.container) : registration);
|
|
1836
|
-
if (nearest && !nearest.signals.serving() && !nearest.signals.demanded()) nearest.signals.demanded(true);
|
|
1837
|
-
}
|
|
1838
|
-
}
|
|
1839
|
-
/**
|
|
1840
1239
|
* Tells whether each optional edge has a final answer for a `resolve` call: no provider, a running provider, or a failed one.
|
|
1841
1240
|
* A service does not wait for optional edges. A `resolve` call does, so that it does not miss a provider that is still starting.
|
|
1842
1241
|
*/
|
|
@@ -1903,8 +1302,10 @@ function prepareInstance(service, snapshot) {
|
|
|
1903
1302
|
startFrom: 0,
|
|
1904
1303
|
value: void 0,
|
|
1905
1304
|
building: true,
|
|
1305
|
+
tearingDown: false,
|
|
1906
1306
|
disposed: false
|
|
1907
1307
|
};
|
|
1308
|
+
recordSignalOrigin(instance.controller.signal, instance);
|
|
1908
1309
|
service.busy = true;
|
|
1909
1310
|
batch(() => {
|
|
1910
1311
|
syncHolds(service, instance);
|
|
@@ -1989,13 +1390,13 @@ function memberEntry(member, built) {
|
|
|
1989
1390
|
};
|
|
1990
1391
|
}
|
|
1991
1392
|
/** Resolves one key inside a transaction: a built member first, then the published providers. */
|
|
1992
|
-
function resolveInTransaction(service, keyName,
|
|
1393
|
+
function resolveInTransaction(service, keyName, edge, members, built) {
|
|
1993
1394
|
const member = members.get(keyName);
|
|
1994
1395
|
if (member && member !== service) {
|
|
1995
|
-
if (inConflict(member) ||
|
|
1396
|
+
if (inConflict(member) || !acceptsCandidate(service, edge, keyName, member)) return void 0;
|
|
1996
1397
|
return memberEntry(member, built);
|
|
1997
1398
|
}
|
|
1998
|
-
const resolution = resolveKey(service, keyName,
|
|
1399
|
+
const resolution = resolveKey(service, keyName, edge);
|
|
1999
1400
|
if (resolution.kind !== "resolved") return void 0;
|
|
2000
1401
|
return {
|
|
2001
1402
|
state: Object.freeze({
|
|
@@ -2010,7 +1411,7 @@ function resolveInTransaction(service, keyName, condition, members, built) {
|
|
|
2010
1411
|
function transactionSnapshot(service, members, built) {
|
|
2011
1412
|
const snapshot = /* @__PURE__ */ new Map();
|
|
2012
1413
|
for (const edge of service.edges) {
|
|
2013
|
-
const entries = edge.candidates.map((keyName) => resolveInTransaction(service, keyName, edge
|
|
1414
|
+
const entries = edge.candidates.map((keyName) => resolveInTransaction(service, keyName, edge, members, built));
|
|
2014
1415
|
const entry = entries.find((candidate) => candidate !== void 0 && candidate.state.status !== "unavailable") ?? entries.find((candidate) => candidate !== void 0);
|
|
2015
1416
|
if (!edge.optional && (!entry || entry.state.status === "unavailable")) return void 0;
|
|
2016
1417
|
snapshot.set(edge.name, entry);
|
|
@@ -2091,6 +1492,7 @@ function abortTransaction(transaction, error) {
|
|
|
2091
1492
|
if (!committing) {
|
|
2092
1493
|
transaction.container.transactions.delete(transaction);
|
|
2093
1494
|
transaction.settled = Promise.resolve();
|
|
1495
|
+
releaseReservations(transaction);
|
|
2094
1496
|
}
|
|
2095
1497
|
return transaction.settled ?? Promise.resolve();
|
|
2096
1498
|
}
|
|
@@ -2105,6 +1507,54 @@ function reconcileConflict(service) {
|
|
|
2105
1507
|
setPhase(service, "pending", { reason: `Only one declaration of the exclusive key '${service.name}' remains.` });
|
|
2106
1508
|
service.container.runtime.reconciler.markDirty(service);
|
|
2107
1509
|
}
|
|
1510
|
+
// src/reconciler/rebuild.ts
|
|
1511
|
+
/**
|
|
1512
|
+
* Tells whether the old instance of a running service can keep serving while its successor builds and starts.
|
|
1513
|
+
*
|
|
1514
|
+
* - Only a service with a key rebuilds make-before-break. An invocation has no dependents, so it stops, then runs again.
|
|
1515
|
+
* - Each edge that the old instance is bound to still resolves, maybe to a new instance. An edge without a provider has nothing to build against, so the service stops first.
|
|
1516
|
+
* - The container is not in disposal.
|
|
1517
|
+
*
|
|
1518
|
+
* A bound provider instance stays usable until its last holder releases it, so the old instance keeps working with its bindings.
|
|
1519
|
+
*/
|
|
1520
|
+
function keepsServing(service, instance) {
|
|
1521
|
+
if (service.kind !== "service" || service.container.state !== "active") return false;
|
|
1522
|
+
return service.edges.every((edge) => !instance.bindings.has(edge.name) || resolvedInstance(service, edge) !== void 0);
|
|
1523
|
+
}
|
|
1524
|
+
/**
|
|
1525
|
+
* Rebuilds a running service, for a changed dependency without a handler, or for a `rebuild` decision.
|
|
1526
|
+
*
|
|
1527
|
+
* When the old instance can keep serving, the rebuild is make-before-break: a successor with the same options builds and starts against the current dependencies.
|
|
1528
|
+
* The old instance serves until the successor completes `start`. Then one batch swaps them, as for `replace()`. The dependents get `dependency-change`, not a withdrawal.
|
|
1529
|
+
* If the successor fails, the old instance stops and the service goes to `failed`.
|
|
1530
|
+
* Otherwise the service stops first and builds again from `pending`.
|
|
1531
|
+
*
|
|
1532
|
+
* A replacement in progress, from `replace()` or a transaction, takes precedence. The old instance serves until it ends, and the service reconciles again then.
|
|
1533
|
+
*/
|
|
1534
|
+
function rebuild(service, instance, intent) {
|
|
1535
|
+
if (service.successor) return;
|
|
1536
|
+
if (!keepsServing(service, instance)) {
|
|
1537
|
+
enterStopping(service, intent);
|
|
1538
|
+
return;
|
|
1539
|
+
}
|
|
1540
|
+
const { registration } = service;
|
|
1541
|
+
const { service: successor } = createSuccessor(service, service, {
|
|
1542
|
+
template: service.template,
|
|
1543
|
+
rebuild: true,
|
|
1544
|
+
onSettle: (next, outcome) => {
|
|
1545
|
+
if (registration?.pending === next) registration.pending = void 0;
|
|
1546
|
+
if (outcome.kind === "swapped" && registration) registration.current = next;
|
|
1547
|
+
if (outcome.kind === "swapped" && service.signals.phase() === "running") enterStopping(service, intent);
|
|
1548
|
+
if (outcome.kind === "failed" && service.signals.present()) enterStopping(service, {
|
|
1549
|
+
failure: { error: outcome.error },
|
|
1550
|
+
reason: "The rebuild failed. The old instance stops.",
|
|
1551
|
+
decision: intent.decision
|
|
1552
|
+
});
|
|
1553
|
+
service.container.runtime.reconciler.markDirty(service);
|
|
1554
|
+
}
|
|
1555
|
+
});
|
|
1556
|
+
if (registration) registration.pending = successor;
|
|
1557
|
+
}
|
|
2108
1558
|
// src/reconciler/dependency-hooks.ts
|
|
2109
1559
|
/** Handles a running service: it stops it, or it runs a dependency hook for the changes since the last handled snapshot. */
|
|
2110
1560
|
function reconcileRunning(service) {
|
|
@@ -2123,6 +1573,7 @@ function reconcileRunning(service) {
|
|
|
2123
1573
|
enterStopping(service, { reason: `The exclusive key '${service.name}' got another declaration.` });
|
|
2124
1574
|
return;
|
|
2125
1575
|
}
|
|
1576
|
+
demandProviders(service);
|
|
2126
1577
|
const snapshot = currentSnapshot(service);
|
|
2127
1578
|
if (requiredWithdrawn(service)) {
|
|
2128
1579
|
cancelRequeueWait(instance);
|
|
@@ -2130,12 +1581,13 @@ function reconcileRunning(service) {
|
|
|
2130
1581
|
else enterStopping(service, { reason: "A required dependency was withdrawn." });
|
|
2131
1582
|
return;
|
|
2132
1583
|
}
|
|
1584
|
+
if (service.successor?.swap?.rebuild) return;
|
|
2133
1585
|
const changes = diffSnapshot(service, instance.handled, snapshot);
|
|
2134
1586
|
if (changes.length === 0 || instance.requeueWait) return;
|
|
2135
1587
|
const identity = changes.some(isIdentityChange);
|
|
2136
1588
|
const hook = identity ? "dependency-change" : "dependency-status-change";
|
|
2137
1589
|
if (instance.hooks[hook].length === 0) {
|
|
2138
|
-
if (identity)
|
|
1590
|
+
if (identity) rebuild(service, instance, {
|
|
2139
1591
|
reason: `A dependency changed without a handler: ${describeChanges(changes)}.`,
|
|
2140
1592
|
decision: "rebuild"
|
|
2141
1593
|
});
|
|
@@ -2199,7 +1651,7 @@ async function runDependencyHook(service, instance, hook, snapshot, changes) {
|
|
|
2199
1651
|
}
|
|
2200
1652
|
case "rebuild":
|
|
2201
1653
|
releaseExtraHolds(service, instance);
|
|
2202
|
-
|
|
1654
|
+
rebuild(service, instance, {
|
|
2203
1655
|
reason: decision.reason,
|
|
2204
1656
|
decision: "rebuild"
|
|
2205
1657
|
});
|
|
@@ -2329,6 +1781,9 @@ function createReconciler(options) {
|
|
|
2329
1781
|
case "failed":
|
|
2330
1782
|
if (!service.signals.present()) finalize(service);
|
|
2331
1783
|
return;
|
|
1784
|
+
case "starting":
|
|
1785
|
+
if (service.swap?.held && service.signals.targetLost()) enterStopping(service, { reason: "The replacement was cancelled before the swap." });
|
|
1786
|
+
return;
|
|
2332
1787
|
case "running":
|
|
2333
1788
|
reconcileRunning(service);
|
|
2334
1789
|
return;
|
|
@@ -2414,6 +1869,16 @@ function readRandom(options) {
|
|
|
2414
1869
|
if (random !== void 0 && typeof random !== "function") throw new InvalidArgumentError("The random option must be a function.");
|
|
2415
1870
|
return random ?? Math.random;
|
|
2416
1871
|
}
|
|
1872
|
+
function readLogging(options, parent) {
|
|
1873
|
+
let logging;
|
|
1874
|
+
try {
|
|
1875
|
+
logging = options.logging;
|
|
1876
|
+
} catch (error) {
|
|
1877
|
+
throw new InvalidArgumentError("Cannot read the logging option.", { cause: error });
|
|
1878
|
+
}
|
|
1879
|
+
if (logging !== void 0 && typeof logging !== "boolean") throw new InvalidArgumentError("The logging option must be a boolean.");
|
|
1880
|
+
return logging ?? parent?.api.logging ?? false;
|
|
1881
|
+
}
|
|
2417
1882
|
function createRuntime(options) {
|
|
2418
1883
|
let seq = 0;
|
|
2419
1884
|
let instanceSeq = 0;
|
|
@@ -2441,7 +1906,8 @@ function createContainer(options = {}) {
|
|
|
2441
1906
|
const owner = readOwner(options.owner);
|
|
2442
1907
|
const isolated = readIsolated(options);
|
|
2443
1908
|
const policy = readContainerPolicy(options);
|
|
2444
|
-
const
|
|
1909
|
+
const logging = readLogging(options, parent);
|
|
1910
|
+
const logger = options.logger ?? parent?.api.logger ?? createDefaultLogger();
|
|
2445
1911
|
const runtime = parent?.runtime ?? createRuntime(options);
|
|
2446
1912
|
let reactiveOwner;
|
|
2447
1913
|
const stopReactive = withOwner(parent?.reactiveOwner, () => effectScope(() => {
|
|
@@ -2449,6 +1915,7 @@ function createContainer(options = {}) {
|
|
|
2449
1915
|
}));
|
|
2450
1916
|
const api = {
|
|
2451
1917
|
logger,
|
|
1918
|
+
logging,
|
|
2452
1919
|
owner,
|
|
2453
1920
|
parent: parent?.api
|
|
2454
1921
|
};
|
|
@@ -2462,7 +1929,8 @@ function createContainer(options = {}) {
|
|
|
2462
1929
|
services: /* @__PURE__ */ new Set(),
|
|
2463
1930
|
signals: {
|
|
2464
1931
|
started: signal(false),
|
|
2465
|
-
slots: /* @__PURE__ */ new Map()
|
|
1932
|
+
slots: /* @__PURE__ */ new Map(),
|
|
1933
|
+
exports: /* @__PURE__ */ new Map()
|
|
2466
1934
|
},
|
|
2467
1935
|
isolated,
|
|
2468
1936
|
policy,
|
|
@@ -2483,20 +1951,31 @@ function createContainer(options = {}) {
|
|
|
2483
1951
|
}
|
|
2484
1952
|
/**
|
|
2485
1953
|
* Runs `setup` and stores the returned disposer on the container. `dispose(container)` runs the disposers in reverse order.
|
|
1954
|
+
*
|
|
1955
|
+
* When the disposal of the container started or ended, the disposer runs at once after `setup`, so that the resource does not leak.
|
|
1956
|
+
* A disposer that throws throws to the caller. A disposer promise that rejects is reported outside the call, as an unhandled error.
|
|
2486
1957
|
*/
|
|
2487
1958
|
function effect$1(container, setup) {
|
|
2488
1959
|
const node = nodeOf(container);
|
|
2489
|
-
assertActive(node, "add an effect");
|
|
2490
1960
|
if (typeof setup !== "function") throw new InvalidArgumentError("The effect setup is not a function.");
|
|
2491
1961
|
const disposer = untracked(setup);
|
|
2492
|
-
if (typeof disposer
|
|
1962
|
+
if (typeof disposer !== "function") return;
|
|
1963
|
+
if (node.state === "active") {
|
|
1964
|
+
node.effects.push(disposer);
|
|
1965
|
+
return;
|
|
1966
|
+
}
|
|
1967
|
+
const result = untracked(disposer);
|
|
1968
|
+
if (result instanceof Promise) result.catch((error) => queueMicrotask(() => {
|
|
1969
|
+
throw error;
|
|
1970
|
+
}));
|
|
2493
1971
|
}
|
|
2494
1972
|
/**
|
|
2495
|
-
* Starts the disposal of `container` and of its descendants.
|
|
1973
|
+
* Starts the disposal of `container` and of its descendants. The whole subtree enters `disposing` at once, so nothing new registers in it.
|
|
2496
1974
|
*
|
|
2497
|
-
* The
|
|
1975
|
+
* The child containers dispose first, one after the other, in reverse creation order. Then the services of the container withdraw in one batch, so that dependents stop without a rebuild.
|
|
1976
|
+
* Then the effects of the container run in reverse order. `after` delays the work until an earlier sibling completed its disposal.
|
|
2498
1977
|
*/
|
|
2499
|
-
function beginDisposal(container, parentFrame) {
|
|
1978
|
+
function beginDisposal(container, parentFrame, after) {
|
|
2500
1979
|
if (container.disposal) return container.disposal;
|
|
2501
1980
|
const { context } = container.runtime;
|
|
2502
1981
|
const frame = {
|
|
@@ -2507,27 +1986,35 @@ function beginDisposal(container, parentFrame) {
|
|
|
2507
1986
|
container.state = "disposing";
|
|
2508
1987
|
container.frame = frame;
|
|
2509
1988
|
for (const transaction of [...container.transactions]) abortTransaction(transaction, new BuildAfterDisposeError("The container was disposed before the transaction committed."));
|
|
2510
|
-
|
|
2511
|
-
for (const service of container.services) if (service.signals.present()) {
|
|
2512
|
-
releaseService(service);
|
|
2513
|
-
withdrawn.add(service);
|
|
2514
|
-
}
|
|
1989
|
+
let previous = after;
|
|
2515
1990
|
const children = [...container.children].reverse().map((child) => {
|
|
2516
|
-
const disposal = beginDisposal(child, frame);
|
|
1991
|
+
const disposal = beginDisposal(child, frame, previous);
|
|
2517
1992
|
disposal.catch(() => {});
|
|
1993
|
+
previous = disposal.then(() => {}, () => {});
|
|
2518
1994
|
return disposal;
|
|
2519
1995
|
});
|
|
1996
|
+
const withdrawn = /* @__PURE__ */ new Set();
|
|
1997
|
+
const withdrawServices = () => untracked(() => batch(() => {
|
|
1998
|
+
for (const service of container.services) if (service.signals.present()) {
|
|
1999
|
+
releaseService(service);
|
|
2000
|
+
withdrawn.add(service);
|
|
2001
|
+
}
|
|
2002
|
+
}));
|
|
2003
|
+
const immediate = after === void 0 && children.length === 0;
|
|
2004
|
+
if (immediate) withdrawServices();
|
|
2520
2005
|
container.disposal = context.run(frame, async () => {
|
|
2521
2006
|
const errors = [];
|
|
2522
|
-
|
|
2523
|
-
const serviceErrors = await service.withdrawal.promise;
|
|
2524
|
-
if (withdrawn.has(service)) errors.push(...serviceErrors);
|
|
2525
|
-
}
|
|
2007
|
+
if (after) await after;
|
|
2526
2008
|
for (const child of children) try {
|
|
2527
2009
|
await child;
|
|
2528
2010
|
} catch (error) {
|
|
2529
2011
|
errors.push(error);
|
|
2530
2012
|
}
|
|
2013
|
+
if (!immediate) withdrawServices();
|
|
2014
|
+
for (const service of [...container.services]) {
|
|
2015
|
+
const serviceErrors = await service.withdrawal.promise;
|
|
2016
|
+
if (withdrawn.has(service)) errors.push(...serviceErrors);
|
|
2017
|
+
}
|
|
2531
2018
|
for (const disposer of container.effects.splice(0).reverse()) try {
|
|
2532
2019
|
await context.run(frame, () => untracked(disposer));
|
|
2533
2020
|
} catch (error) {
|
|
@@ -2551,25 +2038,81 @@ function beginDisposal(container, parentFrame) {
|
|
|
2551
2038
|
});
|
|
2552
2039
|
return container.disposal;
|
|
2553
2040
|
}
|
|
2041
|
+
/** The services of `container` and of its descendants that did not finish their withdrawal. */
|
|
2042
|
+
function unfinishedServices(container) {
|
|
2043
|
+
return [...[...container.services].filter((service) => service.kind !== "resolver" && service.signals.phase() !== "withdrawn"), ...[...container.children].flatMap(unfinishedServices)];
|
|
2044
|
+
}
|
|
2554
2045
|
/**
|
|
2555
2046
|
* Withdraws the registrations and invocations of the container, disposes the child containers, and runs the container effects in reverse order.
|
|
2556
2047
|
*
|
|
2557
|
-
* One error keeps its identity. More than one error becomes an `AggregateError`.
|
|
2558
|
-
*
|
|
2048
|
+
* One error keeps its identity. More than one error becomes an `AggregateError`. A second call from outside without a signal gets the same promise.
|
|
2049
|
+
*
|
|
2050
|
+
* A call from the cleanup chain of the container or of a descendant cannot wait, because the disposal waits for that cleanup.
|
|
2051
|
+
* The same is true for a cleanup of a service that holds a service of the subtree, directly or through other instances, or a consumer service of a `perConsumer` registration of it.
|
|
2052
|
+
* injeca finds such a call by the signal of the cleanup, or by the synchronous part of the cleanup. It warns, sends a `reentrant-call` record, and resolves at once. The disposal continues.
|
|
2053
|
+
*
|
|
2054
|
+
* When `options.signal` aborts, the call rejects with the reason and stops waiting. The disposal continues: the container stays in `disposing`, and calls on it fail with `build-after-dispose`.
|
|
2055
|
+
* A later `dispose()` waits for the same disposal and gets its result. An aborted signal starts the disposal, then rejects at once.
|
|
2559
2056
|
*/
|
|
2560
|
-
function dispose(container) {
|
|
2057
|
+
function dispose(container, options) {
|
|
2561
2058
|
const node = nodeOf(container);
|
|
2059
|
+
const abortSignal = readSignal(options, "dispose");
|
|
2562
2060
|
const { context } = node.runtime;
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
let disposal;
|
|
2061
|
+
const reentry = untracked(() => findReentry(node, "dispose", abortSignal));
|
|
2062
|
+
const disposal = node.disposal ?? untracked(() => {
|
|
2063
|
+
let started;
|
|
2567
2064
|
batch(() => {
|
|
2568
|
-
|
|
2065
|
+
started = beginDisposal(node, context.current());
|
|
2569
2066
|
});
|
|
2570
|
-
return
|
|
2067
|
+
return started;
|
|
2068
|
+
});
|
|
2069
|
+
if (reentry) {
|
|
2070
|
+
untracked(() => reportReentry(node, "dispose", reentry));
|
|
2071
|
+
disposal.catch(() => {});
|
|
2072
|
+
return Promise.resolve();
|
|
2073
|
+
}
|
|
2074
|
+
return abortable(disposal, abortSignal, (reason) => {
|
|
2075
|
+
recordAbortedWait(unfinishedServices(node), "dispose", reason);
|
|
2076
|
+
return reason;
|
|
2077
|
+
});
|
|
2078
|
+
}
|
|
2079
|
+
// src/primitives/withdraw.ts
|
|
2080
|
+
/**
|
|
2081
|
+
* Runs a `withdraw()` call. `release` releases the service and returns it, or `undefined` when there is nothing to withdraw.
|
|
2082
|
+
*
|
|
2083
|
+
* - Resolves `true` after the withdrawal, and rejects with the teardown errors.
|
|
2084
|
+
* - When `options.signal` aborts, it rejects with the reason and stops waiting. The withdrawal continues. An aborted signal still starts it.
|
|
2085
|
+
* - A call from a cleanup that the withdrawal waits for warns, sends a `reentrant-call` record, and resolves `true` at once.
|
|
2086
|
+
*/
|
|
2087
|
+
function withdrawWith(release, options) {
|
|
2088
|
+
let signal;
|
|
2089
|
+
try {
|
|
2090
|
+
signal = readSignal(options, "withdraw");
|
|
2091
|
+
} catch (error) {
|
|
2092
|
+
return Promise.reject(error);
|
|
2093
|
+
}
|
|
2094
|
+
const service = release();
|
|
2095
|
+
if (!service) return Promise.resolve(false);
|
|
2096
|
+
const withdrawal = awaitWithdrawal(service).then(() => true);
|
|
2097
|
+
const reentry = untracked(() => findWithdrawReentry(service, signal));
|
|
2098
|
+
if (reentry) {
|
|
2099
|
+
untracked(() => reportReentry(service.container, "withdraw", reentry, `'${service.name}' in ${JSON.stringify(service.container.owner)}`));
|
|
2100
|
+
withdrawal.catch(() => {});
|
|
2101
|
+
return Promise.resolve(true);
|
|
2102
|
+
}
|
|
2103
|
+
return abortable(withdrawal, signal, (reason) => {
|
|
2104
|
+
recordAbortedWait(untracked(() => service.signals.phase()) === "withdrawn" ? [] : [service], "withdraw", reason);
|
|
2105
|
+
return reason;
|
|
2571
2106
|
});
|
|
2572
2107
|
}
|
|
2108
|
+
/** The `withdraw()` of a registration. Resolves `false` for a stale registration. */
|
|
2109
|
+
function withdrawRegistration(registration, options) {
|
|
2110
|
+
return withdrawWith(() => releaseRegistration(registration), options);
|
|
2111
|
+
}
|
|
2112
|
+
/** The `withdraw()` of an invocation. Resolves `false` when it is already withdrawn. */
|
|
2113
|
+
function withdrawInvocation(service, options) {
|
|
2114
|
+
return withdrawWith(() => releasePresent(service) ? service : void 0, options);
|
|
2115
|
+
}
|
|
2573
2116
|
// src/primitives/invoke.ts
|
|
2574
2117
|
function invoke$1(container, option) {
|
|
2575
2118
|
return invokeIn(container, option);
|
|
@@ -2610,8 +2153,11 @@ function invokeIn(container, option) {
|
|
|
2610
2153
|
});
|
|
2611
2154
|
return service;
|
|
2612
2155
|
});
|
|
2613
|
-
container
|
|
2614
|
-
return Object.freeze({
|
|
2156
|
+
traceLoggerOf(container)?.invoke(edges.map((edge) => edge.keyName));
|
|
2157
|
+
return Object.freeze({
|
|
2158
|
+
key: name,
|
|
2159
|
+
withdraw: (options) => withdrawInvocation(registered, options)
|
|
2160
|
+
});
|
|
2615
2161
|
}
|
|
2616
2162
|
// src/diagnostics/explain.ts
|
|
2617
2163
|
/** The owners of conflicting declarations, in container order. The order of the registrations has no effect. */
|
|
@@ -2647,6 +2193,19 @@ function explainService(service) {
|
|
|
2647
2193
|
for (const edge of service.edges) {
|
|
2648
2194
|
if (edge.optional) continue;
|
|
2649
2195
|
const resolution = resolutionOf(service, edge);
|
|
2196
|
+
if (edge.fallback && resolution.kind !== "resolved") {
|
|
2197
|
+
for (const keyName of edge.candidates) {
|
|
2198
|
+
const candidate = resolveKey(service, keyName, edge);
|
|
2199
|
+
if (candidate.kind === "rejected") blockers.push({
|
|
2200
|
+
kind: "rejected-provider",
|
|
2201
|
+
dependency: edge.name,
|
|
2202
|
+
key: candidate.provider.name,
|
|
2203
|
+
owner: candidate.provider.container.owner,
|
|
2204
|
+
metadata: candidate.provider.metadata
|
|
2205
|
+
});
|
|
2206
|
+
}
|
|
2207
|
+
if (resolution.kind === "rejected") continue;
|
|
2208
|
+
}
|
|
2650
2209
|
switch (resolution.kind) {
|
|
2651
2210
|
case "missing":
|
|
2652
2211
|
blockers.push({
|
|
@@ -2699,6 +2258,7 @@ function explainService(service) {
|
|
|
2699
2258
|
}
|
|
2700
2259
|
}
|
|
2701
2260
|
const status = service.signals.effective();
|
|
2261
|
+
const successor = service.successor;
|
|
2702
2262
|
return Object.freeze({
|
|
2703
2263
|
name: service.name,
|
|
2704
2264
|
kind: service.kind === "invocation" ? "invocation" : "service",
|
|
@@ -2709,7 +2269,8 @@ function explainService(service) {
|
|
|
2709
2269
|
reason: status?.reason,
|
|
2710
2270
|
cause: status?.cause
|
|
2711
2271
|
}),
|
|
2712
|
-
blockers: Object.freeze(blockers)
|
|
2272
|
+
blockers: Object.freeze(blockers),
|
|
2273
|
+
...withoutUndefined({ replacement: successor?.signals.present() ? explainService(successor) : void 0 })
|
|
2713
2274
|
});
|
|
2714
2275
|
}
|
|
2715
2276
|
/** Formats a cause chain, for example `'airi.llm' is unavailable (reason), because 'gateway/store' is unavailable`. */
|
|
@@ -2740,6 +2301,11 @@ function describeExplanations(explanations) {
|
|
|
2740
2301
|
return explanations.map((explanation) => {
|
|
2741
2302
|
const reasons = explanation.blockers.map(describeBlocker);
|
|
2742
2303
|
if (explanation.cause) reasons.push(`it is ${explanation.status ?? "unavailable"}, because ${describeCause(explanation.cause)}`);
|
|
2304
|
+
const { replacement } = explanation;
|
|
2305
|
+
if (replacement) {
|
|
2306
|
+
const blockers = replacement.blockers.map(describeBlocker);
|
|
2307
|
+
reasons.push(`its replacement is ${replacement.phase ?? "missing"}${blockers.length > 0 ? ` (${blockers.join(", ")})` : ""}`);
|
|
2308
|
+
}
|
|
2743
2309
|
return `- ${explanation.name} is ${explanation.phase}${reasons.length > 0 ? `: ${reasons.join(", ")}` : ""}`;
|
|
2744
2310
|
}).join("\n");
|
|
2745
2311
|
}
|
|
@@ -2757,49 +2323,42 @@ function presentServices(containers) {
|
|
|
2757
2323
|
}
|
|
2758
2324
|
/**
|
|
2759
2325
|
* Runs `check` each time the work queue drains, until the result is not `'waiting'`.
|
|
2760
|
-
*
|
|
2326
|
+
* When `signal` aborts first, it stops checking and rejects with the result of `onAborted`. An aborted signal rejects at once.
|
|
2761
2327
|
*/
|
|
2762
|
-
function waitUntilSettled(containers, check,
|
|
2763
|
-
const {
|
|
2328
|
+
function waitUntilSettled(containers, check, signal, onAborted) {
|
|
2329
|
+
const { reconciler } = containers[0].runtime;
|
|
2764
2330
|
return new Promise((done, fail) => {
|
|
2765
|
-
|
|
2331
|
+
if (signal?.aborted) {
|
|
2332
|
+
fail(onAborted(signal.reason));
|
|
2333
|
+
return;
|
|
2334
|
+
}
|
|
2335
|
+
let removeAbort = () => {};
|
|
2766
2336
|
const stopListening = reconciler.onDrained(() => {
|
|
2767
2337
|
const result = check(presentServices(containers));
|
|
2768
2338
|
if (result === "waiting") return;
|
|
2769
2339
|
stopListening();
|
|
2770
|
-
|
|
2340
|
+
removeAbort();
|
|
2771
2341
|
if (result.kind === "done") done();
|
|
2772
2342
|
else fail(errorFrom(result.errors, "Start failed."));
|
|
2773
2343
|
});
|
|
2774
|
-
|
|
2344
|
+
removeAbort = onAbort(signal, (reason) => {
|
|
2775
2345
|
stopListening();
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
}), timeoutMs);
|
|
2346
|
+
fail(onAborted(reason));
|
|
2347
|
+
});
|
|
2779
2348
|
reconciler.requestDrain();
|
|
2780
2349
|
});
|
|
2781
2350
|
}
|
|
2782
|
-
function readTimeout(options) {
|
|
2783
|
-
let timeoutMs;
|
|
2784
|
-
try {
|
|
2785
|
-
timeoutMs = options?.timeoutMs;
|
|
2786
|
-
} catch (error) {
|
|
2787
|
-
throw new InvalidArgumentError("Cannot read the start options.", { cause: error });
|
|
2788
|
-
}
|
|
2789
|
-
if (timeoutMs !== void 0 && (typeof timeoutMs !== "number" || !Number.isFinite(timeoutMs) || timeoutMs < 0)) throw new InvalidArgumentError(`The timeoutMs option of start must be a finite number of 0 or more. Got ${String(timeoutMs)}.`);
|
|
2790
|
-
return timeoutMs;
|
|
2791
|
-
}
|
|
2792
2351
|
/**
|
|
2793
2352
|
* Begins convergence of the container and of each child container that was started.
|
|
2794
2353
|
*
|
|
2795
2354
|
* Resolves when all their services and invocations are `running`. Rejects with the build and start errors.
|
|
2796
|
-
* A service in `failed` tries again. A required key without a provider keeps the promise pending, unless `
|
|
2355
|
+
* A service in `failed` tries again. A required key without a provider keeps the promise pending, unless `options.signal` aborts.
|
|
2797
2356
|
* A child container that was never started does not start. Call `start(child)` for it.
|
|
2798
2357
|
*/
|
|
2799
2358
|
function start$1(container, options) {
|
|
2800
2359
|
const node = nodeOf(container);
|
|
2801
2360
|
assertActive(node, "start");
|
|
2802
|
-
const
|
|
2361
|
+
const signal = readSignal(options, "start");
|
|
2803
2362
|
node.enrolled = true;
|
|
2804
2363
|
const containers = affectedContainers(node);
|
|
2805
2364
|
return untracked(() => {
|
|
@@ -2818,17 +2377,42 @@ function start$1(container, options) {
|
|
|
2818
2377
|
kind: "failed",
|
|
2819
2378
|
errors
|
|
2820
2379
|
} : "waiting";
|
|
2821
|
-
},
|
|
2822
|
-
|
|
2380
|
+
}, signal, (reason) => {
|
|
2381
|
+
if (!isTimeoutReason(reason)) return reason;
|
|
2382
|
+
const explanations = presentServices(containers).filter((service) => service.signals.phase() !== "running").map(explainService);
|
|
2383
|
+
return new StartTimeoutError(`start() timed out. These services are not running:\n${describeExplanations(explanations)}`, explanations, { cause: reason });
|
|
2384
|
+
});
|
|
2385
|
+
}).then(() => traceLoggerOf(container)?.running());
|
|
2823
2386
|
}
|
|
2824
2387
|
/**
|
|
2825
2388
|
* Stops all instances of the container and of each child container that was started. Dependents stop before their providers.
|
|
2826
2389
|
*
|
|
2827
2390
|
* The registrations stay. The container stays usable, and a later `start()` builds them again. `dispose()` clears the container.
|
|
2828
2391
|
* Rejects with the teardown errors.
|
|
2392
|
+
*
|
|
2393
|
+
* When `options.signal` aborts, it rejects with the reason and stops waiting. The container stays stopped: a later `start()` builds again.
|
|
2394
|
+
* The services that did not stop yet stay in `stopping` until their cleanups settle.
|
|
2395
|
+
*
|
|
2396
|
+
* A call from the cleanup chain of the container or of a descendant cannot wait, because the stop waits for that cleanup.
|
|
2397
|
+
* The same is true for a cleanup of a service that holds a service of the subtree, directly or through other instances.
|
|
2398
|
+
* injeca finds such a call by the signal of the cleanup, or by the synchronous part of the cleanup. It warns, sends a `reentrant-call` record, and resolves at once. The stop continues.
|
|
2829
2399
|
*/
|
|
2830
|
-
|
|
2831
|
-
|
|
2400
|
+
function stop$1(container, options) {
|
|
2401
|
+
let node;
|
|
2402
|
+
let signal;
|
|
2403
|
+
try {
|
|
2404
|
+
node = nodeOf(container);
|
|
2405
|
+
signal = readSignal(options, "stop");
|
|
2406
|
+
} catch (error) {
|
|
2407
|
+
return Promise.reject(error);
|
|
2408
|
+
}
|
|
2409
|
+
const reentry = untracked(() => findReentry(node, "stop", signal));
|
|
2410
|
+
if (!reentry) return stopAndWait(node, signal);
|
|
2411
|
+
untracked(() => reportReentry(node, "stop", reentry));
|
|
2412
|
+
stopAndWait(node, void 0).catch(() => {});
|
|
2413
|
+
return Promise.resolve();
|
|
2414
|
+
}
|
|
2415
|
+
async function stopAndWait(node, signal) {
|
|
2832
2416
|
const containers = affectedContainers(node);
|
|
2833
2417
|
const affected = new Set(containers);
|
|
2834
2418
|
node.enrolled = false;
|
|
@@ -2839,9 +2423,16 @@ async function stop$1(container) {
|
|
|
2839
2423
|
const settled = untracked(() => {
|
|
2840
2424
|
batch(() => {
|
|
2841
2425
|
for (const current of containers) current.signals.started(false);
|
|
2842
|
-
for (const service of presentServices(containers))
|
|
2426
|
+
for (const service of presentServices(containers)) {
|
|
2427
|
+
service.signals.demanded(false);
|
|
2428
|
+
service.signals.subscribers(/* @__PURE__ */ new Set());
|
|
2429
|
+
}
|
|
2430
|
+
});
|
|
2431
|
+
const stopped = (service) => service.signals.instance() === void 0 && !transitionalPhases.has(service.signals.phase());
|
|
2432
|
+
return waitUntilSettled(containers, (services) => services.every(stopped) ? { kind: "done" } : "waiting", signal, (reason) => {
|
|
2433
|
+
recordAbortedWait(presentServices(containers).filter((service) => !stopped(service)), "stop", reason);
|
|
2434
|
+
return reason;
|
|
2843
2435
|
});
|
|
2844
|
-
return waitUntilSettled(containers, (services) => services.every((service) => service.signals.instance() === void 0 && !transitionalPhases.has(service.signals.phase())) ? { kind: "done" } : "waiting");
|
|
2845
2436
|
});
|
|
2846
2437
|
try {
|
|
2847
2438
|
await settled;
|
|
@@ -2898,46 +2489,6 @@ function listen(container, eventKey, handler, metadata) {
|
|
|
2898
2489
|
};
|
|
2899
2490
|
}
|
|
2900
2491
|
// src/primitives/replace.ts
|
|
2901
|
-
/**
|
|
2902
|
-
* Creates a new service that replaces `target` after it completes `start`. It is not published before the swap.
|
|
2903
|
-
* `onSwap` runs at the swap, before the outcome settles.
|
|
2904
|
-
*/
|
|
2905
|
-
function createSuccessor(target, definition, template, onSettle, extra = {}) {
|
|
2906
|
-
const outcome = createDeferred();
|
|
2907
|
-
let settled = false;
|
|
2908
|
-
let service;
|
|
2909
|
-
batch(() => {
|
|
2910
|
-
service = createService({
|
|
2911
|
-
container: target.container,
|
|
2912
|
-
kind: "service",
|
|
2913
|
-
name: target.name,
|
|
2914
|
-
edges: definition.edges,
|
|
2915
|
-
build: definition.build,
|
|
2916
|
-
metadata: definition.metadata,
|
|
2917
|
-
policy: definition.policy,
|
|
2918
|
-
exclusive: target.exclusive,
|
|
2919
|
-
template,
|
|
2920
|
-
consumer: target.consumer,
|
|
2921
|
-
registration: target.registration,
|
|
2922
|
-
...extra,
|
|
2923
|
-
swap: {
|
|
2924
|
-
target,
|
|
2925
|
-
settle: (result) => {
|
|
2926
|
-
if (settled) return;
|
|
2927
|
-
settled = true;
|
|
2928
|
-
onSettle(service, result);
|
|
2929
|
-
outcome.resolve(result);
|
|
2930
|
-
}
|
|
2931
|
-
}
|
|
2932
|
-
});
|
|
2933
|
-
service.signals.demanded(target.signals.demanded());
|
|
2934
|
-
target.container.services.add(service);
|
|
2935
|
-
});
|
|
2936
|
-
return {
|
|
2937
|
-
service,
|
|
2938
|
-
outcome: outcome.promise
|
|
2939
|
-
};
|
|
2940
|
-
}
|
|
2941
2492
|
function outcomeResult(outcome, name) {
|
|
2942
2493
|
switch (outcome.kind) {
|
|
2943
2494
|
case "swapped": return true;
|
|
@@ -2967,6 +2518,7 @@ function replaceAtOnce(registration, target, definition) {
|
|
|
2967
2518
|
metadata: definition.metadata,
|
|
2968
2519
|
policy: definition.policy,
|
|
2969
2520
|
exclusive: target.exclusive,
|
|
2521
|
+
exported: target.exported,
|
|
2970
2522
|
perConsumer: target.perConsumer,
|
|
2971
2523
|
registration
|
|
2972
2524
|
});
|
|
@@ -2977,45 +2529,84 @@ function replaceAtOnce(registration, target, definition) {
|
|
|
2977
2529
|
return service;
|
|
2978
2530
|
}
|
|
2979
2531
|
/**
|
|
2980
|
-
*
|
|
2981
|
-
*
|
|
2532
|
+
* Replaces the `perConsumer` registration `target` with the unpublished registration `template`. Resolves with the outcome.
|
|
2533
|
+
*
|
|
2534
|
+
* Each running consumer service gets a successor that builds and starts from `definition`. A successor that completes `start` waits.
|
|
2535
|
+
* When every successor waits, one batch publishes `template` and swaps in all successors. The other consumer services leave and build again on demand.
|
|
2536
|
+
* When one successor fails, all successors leave, `template` leaves, and the old registration and its consumer services do not change.
|
|
2537
|
+
* A consumer service that leaves during the replacement, for example with its container, drops out of the batch.
|
|
2538
|
+
* A newer `replace` or a withdrawal cancels the replacement.
|
|
2982
2539
|
*/
|
|
2983
|
-
function
|
|
2984
|
-
const
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
2540
|
+
function replaceTemplate(registration, target, template) {
|
|
2541
|
+
const definition = {
|
|
2542
|
+
edges: template.edges,
|
|
2543
|
+
build: template.build,
|
|
2544
|
+
metadata: template.metadata,
|
|
2545
|
+
perConsumer: true,
|
|
2546
|
+
exported: false,
|
|
2547
|
+
policy: template.policy
|
|
2548
|
+
};
|
|
2549
|
+
const outcome = createDeferred();
|
|
2550
|
+
const waiting = /* @__PURE__ */ new Set();
|
|
2551
|
+
const held = /* @__PURE__ */ new Set();
|
|
2552
|
+
let settled = false;
|
|
2553
|
+
const finish = (result) => {
|
|
2554
|
+
settled = true;
|
|
2555
|
+
template.swap = void 0;
|
|
2556
|
+
if (target.successor === template) target.successor = void 0;
|
|
2557
|
+
if (registration.pending === template) registration.pending = void 0;
|
|
2558
|
+
outcome.resolve(result);
|
|
2559
|
+
};
|
|
2560
|
+
const abandon = (result) => {
|
|
2561
|
+
if (settled) return;
|
|
2562
|
+
batch(() => {
|
|
2563
|
+
finish(result);
|
|
2564
|
+
for (const successor of [...waiting, ...held]) {
|
|
2565
|
+
successor.signals.present(false);
|
|
2566
|
+
successor.container.runtime.reconciler.markDirty(successor);
|
|
2567
|
+
}
|
|
2568
|
+
releaseService(template);
|
|
2569
|
+
});
|
|
2570
|
+
};
|
|
2571
|
+
const commitIfReady = () => {
|
|
2572
|
+
if (settled || waiting.size > 0) return;
|
|
2573
|
+
batch(() => {
|
|
2574
|
+
finish({ kind: "swapped" });
|
|
2575
|
+
takeOver(registration, target, template);
|
|
2576
|
+
for (const successor of held) completeSwap(successor);
|
|
3014
2577
|
});
|
|
3015
|
-
|
|
3016
|
-
|
|
2578
|
+
};
|
|
2579
|
+
batch(() => {
|
|
2580
|
+
cancelPending(registration);
|
|
2581
|
+
template.swap = {
|
|
2582
|
+
target,
|
|
2583
|
+
held: false,
|
|
2584
|
+
settle: abandon
|
|
2585
|
+
};
|
|
2586
|
+
registration.pending = template;
|
|
2587
|
+
target.successor = template;
|
|
2588
|
+
for (const consumer of target.signals.consumers().values()) {
|
|
2589
|
+
if (!consumer.signals.serving()) continue;
|
|
2590
|
+
cancelSuccessor(consumer);
|
|
2591
|
+
const { service } = createSuccessor(consumer, definition, {
|
|
2592
|
+
template,
|
|
2593
|
+
onSettle: (successor, result) => {
|
|
2594
|
+
waiting.delete(successor);
|
|
2595
|
+
held.delete(successor);
|
|
2596
|
+
if (result.kind === "failed") abandon(result);
|
|
2597
|
+
else if (result.kind !== "swapped") commitIfReady();
|
|
2598
|
+
},
|
|
2599
|
+
ready: (successor) => {
|
|
2600
|
+
waiting.delete(successor);
|
|
2601
|
+
held.add(successor);
|
|
2602
|
+
commitIfReady();
|
|
2603
|
+
}
|
|
2604
|
+
});
|
|
2605
|
+
waiting.add(service);
|
|
2606
|
+
}
|
|
3017
2607
|
});
|
|
3018
|
-
|
|
2608
|
+
commitIfReady();
|
|
2609
|
+
return outcome.promise;
|
|
3019
2610
|
}
|
|
3020
2611
|
/** Settles a replacement for its registration. */
|
|
3021
2612
|
function settleRegistration(registration) {
|
|
@@ -3029,44 +2620,81 @@ function settleRegistration(registration) {
|
|
|
3029
2620
|
* A transaction passes `extra`, and it sets `registration.pending` at its publish.
|
|
3030
2621
|
*/
|
|
3031
2622
|
function replaceRunning(registration, target, definition, extra) {
|
|
3032
|
-
const pending = createSuccessor(target, definition,
|
|
2623
|
+
const pending = createSuccessor(target, definition, {
|
|
2624
|
+
onSettle: settleRegistration(registration),
|
|
2625
|
+
extra
|
|
2626
|
+
});
|
|
3033
2627
|
if (!extra?.transaction) {
|
|
3034
2628
|
cancelPending(registration);
|
|
3035
2629
|
registration.pending = pending.service;
|
|
3036
2630
|
}
|
|
3037
2631
|
return pending;
|
|
3038
2632
|
}
|
|
2633
|
+
/**
|
|
2634
|
+
* Waits for the outcome of the pending replacement `pending`. When `signal` aborts first, it cancels the replacement, as a newer `replace` does.
|
|
2635
|
+
* The cancel settles the outcome as `cancelled`. The listener goes when the outcome settles.
|
|
2636
|
+
*/
|
|
2637
|
+
async function awaitOutcome(registration, pending, outcome, signal) {
|
|
2638
|
+
const removeAbort = onAbort(signal, () => {
|
|
2639
|
+
if (registration.pending === pending) batch(() => cancelPending(registration));
|
|
2640
|
+
});
|
|
2641
|
+
try {
|
|
2642
|
+
return outcomeResult(await outcome, registration.name);
|
|
2643
|
+
} finally {
|
|
2644
|
+
removeAbort();
|
|
2645
|
+
}
|
|
2646
|
+
}
|
|
3039
2647
|
/** The `replace()` of a registration. */
|
|
3040
|
-
async function replace(registration, option) {
|
|
2648
|
+
async function replace(registration, option, options) {
|
|
2649
|
+
const signal = readSignal(options, "replace");
|
|
3041
2650
|
const target = untracked(() => currentService(registration));
|
|
3042
2651
|
if (!target) return false;
|
|
3043
2652
|
const definition = readDefinition(registration.name, option, target);
|
|
2653
|
+
if (signal?.aborted) return false;
|
|
3044
2654
|
const phase = untracked(() => target.signals.phase());
|
|
3045
2655
|
if (phase === "stopping") throw new ReplaceWhileStoppingError(`'${registration.name}' is stopping. Wait until it stops, then call provide.`);
|
|
3046
2656
|
if (target.perConsumer) {
|
|
3047
|
-
|
|
3048
|
-
const
|
|
3049
|
-
|
|
3050
|
-
|
|
2657
|
+
let template;
|
|
2658
|
+
const outcome = untracked(() => {
|
|
2659
|
+
batch(() => {
|
|
2660
|
+
template = createService({
|
|
2661
|
+
container: target.container,
|
|
2662
|
+
kind: "service",
|
|
2663
|
+
name: target.name,
|
|
2664
|
+
edges: definition.edges,
|
|
2665
|
+
build: definition.build,
|
|
2666
|
+
metadata: definition.metadata,
|
|
2667
|
+
policy: definition.policy,
|
|
2668
|
+
exclusive: target.exclusive,
|
|
2669
|
+
exported: target.exported,
|
|
2670
|
+
perConsumer: true,
|
|
2671
|
+
registration
|
|
2672
|
+
});
|
|
2673
|
+
target.container.services.add(template);
|
|
2674
|
+
});
|
|
2675
|
+
return replaceTemplate(registration, target, template);
|
|
2676
|
+
});
|
|
2677
|
+
return awaitOutcome(registration, template, outcome, signal);
|
|
3051
2678
|
}
|
|
3052
2679
|
if (phase !== "running") {
|
|
3053
2680
|
untracked(() => replaceAtOnce(registration, target, definition));
|
|
3054
2681
|
return true;
|
|
3055
2682
|
}
|
|
3056
|
-
|
|
2683
|
+
const pending = untracked(() => replaceRunning(registration, target, definition));
|
|
2684
|
+
return awaitOutcome(registration, pending.service, pending.outcome, signal);
|
|
3057
2685
|
}
|
|
3058
2686
|
const registrations = /* @__PURE__ */ new WeakMap();
|
|
3059
2687
|
/** The registration behind a result of `provide`, or `undefined` for any other value. */
|
|
3060
2688
|
function registrationOf(key) {
|
|
3061
2689
|
return typeof key === "object" && key !== null ? registrations.get(key) : void 0;
|
|
3062
2690
|
}
|
|
3063
|
-
/** Creates the public result of `provide`: the typed key with `withdraw()` and `replace()`. */
|
|
3064
|
-
function registrationApi(registration) {
|
|
2691
|
+
/** Creates the public result of `provide`: the typed key with `withdraw()` and `replace()`. A transaction passes its own `withdraw`. */
|
|
2692
|
+
function registrationApi(registration, overrides = {}) {
|
|
3065
2693
|
const api = Object.freeze({
|
|
3066
2694
|
key: registration.name,
|
|
3067
2695
|
...registration.exclusive ? { exclusive: true } : {},
|
|
3068
|
-
withdraw: () => withdrawRegistration(registration),
|
|
3069
|
-
replace: ((option) => replace(registration, option))
|
|
2696
|
+
withdraw: overrides.withdraw ?? ((options) => withdrawRegistration(registration, options)),
|
|
2697
|
+
replace: ((option, options) => replace(registration, option, options))
|
|
3070
2698
|
});
|
|
3071
2699
|
registrations.set(api, registration);
|
|
3072
2700
|
return api;
|
|
@@ -3096,9 +2724,13 @@ function autoName(stackIndex, nameOrOption, option) {
|
|
|
3096
2724
|
const parentFile = ErrorStackParser.parse(/* @__PURE__ */ new Error("providing"))[index];
|
|
3097
2725
|
return `${parentFile?.fileName ?? `unknown-${nanoid()}`}:${parentFile?.lineNumber ?? 0}:${parentFile?.columnNumber ?? 0}`;
|
|
3098
2726
|
}
|
|
3099
|
-
/**
|
|
3100
|
-
|
|
2727
|
+
/**
|
|
2728
|
+
* Throws `duplicate-service` when a started container already has a registration for `name`,
|
|
2729
|
+
* or when an open transaction of the container, other than `except`, declares or provides `name`.
|
|
2730
|
+
*/
|
|
2731
|
+
function assertNoDuplicate(container, name, except) {
|
|
3101
2732
|
if (slotOf(container, name)() && container.signals.started()) throw new DuplicateServiceError(`The started container already has a registration for '${name}'.`);
|
|
2733
|
+
if (reservedBy(container.transactions, name, except)) throw new DuplicateServiceError(`An open transaction of the container reserves '${name}'.`);
|
|
3102
2734
|
}
|
|
3103
2735
|
/**
|
|
3104
2736
|
* Publishes `service` as the current registration of its key. Call it inside a batch.
|
|
@@ -3129,6 +2761,7 @@ function serviceOf(container, name, definition, exclusive, extra = {}) {
|
|
|
3129
2761
|
metadata: definition.metadata,
|
|
3130
2762
|
policy: definition.policy,
|
|
3131
2763
|
exclusive,
|
|
2764
|
+
exported: definition.exported,
|
|
3132
2765
|
perConsumer: definition.perConsumer,
|
|
3133
2766
|
...extra
|
|
3134
2767
|
});
|
|
@@ -3154,10 +2787,11 @@ function provideIn(container, stackIndex, nameOrOption, option) {
|
|
|
3154
2787
|
assertActive(node, "provide");
|
|
3155
2788
|
const name = typeof nameOrOption === "string" ? nameOrOption : isProvidedKey(nameOrOption) ? normalizeName(nameOrOption) : autoName(stackIndex, nameOrOption, option);
|
|
3156
2789
|
if (typeof name !== "string" || name.length === 0) throw new InvalidArgumentError("A provider name must be a non-empty string.");
|
|
3157
|
-
const
|
|
2790
|
+
const providerObject = normalizeProvideOption(nameOrOption, option);
|
|
2791
|
+
const definition = readDefinition(name, providerObject);
|
|
3158
2792
|
const exclusive = isExclusiveKey(nameOrOption);
|
|
3159
2793
|
const registration = untracked(() => register(node, name, definition, exclusive));
|
|
3160
|
-
container
|
|
2794
|
+
traceLoggerOf(container)?.provide(name, definition.edges.map((edge) => edge.keyName));
|
|
3161
2795
|
return registrationApi(registration);
|
|
3162
2796
|
}
|
|
3163
2797
|
function provide$1(container, nameOrOption, option) {
|
|
@@ -3167,21 +2801,26 @@ function provide$1(container, nameOrOption, option) {
|
|
|
3167
2801
|
/**
|
|
3168
2802
|
* Waits until the dependencies are available and returns their values. Before `start()`, it builds and starts them.
|
|
3169
2803
|
*
|
|
3170
|
-
* Rejects when a required dependency fails.
|
|
2804
|
+
* Rejects when a required dependency fails. With `options.signal`, it rejects when the signal aborts. An aborted signal rejects at once.
|
|
2805
|
+
* Each rejection removes the hidden resolver of the call. The values can become stale after a run-time change. Use it for one-time code.
|
|
3171
2806
|
*/
|
|
3172
|
-
function resolve$1(container, dependsOn) {
|
|
2807
|
+
function resolve$1(container, dependsOn, options) {
|
|
3173
2808
|
const node = nodeOf(container);
|
|
3174
2809
|
assertActive(node, "resolve");
|
|
3175
2810
|
const edges = readEdges("resolve", dependsOn);
|
|
2811
|
+
const signal = readSignal(options, "resolve");
|
|
2812
|
+
if (signal?.aborted) return Promise.reject(waitAbortError(signal.reason, "resolve() timed out."));
|
|
3176
2813
|
const { reconciler } = node.runtime;
|
|
3177
2814
|
return new Promise((done, fail) => untracked(() => {
|
|
3178
2815
|
let settled = false;
|
|
3179
2816
|
let service;
|
|
3180
2817
|
let stopListening = () => {};
|
|
2818
|
+
let disarm = () => {};
|
|
3181
2819
|
const finish = (outcome) => {
|
|
3182
2820
|
if (settled) return;
|
|
3183
2821
|
settled = true;
|
|
3184
2822
|
stopListening();
|
|
2823
|
+
disarm();
|
|
3185
2824
|
batch(() => releaseService(service));
|
|
3186
2825
|
outcome();
|
|
3187
2826
|
};
|
|
@@ -3200,9 +2839,30 @@ function resolve$1(container, dependsOn) {
|
|
|
3200
2839
|
const failure = blockingFailure(service);
|
|
3201
2840
|
if (failure) finish(() => fail(failure.error));
|
|
3202
2841
|
});
|
|
2842
|
+
disarm = onAbort(signal, (reason) => finish(() => fail(waitAbortError(reason, "resolve() timed out."))));
|
|
3203
2843
|
service.withdrawal.promise.then(() => finish(() => fail(new BuildAfterDisposeError("The container was disposed before resolve() got its dependencies."))));
|
|
3204
2844
|
}));
|
|
3205
2845
|
}
|
|
2846
|
+
// src/primitives/timeout.ts
|
|
2847
|
+
function readMilliseconds(ms, action) {
|
|
2848
|
+
if (typeof ms !== "number" || !Number.isFinite(ms) || ms < 0) throw new InvalidArgumentError(`The milliseconds of ${action} must be a finite number of 0 or more. Got ${String(ms)}.`);
|
|
2849
|
+
return ms;
|
|
2850
|
+
}
|
|
2851
|
+
/**
|
|
2852
|
+
* Returns a signal that aborts after `ms` milliseconds of the clock of `container`.
|
|
2853
|
+
*
|
|
2854
|
+
* The reason is a `DOMException` with the name `TimeoutError`, as for `AbortSignal.timeout()`. Use it in place of `AbortSignal.timeout()`, because that one does not use the container clock.
|
|
2855
|
+
* With the system clock, the timer does not keep a Node process alive.
|
|
2856
|
+
*/
|
|
2857
|
+
function timeout(container, ms) {
|
|
2858
|
+
const { clock } = nodeOf(container).runtime;
|
|
2859
|
+
const milliseconds = readMilliseconds(ms, "timeout");
|
|
2860
|
+
return timeoutSignal(clock, milliseconds, () => timeoutReason(`The operation timed out after ${milliseconds}ms.`), true).signal;
|
|
2861
|
+
}
|
|
2862
|
+
/** Returns a signal that aborts when `signal` aborts, or after `ms` milliseconds of the clock of `container`. Without `signal`, it is `timeout(container, ms)`. */
|
|
2863
|
+
function withTimeout(signal, ms, container) {
|
|
2864
|
+
return anySignal(readSignal({ signal }, "withTimeout"), timeout(container, ms));
|
|
2865
|
+
}
|
|
3206
2866
|
// src/primitives/transaction.ts
|
|
3207
2867
|
function readName(key, action) {
|
|
3208
2868
|
let name;
|
|
@@ -3245,7 +2905,7 @@ function createMembers(transaction) {
|
|
|
3245
2905
|
const entries = [...transaction.entries.values()].filter((entry) => entry.definition);
|
|
3246
2906
|
const targets = /* @__PURE__ */ new Map();
|
|
3247
2907
|
for (const entry of entries) if (entry.replace) targets.set(entry, replaceTarget(container, entry));
|
|
3248
|
-
else assertNoDuplicate(container, entry.name);
|
|
2908
|
+
else assertNoDuplicate(container, entry.name, transaction);
|
|
3249
2909
|
const members = [];
|
|
3250
2910
|
batch(() => {
|
|
3251
2911
|
for (const entry of entries) {
|
|
@@ -3268,7 +2928,8 @@ function createMembers(transaction) {
|
|
|
3268
2928
|
const registration = target.registration ?? createRegistration(container, entry.name, target.exclusive);
|
|
3269
2929
|
const service = target.signals.phase() === "running" && !target.perConsumer ? replaceRunning(registration, target, definition, extra).service : serviceOf(container, entry.name, {
|
|
3270
2930
|
...definition,
|
|
3271
|
-
perConsumer: target.perConsumer
|
|
2931
|
+
perConsumer: target.perConsumer,
|
|
2932
|
+
exported: target.exported
|
|
3272
2933
|
}, target.exclusive, {
|
|
3273
2934
|
...extra,
|
|
3274
2935
|
registration
|
|
@@ -3282,22 +2943,17 @@ function createMembers(transaction) {
|
|
|
3282
2943
|
});
|
|
3283
2944
|
return members;
|
|
3284
2945
|
}
|
|
3285
|
-
/** Publishes the members in one batch. A built member goes to `starting`, the others to `pending`. */
|
|
3286
|
-
function publishMembers(members, built) {
|
|
2946
|
+
/** Publishes the members in one batch. A built member goes to `starting`, the others to `pending`. The reservations end in the same batch. */
|
|
2947
|
+
function publishMembers(transaction, members, built) {
|
|
3287
2948
|
batch(() => {
|
|
2949
|
+
releaseReservations(transaction);
|
|
3288
2950
|
for (const { service, target } of members) {
|
|
3289
2951
|
service.transaction = void 0;
|
|
3290
2952
|
const registration = service.registration;
|
|
3291
2953
|
if (target && registration && service.swap) {
|
|
3292
2954
|
cancelPending(registration);
|
|
3293
2955
|
registration.pending = service;
|
|
3294
|
-
} else if (target && registration && target.perConsumer)
|
|
3295
|
-
edges: service.edges,
|
|
3296
|
-
build: service.build,
|
|
3297
|
-
metadata: service.metadata,
|
|
3298
|
-
perConsumer: true,
|
|
3299
|
-
policy: service.policy
|
|
3300
|
-
});
|
|
2956
|
+
} else if (target && registration && target.perConsumer) replaceTemplate(registration, target, service);
|
|
3301
2957
|
else if (target && registration) takeOver(registration, target, service);
|
|
3302
2958
|
else {
|
|
3303
2959
|
publishRegistration(service.container, service);
|
|
@@ -3316,23 +2972,56 @@ async function commit(transaction) {
|
|
|
3316
2972
|
await untracked(() => rollbackMembers(byName, outcome.built));
|
|
3317
2973
|
throw outcome.kind === "failed" ? outcome.error : transaction.abortError;
|
|
3318
2974
|
}
|
|
3319
|
-
untracked(() => publishMembers(members, new Set(outcome.built.keys())));
|
|
2975
|
+
untracked(() => publishMembers(transaction, members, new Set(outcome.built.keys())));
|
|
3320
2976
|
transaction.state = "done";
|
|
3321
2977
|
transaction.container.transactions.delete(transaction);
|
|
3322
2978
|
startMembers(order, outcome.built);
|
|
3323
2979
|
}
|
|
3324
|
-
|
|
2980
|
+
/**
|
|
2981
|
+
* Commits `transaction`. When `signal` aborts before the publish, the transaction aborts with the reason, and the result rejects at once.
|
|
2982
|
+
* The rollback continues: the build in progress gets the abort on its `signal`, and the key reservations end when the rollback settles.
|
|
2983
|
+
*/
|
|
2984
|
+
function startCommit(transaction, options) {
|
|
2985
|
+
let signal;
|
|
2986
|
+
try {
|
|
2987
|
+
signal = readSignal(options, "commit");
|
|
2988
|
+
} catch (error) {
|
|
2989
|
+
return Promise.reject(error);
|
|
2990
|
+
}
|
|
3325
2991
|
if (transaction.state !== "open") return Promise.reject(new InvalidArgumentError(`The transaction is ${transaction.state === "aborted" ? "aborted" : "already committed"}.`));
|
|
2992
|
+
if (signal?.aborted) {
|
|
2993
|
+
abortTransaction(transaction, signal.reason);
|
|
2994
|
+
return Promise.reject(signal.reason);
|
|
2995
|
+
}
|
|
3326
2996
|
transaction.state = "committing";
|
|
3327
|
-
const
|
|
2997
|
+
const committed = commit(transaction).catch((error) => {
|
|
3328
2998
|
if (transaction.state !== "done") {
|
|
3329
2999
|
transaction.state = "aborted";
|
|
3330
3000
|
transaction.container.transactions.delete(transaction);
|
|
3001
|
+
untracked(() => releaseReservations(transaction));
|
|
3331
3002
|
}
|
|
3332
3003
|
throw error;
|
|
3333
3004
|
});
|
|
3334
|
-
transaction.settled =
|
|
3335
|
-
return
|
|
3005
|
+
transaction.settled = committed.then(() => {}, () => {});
|
|
3006
|
+
if (!signal) return committed;
|
|
3007
|
+
return new Promise((resolve, reject) => {
|
|
3008
|
+
const removeAbort = onAbort(signal, (reason) => {
|
|
3009
|
+
if (transaction.state !== "committing") return;
|
|
3010
|
+
abortTransaction(transaction, reason);
|
|
3011
|
+
reject(reason);
|
|
3012
|
+
});
|
|
3013
|
+
committed.then((value) => {
|
|
3014
|
+
removeAbort();
|
|
3015
|
+
resolve(value);
|
|
3016
|
+
}, (error) => {
|
|
3017
|
+
removeAbort();
|
|
3018
|
+
reject(error);
|
|
3019
|
+
});
|
|
3020
|
+
});
|
|
3021
|
+
}
|
|
3022
|
+
/** Throws `duplicate-service` when another open transaction of the container declares or provides `name`. */
|
|
3023
|
+
function assertNotReserved(transaction, name) {
|
|
3024
|
+
if (reservedBy(transaction.container.transactions, name, transaction)) throw new DuplicateServiceError(`Another open transaction of the container reserves '${name}'.`);
|
|
3336
3025
|
}
|
|
3337
3026
|
function assertOpen(transaction, action) {
|
|
3338
3027
|
if (transaction.state !== "open") throw new InvalidArgumentError(`Cannot ${action} after the transaction committed or aborted.`);
|
|
@@ -3348,7 +3037,9 @@ function entryOf$1(transaction, name, exclusive) {
|
|
|
3348
3037
|
replace: false,
|
|
3349
3038
|
target: void 0,
|
|
3350
3039
|
registration: void 0,
|
|
3351
|
-
exclusive
|
|
3040
|
+
exclusive,
|
|
3041
|
+
exported: false,
|
|
3042
|
+
reservation: void 0
|
|
3352
3043
|
};
|
|
3353
3044
|
transaction.entries.set(name, entry);
|
|
3354
3045
|
}
|
|
@@ -3377,17 +3068,44 @@ function transaction(container) {
|
|
|
3377
3068
|
assertOpen(state, "declare");
|
|
3378
3069
|
const name = readName(key, "declare");
|
|
3379
3070
|
const { dependsOn, optional } = readDeclareOptions(name, options);
|
|
3380
|
-
|
|
3381
|
-
|
|
3071
|
+
assertNotReserved(state, name);
|
|
3072
|
+
const declared = readEdges(name, dependsOn);
|
|
3073
|
+
const entry = entryOf$1(state, name, false);
|
|
3074
|
+
entry.exclusive ||= isExclusiveKey(key);
|
|
3075
|
+
entry.declared = declared;
|
|
3382
3076
|
entry.optional = optional;
|
|
3077
|
+
untracked(() => reserveKey(state, entry));
|
|
3078
|
+
};
|
|
3079
|
+
/**
|
|
3080
|
+
* The `withdraw()` of a registration from `tx.provide`. Before the commit, it removes the provide from the transaction and resolves `true`.
|
|
3081
|
+
* A declaration of the key stays, so the commit fails with `transaction-incomplete` unless the declaration is optional.
|
|
3082
|
+
* During the commit, it waits for the commit, then withdraws the published service with `options`.
|
|
3083
|
+
*/
|
|
3084
|
+
const withdrawEntry = async (entry, registration, options) => {
|
|
3085
|
+
if (state.state === "open") {
|
|
3086
|
+
if (entry.registration !== registration) return false;
|
|
3087
|
+
entry.definition = void 0;
|
|
3088
|
+
entry.registration = void 0;
|
|
3089
|
+
entry.exported = false;
|
|
3090
|
+
untracked(() => {
|
|
3091
|
+
if (!entry.declared) {
|
|
3092
|
+
state.entries.delete(entry.name);
|
|
3093
|
+
releaseReservation(entry);
|
|
3094
|
+
} else reserveKey(state, entry);
|
|
3095
|
+
});
|
|
3096
|
+
return true;
|
|
3097
|
+
}
|
|
3098
|
+
if (state.state === "committing") await state.settled;
|
|
3099
|
+
return withdrawRegistration(registration, options);
|
|
3383
3100
|
};
|
|
3384
3101
|
const provide = ((key, option) => {
|
|
3385
3102
|
assertOpen(state, "provide");
|
|
3386
3103
|
const name = readName(key, "provide");
|
|
3387
|
-
|
|
3388
|
-
|
|
3104
|
+
if (state.entries.get(name)?.definition) throw new DuplicateServiceError(`The transaction already provides '${name}'.`);
|
|
3105
|
+
assertNotReserved(state, name);
|
|
3389
3106
|
const read = readDefinition(name, option);
|
|
3390
|
-
const entry = entryOf$1(state, name,
|
|
3107
|
+
const entry = entryOf$1(state, name, false);
|
|
3108
|
+
entry.exclusive ||= isExclusiveKey(key);
|
|
3391
3109
|
const definition = read.edges.length === 0 && entry.declared ? {
|
|
3392
3110
|
...read,
|
|
3393
3111
|
edges: entry.declared
|
|
@@ -3395,7 +3113,9 @@ function transaction(container) {
|
|
|
3395
3113
|
const registration = createRegistration(node, name, entry.exclusive);
|
|
3396
3114
|
entry.definition = definition;
|
|
3397
3115
|
entry.registration = registration;
|
|
3398
|
-
|
|
3116
|
+
entry.exported = definition.exported;
|
|
3117
|
+
untracked(() => reserveKey(state, entry));
|
|
3118
|
+
return registrationApi(registration, { withdraw: (options) => withdrawEntry(entry, registration, options) });
|
|
3399
3119
|
});
|
|
3400
3120
|
const replace = ((key, option) => {
|
|
3401
3121
|
assertOpen(state, "replace");
|
|
@@ -3406,12 +3126,13 @@ function transaction(container) {
|
|
|
3406
3126
|
entry.definition = readDefinition(name, option, untracked(() => target ? currentService(target) : slotOf(node, name)()));
|
|
3407
3127
|
entry.replace = true;
|
|
3408
3128
|
entry.target = target;
|
|
3129
|
+
untracked(() => releaseReservation(entry));
|
|
3409
3130
|
});
|
|
3410
3131
|
return Object.freeze({
|
|
3411
3132
|
declare,
|
|
3412
3133
|
provide,
|
|
3413
3134
|
replace,
|
|
3414
|
-
commit: () => untracked(() => startCommit(state)),
|
|
3135
|
+
commit: (options) => untracked(() => startCommit(state, options)),
|
|
3415
3136
|
abort: () => untracked(() => abortTransaction(state, new TransactionIncompleteError("The transaction was aborted before the commit completed.")))
|
|
3416
3137
|
});
|
|
3417
3138
|
}
|
|
@@ -3458,8 +3179,8 @@ function computeImpact(scope, change) {
|
|
|
3458
3179
|
const gone = (service) => change.removed.has(service) || stopped.has(service);
|
|
3459
3180
|
const renewed = (service) => change.renewed.has(service) || rebuilt.has(service);
|
|
3460
3181
|
/** Tells whether another candidate of the edge serves after the change. */
|
|
3461
|
-
const alternative = (consumer, keyNames) => keyNames.some((keyName) => candidatesOf(consumer.
|
|
3462
|
-
const provider = serviceFor(registration, consumer.
|
|
3182
|
+
const alternative = (consumer, keyNames) => keyNames.some((keyName) => candidatesOf(consumer.scope, keyName, consumer).some((registration) => {
|
|
3183
|
+
const provider = serviceFor(registration, consumer.scope);
|
|
3463
3184
|
return provider !== void 0 && !gone(provider) && !gone(registration) && provider.signals.serving();
|
|
3464
3185
|
}));
|
|
3465
3186
|
let changed = true;
|
|
@@ -3526,7 +3247,8 @@ function entryOf(service) {
|
|
|
3526
3247
|
}),
|
|
3527
3248
|
metadata: service.metadata,
|
|
3528
3249
|
exclusive: service.exclusive,
|
|
3529
|
-
perConsumer: service.perConsumer
|
|
3250
|
+
perConsumer: service.perConsumer,
|
|
3251
|
+
exported: service.exported
|
|
3530
3252
|
});
|
|
3531
3253
|
}
|
|
3532
3254
|
function declaredEntryOf(container, entry) {
|
|
@@ -3537,7 +3259,8 @@ function declaredEntryOf(container, entry) {
|
|
|
3537
3259
|
phase: "declared",
|
|
3538
3260
|
metadata: entry.definition?.metadata ?? emptyMetadata,
|
|
3539
3261
|
exclusive: entry.exclusive,
|
|
3540
|
-
perConsumer: entry.definition?.perConsumer ?? false
|
|
3262
|
+
perConsumer: entry.definition?.perConsumer ?? false,
|
|
3263
|
+
exported: entry.exported
|
|
3541
3264
|
});
|
|
3542
3265
|
}
|
|
3543
3266
|
/** The services and invocations of the scope that are not `withdrawn`, in registration order. */
|
|
@@ -3558,7 +3281,7 @@ function lookup(scope, name) {
|
|
|
3558
3281
|
/** The providers that the edges of `service` select now, served or not. */
|
|
3559
3282
|
function targetsOf(service) {
|
|
3560
3283
|
return service.edges.flatMap((edge) => edge.candidates.flatMap((keyName) => {
|
|
3561
|
-
const provider = findProvider(service.
|
|
3284
|
+
const provider = findProvider(service.scope, keyName, service);
|
|
3562
3285
|
return provider ? [provider] : [];
|
|
3563
3286
|
}));
|
|
3564
3287
|
}
|
|
@@ -3575,7 +3298,7 @@ function dependencyOf(edge, resolution) {
|
|
|
3575
3298
|
keys: edge.candidates,
|
|
3576
3299
|
optional: edge.optional,
|
|
3577
3300
|
fallback: edge.fallback,
|
|
3578
|
-
conditional: edge.condition !== void 0,
|
|
3301
|
+
conditional: edge.condition !== void 0 || edge.candidateConditions !== void 0,
|
|
3579
3302
|
resolution: resolution.kind,
|
|
3580
3303
|
...withoutUndefined({ provider: resolution.provider && entryOf(resolution.provider) })
|
|
3581
3304
|
});
|
|
@@ -3685,7 +3408,8 @@ function proposedChange(scope, change) {
|
|
|
3685
3408
|
/**
|
|
3686
3409
|
* Returns the graph queries of `container`. They cover the container and its descendants. Each query is a pure read.
|
|
3687
3410
|
*
|
|
3688
|
-
* A key resolves as for a consumer in the container: from the container
|
|
3411
|
+
* A key resolves as for a consumer in the container: from the container and the exports of its children, then from each ancestor and the exports of its children.
|
|
3412
|
+
* A key that only a descendant has resolves to the first such service.
|
|
3689
3413
|
*/
|
|
3690
3414
|
function graph(container) {
|
|
3691
3415
|
const scope = nodeOf(container);
|
|
@@ -3729,16 +3453,102 @@ function graph(container) {
|
|
|
3729
3453
|
const service = lookup(scope, name);
|
|
3730
3454
|
if (service) return explainService(service);
|
|
3731
3455
|
const declared = declaredOf(scope).find(({ entry }) => entry.name === name);
|
|
3732
|
-
|
|
3456
|
+
if (declared) return Object.freeze({
|
|
3733
3457
|
name,
|
|
3734
3458
|
kind: "service",
|
|
3735
3459
|
owner: declared.container.owner,
|
|
3736
3460
|
phase: "declared",
|
|
3737
3461
|
blockers: Object.freeze([])
|
|
3738
3462
|
});
|
|
3463
|
+
return Object.freeze({
|
|
3464
|
+
name,
|
|
3465
|
+
kind: "service",
|
|
3466
|
+
owner: scope.owner,
|
|
3467
|
+
phase: void 0,
|
|
3468
|
+
blockers: Object.freeze([Object.freeze({
|
|
3469
|
+
kind: "missing-provider",
|
|
3470
|
+
dependency: name,
|
|
3471
|
+
key: name
|
|
3472
|
+
})])
|
|
3473
|
+
});
|
|
3739
3474
|
})
|
|
3740
3475
|
});
|
|
3741
3476
|
}
|
|
3477
|
+
// src/queries/peek.ts
|
|
3478
|
+
function resultOf(resolution, fallbackKey) {
|
|
3479
|
+
switch (resolution.kind) {
|
|
3480
|
+
case "missing": return Object.freeze({
|
|
3481
|
+
kind: "missing",
|
|
3482
|
+
key: fallbackKey
|
|
3483
|
+
});
|
|
3484
|
+
case "conflict": return Object.freeze({
|
|
3485
|
+
kind: "conflict",
|
|
3486
|
+
key: resolution.provider.name,
|
|
3487
|
+
owners: ownersOf([resolution.provider, ...resolution.rivals])
|
|
3488
|
+
});
|
|
3489
|
+
case "rejected": return Object.freeze({
|
|
3490
|
+
kind: "rejected",
|
|
3491
|
+
key: resolution.provider.name,
|
|
3492
|
+
owner: resolution.provider.container.owner,
|
|
3493
|
+
metadata: resolution.provider.metadata
|
|
3494
|
+
});
|
|
3495
|
+
case "not-running": {
|
|
3496
|
+
const { provider } = resolution;
|
|
3497
|
+
const phase = provider.signals.phase();
|
|
3498
|
+
const owner = (provider.template ?? provider).container.owner;
|
|
3499
|
+
return Object.freeze({
|
|
3500
|
+
kind: "not-running",
|
|
3501
|
+
key: provider.name,
|
|
3502
|
+
owner,
|
|
3503
|
+
phase,
|
|
3504
|
+
...phase === "failed" ? { error: provider.failure?.error } : {}
|
|
3505
|
+
});
|
|
3506
|
+
}
|
|
3507
|
+
case "resolved": {
|
|
3508
|
+
const { provider, instance } = resolution;
|
|
3509
|
+
const owner = (provider.template ?? provider).container.owner;
|
|
3510
|
+
const status = provider.signals.effective();
|
|
3511
|
+
if (!status || status.status === "unavailable") return Object.freeze({
|
|
3512
|
+
kind: "unavailable",
|
|
3513
|
+
key: provider.name,
|
|
3514
|
+
owner,
|
|
3515
|
+
instanceId: instance.id,
|
|
3516
|
+
...withoutUndefined({
|
|
3517
|
+
reason: status?.reason,
|
|
3518
|
+
cause: status?.cause
|
|
3519
|
+
})
|
|
3520
|
+
});
|
|
3521
|
+
return Object.freeze({
|
|
3522
|
+
kind: "resolved",
|
|
3523
|
+
key: provider.name,
|
|
3524
|
+
owner,
|
|
3525
|
+
value: instance.value,
|
|
3526
|
+
status: status.status,
|
|
3527
|
+
instanceId: instance.id
|
|
3528
|
+
});
|
|
3529
|
+
}
|
|
3530
|
+
}
|
|
3531
|
+
}
|
|
3532
|
+
/**
|
|
3533
|
+
* Reads the value of a dependency now, without a wait. It is a pure read: it does not demand, build, or start a service.
|
|
3534
|
+
*
|
|
3535
|
+
* The dependency resolves as for a consumer in `container`: a key, `where(key, condition)`, or `firstAvailable(...)`. An `optional` marker has no effect.
|
|
3536
|
+
* The result is the value with its status, or the reason why no value is available now. A `perConsumer` key has a value only after something demanded its service for `container`.
|
|
3537
|
+
*/
|
|
3538
|
+
function peek(container, dependency) {
|
|
3539
|
+
const node = nodeOf(container);
|
|
3540
|
+
const [edge] = readEdges("peek", { dependency });
|
|
3541
|
+
if (!edge) throw new InvalidArgumentError("peek() needs a dependency.");
|
|
3542
|
+
return untracked(() => {
|
|
3543
|
+
const consumer = {
|
|
3544
|
+
container: node,
|
|
3545
|
+
scope: node,
|
|
3546
|
+
name: "peek",
|
|
3547
|
+
template: void 0
|
|
3548
|
+
};
|
|
3549
|
+
return resultOf(edge.fallback ? resolveFallback(consumer, edge) : resolvePlain(consumer, edge), edge.candidates[0] ?? edge.keyName);
|
|
3550
|
+
});
|
|
3551
|
+
}
|
|
3742
3552
|
// src/queries/listeners.ts
|
|
3743
3553
|
/**
|
|
3744
3554
|
* Returns the current listeners for `eventKey` in `container` and its descendants, in registration order.
|
|
@@ -3776,15 +3586,15 @@ function provide(nameOrOption, option) {
|
|
|
3776
3586
|
function invoke(option) {
|
|
3777
3587
|
return invokeIn(globalContainer, option);
|
|
3778
3588
|
}
|
|
3779
|
-
function resolve(dependsOn) {
|
|
3780
|
-
return resolve$1(globalContainer, dependsOn);
|
|
3589
|
+
function resolve(dependsOn, options) {
|
|
3590
|
+
return resolve$1(globalContainer, dependsOn, options);
|
|
3781
3591
|
}
|
|
3782
3592
|
function start(options) {
|
|
3783
3593
|
return start$1(globalContainer, options);
|
|
3784
3594
|
}
|
|
3785
|
-
function stop() {
|
|
3786
|
-
return stop$1(globalContainer);
|
|
3595
|
+
function stop(options) {
|
|
3596
|
+
return stop$1(globalContainer, options);
|
|
3787
3597
|
}
|
|
3788
|
-
export {
|
|
3598
|
+
export { createNoopLogger as A, effect$1 as C, systemClock as D, waitAbortError as E, createDefaultLogger as O, dispose as S, readSignal as T, listen as _, resolve as a, invoke$1 as b, stop as c, graph as d, transaction as f, provide$1 as g, resolve$1 as h, resetContainer as i, createLoggLogger as k, listeners as l, withTimeout as m, invoke as n, setLogger as o, timeout as p, provide as r, start as s, getContainer as t, peek as u, start$1 as v, onAbort as w, createContainer as x, stop$1 as y };
|
|
3789
3599
|
|
|
3790
|
-
//# sourceMappingURL=global-
|
|
3600
|
+
//# sourceMappingURL=global-B5N-d-TO.mjs.map
|