injeca 0.2.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.
@@ -0,0 +1,3600 @@
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";
3
+ import ErrorStackParser from "error-stack-parser";
4
+ import { nanoid } from "nanoid";
5
+ // package.json
6
+ var name = "injeca";
7
+ // src/diagnostics/logger.ts
8
+ function formatDuration(ms) {
9
+ if (ms < 1) return `${(ms * 1e3).toFixed(3)}µs`;
10
+ if (ms < 1e3) return `${ms.toFixed(3)}ms`;
11
+ return `${(ms / 1e3).toFixed(3)}s`;
12
+ }
13
+ function createDefaultLogger() {
14
+ return {
15
+ provide: (name, dependencies) => {
16
+ const depsStr = dependencies.length > 0 ? ` (depends on: ${dependencies.join(", ")})` : "";
17
+ console.log(`[${name}] PROVIDE ${name}${depsStr}`);
18
+ },
19
+ invoke: (dependencies) => {
20
+ const depsStr = dependencies.length > 0 ? ` (depends on: ${dependencies.join(", ")})` : "";
21
+ console.log(`[${name}] INVOKE${depsStr}`);
22
+ },
23
+ beforeRun: (name) => {
24
+ console.log(`[${name}] BEFORE RUN provide: ${name}()`);
25
+ },
26
+ run: (name, duration) => {
27
+ console.log(`[${name}] RUN provide: ${name}() in ${formatDuration(duration)}`);
28
+ },
29
+ hookOnStart: (name) => {
30
+ console.log(`[${name}] HOOK OnStart ${name}() executing`);
31
+ },
32
+ hookOnStartComplete: (name, duration) => {
33
+ console.log(`[${name}] HOOK OnStart ${name}() ran successfully in ${formatDuration(duration)}`);
34
+ },
35
+ hookOnStop: (name) => {
36
+ console.log(`[${name}] HOOK OnStop ${name}() executing`);
37
+ },
38
+ hookOnStopComplete: (name, duration) => {
39
+ console.log(`[${name}] HOOK OnStop ${name}() ran successfully in ${formatDuration(duration)}`);
40
+ },
41
+ running: () => {
42
+ console.log(`[${name}] RUNNING`);
43
+ },
44
+ warn: (message) => {
45
+ console.warn(`[${name}] WARN ${message}`);
46
+ }
47
+ };
48
+ }
49
+ function createLoggLogger(logg) {
50
+ return {
51
+ provide: (name, dependencies) => {
52
+ const depsStr = dependencies.length > 0 ? ` (depends on: ${dependencies.join(", ")})` : "";
53
+ logg.log(`PROVIDE ${name}${depsStr}`);
54
+ },
55
+ invoke: (dependencies) => {
56
+ const depsStr = dependencies.length > 0 ? ` (depends on: ${dependencies.join(", ")})` : "";
57
+ logg.log(`INVOKE${depsStr}`);
58
+ },
59
+ beforeRun: (name) => {
60
+ logg.log(`BEFORE RUN provide: ${name}()`);
61
+ },
62
+ run: (name, duration) => {
63
+ logg.log(`RUN provide: ${name}() in ${formatDuration(duration)}`);
64
+ },
65
+ hookOnStart: (name) => {
66
+ logg.log(`HOOK OnStart ${name}() executing`);
67
+ },
68
+ hookOnStartComplete: (name, duration) => {
69
+ logg.log(`HOOK OnStart ${name}() ran successfully in ${formatDuration(duration)}`);
70
+ },
71
+ hookOnStop: (name) => {
72
+ logg.log(`HOOK OnStop ${name}() executing`);
73
+ },
74
+ hookOnStopComplete: (name, duration) => {
75
+ logg.log(`HOOK OnStop ${name}() ran successfully in ${formatDuration(duration)}`);
76
+ },
77
+ running: () => {
78
+ logg.log(`RUNNING`);
79
+ },
80
+ warn: (message) => {
81
+ logg.warn(message);
82
+ }
83
+ };
84
+ }
85
+ function createNoopLogger() {
86
+ return {
87
+ provide: () => {},
88
+ invoke: () => {},
89
+ beforeRun: () => {},
90
+ run: () => {},
91
+ hookOnStart: () => {},
92
+ hookOnStartComplete: () => {},
93
+ hookOnStop: () => {},
94
+ hookOnStopComplete: () => {},
95
+ running: () => {},
96
+ warn: () => {}
97
+ };
98
+ }
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);
105
+ }
106
+ });
107
+ /**
108
+ * Calls `callback` after `ms` milliseconds of `clock`, like `clock.setTimeout`. Returns a function that cancels the call.
109
+ *
110
+ * With `systemClock`, the timer does not keep a Node process alive, as for `AbortSignal.timeout()`. Other clocks get a plain `setTimeout` call.
111
+ */
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);
117
+ }
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");
122
+ }
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";
126
+ }
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;
130
+ }
131
+ /**
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.
134
+ */
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;
145
+ }
146
+ const listener = () => {
147
+ release();
148
+ controller.abort(source.reason);
149
+ };
150
+ source.addEventListener("abort", listener, { once: true });
151
+ listeners.push([source, listener]);
152
+ }
153
+ return release;
154
+ }
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();
157
+ /**
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.
161
+ */
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;
184
+ }
185
+ /**
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()`.
189
+ */
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);
194
+ return {
195
+ signal: controller.signal,
196
+ cancel
197
+ };
198
+ }
199
+ /** Reads and checks the `signal` option of `options` once. `action` names the call in errors. */
200
+ function readSignal(options, action) {
201
+ let signal;
202
+ try {
203
+ signal = options?.signal;
204
+ } catch (error) {
205
+ throw new InvalidArgumentError(`Cannot read the ${action} options.`, { cause: error });
206
+ }
207
+ if (signal !== void 0 && !(signal instanceof AbortSignal)) throw new InvalidArgumentError(`The signal option of ${action} must be an AbortSignal.`);
208
+ return signal;
209
+ }
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);
216
+ }
217
+ /**
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.
221
+ */
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));
227
+ }
228
+ return new Promise((resolve, reject) => {
229
+ const remove = onAbort(signal, (reason) => {
230
+ promise.catch(() => {});
231
+ reject(reasonOf(reason));
232
+ });
233
+ promise.then((value) => {
234
+ remove();
235
+ resolve(value);
236
+ }, (error) => {
237
+ remove();
238
+ reject(error);
239
+ });
240
+ });
241
+ }
242
+ /**
243
+ * Waits for `promise` until `signal` aborts. An aborted signal gives `aborted` at once.
244
+ *
245
+ * A rejection of `promise` before the abort rejects the result. After the abort, nothing waits for `promise`, and its rejection is not reported.
246
+ */
247
+ function raceAbort(promise, signal) {
248
+ return new Promise((resolve, reject) => {
249
+ if (signal.aborted) {
250
+ promise.catch(() => {});
251
+ resolve({ kind: "aborted" });
252
+ return;
253
+ }
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);
267
+ });
268
+ });
269
+ }
270
+ // src/helpers/cleanup-context.ts
271
+ function activeIn(frame, owner) {
272
+ for (let current = frame; current; current = current.parent) if (current.active && current.owner === owner) return true;
273
+ return false;
274
+ }
275
+ function createSynchronousContext() {
276
+ let currentFrame;
277
+ return {
278
+ current: () => currentFrame,
279
+ run: (frame, fn) => {
280
+ const previous = currentFrame;
281
+ currentFrame = frame;
282
+ try {
283
+ return fn();
284
+ } finally {
285
+ currentFrame = previous;
286
+ }
287
+ },
288
+ isActive: (owner) => activeIn(currentFrame, owner)
289
+ };
290
+ }
291
+ function createCleanupContext() {
292
+ return createSynchronousContext();
293
+ }
294
+ // src/helpers/delivery-queue.ts
295
+ function createDeliveryQueue() {
296
+ const queue = [];
297
+ let draining = false;
298
+ function deliver(deliveries) {
299
+ queue.push(...deliveries);
300
+ if (draining) return;
301
+ draining = true;
302
+ try {
303
+ while (queue.length > 0) {
304
+ const { run, onError } = queue.shift();
305
+ untracked(() => {
306
+ try {
307
+ run();
308
+ } catch (error) {
309
+ onError(error);
310
+ }
311
+ });
312
+ }
313
+ } finally {
314
+ draining = false;
315
+ }
316
+ }
317
+ return { deliver };
318
+ }
319
+ // src/reconciler/build-context.ts
320
+ const serviceHooks = /* @__PURE__ */ new Set([
321
+ "start",
322
+ "stop",
323
+ "dependency-change",
324
+ "dependency-status-change"
325
+ ]);
326
+ const statuses = /* @__PURE__ */ new Set([
327
+ "ready",
328
+ "degraded",
329
+ "unavailable"
330
+ ]);
331
+ /** A cause chain longer than this is cut. It protects against a cyclic cause object. */
332
+ const maxCauseDepth = 32;
333
+ function readOwner$1(value) {
334
+ try {
335
+ const copy = structuredClone(value);
336
+ if (typeof copy === "object" && copy !== null && !Array.isArray(copy)) return Object.freeze(copy);
337
+ } catch (error) {
338
+ throw new InvalidArgumentError("The owner of a status cause must be a serializable record.", { cause: error });
339
+ }
340
+ throw new InvalidArgumentError("The owner of a status cause must be a serializable record.");
341
+ }
342
+ function readStatusValue(value, what) {
343
+ if (typeof value !== "string" || !statuses.has(value)) throw new InvalidArgumentError(`${what} must be 'ready', 'degraded', or 'unavailable'. Got ${String(value)}.`);
344
+ return value;
345
+ }
346
+ function readReason(value, what) {
347
+ if (value !== void 0 && typeof value !== "string") throw new InvalidArgumentError(`${what} must be a string.`);
348
+ return value;
349
+ }
350
+ /** Reads a caller-owned cause chain once, into frozen records. */
351
+ function readCause(value, depth = 0) {
352
+ if (value === void 0 || depth >= maxCauseDepth) return void 0;
353
+ if (typeof value !== "object" || value === null) throw new InvalidArgumentError("A status cause must be an object with key, owner, and status.");
354
+ let fields;
355
+ try {
356
+ const source = value;
357
+ fields = {
358
+ key: source.key,
359
+ owner: source.owner,
360
+ status: source.status,
361
+ reason: source.reason,
362
+ cause: source.cause
363
+ };
364
+ } catch (error) {
365
+ throw new InvalidArgumentError("Cannot read the status cause.", { cause: error });
366
+ }
367
+ if (typeof fields.key !== "string" || fields.key.length === 0) throw new InvalidArgumentError("The key of a status cause must be a non-empty string.");
368
+ const reason = readReason(fields.reason, "The reason of a status cause");
369
+ const cause = readCause(fields.cause, depth + 1);
370
+ return Object.freeze({
371
+ key: fields.key,
372
+ owner: readOwner$1(fields.owner ?? {}),
373
+ status: readStatusValue(fields.status, "The status of a status cause"),
374
+ ...withoutUndefined({
375
+ reason,
376
+ cause
377
+ })
378
+ });
379
+ }
380
+ /** Reads the arguments of `setStatus` once. */
381
+ function readOwnStatus(service, status, reasonOrOptions) {
382
+ const checked = readStatusValue(status, `The status of '${service.name}'`);
383
+ if (reasonOrOptions === void 0 || typeof reasonOrOptions === "string") return Object.freeze({
384
+ status: checked,
385
+ ...withoutUndefined({ reason: reasonOrOptions })
386
+ });
387
+ if (typeof reasonOrOptions !== "object" || reasonOrOptions === null) throw new InvalidArgumentError(`The second argument of setStatus for '${service.name}' must be a string or an object.`);
388
+ let fields;
389
+ try {
390
+ const options = reasonOrOptions;
391
+ fields = {
392
+ reason: options.reason,
393
+ cause: options.cause
394
+ };
395
+ } catch (error) {
396
+ throw new InvalidArgumentError(`Cannot read the setStatus options of '${service.name}'.`, { cause: error });
397
+ }
398
+ const reason = readReason(fields.reason, `The setStatus reason of '${service.name}'`);
399
+ const cause = readCause(fields.cause);
400
+ return Object.freeze({
401
+ status: checked,
402
+ ...withoutUndefined({
403
+ reason,
404
+ cause
405
+ })
406
+ });
407
+ }
408
+ /** Creates the context that `build` gets. `on` is valid only while the build runs. `setStatus` stays valid after it. */
409
+ function createBuildContext(service, instance) {
410
+ const dependsOn = {};
411
+ for (const edge of service.edges) {
412
+ const binding = instance.bindings.get(edge.name);
413
+ const state = instance.handled.get(edge.name);
414
+ dependsOn[edge.name] = edge.optional ? binding && state ? Object.freeze({
415
+ value: binding.value,
416
+ status: state.status
417
+ }) : void 0 : binding?.value;
418
+ }
419
+ return Object.freeze({
420
+ container: service.container.api,
421
+ name: service.name,
422
+ instanceId: instance.id,
423
+ ...withoutUndefined({ consumer: service.consumer }),
424
+ dependsOn: Object.freeze(dependsOn),
425
+ on: (hook, handler) => {
426
+ if (!instance.building) throw new HookOutsideBuildError(`'on' was called for '${service.name}' after its build settled.`);
427
+ if (typeof hook !== "string" || !serviceHooks.has(hook)) throw new InvalidArgumentError(`'${String(hook)}' is not a service hook.`);
428
+ if (typeof handler !== "function") throw new InvalidArgumentError(`The '${hook}' handler of '${service.name}' is not a function.`);
429
+ instance.hooks[hook].push(handler);
430
+ },
431
+ effect: (setup) => {
432
+ if (instance.disposed) throw new BuildAfterDisposeError(`'effect' was called for '${service.name}' after its instance was disposed.`);
433
+ if (typeof setup !== "function") throw new InvalidArgumentError(`The effect setup of '${service.name}' is not a function.`);
434
+ const disposer = untracked(setup);
435
+ if (typeof disposer === "function") instance.effects.push(disposer);
436
+ },
437
+ signal: instance.controller.signal,
438
+ setStatus: (status, reasonOrOptions) => untracked(() => {
439
+ const next = readOwnStatus(service, status, reasonOrOptions);
440
+ if (instance.disposed) {
441
+ emitRecord(service.container, {
442
+ type: "ignored",
443
+ call: "setStatus",
444
+ key: service.name,
445
+ owner: service.container.owner,
446
+ instanceId: instance.id,
447
+ reason: `setStatus('${next.status}') was called after the instance was disposed.`
448
+ });
449
+ return;
450
+ }
451
+ if (!sameStatus(instance.signals.own(), next)) instance.signals.own(next);
452
+ })
453
+ });
454
+ }
455
+ // src/reconciler/calls.ts
456
+ /** Calls a user callback with tracking paused. A synchronous throw becomes a rejection. */
457
+ function invokeUser(fn) {
458
+ try {
459
+ return Promise.resolve(untracked(fn));
460
+ } catch (error) {
461
+ return Promise.reject(error);
462
+ }
463
+ }
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. */
469
+ function loggerOf(service) {
470
+ return service.kind === "service" ? traceLoggerOf(service.container.api) : void 0;
471
+ }
472
+ function callListeners(listeners, ...args) {
473
+ for (const listener of [...listeners]) try {
474
+ listener(...args);
475
+ } catch (error) {
476
+ queueMicrotask(() => {
477
+ throw error;
478
+ });
479
+ }
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
+ }
494
+ // src/reconciler/holds.ts
495
+ function addHolder(provider, holder) {
496
+ const holders = provider.signals.holders();
497
+ if (!holders.has(holder)) provider.signals.holders(/* @__PURE__ */ new Set([...holders, holder]));
498
+ }
499
+ function removeHolder(provider, holder) {
500
+ const holders = provider.signals.holders();
501
+ if (holders.has(holder)) provider.signals.holders(new Set([...holders].filter((service) => service !== holder)));
502
+ }
503
+ /** Registers `service` as a holder of each bound instance and of each extra hold, and releases the rest. */
504
+ function syncHolds(service, instance) {
505
+ const wanted = /* @__PURE__ */ new Set([...instance.bindings.values(), ...instance.extraHolds]);
506
+ for (const provider of [...instance.held]) if (!wanted.has(provider)) {
507
+ removeHolder(provider, service);
508
+ instance.held.delete(provider);
509
+ }
510
+ for (const provider of wanted) if (!instance.held.has(provider)) {
511
+ addHolder(provider, service);
512
+ instance.held.add(provider);
513
+ }
514
+ }
515
+ function releaseExtraHolds(service, instance) {
516
+ instance.extraHolds.clear();
517
+ batch(() => syncHolds(service, instance));
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
+ }
628
+ // src/reconciler/snapshot.ts
629
+ /** The current snapshot of all dependencies. */
630
+ function currentSnapshot(service) {
631
+ const snapshot = /* @__PURE__ */ new Map();
632
+ for (const edge of service.edges) {
633
+ const resolution = resolutionOf(service, edge);
634
+ snapshot.set(edge.name, resolution.kind === "resolved" ? {
635
+ state: Object.freeze({
636
+ provider: resolution.provider.name,
637
+ instanceId: resolution.instance.id,
638
+ status: resolution.provider.signals.effective()?.status ?? "unavailable"
639
+ }),
640
+ instance: resolution.instance
641
+ } : void 0);
642
+ }
643
+ return snapshot;
644
+ }
645
+ /** The difference between the last handled snapshot and `snapshot`. An identity change wins over a status change. */
646
+ function diffSnapshot(service, handled, snapshot) {
647
+ const changes = [];
648
+ for (const edge of service.edges) {
649
+ const from = handled.get(edge.name);
650
+ const to = snapshot.get(edge.name)?.state;
651
+ if (from?.instanceId !== to?.instanceId || from && to && from.status !== to.status) changes.push(Object.freeze({
652
+ key: edge.name,
653
+ from,
654
+ to
655
+ }));
656
+ }
657
+ return changes;
658
+ }
659
+ function isIdentityChange(change) {
660
+ return change.from?.instanceId !== change.to?.instanceId;
661
+ }
662
+ function describeChanges(changes) {
663
+ return changes.map((change) => `${change.key}: ${change.from ? `${change.from.instanceId}/${change.from.status}` : "-"} -> ${change.to ? `${change.to.instanceId}/${change.to.status}` : "-"}`).join(", ");
664
+ }
665
+ function hookDependsOn(service, snapshot) {
666
+ const dependsOn = {};
667
+ for (const edge of service.edges) {
668
+ const entry = snapshot.get(edge.name);
669
+ dependsOn[edge.name] = entry ? Object.freeze({
670
+ ...entry.state,
671
+ value: entry.instance.value
672
+ }) : void 0;
673
+ }
674
+ return Object.freeze(dependsOn);
675
+ }
676
+ /** Marks `snapshot` as handled and moves the bindings to its instances. */
677
+ function acceptSnapshot(service, instance, snapshot) {
678
+ instance.extraHolds.clear();
679
+ instance.handled = new Map([...snapshot].map(([name, entry]) => [name, entry?.state]));
680
+ instance.bindings.clear();
681
+ for (const [name, entry] of snapshot) if (entry) instance.bindings.set(name, entry.instance);
682
+ batch(() => syncHolds(service, instance));
683
+ }
684
+ // src/reconciler/hook-run.ts
685
+ function hookStateOf(instance, hook) {
686
+ let state = instance.hookStates.get(hook);
687
+ if (!state) {
688
+ state = {
689
+ attempt: 1,
690
+ previous: void 0
691
+ };
692
+ instance.hookStates.set(hook, state);
693
+ }
694
+ return state;
695
+ }
696
+ /** A requeue keeps the attempt count. Any other decision resets it. */
697
+ function finishRun(instance, hook, decision) {
698
+ const state = hookStateOf(instance, hook);
699
+ state.previous = decision;
700
+ state.attempt = decision.kind === "requeue" ? state.attempt + 1 : 1;
701
+ }
702
+ function recordDecision(service, instance, decision, ignored) {
703
+ emitRecord(service.container, {
704
+ type: "decision",
705
+ key: service.name,
706
+ owner: service.container.owner,
707
+ instanceId: instance.id,
708
+ hook: decision.hook,
709
+ decision: decision.kind,
710
+ attempt: decision.attempt,
711
+ ignored,
712
+ ...withoutUndefined({ reason: decision.reason })
713
+ });
714
+ }
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) {
735
+ const controller = new AbortController();
736
+ const release = forwardAbort(controller, timeout ? [instance.controller.signal, timeout] : [instance.controller.signal]);
737
+ recordSignalOrigin(controller.signal, instance);
738
+ instance.hookRun = controller;
739
+ return {
740
+ signal: controller.signal,
741
+ end: () => {
742
+ release();
743
+ if (instance.hookRun === controller) instance.hookRun = void 0;
744
+ }
745
+ };
746
+ }
747
+ /** Calls one handler with a new hook context. A throw becomes `fail`. A value that is not a decision counts as `undefined`. A decision from another run, or of a kind that the hook does not accept, becomes `fail` with `invalid-decision`. */
748
+ async function callHandler(service, instance, hook, handler, runSignal, extras, call = invokeUser) {
749
+ const state = hookStateOf(instance, hook);
750
+ const run = {
751
+ service: service.name,
752
+ instanceId: instance.id,
753
+ hook,
754
+ attempt: state.attempt,
755
+ now: () => service.container.runtime.clock.now()
756
+ };
757
+ const hookContext = Object.freeze({
758
+ decide: createDecide(run),
759
+ attempt: run.attempt,
760
+ previous: state.previous,
761
+ signal: runSignal,
762
+ ...extras
763
+ });
764
+ try {
765
+ const value = await call(() => handler(hookContext));
766
+ return validateDecision(run, value) ?? defaultDecision(run);
767
+ } catch (error) {
768
+ return createDecide(run).fail(error);
769
+ }
770
+ }
771
+ /** Runs the handlers of `hook` in order. It stops at the first decision that is not `handled`, unless `all` is set. */
772
+ async function runHandlers(service, instance, hook, runSignal, extras, options = {}) {
773
+ const decisions = [];
774
+ const handlers = [...instance.hooks[hook]];
775
+ for (let index = options.from ?? 0; index < handlers.length; index++) {
776
+ if (options.stopped?.()) break;
777
+ if (options.progress) options.progress.index = index;
778
+ const decision = await callHandler(service, instance, hook, handlers[index], runSignal, extras);
779
+ decisions.push(decision);
780
+ if (!options.all && decision.kind !== "handled") break;
781
+ }
782
+ return decisions;
783
+ }
784
+ /**
785
+ * Records a timeout of a `start` or `stop` hook in the observe stream, and returns the decision of the `onTimeout` handler.
786
+ *
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.
789
+ */
790
+ function decideTimeout(service, instance, hook, timeoutMs) {
791
+ const state = hookStateOf(instance, hook);
792
+ const run = {
793
+ service: service.name,
794
+ instanceId: instance.id,
795
+ hook,
796
+ attempt: state.attempt,
797
+ now: () => service.container.runtime.clock.now()
798
+ };
799
+ const decide = createDecide(run);
800
+ const error = new HookTimeoutError(`The '${hook}' hook of '${service.name}' did not settle within ${timeoutMs}ms.`, hook, timeoutMs);
801
+ emitRecord(service.container, {
802
+ type: "timeout",
803
+ key: service.name,
804
+ owner: service.container.owner,
805
+ instanceId: instance.id,
806
+ hook,
807
+ timeoutMs,
808
+ attempt: state.attempt,
809
+ reason: error.message
810
+ });
811
+ const fallback = () => hook === "stop" ? decide.handled({ reason: error.message }) : decide.fail(error, { reason: error.message });
812
+ const handler = timeoutHandlerOf(service);
813
+ if (!handler) return fallback();
814
+ try {
815
+ const value = untracked(() => handler(Object.freeze({
816
+ hook,
817
+ timeoutMs,
818
+ attempt: state.attempt,
819
+ previous: state.previous,
820
+ decide
821
+ })));
822
+ return validateDecision(run, value) ?? fallback();
823
+ } catch (handlerError) {
824
+ return decide.fail(handlerError, { reason: `The onTimeout handler of '${service.name}' threw.` });
825
+ }
826
+ }
827
+ // src/reconciler/requeue.ts
828
+ /**
829
+ * The delay of a requeue in milliseconds.
830
+ *
831
+ * `after` is a fixed delay. An exponential backoff doubles `initial` for each attempt, up to `max`.
832
+ * `jitter` is a fraction from 0 to 1. It takes a random part of that size away from the delay, so the delay is between `delay * (1 - jitter)` and `delay`.
833
+ */
834
+ function requeueDelay(decision, random) {
835
+ if (decision.after !== void 0) return Math.max(0, decision.after);
836
+ if (decision.backoff) {
837
+ const { initial, max, jitter } = decision.backoff;
838
+ const delay = Math.max(0, Math.min(max, initial * 2 ** (decision.attempt - 1)));
839
+ if (jitter === void 0) return delay;
840
+ return delay * (1 - Math.min(1, Math.max(0, jitter)) * random());
841
+ }
842
+ return 0;
843
+ }
844
+ /**
845
+ * The stop intent of a requeue that reached its attempt limit. The service stops, then goes to `failed` with the last reason.
846
+ * The limit comes from the decision, then the service, then the containers. It is `undefined` when the requeue can wait.
847
+ */
848
+ function exhaustedIntent(service, decision) {
849
+ const maxAttempts = maxAttemptsOf(service, decision.maxAttempts);
850
+ if (maxAttempts === void 0 || decision.attempt < maxAttempts) return void 0;
851
+ const message = decision.reason ?? `The '${decision.hook}' hook of '${service.name}' reached maxAttempts (${maxAttempts}).`;
852
+ return {
853
+ failure: { error: new Error(message, { cause: decision }) },
854
+ reason: message,
855
+ decision: "requeue"
856
+ };
857
+ }
858
+ /** Waits before the next run of a requeued hook. When the wait ends, the service goes back to the work queue. */
859
+ function scheduleRequeue(service, instance, decision) {
860
+ const { clock, random, reconciler } = service.container.runtime;
861
+ const wait = { cancel: () => {} };
862
+ wait.cancel = clock.setTimeout(() => {
863
+ if (instance.requeueWait !== wait) return;
864
+ instance.requeueWait = void 0;
865
+ reconciler.markDirty(service);
866
+ }, requeueDelay(decision, random));
867
+ instance.requeueWait = wait;
868
+ }
869
+ function cancelRequeueWait(instance) {
870
+ instance.requeueWait?.cancel();
871
+ instance.requeueWait = void 0;
872
+ }
873
+ // src/reconciler/stop.ts
874
+ function mergeIntent(current, next) {
875
+ if (current.failure) return current;
876
+ if (next.failure) return next;
877
+ return {
878
+ parkEpoch: next.parkEpoch ?? current.parkEpoch,
879
+ reason: next.reason ?? current.reason,
880
+ decision: next.decision ?? current.decision
881
+ };
882
+ }
883
+ function enterStopping(service, intent) {
884
+ service.stopIntent = mergeIntent(service.stopIntent, intent);
885
+ setPhase(service, "stopping", {
886
+ reason: service.stopIntent.reason,
887
+ decision: service.stopIntent.decision
888
+ });
889
+ service.container.runtime.reconciler.markDirty(service);
890
+ }
891
+ function reconcileStopping(service) {
892
+ const instance = service.signals.instance();
893
+ if (!instance) {
894
+ settleAfterStop(service, []);
895
+ return;
896
+ }
897
+ if (instance.signals.holders().size > 0) return;
898
+ teardown(service, instance, true);
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
+ }
908
+ /**
909
+ * Runs the `stop` hooks in reverse order, then the effect disposers in reverse order.
910
+ *
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.
915
+ */
916
+ async function runTeardownCallbacks(service, instance, stopHooks, frame) {
917
+ const errors = [];
918
+ const { clock, context } = service.container.runtime;
919
+ const call = (fn) => context.run(frame, () => invokeUser(fn));
920
+ const timeoutMs = timeoutOf(service, "stop");
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);
923
+ let timedOut = false;
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);
935
+ if (result.kind === "settled") return { value: result.value };
936
+ abandon();
937
+ timedOut = true;
938
+ service.container.api.logger.warn(stopTimeoutWarning(service, timeoutMs));
939
+ const decision = decideTimeout(service, instance, "stop", timeoutMs);
940
+ recordDecision(service, instance, decision, false);
941
+ finishRun(instance, "stop", decision);
942
+ if (decision.kind === "fail") errors.push(decision.error);
943
+ };
944
+ if (stopHooks && instance.hooks.stop.length > 0) {
945
+ const logger = loggerOf(service);
946
+ logger?.hookOnStop(service.name);
947
+ const startTime = performance.now();
948
+ for (const handler of [...instance.hooks.stop].reverse()) {
949
+ const settled = await settle(callHandler(service, instance, "stop", handler, hookSignal, {}, call), "stop", (late) => recordLateDecision(service, instance, late, [hookSignal, instance.controller.signal]));
950
+ if (!settled) continue;
951
+ const decision = settled.value;
952
+ recordDecision(service, instance, decision, false);
953
+ finishRun(instance, "stop", decision);
954
+ if (decision.kind === "fail") errors.push(decision.error);
955
+ }
956
+ logger?.hookOnStopComplete(service.name, performance.now() - startTime);
957
+ }
958
+ const effects = instance.effects.splice(0).reverse();
959
+ for (const disposer of effects) try {
960
+ await settle(call(disposer), "effect");
961
+ } catch (error) {
962
+ errors.push(error);
963
+ }
964
+ deadline?.cancel();
965
+ return errors;
966
+ }
967
+ async function teardown(service, instance, stopHooks, phase) {
968
+ service.busy = true;
969
+ instance.tearingDown = true;
970
+ cancelRequeueWait(instance);
971
+ instance.extraHolds.clear();
972
+ if (phase) setPhase(service, phase, { reason: service.stopIntent.reason });
973
+ const frame = serviceFrame(service, service.container.frame);
974
+ const errors = await service.container.runtime.context.run(frame, () => runTeardownCallbacks(service, instance, stopHooks, frame));
975
+ frame.active = false;
976
+ instance.disposed = true;
977
+ instance.controller.abort(/* @__PURE__ */ new Error(`The instance '${instance.id}' of '${service.name}' was disposed.`));
978
+ service.busy = false;
979
+ service.container.runtime.reconciler.notifyTeardown(service, errors);
980
+ batch(() => {
981
+ instance.bindings.clear();
982
+ syncHolds(service, instance);
983
+ settleAfterStop(service, errors);
984
+ service.signals.instance(void 0);
985
+ });
986
+ }
987
+ function settleAfterStop(service, errors) {
988
+ const intent = service.stopIntent;
989
+ service.stopIntent = {};
990
+ const { swap } = service;
991
+ if (swap) {
992
+ const failure = intent.failure ?? (errors.length > 0 ? { error: errors[0] } : void 0);
993
+ service.swap = void 0;
994
+ if (failure) {
995
+ service.failure = failure;
996
+ setPhase(service, "failed", {
997
+ reason: intent.reason ?? "The replacement failed. The old instance stays.",
998
+ decision: intent.decision
999
+ });
1000
+ swap.settle({
1001
+ kind: "failed",
1002
+ error: failure.error
1003
+ });
1004
+ } else swap.settle(service.signals.active() ? { kind: "cancelled" } : { kind: "stopping" });
1005
+ service.signals.present(false);
1006
+ service.teardownErrors.push(...errors);
1007
+ finalize(service);
1008
+ return;
1009
+ }
1010
+ if (!service.signals.present()) {
1011
+ service.teardownErrors.push(...errors);
1012
+ finalize(service);
1013
+ return;
1014
+ }
1015
+ if (intent.failure || errors.length > 0) {
1016
+ service.failure = intent.failure ?? { error: errors[0] };
1017
+ setPhase(service, "failed", {
1018
+ reason: intent.reason,
1019
+ decision: intent.decision
1020
+ });
1021
+ return;
1022
+ }
1023
+ service.parkedEpoch = intent.parkEpoch;
1024
+ setPhase(service, "pending", {
1025
+ reason: intent.reason,
1026
+ decision: intent.decision
1027
+ });
1028
+ service.container.runtime.reconciler.markDirty(service);
1029
+ }
1030
+ function finalize(service) {
1031
+ const { swap, template } = service;
1032
+ service.swap = void 0;
1033
+ swap?.settle({ kind: "cancelled" });
1034
+ service.disposeEffects();
1035
+ service.container.services.delete(service);
1036
+ batch(() => {
1037
+ removeDeclaration(service);
1038
+ if (template) {
1039
+ const consumers = template.signals.consumers();
1040
+ if (consumers.get(service.container) === service) template.signals.consumers(new Map([...consumers].filter(([, consumer]) => consumer !== service)));
1041
+ }
1042
+ setPhase(service, "withdrawn");
1043
+ });
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
+ };
1096
+ }
1097
+ /**
1098
+ * Cancels a replacement that cannot complete: the old service was withdrawn, or the container stopped.
1099
+ * The new service leaves. The old service does not change.
1100
+ */
1101
+ function checkSwap(service) {
1102
+ const { swap } = service;
1103
+ if (!swap || !service.signals.present()) return;
1104
+ if (!swap.target.signals.present()) {
1105
+ swap.settle({ kind: "cancelled" });
1106
+ service.signals.present(false);
1107
+ } else if (!service.signals.active()) {
1108
+ swap.settle({ kind: "stopping" });
1109
+ service.signals.present(false);
1110
+ }
1111
+ }
1112
+ /**
1113
+ * Swaps a new service that completed `start` in for the old one, in one batch.
1114
+ *
1115
+ * The new service takes the slot and goes to `running`. The old service leaves.
1116
+ * It stops and disposes after each holder releases it. Dependents follow the rule for a replaced dependency.
1117
+ */
1118
+ function completeSwap(service) {
1119
+ const swap = service.swap;
1120
+ if (!swap) return;
1121
+ const { target } = swap;
1122
+ service.swap = void 0;
1123
+ batch(() => {
1124
+ target.replacedBy = service;
1125
+ publishService(service);
1126
+ target.signals.present(false);
1127
+ setPhase(service, "running", { reason: `The new instance replaced '${target.signals.instance()?.id ?? target.name}'.` });
1128
+ });
1129
+ swap.settle({ kind: "swapped" });
1130
+ }
1131
+ // src/reconciler/start.ts
1132
+ /**
1133
+ * Runs the `start` handlers of one attempt, from `instance.startFrom`, within the configured `start` timeout.
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.
1138
+ */
1139
+ async function runAttempt(service, 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);
1143
+ const progress = { index: instance.startFrom };
1144
+ let abandoned = false;
1145
+ const running = runHandlers(service, instance, "start", run.signal, {}, {
1146
+ from: instance.startFrom,
1147
+ progress,
1148
+ stopped: () => abandoned
1149
+ });
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;
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;
1160
+ running.then((late) => {
1161
+ for (const decision of late) recordLateDecision(service, instance, decision, [run.signal]);
1162
+ });
1163
+ if (!timedOut) return {
1164
+ decisions: [],
1165
+ superseded: true
1166
+ };
1167
+ return {
1168
+ decisions: [decideTimeout(service, instance, "start", timeoutMs)],
1169
+ superseded: false
1170
+ };
1171
+ }
1172
+ /**
1173
+ * Runs the `start` hooks of a built instance. The service moves to `running`, waits in `retrying` for a requeue, or stops.
1174
+ *
1175
+ * A requeue keeps the instance and its bindings. The next attempt runs the requeued handler again, then the later ones. `build` does not run again.
1176
+ */
1177
+ async function runStart(service, instance) {
1178
+ service.busy = true;
1179
+ const handlers = instance.hooks.start;
1180
+ const logger = handlers.length > 0 ? loggerOf(service) : void 0;
1181
+ if (instance.startFrom === 0) logger?.hookOnStart(service.name);
1182
+ const startTime = performance.now();
1183
+ const attempt = handlers.length > instance.startFrom ? await runAttempt(service, instance) : {
1184
+ decisions: [],
1185
+ superseded: false
1186
+ };
1187
+ const { decisions } = attempt;
1188
+ service.busy = false;
1189
+ const ignored = attempt.superseded || service.signals.targetLost();
1190
+ for (const decision of decisions) recordDecision(service, instance, decision, ignored);
1191
+ if (ignored) {
1192
+ enterStopping(service, { reason: "A newer target superseded the start." });
1193
+ return;
1194
+ }
1195
+ const decision = decisions.at(-1);
1196
+ if (!decision || decision.kind === "handled") {
1197
+ service.demanders.clear();
1198
+ if (decision) finishRun(instance, "start", decision);
1199
+ logger?.hookOnStartComplete(service.name, performance.now() - startTime);
1200
+ if (service.swap?.ready) {
1201
+ service.swap.held = true;
1202
+ service.swap.ready(service);
1203
+ } else if (service.swap) completeSwap(service);
1204
+ else setPhase(service, "running");
1205
+ return;
1206
+ }
1207
+ finishRun(instance, "start", decision);
1208
+ switch (decision.kind) {
1209
+ case "stop":
1210
+ enterStopping(service, {
1211
+ parkEpoch: epochOf(service, (edge) => instance.bindings.get(edge.name)),
1212
+ reason: decision.reason,
1213
+ decision: "stop"
1214
+ });
1215
+ return;
1216
+ case "fail":
1217
+ enterStopping(service, {
1218
+ failure: { error: decision.error },
1219
+ reason: decision.reason,
1220
+ decision: "fail"
1221
+ });
1222
+ return;
1223
+ case "requeue": {
1224
+ const exhausted = exhaustedIntent(service, decision);
1225
+ if (exhausted) {
1226
+ enterStopping(service, exhausted);
1227
+ return;
1228
+ }
1229
+ setPhase(service, "retrying", {
1230
+ reason: decision.reason,
1231
+ decision: "requeue"
1232
+ });
1233
+ scheduleRequeue(service, instance, decision);
1234
+ }
1235
+ }
1236
+ }
1237
+ // src/reconciler/build.ts
1238
+ /**
1239
+ * Tells whether each optional edge has a final answer for a `resolve` call: no provider, a running provider, or a failed one.
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.
1241
+ */
1242
+ function optionalSettled(service) {
1243
+ return service.edges.every((edge) => {
1244
+ if (!edge.optional) return true;
1245
+ const resolution = resolutionOf(service, edge);
1246
+ return resolution.kind !== "not-running" || resolution.provider.signals.phase() === "failed";
1247
+ });
1248
+ }
1249
+ /** Builds a pending service when it is active and its required dependencies are available. An absent one is withdrawn. */
1250
+ function reconcilePending(service) {
1251
+ if (!service.signals.present()) {
1252
+ finalize(service);
1253
+ return;
1254
+ }
1255
+ if (inConflict(service)) {
1256
+ setPhase(service, "conflict", { reason: `The exclusive key '${service.name}' has more than one declaration.` });
1257
+ return;
1258
+ }
1259
+ if (!service.signals.active()) return;
1260
+ demandProviders(service);
1261
+ if (!requiredAvailable(service) || service.kind === "resolver" && !optionalSettled(service)) return;
1262
+ if (service.parkedEpoch !== void 0) {
1263
+ if (epochOf(service, (edge) => resolvedInstance(service, edge)) === service.parkedEpoch) return;
1264
+ service.parkedEpoch = void 0;
1265
+ }
1266
+ beginBuild(service);
1267
+ }
1268
+ function beginBuild(service) {
1269
+ const instance = prepareInstance(service, currentSnapshot(service));
1270
+ runBuild(service, instance).then((result) => completeBuild(service, instance, result));
1271
+ }
1272
+ /** Creates the instance of a build from a snapshot of its dependencies, and moves the service to `building`. The service holds each bound provider. */
1273
+ function prepareInstance(service, snapshot) {
1274
+ const bindings = /* @__PURE__ */ new Map();
1275
+ const handled = /* @__PURE__ */ new Map();
1276
+ for (const [name, entry] of snapshot) {
1277
+ handled.set(name, entry?.state);
1278
+ if (entry) bindings.set(name, entry.instance);
1279
+ }
1280
+ const instance = {
1281
+ id: service.container.runtime.nextInstanceId(),
1282
+ service,
1283
+ bindings,
1284
+ extraHolds: /* @__PURE__ */ new Set(),
1285
+ held: /* @__PURE__ */ new Set(),
1286
+ hooks: {
1287
+ "start": [],
1288
+ "stop": [],
1289
+ "dependency-change": [],
1290
+ "dependency-status-change": []
1291
+ },
1292
+ hookStates: /* @__PURE__ */ new Map(),
1293
+ handled,
1294
+ effects: [],
1295
+ controller: new AbortController(),
1296
+ signals: {
1297
+ holders: signal(/* @__PURE__ */ new Set()),
1298
+ own: signal(Object.freeze({ status: "ready" }))
1299
+ },
1300
+ hookRun: void 0,
1301
+ requeueWait: void 0,
1302
+ startFrom: 0,
1303
+ value: void 0,
1304
+ building: true,
1305
+ tearingDown: false,
1306
+ disposed: false
1307
+ };
1308
+ recordSignalOrigin(instance.controller.signal, instance);
1309
+ service.busy = true;
1310
+ batch(() => {
1311
+ syncHolds(service, instance);
1312
+ service.signals.instance(instance);
1313
+ setPhase(service, "building");
1314
+ });
1315
+ return instance;
1316
+ }
1317
+ /** Runs `build` for a prepared instance. It never rejects. */
1318
+ function runBuild(service, instance) {
1319
+ const logger = loggerOf(service);
1320
+ logger?.beforeRun(service.name);
1321
+ const startTime = performance.now();
1322
+ const buildContext = createBuildContext(service, instance);
1323
+ return invokeUser(() => service.build(buildContext)).then((value) => {
1324
+ logger?.run(service.name, performance.now() - startTime);
1325
+ return {
1326
+ ok: true,
1327
+ value
1328
+ };
1329
+ }, (error) => ({
1330
+ ok: false,
1331
+ error
1332
+ }));
1333
+ }
1334
+ function completeBuild(service, instance, result) {
1335
+ instance.building = false;
1336
+ service.busy = false;
1337
+ if (!result.ok) {
1338
+ service.stopIntent = mergeIntent(service.stopIntent, {
1339
+ failure: { error: result.error },
1340
+ reason: "The build failed."
1341
+ });
1342
+ teardown(service, instance, false);
1343
+ return;
1344
+ }
1345
+ if (buildLost(service, instance)) {
1346
+ service.stopIntent = mergeIntent(service.stopIntent, { reason: "The target changed during the build. The result is discarded." });
1347
+ teardown(service, instance, false, "stopping");
1348
+ return;
1349
+ }
1350
+ instance.value = result.value;
1351
+ setPhase(service, "starting");
1352
+ runStart(service, instance);
1353
+ }
1354
+ // src/reconciler/commit.ts
1355
+ /**
1356
+ * Orders the members so that each member comes after the members that it depends on.
1357
+ * Members in a cycle come last, in registration order. They cannot build in the transaction.
1358
+ */
1359
+ function orderMembers(members) {
1360
+ const order = [];
1361
+ const placed = /* @__PURE__ */ new Set();
1362
+ const remaining = [...members.values()];
1363
+ let progress = true;
1364
+ while (progress) {
1365
+ progress = false;
1366
+ for (const member of remaining) {
1367
+ if (placed.has(member)) continue;
1368
+ if (member.edges.every((edge) => edge.candidates.every((keyName) => {
1369
+ const provider = members.get(keyName);
1370
+ return !provider || provider === member || placed.has(provider);
1371
+ }))) {
1372
+ placed.add(member);
1373
+ order.push(member);
1374
+ progress = true;
1375
+ }
1376
+ }
1377
+ }
1378
+ return [...order, ...remaining.filter((member) => !placed.has(member))];
1379
+ }
1380
+ function memberEntry(member, built) {
1381
+ const instance = built.get(member);
1382
+ if (!instance) return void 0;
1383
+ return {
1384
+ state: Object.freeze({
1385
+ provider: member.name,
1386
+ instanceId: instance.id,
1387
+ status: instance.signals.own().status
1388
+ }),
1389
+ instance
1390
+ };
1391
+ }
1392
+ /** Resolves one key inside a transaction: a built member first, then the published providers. */
1393
+ function resolveInTransaction(service, keyName, edge, members, built) {
1394
+ const member = members.get(keyName);
1395
+ if (member && member !== service) {
1396
+ if (inConflict(member) || !acceptsCandidate(service, edge, keyName, member)) return void 0;
1397
+ return memberEntry(member, built);
1398
+ }
1399
+ const resolution = resolveKey(service, keyName, edge);
1400
+ if (resolution.kind !== "resolved") return void 0;
1401
+ return {
1402
+ state: Object.freeze({
1403
+ provider: resolution.provider.name,
1404
+ instanceId: resolution.instance.id,
1405
+ status: resolution.provider.signals.effective()?.status ?? "unavailable"
1406
+ }),
1407
+ instance: resolution.instance
1408
+ };
1409
+ }
1410
+ /** The snapshot of a member for its build in the transaction. It is `undefined` when a required dependency is not available. */
1411
+ function transactionSnapshot(service, members, built) {
1412
+ const snapshot = /* @__PURE__ */ new Map();
1413
+ for (const edge of service.edges) {
1414
+ const entries = edge.candidates.map((keyName) => resolveInTransaction(service, keyName, edge, members, built));
1415
+ const entry = entries.find((candidate) => candidate !== void 0 && candidate.state.status !== "unavailable") ?? entries.find((candidate) => candidate !== void 0);
1416
+ if (!edge.optional && (!entry || entry.state.status === "unavailable")) return void 0;
1417
+ snapshot.set(edge.name, entry);
1418
+ }
1419
+ return snapshot;
1420
+ }
1421
+ /**
1422
+ * Builds the members of a transaction in `order`. Nothing is published, and no `start` hook runs.
1423
+ *
1424
+ * A member whose required dependencies are not available, that is not active, or whose key is in conflict, does not build. It goes to `pending` at the publish.
1425
+ * The first failed build ends the loop. An abort ends it after the build in progress settles.
1426
+ */
1427
+ async function buildMembers(transaction, order, members) {
1428
+ const built = /* @__PURE__ */ new Map();
1429
+ for (const member of order) {
1430
+ if (transaction.state === "aborted") return {
1431
+ kind: "aborted",
1432
+ built
1433
+ };
1434
+ if (member.perConsumer || !member.signals.active() || inConflict(member)) continue;
1435
+ const snapshot = transactionSnapshot(member, members, built);
1436
+ if (!snapshot) continue;
1437
+ const instance = prepareInstance(member, snapshot);
1438
+ transaction.controllers.add(instance.controller);
1439
+ const result = await runBuild(member, instance);
1440
+ transaction.controllers.delete(instance.controller);
1441
+ instance.building = false;
1442
+ built.set(member, instance);
1443
+ if (!result.ok) return {
1444
+ kind: "failed",
1445
+ error: result.error,
1446
+ built
1447
+ };
1448
+ instance.value = result.value;
1449
+ }
1450
+ return transaction.state === "aborted" ? {
1451
+ kind: "aborted",
1452
+ built
1453
+ } : {
1454
+ kind: "built",
1455
+ built
1456
+ };
1457
+ }
1458
+ /**
1459
+ * Removes all members after a failed build or an abort. A built instance runs its effect disposers. No `stop` hook runs, because no `start` hook ran.
1460
+ * Resolves when each member is `withdrawn`.
1461
+ */
1462
+ async function rollbackMembers(members, built) {
1463
+ for (const member of members.values()) member.signals.present(false);
1464
+ await Promise.all([...members.values()].map((member) => {
1465
+ const instance = built.get(member);
1466
+ if (instance) return teardown(member, instance, false);
1467
+ finalize(member);
1468
+ }));
1469
+ }
1470
+ /**
1471
+ * Runs the `start` hooks of the built members after the publish, in dependency order.
1472
+ * A member whose target changed, or whose dependency did not reach `running`, is discarded without its `stop` hooks. It builds again later.
1473
+ */
1474
+ async function startMembers(order, built) {
1475
+ for (const member of order) {
1476
+ const instance = built.get(member);
1477
+ if (!instance) continue;
1478
+ if (member.signals.targetLost() || requiredChanged(member, instance)) await teardown(member, instance, false, "stopping");
1479
+ else await runStart(member, instance);
1480
+ }
1481
+ }
1482
+ /**
1483
+ * Aborts a transaction. Before the commit, it drops the transaction. During the commit, it aborts the builds in progress, and the commit rolls back.
1484
+ * Resolves when the transaction ended.
1485
+ */
1486
+ function abortTransaction(transaction, error) {
1487
+ if (transaction.state === "done" || transaction.state === "aborted") return transaction.settled ?? Promise.resolve();
1488
+ const committing = transaction.state === "committing";
1489
+ transaction.state = "aborted";
1490
+ transaction.abortError = error;
1491
+ for (const controller of transaction.controllers) controller.abort(error);
1492
+ if (!committing) {
1493
+ transaction.container.transactions.delete(transaction);
1494
+ transaction.settled = Promise.resolve();
1495
+ releaseReservations(transaction);
1496
+ }
1497
+ return transaction.settled ?? Promise.resolve();
1498
+ }
1499
+ // src/reconciler/conflict.ts
1500
+ /** A service in `conflict` runs no code. It goes back to `pending` when only one declaration remains. */
1501
+ function reconcileConflict(service) {
1502
+ if (!service.signals.present()) {
1503
+ finalize(service);
1504
+ return;
1505
+ }
1506
+ if (inConflict(service)) return;
1507
+ setPhase(service, "pending", { reason: `Only one declaration of the exclusive key '${service.name}' remains.` });
1508
+ service.container.runtime.reconciler.markDirty(service);
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
+ }
1558
+ // src/reconciler/dependency-hooks.ts
1559
+ /** Handles a running service: it stops it, or it runs a dependency hook for the changes since the last handled snapshot. */
1560
+ function reconcileRunning(service) {
1561
+ const instance = service.signals.instance();
1562
+ if (!instance) {
1563
+ enterStopping(service, {});
1564
+ return;
1565
+ }
1566
+ if (!service.signals.present() || !service.signals.active()) {
1567
+ cancelRequeueWait(instance);
1568
+ enterStopping(service, { reason: service.signals.present() ? "The container stopped." : service.replacedBy ? "The service was replaced." : "The service was withdrawn." });
1569
+ return;
1570
+ }
1571
+ if (inConflict(service)) {
1572
+ cancelRequeueWait(instance);
1573
+ enterStopping(service, { reason: `The exclusive key '${service.name}' got another declaration.` });
1574
+ return;
1575
+ }
1576
+ demandProviders(service);
1577
+ const snapshot = currentSnapshot(service);
1578
+ if (requiredWithdrawn(service)) {
1579
+ cancelRequeueWait(instance);
1580
+ if (instance.hooks["dependency-change"].length > 0) runNotice(service, instance, snapshot);
1581
+ else enterStopping(service, { reason: "A required dependency was withdrawn." });
1582
+ return;
1583
+ }
1584
+ if (service.successor?.swap?.rebuild) return;
1585
+ const changes = diffSnapshot(service, instance.handled, snapshot);
1586
+ if (changes.length === 0 || instance.requeueWait) return;
1587
+ const identity = changes.some(isIdentityChange);
1588
+ const hook = identity ? "dependency-change" : "dependency-status-change";
1589
+ if (instance.hooks[hook].length === 0) {
1590
+ if (identity) rebuild(service, instance, {
1591
+ reason: `A dependency changed without a handler: ${describeChanges(changes)}.`,
1592
+ decision: "rebuild"
1593
+ });
1594
+ else acceptSnapshot(service, instance, snapshot);
1595
+ return;
1596
+ }
1597
+ runDependencyHook(service, instance, hook, snapshot, changes);
1598
+ }
1599
+ /** Calls `dependency-change` as a notice before the service stops for a withdrawn required dependency. The decisions are ignored. */
1600
+ async function runNotice(service, instance, snapshot) {
1601
+ service.busy = true;
1602
+ const changes = diffSnapshot(service, instance.handled, snapshot);
1603
+ const view = new Map(snapshot);
1604
+ for (const [name, binding] of instance.bindings) if (!view.get(name)) view.set(name, {
1605
+ state: Object.freeze({
1606
+ provider: binding.service.name,
1607
+ instanceId: binding.id,
1608
+ status: "unavailable"
1609
+ }),
1610
+ instance: binding
1611
+ });
1612
+ const decisions = await runHandlers(service, instance, "dependency-change", instance.controller.signal, {
1613
+ dependsOn: hookDependsOn(service, view),
1614
+ changes: Object.freeze(changes)
1615
+ }, { all: true });
1616
+ service.busy = false;
1617
+ for (const decision of decisions) recordDecision(service, instance, decision, true);
1618
+ enterStopping(service, { reason: `A required dependency was withdrawn: ${describeChanges(changes)}.` });
1619
+ }
1620
+ async function runDependencyHook(service, instance, hook, snapshot, changes) {
1621
+ service.busy = true;
1622
+ for (const entry of snapshot.values()) if (entry) instance.extraHolds.add(entry.instance);
1623
+ batch(() => syncHolds(service, instance));
1624
+ const run = beginHookRun(instance);
1625
+ const decisions = await runHandlers(service, instance, hook, run.signal, {
1626
+ dependsOn: hookDependsOn(service, snapshot),
1627
+ changes: Object.freeze([...changes])
1628
+ });
1629
+ run.end();
1630
+ service.busy = false;
1631
+ const ignored = service.signals.targetLost();
1632
+ for (const decision of decisions) recordDecision(service, instance, decision, ignored);
1633
+ if (ignored) {
1634
+ releaseExtraHolds(service, instance);
1635
+ service.container.runtime.reconciler.markDirty(service);
1636
+ return;
1637
+ }
1638
+ const decision = decisions.at(-1);
1639
+ finishRun(instance, hook, decision);
1640
+ switch (decision.kind) {
1641
+ case "handled":
1642
+ acceptSnapshot(service, instance, snapshot);
1643
+ service.container.runtime.reconciler.markDirty(service);
1644
+ return;
1645
+ case "requeue": {
1646
+ releaseExtraHolds(service, instance);
1647
+ const exhausted = exhaustedIntent(service, decision);
1648
+ if (exhausted) enterStopping(service, exhausted);
1649
+ else scheduleRequeue(service, instance, decision);
1650
+ return;
1651
+ }
1652
+ case "rebuild":
1653
+ releaseExtraHolds(service, instance);
1654
+ rebuild(service, instance, {
1655
+ reason: decision.reason,
1656
+ decision: "rebuild"
1657
+ });
1658
+ return;
1659
+ case "stop":
1660
+ releaseExtraHolds(service, instance);
1661
+ enterStopping(service, {
1662
+ parkEpoch: epochOf(service, (edge) => snapshot.get(edge.name)?.instance),
1663
+ reason: decision.reason,
1664
+ decision: "stop"
1665
+ });
1666
+ return;
1667
+ case "fail":
1668
+ releaseExtraHolds(service, instance);
1669
+ enterStopping(service, {
1670
+ failure: { error: decision.error },
1671
+ reason: decision.reason,
1672
+ decision: "fail"
1673
+ });
1674
+ }
1675
+ }
1676
+ // src/reconciler/registration.ts
1677
+ /**
1678
+ * Handles a `perConsumer` registration. It never builds. The service of each consumer container builds.
1679
+ * A withdrawn registration finalizes after its last consumer service.
1680
+ */
1681
+ function reconcileRegistration(service) {
1682
+ const phase = service.signals.phase();
1683
+ if (!service.signals.present()) {
1684
+ if (service.signals.consumers().size === 0) finalize(service);
1685
+ return;
1686
+ }
1687
+ if (phase === "pending" && inConflict(service)) setPhase(service, "conflict", { reason: `The exclusive key '${service.name}' has more than one declaration.` });
1688
+ else if (phase === "conflict" && !inConflict(service)) setPhase(service, "pending", { reason: `Only one declaration of the exclusive key '${service.name}' remains.` });
1689
+ }
1690
+ // src/reconciler/retry.ts
1691
+ function lostReason(service) {
1692
+ if (!service.signals.present()) return service.replacedBy ? "The service was replaced during a start requeue." : "The service was withdrawn during a start requeue.";
1693
+ if (!service.signals.active()) return "The container stopped during a start requeue.";
1694
+ if (requiredWithdrawn(service)) return "A required dependency was withdrawn during a start requeue.";
1695
+ return `The exclusive key '${service.name}' got another declaration during a start requeue.`;
1696
+ }
1697
+ /**
1698
+ * Handles a service in `retrying`: a `start` hook returned `requeue`.
1699
+ *
1700
+ * Changes during the wait merge into the next attempt. When the wait ends, `start` runs again with the same instance.
1701
+ * A requeue cannot keep a service alive. A lost target, for example a withdrawn required dependency, cancels the wait and stops the service at once.
1702
+ */
1703
+ function reconcileRetrying(service) {
1704
+ const instance = service.signals.instance();
1705
+ if (!instance) {
1706
+ enterStopping(service, {});
1707
+ return;
1708
+ }
1709
+ if (service.signals.targetLost()) {
1710
+ cancelRequeueWait(instance);
1711
+ enterStopping(service, { reason: lostReason(service) });
1712
+ return;
1713
+ }
1714
+ if (instance.requeueWait) return;
1715
+ setPhase(service, "starting", { reason: "The requeue wait ended." });
1716
+ runStart(service, instance);
1717
+ }
1718
+ // src/reconciler/reconciler.ts
1719
+ /**
1720
+ * Creates the execution layer.
1721
+ *
1722
+ * A work queue holds dirty services, each at most once. The reconciler runs one transition at a time for each service.
1723
+ * Each transition reads the latest derived values when it starts and when it ends.
1724
+ */
1725
+ function createReconciler(options) {
1726
+ const { context } = options;
1727
+ const queue = /* @__PURE__ */ new Set();
1728
+ const drainedListeners = /* @__PURE__ */ new Set();
1729
+ const teardownListeners = /* @__PURE__ */ new Set();
1730
+ let drainRequested = false;
1731
+ let draining = false;
1732
+ function requestDrain() {
1733
+ if (drainRequested || draining) return;
1734
+ drainRequested = true;
1735
+ queueMicrotask(drain);
1736
+ }
1737
+ function markDirty(service) {
1738
+ queue.add(service);
1739
+ requestDrain();
1740
+ }
1741
+ function drain() {
1742
+ drainRequested = false;
1743
+ untracked(() => context.run(void 0, () => {
1744
+ draining = true;
1745
+ try {
1746
+ for (const service of queue) {
1747
+ queue.delete(service);
1748
+ reconcile(service);
1749
+ }
1750
+ } finally {
1751
+ draining = false;
1752
+ if (queue.size > 0) requestDrain();
1753
+ else callListeners(drainedListeners);
1754
+ }
1755
+ }));
1756
+ }
1757
+ function reconcile(service) {
1758
+ if (service.transaction) return;
1759
+ const phase = service.signals.phase();
1760
+ if (phase === "withdrawn") return;
1761
+ if (service.perConsumer) {
1762
+ reconcileRegistration(service);
1763
+ return;
1764
+ }
1765
+ checkSwap(service);
1766
+ if (service.busy) {
1767
+ const instance = service.signals.instance();
1768
+ if (!instance) return;
1769
+ if (phase === "building") {
1770
+ if (buildLost(service, instance)) instance.controller.abort(/* @__PURE__ */ new Error(`The build of '${service.name}' was superseded.`));
1771
+ } else if (instance.hookRun && service.signals.targetLost()) instance.hookRun.abort(/* @__PURE__ */ new Error(`A newer target superseded the hook run of '${service.name}'.`));
1772
+ return;
1773
+ }
1774
+ switch (phase) {
1775
+ case "pending":
1776
+ reconcilePending(service);
1777
+ return;
1778
+ case "conflict":
1779
+ reconcileConflict(service);
1780
+ return;
1781
+ case "failed":
1782
+ if (!service.signals.present()) finalize(service);
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;
1787
+ case "running":
1788
+ reconcileRunning(service);
1789
+ return;
1790
+ case "retrying":
1791
+ reconcileRetrying(service);
1792
+ return;
1793
+ case "stopping": reconcileStopping(service);
1794
+ }
1795
+ }
1796
+ return {
1797
+ markDirty,
1798
+ onDrained: (listener) => {
1799
+ drainedListeners.add(listener);
1800
+ return () => drainedListeners.delete(listener);
1801
+ },
1802
+ onTeardown: (listener) => {
1803
+ teardownListeners.add(listener);
1804
+ return () => teardownListeners.delete(listener);
1805
+ },
1806
+ notifyTeardown: (service, errors) => callListeners(teardownListeners, service, errors),
1807
+ requestDrain
1808
+ };
1809
+ }
1810
+ // src/primitives/container.ts
1811
+ function deepFreeze(value) {
1812
+ if (typeof value === "object" && value !== null && !Object.isFrozen(value)) {
1813
+ Object.freeze(value);
1814
+ for (const child of Object.values(value)) deepFreeze(child);
1815
+ }
1816
+ return value;
1817
+ }
1818
+ function readOwner(owner) {
1819
+ if (owner === void 0) return Object.freeze({});
1820
+ let copy;
1821
+ try {
1822
+ copy = structuredClone(owner);
1823
+ } catch (error) {
1824
+ throw new InvalidArgumentError("The container owner must be a serializable record.", { cause: error });
1825
+ }
1826
+ if (typeof copy !== "object" || copy === null || Array.isArray(copy)) throw new InvalidArgumentError("The container owner must be a serializable record.");
1827
+ return deepFreeze(copy);
1828
+ }
1829
+ function readIsolated(options) {
1830
+ let isolate;
1831
+ try {
1832
+ isolate = options.isolate;
1833
+ } catch (error) {
1834
+ throw new InvalidArgumentError("Cannot read the isolate option.", { cause: error });
1835
+ }
1836
+ if (isolate === void 0) return /* @__PURE__ */ new Set();
1837
+ if (!Array.isArray(isolate)) throw new InvalidArgumentError("The isolate option must be a list of keys or names.");
1838
+ return new Set(isolate.map((key) => {
1839
+ let name;
1840
+ try {
1841
+ name = normalizeName(key);
1842
+ } catch (error) {
1843
+ throw new InvalidArgumentError("Cannot read a key of the isolate option.", { cause: error });
1844
+ }
1845
+ if (typeof name !== "string" || name.length === 0) throw new InvalidArgumentError("Each entry of the isolate option must be a key or a non-empty name.");
1846
+ return name;
1847
+ }));
1848
+ }
1849
+ function readContainerPolicy(options) {
1850
+ let fields;
1851
+ try {
1852
+ fields = {
1853
+ maxAttempts: options.maxAttempts,
1854
+ timeouts: options.timeouts,
1855
+ onTimeout: options.onTimeout
1856
+ };
1857
+ } catch (error) {
1858
+ throw new InvalidArgumentError("Cannot read the container options.", { cause: error });
1859
+ }
1860
+ return readPolicy("the container", fields);
1861
+ }
1862
+ function readRandom(options) {
1863
+ let random;
1864
+ try {
1865
+ random = options.random;
1866
+ } catch (error) {
1867
+ throw new InvalidArgumentError("Cannot read the random option.", { cause: error });
1868
+ }
1869
+ if (random !== void 0 && typeof random !== "function") throw new InvalidArgumentError("The random option must be a function.");
1870
+ return random ?? Math.random;
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
+ }
1882
+ function createRuntime(options) {
1883
+ let seq = 0;
1884
+ let instanceSeq = 0;
1885
+ const context = createCleanupContext();
1886
+ return {
1887
+ clock: options.clock ?? systemClock,
1888
+ random: readRandom(options),
1889
+ context,
1890
+ deliveries: createDeliveryQueue(),
1891
+ observers: /* @__PURE__ */ new Set(),
1892
+ declarations: /* @__PURE__ */ new Map(),
1893
+ reconciler: createReconciler({ context }),
1894
+ nextSeq: () => ++seq,
1895
+ nextInstanceId: () => `i${++instanceSeq}`
1896
+ };
1897
+ }
1898
+ /**
1899
+ * Creates a container. With `options.parent`, the container is a child of that container.
1900
+ *
1901
+ * A child container does not start when it is created, even under a started parent. It waits for `start(child)`.
1902
+ */
1903
+ function createContainer(options = {}) {
1904
+ const parent = options.parent === void 0 ? void 0 : nodeOf(options.parent);
1905
+ if (parent) assertActive(parent, "create a child container");
1906
+ const owner = readOwner(options.owner);
1907
+ const isolated = readIsolated(options);
1908
+ const policy = readContainerPolicy(options);
1909
+ const logging = readLogging(options, parent);
1910
+ const logger = options.logger ?? parent?.api.logger ?? createDefaultLogger();
1911
+ const runtime = parent?.runtime ?? createRuntime(options);
1912
+ let reactiveOwner;
1913
+ const stopReactive = withOwner(parent?.reactiveOwner, () => effectScope(() => {
1914
+ reactiveOwner = getActiveSub();
1915
+ }));
1916
+ const api = {
1917
+ logger,
1918
+ logging,
1919
+ owner,
1920
+ parent: parent?.api
1921
+ };
1922
+ const node = {
1923
+ seq: runtime.nextSeq(),
1924
+ api,
1925
+ owner,
1926
+ parent,
1927
+ runtime,
1928
+ children: /* @__PURE__ */ new Set(),
1929
+ services: /* @__PURE__ */ new Set(),
1930
+ signals: {
1931
+ started: signal(false),
1932
+ slots: /* @__PURE__ */ new Map(),
1933
+ exports: /* @__PURE__ */ new Map()
1934
+ },
1935
+ isolated,
1936
+ policy,
1937
+ listeners: /* @__PURE__ */ new Map(),
1938
+ transactions: /* @__PURE__ */ new Set(),
1939
+ effects: [],
1940
+ reactiveOwner,
1941
+ stopReactive,
1942
+ enrolled: false,
1943
+ state: "active",
1944
+ frame: void 0,
1945
+ disposal: void 0,
1946
+ disposedListeners: /* @__PURE__ */ new Set()
1947
+ };
1948
+ parent?.children.add(node);
1949
+ registerNode(api, node);
1950
+ return api;
1951
+ }
1952
+ /**
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.
1957
+ */
1958
+ function effect$1(container, setup) {
1959
+ const node = nodeOf(container);
1960
+ if (typeof setup !== "function") throw new InvalidArgumentError("The effect setup is not a function.");
1961
+ const disposer = untracked(setup);
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
+ }));
1971
+ }
1972
+ /**
1973
+ * Starts the disposal of `container` and of its descendants. The whole subtree enters `disposing` at once, so nothing new registers in it.
1974
+ *
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.
1977
+ */
1978
+ function beginDisposal(container, parentFrame, after) {
1979
+ if (container.disposal) return container.disposal;
1980
+ const { context } = container.runtime;
1981
+ const frame = {
1982
+ owner: container,
1983
+ parent: parentFrame,
1984
+ active: true
1985
+ };
1986
+ container.state = "disposing";
1987
+ container.frame = frame;
1988
+ for (const transaction of [...container.transactions]) abortTransaction(transaction, new BuildAfterDisposeError("The container was disposed before the transaction committed."));
1989
+ let previous = after;
1990
+ const children = [...container.children].reverse().map((child) => {
1991
+ const disposal = beginDisposal(child, frame, previous);
1992
+ disposal.catch(() => {});
1993
+ previous = disposal.then(() => {}, () => {});
1994
+ return disposal;
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();
2005
+ container.disposal = context.run(frame, async () => {
2006
+ const errors = [];
2007
+ if (after) await after;
2008
+ for (const child of children) try {
2009
+ await child;
2010
+ } catch (error) {
2011
+ errors.push(error);
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
+ }
2018
+ for (const disposer of container.effects.splice(0).reverse()) try {
2019
+ await context.run(frame, () => untracked(disposer));
2020
+ } catch (error) {
2021
+ errors.push(error);
2022
+ }
2023
+ if (errors.length > 0) throw errorFrom(errors, "Container disposal failed.");
2024
+ }).finally(() => {
2025
+ frame.active = false;
2026
+ container.state = "disposed";
2027
+ container.listeners.clear();
2028
+ container.stopReactive();
2029
+ container.parent?.children.delete(container);
2030
+ for (const listener of [...container.disposedListeners]) try {
2031
+ listener();
2032
+ } catch (error) {
2033
+ queueMicrotask(() => {
2034
+ throw error;
2035
+ });
2036
+ }
2037
+ container.disposedListeners.clear();
2038
+ });
2039
+ return container.disposal;
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
+ }
2045
+ /**
2046
+ * Withdraws the registrations and invocations of the container, disposes the child containers, and runs the container effects in reverse order.
2047
+ *
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.
2056
+ */
2057
+ function dispose(container, options) {
2058
+ const node = nodeOf(container);
2059
+ const abortSignal = readSignal(options, "dispose");
2060
+ const { context } = node.runtime;
2061
+ const reentry = untracked(() => findReentry(node, "dispose", abortSignal));
2062
+ const disposal = node.disposal ?? untracked(() => {
2063
+ let started;
2064
+ batch(() => {
2065
+ started = beginDisposal(node, context.current());
2066
+ });
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;
2106
+ });
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
+ }
2116
+ // src/primitives/invoke.ts
2117
+ function invoke$1(container, option) {
2118
+ return invokeIn(container, option);
2119
+ }
2120
+ /** Registers an invocation. The public `invoke` functions call it. */
2121
+ function invokeIn(container, option) {
2122
+ const node = nodeOf(container);
2123
+ assertActive(node, "invoke");
2124
+ let callback;
2125
+ let dependsOn;
2126
+ try {
2127
+ const invocationObject = typeof option === "function" ? { callback: option } : option;
2128
+ callback = invocationObject.callback;
2129
+ dependsOn = invocationObject.dependsOn;
2130
+ } catch (error) {
2131
+ throw new InvalidArgumentError("Cannot read the invoke options.", { cause: error });
2132
+ }
2133
+ if (typeof callback !== "function") throw new InvalidArgumentError("The callback option of invoke is not a function.");
2134
+ const name = `invocation:${node.runtime.nextSeq()}`;
2135
+ const edges = readEdges(name, dependsOn);
2136
+ const userCallback = callback;
2137
+ const build = (context) => userCallback(context.dependsOn, Object.freeze({
2138
+ on: context.on,
2139
+ effect: context.effect,
2140
+ signal: context.signal
2141
+ }));
2142
+ const registered = untracked(() => {
2143
+ let service;
2144
+ batch(() => {
2145
+ service = createService({
2146
+ container: node,
2147
+ kind: "invocation",
2148
+ name,
2149
+ edges,
2150
+ build
2151
+ });
2152
+ node.services.add(service);
2153
+ });
2154
+ return service;
2155
+ });
2156
+ traceLoggerOf(container)?.invoke(edges.map((edge) => edge.keyName));
2157
+ return Object.freeze({
2158
+ key: name,
2159
+ withdraw: (options) => withdrawInvocation(registered, options)
2160
+ });
2161
+ }
2162
+ // src/diagnostics/explain.ts
2163
+ /** The owners of conflicting declarations, in container order. The order of the registrations has no effect. */
2164
+ function ownersOf(declarations) {
2165
+ return Object.freeze([...declarations].sort((a, b) => a.container.seq - b.container.seq).map((declaration) => declaration.container.owner));
2166
+ }
2167
+ /** Explains one service from its current resolution data. It is a pure read. */
2168
+ function explainService(service) {
2169
+ const blockers = [];
2170
+ const phase = service.signals.phase();
2171
+ if (phase === "failed") blockers.push({
2172
+ kind: "failed",
2173
+ error: service.failure?.error
2174
+ });
2175
+ if (phase === "retrying") {
2176
+ const previous = service.signals.instance()?.hookStates.get("start")?.previous;
2177
+ blockers.push({
2178
+ kind: "retrying",
2179
+ attempt: previous?.attempt ?? 1,
2180
+ ...withoutUndefined({ reason: previous?.reason })
2181
+ });
2182
+ }
2183
+ const rivals = service.signals.rivals();
2184
+ if (rivals.length > 0) {
2185
+ const registration = service.template ?? service;
2186
+ blockers.push({
2187
+ kind: "conflict",
2188
+ key: service.name,
2189
+ owners: ownersOf([registration, ...rivals])
2190
+ });
2191
+ }
2192
+ if (!service.signals.active() && !service.perConsumer) blockers.push({ kind: "not-started" });
2193
+ for (const edge of service.edges) {
2194
+ if (edge.optional) continue;
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
+ }
2209
+ switch (resolution.kind) {
2210
+ case "missing":
2211
+ blockers.push({
2212
+ kind: "missing-provider",
2213
+ dependency: edge.name,
2214
+ key: edge.keyName
2215
+ });
2216
+ continue;
2217
+ case "rejected":
2218
+ blockers.push({
2219
+ kind: "rejected-provider",
2220
+ dependency: edge.name,
2221
+ key: resolution.provider.name,
2222
+ owner: resolution.provider.container.owner,
2223
+ metadata: resolution.provider.metadata
2224
+ });
2225
+ continue;
2226
+ case "conflict":
2227
+ blockers.push({
2228
+ kind: "conflicting-dependency",
2229
+ dependency: edge.name,
2230
+ key: resolution.provider.name,
2231
+ owners: ownersOf([resolution.provider, ...resolution.rivals])
2232
+ });
2233
+ continue;
2234
+ case "resolved": {
2235
+ const status = resolution.provider.signals.effective();
2236
+ if (phase !== "running" && status?.status === "unavailable") blockers.push({
2237
+ kind: "unavailable-dependency",
2238
+ dependency: edge.name,
2239
+ key: resolution.provider.name,
2240
+ cause: causeOf(resolution.provider, status)
2241
+ });
2242
+ continue;
2243
+ }
2244
+ case "not-running": {
2245
+ const providerPhase = resolution.provider.signals.phase();
2246
+ blockers.push(providerPhase === "failed" ? {
2247
+ kind: "failed-dependency",
2248
+ dependency: edge.name,
2249
+ key: edge.keyName,
2250
+ error: resolution.provider.failure?.error
2251
+ } : {
2252
+ kind: "pending-dependency",
2253
+ dependency: edge.name,
2254
+ key: edge.keyName,
2255
+ phase: providerPhase
2256
+ });
2257
+ }
2258
+ }
2259
+ }
2260
+ const status = service.signals.effective();
2261
+ const successor = service.successor;
2262
+ return Object.freeze({
2263
+ name: service.name,
2264
+ kind: service.kind === "invocation" ? "invocation" : "service",
2265
+ owner: service.container.owner,
2266
+ phase,
2267
+ ...withoutUndefined({
2268
+ status: status?.status,
2269
+ reason: status?.reason,
2270
+ cause: status?.cause
2271
+ }),
2272
+ blockers: Object.freeze(blockers),
2273
+ ...withoutUndefined({ replacement: successor?.signals.present() ? explainService(successor) : void 0 })
2274
+ });
2275
+ }
2276
+ /** Formats a cause chain, for example `'airi.llm' is unavailable (reason), because 'gateway/store' is unavailable`. */
2277
+ function describeCause(cause) {
2278
+ const parts = [];
2279
+ for (let current = cause; current; current = current.cause) parts.push(`'${current.key}' is ${current.status}${current.reason === void 0 ? "" : ` (${current.reason})`}`);
2280
+ return parts.join(", because ");
2281
+ }
2282
+ function describeOwners(owners) {
2283
+ return owners.map((owner) => JSON.stringify(owner)).join(", ");
2284
+ }
2285
+ function describeBlocker(blocker) {
2286
+ switch (blocker.kind) {
2287
+ case "missing-provider": return `no provider for '${blocker.key}' (${blocker.dependency})`;
2288
+ case "failed-dependency": return `'${blocker.key}' (${blocker.dependency}) failed`;
2289
+ case "pending-dependency": return `'${blocker.key}' (${blocker.dependency}) is ${blocker.phase}`;
2290
+ case "unavailable-dependency": return `${describeCause(blocker.cause)} (${blocker.dependency})`;
2291
+ case "rejected-provider": return `a resolution condition rejected the provider of '${blocker.key}' from ${JSON.stringify(blocker.owner)} (${blocker.dependency})`;
2292
+ case "conflicting-dependency": return `the exclusive key '${blocker.key}' (${blocker.dependency}) has more than one declaration: ${describeOwners(blocker.owners)}`;
2293
+ case "conflict": return `the exclusive key '${blocker.key}' has more than one declaration: ${describeOwners(blocker.owners)}`;
2294
+ case "failed": return "the service failed";
2295
+ case "retrying": return `its start hook requeued after attempt ${blocker.attempt}${blocker.reason === void 0 ? "" : ` (${blocker.reason})`}`;
2296
+ case "not-started": return "its container is not started";
2297
+ }
2298
+ }
2299
+ /** Formats explanations as one line for each service. */
2300
+ function describeExplanations(explanations) {
2301
+ return explanations.map((explanation) => {
2302
+ const reasons = explanation.blockers.map(describeBlocker);
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
+ }
2309
+ return `- ${explanation.name} is ${explanation.phase}${reasons.length > 0 ? `: ${reasons.join(", ")}` : ""}`;
2310
+ }).join("\n");
2311
+ }
2312
+ // src/primitives/lifecycle.ts
2313
+ const visibleKinds = /* @__PURE__ */ new Set(["service", "invocation"]);
2314
+ const transitionalPhases = /* @__PURE__ */ new Set([
2315
+ "building",
2316
+ "starting",
2317
+ "retrying",
2318
+ "stopping"
2319
+ ]);
2320
+ /** Lists the present services and invocations of `containers`. A `perConsumer` registration never runs, so it is not in the list. Its consumer services are. */
2321
+ function presentServices(containers) {
2322
+ return containers.flatMap((container) => [...container.services].filter((service) => visibleKinds.has(service.kind) && service.signals.present() && !service.perConsumer));
2323
+ }
2324
+ /**
2325
+ * Runs `check` each time the work queue drains, until the result is not `'waiting'`.
2326
+ * When `signal` aborts first, it stops checking and rejects with the result of `onAborted`. An aborted signal rejects at once.
2327
+ */
2328
+ function waitUntilSettled(containers, check, signal, onAborted) {
2329
+ const { reconciler } = containers[0].runtime;
2330
+ return new Promise((done, fail) => {
2331
+ if (signal?.aborted) {
2332
+ fail(onAborted(signal.reason));
2333
+ return;
2334
+ }
2335
+ let removeAbort = () => {};
2336
+ const stopListening = reconciler.onDrained(() => {
2337
+ const result = check(presentServices(containers));
2338
+ if (result === "waiting") return;
2339
+ stopListening();
2340
+ removeAbort();
2341
+ if (result.kind === "done") done();
2342
+ else fail(errorFrom(result.errors, "Start failed."));
2343
+ });
2344
+ removeAbort = onAbort(signal, (reason) => {
2345
+ stopListening();
2346
+ fail(onAborted(reason));
2347
+ });
2348
+ reconciler.requestDrain();
2349
+ });
2350
+ }
2351
+ /**
2352
+ * Begins convergence of the container and of each child container that was started.
2353
+ *
2354
+ * Resolves when all their services and invocations are `running`. Rejects with the build and start errors.
2355
+ * A service in `failed` tries again. A required key without a provider keeps the promise pending, unless `options.signal` aborts.
2356
+ * A child container that was never started does not start. Call `start(child)` for it.
2357
+ */
2358
+ function start$1(container, options) {
2359
+ const node = nodeOf(container);
2360
+ assertActive(node, "start");
2361
+ const signal = readSignal(options, "start");
2362
+ node.enrolled = true;
2363
+ const containers = affectedContainers(node);
2364
+ return untracked(() => {
2365
+ batch(() => {
2366
+ for (const current of containers) current.signals.started(true);
2367
+ for (const service of presentServices(containers)) if (service.signals.phase() === "failed") {
2368
+ service.failure = void 0;
2369
+ setPhase(service, "pending", { reason: "start() tries the failed service again." });
2370
+ }
2371
+ });
2372
+ return waitUntilSettled(containers, (services) => {
2373
+ if (services.every((service) => service.signals.phase() === "running")) return { kind: "done" };
2374
+ if (services.some((service) => transitionalPhases.has(service.signals.phase()))) return "waiting";
2375
+ const errors = services.flatMap((service) => service.signals.phase() === "failed" && service.failure ? [service.failure.error] : []);
2376
+ return errors.length > 0 ? {
2377
+ kind: "failed",
2378
+ errors
2379
+ } : "waiting";
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());
2386
+ }
2387
+ /**
2388
+ * Stops all instances of the container and of each child container that was started. Dependents stop before their providers.
2389
+ *
2390
+ * The registrations stay. The container stays usable, and a later `start()` builds them again. `dispose()` clears the container.
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.
2399
+ */
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) {
2416
+ const containers = affectedContainers(node);
2417
+ const affected = new Set(containers);
2418
+ node.enrolled = false;
2419
+ const errors = [];
2420
+ const stopTeardown = node.runtime.reconciler.onTeardown((service, teardownErrors) => {
2421
+ if (service.kind !== "resolver" && affected.has(service.container)) errors.push(...teardownErrors);
2422
+ });
2423
+ const settled = untracked(() => {
2424
+ batch(() => {
2425
+ for (const current of containers) current.signals.started(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;
2435
+ });
2436
+ });
2437
+ try {
2438
+ await settled;
2439
+ } finally {
2440
+ stopTeardown();
2441
+ }
2442
+ if (errors.length > 0) throw errorFrom(errors, "Stop failed.");
2443
+ }
2444
+ // src/primitives/listen.ts
2445
+ /** Reads the `id` of an event key once. Any object with a non-empty string `id` is an event key. */
2446
+ function readEventId(eventKey) {
2447
+ let id;
2448
+ try {
2449
+ id = typeof eventKey === "object" && eventKey !== null ? eventKey.id : void 0;
2450
+ } catch (error) {
2451
+ throw new InvalidArgumentError("Cannot read the id of the event key.", { cause: error });
2452
+ }
2453
+ if (typeof id !== "string" || id.length === 0) throw new InvalidArgumentError("An event key must be an object with a non-empty string id.");
2454
+ return id;
2455
+ }
2456
+ function readListenerMetadata(metadata) {
2457
+ if (metadata === void 0) return Object.freeze({});
2458
+ if (typeof metadata !== "object" || metadata === null || Array.isArray(metadata)) throw new InvalidArgumentError("The metadata of a listener must be a record.");
2459
+ try {
2460
+ return Object.freeze({ ...metadata });
2461
+ } catch (error) {
2462
+ throw new InvalidArgumentError("Cannot read the metadata of the listener.", { cause: error });
2463
+ }
2464
+ }
2465
+ /**
2466
+ * Stores a listener for `eventKey`. The container owns it: `dispose(container)` removes it.
2467
+ *
2468
+ * injeca does not call the listener. The consumer reads the listeners with `listeners()` and runs them by its own rules.
2469
+ * Returns a function that removes the listener.
2470
+ */
2471
+ function listen(container, eventKey, handler, metadata) {
2472
+ const node = nodeOf(container);
2473
+ assertActive(node, "listen");
2474
+ const id = readEventId(eventKey);
2475
+ if (typeof handler !== "function") throw new InvalidArgumentError(`The listener for '${id}' is not a function.`);
2476
+ const record = Object.freeze({
2477
+ seq: node.runtime.nextSeq(),
2478
+ handler,
2479
+ metadata: readListenerMetadata(metadata)
2480
+ });
2481
+ const records = node.listeners.get(id) ?? [];
2482
+ node.listeners.set(id, [...records, record]);
2483
+ return () => {
2484
+ const current = node.listeners.get(id);
2485
+ if (!current?.includes(record)) return;
2486
+ const rest = current.filter((entry) => entry !== record);
2487
+ if (rest.length > 0) node.listeners.set(id, rest);
2488
+ else node.listeners.delete(id);
2489
+ };
2490
+ }
2491
+ // src/primitives/replace.ts
2492
+ function outcomeResult(outcome, name) {
2493
+ switch (outcome.kind) {
2494
+ case "swapped": return true;
2495
+ case "cancelled": return false;
2496
+ case "failed": throw outcome.error;
2497
+ case "stopping": throw new ReplaceWhileStoppingError(`'${name}' started to stop before its replacement completed.`);
2498
+ }
2499
+ }
2500
+ /** Makes `service` the current registration in place of `target`, in the caller's batch. */
2501
+ function takeOver(registration, target, service) {
2502
+ cancelPending(registration);
2503
+ target.replacedBy = service;
2504
+ releaseService(target);
2505
+ publishService(service);
2506
+ registration.current = service;
2507
+ }
2508
+ /** Replaces a service that has no serving instance. The new service takes the slot at once. */
2509
+ function replaceAtOnce(registration, target, definition) {
2510
+ let service;
2511
+ batch(() => {
2512
+ service = createService({
2513
+ container: target.container,
2514
+ kind: "service",
2515
+ name: target.name,
2516
+ edges: definition.edges,
2517
+ build: definition.build,
2518
+ metadata: definition.metadata,
2519
+ policy: definition.policy,
2520
+ exclusive: target.exclusive,
2521
+ exported: target.exported,
2522
+ perConsumer: target.perConsumer,
2523
+ registration
2524
+ });
2525
+ service.signals.demanded(target.signals.demanded());
2526
+ target.container.services.add(service);
2527
+ takeOver(registration, target, service);
2528
+ });
2529
+ return service;
2530
+ }
2531
+ /**
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.
2539
+ */
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);
2577
+ });
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
+ }
2607
+ });
2608
+ commitIfReady();
2609
+ return outcome.promise;
2610
+ }
2611
+ /** Settles a replacement for its registration. */
2612
+ function settleRegistration(registration) {
2613
+ return (service, outcome) => {
2614
+ if (registration.pending === service) registration.pending = void 0;
2615
+ if (outcome.kind === "swapped") registration.current = service;
2616
+ };
2617
+ }
2618
+ /**
2619
+ * Starts the replacement of a running service. The old instance serves until the new one completes `start`.
2620
+ * A transaction passes `extra`, and it sets `registration.pending` at its publish.
2621
+ */
2622
+ function replaceRunning(registration, target, definition, extra) {
2623
+ const pending = createSuccessor(target, definition, {
2624
+ onSettle: settleRegistration(registration),
2625
+ extra
2626
+ });
2627
+ if (!extra?.transaction) {
2628
+ cancelPending(registration);
2629
+ registration.pending = pending.service;
2630
+ }
2631
+ return pending;
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
+ }
2647
+ /** The `replace()` of a registration. */
2648
+ async function replace(registration, option, options) {
2649
+ const signal = readSignal(options, "replace");
2650
+ const target = untracked(() => currentService(registration));
2651
+ if (!target) return false;
2652
+ const definition = readDefinition(registration.name, option, target);
2653
+ if (signal?.aborted) return false;
2654
+ const phase = untracked(() => target.signals.phase());
2655
+ if (phase === "stopping") throw new ReplaceWhileStoppingError(`'${registration.name}' is stopping. Wait until it stops, then call provide.`);
2656
+ if (target.perConsumer) {
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);
2678
+ }
2679
+ if (phase !== "running") {
2680
+ untracked(() => replaceAtOnce(registration, target, definition));
2681
+ return true;
2682
+ }
2683
+ const pending = untracked(() => replaceRunning(registration, target, definition));
2684
+ return awaitOutcome(registration, pending.service, pending.outcome, signal);
2685
+ }
2686
+ const registrations = /* @__PURE__ */ new WeakMap();
2687
+ /** The registration behind a result of `provide`, or `undefined` for any other value. */
2688
+ function registrationOf(key) {
2689
+ return typeof key === "object" && key !== null ? registrations.get(key) : void 0;
2690
+ }
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 = {}) {
2693
+ const api = Object.freeze({
2694
+ key: registration.name,
2695
+ ...registration.exclusive ? { exclusive: true } : {},
2696
+ withdraw: overrides.withdraw ?? ((options) => withdrawRegistration(registration, options)),
2697
+ replace: ((option, options) => replace(registration, option, options))
2698
+ });
2699
+ registrations.set(api, registration);
2700
+ return api;
2701
+ }
2702
+ // src/primitives/provide.ts
2703
+ function normalizeProvideOption(nameOrOption, option) {
2704
+ if (typeof nameOrOption === "string") {
2705
+ if (option == null || !("build" in option) && typeof option !== "function") throw new Error("When using provide(...) as named callback, the second argument must be either a valid ProvideOptionObject<T, D> or a ProvideOptionFunc<T, D>.");
2706
+ if (typeof option === "function") return { build: option };
2707
+ return option;
2708
+ }
2709
+ if (typeof nameOrOption === "object" && "key" in nameOrOption) {
2710
+ if (option == null || !("build" in option) && typeof option !== "function") throw new Error("When using provide(...) as typed ProvideOptionWithKeys<T, D, Key> callback, the second argument must be either a valid ProvideOptionObject<T, D> or a ProvideOptionFunc<T, D>.");
2711
+ if (typeof option === "function") return { build: option };
2712
+ return option;
2713
+ }
2714
+ if (typeof nameOrOption === "function") return { build: nameOrOption };
2715
+ return nameOrOption;
2716
+ }
2717
+ function isProvidedKey(value) {
2718
+ return typeof value === "object" && value !== null && "key" in value && !("build" in value);
2719
+ }
2720
+ function autoName(stackIndex, nameOrOption, option) {
2721
+ const fromFirst = typeof nameOrOption === "object" && "autoNameStackIndex" in nameOrOption ? nameOrOption.autoNameStackIndex : void 0;
2722
+ const requested = (typeof option === "object" ? option.autoNameStackIndex : void 0) ?? fromFirst;
2723
+ const index = typeof requested === "number" ? requested + stackIndex - 1 : stackIndex;
2724
+ const parentFile = ErrorStackParser.parse(/* @__PURE__ */ new Error("providing"))[index];
2725
+ return `${parentFile?.fileName ?? `unknown-${nanoid()}`}:${parentFile?.lineNumber ?? 0}:${parentFile?.columnNumber ?? 0}`;
2726
+ }
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) {
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}'.`);
2734
+ }
2735
+ /**
2736
+ * Publishes `service` as the current registration of its key. Call it inside a batch.
2737
+ * Before `start()`, a second provide of the same name replaces the first registration, as in 0.2.
2738
+ */
2739
+ function publishRegistration(container, service) {
2740
+ const previous = slotOf(container, service.name)();
2741
+ if (previous) {
2742
+ releaseService(previous);
2743
+ emitRecord(container, {
2744
+ type: "registration-replaced",
2745
+ key: service.name,
2746
+ owner: container.owner,
2747
+ instanceId: previous.signals.instance()?.id,
2748
+ reason: `A second provide of '${service.name}' replaced the registration before start().`
2749
+ });
2750
+ }
2751
+ publishService(service);
2752
+ }
2753
+ /** Creates the service of one registration. It is not published yet. */
2754
+ function serviceOf(container, name, definition, exclusive, extra = {}) {
2755
+ const service = createService({
2756
+ container,
2757
+ kind: "service",
2758
+ name,
2759
+ edges: definition.edges,
2760
+ build: definition.build,
2761
+ metadata: definition.metadata,
2762
+ policy: definition.policy,
2763
+ exclusive,
2764
+ exported: definition.exported,
2765
+ perConsumer: definition.perConsumer,
2766
+ ...extra
2767
+ });
2768
+ container.services.add(service);
2769
+ return service;
2770
+ }
2771
+ /** Makes the service of one `provide` call the current registration of its key. */
2772
+ function register(container, name, definition, exclusive) {
2773
+ assertNoDuplicate(container, name);
2774
+ const registration = createRegistration(container, name, exclusive);
2775
+ batch(() => {
2776
+ const service = serviceOf(container, name, definition, exclusive, { registration });
2777
+ publishRegistration(container, service);
2778
+ registration.current = service;
2779
+ });
2780
+ return registration;
2781
+ }
2782
+ /**
2783
+ * Registers a provider. The public `provide` functions call it directly and pass `stackIndex` 3: this function, the auto-name helper, and the public function come before their caller.
2784
+ */
2785
+ function provideIn(container, stackIndex, nameOrOption, option) {
2786
+ const node = nodeOf(container);
2787
+ assertActive(node, "provide");
2788
+ const name = typeof nameOrOption === "string" ? nameOrOption : isProvidedKey(nameOrOption) ? normalizeName(nameOrOption) : autoName(stackIndex, nameOrOption, option);
2789
+ if (typeof name !== "string" || name.length === 0) throw new InvalidArgumentError("A provider name must be a non-empty string.");
2790
+ const providerObject = normalizeProvideOption(nameOrOption, option);
2791
+ const definition = readDefinition(name, providerObject);
2792
+ const exclusive = isExclusiveKey(nameOrOption);
2793
+ const registration = untracked(() => register(node, name, definition, exclusive));
2794
+ traceLoggerOf(container)?.provide(name, definition.edges.map((edge) => edge.keyName));
2795
+ return registrationApi(registration);
2796
+ }
2797
+ function provide$1(container, nameOrOption, option) {
2798
+ return provideIn(container, 3, nameOrOption, option);
2799
+ }
2800
+ // src/primitives/resolve.ts
2801
+ /**
2802
+ * Waits until the dependencies are available and returns their values. Before `start()`, it builds and starts them.
2803
+ *
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.
2806
+ */
2807
+ function resolve$1(container, dependsOn, options) {
2808
+ const node = nodeOf(container);
2809
+ assertActive(node, "resolve");
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."));
2813
+ const { reconciler } = node.runtime;
2814
+ return new Promise((done, fail) => untracked(() => {
2815
+ let settled = false;
2816
+ let service;
2817
+ let stopListening = () => {};
2818
+ let disarm = () => {};
2819
+ const finish = (outcome) => {
2820
+ if (settled) return;
2821
+ settled = true;
2822
+ stopListening();
2823
+ disarm();
2824
+ batch(() => releaseService(service));
2825
+ outcome();
2826
+ };
2827
+ const build = (context) => finish(() => done({ ...context.dependsOn }));
2828
+ batch(() => {
2829
+ service = createService({
2830
+ container: node,
2831
+ kind: "resolver",
2832
+ name: "resolve",
2833
+ edges,
2834
+ build
2835
+ });
2836
+ node.services.add(service);
2837
+ });
2838
+ stopListening = reconciler.onDrained(() => {
2839
+ const failure = blockingFailure(service);
2840
+ if (failure) finish(() => fail(failure.error));
2841
+ });
2842
+ disarm = onAbort(signal, (reason) => finish(() => fail(waitAbortError(reason, "resolve() timed out."))));
2843
+ service.withdrawal.promise.then(() => finish(() => fail(new BuildAfterDisposeError("The container was disposed before resolve() got its dependencies."))));
2844
+ }));
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
+ }
2866
+ // src/primitives/transaction.ts
2867
+ function readName(key, action) {
2868
+ let name;
2869
+ try {
2870
+ name = normalizeName(key);
2871
+ } catch (error) {
2872
+ throw new InvalidArgumentError(`Cannot read the key of ${action}.`, { cause: error });
2873
+ }
2874
+ if (typeof name !== "string" || name.length === 0) throw new InvalidArgumentError(`The key of ${action} must be a name or a key.`);
2875
+ return name;
2876
+ }
2877
+ function readDeclareOptions(name, options) {
2878
+ let dependsOn;
2879
+ let optional;
2880
+ try {
2881
+ dependsOn = options?.dependsOn;
2882
+ optional = options?.optional;
2883
+ } catch (error) {
2884
+ throw new InvalidArgumentError(`Cannot read the declare options of '${name}'.`, { cause: error });
2885
+ }
2886
+ if (optional !== void 0 && typeof optional !== "boolean") throw new InvalidArgumentError(`The optional option of '${name}' must be a boolean.`);
2887
+ return {
2888
+ dependsOn,
2889
+ optional: optional === true
2890
+ };
2891
+ }
2892
+ /** The service that a `tx.replace` names now. A stale registration, or a key without a registration, fails. */
2893
+ function replaceTarget(container, entry) {
2894
+ const target = entry.target ? currentService(entry.target) : slotOf(container, entry.name)();
2895
+ if (!target || !target.signals.present()) throw new StaleRegistrationError(`The transaction replaces '${entry.name}', but the container has no current registration for it.`);
2896
+ if (target.signals.phase() === "stopping") throw new ReplaceWhileStoppingError(`The transaction replaces '${entry.name}', but it is stopping.`);
2897
+ return target;
2898
+ }
2899
+ /** Checks the entries and creates the services of a commit. The services are not published. */
2900
+ function createMembers(transaction) {
2901
+ const { container } = transaction;
2902
+ assertActive(container, "commit a transaction");
2903
+ const incomplete = [...transaction.entries.values()].filter((entry) => !entry.definition && !entry.optional);
2904
+ if (incomplete.length > 0) throw new TransactionIncompleteError(`These declared services have no provide: ${incomplete.map((entry) => `'${entry.name}'`).join(", ")}.`);
2905
+ const entries = [...transaction.entries.values()].filter((entry) => entry.definition);
2906
+ const targets = /* @__PURE__ */ new Map();
2907
+ for (const entry of entries) if (entry.replace) targets.set(entry, replaceTarget(container, entry));
2908
+ else assertNoDuplicate(container, entry.name, transaction);
2909
+ const members = [];
2910
+ batch(() => {
2911
+ for (const entry of entries) {
2912
+ const definition = entry.definition;
2913
+ const target = targets.get(entry);
2914
+ const extra = {
2915
+ phase: "declared",
2916
+ transaction
2917
+ };
2918
+ if (!target) {
2919
+ members.push({
2920
+ entry,
2921
+ service: serviceOf(container, entry.name, definition, entry.exclusive, {
2922
+ ...extra,
2923
+ registration: entry.registration
2924
+ })
2925
+ });
2926
+ continue;
2927
+ }
2928
+ const registration = target.registration ?? createRegistration(container, entry.name, target.exclusive);
2929
+ const service = target.signals.phase() === "running" && !target.perConsumer ? replaceRunning(registration, target, definition, extra).service : serviceOf(container, entry.name, {
2930
+ ...definition,
2931
+ perConsumer: target.perConsumer,
2932
+ exported: target.exported
2933
+ }, target.exclusive, {
2934
+ ...extra,
2935
+ registration
2936
+ });
2937
+ members.push({
2938
+ entry,
2939
+ service,
2940
+ target
2941
+ });
2942
+ }
2943
+ });
2944
+ return members;
2945
+ }
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) {
2948
+ batch(() => {
2949
+ releaseReservations(transaction);
2950
+ for (const { service, target } of members) {
2951
+ service.transaction = void 0;
2952
+ const registration = service.registration;
2953
+ if (target && registration && service.swap) {
2954
+ cancelPending(registration);
2955
+ registration.pending = service;
2956
+ } else if (target && registration && target.perConsumer) replaceTemplate(registration, target, service);
2957
+ else if (target && registration) takeOver(registration, target, service);
2958
+ else {
2959
+ publishRegistration(service.container, service);
2960
+ if (registration) registration.current = service;
2961
+ }
2962
+ if (service.signals.present()) setPhase(service, built.has(service) ? "starting" : "pending", { reason: built.has(service) ? "The transaction committed." : "The transaction committed. The service waits for its dependencies." });
2963
+ }
2964
+ });
2965
+ }
2966
+ async function commit(transaction) {
2967
+ const members = untracked(() => createMembers(transaction));
2968
+ const byName = new Map(members.map((member) => [member.service.name, member.service]));
2969
+ const order = orderMembers(byName);
2970
+ const outcome = await buildMembers(transaction, order, byName);
2971
+ if (outcome.kind !== "built") {
2972
+ await untracked(() => rollbackMembers(byName, outcome.built));
2973
+ throw outcome.kind === "failed" ? outcome.error : transaction.abortError;
2974
+ }
2975
+ untracked(() => publishMembers(transaction, members, new Set(outcome.built.keys())));
2976
+ transaction.state = "done";
2977
+ transaction.container.transactions.delete(transaction);
2978
+ startMembers(order, outcome.built);
2979
+ }
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
+ }
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
+ }
2996
+ transaction.state = "committing";
2997
+ const committed = commit(transaction).catch((error) => {
2998
+ if (transaction.state !== "done") {
2999
+ transaction.state = "aborted";
3000
+ transaction.container.transactions.delete(transaction);
3001
+ untracked(() => releaseReservations(transaction));
3002
+ }
3003
+ throw error;
3004
+ });
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}'.`);
3025
+ }
3026
+ function assertOpen(transaction, action) {
3027
+ if (transaction.state !== "open") throw new InvalidArgumentError(`Cannot ${action} after the transaction committed or aborted.`);
3028
+ }
3029
+ function entryOf$1(transaction, name, exclusive) {
3030
+ let entry = transaction.entries.get(name);
3031
+ if (!entry) {
3032
+ entry = {
3033
+ name,
3034
+ declared: void 0,
3035
+ optional: false,
3036
+ definition: void 0,
3037
+ replace: false,
3038
+ target: void 0,
3039
+ registration: void 0,
3040
+ exclusive,
3041
+ exported: false,
3042
+ reservation: void 0
3043
+ };
3044
+ transaction.entries.set(name, entry);
3045
+ }
3046
+ return entry;
3047
+ }
3048
+ /**
3049
+ * Creates a transaction for `container`.
3050
+ *
3051
+ * `declare` reserves services. `provide` and `replace` add services. Nothing is visible to consumers before `commit()`.
3052
+ * `commit()` builds the services in dependency order and publishes them in one batch. Then the `start` hooks run.
3053
+ * A failed build publishes nothing. A disposal of the container during the transaction aborts it.
3054
+ */
3055
+ function transaction(container) {
3056
+ const node = nodeOf(container);
3057
+ assertActive(node, "create a transaction");
3058
+ const state = {
3059
+ container: node,
3060
+ state: "open",
3061
+ entries: /* @__PURE__ */ new Map(),
3062
+ abortError: void 0,
3063
+ controllers: /* @__PURE__ */ new Set(),
3064
+ settled: void 0
3065
+ };
3066
+ node.transactions.add(state);
3067
+ const declare = (key, options) => {
3068
+ assertOpen(state, "declare");
3069
+ const name = readName(key, "declare");
3070
+ const { dependsOn, optional } = readDeclareOptions(name, options);
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;
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);
3100
+ };
3101
+ const provide = ((key, option) => {
3102
+ assertOpen(state, "provide");
3103
+ const name = readName(key, "provide");
3104
+ if (state.entries.get(name)?.definition) throw new DuplicateServiceError(`The transaction already provides '${name}'.`);
3105
+ assertNotReserved(state, name);
3106
+ const read = readDefinition(name, option);
3107
+ const entry = entryOf$1(state, name, false);
3108
+ entry.exclusive ||= isExclusiveKey(key);
3109
+ const definition = read.edges.length === 0 && entry.declared ? {
3110
+ ...read,
3111
+ edges: entry.declared
3112
+ } : read;
3113
+ const registration = createRegistration(node, name, entry.exclusive);
3114
+ entry.definition = definition;
3115
+ entry.registration = registration;
3116
+ entry.exported = definition.exported;
3117
+ untracked(() => reserveKey(state, entry));
3118
+ return registrationApi(registration, { withdraw: (options) => withdrawEntry(entry, registration, options) });
3119
+ });
3120
+ const replace = ((key, option) => {
3121
+ assertOpen(state, "replace");
3122
+ const name = readName(key, "replace");
3123
+ if (state.entries.get(name)?.definition) throw new DuplicateServiceError(`The transaction already provides or replaces '${name}'.`);
3124
+ const target = registrationOf(key);
3125
+ const entry = entryOf$1(state, name, target?.exclusive ?? false);
3126
+ entry.definition = readDefinition(name, option, untracked(() => target ? currentService(target) : slotOf(node, name)()));
3127
+ entry.replace = true;
3128
+ entry.target = target;
3129
+ untracked(() => releaseReservation(entry));
3130
+ });
3131
+ return Object.freeze({
3132
+ declare,
3133
+ provide,
3134
+ replace,
3135
+ commit: (options) => untracked(() => startCommit(state, options)),
3136
+ abort: () => untracked(() => abortTransaction(state, new TransactionIncompleteError("The transaction was aborted before the commit completed.")))
3137
+ });
3138
+ }
3139
+ // src/queries/impact.ts
3140
+ const livePhases = /* @__PURE__ */ new Set([
3141
+ "building",
3142
+ "starting",
3143
+ "retrying",
3144
+ "running"
3145
+ ]);
3146
+ /** A service with an instance that a change can stop or rebuild. A new service that waits for its swap does not count. */
3147
+ function isLive(service) {
3148
+ return service.kind !== "resolver" && !service.swap && service.signals.instance() !== void 0 && livePhases.has(service.signals.phase());
3149
+ }
3150
+ function rootOf(container) {
3151
+ let root = container;
3152
+ while (root.parent) root = root.parent;
3153
+ return root;
3154
+ }
3155
+ /** The services of a `perConsumer` registration, with the registration. */
3156
+ function withConsumers(service) {
3157
+ return [service, ...service.signals.consumers().values()];
3158
+ }
3159
+ /** Lists the services that leave when `container` and its descendants are disposed. */
3160
+ function disposedServices(container) {
3161
+ return new Set(subtreeOf(container).flatMap((current) => [...current.services]).flatMap(withConsumers));
3162
+ }
3163
+ /** Lists the services that leave when `service` is withdrawn. */
3164
+ function withdrawnServices(service) {
3165
+ return new Set(withConsumers(service));
3166
+ }
3167
+ /**
3168
+ * Computes what a proposed change does, on a copy of the graph state. It reads the real graph and never writes it.
3169
+ *
3170
+ * - A dependent that loses a required dependency without an alternative provider stops. Its dependents follow.
3171
+ * - A dependent whose dependency gets another instance gets `dependency-change`. With a handler, it is notified. Without one, it rebuilds, and its dependents follow.
3172
+ * - An alternative provider is an ancestor registration that serves, or another `firstAvailable` candidate.
3173
+ */
3174
+ function computeImpact(scope, change) {
3175
+ const services = subtreeOf(rootOf(scope)).flatMap((container) => [...container.services]).filter(isLive);
3176
+ const stopped = new Set([...change.removed].filter(isLive));
3177
+ const rebuilt = /* @__PURE__ */ new Set();
3178
+ const notified = /* @__PURE__ */ new Set();
3179
+ const gone = (service) => change.removed.has(service) || stopped.has(service);
3180
+ const renewed = (service) => change.renewed.has(service) || rebuilt.has(service);
3181
+ /** Tells whether another candidate of the edge serves after the change. */
3182
+ const alternative = (consumer, keyNames) => keyNames.some((keyName) => candidatesOf(consumer.scope, keyName, consumer).some((registration) => {
3183
+ const provider = serviceFor(registration, consumer.scope);
3184
+ return provider !== void 0 && !gone(provider) && !gone(registration) && provider.signals.serving();
3185
+ }));
3186
+ let changed = true;
3187
+ while (changed) {
3188
+ changed = false;
3189
+ for (const service of services) {
3190
+ if (stopped.has(service) || change.removed.has(service)) continue;
3191
+ let lost = false;
3192
+ let identity = false;
3193
+ for (const edge of service.edges) {
3194
+ const resolution = resolutionOf(service, edge);
3195
+ if (resolution.kind !== "resolved") continue;
3196
+ const provider = resolution.provider;
3197
+ if (gone(provider)) {
3198
+ if (alternative(service, edge.candidates)) identity = true;
3199
+ else if (edge.optional) identity = true;
3200
+ else lost = true;
3201
+ } else if (renewed(provider)) identity = true;
3202
+ }
3203
+ if (lost) {
3204
+ rebuilt.delete(service);
3205
+ notified.delete(service);
3206
+ stopped.add(service);
3207
+ changed = true;
3208
+ } else if (identity && !rebuilt.has(service) && !notified.has(service)) {
3209
+ const instance = service.signals.instance();
3210
+ if (instance && instance.hooks["dependency-change"].length > 0) notified.add(service);
3211
+ else {
3212
+ rebuilt.add(service);
3213
+ changed = true;
3214
+ }
3215
+ }
3216
+ }
3217
+ }
3218
+ return {
3219
+ stopped,
3220
+ rebuilt,
3221
+ notified
3222
+ };
3223
+ }
3224
+ // src/queries/graph.ts
3225
+ const emptyMetadata = Object.freeze({});
3226
+ function readKey(key, action) {
3227
+ let name;
3228
+ try {
3229
+ name = normalizeName(key);
3230
+ } catch (error) {
3231
+ throw new InvalidArgumentError(`Cannot read the key of ${action}.`, { cause: error });
3232
+ }
3233
+ if (typeof name !== "string" || name.length === 0) throw new InvalidArgumentError(`The key of ${action} must be a name or a key.`);
3234
+ return name;
3235
+ }
3236
+ function entryOf(service) {
3237
+ const status = service.signals.effective();
3238
+ return Object.freeze({
3239
+ key: service.name,
3240
+ kind: service.kind === "invocation" ? "invocation" : "service",
3241
+ owner: service.container.owner,
3242
+ phase: service.signals.phase(),
3243
+ ...withoutUndefined({
3244
+ status: status?.status,
3245
+ instanceId: service.signals.instance()?.id,
3246
+ consumer: service.consumer
3247
+ }),
3248
+ metadata: service.metadata,
3249
+ exclusive: service.exclusive,
3250
+ perConsumer: service.perConsumer,
3251
+ exported: service.exported
3252
+ });
3253
+ }
3254
+ function declaredEntryOf(container, entry) {
3255
+ return Object.freeze({
3256
+ key: entry.name,
3257
+ kind: "service",
3258
+ owner: container.owner,
3259
+ phase: "declared",
3260
+ metadata: entry.definition?.metadata ?? emptyMetadata,
3261
+ exclusive: entry.exclusive,
3262
+ perConsumer: entry.definition?.perConsumer ?? false,
3263
+ exported: entry.exported
3264
+ });
3265
+ }
3266
+ /** The services and invocations of the scope that are not `withdrawn`, in registration order. */
3267
+ function servicesOf(scope) {
3268
+ return subtreeOf(scope).flatMap((container) => [...container.services]).filter((service) => service.kind !== "resolver" && service.signals.phase() !== "withdrawn").sort((a, b) => a.seq - b.seq);
3269
+ }
3270
+ /** The keys that open transactions declared and did not publish yet. */
3271
+ function declaredOf(scope) {
3272
+ return subtreeOf(scope).flatMap((container) => [...container.transactions].filter((transaction) => transaction.state === "open").flatMap((transaction) => [...transaction.entries.values()].map((entry) => ({
3273
+ container,
3274
+ entry
3275
+ }))));
3276
+ }
3277
+ /** Finds the service of `name`: the provider that a consumer in `scope` sees, then the first service of that name in the scope. */
3278
+ function lookup(scope, name) {
3279
+ return findProvider(scope, name) ?? servicesOf(scope).find((service) => service.name === name);
3280
+ }
3281
+ /** The providers that the edges of `service` select now, served or not. */
3282
+ function targetsOf(service) {
3283
+ return service.edges.flatMap((edge) => edge.candidates.flatMap((keyName) => {
3284
+ const provider = findProvider(service.scope, keyName, service);
3285
+ return provider ? [provider] : [];
3286
+ }));
3287
+ }
3288
+ function resolutionKind(service, edge) {
3289
+ const resolution = resolutionOf(service, edge);
3290
+ return resolution.kind === "missing" ? { kind: "missing" } : {
3291
+ kind: resolution.kind,
3292
+ provider: resolution.provider
3293
+ };
3294
+ }
3295
+ function dependencyOf(edge, resolution) {
3296
+ return Object.freeze({
3297
+ name: edge.name,
3298
+ keys: edge.candidates,
3299
+ optional: edge.optional,
3300
+ fallback: edge.fallback,
3301
+ conditional: edge.condition !== void 0 || edge.candidateConditions !== void 0,
3302
+ resolution: resolution.kind,
3303
+ ...withoutUndefined({ provider: resolution.provider && entryOf(resolution.provider) })
3304
+ });
3305
+ }
3306
+ /** The resolution of a declared edge, which has no computed resolution yet. */
3307
+ function declaredResolution(container, edge) {
3308
+ for (const keyName of edge.candidates) {
3309
+ const selection = selectProvider(container, keyName);
3310
+ if (!selection) continue;
3311
+ const provider = selection.provider ?? selection.registration;
3312
+ return {
3313
+ kind: provider.signals.serving() ? "resolved" : "not-running",
3314
+ provider
3315
+ };
3316
+ }
3317
+ return { kind: "missing" };
3318
+ }
3319
+ /**
3320
+ * Orders `services` so that each provider comes before its dependents. It is Tarjan's algorithm: each cycle comes out as one group.
3321
+ * Returns the groups in order.
3322
+ */
3323
+ function components(services) {
3324
+ const inScope = new Set(services);
3325
+ const index = /* @__PURE__ */ new Map();
3326
+ const low = /* @__PURE__ */ new Map();
3327
+ const stack = [];
3328
+ const onStack = /* @__PURE__ */ new Set();
3329
+ const groups = [];
3330
+ let counter = 0;
3331
+ const visit = (service) => {
3332
+ index.set(service, counter);
3333
+ low.set(service, counter);
3334
+ counter++;
3335
+ stack.push(service);
3336
+ onStack.add(service);
3337
+ for (const target of targetsOf(service)) {
3338
+ if (!inScope.has(target)) continue;
3339
+ if (!index.has(target)) {
3340
+ visit(target);
3341
+ low.set(service, Math.min(low.get(service), low.get(target)));
3342
+ } else if (onStack.has(target)) low.set(service, Math.min(low.get(service), index.get(target)));
3343
+ }
3344
+ if (low.get(service) === index.get(service)) {
3345
+ const group = [];
3346
+ let member;
3347
+ do {
3348
+ member = stack.pop();
3349
+ onStack.delete(member);
3350
+ group.push(member);
3351
+ } while (member !== service);
3352
+ groups.push(group.sort((a, b) => a.seq - b.seq));
3353
+ }
3354
+ };
3355
+ for (const service of services) if (!index.has(service)) visit(service);
3356
+ return groups;
3357
+ }
3358
+ /** The services of `roots` and their transitive dependencies. */
3359
+ function closure(roots) {
3360
+ const seen = /* @__PURE__ */ new Set();
3361
+ const queue = [...roots];
3362
+ while (queue.length > 0) {
3363
+ const service = queue.shift();
3364
+ if (seen.has(service)) continue;
3365
+ seen.add(service);
3366
+ queue.push(...targetsOf(service));
3367
+ }
3368
+ return [...seen].sort((a, b) => a.seq - b.seq);
3369
+ }
3370
+ function sorted(services) {
3371
+ return Object.freeze([...services].sort((a, b) => a.seq - b.seq).map(entryOf));
3372
+ }
3373
+ function proposedChange(scope, change) {
3374
+ let kind;
3375
+ let key;
3376
+ let container;
3377
+ try {
3378
+ kind = change.kind;
3379
+ key = "key" in change ? change.key : void 0;
3380
+ container = "container" in change ? change.container : void 0;
3381
+ } catch (error) {
3382
+ throw new InvalidArgumentError("Cannot read the impact change.", { cause: error });
3383
+ }
3384
+ switch (kind) {
3385
+ case "withdraw":
3386
+ case "replace": {
3387
+ const service = lookup(scope, readKey(key, "impact"));
3388
+ if (!service) return {
3389
+ removed: /* @__PURE__ */ new Set(),
3390
+ renewed: /* @__PURE__ */ new Set()
3391
+ };
3392
+ const registration = service.template ?? service;
3393
+ return kind === "withdraw" ? {
3394
+ removed: withdrawnServices(registration),
3395
+ renewed: /* @__PURE__ */ new Set()
3396
+ } : {
3397
+ removed: /* @__PURE__ */ new Set(),
3398
+ renewed: withdrawnServices(registration)
3399
+ };
3400
+ }
3401
+ case "dispose": return {
3402
+ removed: disposedServices(nodeOf(container)),
3403
+ renewed: /* @__PURE__ */ new Set()
3404
+ };
3405
+ default: throw new InvalidArgumentError(`The impact change kind must be 'withdraw', 'replace', or 'dispose'. Got ${String(kind)}.`);
3406
+ }
3407
+ }
3408
+ /**
3409
+ * Returns the graph queries of `container`. They cover the container and its descendants. Each query is a pure read.
3410
+ *
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.
3413
+ */
3414
+ function graph(container) {
3415
+ const scope = nodeOf(container);
3416
+ return Object.freeze({
3417
+ services: () => untracked(() => Object.freeze([...servicesOf(scope).map(entryOf), ...declaredOf(scope).map(({ container: owner, entry }) => declaredEntryOf(owner, entry))])),
3418
+ dependencies: (key) => untracked(() => {
3419
+ const name = readKey(key, "dependencies");
3420
+ const service = lookup(scope, name);
3421
+ if (service) return Object.freeze(service.edges.map((edge) => dependencyOf(edge, resolutionKind(service, edge))));
3422
+ const declared = declaredOf(scope).find(({ entry }) => entry.name === name);
3423
+ const edges = declared?.entry.definition?.edges ?? declared?.entry.declared ?? [];
3424
+ return Object.freeze(edges.map((edge) => dependencyOf(edge, declaredResolution(declared.container, edge))));
3425
+ }),
3426
+ dependents: (key) => untracked(() => {
3427
+ const target = lookup(scope, readKey(key, "dependents"));
3428
+ if (!target) return Object.freeze([]);
3429
+ const registration = target.template ?? target;
3430
+ return Object.freeze(servicesOf(scope).filter((service) => service.edges.some((edge) => {
3431
+ const resolution = resolutionOf(service, edge);
3432
+ return resolution.kind !== "missing" && (resolution.provider === target || (resolution.provider.template ?? resolution.provider) === registration);
3433
+ })).map(entryOf));
3434
+ }),
3435
+ order: (keys) => untracked(() => {
3436
+ const services = keys === void 0 ? servicesOf(scope) : closure(keys.flatMap((key) => {
3437
+ const service = lookup(scope, readKey(key, "order"));
3438
+ return service ? [service] : [];
3439
+ }));
3440
+ return Object.freeze(components(services).flat().map(entryOf));
3441
+ }),
3442
+ cycles: () => untracked(() => Object.freeze(components(servicesOf(scope)).filter((group) => group.length > 1).map((group) => Object.freeze(group.map(entryOf))))),
3443
+ impact: (change) => untracked(() => {
3444
+ const result = computeImpact(scope, proposedChange(scope, change));
3445
+ return Object.freeze({
3446
+ stopped: sorted(result.stopped),
3447
+ rebuilt: sorted(result.rebuilt),
3448
+ notified: sorted(result.notified)
3449
+ });
3450
+ }),
3451
+ explain: (key) => untracked(() => {
3452
+ const name = readKey(key, "explain");
3453
+ const service = lookup(scope, name);
3454
+ if (service) return explainService(service);
3455
+ const declared = declaredOf(scope).find(({ entry }) => entry.name === name);
3456
+ if (declared) return Object.freeze({
3457
+ name,
3458
+ kind: "service",
3459
+ owner: declared.container.owner,
3460
+ phase: "declared",
3461
+ blockers: Object.freeze([])
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
+ });
3474
+ })
3475
+ });
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
+ }
3552
+ // src/queries/listeners.ts
3553
+ /**
3554
+ * Returns the current listeners for `eventKey` in `container` and its descendants, in registration order.
3555
+ *
3556
+ * Each entry has the handler, the owner record of the container that owns it, and its metadata.
3557
+ * `H` is the handler type that the caller expects. injeca does not check it.
3558
+ */
3559
+ function listeners(container, eventKey) {
3560
+ const id = readEventId(eventKey);
3561
+ return Object.freeze(subtreeOf(nodeOf(container)).filter((node) => node.state !== "disposed").flatMap((node) => (node.listeners.get(id) ?? []).map((record) => ({
3562
+ record,
3563
+ owner: node.owner
3564
+ }))).sort((a, b) => a.record.seq - b.record.seq).map(({ record, owner }) => Object.freeze({
3565
+ handler: record.handler,
3566
+ owner,
3567
+ metadata: record.metadata
3568
+ })));
3569
+ }
3570
+ // src/global.ts
3571
+ let globalContainer = createContainer();
3572
+ function setLogger(logger) {
3573
+ globalContainer.logger = logger;
3574
+ }
3575
+ /** Returns the root container of the global functions. Pass it as `parent` to make a child container. */
3576
+ function getContainer() {
3577
+ return globalContainer;
3578
+ }
3579
+ /** Replaces the global container with a new one. `getContainer()` then returns the new one. It does not stop or dispose the old container. */
3580
+ function resetContainer() {
3581
+ globalContainer = createContainer();
3582
+ }
3583
+ function provide(nameOrOption, option) {
3584
+ return provideIn(globalContainer, 3, nameOrOption, option);
3585
+ }
3586
+ function invoke(option) {
3587
+ return invokeIn(globalContainer, option);
3588
+ }
3589
+ function resolve(dependsOn, options) {
3590
+ return resolve$1(globalContainer, dependsOn, options);
3591
+ }
3592
+ function start(options) {
3593
+ return start$1(globalContainer, options);
3594
+ }
3595
+ function stop(options) {
3596
+ return stop$1(globalContainer, options);
3597
+ }
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 };
3599
+
3600
+ //# sourceMappingURL=global-B5N-d-TO.mjs.map