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.
@@ -1,6 +1,5 @@
1
- import { a as DuplicateServiceError, d as ReentrantDisposeError, f as ReplaceWhileStoppingError, h as TransactionIncompleteError, i as BuildAfterDisposeError, l as InvalidArgumentError, m as StartTimeoutError, n as defaultDecision, o as HookOutsideBuildError, p as StaleRegistrationError, r as validateDecision, s as HookTimeoutError, t as createDecide } from "./decision-odiPb2Au.mjs";
2
- import { computed, effect, effectScope, endBatch, getActiveSub, setActiveSub, signal, startBatch } from "alien-signals";
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/signals.ts
94
- /**
95
- * Runs `fn` with tracking paused.
96
- *
97
- * The tracking rule: each entry point pauses tracking once. The entry points are the public API calls, the reconciler drain, timer callbacks, and the calls of user callbacks.
98
- * Code inside an entry point reads signals directly. Only the bodies of `computed` and `effect` track.
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
- return candidates;
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
- * Selects the provider of `keyName` for a consumer in `container`.
108
+ * Calls `callback` after `ms` milliseconds of `clock`, like `clock.setTimeout`. Returns a function that cancels the call.
221
109
  *
222
- * A nearer registration shadows an ancestor only while it is `running`. Until then, the first serving registration wins.
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 selectProvider(container, keyName, consumer) {
226
- const candidates = candidatesOf(container, keyName, consumer);
227
- for (const registration of candidates) {
228
- const provider = serviceFor(registration, container);
229
- if (provider?.signals.serving()) return {
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
- /** Tells whether each required edge resolves to an available provider. A pending service builds only then. */
260
- function requiredAvailable(service) {
261
- return service.edges.every((edge) => {
262
- if (edge.optional) return true;
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
- /** Tells whether a required edge no longer resolves: its provider was withdrawn, or it does not run. */
268
- function requiredWithdrawn(service) {
269
- return service.edges.some((edge) => !edge.optional && resolvedInstance(service, edge) === void 0);
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
- /** Tells whether the current resolution of a required edge differs from the bindings of `instance`. */
272
- function requiredChanged(service, instance) {
273
- return service.edges.some((edge) => !edge.optional && resolvedInstance(service, edge) !== instance.bindings.get(edge.name));
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
- * Finds the failure that blocks a pending service: a required provider in `failed`, directly or through pending providers.
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 blockingFailure(service, visited = /* @__PURE__ */ new Set()) {
288
- visited.add(service);
289
- for (const edge of service.edges) {
290
- if (edge.optional) continue;
291
- const resolution = resolutionOf(service, edge);
292
- if (resolution.kind !== "not-running" || visited.has(resolution.provider)) continue;
293
- const provider = resolution.provider;
294
- const phase = provider.signals.phase();
295
- if (phase === "failed") return provider.failure;
296
- if (phase === "pending") {
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
- // src/graph/nodes/container.ts
303
- const nodes = /* @__PURE__ */ new WeakMap();
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
- * Makes `service` visible to consumers: the slot of its key, or the consumer map of its `perConsumer` registration.
329
- * An exclusive conflict counts each published service with a key.
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 publishService(service) {
332
- const { template } = service;
333
- if (template) {
334
- const consumers = template.signals.consumers();
335
- if (consumers.get(service.container) !== service) template.signals.consumers(new Map([...consumers, [service.container, service]]));
336
- return;
337
- }
338
- slotOf(service.container, service.name)(service);
339
- addDeclaration(service);
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
- * Clears the slot and marks the service absent. The reconciler stops it after its holders release it.
343
- * A `perConsumer` registration also releases the service of each consumer container.
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 releaseService(service) {
346
- service.signals.present(false);
347
- if (service.kind !== "service" || service.template) return;
348
- for (const consumer of service.signals.consumers().values()) consumer.signals.present(false);
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
- promise: new Promise((done) => {
372
- resolve = done;
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
- * Tells whether `condition` accepts the provider registration.
435
- * A thrown error counts as `false` and goes to the observe stream of the consumer.
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
- verdict = untracked(() => condition(providerInfo(registration))) === true;
203
+ signal = options?.signal;
448
204
  } catch (error) {
449
- verdict = false;
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
- byProvider.set(registration, verdict);
453
- return verdict;
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
- /** Resolves a plain or an optional edge. It has one candidate key. */
490
- function resolvePlain(consumer, edge) {
491
- const [keyName] = edge.candidates;
492
- return keyName === void 0 ? { kind: "missing" } : resolveKey(consumer, keyName, edge.condition);
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
- * Resolves a `firstAvailable` edge. The first serving and available candidate wins.
497
- * If no serving candidate is available, the first serving candidate stays selected, so that a running dependent does not lose it.
498
- * Without a serving candidate, the edge reports the first candidate that exists.
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 resolveFallback(consumer, edge) {
501
- let firstUnresolved;
502
- let firstServing;
503
- for (const keyName of edge.candidates) {
504
- const resolution = resolveKey(consumer, keyName, edge.condition);
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 firstServing ?? firstUnresolved ?? { kind: "missing" };
514
- }
515
- // src/graph/status.ts
516
- const statusRank = {
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
- function sameResolution(a, b) {
615
- if (a.kind !== b.kind) return false;
616
- switch (a.kind) {
617
- case "missing": return true;
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
- * Marks a fallback dependency. It resolves to the first provider in the list whose effective status is available.
243
+ * Waits for `promise` until `signal` aborts. An aborted signal gives `aborted` at once.
806
244
  *
807
- * A change of the selection is a change of identity: the dependent gets `dependency-change`.
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 readEdges(owner, dependsOn) {
880
- if (dependsOn === void 0 || dependsOn === null) return Object.freeze([]);
881
- if (typeof dependsOn !== "object") throw new InvalidArgumentError(`The dependsOn option of '${owner}' is not an object.`);
882
- let entries;
883
- try {
884
- entries = Object.entries(dependsOn);
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
- throw new InvalidArgumentError(`The dependency '${name}' of '${owner}' is not a name, a key, an optional marker, a fallback marker, or a condition marker.`);
897
- }));
898
- }
899
- // src/graph/nodes/policy.ts
900
- const emptyPolicy = Object.freeze({});
901
- function readMaxAttempts(owner, value) {
902
- if (value !== void 0 && (typeof value !== "number" || !Number.isInteger(value) || value < 1)) throw new InvalidArgumentError(`The maxAttempts option of ${owner} must be an integer of 1 or more. Got ${String(value)}.`);
903
- return value;
904
- }
905
- function readTimeout$1(owner, hook, value) {
906
- if (value !== void 0 && (typeof value !== "number" || !Number.isFinite(value) || value < 0)) throw new InvalidArgumentError(`The timeouts.${hook} option of ${owner} must be a finite number of 0 or more. Got ${String(value)}.`);
907
- return value;
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
- /** Only services with a key write to the logger. Invocations and resolvers do not. */
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.logger : void 0;
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
- /** Starts a hook run. Its signal aborts when a newer target supersedes it, when it times out, or when the instance is disposed. */
1408
- function beginHookRun(instance) {
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 parent = instance.controller.signal;
1411
- const forward = () => controller.abort(parent.reason);
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
- parent.removeEventListener("abort", forward);
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. A value that is not a decision of this hook, a promise, a throw, or no handler gives `fail` with `hook-timeout`.
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 decide.fail(error, { reason: error.message });
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) ?? decide.fail(error, { reason: error.message });
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. When it passes, injeca records it, applies the `onTimeout` decision, and stops waiting.
1579
- * 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 cannot block that disposal.
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.now() + timeoutMs;
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
- /** Waits for one callback until the deadline. `undefined` means that the deadline passed. */
1589
- const settle = async (promise) => {
1590
- if (deadline === void 0) return { value: await promise };
1591
- if (timedOut) {
1592
- promise.catch(() => {});
1593
- return;
1594
- }
1595
- const result = await raceTimeout(clock, promise, Math.max(0, deadline - clock.now()));
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, instance.controller.signal, {}, call));
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
- * At the timeout, the run aborts, later handlers do not run, and the `onTimeout` decision replaces the result. Late decisions are recorded as ignored.
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 run = beginHookRun(instance);
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 timedOut = false;
1144
+ let abandoned = false;
1748
1145
  const running = runHandlers(service, instance, "start", run.signal, {}, {
1749
1146
  from: instance.startFrom,
1750
1147
  progress,
1751
- stopped: () => timedOut
1148
+ stopped: () => abandoned
1752
1149
  });
1753
- const timeoutMs = timeoutOf(service, "start");
1754
- const result = timeoutMs === void 0 ? {
1755
- kind: "settled",
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) recordDecision(service, instance, decision, true);
1161
+ for (const decision of late) recordLateDecision(service, instance, decision, [run.signal]);
1768
1162
  });
1769
- return [decideTimeout(service, instance, "start", timeoutMs)];
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 decisions = handlers.length > instance.startFrom ? await runAttempt(service, instance) : [];
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) completeSwap(service);
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, condition, members, built) {
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) || condition && !acceptsProvider(service, keyName, condition, member)) return void 0;
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, condition);
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.condition, members, built));
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) enterStopping(service, {
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
- enterStopping(service, {
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 logger = options.enabled === false ? createNoopLogger() : options.logger ?? parent?.api.logger ?? createDefaultLogger();
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 === "function") node.effects.push(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 synchronous part withdraws all services of the subtree in the caller's batch, so that dependents stop without a rebuild.
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
- const withdrawn = /* @__PURE__ */ new Set();
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
- for (const service of [...container.services]) {
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
- * A call from the cleanup chain of the same container rejects with `reentrant-dispose`. A second call from outside gets the same promise.
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
- if (context.isActive(node)) return Promise.reject(new ReentrantDisposeError("dispose() was called from the cleanup chain of the same container."));
2564
- if (node.disposal) return node.disposal;
2565
- return untracked(() => {
2566
- let disposal;
2061
+ const reentry = untracked(() => findReentry(node, "dispose", abortSignal));
2062
+ const disposal = node.disposal ?? untracked(() => {
2063
+ let started;
2567
2064
  batch(() => {
2568
- disposal = beginDisposal(node, context.current());
2065
+ started = beginDisposal(node, context.current());
2569
2066
  });
2570
- return disposal;
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.logger.invoke(edges.map((edge) => edge.keyName));
2614
- return Object.freeze({ withdraw: () => withdrawService(registered) });
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
- * With `timeoutMs`, it rejects with `start-timeout` and the explanation of each service that is not `running`.
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, timeoutMs) {
2763
- const { clock, reconciler } = containers[0].runtime;
2328
+ function waitUntilSettled(containers, check, signal, onAborted) {
2329
+ const { reconciler } = containers[0].runtime;
2764
2330
  return new Promise((done, fail) => {
2765
- let cancelTimeout = () => {};
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
- cancelTimeout();
2340
+ removeAbort();
2771
2341
  if (result.kind === "done") done();
2772
2342
  else fail(errorFrom(result.errors, "Start failed."));
2773
2343
  });
2774
- if (timeoutMs !== void 0) cancelTimeout = clock.setTimeout(() => untracked(() => {
2344
+ removeAbort = onAbort(signal, (reason) => {
2775
2345
  stopListening();
2776
- const explanations = presentServices(containers).filter((service) => service.signals.phase() !== "running").map(explainService);
2777
- fail(new StartTimeoutError(`start() timed out after ${timeoutMs}ms. These services are not running:\n${describeExplanations(explanations)}`, explanations));
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 `timeoutMs` is set.
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 timeoutMs = readTimeout(options);
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
- }, timeoutMs);
2822
- }).then(() => container.logger.running());
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
- async function stop$1(container) {
2831
- const node = nodeOf(container);
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)) service.signals.demanded(false);
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
- * Moves the consumer services of the `perConsumer` registration `target` to `template`, in the caller's batch.
2981
- * Each running consumer service gets a new service that swaps in after it completes `start`. The others leave and build again on demand.
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 moveConsumers(registration, target, template, definition) {
2984
- const consumers = target.signals.consumers();
2985
- target.signals.consumers(/* @__PURE__ */ new Map());
2986
- takeOver(registration, target, template);
2987
- const swaps = [];
2988
- const kept = /* @__PURE__ */ new Map();
2989
- for (const [container, consumer] of consumers) {
2990
- consumer.template = template;
2991
- if (consumer.signals.serving()) {
2992
- kept.set(container, consumer);
2993
- swaps.push(createSuccessor(consumer, definition, template, () => {}));
2994
- } else consumer.signals.present(false);
2995
- }
2996
- template.signals.consumers(kept);
2997
- return swaps;
2998
- }
2999
- /** Replaces a `perConsumer` registration. The new registration takes the slot at once. Its consumer services swap in one by one. */
3000
- function replaceRegistrationTemplate(registration, target, definition) {
3001
- let swaps = [];
3002
- batch(() => {
3003
- const template = createService({
3004
- container: target.container,
3005
- kind: "service",
3006
- name: target.name,
3007
- edges: definition.edges,
3008
- build: definition.build,
3009
- metadata: definition.metadata,
3010
- policy: definition.policy,
3011
- exclusive: target.exclusive,
3012
- perConsumer: true,
3013
- registration
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
- target.container.services.add(template);
3016
- swaps = moveConsumers(registration, target, template, definition);
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
- return swaps;
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, void 0, settleRegistration(registration), extra);
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
- const swaps = untracked(() => replaceRegistrationTemplate(registration, target, definition));
3048
- const errors = (await Promise.all(swaps.map((swap) => swap.outcome))).flatMap((outcome) => outcome.kind === "failed" ? [outcome.error] : []);
3049
- if (errors.length > 0) throw errorFrom(errors, `The replacement of '${registration.name}' failed for some consumers.`);
3050
- return true;
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
- return outcomeResult(await untracked(() => replaceRunning(registration, target, definition)).outcome, registration.name);
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
- /** Throws `duplicate-service` when a started container already has a registration for `name`. */
3100
- function assertNoDuplicate(container, name) {
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 definition = readDefinition(name, normalizeProvideOption(nameOrOption, option));
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.logger.provide(name, definition.edges.map((edge) => edge.keyName));
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. The values can become stale after a run-time change. Use it for one-time code.
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) moveConsumers(registration, target, service, {
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
- function startCommit(transaction) {
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 result = commit(transaction).catch((error) => {
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 = result.then(() => {}, () => {});
3335
- return result;
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
- const entry = entryOf$1(state, name, isExclusiveKey(key));
3381
- entry.declared = readEdges(name, dependsOn);
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
- const existing = state.entries.get(name);
3388
- if (existing?.definition) throw new DuplicateServiceError(`The transaction already provides '${name}'.`);
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, isExclusiveKey(key) || existing?.exclusive === true);
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
- return registrationApi(registration);
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.container, keyName, consumer).some((registration) => {
3462
- const provider = serviceFor(registration, consumer.container);
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.container, keyName, 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, then from its ancestors. A key that only a descendant has resolves to the first such service.
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
- return declared && Object.freeze({
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 { assertActive as A, firstAvailable as C, statusFields as D, where as E, createDefaultLogger as F, createLoggLogger as I, createNoopLogger as L, findProvider as M, untracked as N, reportCallbackError as O, withOwner as P, defineKey as S, optional as T, invoke$1 as _, resolve as a, effect$1 as b, stop as c, transaction as d, resolve$1 as f, stop$1 as g, start$1 as h, resetContainer as i, nodeOf as j, withoutUndefined as k, listeners as l, listen as m, invoke as n, setLogger as o, provide$1 as p, provide as r, start as s, getContainer as t, graph as u, createContainer as v, normalizeName as w, systemClock as x, dispose as y };
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-C0Nk0pU8.mjs.map
3600
+ //# sourceMappingURL=global-B5N-d-TO.mjs.map