injeca 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +251 -14
- package/dist/decision-BXZu3N1O.mjs +1356 -0
- package/dist/decision-BXZu3N1O.mjs.map +1 -0
- package/dist/{global-C0Nk0pU8.mjs → global-B5N-d-TO.mjs} +1024 -1214
- package/dist/global-B5N-d-TO.mjs.map +1 -0
- package/dist/{global-B9Bk2EG2.d.mts → global-DPfe26g5.d.mts} +81 -17
- package/dist/global.d.mts +1 -1
- package/dist/global.mjs +1 -1
- package/dist/{index-DXguNx2M.d.mts → index-DTYp044J.d.mts} +139 -19
- package/dist/index.d.mts +40 -15
- package/dist/index.mjs +55 -23
- package/dist/index.mjs.map +1 -1
- package/dist/testing.d.mts +31 -1
- package/dist/testing.mjs +60 -2
- package/dist/testing.mjs.map +1 -1
- package/package.json +5 -5
- package/dist/decision-odiPb2Au.mjs +0 -194
- package/dist/decision-odiPb2Au.mjs.map +0 -1
- package/dist/global-C0Nk0pU8.mjs.map +0 -1
|
@@ -0,0 +1,1356 @@
|
|
|
1
|
+
import { computed, effect, endBatch, setActiveSub, signal, startBatch } from "alien-signals";
|
|
2
|
+
import { isUndefined, omitBy } from "es-toolkit";
|
|
3
|
+
// src/diagnostics/errors.ts
|
|
4
|
+
/**
|
|
5
|
+
* The base class of all injeca errors. Use `code` to identify an error. The message text is not stable.
|
|
6
|
+
*/
|
|
7
|
+
var InjecaError = class extends Error {
|
|
8
|
+
code;
|
|
9
|
+
constructor(code, message, options) {
|
|
10
|
+
super(message, options);
|
|
11
|
+
this.name = "InjecaError";
|
|
12
|
+
this.code = code;
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
/** A started container already has a current registration for the key. */
|
|
16
|
+
var DuplicateServiceError = class extends InjecaError {
|
|
17
|
+
constructor(message, options) {
|
|
18
|
+
super("duplicate-service", message, options);
|
|
19
|
+
this.name = "DuplicateServiceError";
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
/** A registration is no longer the current registration of its service. */
|
|
23
|
+
var StaleRegistrationError = class extends InjecaError {
|
|
24
|
+
constructor(message, options) {
|
|
25
|
+
super("stale-registration", message, options);
|
|
26
|
+
this.name = "StaleRegistrationError";
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
/** A hook returned a decision from another run, or a kind that the hook does not accept. */
|
|
30
|
+
var InvalidDecisionError = class extends InjecaError {
|
|
31
|
+
constructor(message, options) {
|
|
32
|
+
super("invalid-decision", message, options);
|
|
33
|
+
this.name = "InvalidDecisionError";
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
/** A caller used a container or an instance after its disposal started. */
|
|
37
|
+
var BuildAfterDisposeError = class extends InjecaError {
|
|
38
|
+
constructor(message, options) {
|
|
39
|
+
super("build-after-dispose", message, options);
|
|
40
|
+
this.name = "BuildAfterDisposeError";
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
/** `on` was called after `build` settled. */
|
|
44
|
+
var HookOutsideBuildError = class extends InjecaError {
|
|
45
|
+
constructor(message, options) {
|
|
46
|
+
super("hook-outside-build", message, options);
|
|
47
|
+
this.name = "HookOutsideBuildError";
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
/** A transaction commit found a declared service without `provide`. */
|
|
51
|
+
var TransactionIncompleteError = class extends InjecaError {
|
|
52
|
+
constructor(message, options) {
|
|
53
|
+
super("transaction-incomplete", message, options);
|
|
54
|
+
this.name = "TransactionIncompleteError";
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
/** A `start` or `stop` hook did not settle before its configured timeout. */
|
|
58
|
+
var HookTimeoutError = class extends InjecaError {
|
|
59
|
+
hook;
|
|
60
|
+
timeoutMs;
|
|
61
|
+
constructor(message, hook, timeoutMs, options) {
|
|
62
|
+
super("hook-timeout", message, options);
|
|
63
|
+
this.name = "HookTimeoutError";
|
|
64
|
+
this.hook = hook;
|
|
65
|
+
this.timeoutMs = timeoutMs;
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
/** `replace` was called on a service in `stopping`. */
|
|
69
|
+
var ReplaceWhileStoppingError = class extends InjecaError {
|
|
70
|
+
constructor(message, options) {
|
|
71
|
+
super("replace-while-stopping", message, options);
|
|
72
|
+
this.name = "ReplaceWhileStoppingError";
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
/** `start()` did not reach `running` for every service before its configured timeout. */
|
|
76
|
+
var StartTimeoutError = class extends InjecaError {
|
|
77
|
+
/** One entry for each service or invocation that is not `running`. */
|
|
78
|
+
explanations;
|
|
79
|
+
constructor(message, explanations, options) {
|
|
80
|
+
super("start-timeout", message, options);
|
|
81
|
+
this.name = "StartTimeoutError";
|
|
82
|
+
this.explanations = explanations;
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
/** `waitFor()` found the service in `failed`, or waiting for a required provider in `failed`. `cause` is the failure. */
|
|
86
|
+
var ServiceFailedError = class extends InjecaError {
|
|
87
|
+
/** The key name that the caller waited for. */
|
|
88
|
+
key;
|
|
89
|
+
constructor(message, key, options) {
|
|
90
|
+
super("service-failed", message, options);
|
|
91
|
+
this.name = "ServiceFailedError";
|
|
92
|
+
this.key = key;
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
/** `waitFor()` or `resolve()` did not match before its configured timeout. */
|
|
96
|
+
var WaitTimeoutError = class extends InjecaError {
|
|
97
|
+
constructor(message, options) {
|
|
98
|
+
super("wait-timeout", message, options);
|
|
99
|
+
this.name = "WaitTimeoutError";
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
/** A caller passed a value with the wrong shape, or reading a caller-owned property failed. */
|
|
103
|
+
var InvalidArgumentError = class extends InjecaError {
|
|
104
|
+
constructor(message, options) {
|
|
105
|
+
super("invalid-argument", message, options);
|
|
106
|
+
this.name = "InvalidArgumentError";
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
// src/helpers/signals.ts
|
|
110
|
+
/**
|
|
111
|
+
* Runs `fn` with tracking paused.
|
|
112
|
+
*
|
|
113
|
+
* 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.
|
|
114
|
+
* Code inside an entry point reads signals directly. Only the bodies of `computed` and `effect` track.
|
|
115
|
+
* A promise continuation starts with no active subscriber, so it needs no extra pause.
|
|
116
|
+
*/
|
|
117
|
+
function untracked(fn) {
|
|
118
|
+
const previous = setActiveSub(void 0);
|
|
119
|
+
try {
|
|
120
|
+
return fn();
|
|
121
|
+
} finally {
|
|
122
|
+
setActiveSub(previous);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
/** Writes signals in one batch. Effects see one consistent change. */
|
|
126
|
+
function batch(fn) {
|
|
127
|
+
startBatch();
|
|
128
|
+
try {
|
|
129
|
+
fn();
|
|
130
|
+
} finally {
|
|
131
|
+
endBatch();
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
/** Runs `fn` with `owner` as the active subscriber, so effects that `fn` creates belong to `owner`. */
|
|
135
|
+
function withOwner(owner, fn) {
|
|
136
|
+
const previous = setActiveSub(owner);
|
|
137
|
+
try {
|
|
138
|
+
return fn();
|
|
139
|
+
} finally {
|
|
140
|
+
setActiveSub(previous);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
// src/helpers/error-from.ts
|
|
144
|
+
/**
|
|
145
|
+
* Makes one error from collected errors. One error keeps its identity. More than one error becomes an `AggregateError`.
|
|
146
|
+
*
|
|
147
|
+
* The result is `unknown`, because a thrown value is not always an `Error`.
|
|
148
|
+
*/
|
|
149
|
+
function errorFrom(errors, message) {
|
|
150
|
+
return errors.length === 1 ? errors[0] : new AggregateError(errors, message);
|
|
151
|
+
}
|
|
152
|
+
/** The message of a thrown value: `message` of an `Error` or a `DOMException`, else `String(value)`. */
|
|
153
|
+
function messageOf(value) {
|
|
154
|
+
if (typeof value === "object" && value !== null && typeof value.message === "string") return value.message;
|
|
155
|
+
return String(value);
|
|
156
|
+
}
|
|
157
|
+
// src/graph/resolvers/exports.ts
|
|
158
|
+
/** Returns the signal of the exported services of `keyName` that the child containers of `container` publish. It creates the signal on first read, so that resolution can track it. */
|
|
159
|
+
function exportsOf(container, keyName) {
|
|
160
|
+
let exports = container.signals.exports.get(keyName);
|
|
161
|
+
if (!exports) {
|
|
162
|
+
exports = signal(Object.freeze([]));
|
|
163
|
+
container.signals.exports.set(keyName, exports);
|
|
164
|
+
}
|
|
165
|
+
return exports;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* The container whose resolution area a registration belongs to.
|
|
169
|
+
* An exported registration belongs to the area of its parent container. A registration of a root container has no parent, so `export` has no effect there.
|
|
170
|
+
*/
|
|
171
|
+
function areaOf(service) {
|
|
172
|
+
return service.exported && service.container.parent || service.container;
|
|
173
|
+
}
|
|
174
|
+
// src/graph/resolvers/exclusive.ts
|
|
175
|
+
/** Tells whether `ancestor` is `container` or an ancestor of it, with no container in between that isolates `keyName`. */
|
|
176
|
+
function reaches(container, ancestor, keyName) {
|
|
177
|
+
for (let current = container; current; current = current.parent) {
|
|
178
|
+
if (current === ancestor) return true;
|
|
179
|
+
if (current.isolated.has(keyName)) return false;
|
|
180
|
+
}
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Tells whether two containers share a resolution area of `keyName`.
|
|
185
|
+
* A resolution area is the container that has the key, with its descendants that do not isolate the key.
|
|
186
|
+
* For an exported registration, the caller passes the parent container. See `areaOf`.
|
|
187
|
+
*/
|
|
188
|
+
function sameArea(a, b, keyName) {
|
|
189
|
+
return reaches(a, b, keyName) || reaches(b, a, keyName);
|
|
190
|
+
}
|
|
191
|
+
/** The published services of `keyName` in all containers of the runtime. */
|
|
192
|
+
function declarationsOf(runtime, keyName) {
|
|
193
|
+
let declarations = runtime.declarations.get(keyName);
|
|
194
|
+
if (!declarations) {
|
|
195
|
+
declarations = signal(/* @__PURE__ */ new Set());
|
|
196
|
+
runtime.declarations.set(keyName, declarations);
|
|
197
|
+
}
|
|
198
|
+
return declarations;
|
|
199
|
+
}
|
|
200
|
+
function addDeclaration(service) {
|
|
201
|
+
const declarations = declarationsOf(service.container.runtime, service.name);
|
|
202
|
+
if (!declarations().has(service)) declarations(/* @__PURE__ */ new Set([...declarations(), service]));
|
|
203
|
+
}
|
|
204
|
+
function removeDeclaration(service) {
|
|
205
|
+
const declarations = declarationsOf(service.container.runtime, service.name);
|
|
206
|
+
if (declarations().has(service)) declarations(new Set([...declarations()].filter((other) => other !== service)));
|
|
207
|
+
}
|
|
208
|
+
function sameList(a, b) {
|
|
209
|
+
return a.length === b.length && a.every((service, index) => service === b[index]);
|
|
210
|
+
}
|
|
211
|
+
const none = Object.freeze([]);
|
|
212
|
+
/**
|
|
213
|
+
* Creates the conflict of a service: the other present declarations of its key in the same resolution area, when the key is exclusive.
|
|
214
|
+
*
|
|
215
|
+
* The result depends only on the set of declarations. The order of the registrations has no effect.
|
|
216
|
+
* Only one registration of a key exists in one container, so a rival is always in another container.
|
|
217
|
+
* An exported registration counts in the resolution area of its parent container. So two sibling exports of one exclusive key conflict, and an export conflicts with a registration of the parent.
|
|
218
|
+
* A service of a `perConsumer` registration has no conflict of its own. It follows the conflict of its registration.
|
|
219
|
+
*/
|
|
220
|
+
function createRivals(service) {
|
|
221
|
+
if (service.template) return () => service.template?.signals.rivals() ?? none;
|
|
222
|
+
if (service.kind !== "service") return () => none;
|
|
223
|
+
return computed((previous) => {
|
|
224
|
+
const rivals = [...declarationsOf(service.container.runtime, service.name)()].filter((other) => other.container !== service.container && other.signals.present() && (service.exclusive || other.exclusive) && sameArea(areaOf(service), areaOf(other), service.name)).sort((a, b) => a.seq - b.seq);
|
|
225
|
+
return previous && sameList(previous, rivals) ? previous : rivals;
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
/** Tells whether the key of the service has more than one declaration in its resolution area. */
|
|
229
|
+
function inConflict(service) {
|
|
230
|
+
return service.signals.rivals().length > 0;
|
|
231
|
+
}
|
|
232
|
+
// src/graph/resolvers/resolvers.ts
|
|
233
|
+
/** Returns the slot signal of `keyName` in `container`. It creates the slot on first read, so that resolution can track it. */
|
|
234
|
+
function slotOf(container, keyName) {
|
|
235
|
+
let slot = container.signals.slots.get(keyName);
|
|
236
|
+
if (!slot) {
|
|
237
|
+
slot = signal(void 0);
|
|
238
|
+
container.signals.slots.set(keyName, slot);
|
|
239
|
+
}
|
|
240
|
+
return slot;
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Lists the registrations of `keyName` that a consumer in `container` can see, nearest first.
|
|
244
|
+
*
|
|
245
|
+
* At each level, from `container` up through its ancestors, the candidates are the registration of that container, then the exports of its child containers in creation order.
|
|
246
|
+
* The walk ends at a container that isolates the key. So an isolated key also hides the exports of the siblings.
|
|
247
|
+
* A service does not resolve itself. A consumer service of a `perConsumer` registration does not resolve its registration.
|
|
248
|
+
*/
|
|
249
|
+
function candidatesOf(container, keyName, consumer) {
|
|
250
|
+
const candidates = [];
|
|
251
|
+
const add = (candidate) => {
|
|
252
|
+
if (candidate && candidate !== consumer && candidate !== consumer?.template && !candidates.includes(candidate)) candidates.push(candidate);
|
|
253
|
+
};
|
|
254
|
+
for (let current = container; current; current = current.parent) {
|
|
255
|
+
add(slotOf(current, keyName)());
|
|
256
|
+
for (const exported of exportsOf(current, keyName)()) add(exported);
|
|
257
|
+
if (current.isolated.has(keyName)) break;
|
|
258
|
+
}
|
|
259
|
+
return candidates;
|
|
260
|
+
}
|
|
261
|
+
/** The service that serves `registration` for a consumer in `container`. A `perConsumer` registration has one for each consumer container, or none yet. */
|
|
262
|
+
function serviceFor(registration, container) {
|
|
263
|
+
return registration.perConsumer ? registration.signals.consumers().get(container) : registration;
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Selects the provider of `keyName` for a consumer in `container`.
|
|
267
|
+
*
|
|
268
|
+
* A nearer registration shadows an ancestor only while it is `running`. Until then, the first serving registration wins.
|
|
269
|
+
* If no registration serves, the nearest one is selected. So shadowing never leaves a gap.
|
|
270
|
+
*/
|
|
271
|
+
function selectProvider(container, keyName, consumer) {
|
|
272
|
+
const candidates = candidatesOf(container, keyName, consumer);
|
|
273
|
+
for (const registration of candidates) {
|
|
274
|
+
const provider = serviceFor(registration, container);
|
|
275
|
+
if (provider?.signals.serving()) return {
|
|
276
|
+
registration,
|
|
277
|
+
provider
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
const [nearest] = candidates;
|
|
281
|
+
return nearest ? {
|
|
282
|
+
registration: nearest,
|
|
283
|
+
provider: serviceFor(nearest, container)
|
|
284
|
+
} : void 0;
|
|
285
|
+
}
|
|
286
|
+
/** Finds the service for `keyName` that a consumer in `container` sees. A `perConsumer` registration without a service for the container gives the registration. */
|
|
287
|
+
function findProvider(container, keyName, consumer) {
|
|
288
|
+
const selection = selectProvider(container, keyName, consumer);
|
|
289
|
+
return selection && (selection.provider ?? selection.registration);
|
|
290
|
+
}
|
|
291
|
+
/** `ready` and `degraded` count as available. A provider outside `running` has no status and is not available. */
|
|
292
|
+
function isAvailable(provider) {
|
|
293
|
+
const status = provider.signals.effective();
|
|
294
|
+
return status !== void 0 && status.status !== "unavailable";
|
|
295
|
+
}
|
|
296
|
+
/** The current resolution of the edge. */
|
|
297
|
+
function resolutionOf(service, edge) {
|
|
298
|
+
return service.signals.resolutions.get(edge.name)?.() ?? { kind: "missing" };
|
|
299
|
+
}
|
|
300
|
+
/** The instance of the edge, when it resolves. */
|
|
301
|
+
function resolvedInstance(service, edge) {
|
|
302
|
+
const resolution = resolutionOf(service, edge);
|
|
303
|
+
return resolution.kind === "resolved" ? resolution.instance : void 0;
|
|
304
|
+
}
|
|
305
|
+
/** Tells whether each required edge resolves to an available provider. A pending service builds only then. */
|
|
306
|
+
function requiredAvailable(service) {
|
|
307
|
+
return service.edges.every((edge) => {
|
|
308
|
+
if (edge.optional) return true;
|
|
309
|
+
const resolution = resolutionOf(service, edge);
|
|
310
|
+
return resolution.kind === "resolved" && isAvailable(resolution.provider);
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
/** Tells whether a required edge no longer resolves: its provider was withdrawn, or it does not run. */
|
|
314
|
+
function requiredWithdrawn(service) {
|
|
315
|
+
return service.edges.some((edge) => !edge.optional && resolvedInstance(service, edge) === void 0);
|
|
316
|
+
}
|
|
317
|
+
/** Tells whether the current resolution of a required edge differs from the bindings of `instance`. */
|
|
318
|
+
function requiredChanged(service, instance) {
|
|
319
|
+
return service.edges.some((edge) => !edge.optional && resolvedInstance(service, edge) !== instance.bindings.get(edge.name));
|
|
320
|
+
}
|
|
321
|
+
/** A build is discarded when its target is lost, a required dependency changed during the build, or the key went into conflict. */
|
|
322
|
+
function buildLost(service, instance) {
|
|
323
|
+
return !service.signals.present() || !service.signals.active() || service.signals.rivals().length > 0 || requiredChanged(service, instance);
|
|
324
|
+
}
|
|
325
|
+
/** The identity of the dependency set: the revision and the instance id of each resolved required dependency. */
|
|
326
|
+
function epochOf(service, pick) {
|
|
327
|
+
const ids = service.edges.filter((edge) => !edge.optional).map((edge) => `${edge.name}=${pick(edge)?.id ?? ""}`);
|
|
328
|
+
return [String(service.revision), ...ids].join("|");
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* Finds the failure that blocks a pending service: a required provider in `failed`, directly or through pending providers.
|
|
332
|
+
*/
|
|
333
|
+
function blockingFailure(service, visited = /* @__PURE__ */ new Set()) {
|
|
334
|
+
visited.add(service);
|
|
335
|
+
for (const edge of service.edges) {
|
|
336
|
+
if (edge.optional) continue;
|
|
337
|
+
const resolution = resolutionOf(service, edge);
|
|
338
|
+
if (resolution.kind !== "not-running" || visited.has(resolution.provider)) continue;
|
|
339
|
+
const provider = resolution.provider;
|
|
340
|
+
const phase = provider.signals.phase();
|
|
341
|
+
if (phase === "failed") return provider.failure;
|
|
342
|
+
if (phase === "pending") {
|
|
343
|
+
const failure = blockingFailure(provider, visited);
|
|
344
|
+
if (failure) return failure;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
// src/graph/nodes/container.ts
|
|
349
|
+
const nodes = /* @__PURE__ */ new WeakMap();
|
|
350
|
+
/** Connects the public container object to its node. `createContainer` calls it once. */
|
|
351
|
+
function registerNode(container, node) {
|
|
352
|
+
nodes.set(container, node);
|
|
353
|
+
}
|
|
354
|
+
/** Returns the internal node of a container from `createContainer`. */
|
|
355
|
+
function nodeOf(container) {
|
|
356
|
+
const node = typeof container === "object" && container !== null ? nodes.get(container) : void 0;
|
|
357
|
+
if (!node) throw new InvalidArgumentError("The value is not a container from createContainer.");
|
|
358
|
+
return node;
|
|
359
|
+
}
|
|
360
|
+
function assertActive(container, action) {
|
|
361
|
+
if (container.state !== "active") throw new BuildAfterDisposeError(`Cannot ${action} after the container disposal started.`);
|
|
362
|
+
}
|
|
363
|
+
/** Lists `container` and its descendants that were started, through started containers only. `start()` and `stop()` apply to them. */
|
|
364
|
+
function affectedContainers(container) {
|
|
365
|
+
const result = [container];
|
|
366
|
+
for (const child of container.children) if (child.enrolled) result.push(...affectedContainers(child));
|
|
367
|
+
return result;
|
|
368
|
+
}
|
|
369
|
+
/** Lists `container` and all its descendants. */
|
|
370
|
+
function subtreeOf(container) {
|
|
371
|
+
return [container, ...[...container.children].flatMap(subtreeOf)];
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* Adds an exported service to the exports of its parent container, in container creation order.
|
|
375
|
+
* It takes the place of an earlier service of the same container, for example at the swap of a replacement.
|
|
376
|
+
*/
|
|
377
|
+
function addExport(service) {
|
|
378
|
+
const parent = service.container.parent;
|
|
379
|
+
if (!service.exported || !parent) return;
|
|
380
|
+
const exports = exportsOf(parent, service.name);
|
|
381
|
+
const current = exports();
|
|
382
|
+
if (current.includes(service)) return;
|
|
383
|
+
exports(Object.freeze([...current.filter((other) => other.container !== service.container), service].sort((a, b) => a.container.seq - b.container.seq)));
|
|
384
|
+
}
|
|
385
|
+
function removeExport(service) {
|
|
386
|
+
const parent = service.container.parent;
|
|
387
|
+
if (!service.exported || !parent) return;
|
|
388
|
+
const exports = exportsOf(parent, service.name);
|
|
389
|
+
if (exports().includes(service)) exports(Object.freeze(exports().filter((other) => other !== service)));
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* Makes `service` visible to consumers: the slot of its key, or the consumer map of its `perConsumer` registration.
|
|
393
|
+
* An exported service is also visible in the resolution area of the parent container.
|
|
394
|
+
* An exclusive conflict counts each published service with a key.
|
|
395
|
+
*/
|
|
396
|
+
function publishService(service) {
|
|
397
|
+
const { template } = service;
|
|
398
|
+
if (template) {
|
|
399
|
+
const consumers = template.signals.consumers();
|
|
400
|
+
if (consumers.get(service.container) !== service) template.signals.consumers(new Map([...consumers, [service.container, service]]));
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
slotOf(service.container, service.name)(service);
|
|
404
|
+
addExport(service);
|
|
405
|
+
addDeclaration(service);
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* Clears the slot and marks the service absent. The reconciler stops it after its holders release it.
|
|
409
|
+
* A `perConsumer` registration also releases the service of each consumer container.
|
|
410
|
+
*/
|
|
411
|
+
function releaseService(service) {
|
|
412
|
+
service.signals.present(false);
|
|
413
|
+
if (service.kind !== "service" || service.template) return;
|
|
414
|
+
for (const consumer of service.signals.consumers().values()) consumer.signals.present(false);
|
|
415
|
+
const slot = slotOf(service.container, service.name);
|
|
416
|
+
if (slot() === service) slot(void 0);
|
|
417
|
+
removeExport(service);
|
|
418
|
+
}
|
|
419
|
+
/** Waits until `service` is `withdrawn`. Rejects with its teardown errors. */
|
|
420
|
+
async function awaitWithdrawal(service) {
|
|
421
|
+
const errors = await service.withdrawal.promise;
|
|
422
|
+
if (errors.length > 0) throw errorFrom(errors, `Withdrawal of '${service.name}' failed.`);
|
|
423
|
+
}
|
|
424
|
+
/** Releases a service or an invocation. Returns `false` when it is already withdrawn. */
|
|
425
|
+
function releasePresent(service) {
|
|
426
|
+
return untracked(() => {
|
|
427
|
+
if (!service.signals.present()) return false;
|
|
428
|
+
batch(() => releaseService(service));
|
|
429
|
+
return true;
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
// src/helpers/deferred.ts
|
|
433
|
+
function createDeferred() {
|
|
434
|
+
let resolve;
|
|
435
|
+
return {
|
|
436
|
+
promise: new Promise((done) => {
|
|
437
|
+
resolve = done;
|
|
438
|
+
}),
|
|
439
|
+
resolve
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
// src/helpers/without-undefined.ts
|
|
443
|
+
/** Copies `fields` without the fields that are `undefined`. Spread the result to add optional fields to a record. */
|
|
444
|
+
function withoutUndefined(fields) {
|
|
445
|
+
return omitBy(fields, isUndefined);
|
|
446
|
+
}
|
|
447
|
+
// src/graph/observers.ts
|
|
448
|
+
function within(container, ancestor) {
|
|
449
|
+
for (let current = container; current; current = current.parent) if (current === ancestor) return true;
|
|
450
|
+
return false;
|
|
451
|
+
}
|
|
452
|
+
/** The active observers of `container`: the observers of the container and of its ancestors. */
|
|
453
|
+
function targetsOf(container, source) {
|
|
454
|
+
return [...container.runtime.observers].filter((observer) => observer !== source && observer.active && within(container, observer.container));
|
|
455
|
+
}
|
|
456
|
+
function send(container, targets, record) {
|
|
457
|
+
container.runtime.deliveries.deliver(targets.map((observer) => ({
|
|
458
|
+
run: () => {
|
|
459
|
+
if (observer.active && observer.container.state !== "disposed") observer.callback(record);
|
|
460
|
+
},
|
|
461
|
+
onError: (error) => {
|
|
462
|
+
if (record.type !== "error") reportCallbackError(observer.container, record.key, error, "An observe callback threw.", observer);
|
|
463
|
+
}
|
|
464
|
+
})));
|
|
465
|
+
}
|
|
466
|
+
/** Sends `record` to the observers of `container`. */
|
|
467
|
+
function emitRecord(container, record) {
|
|
468
|
+
const targets = targetsOf(container);
|
|
469
|
+
if (targets.length === 0) return;
|
|
470
|
+
send(container, targets, Object.freeze(record));
|
|
471
|
+
}
|
|
472
|
+
/** Reports a callback error as an `error` record. An error from an observe callback does not go back to that callback. */
|
|
473
|
+
function reportCallbackError(container, key, error, reason, source) {
|
|
474
|
+
const record = {
|
|
475
|
+
type: "error",
|
|
476
|
+
key,
|
|
477
|
+
owner: container.owner,
|
|
478
|
+
instanceId: void 0,
|
|
479
|
+
reason,
|
|
480
|
+
error
|
|
481
|
+
};
|
|
482
|
+
send(container, targetsOf(container, source), record);
|
|
483
|
+
}
|
|
484
|
+
/** Sends a `wait-aborted` record for each service of `services`: the `signal` of `call` aborted before the service stopped. */
|
|
485
|
+
function recordAbortedWait(services, call, reason) {
|
|
486
|
+
const message = messageOf(reason);
|
|
487
|
+
for (const service of services) emitRecord(service.container, {
|
|
488
|
+
type: "wait-aborted",
|
|
489
|
+
key: service.name,
|
|
490
|
+
owner: service.container.owner,
|
|
491
|
+
instanceId: service.signals.instance()?.id,
|
|
492
|
+
call,
|
|
493
|
+
phase: service.signals.phase(),
|
|
494
|
+
reason: message,
|
|
495
|
+
abortReason: reason
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
// src/graph/resolvers/condition.ts
|
|
499
|
+
/**
|
|
500
|
+
* The verdict of each condition for each provider registration.
|
|
501
|
+
* The input of a condition is frozen, and a condition must be pure. So each pair runs once, and a throw is reported once.
|
|
502
|
+
*/
|
|
503
|
+
const verdicts = /* @__PURE__ */ new WeakMap();
|
|
504
|
+
/** The read-only information about a provider registration that a condition gets. */
|
|
505
|
+
function providerInfo(registration) {
|
|
506
|
+
return Object.freeze({
|
|
507
|
+
key: registration.name,
|
|
508
|
+
owner: registration.container.owner,
|
|
509
|
+
metadata: registration.metadata
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
/**
|
|
513
|
+
* Tells whether `condition` accepts the provider registration.
|
|
514
|
+
* A thrown error counts as `false` and goes to the observe stream of the consumer.
|
|
515
|
+
*/
|
|
516
|
+
function acceptsProvider(consumer, keyName, condition, registration) {
|
|
517
|
+
let byProvider = verdicts.get(condition);
|
|
518
|
+
if (!byProvider) {
|
|
519
|
+
byProvider = /* @__PURE__ */ new WeakMap();
|
|
520
|
+
verdicts.set(condition, byProvider);
|
|
521
|
+
}
|
|
522
|
+
const known = byProvider.get(registration);
|
|
523
|
+
if (known !== void 0) return known;
|
|
524
|
+
let verdict;
|
|
525
|
+
try {
|
|
526
|
+
verdict = untracked(() => condition(providerInfo(registration))) === true;
|
|
527
|
+
} catch (error) {
|
|
528
|
+
verdict = false;
|
|
529
|
+
queueMicrotask(() => reportCallbackError(consumer.container, consumer.name, error, `A resolution condition for '${keyName}' threw. It counts as false.`));
|
|
530
|
+
}
|
|
531
|
+
byProvider.set(registration, verdict);
|
|
532
|
+
return verdict;
|
|
533
|
+
}
|
|
534
|
+
/** The resolution conditions that a candidate of `edge` must pass: the condition of the edge, then the condition of the candidate. */
|
|
535
|
+
function conditionsOf(edge, keyName) {
|
|
536
|
+
const conditions = [];
|
|
537
|
+
if (edge?.condition) conditions.push(edge.condition);
|
|
538
|
+
const own = edge?.candidateConditions?.[keyName];
|
|
539
|
+
if (own) conditions.push(own);
|
|
540
|
+
return conditions;
|
|
541
|
+
}
|
|
542
|
+
/** Tells whether the provider registration passes each condition of the candidate `keyName` of `edge`. */
|
|
543
|
+
function acceptsCandidate(consumer, edge, keyName, registration) {
|
|
544
|
+
return conditionsOf(edge, keyName).every((condition) => acceptsProvider(consumer, keyName, condition, registration));
|
|
545
|
+
}
|
|
546
|
+
// src/graph/resolvers/plain.ts
|
|
547
|
+
/**
|
|
548
|
+
* Resolves one key name: the selected provider, and its instance when the provider serves.
|
|
549
|
+
* A provider in conflict, or one that a condition of `edge` for `keyName` rejects, does not resolve.
|
|
550
|
+
*/
|
|
551
|
+
function resolveKey(consumer, keyName, edge) {
|
|
552
|
+
const selection = selectProvider(consumer.scope, keyName, consumer);
|
|
553
|
+
if (!selection) return { kind: "missing" };
|
|
554
|
+
const { registration, provider } = selection;
|
|
555
|
+
const rivals = registration.signals.rivals();
|
|
556
|
+
if (rivals.length > 0) return {
|
|
557
|
+
kind: "conflict",
|
|
558
|
+
provider: registration,
|
|
559
|
+
rivals
|
|
560
|
+
};
|
|
561
|
+
if (!acceptsCandidate(consumer, edge, keyName, registration)) return {
|
|
562
|
+
kind: "rejected",
|
|
563
|
+
provider: registration
|
|
564
|
+
};
|
|
565
|
+
if (!provider) return {
|
|
566
|
+
kind: "not-running",
|
|
567
|
+
provider: registration
|
|
568
|
+
};
|
|
569
|
+
const instance = provider.signals.instance();
|
|
570
|
+
if (!provider.signals.serving() || !instance) return {
|
|
571
|
+
kind: "not-running",
|
|
572
|
+
provider
|
|
573
|
+
};
|
|
574
|
+
return {
|
|
575
|
+
kind: "resolved",
|
|
576
|
+
provider,
|
|
577
|
+
instance
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
/** Resolves a plain or an optional edge. It has one candidate key. */
|
|
581
|
+
function resolvePlain(consumer, edge) {
|
|
582
|
+
const [keyName] = edge.candidates;
|
|
583
|
+
return keyName === void 0 ? { kind: "missing" } : resolveKey(consumer, keyName, edge);
|
|
584
|
+
}
|
|
585
|
+
// src/graph/resolvers/fallback.ts
|
|
586
|
+
/**
|
|
587
|
+
* Resolves a `firstAvailable` edge. The first serving and available candidate that passes its conditions wins. A rejected candidate is skipped.
|
|
588
|
+
* If no serving candidate is available, the first serving candidate stays selected, so that a running dependent does not lose it.
|
|
589
|
+
* Without a serving candidate, the edge reports the first candidate that exists.
|
|
590
|
+
*/
|
|
591
|
+
function resolveFallback(consumer, edge) {
|
|
592
|
+
let firstUnresolved;
|
|
593
|
+
let firstServing;
|
|
594
|
+
for (const keyName of edge.candidates) {
|
|
595
|
+
const resolution = resolveKey(consumer, keyName, edge);
|
|
596
|
+
if (resolution.kind === "missing") continue;
|
|
597
|
+
if (resolution.kind !== "resolved") {
|
|
598
|
+
firstUnresolved ??= resolution;
|
|
599
|
+
continue;
|
|
600
|
+
}
|
|
601
|
+
if (isAvailable(resolution.provider)) return resolution;
|
|
602
|
+
firstServing ??= resolution;
|
|
603
|
+
}
|
|
604
|
+
return firstServing ?? firstUnresolved ?? { kind: "missing" };
|
|
605
|
+
}
|
|
606
|
+
// src/graph/status.ts
|
|
607
|
+
const statusRank = {
|
|
608
|
+
ready: 0,
|
|
609
|
+
degraded: 1,
|
|
610
|
+
unavailable: 2
|
|
611
|
+
};
|
|
612
|
+
/** Tells whether `a` is worse than `b`. The order is `ready`, then `degraded`, then `unavailable`. */
|
|
613
|
+
function worseThan(a, b) {
|
|
614
|
+
return statusRank[a] > statusRank[b];
|
|
615
|
+
}
|
|
616
|
+
function sameCause(a, b) {
|
|
617
|
+
if (a === b) return true;
|
|
618
|
+
if (!a || !b) return false;
|
|
619
|
+
return a.key === b.key && a.owner === b.owner && a.status === b.status && a.reason === b.reason && sameCause(a.cause, b.cause);
|
|
620
|
+
}
|
|
621
|
+
/** Tells whether two status values have the same status, reason, and cause. */
|
|
622
|
+
function sameStatus(a, b) {
|
|
623
|
+
if (a === b) return true;
|
|
624
|
+
if (!a || !b) return false;
|
|
625
|
+
return a.status === b.status && a.reason === b.reason && sameCause(a.cause, b.cause);
|
|
626
|
+
}
|
|
627
|
+
function statusFields(status) {
|
|
628
|
+
return withoutUndefined({
|
|
629
|
+
reason: status.reason,
|
|
630
|
+
cause: status.cause
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
/** The cause that a dependent reports when `provider` makes its effective status worse. */
|
|
634
|
+
function causeOf(provider, status) {
|
|
635
|
+
return Object.freeze({
|
|
636
|
+
key: provider.name,
|
|
637
|
+
owner: provider.container.owner,
|
|
638
|
+
status: status.status,
|
|
639
|
+
...withoutUndefined({
|
|
640
|
+
reason: status.reason,
|
|
641
|
+
cause: status.cause
|
|
642
|
+
})
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
/**
|
|
646
|
+
* Creates the effective status of a service: the worst of the own status and the effective status of each required dependency.
|
|
647
|
+
* An optional dependency does not count. It is `undefined` outside `running`.
|
|
648
|
+
*/
|
|
649
|
+
function createEffectiveStatus(input) {
|
|
650
|
+
const { phase, instance, edges, resolutions } = input;
|
|
651
|
+
return computed((previous) => {
|
|
652
|
+
const current = instance();
|
|
653
|
+
if (phase() !== "running" || !current) return void 0;
|
|
654
|
+
let result = current.signals.own();
|
|
655
|
+
for (const edge of edges) {
|
|
656
|
+
if (edge.optional) continue;
|
|
657
|
+
const resolution = resolutions.get(edge.name)?.();
|
|
658
|
+
if (resolution?.kind !== "resolved") continue;
|
|
659
|
+
const dependency = resolution.provider.signals.effective();
|
|
660
|
+
if (dependency && worseThan(dependency.status, result.status)) result = {
|
|
661
|
+
status: dependency.status,
|
|
662
|
+
cause: causeOf(resolution.provider, dependency)
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
return previous && sameStatus(previous, result) ? previous : result;
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
/** Reports a change of the effective status. The status effect of each service calls it. */
|
|
669
|
+
function reportStatus(service, next) {
|
|
670
|
+
const previous = service.reportedStatus;
|
|
671
|
+
service.reportedStatus = next;
|
|
672
|
+
if (!next || sameStatus(previous, next)) return;
|
|
673
|
+
emitRecord(service.container, {
|
|
674
|
+
type: "status",
|
|
675
|
+
key: service.name,
|
|
676
|
+
owner: service.container.owner,
|
|
677
|
+
instanceId: service.signals.instance()?.id,
|
|
678
|
+
from: previous?.status,
|
|
679
|
+
to: next.status,
|
|
680
|
+
...statusFields(next)
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
// src/graph/nodes/service.ts
|
|
684
|
+
/** Writes the phase and sends the change to the observe stream. */
|
|
685
|
+
function setPhase(service, to, details = {}) {
|
|
686
|
+
const from = service.signals.phase();
|
|
687
|
+
if (from === to) return;
|
|
688
|
+
batch(() => {
|
|
689
|
+
service.signals.phase(to);
|
|
690
|
+
if (service.kind === "resolver") return;
|
|
691
|
+
emitRecord(service.container, {
|
|
692
|
+
type: "phase",
|
|
693
|
+
key: service.name,
|
|
694
|
+
owner: service.container.owner,
|
|
695
|
+
instanceId: service.signals.instance()?.id,
|
|
696
|
+
from,
|
|
697
|
+
to,
|
|
698
|
+
...withoutUndefined({
|
|
699
|
+
reason: details.reason,
|
|
700
|
+
decision: details.decision
|
|
701
|
+
})
|
|
702
|
+
});
|
|
703
|
+
});
|
|
704
|
+
}
|
|
705
|
+
function sameResolution(a, b) {
|
|
706
|
+
if (a.kind !== b.kind) return false;
|
|
707
|
+
switch (a.kind) {
|
|
708
|
+
case "missing": return true;
|
|
709
|
+
case "not-running":
|
|
710
|
+
case "rejected": return "provider" in b && a.provider === b.provider;
|
|
711
|
+
case "conflict": return b.kind === "conflict" && a.provider === b.provider && a.rivals === b.rivals;
|
|
712
|
+
case "resolved": return b.kind === "resolved" && a.provider === b.provider && a.instance === b.instance;
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
/** Creates the resolution of one declared edge. It keeps the previous value when nothing changed. */
|
|
716
|
+
function resolveEdge(consumer, edge) {
|
|
717
|
+
return computed((previous) => {
|
|
718
|
+
const next = edge.fallback ? resolveFallback(consumer, edge) : resolvePlain(consumer, edge);
|
|
719
|
+
return previous && sameResolution(previous, next) ? previous : next;
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
const emptyMetadata$1 = Object.freeze({});
|
|
723
|
+
const emptyPolicy$1 = Object.freeze({});
|
|
724
|
+
/**
|
|
725
|
+
* Creates a service with its signals, its derived values, and its effects.
|
|
726
|
+
*
|
|
727
|
+
* The main effect reads the derived values of the service and only marks the service dirty.
|
|
728
|
+
* The status effect sends each change of the effective status to the observe stream.
|
|
729
|
+
*/
|
|
730
|
+
function createService(input) {
|
|
731
|
+
const { container } = input;
|
|
732
|
+
const { runtime } = container;
|
|
733
|
+
const present = signal(true);
|
|
734
|
+
const phase = signal(input.phase ?? "pending");
|
|
735
|
+
const instance = signal(void 0);
|
|
736
|
+
const demanded = signal(input.kind === "resolver");
|
|
737
|
+
const subscribers = signal(/* @__PURE__ */ new Set());
|
|
738
|
+
const resolutions = /* @__PURE__ */ new Map();
|
|
739
|
+
const effective = createEffectiveStatus({
|
|
740
|
+
phase,
|
|
741
|
+
instance,
|
|
742
|
+
edges: input.edges,
|
|
743
|
+
resolutions
|
|
744
|
+
});
|
|
745
|
+
let rivals = () => [];
|
|
746
|
+
const service = {
|
|
747
|
+
seq: input.seq ?? runtime.nextSeq(),
|
|
748
|
+
kind: input.kind,
|
|
749
|
+
name: input.name,
|
|
750
|
+
container,
|
|
751
|
+
scope: input.template?.container ?? container,
|
|
752
|
+
edges: input.edges,
|
|
753
|
+
build: input.build,
|
|
754
|
+
metadata: input.metadata ?? emptyMetadata$1,
|
|
755
|
+
policy: input.policy ?? emptyPolicy$1,
|
|
756
|
+
exclusive: input.exclusive ?? false,
|
|
757
|
+
exported: input.template === void 0 && (input.exported ?? false),
|
|
758
|
+
perConsumer: input.perConsumer ?? false,
|
|
759
|
+
template: input.template,
|
|
760
|
+
consumer: input.consumer,
|
|
761
|
+
registration: input.registration,
|
|
762
|
+
swap: input.swap,
|
|
763
|
+
replacedBy: void 0,
|
|
764
|
+
successor: void 0,
|
|
765
|
+
transaction: input.transaction,
|
|
766
|
+
revision: 0,
|
|
767
|
+
signals: {
|
|
768
|
+
present,
|
|
769
|
+
phase,
|
|
770
|
+
instance,
|
|
771
|
+
demanded,
|
|
772
|
+
subscribers,
|
|
773
|
+
active: computed(() => container.signals.started() || input.kind !== "invocation" && (demanded() || subscribers().size > 0)),
|
|
774
|
+
serving: computed(() => present() && phase() === "running"),
|
|
775
|
+
resolutions,
|
|
776
|
+
effective,
|
|
777
|
+
rivals: () => rivals(),
|
|
778
|
+
consumers: signal(/* @__PURE__ */ new Map()),
|
|
779
|
+
targetLost: computed(() => !present() || !service.signals.active() || service.signals.rivals().length > 0 || requiredWithdrawn(service))
|
|
780
|
+
},
|
|
781
|
+
busy: false,
|
|
782
|
+
stopIntent: {},
|
|
783
|
+
parkedEpoch: void 0,
|
|
784
|
+
failure: void 0,
|
|
785
|
+
demanders: /* @__PURE__ */ new Set(),
|
|
786
|
+
teardownErrors: [],
|
|
787
|
+
withdrawal: createDeferred(),
|
|
788
|
+
reportedStatus: void 0,
|
|
789
|
+
disposeEffects: () => {}
|
|
790
|
+
};
|
|
791
|
+
for (const edge of input.edges) resolutions.set(edge.name, resolveEdge(service, edge));
|
|
792
|
+
rivals = createRivals(service);
|
|
793
|
+
const stopMain = withOwner(container.reactiveOwner, () => effect(() => {
|
|
794
|
+
present();
|
|
795
|
+
phase();
|
|
796
|
+
service.signals.active();
|
|
797
|
+
service.signals.rivals();
|
|
798
|
+
instance()?.signals.holders();
|
|
799
|
+
service.swap?.target.signals.present();
|
|
800
|
+
for (const resolution of resolutions.values()) {
|
|
801
|
+
const current = resolution();
|
|
802
|
+
if (current.kind === "not-running") current.provider.signals.phase();
|
|
803
|
+
if (current.kind === "resolved") current.provider.signals.effective();
|
|
804
|
+
}
|
|
805
|
+
runtime.reconciler.markDirty(service);
|
|
806
|
+
}));
|
|
807
|
+
const stopStatus = input.kind === "resolver" ? () => {} : withOwner(container.reactiveOwner, () => effect(() => {
|
|
808
|
+
const status = effective();
|
|
809
|
+
untracked(() => reportStatus(service, status));
|
|
810
|
+
}));
|
|
811
|
+
service.disposeEffects = () => {
|
|
812
|
+
stopMain();
|
|
813
|
+
stopStatus();
|
|
814
|
+
};
|
|
815
|
+
return service;
|
|
816
|
+
}
|
|
817
|
+
// src/graph/nodes/consumer.ts
|
|
818
|
+
/** The phases of a consumer service that lead to a build outcome. */
|
|
819
|
+
const waitingPhases = /* @__PURE__ */ new Set([
|
|
820
|
+
"pending",
|
|
821
|
+
"building",
|
|
822
|
+
"starting",
|
|
823
|
+
"retrying"
|
|
824
|
+
]);
|
|
825
|
+
/**
|
|
826
|
+
* Returns the service of a `perConsumer` registration for one consumer container. It creates the service on first use.
|
|
827
|
+
*
|
|
828
|
+
* The service lives in the consumer container, so it disposes with that container. Its build context has the owner of the container as `consumer`.
|
|
829
|
+
* It returns `undefined` when the registration is withdrawn or the container disposal started.
|
|
830
|
+
*
|
|
831
|
+
* `demander` is the dependent, the `resolve` call, or the subscription that needs the service.
|
|
832
|
+
* A failed service builds again for a demander that did not get a build since the service last ran. So a new demand retries a failed consumer service.
|
|
833
|
+
*/
|
|
834
|
+
function ensureConsumerService(registration, container, demander) {
|
|
835
|
+
const existing = registration.signals.consumers().get(container);
|
|
836
|
+
if (existing) {
|
|
837
|
+
if (existing.signals.phase() === "failed" && existing.signals.present() && !existing.demanders.has(demander)) {
|
|
838
|
+
existing.demanders.add(demander);
|
|
839
|
+
existing.failure = void 0;
|
|
840
|
+
setPhase(existing, "pending", { reason: "A new demand tries the failed consumer service again." });
|
|
841
|
+
existing.container.runtime.reconciler.markDirty(existing);
|
|
842
|
+
} else if (waitingPhases.has(existing.signals.phase())) existing.demanders.add(demander);
|
|
843
|
+
return existing;
|
|
844
|
+
}
|
|
845
|
+
if (!registration.signals.present() || container.state !== "active") return void 0;
|
|
846
|
+
let service;
|
|
847
|
+
batch(() => {
|
|
848
|
+
service = createService({
|
|
849
|
+
container,
|
|
850
|
+
kind: "service",
|
|
851
|
+
name: registration.name,
|
|
852
|
+
edges: registration.edges,
|
|
853
|
+
build: registration.build,
|
|
854
|
+
metadata: registration.metadata,
|
|
855
|
+
policy: registration.policy,
|
|
856
|
+
exclusive: registration.exclusive,
|
|
857
|
+
template: registration,
|
|
858
|
+
consumer: container.owner
|
|
859
|
+
});
|
|
860
|
+
service.demanders.add(demander);
|
|
861
|
+
container.services.add(service);
|
|
862
|
+
publishService(service);
|
|
863
|
+
});
|
|
864
|
+
return service;
|
|
865
|
+
}
|
|
866
|
+
/**
|
|
867
|
+
* Makes `subscription` demand the consumer service of `registration` for `container`, so that it builds also before `start()`.
|
|
868
|
+
* Returns the service, or `undefined` when it cannot exist. `releaseConsumerDemand` ends the demand.
|
|
869
|
+
*/
|
|
870
|
+
function demandConsumerService(registration, container, subscription) {
|
|
871
|
+
const service = ensureConsumerService(registration, container, subscription);
|
|
872
|
+
if (service && !service.signals.subscribers().has(subscription)) service.signals.subscribers(/* @__PURE__ */ new Set([...service.signals.subscribers(), subscription]));
|
|
873
|
+
return service;
|
|
874
|
+
}
|
|
875
|
+
/** Ends the demand of `subscription` on `service`. */
|
|
876
|
+
function releaseConsumerDemand(service, subscription) {
|
|
877
|
+
const subscribers = service.signals.subscribers();
|
|
878
|
+
if (subscribers.has(subscription)) service.signals.subscribers(new Set([...subscribers].filter((other) => other !== subscription)));
|
|
879
|
+
}
|
|
880
|
+
// src/types/keys.ts
|
|
881
|
+
const optionalBrand = Symbol("injeca.optional");
|
|
882
|
+
const fallbackBrand = Symbol("injeca.fallback");
|
|
883
|
+
const conditionBrand = Symbol("injeca.condition");
|
|
884
|
+
// src/primitives/keys.ts
|
|
885
|
+
function assertName(name) {
|
|
886
|
+
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)}.`);
|
|
887
|
+
}
|
|
888
|
+
function readKeyOptions(options) {
|
|
889
|
+
if (options === void 0) return false;
|
|
890
|
+
if (typeof options !== "object" || options === null) throw new InvalidArgumentError("The options of defineKey must be an object.");
|
|
891
|
+
let exclusive;
|
|
892
|
+
try {
|
|
893
|
+
exclusive = options.exclusive;
|
|
894
|
+
} catch (error) {
|
|
895
|
+
throw new InvalidArgumentError("Cannot read the options of defineKey.", { cause: error });
|
|
896
|
+
}
|
|
897
|
+
if (exclusive !== void 0 && typeof exclusive !== "boolean") throw new InvalidArgumentError("The exclusive option of defineKey must be a boolean.");
|
|
898
|
+
return exclusive === true;
|
|
899
|
+
}
|
|
900
|
+
/**
|
|
901
|
+
* Creates a typed key without a provider. A consumer can depend on it now. `provide(key, ...)` registers a provider later.
|
|
902
|
+
*
|
|
903
|
+
* With `options.exclusive`, the key can have only one provider in its resolution area. Two or more providers all go to `conflict`.
|
|
904
|
+
*/
|
|
905
|
+
function defineKey(name, options) {
|
|
906
|
+
assertName(name);
|
|
907
|
+
return readKeyOptions(options) ? Object.freeze({
|
|
908
|
+
key: name,
|
|
909
|
+
exclusive: true
|
|
910
|
+
}) : Object.freeze({ key: name });
|
|
911
|
+
}
|
|
912
|
+
/** 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`. */
|
|
913
|
+
function isExclusiveKey(key) {
|
|
914
|
+
if (typeof key !== "object" || key === null) return false;
|
|
915
|
+
try {
|
|
916
|
+
return key.exclusive === true;
|
|
917
|
+
} catch (error) {
|
|
918
|
+
throw new InvalidArgumentError("Cannot read the exclusive property of the key.", { cause: error });
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
function normalizeName(nameOrProvidedKey) {
|
|
922
|
+
if (typeof nameOrProvidedKey === "object" && nameOrProvidedKey !== null && "key" in nameOrProvidedKey) return nameOrProvidedKey.key;
|
|
923
|
+
return nameOrProvidedKey;
|
|
924
|
+
}
|
|
925
|
+
const optionalMarkers = /* @__PURE__ */ new WeakSet();
|
|
926
|
+
const fallbackMarkers = /* @__PURE__ */ new WeakSet();
|
|
927
|
+
const conditionMarkers = /* @__PURE__ */ new WeakSet();
|
|
928
|
+
function isConditionalDependency(value) {
|
|
929
|
+
return typeof value === "object" && value !== null && conditionMarkers.has(value);
|
|
930
|
+
}
|
|
931
|
+
function isFallbackDependency(value) {
|
|
932
|
+
return typeof value === "object" && value !== null && fallbackMarkers.has(value);
|
|
933
|
+
}
|
|
934
|
+
/** The candidate conditions of a marker, without empty records. */
|
|
935
|
+
function withCandidateConditions(candidateConditions) {
|
|
936
|
+
return candidateConditions && Object.keys(candidateConditions).length > 0 ? { candidateConditions } : {};
|
|
937
|
+
}
|
|
938
|
+
/**
|
|
939
|
+
* Marks a fallback dependency. It resolves to the first provider in the list whose effective status is available.
|
|
940
|
+
*
|
|
941
|
+
* A change of the selection is a change of identity: the dependent gets `dependency-change`.
|
|
942
|
+
* It is required, unless it is wrapped in `optional()`.
|
|
943
|
+
* A `where(key, condition)` marker in the list filters that candidate only. `where(firstAvailable(...), condition)` filters each candidate.
|
|
944
|
+
* The selection is the first available candidate that passes its conditions.
|
|
945
|
+
*/
|
|
946
|
+
function firstAvailable(...keys) {
|
|
947
|
+
if (keys.length === 0) throw new InvalidArgumentError("firstAvailable() needs at least one key.");
|
|
948
|
+
const candidateConditions = {};
|
|
949
|
+
const dependencies = keys.map((key) => {
|
|
950
|
+
if (isConditionalDependency(key)) {
|
|
951
|
+
const [name] = key.dependencies;
|
|
952
|
+
candidateConditions[name] = key.condition;
|
|
953
|
+
return name;
|
|
954
|
+
}
|
|
955
|
+
if (isFallbackDependency(key) || isOptionalDependency(key)) throw new InvalidArgumentError("firstAvailable() accepts keys, names, and where() markers.");
|
|
956
|
+
const name = normalizeName(key);
|
|
957
|
+
assertName(name);
|
|
958
|
+
return name;
|
|
959
|
+
});
|
|
960
|
+
if (new Set(dependencies).size !== dependencies.length) throw new InvalidArgumentError(`firstAvailable() got the same key more than once: ${dependencies.join(", ")}.`);
|
|
961
|
+
return fallbackMarker(Object.freeze(dependencies), void 0, Object.freeze(candidateConditions));
|
|
962
|
+
}
|
|
963
|
+
function fallbackMarker(dependencies, condition, candidateConditions) {
|
|
964
|
+
const marker = Object.freeze({
|
|
965
|
+
[fallbackBrand]: true,
|
|
966
|
+
dependencies,
|
|
967
|
+
...condition ? { condition } : {},
|
|
968
|
+
...withCandidateConditions(candidateConditions)
|
|
969
|
+
});
|
|
970
|
+
fallbackMarkers.add(marker);
|
|
971
|
+
return marker;
|
|
972
|
+
}
|
|
973
|
+
function optionalMarker(dependencies, condition, candidateConditions) {
|
|
974
|
+
const marker = Object.freeze({
|
|
975
|
+
[optionalBrand]: true,
|
|
976
|
+
dependencies,
|
|
977
|
+
...condition ? { condition } : {},
|
|
978
|
+
...withCandidateConditions(candidateConditions)
|
|
979
|
+
});
|
|
980
|
+
optionalMarkers.add(marker);
|
|
981
|
+
return marker;
|
|
982
|
+
}
|
|
983
|
+
/**
|
|
984
|
+
* Marks an optional dependency. The dependent gets `{ value, status }`, or `undefined` when no provider runs.
|
|
985
|
+
* It also accepts a `firstAvailable` marker and a `where` marker, with their conditions.
|
|
986
|
+
*/
|
|
987
|
+
function optional(dependency) {
|
|
988
|
+
if (isFallbackDependency(dependency)) return optionalMarker(dependency.dependencies, dependency.condition, dependency.candidateConditions);
|
|
989
|
+
if (isConditionalDependency(dependency)) return optionalMarker(dependency.dependencies, dependency.condition);
|
|
990
|
+
const name = normalizeName(dependency);
|
|
991
|
+
assertName(name);
|
|
992
|
+
return optionalMarker(Object.freeze([name]), void 0);
|
|
993
|
+
}
|
|
994
|
+
function where(dependency, condition) {
|
|
995
|
+
if (typeof condition !== "function") throw new InvalidArgumentError("The condition of where() is not a function.");
|
|
996
|
+
if (isOptionalDependency(dependency)) {
|
|
997
|
+
if (dependency.condition) throw new InvalidArgumentError("The optional dependency already has a condition.");
|
|
998
|
+
return optionalMarker(dependency.dependencies, condition, dependency.candidateConditions);
|
|
999
|
+
}
|
|
1000
|
+
if (isFallbackDependency(dependency)) {
|
|
1001
|
+
if (dependency.condition) throw new InvalidArgumentError("The fallback dependency already has a condition.");
|
|
1002
|
+
return fallbackMarker(dependency.dependencies, condition, dependency.candidateConditions);
|
|
1003
|
+
}
|
|
1004
|
+
if (isConditionalDependency(dependency)) throw new InvalidArgumentError("where() accepts a key, a name, an optional() marker, or a firstAvailable() marker.");
|
|
1005
|
+
const name = normalizeName(dependency);
|
|
1006
|
+
assertName(name);
|
|
1007
|
+
const marker = Object.freeze({
|
|
1008
|
+
[conditionBrand]: true,
|
|
1009
|
+
dependencies: Object.freeze([name]),
|
|
1010
|
+
condition
|
|
1011
|
+
});
|
|
1012
|
+
conditionMarkers.add(marker);
|
|
1013
|
+
return marker;
|
|
1014
|
+
}
|
|
1015
|
+
function isOptionalDependency(value) {
|
|
1016
|
+
return typeof value === "object" && value !== null && optionalMarkers.has(value);
|
|
1017
|
+
}
|
|
1018
|
+
// src/graph/nodes/edge.ts
|
|
1019
|
+
function edgeOf(name, candidates, optional, fallback, condition, candidateConditions) {
|
|
1020
|
+
return Object.freeze({
|
|
1021
|
+
name,
|
|
1022
|
+
keyName: candidates.join(" | "),
|
|
1023
|
+
candidates,
|
|
1024
|
+
optional,
|
|
1025
|
+
fallback,
|
|
1026
|
+
...condition ? { condition } : {},
|
|
1027
|
+
...candidateConditions ? { candidateConditions } : {}
|
|
1028
|
+
});
|
|
1029
|
+
}
|
|
1030
|
+
/**
|
|
1031
|
+
* Reads a `dependsOn` declaration into edges. Reading a caller-owned property happens once, inside a narrow `try`.
|
|
1032
|
+
*/
|
|
1033
|
+
function readEdges(owner, dependsOn) {
|
|
1034
|
+
if (dependsOn === void 0 || dependsOn === null) return Object.freeze([]);
|
|
1035
|
+
if (typeof dependsOn !== "object") throw new InvalidArgumentError(`The dependsOn option of '${owner}' is not an object.`);
|
|
1036
|
+
let entries;
|
|
1037
|
+
try {
|
|
1038
|
+
entries = Object.entries(dependsOn);
|
|
1039
|
+
} catch (error) {
|
|
1040
|
+
throw new InvalidArgumentError(`Cannot read the dependsOn option of '${owner}'.`, { cause: error });
|
|
1041
|
+
}
|
|
1042
|
+
return Object.freeze(entries.map(([name, dependency]) => {
|
|
1043
|
+
if (isOptionalDependency(dependency)) return edgeOf(name, dependency.dependencies, true, dependency.dependencies.length > 1, dependency.condition, dependency.candidateConditions);
|
|
1044
|
+
if (isConditionalDependency(dependency)) return edgeOf(name, dependency.dependencies, false, false, dependency.condition);
|
|
1045
|
+
if (isFallbackDependency(dependency)) return edgeOf(name, dependency.dependencies, false, true, dependency.condition, dependency.candidateConditions);
|
|
1046
|
+
if (typeof dependency === "string" || typeof dependency === "object" && dependency !== null && "key" in dependency) {
|
|
1047
|
+
const keyName = normalizeName(dependency);
|
|
1048
|
+
if (typeof keyName === "string" && keyName.length > 0) return edgeOf(name, Object.freeze([keyName]), false, false);
|
|
1049
|
+
}
|
|
1050
|
+
throw new InvalidArgumentError(`The dependency '${name}' of '${owner}' is not a name, a key, an optional marker, a fallback marker, or a condition marker.`);
|
|
1051
|
+
}));
|
|
1052
|
+
}
|
|
1053
|
+
// src/graph/nodes/policy.ts
|
|
1054
|
+
const emptyPolicy = Object.freeze({});
|
|
1055
|
+
function readMaxAttempts(owner, value) {
|
|
1056
|
+
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)}.`);
|
|
1057
|
+
return value;
|
|
1058
|
+
}
|
|
1059
|
+
function readTimeout(owner, hook, value) {
|
|
1060
|
+
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)}.`);
|
|
1061
|
+
return value;
|
|
1062
|
+
}
|
|
1063
|
+
function readTimeouts(owner, value) {
|
|
1064
|
+
if (value === void 0) return void 0;
|
|
1065
|
+
if (typeof value !== "object" || value === null) throw new InvalidArgumentError(`The timeouts option of ${owner} must be an object.`);
|
|
1066
|
+
let fields;
|
|
1067
|
+
try {
|
|
1068
|
+
const source = value;
|
|
1069
|
+
fields = {
|
|
1070
|
+
start: source.start,
|
|
1071
|
+
stop: source.stop
|
|
1072
|
+
};
|
|
1073
|
+
} catch (error) {
|
|
1074
|
+
throw new InvalidArgumentError(`Cannot read the timeouts option of ${owner}.`, { cause: error });
|
|
1075
|
+
}
|
|
1076
|
+
const start = readTimeout(owner, "start", fields.start);
|
|
1077
|
+
const stop = readTimeout(owner, "stop", fields.stop);
|
|
1078
|
+
return Object.freeze({
|
|
1079
|
+
...start === void 0 ? {} : { start },
|
|
1080
|
+
...stop === void 0 ? {} : { stop }
|
|
1081
|
+
});
|
|
1082
|
+
}
|
|
1083
|
+
/**
|
|
1084
|
+
* Checks the retry and timeout options that a caller gave. `owner` names the caller in the messages, for example `'voice.speech'`.
|
|
1085
|
+
* A field that is `undefined` takes the value of `inherit`, so that a replacement keeps it.
|
|
1086
|
+
*/
|
|
1087
|
+
function readPolicy(owner, fields, inherit) {
|
|
1088
|
+
const maxAttempts = readMaxAttempts(owner, fields.maxAttempts) ?? inherit?.maxAttempts;
|
|
1089
|
+
const timeouts = readTimeouts(owner, fields.timeouts) ?? inherit?.timeouts;
|
|
1090
|
+
if (fields.onTimeout !== void 0 && typeof fields.onTimeout !== "function") throw new InvalidArgumentError(`The onTimeout option of ${owner} must be a function.`);
|
|
1091
|
+
const onTimeout = fields.onTimeout ?? inherit?.onTimeout;
|
|
1092
|
+
if (maxAttempts === void 0 && timeouts === void 0 && onTimeout === void 0) return emptyPolicy;
|
|
1093
|
+
return Object.freeze({
|
|
1094
|
+
...maxAttempts === void 0 ? {} : { maxAttempts },
|
|
1095
|
+
...timeouts === void 0 ? {} : { timeouts },
|
|
1096
|
+
...onTimeout === void 0 ? {} : { onTimeout }
|
|
1097
|
+
});
|
|
1098
|
+
}
|
|
1099
|
+
/** The value of the nearest container, from `container` up to the root, that sets it. */
|
|
1100
|
+
function fromContainers(container, pick) {
|
|
1101
|
+
for (let current = container; current; current = current.parent) {
|
|
1102
|
+
const value = pick(current.policy);
|
|
1103
|
+
if (value !== void 0) return value;
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
/** The attempt limit of a requeue: the decision, then the service, then the containers. `undefined` means no limit. */
|
|
1107
|
+
function maxAttemptsOf(service, fromDecision) {
|
|
1108
|
+
return fromDecision ?? service.policy.maxAttempts ?? fromContainers(service.container, (policy) => policy.maxAttempts);
|
|
1109
|
+
}
|
|
1110
|
+
/** The timeout of a hook: the service, then the containers. `undefined` means no timeout. */
|
|
1111
|
+
function timeoutOf(service, hook) {
|
|
1112
|
+
return service.policy.timeouts?.[hook] ?? fromContainers(service.container, (policy) => policy.timeouts?.[hook]);
|
|
1113
|
+
}
|
|
1114
|
+
/** The `onTimeout` handler: the service, then the containers. */
|
|
1115
|
+
function timeoutHandlerOf(service) {
|
|
1116
|
+
return service.policy.onTimeout ?? fromContainers(service.container, (policy) => policy.onTimeout);
|
|
1117
|
+
}
|
|
1118
|
+
// src/graph/nodes/definition.ts
|
|
1119
|
+
const emptyMetadata = Object.freeze({});
|
|
1120
|
+
function readMetadata(name, metadata, fallback) {
|
|
1121
|
+
if (metadata === void 0) return fallback ?? emptyMetadata;
|
|
1122
|
+
if (typeof metadata !== "object" || metadata === null || Array.isArray(metadata)) throw new InvalidArgumentError(`The metadata option of '${name}' must be a record.`);
|
|
1123
|
+
try {
|
|
1124
|
+
return Object.freeze({ ...metadata });
|
|
1125
|
+
} catch (error) {
|
|
1126
|
+
throw new InvalidArgumentError(`Cannot read the metadata option of '${name}'.`, { cause: error });
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
/**
|
|
1130
|
+
* Reads the options of one registration once: the function shorthand, or the object form.
|
|
1131
|
+
* Reading caller-owned properties happens inside a narrow `try`. `inherit` gives the metadata and each policy field when the options have none.
|
|
1132
|
+
* A replacement keeps `perConsumer` and `export` of its registration, so it ignores these fields of the definition.
|
|
1133
|
+
*/
|
|
1134
|
+
function readDefinition(name, option, inherit) {
|
|
1135
|
+
let build;
|
|
1136
|
+
let dependsOn;
|
|
1137
|
+
let metadata;
|
|
1138
|
+
let perConsumer;
|
|
1139
|
+
let exported;
|
|
1140
|
+
let policy;
|
|
1141
|
+
try {
|
|
1142
|
+
const object = typeof option === "function" ? { build: option } : typeof option === "object" && option !== null ? option : {};
|
|
1143
|
+
build = object.build;
|
|
1144
|
+
dependsOn = object.dependsOn;
|
|
1145
|
+
metadata = object.metadata;
|
|
1146
|
+
perConsumer = object.perConsumer;
|
|
1147
|
+
exported = object.export;
|
|
1148
|
+
policy = {
|
|
1149
|
+
maxAttempts: object.maxAttempts,
|
|
1150
|
+
timeouts: object.timeouts,
|
|
1151
|
+
onTimeout: object.onTimeout
|
|
1152
|
+
};
|
|
1153
|
+
} catch (error) {
|
|
1154
|
+
throw new InvalidArgumentError(`Cannot read the provide options of '${name}'.`, { cause: error });
|
|
1155
|
+
}
|
|
1156
|
+
if (typeof build !== "function") throw new InvalidArgumentError(`The build option of '${name}' is not a function.`);
|
|
1157
|
+
if (perConsumer !== void 0 && typeof perConsumer !== "boolean") throw new InvalidArgumentError(`The perConsumer option of '${name}' must be a boolean.`);
|
|
1158
|
+
if (exported !== void 0 && typeof exported !== "boolean") throw new InvalidArgumentError(`The export option of '${name}' must be a boolean.`);
|
|
1159
|
+
return {
|
|
1160
|
+
edges: readEdges(name, dependsOn),
|
|
1161
|
+
build,
|
|
1162
|
+
metadata: readMetadata(name, metadata, inherit?.metadata),
|
|
1163
|
+
perConsumer: perConsumer === true,
|
|
1164
|
+
exported: exported === true,
|
|
1165
|
+
policy: readPolicy(`'${name}'`, policy, inherit?.policy)
|
|
1166
|
+
};
|
|
1167
|
+
}
|
|
1168
|
+
// src/graph/nodes/registration.ts
|
|
1169
|
+
function createRegistration(container, name, exclusive) {
|
|
1170
|
+
return {
|
|
1171
|
+
container,
|
|
1172
|
+
name,
|
|
1173
|
+
exclusive,
|
|
1174
|
+
current: void 0,
|
|
1175
|
+
pending: void 0
|
|
1176
|
+
};
|
|
1177
|
+
}
|
|
1178
|
+
/**
|
|
1179
|
+
* The published service of a registration that is still current, or `undefined` for a stale registration.
|
|
1180
|
+
* A registration is stale after its own withdrawal, or after a newer `provide` of the same key in the same container.
|
|
1181
|
+
*/
|
|
1182
|
+
function currentService(registration) {
|
|
1183
|
+
const service = registration.current;
|
|
1184
|
+
return service?.signals.present() ? service : void 0;
|
|
1185
|
+
}
|
|
1186
|
+
/** Cancels the replacement that waits for its swap. The old service does not change. */
|
|
1187
|
+
function cancelPending(registration) {
|
|
1188
|
+
const pending = registration.pending;
|
|
1189
|
+
if (!pending) return;
|
|
1190
|
+
registration.pending = void 0;
|
|
1191
|
+
pending.swap?.settle({ kind: "cancelled" });
|
|
1192
|
+
releaseService(pending);
|
|
1193
|
+
}
|
|
1194
|
+
/** Cancels the successor that waits to replace `service`, for example a make-before-break rebuild. `service` does not change. */
|
|
1195
|
+
function cancelSuccessor(service) {
|
|
1196
|
+
const successor = service.successor;
|
|
1197
|
+
if (!successor) return;
|
|
1198
|
+
service.successor = void 0;
|
|
1199
|
+
successor.swap?.settle({ kind: "cancelled" });
|
|
1200
|
+
releaseService(successor);
|
|
1201
|
+
}
|
|
1202
|
+
/** Releases the current service of a registration and cancels its pending replacement. Returns the service, or `undefined` for a stale registration. */
|
|
1203
|
+
function releaseRegistration(registration) {
|
|
1204
|
+
return untracked(() => {
|
|
1205
|
+
const current = currentService(registration);
|
|
1206
|
+
if (current) batch(() => {
|
|
1207
|
+
cancelPending(registration);
|
|
1208
|
+
releaseService(current);
|
|
1209
|
+
});
|
|
1210
|
+
return current;
|
|
1211
|
+
});
|
|
1212
|
+
}
|
|
1213
|
+
// src/graph/nodes/reservation.ts
|
|
1214
|
+
/**
|
|
1215
|
+
* Reserves the exclusive key of a transaction entry until the commit or the abort.
|
|
1216
|
+
*
|
|
1217
|
+
* The reservation is a declaration without a slot. Consumers cannot resolve it. An exclusive conflict counts it, so a provider of the key in another container of the resolution area goes to `conflict`.
|
|
1218
|
+
* The reservation of an exported key counts in the resolution area of the parent container, as the published service does.
|
|
1219
|
+
* A plain key needs no reservation. In the same container, `provide` checks the open transactions and fails with `duplicate-service`.
|
|
1220
|
+
*/
|
|
1221
|
+
function reserveKey(transaction, entry) {
|
|
1222
|
+
if (!entry.exclusive || entry.replace) return;
|
|
1223
|
+
if (entry.reservation?.exported === entry.exported) return;
|
|
1224
|
+
releaseReservation(entry);
|
|
1225
|
+
batch(() => {
|
|
1226
|
+
const reservation = createService({
|
|
1227
|
+
container: transaction.container,
|
|
1228
|
+
kind: "service",
|
|
1229
|
+
name: entry.name,
|
|
1230
|
+
edges: [],
|
|
1231
|
+
build: () => void 0,
|
|
1232
|
+
exclusive: true,
|
|
1233
|
+
exported: entry.exported,
|
|
1234
|
+
phase: "declared",
|
|
1235
|
+
transaction
|
|
1236
|
+
});
|
|
1237
|
+
entry.reservation = reservation;
|
|
1238
|
+
addDeclaration(reservation);
|
|
1239
|
+
});
|
|
1240
|
+
}
|
|
1241
|
+
/** Removes the reservation of a transaction entry. The commit calls it in the batch of its publish. */
|
|
1242
|
+
function releaseReservation(entry) {
|
|
1243
|
+
const reservation = entry.reservation;
|
|
1244
|
+
if (!reservation) return;
|
|
1245
|
+
entry.reservation = void 0;
|
|
1246
|
+
batch(() => {
|
|
1247
|
+
reservation.signals.present(false);
|
|
1248
|
+
removeDeclaration(reservation);
|
|
1249
|
+
});
|
|
1250
|
+
reservation.disposeEffects();
|
|
1251
|
+
}
|
|
1252
|
+
/** Removes the reservations of all entries of a transaction. */
|
|
1253
|
+
function releaseReservations(transaction) {
|
|
1254
|
+
batch(() => {
|
|
1255
|
+
for (const entry of transaction.entries.values()) releaseReservation(entry);
|
|
1256
|
+
});
|
|
1257
|
+
}
|
|
1258
|
+
/** Tells whether an open or committing transaction of the container, other than `except`, has an entry that reserves `name`. */
|
|
1259
|
+
function reservedBy(transactions, name, except) {
|
|
1260
|
+
for (const transaction of transactions) {
|
|
1261
|
+
if (transaction === except || transaction.state !== "open" && transaction.state !== "committing") continue;
|
|
1262
|
+
const entry = transaction.entries.get(name);
|
|
1263
|
+
if (entry && !entry.replace) return transaction;
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
// src/primitives/decision.ts
|
|
1267
|
+
const issued = /* @__PURE__ */ new WeakMap();
|
|
1268
|
+
function reasonFrom(options) {
|
|
1269
|
+
return options?.reason === void 0 ? {} : { reason: options.reason };
|
|
1270
|
+
}
|
|
1271
|
+
/** Creates the `decide` object of one run. */
|
|
1272
|
+
function createDecide(run) {
|
|
1273
|
+
function issue(fields) {
|
|
1274
|
+
const decision = Object.freeze({
|
|
1275
|
+
...fields,
|
|
1276
|
+
service: run.service,
|
|
1277
|
+
instanceId: run.instanceId,
|
|
1278
|
+
hook: run.hook,
|
|
1279
|
+
attempt: run.attempt,
|
|
1280
|
+
issuedAt: run.now()
|
|
1281
|
+
});
|
|
1282
|
+
issued.set(decision, run);
|
|
1283
|
+
return decision;
|
|
1284
|
+
}
|
|
1285
|
+
return Object.freeze({
|
|
1286
|
+
handled: (options) => issue({
|
|
1287
|
+
kind: "handled",
|
|
1288
|
+
...reasonFrom(options)
|
|
1289
|
+
}),
|
|
1290
|
+
rebuild: (options) => issue({
|
|
1291
|
+
kind: "rebuild",
|
|
1292
|
+
...reasonFrom(options)
|
|
1293
|
+
}),
|
|
1294
|
+
requeue: (options) => issue({
|
|
1295
|
+
kind: "requeue",
|
|
1296
|
+
...reasonFrom(options),
|
|
1297
|
+
...options?.after === void 0 ? {} : { after: options.after },
|
|
1298
|
+
...options?.backoff === void 0 ? {} : { backoff: Object.freeze({ ...options.backoff }) },
|
|
1299
|
+
...options?.maxAttempts === void 0 ? {} : { maxAttempts: options.maxAttempts }
|
|
1300
|
+
}),
|
|
1301
|
+
stop: (options) => issue({
|
|
1302
|
+
kind: "stop",
|
|
1303
|
+
...reasonFrom(options)
|
|
1304
|
+
}),
|
|
1305
|
+
fail: (error, options) => issue({
|
|
1306
|
+
kind: "fail",
|
|
1307
|
+
error,
|
|
1308
|
+
...reasonFrom(options)
|
|
1309
|
+
})
|
|
1310
|
+
});
|
|
1311
|
+
}
|
|
1312
|
+
const validKinds = {
|
|
1313
|
+
"start": /* @__PURE__ */ new Set([
|
|
1314
|
+
"handled",
|
|
1315
|
+
"requeue",
|
|
1316
|
+
"stop",
|
|
1317
|
+
"fail"
|
|
1318
|
+
]),
|
|
1319
|
+
"stop": /* @__PURE__ */ new Set(["handled", "fail"]),
|
|
1320
|
+
"dependency-change": /* @__PURE__ */ new Set([
|
|
1321
|
+
"handled",
|
|
1322
|
+
"rebuild",
|
|
1323
|
+
"requeue",
|
|
1324
|
+
"stop",
|
|
1325
|
+
"fail"
|
|
1326
|
+
]),
|
|
1327
|
+
"dependency-status-change": /* @__PURE__ */ new Set([
|
|
1328
|
+
"handled",
|
|
1329
|
+
"rebuild",
|
|
1330
|
+
"requeue",
|
|
1331
|
+
"stop",
|
|
1332
|
+
"fail"
|
|
1333
|
+
])
|
|
1334
|
+
};
|
|
1335
|
+
/** The decision for `undefined`. A `dependency-change` handler without a decision gets the default: rebuild. */
|
|
1336
|
+
function defaultDecision(run) {
|
|
1337
|
+
const decide = createDecide(run);
|
|
1338
|
+
return run.hook === "dependency-change" ? decide.rebuild() : decide.handled();
|
|
1339
|
+
}
|
|
1340
|
+
/**
|
|
1341
|
+
* Checks the value that a hook handler returned. It gives `undefined` for a value that `decide` did not create, so that the caller applies the default.
|
|
1342
|
+
* This keeps a shorthand such as `on('start', () => app.listen(port))` valid.
|
|
1343
|
+
*
|
|
1344
|
+
* @throws {InvalidDecisionError} when another run created the decision, or when the hook does not accept its kind.
|
|
1345
|
+
*/
|
|
1346
|
+
function validateDecision(run, value) {
|
|
1347
|
+
const owner = typeof value === "object" && value !== null ? issued.get(value) : void 0;
|
|
1348
|
+
if (!owner) return void 0;
|
|
1349
|
+
if (owner !== run) throw new InvalidDecisionError(`The '${run.hook}' hook of '${run.service}' returned a decision from another run: the '${owner.hook}' hook of '${owner.service}', instance '${owner.instanceId}', attempt ${owner.attempt}.`);
|
|
1350
|
+
const decision = value;
|
|
1351
|
+
if (!validKinds[run.hook].has(decision.kind)) throw new InvalidDecisionError(`The '${run.hook}' hook cannot return a '${decision.kind}' decision.`);
|
|
1352
|
+
return decision;
|
|
1353
|
+
}
|
|
1354
|
+
export { candidatesOf as $, causeOf as A, withoutUndefined as B, optional as C, InvalidDecisionError as Ct, releaseConsumerDemand as D, StartTimeoutError as Dt, ensureConsumerService as E, StaleRegistrationError as Et, resolvePlain as F, nodeOf as G, affectedContainers as H, acceptsCandidate as I, releasePresent as J, publishService as K, emitRecord as L, statusFields as M, resolveFallback as N, createService as O, TransactionIncompleteError as Ot, resolveKey as P, buildLost as Q, recordAbortedWait as R, normalizeName as S, InvalidArgumentError as St, demandConsumerService as T, ServiceFailedError as Tt, assertActive as U, createDeferred as V, awaitWithdrawal as W, subtreeOf as X, releaseService as Y, blockingFailure as Z, timeoutOf as _, BuildAfterDisposeError as _t, releaseReservations as a, resolutionOf as at, firstAvailable as b, HookTimeoutError as bt, cancelPending as c, serviceFor as ct, currentService as d, removeDeclaration as dt, epochOf as et, releaseRegistration as f, errorFrom as ft, timeoutHandlerOf as g, withOwner as gt, readPolicy as h, untracked as ht, releaseReservation as i, requiredWithdrawn as it, sameStatus as j, setPhase as k, WaitTimeoutError as kt, cancelSuccessor as l, slotOf as lt, maxAttemptsOf as m, batch as mt, defaultDecision as n, requiredAvailable as nt, reserveKey as o, resolvedInstance as ot, readDefinition as p, messageOf as pt, registerNode as q, validateDecision as r, requiredChanged as rt, reservedBy as s, selectProvider as st, createDecide as t, findProvider as tt, createRegistration as u, inConflict as ut, readEdges as v, DuplicateServiceError as vt, where as w, ReplaceWhileStoppingError as wt, isExclusiveKey as x, InjecaError as xt, defineKey as y, HookOutsideBuildError as yt, reportCallbackError as z };
|
|
1355
|
+
|
|
1356
|
+
//# sourceMappingURL=decision-BXZu3N1O.mjs.map
|