injeca 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,3790 @@
1
+ import { a as DuplicateServiceError, d as ReentrantDisposeError, f as ReplaceWhileStoppingError, h as TransactionIncompleteError, i as BuildAfterDisposeError, l as InvalidArgumentError, m as StartTimeoutError, n as defaultDecision, o as HookOutsideBuildError, p as StaleRegistrationError, r as validateDecision, s as HookTimeoutError, t as createDecide } from "./decision-odiPb2Au.mjs";
2
+ import { computed, effect, effectScope, endBatch, getActiveSub, setActiveSub, signal, startBatch } from "alien-signals";
3
+ import { isUndefined, omitBy } from "es-toolkit";
4
+ import ErrorStackParser from "error-stack-parser";
5
+ import { nanoid } from "nanoid";
6
+ // package.json
7
+ var name = "injeca";
8
+ // src/diagnostics/logger.ts
9
+ function formatDuration(ms) {
10
+ if (ms < 1) return `${(ms * 1e3).toFixed(3)}µs`;
11
+ if (ms < 1e3) return `${ms.toFixed(3)}ms`;
12
+ return `${(ms / 1e3).toFixed(3)}s`;
13
+ }
14
+ function createDefaultLogger() {
15
+ return {
16
+ provide: (name, dependencies) => {
17
+ const depsStr = dependencies.length > 0 ? ` (depends on: ${dependencies.join(", ")})` : "";
18
+ console.log(`[${name}] PROVIDE ${name}${depsStr}`);
19
+ },
20
+ invoke: (dependencies) => {
21
+ const depsStr = dependencies.length > 0 ? ` (depends on: ${dependencies.join(", ")})` : "";
22
+ console.log(`[${name}] INVOKE${depsStr}`);
23
+ },
24
+ beforeRun: (name) => {
25
+ console.log(`[${name}] BEFORE RUN provide: ${name}()`);
26
+ },
27
+ run: (name, duration) => {
28
+ console.log(`[${name}] RUN provide: ${name}() in ${formatDuration(duration)}`);
29
+ },
30
+ hookOnStart: (name) => {
31
+ console.log(`[${name}] HOOK OnStart ${name}() executing`);
32
+ },
33
+ hookOnStartComplete: (name, duration) => {
34
+ console.log(`[${name}] HOOK OnStart ${name}() ran successfully in ${formatDuration(duration)}`);
35
+ },
36
+ hookOnStop: (name) => {
37
+ console.log(`[${name}] HOOK OnStop ${name}() executing`);
38
+ },
39
+ hookOnStopComplete: (name, duration) => {
40
+ console.log(`[${name}] HOOK OnStop ${name}() ran successfully in ${formatDuration(duration)}`);
41
+ },
42
+ running: () => {
43
+ console.log(`[${name}] RUNNING`);
44
+ }
45
+ };
46
+ }
47
+ function createLoggLogger(logg) {
48
+ return {
49
+ provide: (name, dependencies) => {
50
+ const depsStr = dependencies.length > 0 ? ` (depends on: ${dependencies.join(", ")})` : "";
51
+ logg.log(`PROVIDE ${name}${depsStr}`);
52
+ },
53
+ invoke: (dependencies) => {
54
+ const depsStr = dependencies.length > 0 ? ` (depends on: ${dependencies.join(", ")})` : "";
55
+ logg.log(`INVOKE${depsStr}`);
56
+ },
57
+ beforeRun: (name) => {
58
+ logg.log(`BEFORE RUN provide: ${name}()`);
59
+ },
60
+ run: (name, duration) => {
61
+ logg.log(`RUN provide: ${name}() in ${formatDuration(duration)}`);
62
+ },
63
+ hookOnStart: (name) => {
64
+ logg.log(`HOOK OnStart ${name}() executing`);
65
+ },
66
+ hookOnStartComplete: (name, duration) => {
67
+ logg.log(`HOOK OnStart ${name}() ran successfully in ${formatDuration(duration)}`);
68
+ },
69
+ hookOnStop: (name) => {
70
+ logg.log(`HOOK OnStop ${name}() executing`);
71
+ },
72
+ hookOnStopComplete: (name, duration) => {
73
+ logg.log(`HOOK OnStop ${name}() ran successfully in ${formatDuration(duration)}`);
74
+ },
75
+ running: () => {
76
+ logg.log(`RUNNING`);
77
+ }
78
+ };
79
+ }
80
+ function createNoopLogger() {
81
+ return {
82
+ provide: () => {},
83
+ invoke: () => {},
84
+ beforeRun: () => {},
85
+ run: () => {},
86
+ hookOnStart: () => {},
87
+ hookOnStartComplete: () => {},
88
+ hookOnStop: () => {},
89
+ hookOnStopComplete: () => {},
90
+ running: () => {}
91
+ };
92
+ }
93
+ // src/helpers/signals.ts
94
+ /**
95
+ * Runs `fn` with tracking paused.
96
+ *
97
+ * The tracking rule: each entry point pauses tracking once. The entry points are the public API calls, the reconciler drain, timer callbacks, and the calls of user callbacks.
98
+ * Code inside an entry point reads signals directly. Only the bodies of `computed` and `effect` track.
99
+ * A promise continuation starts with no active subscriber, so it needs no extra pause.
100
+ */
101
+ function untracked(fn) {
102
+ const previous = setActiveSub(void 0);
103
+ try {
104
+ return fn();
105
+ } finally {
106
+ setActiveSub(previous);
107
+ }
108
+ }
109
+ /** Writes signals in one batch. Effects see one consistent change. */
110
+ function batch(fn) {
111
+ startBatch();
112
+ try {
113
+ fn();
114
+ } finally {
115
+ endBatch();
116
+ }
117
+ }
118
+ /** Runs `fn` with `owner` as the active subscriber, so effects that `fn` creates belong to `owner`. */
119
+ function withOwner(owner, fn) {
120
+ const previous = setActiveSub(owner);
121
+ try {
122
+ return fn();
123
+ } finally {
124
+ setActiveSub(previous);
125
+ }
126
+ }
127
+ // src/helpers/error-from.ts
128
+ /**
129
+ * Makes one error from collected errors. One error keeps its identity. More than one error becomes an `AggregateError`.
130
+ *
131
+ * The result is `unknown`, because a thrown value is not always an `Error`.
132
+ */
133
+ function errorFrom(errors, message) {
134
+ return errors.length === 1 ? errors[0] : new AggregateError(errors, message);
135
+ }
136
+ // src/graph/resolvers/exclusive.ts
137
+ /** Tells whether `ancestor` is `container` or an ancestor of it, with no container in between that isolates `keyName`. */
138
+ function reaches(container, ancestor, keyName) {
139
+ for (let current = container; current; current = current.parent) {
140
+ if (current === ancestor) return true;
141
+ if (current.isolated.has(keyName)) return false;
142
+ }
143
+ return false;
144
+ }
145
+ /**
146
+ * Tells whether two containers share a resolution area of `keyName`.
147
+ * A resolution area is the container that has the key, with its descendants that do not isolate the key.
148
+ */
149
+ function sameArea(a, b, keyName) {
150
+ return reaches(a, b, keyName) || reaches(b, a, keyName);
151
+ }
152
+ /** The published services of `keyName` in all containers of the runtime. */
153
+ function declarationsOf(runtime, keyName) {
154
+ let declarations = runtime.declarations.get(keyName);
155
+ if (!declarations) {
156
+ declarations = signal(/* @__PURE__ */ new Set());
157
+ runtime.declarations.set(keyName, declarations);
158
+ }
159
+ return declarations;
160
+ }
161
+ function addDeclaration(service) {
162
+ const declarations = declarationsOf(service.container.runtime, service.name);
163
+ if (!declarations().has(service)) declarations(/* @__PURE__ */ new Set([...declarations(), service]));
164
+ }
165
+ function removeDeclaration(service) {
166
+ const declarations = declarationsOf(service.container.runtime, service.name);
167
+ if (declarations().has(service)) declarations(new Set([...declarations()].filter((other) => other !== service)));
168
+ }
169
+ function sameList(a, b) {
170
+ return a.length === b.length && a.every((service, index) => service === b[index]);
171
+ }
172
+ const none = Object.freeze([]);
173
+ /**
174
+ * Creates the conflict of a service: the other present declarations of its key in the same resolution area, when the key is exclusive.
175
+ *
176
+ * The result depends only on the set of declarations. The order of the registrations has no effect.
177
+ * Only one registration of a key exists in one container, so a rival is always in another container.
178
+ * A service of a `perConsumer` registration has no conflict of its own. It follows the conflict of its registration.
179
+ */
180
+ function createRivals(service) {
181
+ if (service.template) return () => service.template?.signals.rivals() ?? none;
182
+ if (service.kind !== "service") return () => none;
183
+ return computed((previous) => {
184
+ const rivals = [...declarationsOf(service.container.runtime, service.name)()].filter((other) => other.container !== service.container && other.signals.present() && (service.exclusive || other.exclusive) && sameArea(service.container, other.container, service.name)).sort((a, b) => a.seq - b.seq);
185
+ return previous && sameList(previous, rivals) ? previous : rivals;
186
+ });
187
+ }
188
+ /** Tells whether the key of the service has more than one declaration in its resolution area. */
189
+ function inConflict(service) {
190
+ return service.signals.rivals().length > 0;
191
+ }
192
+ // src/graph/resolvers/resolvers.ts
193
+ /** Returns the slot signal of `keyName` in `container`. It creates the slot on first read, so that resolution can track it. */
194
+ function slotOf(container, keyName) {
195
+ let slot = container.signals.slots.get(keyName);
196
+ if (!slot) {
197
+ slot = signal(void 0);
198
+ container.signals.slots.set(keyName, slot);
199
+ }
200
+ return slot;
201
+ }
202
+ /**
203
+ * Lists the registrations of `keyName` that a consumer in `container` can see, nearest first.
204
+ * The walk ends at a container that isolates the key. A service does not resolve itself.
205
+ */
206
+ function candidatesOf(container, keyName, consumer) {
207
+ const candidates = [];
208
+ for (let current = container; current; current = current.parent) {
209
+ const candidate = slotOf(current, keyName)();
210
+ if (candidate && candidate !== consumer) candidates.push(candidate);
211
+ if (current.isolated.has(keyName)) break;
212
+ }
213
+ return candidates;
214
+ }
215
+ /** The service that serves `registration` for a consumer in `container`. A `perConsumer` registration has one for each consumer container, or none yet. */
216
+ function serviceFor(registration, container) {
217
+ return registration.perConsumer ? registration.signals.consumers().get(container) : registration;
218
+ }
219
+ /**
220
+ * Selects the provider of `keyName` for a consumer in `container`.
221
+ *
222
+ * A nearer registration shadows an ancestor only while it is `running`. Until then, the first serving registration wins.
223
+ * If no registration serves, the nearest one is selected. So shadowing never leaves a gap.
224
+ */
225
+ function selectProvider(container, keyName, consumer) {
226
+ const candidates = candidatesOf(container, keyName, consumer);
227
+ for (const registration of candidates) {
228
+ const provider = serviceFor(registration, container);
229
+ if (provider?.signals.serving()) return {
230
+ registration,
231
+ provider
232
+ };
233
+ }
234
+ const [nearest] = candidates;
235
+ return nearest ? {
236
+ registration: nearest,
237
+ provider: serviceFor(nearest, container)
238
+ } : void 0;
239
+ }
240
+ /** Finds the service for `keyName` that a consumer in `container` sees. A `perConsumer` registration without a service for the container gives the registration. */
241
+ function findProvider(container, keyName, consumer) {
242
+ const selection = selectProvider(container, keyName, consumer);
243
+ return selection && (selection.provider ?? selection.registration);
244
+ }
245
+ /** `ready` and `degraded` count as available. A provider outside `running` has no status and is not available. */
246
+ function isAvailable(provider) {
247
+ const status = provider.signals.effective();
248
+ return status !== void 0 && status.status !== "unavailable";
249
+ }
250
+ /** The current resolution of the edge. */
251
+ function resolutionOf(service, edge) {
252
+ return service.signals.resolutions.get(edge.name)?.() ?? { kind: "missing" };
253
+ }
254
+ /** The instance of the edge, when it resolves. */
255
+ function resolvedInstance(service, edge) {
256
+ const resolution = resolutionOf(service, edge);
257
+ return resolution.kind === "resolved" ? resolution.instance : void 0;
258
+ }
259
+ /** Tells whether each required edge resolves to an available provider. A pending service builds only then. */
260
+ function requiredAvailable(service) {
261
+ return service.edges.every((edge) => {
262
+ if (edge.optional) return true;
263
+ const resolution = resolutionOf(service, edge);
264
+ return resolution.kind === "resolved" && isAvailable(resolution.provider);
265
+ });
266
+ }
267
+ /** Tells whether a required edge no longer resolves: its provider was withdrawn, or it does not run. */
268
+ function requiredWithdrawn(service) {
269
+ return service.edges.some((edge) => !edge.optional && resolvedInstance(service, edge) === void 0);
270
+ }
271
+ /** Tells whether the current resolution of a required edge differs from the bindings of `instance`. */
272
+ function requiredChanged(service, instance) {
273
+ return service.edges.some((edge) => !edge.optional && resolvedInstance(service, edge) !== instance.bindings.get(edge.name));
274
+ }
275
+ /** A build is discarded when its target is lost, a required dependency changed during the build, or the key went into conflict. */
276
+ function buildLost(service, instance) {
277
+ return !service.signals.present() || !service.signals.active() || service.signals.rivals().length > 0 || requiredChanged(service, instance);
278
+ }
279
+ /** The identity of the dependency set: the revision and the instance id of each resolved required dependency. */
280
+ function epochOf(service, pick) {
281
+ const ids = service.edges.filter((edge) => !edge.optional).map((edge) => `${edge.name}=${pick(edge)?.id ?? ""}`);
282
+ return [String(service.revision), ...ids].join("|");
283
+ }
284
+ /**
285
+ * Finds the failure that blocks a pending service: a required provider in `failed`, directly or through pending providers.
286
+ */
287
+ function blockingFailure(service, visited = /* @__PURE__ */ new Set()) {
288
+ visited.add(service);
289
+ for (const edge of service.edges) {
290
+ if (edge.optional) continue;
291
+ const resolution = resolutionOf(service, edge);
292
+ if (resolution.kind !== "not-running" || visited.has(resolution.provider)) continue;
293
+ const provider = resolution.provider;
294
+ const phase = provider.signals.phase();
295
+ if (phase === "failed") return provider.failure;
296
+ if (phase === "pending") {
297
+ const failure = blockingFailure(provider, visited);
298
+ if (failure) return failure;
299
+ }
300
+ }
301
+ }
302
+ // src/graph/nodes/container.ts
303
+ const nodes = /* @__PURE__ */ new WeakMap();
304
+ /** Connects the public container object to its node. `createContainer` calls it once. */
305
+ function registerNode(container, node) {
306
+ nodes.set(container, node);
307
+ }
308
+ /** Returns the internal node of a container from `createContainer`. */
309
+ function nodeOf(container) {
310
+ const node = typeof container === "object" && container !== null ? nodes.get(container) : void 0;
311
+ if (!node) throw new InvalidArgumentError("The value is not a container from createContainer.");
312
+ return node;
313
+ }
314
+ function assertActive(container, action) {
315
+ if (container.state !== "active") throw new BuildAfterDisposeError(`Cannot ${action} after the container disposal started.`);
316
+ }
317
+ /** Lists `container` and its descendants that were started, through started containers only. `start()` and `stop()` apply to them. */
318
+ function affectedContainers(container) {
319
+ const result = [container];
320
+ for (const child of container.children) if (child.enrolled) result.push(...affectedContainers(child));
321
+ return result;
322
+ }
323
+ /** Lists `container` and all its descendants. */
324
+ function subtreeOf(container) {
325
+ return [container, ...[...container.children].flatMap(subtreeOf)];
326
+ }
327
+ /**
328
+ * Makes `service` visible to consumers: the slot of its key, or the consumer map of its `perConsumer` registration.
329
+ * An exclusive conflict counts each published service with a key.
330
+ */
331
+ function publishService(service) {
332
+ const { template } = service;
333
+ if (template) {
334
+ const consumers = template.signals.consumers();
335
+ if (consumers.get(service.container) !== service) template.signals.consumers(new Map([...consumers, [service.container, service]]));
336
+ return;
337
+ }
338
+ slotOf(service.container, service.name)(service);
339
+ addDeclaration(service);
340
+ }
341
+ /**
342
+ * Clears the slot and marks the service absent. The reconciler stops it after its holders release it.
343
+ * A `perConsumer` registration also releases the service of each consumer container.
344
+ */
345
+ function releaseService(service) {
346
+ service.signals.present(false);
347
+ if (service.kind !== "service" || service.template) return;
348
+ for (const consumer of service.signals.consumers().values()) consumer.signals.present(false);
349
+ const slot = slotOf(service.container, service.name);
350
+ if (slot() === service) slot(void 0);
351
+ }
352
+ /** Waits until `service` is `withdrawn`. Rejects with its teardown errors. */
353
+ async function awaitWithdrawal(service) {
354
+ const errors = await service.withdrawal.promise;
355
+ if (errors.length > 0) throw errorFrom(errors, `Withdrawal of '${service.name}' failed.`);
356
+ }
357
+ /** Withdraws a service or an invocation. Resolves `false` when it is already withdrawn. It is the `withdraw()` entry point. */
358
+ async function withdrawService(service) {
359
+ if (!untracked(() => {
360
+ if (!service.signals.present()) return false;
361
+ batch(() => releaseService(service));
362
+ return true;
363
+ })) return false;
364
+ await awaitWithdrawal(service);
365
+ return true;
366
+ }
367
+ // src/helpers/deferred.ts
368
+ function createDeferred() {
369
+ let resolve;
370
+ return {
371
+ promise: new Promise((done) => {
372
+ resolve = done;
373
+ }),
374
+ resolve
375
+ };
376
+ }
377
+ // src/helpers/without-undefined.ts
378
+ /** Copies `fields` without the fields that are `undefined`. Spread the result to add optional fields to a record. */
379
+ function withoutUndefined(fields) {
380
+ return omitBy(fields, isUndefined);
381
+ }
382
+ // src/graph/observers.ts
383
+ function within(container, ancestor) {
384
+ for (let current = container; current; current = current.parent) if (current === ancestor) return true;
385
+ return false;
386
+ }
387
+ /** The active observers of `container`: the observers of the container and of its ancestors. */
388
+ function targetsOf$1(container, source) {
389
+ return [...container.runtime.observers].filter((observer) => observer !== source && observer.active && within(container, observer.container));
390
+ }
391
+ function send(container, targets, record) {
392
+ container.runtime.deliveries.deliver(targets.map((observer) => ({
393
+ run: () => {
394
+ if (observer.active && observer.container.state !== "disposed") observer.callback(record);
395
+ },
396
+ onError: (error) => {
397
+ if (record.type !== "error") reportCallbackError(observer.container, record.key, error, "An observe callback threw.", observer);
398
+ }
399
+ })));
400
+ }
401
+ /** Sends `record` to the observers of `container`. */
402
+ function emitRecord(container, record) {
403
+ const targets = targetsOf$1(container);
404
+ if (targets.length === 0) return;
405
+ send(container, targets, Object.freeze(record));
406
+ }
407
+ /** Reports a callback error as an `error` record. An error from an observe callback does not go back to that callback. */
408
+ function reportCallbackError(container, key, error, reason, source) {
409
+ const record = {
410
+ type: "error",
411
+ key,
412
+ owner: container.owner,
413
+ instanceId: void 0,
414
+ reason,
415
+ error
416
+ };
417
+ send(container, targetsOf$1(container, source), record);
418
+ }
419
+ // src/graph/resolvers/condition.ts
420
+ /**
421
+ * The verdict of each condition for each provider registration.
422
+ * The input of a condition is frozen, and a condition must be pure. So each pair runs once, and a throw is reported once.
423
+ */
424
+ const verdicts = /* @__PURE__ */ new WeakMap();
425
+ /** The read-only information about a provider registration that a condition gets. */
426
+ function providerInfo(registration) {
427
+ return Object.freeze({
428
+ key: registration.name,
429
+ owner: registration.container.owner,
430
+ metadata: registration.metadata
431
+ });
432
+ }
433
+ /**
434
+ * Tells whether `condition` accepts the provider registration.
435
+ * A thrown error counts as `false` and goes to the observe stream of the consumer.
436
+ */
437
+ function acceptsProvider(consumer, keyName, condition, registration) {
438
+ let byProvider = verdicts.get(condition);
439
+ if (!byProvider) {
440
+ byProvider = /* @__PURE__ */ new WeakMap();
441
+ verdicts.set(condition, byProvider);
442
+ }
443
+ const known = byProvider.get(registration);
444
+ if (known !== void 0) return known;
445
+ let verdict;
446
+ try {
447
+ verdict = untracked(() => condition(providerInfo(registration))) === true;
448
+ } catch (error) {
449
+ verdict = false;
450
+ queueMicrotask(() => reportCallbackError(consumer.container, consumer.name, error, `A resolution condition for '${keyName}' threw. It counts as false.`));
451
+ }
452
+ byProvider.set(registration, verdict);
453
+ return verdict;
454
+ }
455
+ // src/graph/resolvers/plain.ts
456
+ /**
457
+ * Resolves one key name: the selected provider, and its instance when the provider serves.
458
+ * A provider in conflict, or one that `condition` rejects, does not resolve.
459
+ */
460
+ function resolveKey(consumer, keyName, condition) {
461
+ const selection = selectProvider(consumer.container, keyName, consumer);
462
+ if (!selection) return { kind: "missing" };
463
+ const { registration, provider } = selection;
464
+ const rivals = registration.signals.rivals();
465
+ if (rivals.length > 0) return {
466
+ kind: "conflict",
467
+ provider: registration,
468
+ rivals
469
+ };
470
+ if (condition && !acceptsProvider(consumer, keyName, condition, registration)) return {
471
+ kind: "rejected",
472
+ provider: registration
473
+ };
474
+ if (!provider) return {
475
+ kind: "not-running",
476
+ provider: registration
477
+ };
478
+ const instance = provider.signals.instance();
479
+ if (!provider.signals.serving() || !instance) return {
480
+ kind: "not-running",
481
+ provider
482
+ };
483
+ return {
484
+ kind: "resolved",
485
+ provider,
486
+ instance
487
+ };
488
+ }
489
+ /** Resolves a plain or an optional edge. It has one candidate key. */
490
+ function resolvePlain(consumer, edge) {
491
+ const [keyName] = edge.candidates;
492
+ return keyName === void 0 ? { kind: "missing" } : resolveKey(consumer, keyName, edge.condition);
493
+ }
494
+ // src/graph/resolvers/fallback.ts
495
+ /**
496
+ * Resolves a `firstAvailable` edge. The first serving and available candidate wins.
497
+ * If no serving candidate is available, the first serving candidate stays selected, so that a running dependent does not lose it.
498
+ * Without a serving candidate, the edge reports the first candidate that exists.
499
+ */
500
+ function resolveFallback(consumer, edge) {
501
+ let firstUnresolved;
502
+ let firstServing;
503
+ for (const keyName of edge.candidates) {
504
+ const resolution = resolveKey(consumer, keyName, edge.condition);
505
+ if (resolution.kind === "missing") continue;
506
+ if (resolution.kind !== "resolved") {
507
+ firstUnresolved ??= resolution;
508
+ continue;
509
+ }
510
+ if (isAvailable(resolution.provider)) return resolution;
511
+ firstServing ??= resolution;
512
+ }
513
+ return firstServing ?? firstUnresolved ?? { kind: "missing" };
514
+ }
515
+ // src/graph/status.ts
516
+ const statusRank = {
517
+ ready: 0,
518
+ degraded: 1,
519
+ unavailable: 2
520
+ };
521
+ /** Tells whether `a` is worse than `b`. The order is `ready`, then `degraded`, then `unavailable`. */
522
+ function worseThan(a, b) {
523
+ return statusRank[a] > statusRank[b];
524
+ }
525
+ function sameCause(a, b) {
526
+ if (a === b) return true;
527
+ if (!a || !b) return false;
528
+ return a.key === b.key && a.owner === b.owner && a.status === b.status && a.reason === b.reason && sameCause(a.cause, b.cause);
529
+ }
530
+ /** Tells whether two status values have the same status, reason, and cause. */
531
+ function sameStatus(a, b) {
532
+ if (a === b) return true;
533
+ if (!a || !b) return false;
534
+ return a.status === b.status && a.reason === b.reason && sameCause(a.cause, b.cause);
535
+ }
536
+ function statusFields(status) {
537
+ return withoutUndefined({
538
+ reason: status.reason,
539
+ cause: status.cause
540
+ });
541
+ }
542
+ /** The cause that a dependent reports when `provider` makes its effective status worse. */
543
+ function causeOf(provider, status) {
544
+ return Object.freeze({
545
+ key: provider.name,
546
+ owner: provider.container.owner,
547
+ status: status.status,
548
+ ...withoutUndefined({
549
+ reason: status.reason,
550
+ cause: status.cause
551
+ })
552
+ });
553
+ }
554
+ /**
555
+ * Creates the effective status of a service: the worst of the own status and the effective status of each required dependency.
556
+ * An optional dependency does not count. It is `undefined` outside `running`.
557
+ */
558
+ function createEffectiveStatus(input) {
559
+ const { phase, instance, edges, resolutions } = input;
560
+ return computed((previous) => {
561
+ const current = instance();
562
+ if (phase() !== "running" || !current) return void 0;
563
+ let result = current.signals.own();
564
+ for (const edge of edges) {
565
+ if (edge.optional) continue;
566
+ const resolution = resolutions.get(edge.name)?.();
567
+ if (resolution?.kind !== "resolved") continue;
568
+ const dependency = resolution.provider.signals.effective();
569
+ if (dependency && worseThan(dependency.status, result.status)) result = {
570
+ status: dependency.status,
571
+ cause: causeOf(resolution.provider, dependency)
572
+ };
573
+ }
574
+ return previous && sameStatus(previous, result) ? previous : result;
575
+ });
576
+ }
577
+ /** Reports a change of the effective status. The status effect of each service calls it. */
578
+ function reportStatus(service, next) {
579
+ const previous = service.reportedStatus;
580
+ service.reportedStatus = next;
581
+ if (!next || sameStatus(previous, next)) return;
582
+ emitRecord(service.container, {
583
+ type: "status",
584
+ key: service.name,
585
+ owner: service.container.owner,
586
+ instanceId: service.signals.instance()?.id,
587
+ from: previous?.status,
588
+ to: next.status,
589
+ ...statusFields(next)
590
+ });
591
+ }
592
+ // src/graph/nodes/service.ts
593
+ /** Writes the phase and sends the change to the observe stream. */
594
+ function setPhase(service, to, details = {}) {
595
+ const from = service.signals.phase();
596
+ if (from === to) return;
597
+ batch(() => {
598
+ service.signals.phase(to);
599
+ if (service.kind === "resolver") return;
600
+ emitRecord(service.container, {
601
+ type: "phase",
602
+ key: service.name,
603
+ owner: service.container.owner,
604
+ instanceId: service.signals.instance()?.id,
605
+ from,
606
+ to,
607
+ ...withoutUndefined({
608
+ reason: details.reason,
609
+ decision: details.decision
610
+ })
611
+ });
612
+ });
613
+ }
614
+ function sameResolution(a, b) {
615
+ if (a.kind !== b.kind) return false;
616
+ switch (a.kind) {
617
+ case "missing": return true;
618
+ case "not-running":
619
+ case "rejected": return "provider" in b && a.provider === b.provider;
620
+ case "conflict": return b.kind === "conflict" && a.provider === b.provider && a.rivals === b.rivals;
621
+ case "resolved": return b.kind === "resolved" && a.provider === b.provider && a.instance === b.instance;
622
+ }
623
+ }
624
+ /** Creates the resolution of one declared edge. It keeps the previous value when nothing changed. */
625
+ function resolveEdge(consumer, edge) {
626
+ return computed((previous) => {
627
+ const next = edge.fallback ? resolveFallback(consumer, edge) : resolvePlain(consumer, edge);
628
+ return previous && sameResolution(previous, next) ? previous : next;
629
+ });
630
+ }
631
+ const emptyMetadata$2 = Object.freeze({});
632
+ const emptyPolicy$1 = Object.freeze({});
633
+ /**
634
+ * Creates a service with its signals, its derived values, and its effects.
635
+ *
636
+ * The main effect reads the derived values of the service and only marks the service dirty.
637
+ * The status effect sends each change of the effective status to the observe stream.
638
+ */
639
+ function createService(input) {
640
+ const { container } = input;
641
+ const { runtime } = container;
642
+ const present = signal(true);
643
+ const phase = signal(input.phase ?? "pending");
644
+ const instance = signal(void 0);
645
+ const demanded = signal(input.kind === "resolver");
646
+ const resolutions = /* @__PURE__ */ new Map();
647
+ const effective = createEffectiveStatus({
648
+ phase,
649
+ instance,
650
+ edges: input.edges,
651
+ resolutions
652
+ });
653
+ let rivals = () => [];
654
+ const service = {
655
+ seq: runtime.nextSeq(),
656
+ kind: input.kind,
657
+ name: input.name,
658
+ container,
659
+ edges: input.edges,
660
+ build: input.build,
661
+ metadata: input.metadata ?? emptyMetadata$2,
662
+ policy: input.policy ?? emptyPolicy$1,
663
+ exclusive: input.exclusive ?? false,
664
+ perConsumer: input.perConsumer ?? false,
665
+ template: input.template,
666
+ consumer: input.consumer,
667
+ registration: input.registration,
668
+ swap: input.swap,
669
+ replacedBy: void 0,
670
+ transaction: input.transaction,
671
+ revision: 0,
672
+ signals: {
673
+ present,
674
+ phase,
675
+ instance,
676
+ demanded,
677
+ active: computed(() => container.signals.started() || input.kind !== "invocation" && demanded()),
678
+ serving: computed(() => present() && phase() === "running"),
679
+ resolutions,
680
+ effective,
681
+ rivals: () => rivals(),
682
+ consumers: signal(/* @__PURE__ */ new Map()),
683
+ targetLost: computed(() => !present() || !service.signals.active() || service.signals.rivals().length > 0 || requiredWithdrawn(service))
684
+ },
685
+ busy: false,
686
+ stopIntent: {},
687
+ parkedEpoch: void 0,
688
+ failure: void 0,
689
+ teardownErrors: [],
690
+ withdrawal: createDeferred(),
691
+ reportedStatus: void 0,
692
+ disposeEffects: () => {}
693
+ };
694
+ for (const edge of input.edges) resolutions.set(edge.name, resolveEdge(service, edge));
695
+ rivals = createRivals(service);
696
+ const stopMain = withOwner(container.reactiveOwner, () => effect(() => {
697
+ present();
698
+ phase();
699
+ service.signals.active();
700
+ service.signals.rivals();
701
+ instance()?.signals.holders();
702
+ service.swap?.target.signals.present();
703
+ for (const resolution of resolutions.values()) {
704
+ const current = resolution();
705
+ if (current.kind === "not-running") current.provider.signals.phase();
706
+ if (current.kind === "resolved") current.provider.signals.effective();
707
+ }
708
+ runtime.reconciler.markDirty(service);
709
+ }));
710
+ const stopStatus = input.kind === "resolver" ? () => {} : withOwner(container.reactiveOwner, () => effect(() => {
711
+ const status = effective();
712
+ untracked(() => reportStatus(service, status));
713
+ }));
714
+ service.disposeEffects = () => {
715
+ stopMain();
716
+ stopStatus();
717
+ };
718
+ return service;
719
+ }
720
+ // src/graph/nodes/consumer.ts
721
+ /**
722
+ * Returns the service of a `perConsumer` registration for one consumer container. It creates the service on first use.
723
+ *
724
+ * The service lives in the consumer container, so it disposes with that container. Its build context has the owner of the container as `consumer`.
725
+ * It returns `undefined` when the registration is withdrawn or the container disposal started.
726
+ */
727
+ function ensureConsumerService(registration, container) {
728
+ const existing = registration.signals.consumers().get(container);
729
+ if (existing) return existing;
730
+ if (!registration.signals.present() || container.state !== "active") return void 0;
731
+ let service;
732
+ batch(() => {
733
+ service = createService({
734
+ container,
735
+ kind: "service",
736
+ name: registration.name,
737
+ edges: registration.edges,
738
+ build: registration.build,
739
+ metadata: registration.metadata,
740
+ policy: registration.policy,
741
+ exclusive: registration.exclusive,
742
+ template: registration,
743
+ consumer: container.owner
744
+ });
745
+ container.services.add(service);
746
+ publishService(service);
747
+ });
748
+ return service;
749
+ }
750
+ // src/types/keys.ts
751
+ const optionalBrand = Symbol("injeca.optional");
752
+ const fallbackBrand = Symbol("injeca.fallback");
753
+ const conditionBrand = Symbol("injeca.condition");
754
+ // src/primitives/keys.ts
755
+ function assertName(name) {
756
+ if (typeof name !== "string" || name.length === 0 || name.trim() !== name) throw new InvalidArgumentError(`A key name must be a non-empty string without outer whitespace. Got ${JSON.stringify(name)}.`);
757
+ }
758
+ function readKeyOptions(options) {
759
+ if (options === void 0) return false;
760
+ if (typeof options !== "object" || options === null) throw new InvalidArgumentError("The options of defineKey must be an object.");
761
+ let exclusive;
762
+ try {
763
+ exclusive = options.exclusive;
764
+ } catch (error) {
765
+ throw new InvalidArgumentError("Cannot read the options of defineKey.", { cause: error });
766
+ }
767
+ if (exclusive !== void 0 && typeof exclusive !== "boolean") throw new InvalidArgumentError("The exclusive option of defineKey must be a boolean.");
768
+ return exclusive === true;
769
+ }
770
+ /**
771
+ * Creates a typed key without a provider. A consumer can depend on it now. `provide(key, ...)` registers a provider later.
772
+ *
773
+ * With `options.exclusive`, the key can have only one provider in its resolution area. Two or more providers all go to `conflict`.
774
+ */
775
+ function defineKey(name, options) {
776
+ assertName(name);
777
+ return readKeyOptions(options) ? Object.freeze({
778
+ key: name,
779
+ exclusive: true
780
+ }) : Object.freeze({ key: name });
781
+ }
782
+ /** Tells whether a key from `provide` or `defineKey` is exclusive. A name is not. Reading a caller-owned property happens once, inside a narrow `try`. */
783
+ function isExclusiveKey(key) {
784
+ if (typeof key !== "object" || key === null) return false;
785
+ try {
786
+ return key.exclusive === true;
787
+ } catch (error) {
788
+ throw new InvalidArgumentError("Cannot read the exclusive property of the key.", { cause: error });
789
+ }
790
+ }
791
+ function normalizeName(nameOrProvidedKey) {
792
+ if (typeof nameOrProvidedKey === "object" && nameOrProvidedKey !== null && "key" in nameOrProvidedKey) return nameOrProvidedKey.key;
793
+ return nameOrProvidedKey;
794
+ }
795
+ const optionalMarkers = /* @__PURE__ */ new WeakSet();
796
+ const fallbackMarkers = /* @__PURE__ */ new WeakSet();
797
+ const conditionMarkers = /* @__PURE__ */ new WeakSet();
798
+ function isConditionalDependency(value) {
799
+ return typeof value === "object" && value !== null && conditionMarkers.has(value);
800
+ }
801
+ function isFallbackDependency(value) {
802
+ return typeof value === "object" && value !== null && fallbackMarkers.has(value);
803
+ }
804
+ /**
805
+ * Marks a fallback dependency. It resolves to the first provider in the list whose effective status is available.
806
+ *
807
+ * A change of the selection is a change of identity: the dependent gets `dependency-change`.
808
+ * It is required, unless it is wrapped in `optional()`.
809
+ */
810
+ function firstAvailable(...keys) {
811
+ if (keys.length === 0) throw new InvalidArgumentError("firstAvailable() needs at least one key.");
812
+ const dependencies = keys.map((key) => {
813
+ const name = normalizeName(key);
814
+ assertName(name);
815
+ return name;
816
+ });
817
+ if (new Set(dependencies).size !== dependencies.length) throw new InvalidArgumentError(`firstAvailable() got the same key more than once: ${dependencies.join(", ")}.`);
818
+ const marker = Object.freeze({
819
+ [fallbackBrand]: true,
820
+ dependencies: Object.freeze(dependencies)
821
+ });
822
+ fallbackMarkers.add(marker);
823
+ return marker;
824
+ }
825
+ function optionalMarker(dependencies, condition) {
826
+ const marker = Object.freeze({
827
+ [optionalBrand]: true,
828
+ dependencies,
829
+ ...condition ? { condition } : {}
830
+ });
831
+ optionalMarkers.add(marker);
832
+ return marker;
833
+ }
834
+ /**
835
+ * Marks an optional dependency. The dependent gets `{ value, status }`, or `undefined` when no provider runs.
836
+ * It also accepts a `firstAvailable` marker and a `where` marker.
837
+ */
838
+ function optional(dependency) {
839
+ if (isFallbackDependency(dependency)) return optionalMarker(dependency.dependencies, void 0);
840
+ if (isConditionalDependency(dependency)) return optionalMarker(dependency.dependencies, dependency.condition);
841
+ const name = normalizeName(dependency);
842
+ assertName(name);
843
+ return optionalMarker(Object.freeze([name]), void 0);
844
+ }
845
+ function where(dependency, condition) {
846
+ if (typeof condition !== "function") throw new InvalidArgumentError("The condition of where() is not a function.");
847
+ if (isOptionalDependency(dependency)) {
848
+ if (dependency.condition) throw new InvalidArgumentError("The optional dependency already has a condition.");
849
+ return optionalMarker(dependency.dependencies, condition);
850
+ }
851
+ if (isFallbackDependency(dependency) || isConditionalDependency(dependency)) throw new InvalidArgumentError("where() accepts a key, a name, or an optional() marker.");
852
+ const name = normalizeName(dependency);
853
+ assertName(name);
854
+ const marker = Object.freeze({
855
+ [conditionBrand]: true,
856
+ dependencies: Object.freeze([name]),
857
+ condition
858
+ });
859
+ conditionMarkers.add(marker);
860
+ return marker;
861
+ }
862
+ function isOptionalDependency(value) {
863
+ return typeof value === "object" && value !== null && optionalMarkers.has(value);
864
+ }
865
+ // src/graph/nodes/edge.ts
866
+ function edgeOf(name, candidates, optional, fallback, condition) {
867
+ return Object.freeze({
868
+ name,
869
+ keyName: candidates.join(" | "),
870
+ candidates,
871
+ optional,
872
+ fallback,
873
+ ...condition ? { condition } : {}
874
+ });
875
+ }
876
+ /**
877
+ * Reads a `dependsOn` declaration into edges. Reading a caller-owned property happens once, inside a narrow `try`.
878
+ */
879
+ function readEdges(owner, dependsOn) {
880
+ if (dependsOn === void 0 || dependsOn === null) return Object.freeze([]);
881
+ if (typeof dependsOn !== "object") throw new InvalidArgumentError(`The dependsOn option of '${owner}' is not an object.`);
882
+ let entries;
883
+ try {
884
+ entries = Object.entries(dependsOn);
885
+ } catch (error) {
886
+ throw new InvalidArgumentError(`Cannot read the dependsOn option of '${owner}'.`, { cause: error });
887
+ }
888
+ return Object.freeze(entries.map(([name, dependency]) => {
889
+ if (isOptionalDependency(dependency)) return edgeOf(name, dependency.dependencies, true, dependency.dependencies.length > 1, dependency.condition);
890
+ if (isConditionalDependency(dependency)) return edgeOf(name, dependency.dependencies, false, false, dependency.condition);
891
+ if (isFallbackDependency(dependency)) return edgeOf(name, dependency.dependencies, false, true);
892
+ if (typeof dependency === "string" || typeof dependency === "object" && dependency !== null && "key" in dependency) {
893
+ const keyName = normalizeName(dependency);
894
+ if (typeof keyName === "string" && keyName.length > 0) return edgeOf(name, Object.freeze([keyName]), false, false);
895
+ }
896
+ throw new InvalidArgumentError(`The dependency '${name}' of '${owner}' is not a name, a key, an optional marker, a fallback marker, or a condition marker.`);
897
+ }));
898
+ }
899
+ // src/graph/nodes/policy.ts
900
+ const emptyPolicy = Object.freeze({});
901
+ function readMaxAttempts(owner, value) {
902
+ if (value !== void 0 && (typeof value !== "number" || !Number.isInteger(value) || value < 1)) throw new InvalidArgumentError(`The maxAttempts option of ${owner} must be an integer of 1 or more. Got ${String(value)}.`);
903
+ return value;
904
+ }
905
+ function readTimeout$1(owner, hook, value) {
906
+ if (value !== void 0 && (typeof value !== "number" || !Number.isFinite(value) || value < 0)) throw new InvalidArgumentError(`The timeouts.${hook} option of ${owner} must be a finite number of 0 or more. Got ${String(value)}.`);
907
+ return value;
908
+ }
909
+ function readTimeouts(owner, value) {
910
+ if (value === void 0) return void 0;
911
+ if (typeof value !== "object" || value === null) throw new InvalidArgumentError(`The timeouts option of ${owner} must be an object.`);
912
+ let fields;
913
+ try {
914
+ const source = value;
915
+ fields = {
916
+ start: source.start,
917
+ stop: source.stop
918
+ };
919
+ } catch (error) {
920
+ throw new InvalidArgumentError(`Cannot read the timeouts option of ${owner}.`, { cause: error });
921
+ }
922
+ const start = readTimeout$1(owner, "start", fields.start);
923
+ const stop = readTimeout$1(owner, "stop", fields.stop);
924
+ return Object.freeze({
925
+ ...start === void 0 ? {} : { start },
926
+ ...stop === void 0 ? {} : { stop }
927
+ });
928
+ }
929
+ /**
930
+ * Checks the retry and timeout options that a caller gave. `owner` names the caller in the messages, for example `'voice.speech'`.
931
+ * A field that is `undefined` takes the value of `inherit`, so that a replacement keeps it.
932
+ */
933
+ function readPolicy(owner, fields, inherit) {
934
+ const maxAttempts = readMaxAttempts(owner, fields.maxAttempts) ?? inherit?.maxAttempts;
935
+ const timeouts = readTimeouts(owner, fields.timeouts) ?? inherit?.timeouts;
936
+ if (fields.onTimeout !== void 0 && typeof fields.onTimeout !== "function") throw new InvalidArgumentError(`The onTimeout option of ${owner} must be a function.`);
937
+ const onTimeout = fields.onTimeout ?? inherit?.onTimeout;
938
+ if (maxAttempts === void 0 && timeouts === void 0 && onTimeout === void 0) return emptyPolicy;
939
+ return Object.freeze({
940
+ ...maxAttempts === void 0 ? {} : { maxAttempts },
941
+ ...timeouts === void 0 ? {} : { timeouts },
942
+ ...onTimeout === void 0 ? {} : { onTimeout }
943
+ });
944
+ }
945
+ /** The value of the nearest container, from `container` up to the root, that sets it. */
946
+ function fromContainers(container, pick) {
947
+ for (let current = container; current; current = current.parent) {
948
+ const value = pick(current.policy);
949
+ if (value !== void 0) return value;
950
+ }
951
+ }
952
+ /** The attempt limit of a requeue: the decision, then the service, then the containers. `undefined` means no limit. */
953
+ function maxAttemptsOf(service, fromDecision) {
954
+ return fromDecision ?? service.policy.maxAttempts ?? fromContainers(service.container, (policy) => policy.maxAttempts);
955
+ }
956
+ /** The timeout of a hook: the service, then the containers. `undefined` means no timeout. */
957
+ function timeoutOf(service, hook) {
958
+ return service.policy.timeouts?.[hook] ?? fromContainers(service.container, (policy) => policy.timeouts?.[hook]);
959
+ }
960
+ /** The `onTimeout` handler: the service, then the containers. */
961
+ function timeoutHandlerOf(service) {
962
+ return service.policy.onTimeout ?? fromContainers(service.container, (policy) => policy.onTimeout);
963
+ }
964
+ // src/graph/nodes/definition.ts
965
+ const emptyMetadata$1 = Object.freeze({});
966
+ function readMetadata(name, metadata, fallback) {
967
+ if (metadata === void 0) return fallback ?? emptyMetadata$1;
968
+ if (typeof metadata !== "object" || metadata === null || Array.isArray(metadata)) throw new InvalidArgumentError(`The metadata option of '${name}' must be a record.`);
969
+ try {
970
+ return Object.freeze({ ...metadata });
971
+ } catch (error) {
972
+ throw new InvalidArgumentError(`Cannot read the metadata option of '${name}'.`, { cause: error });
973
+ }
974
+ }
975
+ /**
976
+ * Reads the options of one registration once: the function shorthand, or the object form.
977
+ * Reading caller-owned properties happens inside a narrow `try`. `inherit` gives the metadata and each policy field when the options have none.
978
+ */
979
+ function readDefinition(name, option, inherit) {
980
+ let build;
981
+ let dependsOn;
982
+ let metadata;
983
+ let perConsumer;
984
+ let policy;
985
+ try {
986
+ const object = typeof option === "function" ? { build: option } : typeof option === "object" && option !== null ? option : {};
987
+ build = object.build;
988
+ dependsOn = object.dependsOn;
989
+ metadata = object.metadata;
990
+ perConsumer = object.perConsumer;
991
+ policy = {
992
+ maxAttempts: object.maxAttempts,
993
+ timeouts: object.timeouts,
994
+ onTimeout: object.onTimeout
995
+ };
996
+ } catch (error) {
997
+ throw new InvalidArgumentError(`Cannot read the provide options of '${name}'.`, { cause: error });
998
+ }
999
+ if (typeof build !== "function") throw new InvalidArgumentError(`The build option of '${name}' is not a function.`);
1000
+ if (perConsumer !== void 0 && typeof perConsumer !== "boolean") throw new InvalidArgumentError(`The perConsumer option of '${name}' must be a boolean.`);
1001
+ return {
1002
+ edges: readEdges(name, dependsOn),
1003
+ build,
1004
+ metadata: readMetadata(name, metadata, inherit?.metadata),
1005
+ perConsumer: perConsumer === true,
1006
+ policy: readPolicy(`'${name}'`, policy, inherit?.policy)
1007
+ };
1008
+ }
1009
+ // src/graph/nodes/registration.ts
1010
+ function createRegistration(container, name, exclusive) {
1011
+ return {
1012
+ container,
1013
+ name,
1014
+ exclusive,
1015
+ current: void 0,
1016
+ pending: void 0
1017
+ };
1018
+ }
1019
+ /**
1020
+ * The published service of a registration that is still current, or `undefined` for a stale registration.
1021
+ * A registration is stale after its own withdrawal, or after a newer `provide` of the same key in the same container.
1022
+ */
1023
+ function currentService(registration) {
1024
+ const service = registration.current;
1025
+ return service?.signals.present() ? service : void 0;
1026
+ }
1027
+ /** Cancels the replacement that waits for its swap. The old service does not change. */
1028
+ function cancelPending(registration) {
1029
+ const pending = registration.pending;
1030
+ if (!pending) return;
1031
+ registration.pending = void 0;
1032
+ pending.swap?.settle({ kind: "cancelled" });
1033
+ releaseService(pending);
1034
+ }
1035
+ /** The `withdraw()` of a registration. Resolves `false` for a stale registration. Rejects with the teardown errors. */
1036
+ async function withdrawRegistration(registration) {
1037
+ const service = untracked(() => {
1038
+ const current = currentService(registration);
1039
+ if (current) batch(() => {
1040
+ cancelPending(registration);
1041
+ releaseService(current);
1042
+ });
1043
+ return current;
1044
+ });
1045
+ if (!service) return false;
1046
+ await awaitWithdrawal(service);
1047
+ return true;
1048
+ }
1049
+ // src/helpers/cleanup-context.ts
1050
+ function activeIn(frame, owner) {
1051
+ for (let current = frame; current; current = current.parent) if (current.active && current.owner === owner) return true;
1052
+ return false;
1053
+ }
1054
+ function createSynchronousContext() {
1055
+ let currentFrame;
1056
+ return {
1057
+ current: () => currentFrame,
1058
+ run: (frame, fn) => {
1059
+ const previous = currentFrame;
1060
+ currentFrame = frame;
1061
+ try {
1062
+ return fn();
1063
+ } finally {
1064
+ currentFrame = previous;
1065
+ }
1066
+ },
1067
+ isActive: (owner) => activeIn(currentFrame, owner)
1068
+ };
1069
+ }
1070
+ function createCleanupContext() {
1071
+ return createSynchronousContext();
1072
+ }
1073
+ // src/helpers/clock.ts
1074
+ const systemClock = Object.freeze({
1075
+ now: () => Date.now(),
1076
+ setTimeout: (callback, ms) => {
1077
+ const handle = globalThis.setTimeout(callback, ms);
1078
+ return () => globalThis.clearTimeout(handle);
1079
+ }
1080
+ });
1081
+ // src/helpers/delivery-queue.ts
1082
+ function createDeliveryQueue() {
1083
+ const queue = [];
1084
+ let draining = false;
1085
+ function deliver(deliveries) {
1086
+ queue.push(...deliveries);
1087
+ if (draining) return;
1088
+ draining = true;
1089
+ try {
1090
+ while (queue.length > 0) {
1091
+ const { run, onError } = queue.shift();
1092
+ untracked(() => {
1093
+ try {
1094
+ run();
1095
+ } catch (error) {
1096
+ onError(error);
1097
+ }
1098
+ });
1099
+ }
1100
+ } finally {
1101
+ draining = false;
1102
+ }
1103
+ }
1104
+ return { deliver };
1105
+ }
1106
+ // src/reconciler/build-context.ts
1107
+ const serviceHooks = /* @__PURE__ */ new Set([
1108
+ "start",
1109
+ "stop",
1110
+ "dependency-change",
1111
+ "dependency-status-change"
1112
+ ]);
1113
+ const statuses = /* @__PURE__ */ new Set([
1114
+ "ready",
1115
+ "degraded",
1116
+ "unavailable"
1117
+ ]);
1118
+ /** A cause chain longer than this is cut. It protects against a cyclic cause object. */
1119
+ const maxCauseDepth = 32;
1120
+ function readOwner$1(value) {
1121
+ try {
1122
+ const copy = structuredClone(value);
1123
+ if (typeof copy === "object" && copy !== null && !Array.isArray(copy)) return Object.freeze(copy);
1124
+ } catch (error) {
1125
+ throw new InvalidArgumentError("The owner of a status cause must be a serializable record.", { cause: error });
1126
+ }
1127
+ throw new InvalidArgumentError("The owner of a status cause must be a serializable record.");
1128
+ }
1129
+ function readStatusValue(value, what) {
1130
+ if (typeof value !== "string" || !statuses.has(value)) throw new InvalidArgumentError(`${what} must be 'ready', 'degraded', or 'unavailable'. Got ${String(value)}.`);
1131
+ return value;
1132
+ }
1133
+ function readReason(value, what) {
1134
+ if (value !== void 0 && typeof value !== "string") throw new InvalidArgumentError(`${what} must be a string.`);
1135
+ return value;
1136
+ }
1137
+ /** Reads a caller-owned cause chain once, into frozen records. */
1138
+ function readCause(value, depth = 0) {
1139
+ if (value === void 0 || depth >= maxCauseDepth) return void 0;
1140
+ if (typeof value !== "object" || value === null) throw new InvalidArgumentError("A status cause must be an object with key, owner, and status.");
1141
+ let fields;
1142
+ try {
1143
+ const source = value;
1144
+ fields = {
1145
+ key: source.key,
1146
+ owner: source.owner,
1147
+ status: source.status,
1148
+ reason: source.reason,
1149
+ cause: source.cause
1150
+ };
1151
+ } catch (error) {
1152
+ throw new InvalidArgumentError("Cannot read the status cause.", { cause: error });
1153
+ }
1154
+ if (typeof fields.key !== "string" || fields.key.length === 0) throw new InvalidArgumentError("The key of a status cause must be a non-empty string.");
1155
+ const reason = readReason(fields.reason, "The reason of a status cause");
1156
+ const cause = readCause(fields.cause, depth + 1);
1157
+ return Object.freeze({
1158
+ key: fields.key,
1159
+ owner: readOwner$1(fields.owner ?? {}),
1160
+ status: readStatusValue(fields.status, "The status of a status cause"),
1161
+ ...withoutUndefined({
1162
+ reason,
1163
+ cause
1164
+ })
1165
+ });
1166
+ }
1167
+ /** Reads the arguments of `setStatus` once. */
1168
+ function readOwnStatus(service, status, reasonOrOptions) {
1169
+ const checked = readStatusValue(status, `The status of '${service.name}'`);
1170
+ if (reasonOrOptions === void 0 || typeof reasonOrOptions === "string") return Object.freeze({
1171
+ status: checked,
1172
+ ...withoutUndefined({ reason: reasonOrOptions })
1173
+ });
1174
+ if (typeof reasonOrOptions !== "object" || reasonOrOptions === null) throw new InvalidArgumentError(`The second argument of setStatus for '${service.name}' must be a string or an object.`);
1175
+ let fields;
1176
+ try {
1177
+ const options = reasonOrOptions;
1178
+ fields = {
1179
+ reason: options.reason,
1180
+ cause: options.cause
1181
+ };
1182
+ } catch (error) {
1183
+ throw new InvalidArgumentError(`Cannot read the setStatus options of '${service.name}'.`, { cause: error });
1184
+ }
1185
+ const reason = readReason(fields.reason, `The setStatus reason of '${service.name}'`);
1186
+ const cause = readCause(fields.cause);
1187
+ return Object.freeze({
1188
+ status: checked,
1189
+ ...withoutUndefined({
1190
+ reason,
1191
+ cause
1192
+ })
1193
+ });
1194
+ }
1195
+ /** Creates the context that `build` gets. `on` is valid only while the build runs. `setStatus` stays valid after it. */
1196
+ function createBuildContext(service, instance) {
1197
+ const dependsOn = {};
1198
+ for (const edge of service.edges) {
1199
+ const binding = instance.bindings.get(edge.name);
1200
+ const state = instance.handled.get(edge.name);
1201
+ dependsOn[edge.name] = edge.optional ? binding && state ? Object.freeze({
1202
+ value: binding.value,
1203
+ status: state.status
1204
+ }) : void 0 : binding?.value;
1205
+ }
1206
+ return Object.freeze({
1207
+ container: service.container.api,
1208
+ name: service.name,
1209
+ instanceId: instance.id,
1210
+ ...withoutUndefined({ consumer: service.consumer }),
1211
+ dependsOn: Object.freeze(dependsOn),
1212
+ on: (hook, handler) => {
1213
+ if (!instance.building) throw new HookOutsideBuildError(`'on' was called for '${service.name}' after its build settled.`);
1214
+ if (typeof hook !== "string" || !serviceHooks.has(hook)) throw new InvalidArgumentError(`'${String(hook)}' is not a service hook.`);
1215
+ if (typeof handler !== "function") throw new InvalidArgumentError(`The '${hook}' handler of '${service.name}' is not a function.`);
1216
+ instance.hooks[hook].push(handler);
1217
+ },
1218
+ effect: (setup) => {
1219
+ if (instance.disposed) throw new BuildAfterDisposeError(`'effect' was called for '${service.name}' after its instance was disposed.`);
1220
+ if (typeof setup !== "function") throw new InvalidArgumentError(`The effect setup of '${service.name}' is not a function.`);
1221
+ const disposer = untracked(setup);
1222
+ if (typeof disposer === "function") instance.effects.push(disposer);
1223
+ },
1224
+ signal: instance.controller.signal,
1225
+ setStatus: (status, reasonOrOptions) => untracked(() => {
1226
+ const next = readOwnStatus(service, status, reasonOrOptions);
1227
+ if (instance.disposed) {
1228
+ emitRecord(service.container, {
1229
+ type: "ignored",
1230
+ call: "setStatus",
1231
+ key: service.name,
1232
+ owner: service.container.owner,
1233
+ instanceId: instance.id,
1234
+ reason: `setStatus('${next.status}') was called after the instance was disposed.`
1235
+ });
1236
+ return;
1237
+ }
1238
+ if (!sameStatus(instance.signals.own(), next)) instance.signals.own(next);
1239
+ })
1240
+ });
1241
+ }
1242
+ // src/reconciler/calls.ts
1243
+ /** Calls a user callback with tracking paused. A synchronous throw becomes a rejection. */
1244
+ function invokeUser(fn) {
1245
+ try {
1246
+ return Promise.resolve(untracked(fn));
1247
+ } catch (error) {
1248
+ return Promise.reject(error);
1249
+ }
1250
+ }
1251
+ /** Only services with a key write to the logger. Invocations and resolvers do not. */
1252
+ function loggerOf(service) {
1253
+ return service.kind === "service" ? service.container.api.logger : void 0;
1254
+ }
1255
+ function callListeners(listeners, ...args) {
1256
+ for (const listener of [...listeners]) try {
1257
+ listener(...args);
1258
+ } catch (error) {
1259
+ queueMicrotask(() => {
1260
+ throw error;
1261
+ });
1262
+ }
1263
+ }
1264
+ // src/reconciler/holds.ts
1265
+ function addHolder(provider, holder) {
1266
+ const holders = provider.signals.holders();
1267
+ if (!holders.has(holder)) provider.signals.holders(/* @__PURE__ */ new Set([...holders, holder]));
1268
+ }
1269
+ function removeHolder(provider, holder) {
1270
+ const holders = provider.signals.holders();
1271
+ if (holders.has(holder)) provider.signals.holders(new Set([...holders].filter((service) => service !== holder)));
1272
+ }
1273
+ /** Registers `service` as a holder of each bound instance and of each extra hold, and releases the rest. */
1274
+ function syncHolds(service, instance) {
1275
+ const wanted = /* @__PURE__ */ new Set([...instance.bindings.values(), ...instance.extraHolds]);
1276
+ for (const provider of [...instance.held]) if (!wanted.has(provider)) {
1277
+ removeHolder(provider, service);
1278
+ instance.held.delete(provider);
1279
+ }
1280
+ for (const provider of wanted) if (!instance.held.has(provider)) {
1281
+ addHolder(provider, service);
1282
+ instance.held.add(provider);
1283
+ }
1284
+ }
1285
+ function releaseExtraHolds(service, instance) {
1286
+ instance.extraHolds.clear();
1287
+ batch(() => syncHolds(service, instance));
1288
+ }
1289
+ // src/reconciler/snapshot.ts
1290
+ /** The current snapshot of all dependencies. */
1291
+ function currentSnapshot(service) {
1292
+ const snapshot = /* @__PURE__ */ new Map();
1293
+ for (const edge of service.edges) {
1294
+ const resolution = resolutionOf(service, edge);
1295
+ snapshot.set(edge.name, resolution.kind === "resolved" ? {
1296
+ state: Object.freeze({
1297
+ provider: resolution.provider.name,
1298
+ instanceId: resolution.instance.id,
1299
+ status: resolution.provider.signals.effective()?.status ?? "unavailable"
1300
+ }),
1301
+ instance: resolution.instance
1302
+ } : void 0);
1303
+ }
1304
+ return snapshot;
1305
+ }
1306
+ /** The difference between the last handled snapshot and `snapshot`. An identity change wins over a status change. */
1307
+ function diffSnapshot(service, handled, snapshot) {
1308
+ const changes = [];
1309
+ for (const edge of service.edges) {
1310
+ const from = handled.get(edge.name);
1311
+ const to = snapshot.get(edge.name)?.state;
1312
+ if (from?.instanceId !== to?.instanceId || from && to && from.status !== to.status) changes.push(Object.freeze({
1313
+ key: edge.name,
1314
+ from,
1315
+ to
1316
+ }));
1317
+ }
1318
+ return changes;
1319
+ }
1320
+ function isIdentityChange(change) {
1321
+ return change.from?.instanceId !== change.to?.instanceId;
1322
+ }
1323
+ function describeChanges(changes) {
1324
+ return changes.map((change) => `${change.key}: ${change.from ? `${change.from.instanceId}/${change.from.status}` : "-"} -> ${change.to ? `${change.to.instanceId}/${change.to.status}` : "-"}`).join(", ");
1325
+ }
1326
+ function hookDependsOn(service, snapshot) {
1327
+ const dependsOn = {};
1328
+ for (const edge of service.edges) {
1329
+ const entry = snapshot.get(edge.name);
1330
+ dependsOn[edge.name] = entry ? Object.freeze({
1331
+ ...entry.state,
1332
+ value: entry.instance.value
1333
+ }) : void 0;
1334
+ }
1335
+ return Object.freeze(dependsOn);
1336
+ }
1337
+ /** Marks `snapshot` as handled and moves the bindings to its instances. */
1338
+ function acceptSnapshot(service, instance, snapshot) {
1339
+ instance.extraHolds.clear();
1340
+ instance.handled = new Map([...snapshot].map(([name, entry]) => [name, entry?.state]));
1341
+ instance.bindings.clear();
1342
+ for (const [name, entry] of snapshot) if (entry) instance.bindings.set(name, entry.instance);
1343
+ batch(() => syncHolds(service, instance));
1344
+ }
1345
+ // src/helpers/timeout.ts
1346
+ /**
1347
+ * Waits for `promise` until `ms` milliseconds of `clock` pass.
1348
+ *
1349
+ * A rejection of `promise` before the timeout rejects the result. After the timeout, nothing waits for `promise`, and its rejection is not reported.
1350
+ */
1351
+ function raceTimeout(clock, promise, ms) {
1352
+ return new Promise((resolve, reject) => {
1353
+ let done = false;
1354
+ const cancel = clock.setTimeout(() => untracked(() => {
1355
+ if (done) return;
1356
+ done = true;
1357
+ promise.catch(() => {});
1358
+ resolve({ kind: "timeout" });
1359
+ }), ms);
1360
+ promise.then((value) => {
1361
+ if (done) return;
1362
+ done = true;
1363
+ cancel();
1364
+ resolve({
1365
+ kind: "settled",
1366
+ value
1367
+ });
1368
+ }, (error) => {
1369
+ if (done) return;
1370
+ done = true;
1371
+ cancel();
1372
+ reject(error);
1373
+ });
1374
+ });
1375
+ }
1376
+ // src/reconciler/hook-run.ts
1377
+ function hookStateOf(instance, hook) {
1378
+ let state = instance.hookStates.get(hook);
1379
+ if (!state) {
1380
+ state = {
1381
+ attempt: 1,
1382
+ previous: void 0
1383
+ };
1384
+ instance.hookStates.set(hook, state);
1385
+ }
1386
+ return state;
1387
+ }
1388
+ /** A requeue keeps the attempt count. Any other decision resets it. */
1389
+ function finishRun(instance, hook, decision) {
1390
+ const state = hookStateOf(instance, hook);
1391
+ state.previous = decision;
1392
+ state.attempt = decision.kind === "requeue" ? state.attempt + 1 : 1;
1393
+ }
1394
+ function recordDecision(service, instance, decision, ignored) {
1395
+ emitRecord(service.container, {
1396
+ type: "decision",
1397
+ key: service.name,
1398
+ owner: service.container.owner,
1399
+ instanceId: instance.id,
1400
+ hook: decision.hook,
1401
+ decision: decision.kind,
1402
+ attempt: decision.attempt,
1403
+ ignored,
1404
+ ...withoutUndefined({ reason: decision.reason })
1405
+ });
1406
+ }
1407
+ /** Starts a hook run. Its signal aborts when a newer target supersedes it, when it times out, or when the instance is disposed. */
1408
+ function beginHookRun(instance) {
1409
+ const controller = new AbortController();
1410
+ const parent = instance.controller.signal;
1411
+ const forward = () => controller.abort(parent.reason);
1412
+ if (parent.aborted) forward();
1413
+ else parent.addEventListener("abort", forward, { once: true });
1414
+ instance.hookRun = controller;
1415
+ return {
1416
+ signal: controller.signal,
1417
+ abort: (reason) => controller.abort(reason),
1418
+ end: () => {
1419
+ parent.removeEventListener("abort", forward);
1420
+ if (instance.hookRun === controller) instance.hookRun = void 0;
1421
+ }
1422
+ };
1423
+ }
1424
+ /** 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`. */
1425
+ async function callHandler(service, instance, hook, handler, runSignal, extras, call = invokeUser) {
1426
+ const state = hookStateOf(instance, hook);
1427
+ const run = {
1428
+ service: service.name,
1429
+ instanceId: instance.id,
1430
+ hook,
1431
+ attempt: state.attempt,
1432
+ now: () => service.container.runtime.clock.now()
1433
+ };
1434
+ const hookContext = Object.freeze({
1435
+ decide: createDecide(run),
1436
+ attempt: run.attempt,
1437
+ previous: state.previous,
1438
+ signal: runSignal,
1439
+ ...extras
1440
+ });
1441
+ try {
1442
+ const value = await call(() => handler(hookContext));
1443
+ return validateDecision(run, value) ?? defaultDecision(run);
1444
+ } catch (error) {
1445
+ return createDecide(run).fail(error);
1446
+ }
1447
+ }
1448
+ /** Runs the handlers of `hook` in order. It stops at the first decision that is not `handled`, unless `all` is set. */
1449
+ async function runHandlers(service, instance, hook, runSignal, extras, options = {}) {
1450
+ const decisions = [];
1451
+ const handlers = [...instance.hooks[hook]];
1452
+ for (let index = options.from ?? 0; index < handlers.length; index++) {
1453
+ if (options.stopped?.()) break;
1454
+ if (options.progress) options.progress.index = index;
1455
+ const decision = await callHandler(service, instance, hook, handlers[index], runSignal, extras);
1456
+ decisions.push(decision);
1457
+ if (!options.all && decision.kind !== "handled") break;
1458
+ }
1459
+ return decisions;
1460
+ }
1461
+ /**
1462
+ * Records a timeout of a `start` or `stop` hook in the observe stream, and returns the decision of the `onTimeout` handler.
1463
+ *
1464
+ * The handler runs synchronously. A value that is not a decision of this hook, a promise, a throw, or no handler gives `fail` with `hook-timeout`.
1465
+ */
1466
+ function decideTimeout(service, instance, hook, timeoutMs) {
1467
+ const state = hookStateOf(instance, hook);
1468
+ const run = {
1469
+ service: service.name,
1470
+ instanceId: instance.id,
1471
+ hook,
1472
+ attempt: state.attempt,
1473
+ now: () => service.container.runtime.clock.now()
1474
+ };
1475
+ const decide = createDecide(run);
1476
+ const error = new HookTimeoutError(`The '${hook}' hook of '${service.name}' did not settle within ${timeoutMs}ms.`, hook, timeoutMs);
1477
+ emitRecord(service.container, {
1478
+ type: "timeout",
1479
+ key: service.name,
1480
+ owner: service.container.owner,
1481
+ instanceId: instance.id,
1482
+ hook,
1483
+ timeoutMs,
1484
+ attempt: state.attempt,
1485
+ reason: error.message
1486
+ });
1487
+ const handler = timeoutHandlerOf(service);
1488
+ if (!handler) return decide.fail(error, { reason: error.message });
1489
+ try {
1490
+ const value = untracked(() => handler(Object.freeze({
1491
+ hook,
1492
+ timeoutMs,
1493
+ attempt: state.attempt,
1494
+ previous: state.previous,
1495
+ decide
1496
+ })));
1497
+ return validateDecision(run, value) ?? decide.fail(error, { reason: error.message });
1498
+ } catch (handlerError) {
1499
+ return decide.fail(handlerError, { reason: `The onTimeout handler of '${service.name}' threw.` });
1500
+ }
1501
+ }
1502
+ // src/reconciler/requeue.ts
1503
+ /**
1504
+ * The delay of a requeue in milliseconds.
1505
+ *
1506
+ * `after` is a fixed delay. An exponential backoff doubles `initial` for each attempt, up to `max`.
1507
+ * `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`.
1508
+ */
1509
+ function requeueDelay(decision, random) {
1510
+ if (decision.after !== void 0) return Math.max(0, decision.after);
1511
+ if (decision.backoff) {
1512
+ const { initial, max, jitter } = decision.backoff;
1513
+ const delay = Math.max(0, Math.min(max, initial * 2 ** (decision.attempt - 1)));
1514
+ if (jitter === void 0) return delay;
1515
+ return delay * (1 - Math.min(1, Math.max(0, jitter)) * random());
1516
+ }
1517
+ return 0;
1518
+ }
1519
+ /**
1520
+ * The stop intent of a requeue that reached its attempt limit. The service stops, then goes to `failed` with the last reason.
1521
+ * The limit comes from the decision, then the service, then the containers. It is `undefined` when the requeue can wait.
1522
+ */
1523
+ function exhaustedIntent(service, decision) {
1524
+ const maxAttempts = maxAttemptsOf(service, decision.maxAttempts);
1525
+ if (maxAttempts === void 0 || decision.attempt < maxAttempts) return void 0;
1526
+ const message = decision.reason ?? `The '${decision.hook}' hook of '${service.name}' reached maxAttempts (${maxAttempts}).`;
1527
+ return {
1528
+ failure: { error: new Error(message, { cause: decision }) },
1529
+ reason: message,
1530
+ decision: "requeue"
1531
+ };
1532
+ }
1533
+ /** Waits before the next run of a requeued hook. When the wait ends, the service goes back to the work queue. */
1534
+ function scheduleRequeue(service, instance, decision) {
1535
+ const { clock, random, reconciler } = service.container.runtime;
1536
+ const wait = { cancel: () => {} };
1537
+ wait.cancel = clock.setTimeout(() => {
1538
+ if (instance.requeueWait !== wait) return;
1539
+ instance.requeueWait = void 0;
1540
+ reconciler.markDirty(service);
1541
+ }, requeueDelay(decision, random));
1542
+ instance.requeueWait = wait;
1543
+ }
1544
+ function cancelRequeueWait(instance) {
1545
+ instance.requeueWait?.cancel();
1546
+ instance.requeueWait = void 0;
1547
+ }
1548
+ // src/reconciler/stop.ts
1549
+ function mergeIntent(current, next) {
1550
+ if (current.failure) return current;
1551
+ if (next.failure) return next;
1552
+ return {
1553
+ parkEpoch: next.parkEpoch ?? current.parkEpoch,
1554
+ reason: next.reason ?? current.reason,
1555
+ decision: next.decision ?? current.decision
1556
+ };
1557
+ }
1558
+ function enterStopping(service, intent) {
1559
+ service.stopIntent = mergeIntent(service.stopIntent, intent);
1560
+ setPhase(service, "stopping", {
1561
+ reason: service.stopIntent.reason,
1562
+ decision: service.stopIntent.decision
1563
+ });
1564
+ service.container.runtime.reconciler.markDirty(service);
1565
+ }
1566
+ function reconcileStopping(service) {
1567
+ const instance = service.signals.instance();
1568
+ if (!instance) {
1569
+ settleAfterStop(service, []);
1570
+ return;
1571
+ }
1572
+ if (instance.signals.holders().size > 0) return;
1573
+ teardown(service, instance, true);
1574
+ }
1575
+ /**
1576
+ * Runs the `stop` hooks in reverse order, then the effect disposers in reverse order.
1577
+ *
1578
+ * A configured `stop` timeout covers all of them. When it passes, injeca records it, applies the `onTimeout` decision, and stops waiting.
1579
+ * The callback in progress is abandoned. The remaining callbacks still run, but injeca does not wait for them. So a cleanup that awaits the disposal of its own container cannot block that disposal.
1580
+ */
1581
+ async function runTeardownCallbacks(service, instance, stopHooks, frame) {
1582
+ const errors = [];
1583
+ const { clock, context } = service.container.runtime;
1584
+ const call = (fn) => context.run(frame, () => invokeUser(fn));
1585
+ const timeoutMs = timeoutOf(service, "stop");
1586
+ const deadline = timeoutMs === void 0 ? void 0 : clock.now() + timeoutMs;
1587
+ let timedOut = false;
1588
+ /** Waits for one callback until the deadline. `undefined` means that the deadline passed. */
1589
+ const settle = async (promise) => {
1590
+ if (deadline === void 0) return { value: await promise };
1591
+ if (timedOut) {
1592
+ promise.catch(() => {});
1593
+ return;
1594
+ }
1595
+ const result = await raceTimeout(clock, promise, Math.max(0, deadline - clock.now()));
1596
+ if (result.kind === "settled") return { value: result.value };
1597
+ timedOut = true;
1598
+ const decision = decideTimeout(service, instance, "stop", timeoutMs);
1599
+ recordDecision(service, instance, decision, false);
1600
+ finishRun(instance, "stop", decision);
1601
+ if (decision.kind === "fail") errors.push(decision.error);
1602
+ };
1603
+ if (stopHooks && instance.hooks.stop.length > 0) {
1604
+ const logger = loggerOf(service);
1605
+ logger?.hookOnStop(service.name);
1606
+ const startTime = performance.now();
1607
+ for (const handler of [...instance.hooks.stop].reverse()) {
1608
+ const settled = await settle(callHandler(service, instance, "stop", handler, instance.controller.signal, {}, call));
1609
+ if (!settled) continue;
1610
+ const decision = settled.value;
1611
+ recordDecision(service, instance, decision, false);
1612
+ finishRun(instance, "stop", decision);
1613
+ if (decision.kind === "fail") errors.push(decision.error);
1614
+ }
1615
+ logger?.hookOnStopComplete(service.name, performance.now() - startTime);
1616
+ }
1617
+ const effects = instance.effects.splice(0).reverse();
1618
+ for (const disposer of effects) try {
1619
+ await settle(call(disposer));
1620
+ } catch (error) {
1621
+ errors.push(error);
1622
+ }
1623
+ return errors;
1624
+ }
1625
+ async function teardown(service, instance, stopHooks, phase) {
1626
+ service.busy = true;
1627
+ cancelRequeueWait(instance);
1628
+ instance.extraHolds.clear();
1629
+ if (phase) setPhase(service, phase, { reason: service.stopIntent.reason });
1630
+ const frame = service.container.frame;
1631
+ const errors = await service.container.runtime.context.run(frame, () => runTeardownCallbacks(service, instance, stopHooks, frame));
1632
+ instance.disposed = true;
1633
+ instance.controller.abort(/* @__PURE__ */ new Error(`The instance '${instance.id}' of '${service.name}' was disposed.`));
1634
+ service.busy = false;
1635
+ service.container.runtime.reconciler.notifyTeardown(service, errors);
1636
+ batch(() => {
1637
+ instance.bindings.clear();
1638
+ syncHolds(service, instance);
1639
+ settleAfterStop(service, errors);
1640
+ service.signals.instance(void 0);
1641
+ });
1642
+ }
1643
+ function settleAfterStop(service, errors) {
1644
+ const intent = service.stopIntent;
1645
+ service.stopIntent = {};
1646
+ const { swap } = service;
1647
+ if (swap) {
1648
+ const failure = intent.failure ?? (errors.length > 0 ? { error: errors[0] } : void 0);
1649
+ service.swap = void 0;
1650
+ if (failure) {
1651
+ service.failure = failure;
1652
+ setPhase(service, "failed", {
1653
+ reason: intent.reason ?? "The replacement failed. The old instance stays.",
1654
+ decision: intent.decision
1655
+ });
1656
+ swap.settle({
1657
+ kind: "failed",
1658
+ error: failure.error
1659
+ });
1660
+ } else swap.settle(service.signals.active() ? { kind: "cancelled" } : { kind: "stopping" });
1661
+ service.signals.present(false);
1662
+ service.teardownErrors.push(...errors);
1663
+ finalize(service);
1664
+ return;
1665
+ }
1666
+ if (!service.signals.present()) {
1667
+ service.teardownErrors.push(...errors);
1668
+ finalize(service);
1669
+ return;
1670
+ }
1671
+ if (intent.failure || errors.length > 0) {
1672
+ service.failure = intent.failure ?? { error: errors[0] };
1673
+ setPhase(service, "failed", {
1674
+ reason: intent.reason,
1675
+ decision: intent.decision
1676
+ });
1677
+ return;
1678
+ }
1679
+ service.parkedEpoch = intent.parkEpoch;
1680
+ setPhase(service, "pending", {
1681
+ reason: intent.reason,
1682
+ decision: intent.decision
1683
+ });
1684
+ service.container.runtime.reconciler.markDirty(service);
1685
+ }
1686
+ function finalize(service) {
1687
+ const { swap, template } = service;
1688
+ service.swap = void 0;
1689
+ swap?.settle({ kind: "cancelled" });
1690
+ service.disposeEffects();
1691
+ service.container.services.delete(service);
1692
+ batch(() => {
1693
+ removeDeclaration(service);
1694
+ if (template) {
1695
+ const consumers = template.signals.consumers();
1696
+ if (consumers.get(service.container) === service) template.signals.consumers(new Map([...consumers].filter(([, consumer]) => consumer !== service)));
1697
+ }
1698
+ setPhase(service, "withdrawn");
1699
+ });
1700
+ service.withdrawal.resolve([...service.teardownErrors]);
1701
+ if (template) service.container.runtime.reconciler.markDirty(template);
1702
+ service.container.runtime.reconciler.requestDrain();
1703
+ }
1704
+ // src/reconciler/swap.ts
1705
+ /**
1706
+ * Cancels a replacement that cannot complete: the old service was withdrawn, or the container stopped.
1707
+ * The new service leaves. The old service does not change.
1708
+ */
1709
+ function checkSwap(service) {
1710
+ const { swap } = service;
1711
+ if (!swap || !service.signals.present()) return;
1712
+ if (!swap.target.signals.present()) {
1713
+ swap.settle({ kind: "cancelled" });
1714
+ service.signals.present(false);
1715
+ } else if (!service.signals.active()) {
1716
+ swap.settle({ kind: "stopping" });
1717
+ service.signals.present(false);
1718
+ }
1719
+ }
1720
+ /**
1721
+ * Swaps a new service that completed `start` in for the old one, in one batch.
1722
+ *
1723
+ * The new service takes the slot and goes to `running`. The old service leaves.
1724
+ * It stops and disposes after each holder releases it. Dependents follow the rule for a replaced dependency.
1725
+ */
1726
+ function completeSwap(service) {
1727
+ const swap = service.swap;
1728
+ if (!swap) return;
1729
+ const { target } = swap;
1730
+ service.swap = void 0;
1731
+ batch(() => {
1732
+ target.replacedBy = service;
1733
+ publishService(service);
1734
+ target.signals.present(false);
1735
+ setPhase(service, "running", { reason: `The new instance replaced '${target.signals.instance()?.id ?? target.name}'.` });
1736
+ });
1737
+ swap.settle({ kind: "swapped" });
1738
+ }
1739
+ // src/reconciler/start.ts
1740
+ /**
1741
+ * Runs the `start` handlers of one attempt, from `instance.startFrom`, within the configured `start` timeout.
1742
+ * At the timeout, the run aborts, later handlers do not run, and the `onTimeout` decision replaces the result. Late decisions are recorded as ignored.
1743
+ */
1744
+ async function runAttempt(service, instance) {
1745
+ const run = beginHookRun(instance);
1746
+ const progress = { index: instance.startFrom };
1747
+ let timedOut = false;
1748
+ const running = runHandlers(service, instance, "start", run.signal, {}, {
1749
+ from: instance.startFrom,
1750
+ progress,
1751
+ stopped: () => timedOut
1752
+ });
1753
+ const timeoutMs = timeoutOf(service, "start");
1754
+ const result = timeoutMs === void 0 ? {
1755
+ kind: "settled",
1756
+ value: await running
1757
+ } : await raceTimeout(service.container.runtime.clock, running, timeoutMs);
1758
+ instance.startFrom = progress.index;
1759
+ if (result.kind === "settled") {
1760
+ run.end();
1761
+ return result.value;
1762
+ }
1763
+ timedOut = true;
1764
+ run.abort(new HookTimeoutError(`The 'start' hook of '${service.name}' timed out.`, "start", timeoutMs));
1765
+ run.end();
1766
+ running.then((late) => {
1767
+ for (const decision of late) recordDecision(service, instance, decision, true);
1768
+ });
1769
+ return [decideTimeout(service, instance, "start", timeoutMs)];
1770
+ }
1771
+ /**
1772
+ * Runs the `start` hooks of a built instance. The service moves to `running`, waits in `retrying` for a requeue, or stops.
1773
+ *
1774
+ * 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.
1775
+ */
1776
+ async function runStart(service, instance) {
1777
+ service.busy = true;
1778
+ const handlers = instance.hooks.start;
1779
+ const logger = handlers.length > 0 ? loggerOf(service) : void 0;
1780
+ if (instance.startFrom === 0) logger?.hookOnStart(service.name);
1781
+ const startTime = performance.now();
1782
+ const decisions = handlers.length > instance.startFrom ? await runAttempt(service, instance) : [];
1783
+ service.busy = false;
1784
+ const ignored = service.signals.targetLost();
1785
+ for (const decision of decisions) recordDecision(service, instance, decision, ignored);
1786
+ if (ignored) {
1787
+ enterStopping(service, { reason: "A newer target superseded the start." });
1788
+ return;
1789
+ }
1790
+ const decision = decisions.at(-1);
1791
+ if (!decision || decision.kind === "handled") {
1792
+ if (decision) finishRun(instance, "start", decision);
1793
+ logger?.hookOnStartComplete(service.name, performance.now() - startTime);
1794
+ if (service.swap) completeSwap(service);
1795
+ else setPhase(service, "running");
1796
+ return;
1797
+ }
1798
+ finishRun(instance, "start", decision);
1799
+ switch (decision.kind) {
1800
+ case "stop":
1801
+ enterStopping(service, {
1802
+ parkEpoch: epochOf(service, (edge) => instance.bindings.get(edge.name)),
1803
+ reason: decision.reason,
1804
+ decision: "stop"
1805
+ });
1806
+ return;
1807
+ case "fail":
1808
+ enterStopping(service, {
1809
+ failure: { error: decision.error },
1810
+ reason: decision.reason,
1811
+ decision: "fail"
1812
+ });
1813
+ return;
1814
+ case "requeue": {
1815
+ const exhausted = exhaustedIntent(service, decision);
1816
+ if (exhausted) {
1817
+ enterStopping(service, exhausted);
1818
+ return;
1819
+ }
1820
+ setPhase(service, "retrying", {
1821
+ reason: decision.reason,
1822
+ decision: "requeue"
1823
+ });
1824
+ scheduleRequeue(service, instance, decision);
1825
+ }
1826
+ }
1827
+ }
1828
+ // src/reconciler/build.ts
1829
+ /**
1830
+ * An active service makes the providers of its edges converge too, before `start()`. A fallback edge demands each candidate.
1831
+ * A `perConsumer` registration gets a service for the container of `service`. The nearest registration of each key is demanded.
1832
+ */
1833
+ function demandProviders(service) {
1834
+ for (const edge of service.edges) for (const keyName of edge.candidates) {
1835
+ const [nearest] = candidatesOf(service.container, keyName, service).map((registration) => registration.perConsumer ? ensureConsumerService(registration, service.container) : registration);
1836
+ if (nearest && !nearest.signals.serving() && !nearest.signals.demanded()) nearest.signals.demanded(true);
1837
+ }
1838
+ }
1839
+ /**
1840
+ * Tells whether each optional edge has a final answer for a `resolve` call: no provider, a running provider, or a failed one.
1841
+ * A service does not wait for optional edges. A `resolve` call does, so that it does not miss a provider that is still starting.
1842
+ */
1843
+ function optionalSettled(service) {
1844
+ return service.edges.every((edge) => {
1845
+ if (!edge.optional) return true;
1846
+ const resolution = resolutionOf(service, edge);
1847
+ return resolution.kind !== "not-running" || resolution.provider.signals.phase() === "failed";
1848
+ });
1849
+ }
1850
+ /** Builds a pending service when it is active and its required dependencies are available. An absent one is withdrawn. */
1851
+ function reconcilePending(service) {
1852
+ if (!service.signals.present()) {
1853
+ finalize(service);
1854
+ return;
1855
+ }
1856
+ if (inConflict(service)) {
1857
+ setPhase(service, "conflict", { reason: `The exclusive key '${service.name}' has more than one declaration.` });
1858
+ return;
1859
+ }
1860
+ if (!service.signals.active()) return;
1861
+ demandProviders(service);
1862
+ if (!requiredAvailable(service) || service.kind === "resolver" && !optionalSettled(service)) return;
1863
+ if (service.parkedEpoch !== void 0) {
1864
+ if (epochOf(service, (edge) => resolvedInstance(service, edge)) === service.parkedEpoch) return;
1865
+ service.parkedEpoch = void 0;
1866
+ }
1867
+ beginBuild(service);
1868
+ }
1869
+ function beginBuild(service) {
1870
+ const instance = prepareInstance(service, currentSnapshot(service));
1871
+ runBuild(service, instance).then((result) => completeBuild(service, instance, result));
1872
+ }
1873
+ /** Creates the instance of a build from a snapshot of its dependencies, and moves the service to `building`. The service holds each bound provider. */
1874
+ function prepareInstance(service, snapshot) {
1875
+ const bindings = /* @__PURE__ */ new Map();
1876
+ const handled = /* @__PURE__ */ new Map();
1877
+ for (const [name, entry] of snapshot) {
1878
+ handled.set(name, entry?.state);
1879
+ if (entry) bindings.set(name, entry.instance);
1880
+ }
1881
+ const instance = {
1882
+ id: service.container.runtime.nextInstanceId(),
1883
+ service,
1884
+ bindings,
1885
+ extraHolds: /* @__PURE__ */ new Set(),
1886
+ held: /* @__PURE__ */ new Set(),
1887
+ hooks: {
1888
+ "start": [],
1889
+ "stop": [],
1890
+ "dependency-change": [],
1891
+ "dependency-status-change": []
1892
+ },
1893
+ hookStates: /* @__PURE__ */ new Map(),
1894
+ handled,
1895
+ effects: [],
1896
+ controller: new AbortController(),
1897
+ signals: {
1898
+ holders: signal(/* @__PURE__ */ new Set()),
1899
+ own: signal(Object.freeze({ status: "ready" }))
1900
+ },
1901
+ hookRun: void 0,
1902
+ requeueWait: void 0,
1903
+ startFrom: 0,
1904
+ value: void 0,
1905
+ building: true,
1906
+ disposed: false
1907
+ };
1908
+ service.busy = true;
1909
+ batch(() => {
1910
+ syncHolds(service, instance);
1911
+ service.signals.instance(instance);
1912
+ setPhase(service, "building");
1913
+ });
1914
+ return instance;
1915
+ }
1916
+ /** Runs `build` for a prepared instance. It never rejects. */
1917
+ function runBuild(service, instance) {
1918
+ const logger = loggerOf(service);
1919
+ logger?.beforeRun(service.name);
1920
+ const startTime = performance.now();
1921
+ const buildContext = createBuildContext(service, instance);
1922
+ return invokeUser(() => service.build(buildContext)).then((value) => {
1923
+ logger?.run(service.name, performance.now() - startTime);
1924
+ return {
1925
+ ok: true,
1926
+ value
1927
+ };
1928
+ }, (error) => ({
1929
+ ok: false,
1930
+ error
1931
+ }));
1932
+ }
1933
+ function completeBuild(service, instance, result) {
1934
+ instance.building = false;
1935
+ service.busy = false;
1936
+ if (!result.ok) {
1937
+ service.stopIntent = mergeIntent(service.stopIntent, {
1938
+ failure: { error: result.error },
1939
+ reason: "The build failed."
1940
+ });
1941
+ teardown(service, instance, false);
1942
+ return;
1943
+ }
1944
+ if (buildLost(service, instance)) {
1945
+ service.stopIntent = mergeIntent(service.stopIntent, { reason: "The target changed during the build. The result is discarded." });
1946
+ teardown(service, instance, false, "stopping");
1947
+ return;
1948
+ }
1949
+ instance.value = result.value;
1950
+ setPhase(service, "starting");
1951
+ runStart(service, instance);
1952
+ }
1953
+ // src/reconciler/commit.ts
1954
+ /**
1955
+ * Orders the members so that each member comes after the members that it depends on.
1956
+ * Members in a cycle come last, in registration order. They cannot build in the transaction.
1957
+ */
1958
+ function orderMembers(members) {
1959
+ const order = [];
1960
+ const placed = /* @__PURE__ */ new Set();
1961
+ const remaining = [...members.values()];
1962
+ let progress = true;
1963
+ while (progress) {
1964
+ progress = false;
1965
+ for (const member of remaining) {
1966
+ if (placed.has(member)) continue;
1967
+ if (member.edges.every((edge) => edge.candidates.every((keyName) => {
1968
+ const provider = members.get(keyName);
1969
+ return !provider || provider === member || placed.has(provider);
1970
+ }))) {
1971
+ placed.add(member);
1972
+ order.push(member);
1973
+ progress = true;
1974
+ }
1975
+ }
1976
+ }
1977
+ return [...order, ...remaining.filter((member) => !placed.has(member))];
1978
+ }
1979
+ function memberEntry(member, built) {
1980
+ const instance = built.get(member);
1981
+ if (!instance) return void 0;
1982
+ return {
1983
+ state: Object.freeze({
1984
+ provider: member.name,
1985
+ instanceId: instance.id,
1986
+ status: instance.signals.own().status
1987
+ }),
1988
+ instance
1989
+ };
1990
+ }
1991
+ /** Resolves one key inside a transaction: a built member first, then the published providers. */
1992
+ function resolveInTransaction(service, keyName, condition, members, built) {
1993
+ const member = members.get(keyName);
1994
+ if (member && member !== service) {
1995
+ if (inConflict(member) || condition && !acceptsProvider(service, keyName, condition, member)) return void 0;
1996
+ return memberEntry(member, built);
1997
+ }
1998
+ const resolution = resolveKey(service, keyName, condition);
1999
+ if (resolution.kind !== "resolved") return void 0;
2000
+ return {
2001
+ state: Object.freeze({
2002
+ provider: resolution.provider.name,
2003
+ instanceId: resolution.instance.id,
2004
+ status: resolution.provider.signals.effective()?.status ?? "unavailable"
2005
+ }),
2006
+ instance: resolution.instance
2007
+ };
2008
+ }
2009
+ /** The snapshot of a member for its build in the transaction. It is `undefined` when a required dependency is not available. */
2010
+ function transactionSnapshot(service, members, built) {
2011
+ const snapshot = /* @__PURE__ */ new Map();
2012
+ for (const edge of service.edges) {
2013
+ const entries = edge.candidates.map((keyName) => resolveInTransaction(service, keyName, edge.condition, members, built));
2014
+ const entry = entries.find((candidate) => candidate !== void 0 && candidate.state.status !== "unavailable") ?? entries.find((candidate) => candidate !== void 0);
2015
+ if (!edge.optional && (!entry || entry.state.status === "unavailable")) return void 0;
2016
+ snapshot.set(edge.name, entry);
2017
+ }
2018
+ return snapshot;
2019
+ }
2020
+ /**
2021
+ * Builds the members of a transaction in `order`. Nothing is published, and no `start` hook runs.
2022
+ *
2023
+ * 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.
2024
+ * The first failed build ends the loop. An abort ends it after the build in progress settles.
2025
+ */
2026
+ async function buildMembers(transaction, order, members) {
2027
+ const built = /* @__PURE__ */ new Map();
2028
+ for (const member of order) {
2029
+ if (transaction.state === "aborted") return {
2030
+ kind: "aborted",
2031
+ built
2032
+ };
2033
+ if (member.perConsumer || !member.signals.active() || inConflict(member)) continue;
2034
+ const snapshot = transactionSnapshot(member, members, built);
2035
+ if (!snapshot) continue;
2036
+ const instance = prepareInstance(member, snapshot);
2037
+ transaction.controllers.add(instance.controller);
2038
+ const result = await runBuild(member, instance);
2039
+ transaction.controllers.delete(instance.controller);
2040
+ instance.building = false;
2041
+ built.set(member, instance);
2042
+ if (!result.ok) return {
2043
+ kind: "failed",
2044
+ error: result.error,
2045
+ built
2046
+ };
2047
+ instance.value = result.value;
2048
+ }
2049
+ return transaction.state === "aborted" ? {
2050
+ kind: "aborted",
2051
+ built
2052
+ } : {
2053
+ kind: "built",
2054
+ built
2055
+ };
2056
+ }
2057
+ /**
2058
+ * 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.
2059
+ * Resolves when each member is `withdrawn`.
2060
+ */
2061
+ async function rollbackMembers(members, built) {
2062
+ for (const member of members.values()) member.signals.present(false);
2063
+ await Promise.all([...members.values()].map((member) => {
2064
+ const instance = built.get(member);
2065
+ if (instance) return teardown(member, instance, false);
2066
+ finalize(member);
2067
+ }));
2068
+ }
2069
+ /**
2070
+ * Runs the `start` hooks of the built members after the publish, in dependency order.
2071
+ * A member whose target changed, or whose dependency did not reach `running`, is discarded without its `stop` hooks. It builds again later.
2072
+ */
2073
+ async function startMembers(order, built) {
2074
+ for (const member of order) {
2075
+ const instance = built.get(member);
2076
+ if (!instance) continue;
2077
+ if (member.signals.targetLost() || requiredChanged(member, instance)) await teardown(member, instance, false, "stopping");
2078
+ else await runStart(member, instance);
2079
+ }
2080
+ }
2081
+ /**
2082
+ * Aborts a transaction. Before the commit, it drops the transaction. During the commit, it aborts the builds in progress, and the commit rolls back.
2083
+ * Resolves when the transaction ended.
2084
+ */
2085
+ function abortTransaction(transaction, error) {
2086
+ if (transaction.state === "done" || transaction.state === "aborted") return transaction.settled ?? Promise.resolve();
2087
+ const committing = transaction.state === "committing";
2088
+ transaction.state = "aborted";
2089
+ transaction.abortError = error;
2090
+ for (const controller of transaction.controllers) controller.abort(error);
2091
+ if (!committing) {
2092
+ transaction.container.transactions.delete(transaction);
2093
+ transaction.settled = Promise.resolve();
2094
+ }
2095
+ return transaction.settled ?? Promise.resolve();
2096
+ }
2097
+ // src/reconciler/conflict.ts
2098
+ /** A service in `conflict` runs no code. It goes back to `pending` when only one declaration remains. */
2099
+ function reconcileConflict(service) {
2100
+ if (!service.signals.present()) {
2101
+ finalize(service);
2102
+ return;
2103
+ }
2104
+ if (inConflict(service)) return;
2105
+ setPhase(service, "pending", { reason: `Only one declaration of the exclusive key '${service.name}' remains.` });
2106
+ service.container.runtime.reconciler.markDirty(service);
2107
+ }
2108
+ // src/reconciler/dependency-hooks.ts
2109
+ /** Handles a running service: it stops it, or it runs a dependency hook for the changes since the last handled snapshot. */
2110
+ function reconcileRunning(service) {
2111
+ const instance = service.signals.instance();
2112
+ if (!instance) {
2113
+ enterStopping(service, {});
2114
+ return;
2115
+ }
2116
+ if (!service.signals.present() || !service.signals.active()) {
2117
+ cancelRequeueWait(instance);
2118
+ enterStopping(service, { reason: service.signals.present() ? "The container stopped." : service.replacedBy ? "The service was replaced." : "The service was withdrawn." });
2119
+ return;
2120
+ }
2121
+ if (inConflict(service)) {
2122
+ cancelRequeueWait(instance);
2123
+ enterStopping(service, { reason: `The exclusive key '${service.name}' got another declaration.` });
2124
+ return;
2125
+ }
2126
+ const snapshot = currentSnapshot(service);
2127
+ if (requiredWithdrawn(service)) {
2128
+ cancelRequeueWait(instance);
2129
+ if (instance.hooks["dependency-change"].length > 0) runNotice(service, instance, snapshot);
2130
+ else enterStopping(service, { reason: "A required dependency was withdrawn." });
2131
+ return;
2132
+ }
2133
+ const changes = diffSnapshot(service, instance.handled, snapshot);
2134
+ if (changes.length === 0 || instance.requeueWait) return;
2135
+ const identity = changes.some(isIdentityChange);
2136
+ const hook = identity ? "dependency-change" : "dependency-status-change";
2137
+ if (instance.hooks[hook].length === 0) {
2138
+ if (identity) enterStopping(service, {
2139
+ reason: `A dependency changed without a handler: ${describeChanges(changes)}.`,
2140
+ decision: "rebuild"
2141
+ });
2142
+ else acceptSnapshot(service, instance, snapshot);
2143
+ return;
2144
+ }
2145
+ runDependencyHook(service, instance, hook, snapshot, changes);
2146
+ }
2147
+ /** Calls `dependency-change` as a notice before the service stops for a withdrawn required dependency. The decisions are ignored. */
2148
+ async function runNotice(service, instance, snapshot) {
2149
+ service.busy = true;
2150
+ const changes = diffSnapshot(service, instance.handled, snapshot);
2151
+ const view = new Map(snapshot);
2152
+ for (const [name, binding] of instance.bindings) if (!view.get(name)) view.set(name, {
2153
+ state: Object.freeze({
2154
+ provider: binding.service.name,
2155
+ instanceId: binding.id,
2156
+ status: "unavailable"
2157
+ }),
2158
+ instance: binding
2159
+ });
2160
+ const decisions = await runHandlers(service, instance, "dependency-change", instance.controller.signal, {
2161
+ dependsOn: hookDependsOn(service, view),
2162
+ changes: Object.freeze(changes)
2163
+ }, { all: true });
2164
+ service.busy = false;
2165
+ for (const decision of decisions) recordDecision(service, instance, decision, true);
2166
+ enterStopping(service, { reason: `A required dependency was withdrawn: ${describeChanges(changes)}.` });
2167
+ }
2168
+ async function runDependencyHook(service, instance, hook, snapshot, changes) {
2169
+ service.busy = true;
2170
+ for (const entry of snapshot.values()) if (entry) instance.extraHolds.add(entry.instance);
2171
+ batch(() => syncHolds(service, instance));
2172
+ const run = beginHookRun(instance);
2173
+ const decisions = await runHandlers(service, instance, hook, run.signal, {
2174
+ dependsOn: hookDependsOn(service, snapshot),
2175
+ changes: Object.freeze([...changes])
2176
+ });
2177
+ run.end();
2178
+ service.busy = false;
2179
+ const ignored = service.signals.targetLost();
2180
+ for (const decision of decisions) recordDecision(service, instance, decision, ignored);
2181
+ if (ignored) {
2182
+ releaseExtraHolds(service, instance);
2183
+ service.container.runtime.reconciler.markDirty(service);
2184
+ return;
2185
+ }
2186
+ const decision = decisions.at(-1);
2187
+ finishRun(instance, hook, decision);
2188
+ switch (decision.kind) {
2189
+ case "handled":
2190
+ acceptSnapshot(service, instance, snapshot);
2191
+ service.container.runtime.reconciler.markDirty(service);
2192
+ return;
2193
+ case "requeue": {
2194
+ releaseExtraHolds(service, instance);
2195
+ const exhausted = exhaustedIntent(service, decision);
2196
+ if (exhausted) enterStopping(service, exhausted);
2197
+ else scheduleRequeue(service, instance, decision);
2198
+ return;
2199
+ }
2200
+ case "rebuild":
2201
+ releaseExtraHolds(service, instance);
2202
+ enterStopping(service, {
2203
+ reason: decision.reason,
2204
+ decision: "rebuild"
2205
+ });
2206
+ return;
2207
+ case "stop":
2208
+ releaseExtraHolds(service, instance);
2209
+ enterStopping(service, {
2210
+ parkEpoch: epochOf(service, (edge) => snapshot.get(edge.name)?.instance),
2211
+ reason: decision.reason,
2212
+ decision: "stop"
2213
+ });
2214
+ return;
2215
+ case "fail":
2216
+ releaseExtraHolds(service, instance);
2217
+ enterStopping(service, {
2218
+ failure: { error: decision.error },
2219
+ reason: decision.reason,
2220
+ decision: "fail"
2221
+ });
2222
+ }
2223
+ }
2224
+ // src/reconciler/registration.ts
2225
+ /**
2226
+ * Handles a `perConsumer` registration. It never builds. The service of each consumer container builds.
2227
+ * A withdrawn registration finalizes after its last consumer service.
2228
+ */
2229
+ function reconcileRegistration(service) {
2230
+ const phase = service.signals.phase();
2231
+ if (!service.signals.present()) {
2232
+ if (service.signals.consumers().size === 0) finalize(service);
2233
+ return;
2234
+ }
2235
+ if (phase === "pending" && inConflict(service)) setPhase(service, "conflict", { reason: `The exclusive key '${service.name}' has more than one declaration.` });
2236
+ else if (phase === "conflict" && !inConflict(service)) setPhase(service, "pending", { reason: `Only one declaration of the exclusive key '${service.name}' remains.` });
2237
+ }
2238
+ // src/reconciler/retry.ts
2239
+ function lostReason(service) {
2240
+ if (!service.signals.present()) return service.replacedBy ? "The service was replaced during a start requeue." : "The service was withdrawn during a start requeue.";
2241
+ if (!service.signals.active()) return "The container stopped during a start requeue.";
2242
+ if (requiredWithdrawn(service)) return "A required dependency was withdrawn during a start requeue.";
2243
+ return `The exclusive key '${service.name}' got another declaration during a start requeue.`;
2244
+ }
2245
+ /**
2246
+ * Handles a service in `retrying`: a `start` hook returned `requeue`.
2247
+ *
2248
+ * Changes during the wait merge into the next attempt. When the wait ends, `start` runs again with the same instance.
2249
+ * 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.
2250
+ */
2251
+ function reconcileRetrying(service) {
2252
+ const instance = service.signals.instance();
2253
+ if (!instance) {
2254
+ enterStopping(service, {});
2255
+ return;
2256
+ }
2257
+ if (service.signals.targetLost()) {
2258
+ cancelRequeueWait(instance);
2259
+ enterStopping(service, { reason: lostReason(service) });
2260
+ return;
2261
+ }
2262
+ if (instance.requeueWait) return;
2263
+ setPhase(service, "starting", { reason: "The requeue wait ended." });
2264
+ runStart(service, instance);
2265
+ }
2266
+ // src/reconciler/reconciler.ts
2267
+ /**
2268
+ * Creates the execution layer.
2269
+ *
2270
+ * A work queue holds dirty services, each at most once. The reconciler runs one transition at a time for each service.
2271
+ * Each transition reads the latest derived values when it starts and when it ends.
2272
+ */
2273
+ function createReconciler(options) {
2274
+ const { context } = options;
2275
+ const queue = /* @__PURE__ */ new Set();
2276
+ const drainedListeners = /* @__PURE__ */ new Set();
2277
+ const teardownListeners = /* @__PURE__ */ new Set();
2278
+ let drainRequested = false;
2279
+ let draining = false;
2280
+ function requestDrain() {
2281
+ if (drainRequested || draining) return;
2282
+ drainRequested = true;
2283
+ queueMicrotask(drain);
2284
+ }
2285
+ function markDirty(service) {
2286
+ queue.add(service);
2287
+ requestDrain();
2288
+ }
2289
+ function drain() {
2290
+ drainRequested = false;
2291
+ untracked(() => context.run(void 0, () => {
2292
+ draining = true;
2293
+ try {
2294
+ for (const service of queue) {
2295
+ queue.delete(service);
2296
+ reconcile(service);
2297
+ }
2298
+ } finally {
2299
+ draining = false;
2300
+ if (queue.size > 0) requestDrain();
2301
+ else callListeners(drainedListeners);
2302
+ }
2303
+ }));
2304
+ }
2305
+ function reconcile(service) {
2306
+ if (service.transaction) return;
2307
+ const phase = service.signals.phase();
2308
+ if (phase === "withdrawn") return;
2309
+ if (service.perConsumer) {
2310
+ reconcileRegistration(service);
2311
+ return;
2312
+ }
2313
+ checkSwap(service);
2314
+ if (service.busy) {
2315
+ const instance = service.signals.instance();
2316
+ if (!instance) return;
2317
+ if (phase === "building") {
2318
+ if (buildLost(service, instance)) instance.controller.abort(/* @__PURE__ */ new Error(`The build of '${service.name}' was superseded.`));
2319
+ } else if (instance.hookRun && service.signals.targetLost()) instance.hookRun.abort(/* @__PURE__ */ new Error(`A newer target superseded the hook run of '${service.name}'.`));
2320
+ return;
2321
+ }
2322
+ switch (phase) {
2323
+ case "pending":
2324
+ reconcilePending(service);
2325
+ return;
2326
+ case "conflict":
2327
+ reconcileConflict(service);
2328
+ return;
2329
+ case "failed":
2330
+ if (!service.signals.present()) finalize(service);
2331
+ return;
2332
+ case "running":
2333
+ reconcileRunning(service);
2334
+ return;
2335
+ case "retrying":
2336
+ reconcileRetrying(service);
2337
+ return;
2338
+ case "stopping": reconcileStopping(service);
2339
+ }
2340
+ }
2341
+ return {
2342
+ markDirty,
2343
+ onDrained: (listener) => {
2344
+ drainedListeners.add(listener);
2345
+ return () => drainedListeners.delete(listener);
2346
+ },
2347
+ onTeardown: (listener) => {
2348
+ teardownListeners.add(listener);
2349
+ return () => teardownListeners.delete(listener);
2350
+ },
2351
+ notifyTeardown: (service, errors) => callListeners(teardownListeners, service, errors),
2352
+ requestDrain
2353
+ };
2354
+ }
2355
+ // src/primitives/container.ts
2356
+ function deepFreeze(value) {
2357
+ if (typeof value === "object" && value !== null && !Object.isFrozen(value)) {
2358
+ Object.freeze(value);
2359
+ for (const child of Object.values(value)) deepFreeze(child);
2360
+ }
2361
+ return value;
2362
+ }
2363
+ function readOwner(owner) {
2364
+ if (owner === void 0) return Object.freeze({});
2365
+ let copy;
2366
+ try {
2367
+ copy = structuredClone(owner);
2368
+ } catch (error) {
2369
+ throw new InvalidArgumentError("The container owner must be a serializable record.", { cause: error });
2370
+ }
2371
+ if (typeof copy !== "object" || copy === null || Array.isArray(copy)) throw new InvalidArgumentError("The container owner must be a serializable record.");
2372
+ return deepFreeze(copy);
2373
+ }
2374
+ function readIsolated(options) {
2375
+ let isolate;
2376
+ try {
2377
+ isolate = options.isolate;
2378
+ } catch (error) {
2379
+ throw new InvalidArgumentError("Cannot read the isolate option.", { cause: error });
2380
+ }
2381
+ if (isolate === void 0) return /* @__PURE__ */ new Set();
2382
+ if (!Array.isArray(isolate)) throw new InvalidArgumentError("The isolate option must be a list of keys or names.");
2383
+ return new Set(isolate.map((key) => {
2384
+ let name;
2385
+ try {
2386
+ name = normalizeName(key);
2387
+ } catch (error) {
2388
+ throw new InvalidArgumentError("Cannot read a key of the isolate option.", { cause: error });
2389
+ }
2390
+ 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.");
2391
+ return name;
2392
+ }));
2393
+ }
2394
+ function readContainerPolicy(options) {
2395
+ let fields;
2396
+ try {
2397
+ fields = {
2398
+ maxAttempts: options.maxAttempts,
2399
+ timeouts: options.timeouts,
2400
+ onTimeout: options.onTimeout
2401
+ };
2402
+ } catch (error) {
2403
+ throw new InvalidArgumentError("Cannot read the container options.", { cause: error });
2404
+ }
2405
+ return readPolicy("the container", fields);
2406
+ }
2407
+ function readRandom(options) {
2408
+ let random;
2409
+ try {
2410
+ random = options.random;
2411
+ } catch (error) {
2412
+ throw new InvalidArgumentError("Cannot read the random option.", { cause: error });
2413
+ }
2414
+ if (random !== void 0 && typeof random !== "function") throw new InvalidArgumentError("The random option must be a function.");
2415
+ return random ?? Math.random;
2416
+ }
2417
+ function createRuntime(options) {
2418
+ let seq = 0;
2419
+ let instanceSeq = 0;
2420
+ const context = createCleanupContext();
2421
+ return {
2422
+ clock: options.clock ?? systemClock,
2423
+ random: readRandom(options),
2424
+ context,
2425
+ deliveries: createDeliveryQueue(),
2426
+ observers: /* @__PURE__ */ new Set(),
2427
+ declarations: /* @__PURE__ */ new Map(),
2428
+ reconciler: createReconciler({ context }),
2429
+ nextSeq: () => ++seq,
2430
+ nextInstanceId: () => `i${++instanceSeq}`
2431
+ };
2432
+ }
2433
+ /**
2434
+ * Creates a container. With `options.parent`, the container is a child of that container.
2435
+ *
2436
+ * A child container does not start when it is created, even under a started parent. It waits for `start(child)`.
2437
+ */
2438
+ function createContainer(options = {}) {
2439
+ const parent = options.parent === void 0 ? void 0 : nodeOf(options.parent);
2440
+ if (parent) assertActive(parent, "create a child container");
2441
+ const owner = readOwner(options.owner);
2442
+ const isolated = readIsolated(options);
2443
+ const policy = readContainerPolicy(options);
2444
+ const logger = options.enabled === false ? createNoopLogger() : options.logger ?? parent?.api.logger ?? createDefaultLogger();
2445
+ const runtime = parent?.runtime ?? createRuntime(options);
2446
+ let reactiveOwner;
2447
+ const stopReactive = withOwner(parent?.reactiveOwner, () => effectScope(() => {
2448
+ reactiveOwner = getActiveSub();
2449
+ }));
2450
+ const api = {
2451
+ logger,
2452
+ owner,
2453
+ parent: parent?.api
2454
+ };
2455
+ const node = {
2456
+ seq: runtime.nextSeq(),
2457
+ api,
2458
+ owner,
2459
+ parent,
2460
+ runtime,
2461
+ children: /* @__PURE__ */ new Set(),
2462
+ services: /* @__PURE__ */ new Set(),
2463
+ signals: {
2464
+ started: signal(false),
2465
+ slots: /* @__PURE__ */ new Map()
2466
+ },
2467
+ isolated,
2468
+ policy,
2469
+ listeners: /* @__PURE__ */ new Map(),
2470
+ transactions: /* @__PURE__ */ new Set(),
2471
+ effects: [],
2472
+ reactiveOwner,
2473
+ stopReactive,
2474
+ enrolled: false,
2475
+ state: "active",
2476
+ frame: void 0,
2477
+ disposal: void 0,
2478
+ disposedListeners: /* @__PURE__ */ new Set()
2479
+ };
2480
+ parent?.children.add(node);
2481
+ registerNode(api, node);
2482
+ return api;
2483
+ }
2484
+ /**
2485
+ * Runs `setup` and stores the returned disposer on the container. `dispose(container)` runs the disposers in reverse order.
2486
+ */
2487
+ function effect$1(container, setup) {
2488
+ const node = nodeOf(container);
2489
+ assertActive(node, "add an effect");
2490
+ if (typeof setup !== "function") throw new InvalidArgumentError("The effect setup is not a function.");
2491
+ const disposer = untracked(setup);
2492
+ if (typeof disposer === "function") node.effects.push(disposer);
2493
+ }
2494
+ /**
2495
+ * Starts the disposal of `container` and of its descendants.
2496
+ *
2497
+ * The synchronous part withdraws all services of the subtree in the caller's batch, so that dependents stop without a rebuild.
2498
+ */
2499
+ function beginDisposal(container, parentFrame) {
2500
+ if (container.disposal) return container.disposal;
2501
+ const { context } = container.runtime;
2502
+ const frame = {
2503
+ owner: container,
2504
+ parent: parentFrame,
2505
+ active: true
2506
+ };
2507
+ container.state = "disposing";
2508
+ container.frame = frame;
2509
+ for (const transaction of [...container.transactions]) abortTransaction(transaction, new BuildAfterDisposeError("The container was disposed before the transaction committed."));
2510
+ const withdrawn = /* @__PURE__ */ new Set();
2511
+ for (const service of container.services) if (service.signals.present()) {
2512
+ releaseService(service);
2513
+ withdrawn.add(service);
2514
+ }
2515
+ const children = [...container.children].reverse().map((child) => {
2516
+ const disposal = beginDisposal(child, frame);
2517
+ disposal.catch(() => {});
2518
+ return disposal;
2519
+ });
2520
+ container.disposal = context.run(frame, async () => {
2521
+ const errors = [];
2522
+ for (const service of [...container.services]) {
2523
+ const serviceErrors = await service.withdrawal.promise;
2524
+ if (withdrawn.has(service)) errors.push(...serviceErrors);
2525
+ }
2526
+ for (const child of children) try {
2527
+ await child;
2528
+ } catch (error) {
2529
+ errors.push(error);
2530
+ }
2531
+ for (const disposer of container.effects.splice(0).reverse()) try {
2532
+ await context.run(frame, () => untracked(disposer));
2533
+ } catch (error) {
2534
+ errors.push(error);
2535
+ }
2536
+ if (errors.length > 0) throw errorFrom(errors, "Container disposal failed.");
2537
+ }).finally(() => {
2538
+ frame.active = false;
2539
+ container.state = "disposed";
2540
+ container.listeners.clear();
2541
+ container.stopReactive();
2542
+ container.parent?.children.delete(container);
2543
+ for (const listener of [...container.disposedListeners]) try {
2544
+ listener();
2545
+ } catch (error) {
2546
+ queueMicrotask(() => {
2547
+ throw error;
2548
+ });
2549
+ }
2550
+ container.disposedListeners.clear();
2551
+ });
2552
+ return container.disposal;
2553
+ }
2554
+ /**
2555
+ * Withdraws the registrations and invocations of the container, disposes the child containers, and runs the container effects in reverse order.
2556
+ *
2557
+ * One error keeps its identity. More than one error becomes an `AggregateError`.
2558
+ * A call from the cleanup chain of the same container rejects with `reentrant-dispose`. A second call from outside gets the same promise.
2559
+ */
2560
+ function dispose(container) {
2561
+ const node = nodeOf(container);
2562
+ const { context } = node.runtime;
2563
+ if (context.isActive(node)) return Promise.reject(new ReentrantDisposeError("dispose() was called from the cleanup chain of the same container."));
2564
+ if (node.disposal) return node.disposal;
2565
+ return untracked(() => {
2566
+ let disposal;
2567
+ batch(() => {
2568
+ disposal = beginDisposal(node, context.current());
2569
+ });
2570
+ return disposal;
2571
+ });
2572
+ }
2573
+ // src/primitives/invoke.ts
2574
+ function invoke$1(container, option) {
2575
+ return invokeIn(container, option);
2576
+ }
2577
+ /** Registers an invocation. The public `invoke` functions call it. */
2578
+ function invokeIn(container, option) {
2579
+ const node = nodeOf(container);
2580
+ assertActive(node, "invoke");
2581
+ let callback;
2582
+ let dependsOn;
2583
+ try {
2584
+ const invocationObject = typeof option === "function" ? { callback: option } : option;
2585
+ callback = invocationObject.callback;
2586
+ dependsOn = invocationObject.dependsOn;
2587
+ } catch (error) {
2588
+ throw new InvalidArgumentError("Cannot read the invoke options.", { cause: error });
2589
+ }
2590
+ if (typeof callback !== "function") throw new InvalidArgumentError("The callback option of invoke is not a function.");
2591
+ const name = `invocation:${node.runtime.nextSeq()}`;
2592
+ const edges = readEdges(name, dependsOn);
2593
+ const userCallback = callback;
2594
+ const build = (context) => userCallback(context.dependsOn, Object.freeze({
2595
+ on: context.on,
2596
+ effect: context.effect,
2597
+ signal: context.signal
2598
+ }));
2599
+ const registered = untracked(() => {
2600
+ let service;
2601
+ batch(() => {
2602
+ service = createService({
2603
+ container: node,
2604
+ kind: "invocation",
2605
+ name,
2606
+ edges,
2607
+ build
2608
+ });
2609
+ node.services.add(service);
2610
+ });
2611
+ return service;
2612
+ });
2613
+ container.logger.invoke(edges.map((edge) => edge.keyName));
2614
+ return Object.freeze({ withdraw: () => withdrawService(registered) });
2615
+ }
2616
+ // src/diagnostics/explain.ts
2617
+ /** The owners of conflicting declarations, in container order. The order of the registrations has no effect. */
2618
+ function ownersOf(declarations) {
2619
+ return Object.freeze([...declarations].sort((a, b) => a.container.seq - b.container.seq).map((declaration) => declaration.container.owner));
2620
+ }
2621
+ /** Explains one service from its current resolution data. It is a pure read. */
2622
+ function explainService(service) {
2623
+ const blockers = [];
2624
+ const phase = service.signals.phase();
2625
+ if (phase === "failed") blockers.push({
2626
+ kind: "failed",
2627
+ error: service.failure?.error
2628
+ });
2629
+ if (phase === "retrying") {
2630
+ const previous = service.signals.instance()?.hookStates.get("start")?.previous;
2631
+ blockers.push({
2632
+ kind: "retrying",
2633
+ attempt: previous?.attempt ?? 1,
2634
+ ...withoutUndefined({ reason: previous?.reason })
2635
+ });
2636
+ }
2637
+ const rivals = service.signals.rivals();
2638
+ if (rivals.length > 0) {
2639
+ const registration = service.template ?? service;
2640
+ blockers.push({
2641
+ kind: "conflict",
2642
+ key: service.name,
2643
+ owners: ownersOf([registration, ...rivals])
2644
+ });
2645
+ }
2646
+ if (!service.signals.active() && !service.perConsumer) blockers.push({ kind: "not-started" });
2647
+ for (const edge of service.edges) {
2648
+ if (edge.optional) continue;
2649
+ const resolution = resolutionOf(service, edge);
2650
+ switch (resolution.kind) {
2651
+ case "missing":
2652
+ blockers.push({
2653
+ kind: "missing-provider",
2654
+ dependency: edge.name,
2655
+ key: edge.keyName
2656
+ });
2657
+ continue;
2658
+ case "rejected":
2659
+ blockers.push({
2660
+ kind: "rejected-provider",
2661
+ dependency: edge.name,
2662
+ key: resolution.provider.name,
2663
+ owner: resolution.provider.container.owner,
2664
+ metadata: resolution.provider.metadata
2665
+ });
2666
+ continue;
2667
+ case "conflict":
2668
+ blockers.push({
2669
+ kind: "conflicting-dependency",
2670
+ dependency: edge.name,
2671
+ key: resolution.provider.name,
2672
+ owners: ownersOf([resolution.provider, ...resolution.rivals])
2673
+ });
2674
+ continue;
2675
+ case "resolved": {
2676
+ const status = resolution.provider.signals.effective();
2677
+ if (phase !== "running" && status?.status === "unavailable") blockers.push({
2678
+ kind: "unavailable-dependency",
2679
+ dependency: edge.name,
2680
+ key: resolution.provider.name,
2681
+ cause: causeOf(resolution.provider, status)
2682
+ });
2683
+ continue;
2684
+ }
2685
+ case "not-running": {
2686
+ const providerPhase = resolution.provider.signals.phase();
2687
+ blockers.push(providerPhase === "failed" ? {
2688
+ kind: "failed-dependency",
2689
+ dependency: edge.name,
2690
+ key: edge.keyName,
2691
+ error: resolution.provider.failure?.error
2692
+ } : {
2693
+ kind: "pending-dependency",
2694
+ dependency: edge.name,
2695
+ key: edge.keyName,
2696
+ phase: providerPhase
2697
+ });
2698
+ }
2699
+ }
2700
+ }
2701
+ const status = service.signals.effective();
2702
+ return Object.freeze({
2703
+ name: service.name,
2704
+ kind: service.kind === "invocation" ? "invocation" : "service",
2705
+ owner: service.container.owner,
2706
+ phase,
2707
+ ...withoutUndefined({
2708
+ status: status?.status,
2709
+ reason: status?.reason,
2710
+ cause: status?.cause
2711
+ }),
2712
+ blockers: Object.freeze(blockers)
2713
+ });
2714
+ }
2715
+ /** Formats a cause chain, for example `'airi.llm' is unavailable (reason), because 'gateway/store' is unavailable`. */
2716
+ function describeCause(cause) {
2717
+ const parts = [];
2718
+ for (let current = cause; current; current = current.cause) parts.push(`'${current.key}' is ${current.status}${current.reason === void 0 ? "" : ` (${current.reason})`}`);
2719
+ return parts.join(", because ");
2720
+ }
2721
+ function describeOwners(owners) {
2722
+ return owners.map((owner) => JSON.stringify(owner)).join(", ");
2723
+ }
2724
+ function describeBlocker(blocker) {
2725
+ switch (blocker.kind) {
2726
+ case "missing-provider": return `no provider for '${blocker.key}' (${blocker.dependency})`;
2727
+ case "failed-dependency": return `'${blocker.key}' (${blocker.dependency}) failed`;
2728
+ case "pending-dependency": return `'${blocker.key}' (${blocker.dependency}) is ${blocker.phase}`;
2729
+ case "unavailable-dependency": return `${describeCause(blocker.cause)} (${blocker.dependency})`;
2730
+ case "rejected-provider": return `a resolution condition rejected the provider of '${blocker.key}' from ${JSON.stringify(blocker.owner)} (${blocker.dependency})`;
2731
+ case "conflicting-dependency": return `the exclusive key '${blocker.key}' (${blocker.dependency}) has more than one declaration: ${describeOwners(blocker.owners)}`;
2732
+ case "conflict": return `the exclusive key '${blocker.key}' has more than one declaration: ${describeOwners(blocker.owners)}`;
2733
+ case "failed": return "the service failed";
2734
+ case "retrying": return `its start hook requeued after attempt ${blocker.attempt}${blocker.reason === void 0 ? "" : ` (${blocker.reason})`}`;
2735
+ case "not-started": return "its container is not started";
2736
+ }
2737
+ }
2738
+ /** Formats explanations as one line for each service. */
2739
+ function describeExplanations(explanations) {
2740
+ return explanations.map((explanation) => {
2741
+ const reasons = explanation.blockers.map(describeBlocker);
2742
+ if (explanation.cause) reasons.push(`it is ${explanation.status ?? "unavailable"}, because ${describeCause(explanation.cause)}`);
2743
+ return `- ${explanation.name} is ${explanation.phase}${reasons.length > 0 ? `: ${reasons.join(", ")}` : ""}`;
2744
+ }).join("\n");
2745
+ }
2746
+ // src/primitives/lifecycle.ts
2747
+ const visibleKinds = /* @__PURE__ */ new Set(["service", "invocation"]);
2748
+ const transitionalPhases = /* @__PURE__ */ new Set([
2749
+ "building",
2750
+ "starting",
2751
+ "retrying",
2752
+ "stopping"
2753
+ ]);
2754
+ /** Lists the present services and invocations of `containers`. A `perConsumer` registration never runs, so it is not in the list. Its consumer services are. */
2755
+ function presentServices(containers) {
2756
+ return containers.flatMap((container) => [...container.services].filter((service) => visibleKinds.has(service.kind) && service.signals.present() && !service.perConsumer));
2757
+ }
2758
+ /**
2759
+ * Runs `check` each time the work queue drains, until the result is not `'waiting'`.
2760
+ * With `timeoutMs`, it rejects with `start-timeout` and the explanation of each service that is not `running`.
2761
+ */
2762
+ function waitUntilSettled(containers, check, timeoutMs) {
2763
+ const { clock, reconciler } = containers[0].runtime;
2764
+ return new Promise((done, fail) => {
2765
+ let cancelTimeout = () => {};
2766
+ const stopListening = reconciler.onDrained(() => {
2767
+ const result = check(presentServices(containers));
2768
+ if (result === "waiting") return;
2769
+ stopListening();
2770
+ cancelTimeout();
2771
+ if (result.kind === "done") done();
2772
+ else fail(errorFrom(result.errors, "Start failed."));
2773
+ });
2774
+ if (timeoutMs !== void 0) cancelTimeout = clock.setTimeout(() => untracked(() => {
2775
+ stopListening();
2776
+ const explanations = presentServices(containers).filter((service) => service.signals.phase() !== "running").map(explainService);
2777
+ fail(new StartTimeoutError(`start() timed out after ${timeoutMs}ms. These services are not running:\n${describeExplanations(explanations)}`, explanations));
2778
+ }), timeoutMs);
2779
+ reconciler.requestDrain();
2780
+ });
2781
+ }
2782
+ function readTimeout(options) {
2783
+ let timeoutMs;
2784
+ try {
2785
+ timeoutMs = options?.timeoutMs;
2786
+ } catch (error) {
2787
+ throw new InvalidArgumentError("Cannot read the start options.", { cause: error });
2788
+ }
2789
+ if (timeoutMs !== void 0 && (typeof timeoutMs !== "number" || !Number.isFinite(timeoutMs) || timeoutMs < 0)) throw new InvalidArgumentError(`The timeoutMs option of start must be a finite number of 0 or more. Got ${String(timeoutMs)}.`);
2790
+ return timeoutMs;
2791
+ }
2792
+ /**
2793
+ * Begins convergence of the container and of each child container that was started.
2794
+ *
2795
+ * Resolves when all their services and invocations are `running`. Rejects with the build and start errors.
2796
+ * A service in `failed` tries again. A required key without a provider keeps the promise pending, unless `timeoutMs` is set.
2797
+ * A child container that was never started does not start. Call `start(child)` for it.
2798
+ */
2799
+ function start$1(container, options) {
2800
+ const node = nodeOf(container);
2801
+ assertActive(node, "start");
2802
+ const timeoutMs = readTimeout(options);
2803
+ node.enrolled = true;
2804
+ const containers = affectedContainers(node);
2805
+ return untracked(() => {
2806
+ batch(() => {
2807
+ for (const current of containers) current.signals.started(true);
2808
+ for (const service of presentServices(containers)) if (service.signals.phase() === "failed") {
2809
+ service.failure = void 0;
2810
+ setPhase(service, "pending", { reason: "start() tries the failed service again." });
2811
+ }
2812
+ });
2813
+ return waitUntilSettled(containers, (services) => {
2814
+ if (services.every((service) => service.signals.phase() === "running")) return { kind: "done" };
2815
+ if (services.some((service) => transitionalPhases.has(service.signals.phase()))) return "waiting";
2816
+ const errors = services.flatMap((service) => service.signals.phase() === "failed" && service.failure ? [service.failure.error] : []);
2817
+ return errors.length > 0 ? {
2818
+ kind: "failed",
2819
+ errors
2820
+ } : "waiting";
2821
+ }, timeoutMs);
2822
+ }).then(() => container.logger.running());
2823
+ }
2824
+ /**
2825
+ * Stops all instances of the container and of each child container that was started. Dependents stop before their providers.
2826
+ *
2827
+ * The registrations stay. The container stays usable, and a later `start()` builds them again. `dispose()` clears the container.
2828
+ * Rejects with the teardown errors.
2829
+ */
2830
+ async function stop$1(container) {
2831
+ const node = nodeOf(container);
2832
+ const containers = affectedContainers(node);
2833
+ const affected = new Set(containers);
2834
+ node.enrolled = false;
2835
+ const errors = [];
2836
+ const stopTeardown = node.runtime.reconciler.onTeardown((service, teardownErrors) => {
2837
+ if (service.kind !== "resolver" && affected.has(service.container)) errors.push(...teardownErrors);
2838
+ });
2839
+ const settled = untracked(() => {
2840
+ batch(() => {
2841
+ for (const current of containers) current.signals.started(false);
2842
+ for (const service of presentServices(containers)) service.signals.demanded(false);
2843
+ });
2844
+ return waitUntilSettled(containers, (services) => services.every((service) => service.signals.instance() === void 0 && !transitionalPhases.has(service.signals.phase())) ? { kind: "done" } : "waiting");
2845
+ });
2846
+ try {
2847
+ await settled;
2848
+ } finally {
2849
+ stopTeardown();
2850
+ }
2851
+ if (errors.length > 0) throw errorFrom(errors, "Stop failed.");
2852
+ }
2853
+ // src/primitives/listen.ts
2854
+ /** Reads the `id` of an event key once. Any object with a non-empty string `id` is an event key. */
2855
+ function readEventId(eventKey) {
2856
+ let id;
2857
+ try {
2858
+ id = typeof eventKey === "object" && eventKey !== null ? eventKey.id : void 0;
2859
+ } catch (error) {
2860
+ throw new InvalidArgumentError("Cannot read the id of the event key.", { cause: error });
2861
+ }
2862
+ if (typeof id !== "string" || id.length === 0) throw new InvalidArgumentError("An event key must be an object with a non-empty string id.");
2863
+ return id;
2864
+ }
2865
+ function readListenerMetadata(metadata) {
2866
+ if (metadata === void 0) return Object.freeze({});
2867
+ if (typeof metadata !== "object" || metadata === null || Array.isArray(metadata)) throw new InvalidArgumentError("The metadata of a listener must be a record.");
2868
+ try {
2869
+ return Object.freeze({ ...metadata });
2870
+ } catch (error) {
2871
+ throw new InvalidArgumentError("Cannot read the metadata of the listener.", { cause: error });
2872
+ }
2873
+ }
2874
+ /**
2875
+ * Stores a listener for `eventKey`. The container owns it: `dispose(container)` removes it.
2876
+ *
2877
+ * injeca does not call the listener. The consumer reads the listeners with `listeners()` and runs them by its own rules.
2878
+ * Returns a function that removes the listener.
2879
+ */
2880
+ function listen(container, eventKey, handler, metadata) {
2881
+ const node = nodeOf(container);
2882
+ assertActive(node, "listen");
2883
+ const id = readEventId(eventKey);
2884
+ if (typeof handler !== "function") throw new InvalidArgumentError(`The listener for '${id}' is not a function.`);
2885
+ const record = Object.freeze({
2886
+ seq: node.runtime.nextSeq(),
2887
+ handler,
2888
+ metadata: readListenerMetadata(metadata)
2889
+ });
2890
+ const records = node.listeners.get(id) ?? [];
2891
+ node.listeners.set(id, [...records, record]);
2892
+ return () => {
2893
+ const current = node.listeners.get(id);
2894
+ if (!current?.includes(record)) return;
2895
+ const rest = current.filter((entry) => entry !== record);
2896
+ if (rest.length > 0) node.listeners.set(id, rest);
2897
+ else node.listeners.delete(id);
2898
+ };
2899
+ }
2900
+ // src/primitives/replace.ts
2901
+ /**
2902
+ * Creates a new service that replaces `target` after it completes `start`. It is not published before the swap.
2903
+ * `onSwap` runs at the swap, before the outcome settles.
2904
+ */
2905
+ function createSuccessor(target, definition, template, onSettle, extra = {}) {
2906
+ const outcome = createDeferred();
2907
+ let settled = false;
2908
+ let service;
2909
+ batch(() => {
2910
+ service = createService({
2911
+ container: target.container,
2912
+ kind: "service",
2913
+ name: target.name,
2914
+ edges: definition.edges,
2915
+ build: definition.build,
2916
+ metadata: definition.metadata,
2917
+ policy: definition.policy,
2918
+ exclusive: target.exclusive,
2919
+ template,
2920
+ consumer: target.consumer,
2921
+ registration: target.registration,
2922
+ ...extra,
2923
+ swap: {
2924
+ target,
2925
+ settle: (result) => {
2926
+ if (settled) return;
2927
+ settled = true;
2928
+ onSettle(service, result);
2929
+ outcome.resolve(result);
2930
+ }
2931
+ }
2932
+ });
2933
+ service.signals.demanded(target.signals.demanded());
2934
+ target.container.services.add(service);
2935
+ });
2936
+ return {
2937
+ service,
2938
+ outcome: outcome.promise
2939
+ };
2940
+ }
2941
+ function outcomeResult(outcome, name) {
2942
+ switch (outcome.kind) {
2943
+ case "swapped": return true;
2944
+ case "cancelled": return false;
2945
+ case "failed": throw outcome.error;
2946
+ case "stopping": throw new ReplaceWhileStoppingError(`'${name}' started to stop before its replacement completed.`);
2947
+ }
2948
+ }
2949
+ /** Makes `service` the current registration in place of `target`, in the caller's batch. */
2950
+ function takeOver(registration, target, service) {
2951
+ cancelPending(registration);
2952
+ target.replacedBy = service;
2953
+ releaseService(target);
2954
+ publishService(service);
2955
+ registration.current = service;
2956
+ }
2957
+ /** Replaces a service that has no serving instance. The new service takes the slot at once. */
2958
+ function replaceAtOnce(registration, target, definition) {
2959
+ let service;
2960
+ batch(() => {
2961
+ service = createService({
2962
+ container: target.container,
2963
+ kind: "service",
2964
+ name: target.name,
2965
+ edges: definition.edges,
2966
+ build: definition.build,
2967
+ metadata: definition.metadata,
2968
+ policy: definition.policy,
2969
+ exclusive: target.exclusive,
2970
+ perConsumer: target.perConsumer,
2971
+ registration
2972
+ });
2973
+ service.signals.demanded(target.signals.demanded());
2974
+ target.container.services.add(service);
2975
+ takeOver(registration, target, service);
2976
+ });
2977
+ return service;
2978
+ }
2979
+ /**
2980
+ * Moves the consumer services of the `perConsumer` registration `target` to `template`, in the caller's batch.
2981
+ * Each running consumer service gets a new service that swaps in after it completes `start`. The others leave and build again on demand.
2982
+ */
2983
+ function moveConsumers(registration, target, template, definition) {
2984
+ const consumers = target.signals.consumers();
2985
+ target.signals.consumers(/* @__PURE__ */ new Map());
2986
+ takeOver(registration, target, template);
2987
+ const swaps = [];
2988
+ const kept = /* @__PURE__ */ new Map();
2989
+ for (const [container, consumer] of consumers) {
2990
+ consumer.template = template;
2991
+ if (consumer.signals.serving()) {
2992
+ kept.set(container, consumer);
2993
+ swaps.push(createSuccessor(consumer, definition, template, () => {}));
2994
+ } else consumer.signals.present(false);
2995
+ }
2996
+ template.signals.consumers(kept);
2997
+ return swaps;
2998
+ }
2999
+ /** Replaces a `perConsumer` registration. The new registration takes the slot at once. Its consumer services swap in one by one. */
3000
+ function replaceRegistrationTemplate(registration, target, definition) {
3001
+ let swaps = [];
3002
+ batch(() => {
3003
+ const template = createService({
3004
+ container: target.container,
3005
+ kind: "service",
3006
+ name: target.name,
3007
+ edges: definition.edges,
3008
+ build: definition.build,
3009
+ metadata: definition.metadata,
3010
+ policy: definition.policy,
3011
+ exclusive: target.exclusive,
3012
+ perConsumer: true,
3013
+ registration
3014
+ });
3015
+ target.container.services.add(template);
3016
+ swaps = moveConsumers(registration, target, template, definition);
3017
+ });
3018
+ return swaps;
3019
+ }
3020
+ /** Settles a replacement for its registration. */
3021
+ function settleRegistration(registration) {
3022
+ return (service, outcome) => {
3023
+ if (registration.pending === service) registration.pending = void 0;
3024
+ if (outcome.kind === "swapped") registration.current = service;
3025
+ };
3026
+ }
3027
+ /**
3028
+ * Starts the replacement of a running service. The old instance serves until the new one completes `start`.
3029
+ * A transaction passes `extra`, and it sets `registration.pending` at its publish.
3030
+ */
3031
+ function replaceRunning(registration, target, definition, extra) {
3032
+ const pending = createSuccessor(target, definition, void 0, settleRegistration(registration), extra);
3033
+ if (!extra?.transaction) {
3034
+ cancelPending(registration);
3035
+ registration.pending = pending.service;
3036
+ }
3037
+ return pending;
3038
+ }
3039
+ /** The `replace()` of a registration. */
3040
+ async function replace(registration, option) {
3041
+ const target = untracked(() => currentService(registration));
3042
+ if (!target) return false;
3043
+ const definition = readDefinition(registration.name, option, target);
3044
+ const phase = untracked(() => target.signals.phase());
3045
+ if (phase === "stopping") throw new ReplaceWhileStoppingError(`'${registration.name}' is stopping. Wait until it stops, then call provide.`);
3046
+ if (target.perConsumer) {
3047
+ const swaps = untracked(() => replaceRegistrationTemplate(registration, target, definition));
3048
+ const errors = (await Promise.all(swaps.map((swap) => swap.outcome))).flatMap((outcome) => outcome.kind === "failed" ? [outcome.error] : []);
3049
+ if (errors.length > 0) throw errorFrom(errors, `The replacement of '${registration.name}' failed for some consumers.`);
3050
+ return true;
3051
+ }
3052
+ if (phase !== "running") {
3053
+ untracked(() => replaceAtOnce(registration, target, definition));
3054
+ return true;
3055
+ }
3056
+ return outcomeResult(await untracked(() => replaceRunning(registration, target, definition)).outcome, registration.name);
3057
+ }
3058
+ const registrations = /* @__PURE__ */ new WeakMap();
3059
+ /** The registration behind a result of `provide`, or `undefined` for any other value. */
3060
+ function registrationOf(key) {
3061
+ return typeof key === "object" && key !== null ? registrations.get(key) : void 0;
3062
+ }
3063
+ /** Creates the public result of `provide`: the typed key with `withdraw()` and `replace()`. */
3064
+ function registrationApi(registration) {
3065
+ const api = Object.freeze({
3066
+ key: registration.name,
3067
+ ...registration.exclusive ? { exclusive: true } : {},
3068
+ withdraw: () => withdrawRegistration(registration),
3069
+ replace: ((option) => replace(registration, option))
3070
+ });
3071
+ registrations.set(api, registration);
3072
+ return api;
3073
+ }
3074
+ // src/primitives/provide.ts
3075
+ function normalizeProvideOption(nameOrOption, option) {
3076
+ if (typeof nameOrOption === "string") {
3077
+ 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>.");
3078
+ if (typeof option === "function") return { build: option };
3079
+ return option;
3080
+ }
3081
+ if (typeof nameOrOption === "object" && "key" in nameOrOption) {
3082
+ 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>.");
3083
+ if (typeof option === "function") return { build: option };
3084
+ return option;
3085
+ }
3086
+ if (typeof nameOrOption === "function") return { build: nameOrOption };
3087
+ return nameOrOption;
3088
+ }
3089
+ function isProvidedKey(value) {
3090
+ return typeof value === "object" && value !== null && "key" in value && !("build" in value);
3091
+ }
3092
+ function autoName(stackIndex, nameOrOption, option) {
3093
+ const fromFirst = typeof nameOrOption === "object" && "autoNameStackIndex" in nameOrOption ? nameOrOption.autoNameStackIndex : void 0;
3094
+ const requested = (typeof option === "object" ? option.autoNameStackIndex : void 0) ?? fromFirst;
3095
+ const index = typeof requested === "number" ? requested + stackIndex - 1 : stackIndex;
3096
+ const parentFile = ErrorStackParser.parse(/* @__PURE__ */ new Error("providing"))[index];
3097
+ return `${parentFile?.fileName ?? `unknown-${nanoid()}`}:${parentFile?.lineNumber ?? 0}:${parentFile?.columnNumber ?? 0}`;
3098
+ }
3099
+ /** Throws `duplicate-service` when a started container already has a registration for `name`. */
3100
+ function assertNoDuplicate(container, name) {
3101
+ if (slotOf(container, name)() && container.signals.started()) throw new DuplicateServiceError(`The started container already has a registration for '${name}'.`);
3102
+ }
3103
+ /**
3104
+ * Publishes `service` as the current registration of its key. Call it inside a batch.
3105
+ * Before `start()`, a second provide of the same name replaces the first registration, as in 0.2.
3106
+ */
3107
+ function publishRegistration(container, service) {
3108
+ const previous = slotOf(container, service.name)();
3109
+ if (previous) {
3110
+ releaseService(previous);
3111
+ emitRecord(container, {
3112
+ type: "registration-replaced",
3113
+ key: service.name,
3114
+ owner: container.owner,
3115
+ instanceId: previous.signals.instance()?.id,
3116
+ reason: `A second provide of '${service.name}' replaced the registration before start().`
3117
+ });
3118
+ }
3119
+ publishService(service);
3120
+ }
3121
+ /** Creates the service of one registration. It is not published yet. */
3122
+ function serviceOf(container, name, definition, exclusive, extra = {}) {
3123
+ const service = createService({
3124
+ container,
3125
+ kind: "service",
3126
+ name,
3127
+ edges: definition.edges,
3128
+ build: definition.build,
3129
+ metadata: definition.metadata,
3130
+ policy: definition.policy,
3131
+ exclusive,
3132
+ perConsumer: definition.perConsumer,
3133
+ ...extra
3134
+ });
3135
+ container.services.add(service);
3136
+ return service;
3137
+ }
3138
+ /** Makes the service of one `provide` call the current registration of its key. */
3139
+ function register(container, name, definition, exclusive) {
3140
+ assertNoDuplicate(container, name);
3141
+ const registration = createRegistration(container, name, exclusive);
3142
+ batch(() => {
3143
+ const service = serviceOf(container, name, definition, exclusive, { registration });
3144
+ publishRegistration(container, service);
3145
+ registration.current = service;
3146
+ });
3147
+ return registration;
3148
+ }
3149
+ /**
3150
+ * 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.
3151
+ */
3152
+ function provideIn(container, stackIndex, nameOrOption, option) {
3153
+ const node = nodeOf(container);
3154
+ assertActive(node, "provide");
3155
+ const name = typeof nameOrOption === "string" ? nameOrOption : isProvidedKey(nameOrOption) ? normalizeName(nameOrOption) : autoName(stackIndex, nameOrOption, option);
3156
+ if (typeof name !== "string" || name.length === 0) throw new InvalidArgumentError("A provider name must be a non-empty string.");
3157
+ const definition = readDefinition(name, normalizeProvideOption(nameOrOption, option));
3158
+ const exclusive = isExclusiveKey(nameOrOption);
3159
+ const registration = untracked(() => register(node, name, definition, exclusive));
3160
+ container.logger.provide(name, definition.edges.map((edge) => edge.keyName));
3161
+ return registrationApi(registration);
3162
+ }
3163
+ function provide$1(container, nameOrOption, option) {
3164
+ return provideIn(container, 3, nameOrOption, option);
3165
+ }
3166
+ // src/primitives/resolve.ts
3167
+ /**
3168
+ * Waits until the dependencies are available and returns their values. Before `start()`, it builds and starts them.
3169
+ *
3170
+ * Rejects when a required dependency fails. The values can become stale after a run-time change. Use it for one-time code.
3171
+ */
3172
+ function resolve$1(container, dependsOn) {
3173
+ const node = nodeOf(container);
3174
+ assertActive(node, "resolve");
3175
+ const edges = readEdges("resolve", dependsOn);
3176
+ const { reconciler } = node.runtime;
3177
+ return new Promise((done, fail) => untracked(() => {
3178
+ let settled = false;
3179
+ let service;
3180
+ let stopListening = () => {};
3181
+ const finish = (outcome) => {
3182
+ if (settled) return;
3183
+ settled = true;
3184
+ stopListening();
3185
+ batch(() => releaseService(service));
3186
+ outcome();
3187
+ };
3188
+ const build = (context) => finish(() => done({ ...context.dependsOn }));
3189
+ batch(() => {
3190
+ service = createService({
3191
+ container: node,
3192
+ kind: "resolver",
3193
+ name: "resolve",
3194
+ edges,
3195
+ build
3196
+ });
3197
+ node.services.add(service);
3198
+ });
3199
+ stopListening = reconciler.onDrained(() => {
3200
+ const failure = blockingFailure(service);
3201
+ if (failure) finish(() => fail(failure.error));
3202
+ });
3203
+ service.withdrawal.promise.then(() => finish(() => fail(new BuildAfterDisposeError("The container was disposed before resolve() got its dependencies."))));
3204
+ }));
3205
+ }
3206
+ // src/primitives/transaction.ts
3207
+ function readName(key, action) {
3208
+ let name;
3209
+ try {
3210
+ name = normalizeName(key);
3211
+ } catch (error) {
3212
+ throw new InvalidArgumentError(`Cannot read the key of ${action}.`, { cause: error });
3213
+ }
3214
+ if (typeof name !== "string" || name.length === 0) throw new InvalidArgumentError(`The key of ${action} must be a name or a key.`);
3215
+ return name;
3216
+ }
3217
+ function readDeclareOptions(name, options) {
3218
+ let dependsOn;
3219
+ let optional;
3220
+ try {
3221
+ dependsOn = options?.dependsOn;
3222
+ optional = options?.optional;
3223
+ } catch (error) {
3224
+ throw new InvalidArgumentError(`Cannot read the declare options of '${name}'.`, { cause: error });
3225
+ }
3226
+ if (optional !== void 0 && typeof optional !== "boolean") throw new InvalidArgumentError(`The optional option of '${name}' must be a boolean.`);
3227
+ return {
3228
+ dependsOn,
3229
+ optional: optional === true
3230
+ };
3231
+ }
3232
+ /** The service that a `tx.replace` names now. A stale registration, or a key without a registration, fails. */
3233
+ function replaceTarget(container, entry) {
3234
+ const target = entry.target ? currentService(entry.target) : slotOf(container, entry.name)();
3235
+ if (!target || !target.signals.present()) throw new StaleRegistrationError(`The transaction replaces '${entry.name}', but the container has no current registration for it.`);
3236
+ if (target.signals.phase() === "stopping") throw new ReplaceWhileStoppingError(`The transaction replaces '${entry.name}', but it is stopping.`);
3237
+ return target;
3238
+ }
3239
+ /** Checks the entries and creates the services of a commit. The services are not published. */
3240
+ function createMembers(transaction) {
3241
+ const { container } = transaction;
3242
+ assertActive(container, "commit a transaction");
3243
+ const incomplete = [...transaction.entries.values()].filter((entry) => !entry.definition && !entry.optional);
3244
+ if (incomplete.length > 0) throw new TransactionIncompleteError(`These declared services have no provide: ${incomplete.map((entry) => `'${entry.name}'`).join(", ")}.`);
3245
+ const entries = [...transaction.entries.values()].filter((entry) => entry.definition);
3246
+ const targets = /* @__PURE__ */ new Map();
3247
+ for (const entry of entries) if (entry.replace) targets.set(entry, replaceTarget(container, entry));
3248
+ else assertNoDuplicate(container, entry.name);
3249
+ const members = [];
3250
+ batch(() => {
3251
+ for (const entry of entries) {
3252
+ const definition = entry.definition;
3253
+ const target = targets.get(entry);
3254
+ const extra = {
3255
+ phase: "declared",
3256
+ transaction
3257
+ };
3258
+ if (!target) {
3259
+ members.push({
3260
+ entry,
3261
+ service: serviceOf(container, entry.name, definition, entry.exclusive, {
3262
+ ...extra,
3263
+ registration: entry.registration
3264
+ })
3265
+ });
3266
+ continue;
3267
+ }
3268
+ const registration = target.registration ?? createRegistration(container, entry.name, target.exclusive);
3269
+ const service = target.signals.phase() === "running" && !target.perConsumer ? replaceRunning(registration, target, definition, extra).service : serviceOf(container, entry.name, {
3270
+ ...definition,
3271
+ perConsumer: target.perConsumer
3272
+ }, target.exclusive, {
3273
+ ...extra,
3274
+ registration
3275
+ });
3276
+ members.push({
3277
+ entry,
3278
+ service,
3279
+ target
3280
+ });
3281
+ }
3282
+ });
3283
+ return members;
3284
+ }
3285
+ /** Publishes the members in one batch. A built member goes to `starting`, the others to `pending`. */
3286
+ function publishMembers(members, built) {
3287
+ batch(() => {
3288
+ for (const { service, target } of members) {
3289
+ service.transaction = void 0;
3290
+ const registration = service.registration;
3291
+ if (target && registration && service.swap) {
3292
+ cancelPending(registration);
3293
+ registration.pending = service;
3294
+ } else if (target && registration && target.perConsumer) moveConsumers(registration, target, service, {
3295
+ edges: service.edges,
3296
+ build: service.build,
3297
+ metadata: service.metadata,
3298
+ perConsumer: true,
3299
+ policy: service.policy
3300
+ });
3301
+ else if (target && registration) takeOver(registration, target, service);
3302
+ else {
3303
+ publishRegistration(service.container, service);
3304
+ if (registration) registration.current = service;
3305
+ }
3306
+ 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." });
3307
+ }
3308
+ });
3309
+ }
3310
+ async function commit(transaction) {
3311
+ const members = untracked(() => createMembers(transaction));
3312
+ const byName = new Map(members.map((member) => [member.service.name, member.service]));
3313
+ const order = orderMembers(byName);
3314
+ const outcome = await buildMembers(transaction, order, byName);
3315
+ if (outcome.kind !== "built") {
3316
+ await untracked(() => rollbackMembers(byName, outcome.built));
3317
+ throw outcome.kind === "failed" ? outcome.error : transaction.abortError;
3318
+ }
3319
+ untracked(() => publishMembers(members, new Set(outcome.built.keys())));
3320
+ transaction.state = "done";
3321
+ transaction.container.transactions.delete(transaction);
3322
+ startMembers(order, outcome.built);
3323
+ }
3324
+ function startCommit(transaction) {
3325
+ if (transaction.state !== "open") return Promise.reject(new InvalidArgumentError(`The transaction is ${transaction.state === "aborted" ? "aborted" : "already committed"}.`));
3326
+ transaction.state = "committing";
3327
+ const result = commit(transaction).catch((error) => {
3328
+ if (transaction.state !== "done") {
3329
+ transaction.state = "aborted";
3330
+ transaction.container.transactions.delete(transaction);
3331
+ }
3332
+ throw error;
3333
+ });
3334
+ transaction.settled = result.then(() => {}, () => {});
3335
+ return result;
3336
+ }
3337
+ function assertOpen(transaction, action) {
3338
+ if (transaction.state !== "open") throw new InvalidArgumentError(`Cannot ${action} after the transaction committed or aborted.`);
3339
+ }
3340
+ function entryOf$1(transaction, name, exclusive) {
3341
+ let entry = transaction.entries.get(name);
3342
+ if (!entry) {
3343
+ entry = {
3344
+ name,
3345
+ declared: void 0,
3346
+ optional: false,
3347
+ definition: void 0,
3348
+ replace: false,
3349
+ target: void 0,
3350
+ registration: void 0,
3351
+ exclusive
3352
+ };
3353
+ transaction.entries.set(name, entry);
3354
+ }
3355
+ return entry;
3356
+ }
3357
+ /**
3358
+ * Creates a transaction for `container`.
3359
+ *
3360
+ * `declare` reserves services. `provide` and `replace` add services. Nothing is visible to consumers before `commit()`.
3361
+ * `commit()` builds the services in dependency order and publishes them in one batch. Then the `start` hooks run.
3362
+ * A failed build publishes nothing. A disposal of the container during the transaction aborts it.
3363
+ */
3364
+ function transaction(container) {
3365
+ const node = nodeOf(container);
3366
+ assertActive(node, "create a transaction");
3367
+ const state = {
3368
+ container: node,
3369
+ state: "open",
3370
+ entries: /* @__PURE__ */ new Map(),
3371
+ abortError: void 0,
3372
+ controllers: /* @__PURE__ */ new Set(),
3373
+ settled: void 0
3374
+ };
3375
+ node.transactions.add(state);
3376
+ const declare = (key, options) => {
3377
+ assertOpen(state, "declare");
3378
+ const name = readName(key, "declare");
3379
+ const { dependsOn, optional } = readDeclareOptions(name, options);
3380
+ const entry = entryOf$1(state, name, isExclusiveKey(key));
3381
+ entry.declared = readEdges(name, dependsOn);
3382
+ entry.optional = optional;
3383
+ };
3384
+ const provide = ((key, option) => {
3385
+ assertOpen(state, "provide");
3386
+ const name = readName(key, "provide");
3387
+ const existing = state.entries.get(name);
3388
+ if (existing?.definition) throw new DuplicateServiceError(`The transaction already provides '${name}'.`);
3389
+ const read = readDefinition(name, option);
3390
+ const entry = entryOf$1(state, name, isExclusiveKey(key) || existing?.exclusive === true);
3391
+ const definition = read.edges.length === 0 && entry.declared ? {
3392
+ ...read,
3393
+ edges: entry.declared
3394
+ } : read;
3395
+ const registration = createRegistration(node, name, entry.exclusive);
3396
+ entry.definition = definition;
3397
+ entry.registration = registration;
3398
+ return registrationApi(registration);
3399
+ });
3400
+ const replace = ((key, option) => {
3401
+ assertOpen(state, "replace");
3402
+ const name = readName(key, "replace");
3403
+ if (state.entries.get(name)?.definition) throw new DuplicateServiceError(`The transaction already provides or replaces '${name}'.`);
3404
+ const target = registrationOf(key);
3405
+ const entry = entryOf$1(state, name, target?.exclusive ?? false);
3406
+ entry.definition = readDefinition(name, option, untracked(() => target ? currentService(target) : slotOf(node, name)()));
3407
+ entry.replace = true;
3408
+ entry.target = target;
3409
+ });
3410
+ return Object.freeze({
3411
+ declare,
3412
+ provide,
3413
+ replace,
3414
+ commit: () => untracked(() => startCommit(state)),
3415
+ abort: () => untracked(() => abortTransaction(state, new TransactionIncompleteError("The transaction was aborted before the commit completed.")))
3416
+ });
3417
+ }
3418
+ // src/queries/impact.ts
3419
+ const livePhases = /* @__PURE__ */ new Set([
3420
+ "building",
3421
+ "starting",
3422
+ "retrying",
3423
+ "running"
3424
+ ]);
3425
+ /** A service with an instance that a change can stop or rebuild. A new service that waits for its swap does not count. */
3426
+ function isLive(service) {
3427
+ return service.kind !== "resolver" && !service.swap && service.signals.instance() !== void 0 && livePhases.has(service.signals.phase());
3428
+ }
3429
+ function rootOf(container) {
3430
+ let root = container;
3431
+ while (root.parent) root = root.parent;
3432
+ return root;
3433
+ }
3434
+ /** The services of a `perConsumer` registration, with the registration. */
3435
+ function withConsumers(service) {
3436
+ return [service, ...service.signals.consumers().values()];
3437
+ }
3438
+ /** Lists the services that leave when `container` and its descendants are disposed. */
3439
+ function disposedServices(container) {
3440
+ return new Set(subtreeOf(container).flatMap((current) => [...current.services]).flatMap(withConsumers));
3441
+ }
3442
+ /** Lists the services that leave when `service` is withdrawn. */
3443
+ function withdrawnServices(service) {
3444
+ return new Set(withConsumers(service));
3445
+ }
3446
+ /**
3447
+ * Computes what a proposed change does, on a copy of the graph state. It reads the real graph and never writes it.
3448
+ *
3449
+ * - A dependent that loses a required dependency without an alternative provider stops. Its dependents follow.
3450
+ * - A dependent whose dependency gets another instance gets `dependency-change`. With a handler, it is notified. Without one, it rebuilds, and its dependents follow.
3451
+ * - An alternative provider is an ancestor registration that serves, or another `firstAvailable` candidate.
3452
+ */
3453
+ function computeImpact(scope, change) {
3454
+ const services = subtreeOf(rootOf(scope)).flatMap((container) => [...container.services]).filter(isLive);
3455
+ const stopped = new Set([...change.removed].filter(isLive));
3456
+ const rebuilt = /* @__PURE__ */ new Set();
3457
+ const notified = /* @__PURE__ */ new Set();
3458
+ const gone = (service) => change.removed.has(service) || stopped.has(service);
3459
+ const renewed = (service) => change.renewed.has(service) || rebuilt.has(service);
3460
+ /** Tells whether another candidate of the edge serves after the change. */
3461
+ const alternative = (consumer, keyNames) => keyNames.some((keyName) => candidatesOf(consumer.container, keyName, consumer).some((registration) => {
3462
+ const provider = serviceFor(registration, consumer.container);
3463
+ return provider !== void 0 && !gone(provider) && !gone(registration) && provider.signals.serving();
3464
+ }));
3465
+ let changed = true;
3466
+ while (changed) {
3467
+ changed = false;
3468
+ for (const service of services) {
3469
+ if (stopped.has(service) || change.removed.has(service)) continue;
3470
+ let lost = false;
3471
+ let identity = false;
3472
+ for (const edge of service.edges) {
3473
+ const resolution = resolutionOf(service, edge);
3474
+ if (resolution.kind !== "resolved") continue;
3475
+ const provider = resolution.provider;
3476
+ if (gone(provider)) {
3477
+ if (alternative(service, edge.candidates)) identity = true;
3478
+ else if (edge.optional) identity = true;
3479
+ else lost = true;
3480
+ } else if (renewed(provider)) identity = true;
3481
+ }
3482
+ if (lost) {
3483
+ rebuilt.delete(service);
3484
+ notified.delete(service);
3485
+ stopped.add(service);
3486
+ changed = true;
3487
+ } else if (identity && !rebuilt.has(service) && !notified.has(service)) {
3488
+ const instance = service.signals.instance();
3489
+ if (instance && instance.hooks["dependency-change"].length > 0) notified.add(service);
3490
+ else {
3491
+ rebuilt.add(service);
3492
+ changed = true;
3493
+ }
3494
+ }
3495
+ }
3496
+ }
3497
+ return {
3498
+ stopped,
3499
+ rebuilt,
3500
+ notified
3501
+ };
3502
+ }
3503
+ // src/queries/graph.ts
3504
+ const emptyMetadata = Object.freeze({});
3505
+ function readKey(key, action) {
3506
+ let name;
3507
+ try {
3508
+ name = normalizeName(key);
3509
+ } catch (error) {
3510
+ throw new InvalidArgumentError(`Cannot read the key of ${action}.`, { cause: error });
3511
+ }
3512
+ if (typeof name !== "string" || name.length === 0) throw new InvalidArgumentError(`The key of ${action} must be a name or a key.`);
3513
+ return name;
3514
+ }
3515
+ function entryOf(service) {
3516
+ const status = service.signals.effective();
3517
+ return Object.freeze({
3518
+ key: service.name,
3519
+ kind: service.kind === "invocation" ? "invocation" : "service",
3520
+ owner: service.container.owner,
3521
+ phase: service.signals.phase(),
3522
+ ...withoutUndefined({
3523
+ status: status?.status,
3524
+ instanceId: service.signals.instance()?.id,
3525
+ consumer: service.consumer
3526
+ }),
3527
+ metadata: service.metadata,
3528
+ exclusive: service.exclusive,
3529
+ perConsumer: service.perConsumer
3530
+ });
3531
+ }
3532
+ function declaredEntryOf(container, entry) {
3533
+ return Object.freeze({
3534
+ key: entry.name,
3535
+ kind: "service",
3536
+ owner: container.owner,
3537
+ phase: "declared",
3538
+ metadata: entry.definition?.metadata ?? emptyMetadata,
3539
+ exclusive: entry.exclusive,
3540
+ perConsumer: entry.definition?.perConsumer ?? false
3541
+ });
3542
+ }
3543
+ /** The services and invocations of the scope that are not `withdrawn`, in registration order. */
3544
+ function servicesOf(scope) {
3545
+ return subtreeOf(scope).flatMap((container) => [...container.services]).filter((service) => service.kind !== "resolver" && service.signals.phase() !== "withdrawn").sort((a, b) => a.seq - b.seq);
3546
+ }
3547
+ /** The keys that open transactions declared and did not publish yet. */
3548
+ function declaredOf(scope) {
3549
+ return subtreeOf(scope).flatMap((container) => [...container.transactions].filter((transaction) => transaction.state === "open").flatMap((transaction) => [...transaction.entries.values()].map((entry) => ({
3550
+ container,
3551
+ entry
3552
+ }))));
3553
+ }
3554
+ /** Finds the service of `name`: the provider that a consumer in `scope` sees, then the first service of that name in the scope. */
3555
+ function lookup(scope, name) {
3556
+ return findProvider(scope, name) ?? servicesOf(scope).find((service) => service.name === name);
3557
+ }
3558
+ /** The providers that the edges of `service` select now, served or not. */
3559
+ function targetsOf(service) {
3560
+ return service.edges.flatMap((edge) => edge.candidates.flatMap((keyName) => {
3561
+ const provider = findProvider(service.container, keyName, service);
3562
+ return provider ? [provider] : [];
3563
+ }));
3564
+ }
3565
+ function resolutionKind(service, edge) {
3566
+ const resolution = resolutionOf(service, edge);
3567
+ return resolution.kind === "missing" ? { kind: "missing" } : {
3568
+ kind: resolution.kind,
3569
+ provider: resolution.provider
3570
+ };
3571
+ }
3572
+ function dependencyOf(edge, resolution) {
3573
+ return Object.freeze({
3574
+ name: edge.name,
3575
+ keys: edge.candidates,
3576
+ optional: edge.optional,
3577
+ fallback: edge.fallback,
3578
+ conditional: edge.condition !== void 0,
3579
+ resolution: resolution.kind,
3580
+ ...withoutUndefined({ provider: resolution.provider && entryOf(resolution.provider) })
3581
+ });
3582
+ }
3583
+ /** The resolution of a declared edge, which has no computed resolution yet. */
3584
+ function declaredResolution(container, edge) {
3585
+ for (const keyName of edge.candidates) {
3586
+ const selection = selectProvider(container, keyName);
3587
+ if (!selection) continue;
3588
+ const provider = selection.provider ?? selection.registration;
3589
+ return {
3590
+ kind: provider.signals.serving() ? "resolved" : "not-running",
3591
+ provider
3592
+ };
3593
+ }
3594
+ return { kind: "missing" };
3595
+ }
3596
+ /**
3597
+ * Orders `services` so that each provider comes before its dependents. It is Tarjan's algorithm: each cycle comes out as one group.
3598
+ * Returns the groups in order.
3599
+ */
3600
+ function components(services) {
3601
+ const inScope = new Set(services);
3602
+ const index = /* @__PURE__ */ new Map();
3603
+ const low = /* @__PURE__ */ new Map();
3604
+ const stack = [];
3605
+ const onStack = /* @__PURE__ */ new Set();
3606
+ const groups = [];
3607
+ let counter = 0;
3608
+ const visit = (service) => {
3609
+ index.set(service, counter);
3610
+ low.set(service, counter);
3611
+ counter++;
3612
+ stack.push(service);
3613
+ onStack.add(service);
3614
+ for (const target of targetsOf(service)) {
3615
+ if (!inScope.has(target)) continue;
3616
+ if (!index.has(target)) {
3617
+ visit(target);
3618
+ low.set(service, Math.min(low.get(service), low.get(target)));
3619
+ } else if (onStack.has(target)) low.set(service, Math.min(low.get(service), index.get(target)));
3620
+ }
3621
+ if (low.get(service) === index.get(service)) {
3622
+ const group = [];
3623
+ let member;
3624
+ do {
3625
+ member = stack.pop();
3626
+ onStack.delete(member);
3627
+ group.push(member);
3628
+ } while (member !== service);
3629
+ groups.push(group.sort((a, b) => a.seq - b.seq));
3630
+ }
3631
+ };
3632
+ for (const service of services) if (!index.has(service)) visit(service);
3633
+ return groups;
3634
+ }
3635
+ /** The services of `roots` and their transitive dependencies. */
3636
+ function closure(roots) {
3637
+ const seen = /* @__PURE__ */ new Set();
3638
+ const queue = [...roots];
3639
+ while (queue.length > 0) {
3640
+ const service = queue.shift();
3641
+ if (seen.has(service)) continue;
3642
+ seen.add(service);
3643
+ queue.push(...targetsOf(service));
3644
+ }
3645
+ return [...seen].sort((a, b) => a.seq - b.seq);
3646
+ }
3647
+ function sorted(services) {
3648
+ return Object.freeze([...services].sort((a, b) => a.seq - b.seq).map(entryOf));
3649
+ }
3650
+ function proposedChange(scope, change) {
3651
+ let kind;
3652
+ let key;
3653
+ let container;
3654
+ try {
3655
+ kind = change.kind;
3656
+ key = "key" in change ? change.key : void 0;
3657
+ container = "container" in change ? change.container : void 0;
3658
+ } catch (error) {
3659
+ throw new InvalidArgumentError("Cannot read the impact change.", { cause: error });
3660
+ }
3661
+ switch (kind) {
3662
+ case "withdraw":
3663
+ case "replace": {
3664
+ const service = lookup(scope, readKey(key, "impact"));
3665
+ if (!service) return {
3666
+ removed: /* @__PURE__ */ new Set(),
3667
+ renewed: /* @__PURE__ */ new Set()
3668
+ };
3669
+ const registration = service.template ?? service;
3670
+ return kind === "withdraw" ? {
3671
+ removed: withdrawnServices(registration),
3672
+ renewed: /* @__PURE__ */ new Set()
3673
+ } : {
3674
+ removed: /* @__PURE__ */ new Set(),
3675
+ renewed: withdrawnServices(registration)
3676
+ };
3677
+ }
3678
+ case "dispose": return {
3679
+ removed: disposedServices(nodeOf(container)),
3680
+ renewed: /* @__PURE__ */ new Set()
3681
+ };
3682
+ default: throw new InvalidArgumentError(`The impact change kind must be 'withdraw', 'replace', or 'dispose'. Got ${String(kind)}.`);
3683
+ }
3684
+ }
3685
+ /**
3686
+ * Returns the graph queries of `container`. They cover the container and its descendants. Each query is a pure read.
3687
+ *
3688
+ * A key resolves as for a consumer in the container: from the container, then from its ancestors. A key that only a descendant has resolves to the first such service.
3689
+ */
3690
+ function graph(container) {
3691
+ const scope = nodeOf(container);
3692
+ return Object.freeze({
3693
+ services: () => untracked(() => Object.freeze([...servicesOf(scope).map(entryOf), ...declaredOf(scope).map(({ container: owner, entry }) => declaredEntryOf(owner, entry))])),
3694
+ dependencies: (key) => untracked(() => {
3695
+ const name = readKey(key, "dependencies");
3696
+ const service = lookup(scope, name);
3697
+ if (service) return Object.freeze(service.edges.map((edge) => dependencyOf(edge, resolutionKind(service, edge))));
3698
+ const declared = declaredOf(scope).find(({ entry }) => entry.name === name);
3699
+ const edges = declared?.entry.definition?.edges ?? declared?.entry.declared ?? [];
3700
+ return Object.freeze(edges.map((edge) => dependencyOf(edge, declaredResolution(declared.container, edge))));
3701
+ }),
3702
+ dependents: (key) => untracked(() => {
3703
+ const target = lookup(scope, readKey(key, "dependents"));
3704
+ if (!target) return Object.freeze([]);
3705
+ const registration = target.template ?? target;
3706
+ return Object.freeze(servicesOf(scope).filter((service) => service.edges.some((edge) => {
3707
+ const resolution = resolutionOf(service, edge);
3708
+ return resolution.kind !== "missing" && (resolution.provider === target || (resolution.provider.template ?? resolution.provider) === registration);
3709
+ })).map(entryOf));
3710
+ }),
3711
+ order: (keys) => untracked(() => {
3712
+ const services = keys === void 0 ? servicesOf(scope) : closure(keys.flatMap((key) => {
3713
+ const service = lookup(scope, readKey(key, "order"));
3714
+ return service ? [service] : [];
3715
+ }));
3716
+ return Object.freeze(components(services).flat().map(entryOf));
3717
+ }),
3718
+ cycles: () => untracked(() => Object.freeze(components(servicesOf(scope)).filter((group) => group.length > 1).map((group) => Object.freeze(group.map(entryOf))))),
3719
+ impact: (change) => untracked(() => {
3720
+ const result = computeImpact(scope, proposedChange(scope, change));
3721
+ return Object.freeze({
3722
+ stopped: sorted(result.stopped),
3723
+ rebuilt: sorted(result.rebuilt),
3724
+ notified: sorted(result.notified)
3725
+ });
3726
+ }),
3727
+ explain: (key) => untracked(() => {
3728
+ const name = readKey(key, "explain");
3729
+ const service = lookup(scope, name);
3730
+ if (service) return explainService(service);
3731
+ const declared = declaredOf(scope).find(({ entry }) => entry.name === name);
3732
+ return declared && Object.freeze({
3733
+ name,
3734
+ kind: "service",
3735
+ owner: declared.container.owner,
3736
+ phase: "declared",
3737
+ blockers: Object.freeze([])
3738
+ });
3739
+ })
3740
+ });
3741
+ }
3742
+ // src/queries/listeners.ts
3743
+ /**
3744
+ * Returns the current listeners for `eventKey` in `container` and its descendants, in registration order.
3745
+ *
3746
+ * Each entry has the handler, the owner record of the container that owns it, and its metadata.
3747
+ * `H` is the handler type that the caller expects. injeca does not check it.
3748
+ */
3749
+ function listeners(container, eventKey) {
3750
+ const id = readEventId(eventKey);
3751
+ return Object.freeze(subtreeOf(nodeOf(container)).filter((node) => node.state !== "disposed").flatMap((node) => (node.listeners.get(id) ?? []).map((record) => ({
3752
+ record,
3753
+ owner: node.owner
3754
+ }))).sort((a, b) => a.record.seq - b.record.seq).map(({ record, owner }) => Object.freeze({
3755
+ handler: record.handler,
3756
+ owner,
3757
+ metadata: record.metadata
3758
+ })));
3759
+ }
3760
+ // src/global.ts
3761
+ let globalContainer = createContainer();
3762
+ function setLogger(logger) {
3763
+ globalContainer.logger = logger;
3764
+ }
3765
+ /** Returns the root container of the global functions. Pass it as `parent` to make a child container. */
3766
+ function getContainer() {
3767
+ return globalContainer;
3768
+ }
3769
+ /** Replaces the global container with a new one. `getContainer()` then returns the new one. It does not stop or dispose the old container. */
3770
+ function resetContainer() {
3771
+ globalContainer = createContainer();
3772
+ }
3773
+ function provide(nameOrOption, option) {
3774
+ return provideIn(globalContainer, 3, nameOrOption, option);
3775
+ }
3776
+ function invoke(option) {
3777
+ return invokeIn(globalContainer, option);
3778
+ }
3779
+ function resolve(dependsOn) {
3780
+ return resolve$1(globalContainer, dependsOn);
3781
+ }
3782
+ function start(options) {
3783
+ return start$1(globalContainer, options);
3784
+ }
3785
+ function stop() {
3786
+ return stop$1(globalContainer);
3787
+ }
3788
+ export { assertActive as A, firstAvailable as C, statusFields as D, where as E, createDefaultLogger as F, createLoggLogger as I, createNoopLogger as L, findProvider as M, untracked as N, reportCallbackError as O, withOwner as P, defineKey as S, optional as T, invoke$1 as _, resolve as a, effect$1 as b, stop as c, transaction as d, resolve$1 as f, stop$1 as g, start$1 as h, resetContainer as i, nodeOf as j, withoutUndefined as k, listeners as l, listen as m, invoke as n, setLogger as o, provide$1 as p, provide as r, start as s, getContainer as t, graph as u, createContainer as v, normalizeName as w, systemClock as x, dispose as y };
3789
+
3790
+ //# sourceMappingURL=global-C0Nk0pU8.mjs.map