stitchkit 0.78.0 → 0.80.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/dist/agent-runtime-browser.js +53 -581
- package/dist/agent-runtime-coding-tools.js +6 -5
- package/dist/agent-runtime-harness.js +21 -94
- package/dist/agent-runtime-openrouter.js +1 -1
- package/dist/agent-runtime.js +68 -64
- package/dist/application/kernel.d.ts.map +1 -1
- package/dist/application/schemas.d.ts +2 -0
- package/dist/application/schemas.d.ts.map +1 -1
- package/dist/application/watch-hub.d.ts.map +1 -1
- package/dist/application-diagnostic-journal.js +14 -96
- package/dist/application-opentelemetry.js +6 -92
- package/dist/application-schemas.js +2 -1
- package/dist/application.js +158 -1802
- package/dist/cli.js +13 -11
- package/dist/contract/index.js +7 -4
- package/dist/index-2qzskm8h.js +63 -0
- package/dist/{index-93xp5tgg.js → index-2rwwmr7v.js} +3 -3
- package/dist/{index-kw12fsbc.js → index-3gye9wzb.js} +7 -3
- package/dist/{index-1ckavx4h.js → index-3z73fh2c.js} +1 -49
- package/dist/index-4fpa9dy2.js +51 -0
- package/dist/{index-cwp38wxn.js → index-5mtc969c.js} +2 -2
- package/dist/index-5s3zajp8.js +192 -0
- package/dist/index-6s7n2v50.js +583 -0
- package/dist/{index-qaajjfp6.js → index-758zx2ha.js} +15 -94
- package/dist/{index-qyrqwr4c.js → index-77fekveh.js} +4 -1
- package/dist/{index-k45qqrdh.js → index-7ag81v2f.js} +13 -166
- package/dist/{index-n1g2xm6a.js → index-7gnkny4z.js} +124 -1
- package/dist/index-7zbps32p.js +89 -0
- package/dist/index-85vfqd7m.js +135 -0
- package/dist/{index-4ntp63ps.js → index-9s6z8vgf.js} +18 -142
- package/dist/index-a916km3r.js +45 -0
- package/dist/index-b9aefrqd.js +20 -0
- package/dist/index-dre2ywck.js +48 -0
- package/dist/{index-s5tn4kdv.js → index-dwf1f2ap.js} +3 -3
- package/dist/{index-wwst0td5.js → index-dx9xehwt.js} +1 -1
- package/dist/{index-5r0ak8fr.js → index-dxesg10d.js} +4 -4
- package/dist/{index-aczggrty.js → index-dxjjv399.js} +2 -2
- package/dist/{index-j6mfaw26.js → index-egaabxkz.js} +3 -3
- package/dist/index-evcnmsc3.js +866 -0
- package/dist/{index-efm026pw.js → index-fxz0v7vg.js} +86 -86
- package/dist/{index-56fm73re.js → index-hb5pf3s4.js} +1 -1
- package/dist/{index-n7apa6sy.js → index-k9ay6v6d.js} +24 -275
- package/dist/index-kzfs85xp.js +9 -0
- package/dist/{index-vbf2p6me.js → index-m9kzax52.js} +91 -89
- package/dist/{index-22by16v6.js → index-mgtxfe6b.js} +1 -1
- package/dist/index-p7hgdyhb.js +22 -0
- package/dist/index-pq95qkbf.js +0 -0
- package/dist/index-t0z5p5w6.js +318 -0
- package/dist/{index-31k1ns1s.js → index-vwp69871.js} +13 -11
- package/dist/index-wc4x00bn.js +103 -0
- package/dist/{index-aewy3x5j.js → index-xcetq6j3.js} +10 -8
- package/dist/{index-7z69kyrh.js → index-y01f4223.js} +24 -40
- package/dist/{index-dmfn9m4x.js → index-ywd0y6m1.js} +20 -341
- package/dist/index.js +46 -407
- package/dist/live.js +6 -3
- package/dist/node.js +17 -14
- package/dist/observability/index.js +8 -7
- package/dist/primitives.js +4 -2
- package/dist/react.js +1 -1
- package/dist/remote.js +7 -3
- package/dist/server/index.js +32 -45
- package/dist/telegram.js +1 -1
- package/dist/testing.js +90 -1576
- package/dist/tool-invoker.js +11 -9
- package/dist/tools/async-operation-contract.d.ts +93 -0
- package/dist/tools/async-operation-contract.d.ts.map +1 -0
- package/dist/tools/async-operation.d.ts +2 -47
- package/dist/tools/async-operation.d.ts.map +1 -1
- package/dist/tools/view-file-contract.d.ts +98 -0
- package/dist/tools/view-file-contract.d.ts.map +1 -0
- package/dist/tools/view-file.d.ts +4 -83
- package/dist/tools/view-file.d.ts.map +1 -1
- package/dist/tools-contract.d.ts +29 -0
- package/dist/tools-contract.d.ts.map +1 -0
- package/dist/tools-contract.js +24 -0
- package/dist/tools.d.ts +3 -3
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +61 -140
- package/llms-full.txt +86 -3
- package/package.json +9 -6
- package/dist/index-anyqnqcv.js +0 -79
- package/dist/index-ksp6e2ye.js +0 -24
- package/dist/index-vj3vvpaa.js +0 -307
- package/dist/index-wfq8zerm.js +0 -92
|
@@ -0,0 +1,866 @@
|
|
|
1
|
+
import {
|
|
2
|
+
managedResourceDependencyId
|
|
3
|
+
} from "./index-2k4yrqkc.js";
|
|
4
|
+
import {
|
|
5
|
+
ApplicationIdSchema,
|
|
6
|
+
ApplicationShutdownResultSchema,
|
|
7
|
+
ApplicationSnapshotSchema
|
|
8
|
+
} from "./index-3gye9wzb.js";
|
|
9
|
+
import {
|
|
10
|
+
ShutdownOptionsSchema
|
|
11
|
+
} from "./index-3z73fh2c.js";
|
|
12
|
+
import {
|
|
13
|
+
AppError
|
|
14
|
+
} from "./index-0w9abg87.js";
|
|
15
|
+
|
|
16
|
+
// src/application/kernel.ts
|
|
17
|
+
import { z } from "zod";
|
|
18
|
+
|
|
19
|
+
// src/application/graph.ts
|
|
20
|
+
function resolveResourceGraph(resources) {
|
|
21
|
+
const entries = resources.map((resource, declarationIndex) => ({
|
|
22
|
+
resource,
|
|
23
|
+
id: ApplicationIdSchema.parse(resource.id),
|
|
24
|
+
dependsOn: [...resource.dependsOn ?? []].map((dependency) => ApplicationIdSchema.parse(managedResourceDependencyId(dependency))),
|
|
25
|
+
required: resource.required ?? true,
|
|
26
|
+
declarationIndex
|
|
27
|
+
}));
|
|
28
|
+
const byId = new Map;
|
|
29
|
+
for (const entry of entries) {
|
|
30
|
+
if (byId.has(entry.id)) {
|
|
31
|
+
throw new Error(`[stitchkit] createApplication: duplicate resource id "${entry.id}"`);
|
|
32
|
+
}
|
|
33
|
+
byId.set(entry.id, entry);
|
|
34
|
+
}
|
|
35
|
+
for (const entry of entries) {
|
|
36
|
+
for (const dependencyId of entry.dependsOn) {
|
|
37
|
+
const dependency = byId.get(dependencyId);
|
|
38
|
+
if (!dependency) {
|
|
39
|
+
throw new Error(`[stitchkit] createApplication: resource "${entry.id}" depends on missing resource "${dependencyId}"`);
|
|
40
|
+
}
|
|
41
|
+
if (entry.required && !dependency.required) {
|
|
42
|
+
throw new Error(`[stitchkit] createApplication: required resource "${entry.id}" cannot depend on optional resource "${dependencyId}"`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const pending = new Map(entries.map((entry) => [entry.id, entry]));
|
|
47
|
+
const resolved = new Set;
|
|
48
|
+
const ordered = [];
|
|
49
|
+
while (pending.size > 0) {
|
|
50
|
+
const ready = [...pending.values()].filter((entry) => entry.dependsOn.every((id) => resolved.has(id))).sort((left, right) => left.declarationIndex - right.declarationIndex);
|
|
51
|
+
if (ready.length === 0) {
|
|
52
|
+
throw new Error(`[stitchkit] createApplication: resource dependency cycle: ${[...pending.keys()].join(", ")}`);
|
|
53
|
+
}
|
|
54
|
+
for (const entry of ready) {
|
|
55
|
+
pending.delete(entry.id);
|
|
56
|
+
resolved.add(entry.id);
|
|
57
|
+
ordered.push(entry);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return ordered;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// src/application/kernel.ts
|
|
64
|
+
class ResourceCompletionBeforeReadyError extends Error {
|
|
65
|
+
constructor(resourceId, cause) {
|
|
66
|
+
super(`[stitchkit] resource "${resourceId}" completed before reaching readiness`, {
|
|
67
|
+
...cause !== undefined && { cause }
|
|
68
|
+
});
|
|
69
|
+
this.name = "ResourceCompletionBeforeReadyError";
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
class ApplicationStartupInterruptedError extends Error {
|
|
74
|
+
constructor() {
|
|
75
|
+
super("[stitchkit] application startup interrupted by shutdown");
|
|
76
|
+
this.name = "ApplicationStartupInterruptedError";
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
var ApplicationShutdownOptionsSchema = ShutdownOptionsSchema.pick({
|
|
80
|
+
gracePeriodMs: true,
|
|
81
|
+
forceTimeoutMs: true,
|
|
82
|
+
signal: true
|
|
83
|
+
});
|
|
84
|
+
var ApplicationShutdownBudgetSchema = ShutdownOptionsSchema.pick({
|
|
85
|
+
gracePeriodMs: true,
|
|
86
|
+
forceTimeoutMs: true
|
|
87
|
+
});
|
|
88
|
+
var ApplicationRestartInputSchema = z.object({
|
|
89
|
+
resourceId: z.string().min(1),
|
|
90
|
+
gracePeriodMs: z.number().int().nonnegative().optional(),
|
|
91
|
+
forceTimeoutMs: z.number().int().nonnegative().optional()
|
|
92
|
+
}).strict().readonly();
|
|
93
|
+
var ApplicationRestartOutcomeSchema = z.enum(["restarted", "failed", "refused"]);
|
|
94
|
+
var ApplicationRestartResultSchema = z.object({
|
|
95
|
+
resourceId: z.string(),
|
|
96
|
+
affected: z.array(z.string()).readonly(),
|
|
97
|
+
outcome: ApplicationRestartOutcomeSchema,
|
|
98
|
+
reason: z.string().optional(),
|
|
99
|
+
durationMs: z.number().int().nonnegative()
|
|
100
|
+
}).strict().readonly();
|
|
101
|
+
|
|
102
|
+
class ApplicationAdmissionError extends AppError {
|
|
103
|
+
constructor() {
|
|
104
|
+
super("APPLICATION_NOT_ACCEPTING", "Application is not accepting new operations", 503);
|
|
105
|
+
this.name = "ApplicationAdmissionError";
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
function isStartResult(value) {
|
|
109
|
+
return typeof value === "object" && value !== null;
|
|
110
|
+
}
|
|
111
|
+
function waitForAbort(signal) {
|
|
112
|
+
if (signal.aborted)
|
|
113
|
+
return Promise.resolve();
|
|
114
|
+
return new Promise((resolve) => signal.addEventListener("abort", () => resolve(), { once: true }));
|
|
115
|
+
}
|
|
116
|
+
async function untilDeadline(work, signal) {
|
|
117
|
+
const result = await Promise.race([
|
|
118
|
+
work.then((value) => ({ settled: true, value }), (error) => ({ settled: true, error })),
|
|
119
|
+
waitForAbort(signal).then(() => ({ settled: false }))
|
|
120
|
+
]);
|
|
121
|
+
return result;
|
|
122
|
+
}
|
|
123
|
+
function createApplication(config) {
|
|
124
|
+
const id = ApplicationIdSchema.parse(config.id);
|
|
125
|
+
const ordered = resolveResourceGraph(config.resources ?? []);
|
|
126
|
+
const shutdownBudget = ApplicationShutdownBudgetSchema.parse(config.shutdown ?? {});
|
|
127
|
+
const reverse = [...ordered].reverse();
|
|
128
|
+
const reportFailure = (resourceId, phase, error) => {
|
|
129
|
+
if (!config.onResourceFailure)
|
|
130
|
+
return;
|
|
131
|
+
try {
|
|
132
|
+
Promise.resolve(config.onResourceFailure({ resourceId, phase, error })).catch(() => {
|
|
133
|
+
return;
|
|
134
|
+
});
|
|
135
|
+
} catch {}
|
|
136
|
+
};
|
|
137
|
+
const published = new Map;
|
|
138
|
+
const records = new Map;
|
|
139
|
+
for (const entry of ordered) {
|
|
140
|
+
records.set(entry.id, {
|
|
141
|
+
entry,
|
|
142
|
+
state: "registered",
|
|
143
|
+
health: "unknown",
|
|
144
|
+
healthReported: false,
|
|
145
|
+
everHealthy: false,
|
|
146
|
+
attempted: false,
|
|
147
|
+
activated: false,
|
|
148
|
+
closeInvoked: false,
|
|
149
|
+
closed: false,
|
|
150
|
+
failures: []
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
const epoch = crypto.randomUUID();
|
|
154
|
+
const listeners = new Set;
|
|
155
|
+
const lifetimeAbort = new AbortController;
|
|
156
|
+
const startupAbort = new AbortController;
|
|
157
|
+
let lifecycle = "created";
|
|
158
|
+
let revision = 0;
|
|
159
|
+
let changedAt = new Date().toISOString();
|
|
160
|
+
let accepting = false;
|
|
161
|
+
let accepted = 0;
|
|
162
|
+
let completed = 0;
|
|
163
|
+
let pending = 0;
|
|
164
|
+
let startPromise;
|
|
165
|
+
let shutdownPromise;
|
|
166
|
+
let shutdownRequested = false;
|
|
167
|
+
let activationComplete = false;
|
|
168
|
+
const pendingWaiters = new Set;
|
|
169
|
+
const aggregateHealth = () => {
|
|
170
|
+
if (lifecycle === "created" || lifecycle === "starting")
|
|
171
|
+
return "unknown";
|
|
172
|
+
let optionalUnhealthy = false;
|
|
173
|
+
for (const record of records.values()) {
|
|
174
|
+
if (record.entry.required && (record.state !== "ready" || record.health !== "healthy")) {
|
|
175
|
+
return "unhealthy";
|
|
176
|
+
}
|
|
177
|
+
if (!record.entry.required && (record.state !== "ready" || record.health !== "healthy")) {
|
|
178
|
+
optionalUnhealthy = true;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return optionalUnhealthy ? "degraded" : "healthy";
|
|
182
|
+
};
|
|
183
|
+
const isReady = () => lifecycle === "ready" && [...records.values()].every((record) => !record.entry.required || record.state === "ready" && record.health === "healthy");
|
|
184
|
+
let restartingIds = [];
|
|
185
|
+
const snapshot = () => ApplicationSnapshotSchema.parse({
|
|
186
|
+
id,
|
|
187
|
+
epoch,
|
|
188
|
+
revision,
|
|
189
|
+
lifecycle,
|
|
190
|
+
health: aggregateHealth(),
|
|
191
|
+
ready: isReady(),
|
|
192
|
+
capturedAt: new Date().toISOString(),
|
|
193
|
+
changedAt,
|
|
194
|
+
admission: { accepting, accepted, completed, pending },
|
|
195
|
+
restarting: [...restartingIds],
|
|
196
|
+
resources: ordered.map((entry) => {
|
|
197
|
+
const record = records.get(entry.id);
|
|
198
|
+
if (!record)
|
|
199
|
+
throw new Error("Managed resource record disappeared");
|
|
200
|
+
return {
|
|
201
|
+
id: entry.id,
|
|
202
|
+
required: entry.required,
|
|
203
|
+
dependsOn: entry.dependsOn,
|
|
204
|
+
state: record.state,
|
|
205
|
+
health: record.health,
|
|
206
|
+
ready: record.state === "ready" && record.health === "healthy"
|
|
207
|
+
};
|
|
208
|
+
})
|
|
209
|
+
});
|
|
210
|
+
const publish = () => {
|
|
211
|
+
revision += 1;
|
|
212
|
+
changedAt = new Date().toISOString();
|
|
213
|
+
const value = snapshot();
|
|
214
|
+
for (const listener of listeners) {
|
|
215
|
+
try {
|
|
216
|
+
listener(value);
|
|
217
|
+
} catch {}
|
|
218
|
+
}
|
|
219
|
+
if (config.onSnapshot) {
|
|
220
|
+
Promise.resolve().then(() => config.onSnapshot?.(value)).catch(() => {});
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
const contextFor = (record, options = {}) => ({
|
|
224
|
+
applicationId: id,
|
|
225
|
+
signal: options.signal ?? lifetimeAbort.signal,
|
|
226
|
+
...options.deadlineAt !== undefined && { deadlineAt: options.deadlineAt },
|
|
227
|
+
...options.forceDeadlineAt !== undefined && {
|
|
228
|
+
forceDeadlineAt: options.forceDeadlineAt
|
|
229
|
+
},
|
|
230
|
+
now: () => performance.now(),
|
|
231
|
+
use(resource) {
|
|
232
|
+
const dependencyId = resource?.id;
|
|
233
|
+
if (typeof dependencyId !== "string" || dependencyId.length === 0) {
|
|
234
|
+
throw new Error(`[stitchkit] resource "${record.entry.id}": use() takes a managed resource, and this one has no id`);
|
|
235
|
+
}
|
|
236
|
+
if (!record.entry.dependsOn.includes(dependencyId)) {
|
|
237
|
+
throw new Error(`[stitchkit] resource "${record.entry.id}" used "${dependencyId}" without declaring it in dependsOn`);
|
|
238
|
+
}
|
|
239
|
+
if (!published.has(dependencyId)) {
|
|
240
|
+
throw new Error(`[stitchkit] resource "${record.entry.id}" used "${dependencyId}", which published no value from start()`);
|
|
241
|
+
}
|
|
242
|
+
return published.get(dependencyId);
|
|
243
|
+
},
|
|
244
|
+
reportHealth(health) {
|
|
245
|
+
if (record.state === "stopped")
|
|
246
|
+
return;
|
|
247
|
+
record.health = health;
|
|
248
|
+
record.healthReported = true;
|
|
249
|
+
if (health === "healthy")
|
|
250
|
+
record.everHealthy = true;
|
|
251
|
+
accepting = activationComplete && !shutdownRequested && isReady();
|
|
252
|
+
publish();
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
const markLateCompletion = (record, failure) => {
|
|
256
|
+
if (shutdownRequested || record.state === "stopping" || record.state === "stopped")
|
|
257
|
+
return;
|
|
258
|
+
if (failure) {
|
|
259
|
+
record.failures.push("completion");
|
|
260
|
+
reportFailure(record.entry.id, "completion", failure.error);
|
|
261
|
+
}
|
|
262
|
+
record.state = "failed";
|
|
263
|
+
record.health = "unhealthy";
|
|
264
|
+
accepting = activationComplete && isReady();
|
|
265
|
+
publish();
|
|
266
|
+
};
|
|
267
|
+
const closeAttempted = async () => {
|
|
268
|
+
const errors = [];
|
|
269
|
+
const rollbackStartedAt = performance.now();
|
|
270
|
+
const rollbackDeadlineAt = rollbackStartedAt + shutdownBudget.gracePeriodMs;
|
|
271
|
+
const rollbackForceDeadlineAt = rollbackDeadlineAt + shutdownBudget.forceTimeoutMs;
|
|
272
|
+
const rollbackAbort = new AbortController;
|
|
273
|
+
const rollbackTimer = setTimeout(() => rollbackAbort.abort(), Math.max(0, rollbackForceDeadlineAt - performance.now()));
|
|
274
|
+
try {
|
|
275
|
+
await closeEachAttempted(rollbackAbort.signal, errors, {
|
|
276
|
+
deadlineAt: rollbackDeadlineAt,
|
|
277
|
+
forceDeadlineAt: rollbackForceDeadlineAt
|
|
278
|
+
});
|
|
279
|
+
} finally {
|
|
280
|
+
clearTimeout(rollbackTimer);
|
|
281
|
+
}
|
|
282
|
+
return errors;
|
|
283
|
+
};
|
|
284
|
+
const closeEachAttempted = async (bound, errors, deadlines) => {
|
|
285
|
+
for (const entry of reverse) {
|
|
286
|
+
const record = records.get(entry.id);
|
|
287
|
+
if (!record?.attempted || record.closed)
|
|
288
|
+
continue;
|
|
289
|
+
record.state = "stopping";
|
|
290
|
+
try {
|
|
291
|
+
record.closeInvoked = true;
|
|
292
|
+
const settled = await untilDeadline(Promise.resolve(entry.resource.close?.(contextFor(record, { signal: startupAbort.signal, ...deadlines }))), bound);
|
|
293
|
+
if (!settled.settled) {
|
|
294
|
+
record.failures.push("close");
|
|
295
|
+
record.state = "failed";
|
|
296
|
+
const timedOut = new Error(`[stitchkit] resource "${entry.id}" did not finish closing during rollback`);
|
|
297
|
+
reportFailure(entry.id, "close", timedOut);
|
|
298
|
+
errors.push(timedOut);
|
|
299
|
+
publish();
|
|
300
|
+
break;
|
|
301
|
+
}
|
|
302
|
+
if (settled.error !== undefined)
|
|
303
|
+
throw settled.error;
|
|
304
|
+
record.closed = true;
|
|
305
|
+
record.state = "stopped";
|
|
306
|
+
} catch (error) {
|
|
307
|
+
record.failures.push("close");
|
|
308
|
+
record.state = "failed";
|
|
309
|
+
reportFailure(entry.id, "close", error);
|
|
310
|
+
errors.push(error);
|
|
311
|
+
}
|
|
312
|
+
publish();
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
const startEach = async (entries, signal) => {
|
|
316
|
+
for (const entry of entries) {
|
|
317
|
+
if (shutdownRequested || signal.aborted) {
|
|
318
|
+
throw new ApplicationStartupInterruptedError;
|
|
319
|
+
}
|
|
320
|
+
const record = records.get(entry.id);
|
|
321
|
+
if (!record)
|
|
322
|
+
throw new Error("Managed resource record disappeared");
|
|
323
|
+
const dependencyFailed = entry.dependsOn.some((dependencyId) => records.get(dependencyId)?.state !== "ready");
|
|
324
|
+
if (dependencyFailed) {
|
|
325
|
+
record.state = "failed";
|
|
326
|
+
record.health = "unhealthy";
|
|
327
|
+
record.failures.push("start");
|
|
328
|
+
publish();
|
|
329
|
+
if (entry.required) {
|
|
330
|
+
throw new Error(`[stitchkit] required resource "${entry.id}" has an unavailable dependency`);
|
|
331
|
+
}
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
record.attempted = true;
|
|
335
|
+
record.state = "starting";
|
|
336
|
+
publish();
|
|
337
|
+
try {
|
|
338
|
+
const started = await entry.resource.start(contextFor(record, { signal }));
|
|
339
|
+
if (shutdownRequested || signal.aborted) {
|
|
340
|
+
throw new ApplicationStartupInterruptedError;
|
|
341
|
+
}
|
|
342
|
+
if (isStartResult(started)) {
|
|
343
|
+
record.runtime = started;
|
|
344
|
+
if (started.value !== undefined)
|
|
345
|
+
published.set(entry.id, started.value);
|
|
346
|
+
let resourceReady = started.ready === undefined;
|
|
347
|
+
let completionSettled = false;
|
|
348
|
+
let completionFailure;
|
|
349
|
+
const completion = started.completion?.then(() => {
|
|
350
|
+
completionSettled = true;
|
|
351
|
+
if (resourceReady)
|
|
352
|
+
markLateCompletion(record);
|
|
353
|
+
}, (error) => {
|
|
354
|
+
completionSettled = true;
|
|
355
|
+
completionFailure = error;
|
|
356
|
+
if (resourceReady)
|
|
357
|
+
markLateCompletion(record, { error });
|
|
358
|
+
});
|
|
359
|
+
if (started.ready && completion) {
|
|
360
|
+
const readiness = started.ready.then(() => "ready");
|
|
361
|
+
const completionBeforeReady = completion.then(() => "completion");
|
|
362
|
+
const first = await Promise.race([readiness, completionBeforeReady]);
|
|
363
|
+
if (first === "completion") {
|
|
364
|
+
throw new ResourceCompletionBeforeReadyError(entry.id, completionFailure);
|
|
365
|
+
}
|
|
366
|
+
resourceReady = true;
|
|
367
|
+
if (completionSettled) {
|
|
368
|
+
throw new ResourceCompletionBeforeReadyError(entry.id, completionFailure);
|
|
369
|
+
}
|
|
370
|
+
} else if (started.ready) {
|
|
371
|
+
await started.ready;
|
|
372
|
+
resourceReady = true;
|
|
373
|
+
} else if (completion) {}
|
|
374
|
+
}
|
|
375
|
+
if (shutdownRequested || signal.aborted) {
|
|
376
|
+
throw new ApplicationStartupInterruptedError;
|
|
377
|
+
}
|
|
378
|
+
record.state = "ready";
|
|
379
|
+
if (!record.healthReported) {
|
|
380
|
+
record.health = "healthy";
|
|
381
|
+
record.everHealthy = true;
|
|
382
|
+
}
|
|
383
|
+
publish();
|
|
384
|
+
} catch (error) {
|
|
385
|
+
const interrupted = shutdownRequested || signal.aborted;
|
|
386
|
+
if (!(error instanceof ApplicationStartupInterruptedError)) {
|
|
387
|
+
record.failures.push(error instanceof ResourceCompletionBeforeReadyError ? "completion" : record.runtime?.ready ? "ready" : "start");
|
|
388
|
+
record.state = "failed";
|
|
389
|
+
record.health = "unhealthy";
|
|
390
|
+
reportFailure(entry.id, record.failures[record.failures.length - 1] ?? "start", error);
|
|
391
|
+
publish();
|
|
392
|
+
}
|
|
393
|
+
if (interrupted || entry.required)
|
|
394
|
+
throw error;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
};
|
|
398
|
+
const activateEach = async (entries, signal) => {
|
|
399
|
+
for (const entry of entries) {
|
|
400
|
+
if (shutdownRequested || signal.aborted) {
|
|
401
|
+
throw new ApplicationStartupInterruptedError;
|
|
402
|
+
}
|
|
403
|
+
const record = records.get(entry.id);
|
|
404
|
+
if (record?.state !== "ready")
|
|
405
|
+
continue;
|
|
406
|
+
const dependencyUnavailable = entry.dependsOn.some((dependencyId) => {
|
|
407
|
+
const dependency = records.get(dependencyId);
|
|
408
|
+
return dependency?.state !== "ready" || !dependency.activated;
|
|
409
|
+
});
|
|
410
|
+
if (dependencyUnavailable) {
|
|
411
|
+
record.failures.push("start");
|
|
412
|
+
record.state = "failed";
|
|
413
|
+
record.health = "unhealthy";
|
|
414
|
+
publish();
|
|
415
|
+
if (entry.required) {
|
|
416
|
+
throw new Error(`[stitchkit] required resource "${entry.id}" has an unavailable activation dependency`);
|
|
417
|
+
}
|
|
418
|
+
continue;
|
|
419
|
+
}
|
|
420
|
+
try {
|
|
421
|
+
await entry.resource.activate?.(contextFor(record));
|
|
422
|
+
if (shutdownRequested || signal.aborted) {
|
|
423
|
+
throw new ApplicationStartupInterruptedError;
|
|
424
|
+
}
|
|
425
|
+
record.activated = true;
|
|
426
|
+
if (entry.required && (record.state !== "ready" || record.health !== "healthy")) {
|
|
427
|
+
const observed = `${record.state}/${record.health}`;
|
|
428
|
+
throw new Error(record.everHealthy ? `[stitchkit] required resource "${entry.id}" lost readiness during activation (${observed})` : `[stitchkit] required resource "${entry.id}" is not healthy (${observed}). A required resource must be healthy for the application to be ready; a resource that is expected to start degraded belongs behind \`required: false\`.`);
|
|
429
|
+
}
|
|
430
|
+
} catch (error) {
|
|
431
|
+
const interrupted = shutdownRequested || signal.aborted;
|
|
432
|
+
if (!(error instanceof ApplicationStartupInterruptedError)) {
|
|
433
|
+
record.failures.push("start");
|
|
434
|
+
record.state = "failed";
|
|
435
|
+
record.health = "unhealthy";
|
|
436
|
+
reportFailure(entry.id, "start", error);
|
|
437
|
+
publish();
|
|
438
|
+
}
|
|
439
|
+
if (interrupted || entry.required)
|
|
440
|
+
throw error;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
};
|
|
444
|
+
const subtreeOf = (resourceId) => {
|
|
445
|
+
const affected = new Set([resourceId]);
|
|
446
|
+
for (const entry of ordered) {
|
|
447
|
+
if (entry.dependsOn.some((dependencyId) => affected.has(dependencyId))) {
|
|
448
|
+
affected.add(entry.id);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
return ordered.filter((entry) => affected.has(entry.id));
|
|
452
|
+
};
|
|
453
|
+
const closeOne = async (entry, record, signal, deadlines) => {
|
|
454
|
+
const context = () => contextFor(record, { ...deadlines, signal });
|
|
455
|
+
if (record.attempted && !record.closed) {
|
|
456
|
+
const phase = async (work, what) => {
|
|
457
|
+
const settled = await untilDeadline(Promise.resolve(work), signal);
|
|
458
|
+
if (!settled.settled) {
|
|
459
|
+
throw new Error(`[stitchkit] resource "${entry.id}" did not ${what} within the restart budget`);
|
|
460
|
+
}
|
|
461
|
+
if (settled.error !== undefined)
|
|
462
|
+
throw settled.error;
|
|
463
|
+
};
|
|
464
|
+
await phase(entry.resource.stopAdmission?.(context()), "stop admitting");
|
|
465
|
+
await phase(entry.resource.drain?.(context()), "drain");
|
|
466
|
+
record.closeInvoked = true;
|
|
467
|
+
await phase(entry.resource.close?.(context()), "close");
|
|
468
|
+
}
|
|
469
|
+
record.closed = false;
|
|
470
|
+
record.closeInvoked = false;
|
|
471
|
+
record.attempted = false;
|
|
472
|
+
record.activated = false;
|
|
473
|
+
record.state = "registered";
|
|
474
|
+
record.health = "unknown";
|
|
475
|
+
record.healthReported = false;
|
|
476
|
+
record.runtime = undefined;
|
|
477
|
+
published.delete(entry.id);
|
|
478
|
+
};
|
|
479
|
+
let restarting = Promise.resolve();
|
|
480
|
+
const runRestart = async (input) => {
|
|
481
|
+
const startedAt = Date.now();
|
|
482
|
+
const parsed = ApplicationRestartInputSchema.parse(input);
|
|
483
|
+
const affected = subtreeOf(parsed.resourceId);
|
|
484
|
+
const affectedIds = affected.map((entry) => entry.id);
|
|
485
|
+
const refuse = (reason) => ({
|
|
486
|
+
resourceId: parsed.resourceId,
|
|
487
|
+
affected: affectedIds,
|
|
488
|
+
outcome: "refused",
|
|
489
|
+
reason,
|
|
490
|
+
durationMs: Date.now() - startedAt
|
|
491
|
+
});
|
|
492
|
+
if (!records.has(parsed.resourceId)) {
|
|
493
|
+
return refuse(`no resource is registered as "${parsed.resourceId}"`);
|
|
494
|
+
}
|
|
495
|
+
if (shutdownRequested) {
|
|
496
|
+
return refuse("the application is shutting down");
|
|
497
|
+
}
|
|
498
|
+
if (lifecycle !== "ready") {
|
|
499
|
+
return refuse(`the application is ${lifecycle}, not ready`);
|
|
500
|
+
}
|
|
501
|
+
const budget = {
|
|
502
|
+
gracePeriodMs: parsed.gracePeriodMs ?? shutdownBudget.gracePeriodMs,
|
|
503
|
+
forceTimeoutMs: parsed.forceTimeoutMs ?? shutdownBudget.forceTimeoutMs
|
|
504
|
+
};
|
|
505
|
+
const closeDeadlineAt = performance.now() + budget.gracePeriodMs;
|
|
506
|
+
const deadlines = {
|
|
507
|
+
deadlineAt: closeDeadlineAt,
|
|
508
|
+
forceDeadlineAt: closeDeadlineAt + budget.forceTimeoutMs
|
|
509
|
+
};
|
|
510
|
+
restartingIds = affectedIds;
|
|
511
|
+
const restartAbort = new AbortController;
|
|
512
|
+
const closeTimer = setTimeout(() => restartAbort.abort(), Math.max(0, deadlines.forceDeadlineAt - performance.now()));
|
|
513
|
+
const stillFailed = () => affectedIds.filter((id2) => records.get(id2)?.state === "failed");
|
|
514
|
+
try {
|
|
515
|
+
for (const entry of [...affected].reverse()) {
|
|
516
|
+
const record = records.get(entry.id);
|
|
517
|
+
if (record)
|
|
518
|
+
await closeOne(entry, record, restartAbort.signal, deadlines);
|
|
519
|
+
}
|
|
520
|
+
publish();
|
|
521
|
+
await startEach(affected, restartAbort.signal);
|
|
522
|
+
await activateEach(affected, restartAbort.signal);
|
|
523
|
+
publish();
|
|
524
|
+
const failed = stillFailed();
|
|
525
|
+
if (failed.length > 0) {
|
|
526
|
+
return {
|
|
527
|
+
resourceId: parsed.resourceId,
|
|
528
|
+
affected: affectedIds,
|
|
529
|
+
outcome: "failed",
|
|
530
|
+
reason: `did not come back: ${failed.join(", ")}`,
|
|
531
|
+
durationMs: Date.now() - startedAt
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
return {
|
|
535
|
+
resourceId: parsed.resourceId,
|
|
536
|
+
affected: affectedIds,
|
|
537
|
+
outcome: "restarted",
|
|
538
|
+
durationMs: Date.now() - startedAt
|
|
539
|
+
};
|
|
540
|
+
} catch (error) {
|
|
541
|
+
restartAbort.abort();
|
|
542
|
+
publish();
|
|
543
|
+
return {
|
|
544
|
+
resourceId: parsed.resourceId,
|
|
545
|
+
affected: affectedIds,
|
|
546
|
+
outcome: "failed",
|
|
547
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
548
|
+
durationMs: Date.now() - startedAt
|
|
549
|
+
};
|
|
550
|
+
} finally {
|
|
551
|
+
restartingIds = [];
|
|
552
|
+
clearTimeout(closeTimer);
|
|
553
|
+
publish();
|
|
554
|
+
}
|
|
555
|
+
};
|
|
556
|
+
const runStart = async () => {
|
|
557
|
+
lifecycle = "starting";
|
|
558
|
+
publish();
|
|
559
|
+
let startFailure;
|
|
560
|
+
try {
|
|
561
|
+
await startEach(ordered, startupAbort.signal);
|
|
562
|
+
lifecycle = "ready";
|
|
563
|
+
publish();
|
|
564
|
+
await activateEach(ordered, startupAbort.signal);
|
|
565
|
+
if (shutdownRequested) {
|
|
566
|
+
throw new ApplicationStartupInterruptedError;
|
|
567
|
+
}
|
|
568
|
+
if (!isReady()) {
|
|
569
|
+
const blocking = [...records.values()].filter((record) => record.entry.required && (record.state !== "ready" || record.health !== "healthy")).map((record) => `${record.entry.id} (${record.state}/${record.health})`);
|
|
570
|
+
if (blocking.length === 0)
|
|
571
|
+
blocking.push("(none identified)");
|
|
572
|
+
const chosen = [...records.values()].some((record) => record.entry.required && record.health !== "healthy" && !record.everHealthy);
|
|
573
|
+
throw new Error(`[stitchkit] the application is not ready after startup — required ${blocking.length === 1 ? "resource" : "resources"} ${blocking.join(", ")}. A required resource must be healthy for the application to be ready; ${chosen ? "a resource that is expected to start degraded belongs behind `required: false`." : "this one was healthy and stopped being so — read `onResourceFailure` for the cause."}`);
|
|
574
|
+
}
|
|
575
|
+
activationComplete = true;
|
|
576
|
+
accepting = isReady();
|
|
577
|
+
publish();
|
|
578
|
+
return snapshot();
|
|
579
|
+
} catch (error) {
|
|
580
|
+
startFailure = error;
|
|
581
|
+
}
|
|
582
|
+
if (!shutdownRequested) {
|
|
583
|
+
const rollbackErrors = await closeAttempted();
|
|
584
|
+
lifecycle = "failed";
|
|
585
|
+
accepting = false;
|
|
586
|
+
publish();
|
|
587
|
+
if (rollbackErrors.length > 0) {
|
|
588
|
+
throw new AggregateError([startFailure, ...rollbackErrors], "[stitchkit] application startup and rollback failed", { cause: startFailure });
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
throw startFailure;
|
|
592
|
+
};
|
|
593
|
+
const start = () => {
|
|
594
|
+
if (startPromise)
|
|
595
|
+
return startPromise;
|
|
596
|
+
if (lifecycle !== "created") {
|
|
597
|
+
return Promise.reject(new Error(`[stitchkit] application cannot start from lifecycle "${lifecycle}"`));
|
|
598
|
+
}
|
|
599
|
+
startPromise = runStart();
|
|
600
|
+
startPromise.catch(() => {
|
|
601
|
+
return;
|
|
602
|
+
});
|
|
603
|
+
return startPromise;
|
|
604
|
+
};
|
|
605
|
+
const acquire = () => {
|
|
606
|
+
if (!accepting || !isReady())
|
|
607
|
+
return null;
|
|
608
|
+
accepted += 1;
|
|
609
|
+
pending += 1;
|
|
610
|
+
publish();
|
|
611
|
+
let released = false;
|
|
612
|
+
return {
|
|
613
|
+
get released() {
|
|
614
|
+
return released;
|
|
615
|
+
},
|
|
616
|
+
release() {
|
|
617
|
+
if (released)
|
|
618
|
+
return;
|
|
619
|
+
released = true;
|
|
620
|
+
pending -= 1;
|
|
621
|
+
completed += 1;
|
|
622
|
+
if (pending === 0) {
|
|
623
|
+
for (const waiter of pendingWaiters)
|
|
624
|
+
waiter();
|
|
625
|
+
pendingWaiters.clear();
|
|
626
|
+
}
|
|
627
|
+
publish();
|
|
628
|
+
}
|
|
629
|
+
};
|
|
630
|
+
};
|
|
631
|
+
const run = async (work) => {
|
|
632
|
+
const lease = acquire();
|
|
633
|
+
if (!lease)
|
|
634
|
+
throw new ApplicationAdmissionError;
|
|
635
|
+
try {
|
|
636
|
+
return await work();
|
|
637
|
+
} finally {
|
|
638
|
+
lease.release();
|
|
639
|
+
}
|
|
640
|
+
};
|
|
641
|
+
const waitForPending = () => {
|
|
642
|
+
if (pending === 0)
|
|
643
|
+
return Promise.resolve();
|
|
644
|
+
return new Promise((resolve) => pendingWaiters.add(resolve));
|
|
645
|
+
};
|
|
646
|
+
const shutdown = (options) => {
|
|
647
|
+
if (shutdownPromise)
|
|
648
|
+
return shutdownPromise;
|
|
649
|
+
const requested = options ?? {};
|
|
650
|
+
const parsed = ApplicationShutdownOptionsSchema.parse({
|
|
651
|
+
gracePeriodMs: requested.gracePeriodMs ?? shutdownBudget.gracePeriodMs,
|
|
652
|
+
forceTimeoutMs: requested.forceTimeoutMs ?? shutdownBudget.forceTimeoutMs,
|
|
653
|
+
...requested.signal !== undefined && { signal: requested.signal }
|
|
654
|
+
});
|
|
655
|
+
const startedAt = performance.now();
|
|
656
|
+
const graceDeadlineAt = startedAt + parsed.gracePeriodMs;
|
|
657
|
+
const forceDeadlineAt = graceDeadlineAt + parsed.forceTimeoutMs;
|
|
658
|
+
shutdownRequested = true;
|
|
659
|
+
accepting = false;
|
|
660
|
+
startupAbort.abort();
|
|
661
|
+
lifecycle = lifecycle === "created" ? "stopping" : "draining";
|
|
662
|
+
publish();
|
|
663
|
+
shutdownPromise = (async () => {
|
|
664
|
+
const gracefulAbort = new AbortController;
|
|
665
|
+
let forcedReason;
|
|
666
|
+
let gracefulFailed = [...records.values()].some((record) => record.attempted && record.closeInvoked && !record.closed);
|
|
667
|
+
const force = (reason) => {
|
|
668
|
+
if (forcedReason)
|
|
669
|
+
return;
|
|
670
|
+
forcedReason = reason;
|
|
671
|
+
gracefulAbort.abort();
|
|
672
|
+
lifetimeAbort.abort();
|
|
673
|
+
};
|
|
674
|
+
const graceTimer = setTimeout(() => force("deadline"), Math.max(0, graceDeadlineAt - performance.now()));
|
|
675
|
+
const onExternalAbort = () => force("signal");
|
|
676
|
+
parsed.signal?.addEventListener("abort", onExternalAbort, { once: true });
|
|
677
|
+
if (parsed.signal?.aborted)
|
|
678
|
+
force("signal");
|
|
679
|
+
if (startPromise) {
|
|
680
|
+
await untilDeadline(startPromise.catch(() => {
|
|
681
|
+
return;
|
|
682
|
+
}), gracefulAbort.signal);
|
|
683
|
+
}
|
|
684
|
+
const gracefulContext = (record) => contextFor(record, {
|
|
685
|
+
signal: gracefulAbort.signal,
|
|
686
|
+
deadlineAt: graceDeadlineAt,
|
|
687
|
+
forceDeadlineAt
|
|
688
|
+
});
|
|
689
|
+
for (const entry of reverse) {
|
|
690
|
+
if (gracefulAbort.signal.aborted)
|
|
691
|
+
break;
|
|
692
|
+
const record = records.get(entry.id);
|
|
693
|
+
if (!record?.attempted || record.closed)
|
|
694
|
+
continue;
|
|
695
|
+
try {
|
|
696
|
+
const result = await untilDeadline(Promise.resolve(entry.resource.stopAdmission?.(gracefulContext(record))), gracefulAbort.signal);
|
|
697
|
+
if (!result.settled)
|
|
698
|
+
break;
|
|
699
|
+
if (result.error !== undefined)
|
|
700
|
+
throw result.error;
|
|
701
|
+
} catch (error) {
|
|
702
|
+
record.failures.push("admission");
|
|
703
|
+
reportFailure(entry.id, "admission", error);
|
|
704
|
+
gracefulFailed = true;
|
|
705
|
+
lifetimeAbort.abort();
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
if (!gracefulAbort.signal.aborted && !gracefulFailed) {
|
|
709
|
+
const result = await untilDeadline(waitForPending(), gracefulAbort.signal);
|
|
710
|
+
if (!result.settled)
|
|
711
|
+
force("deadline");
|
|
712
|
+
}
|
|
713
|
+
for (const entry of reverse) {
|
|
714
|
+
if (gracefulAbort.signal.aborted || gracefulFailed)
|
|
715
|
+
break;
|
|
716
|
+
const record = records.get(entry.id);
|
|
717
|
+
if (!record?.attempted || record.closed)
|
|
718
|
+
continue;
|
|
719
|
+
record.state = "stopping";
|
|
720
|
+
publish();
|
|
721
|
+
try {
|
|
722
|
+
const drained = await untilDeadline(Promise.resolve(entry.resource.drain?.(gracefulContext(record))), gracefulAbort.signal);
|
|
723
|
+
if (!drained.settled) {
|
|
724
|
+
force("deadline");
|
|
725
|
+
break;
|
|
726
|
+
}
|
|
727
|
+
if (drained.error !== undefined)
|
|
728
|
+
throw drained.error;
|
|
729
|
+
} catch (error) {
|
|
730
|
+
record.failures.push("drain");
|
|
731
|
+
reportFailure(entry.id, "drain", error);
|
|
732
|
+
gracefulFailed = true;
|
|
733
|
+
lifetimeAbort.abort();
|
|
734
|
+
break;
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
lifecycle = "stopping";
|
|
738
|
+
publish();
|
|
739
|
+
for (const entry of reverse) {
|
|
740
|
+
if (gracefulAbort.signal.aborted || gracefulFailed)
|
|
741
|
+
break;
|
|
742
|
+
const record = records.get(entry.id);
|
|
743
|
+
if (!record?.attempted || record.closed || record.closeInvoked)
|
|
744
|
+
continue;
|
|
745
|
+
try {
|
|
746
|
+
record.closeInvoked = true;
|
|
747
|
+
const closed = await untilDeadline(Promise.resolve(entry.resource.close?.(gracefulContext(record))), gracefulAbort.signal);
|
|
748
|
+
if (!closed.settled) {
|
|
749
|
+
force("deadline");
|
|
750
|
+
break;
|
|
751
|
+
}
|
|
752
|
+
if (closed.error !== undefined)
|
|
753
|
+
throw closed.error;
|
|
754
|
+
record.closed = true;
|
|
755
|
+
record.state = "stopped";
|
|
756
|
+
publish();
|
|
757
|
+
} catch (error) {
|
|
758
|
+
record.failures.push("close");
|
|
759
|
+
reportFailure(entry.id, "close", error);
|
|
760
|
+
record.state = "failed";
|
|
761
|
+
gracefulFailed = true;
|
|
762
|
+
lifetimeAbort.abort();
|
|
763
|
+
publish();
|
|
764
|
+
break;
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
clearTimeout(graceTimer);
|
|
768
|
+
parsed.signal?.removeEventListener("abort", onExternalAbort);
|
|
769
|
+
const mustForce = forcedReason !== undefined || gracefulFailed;
|
|
770
|
+
const pendingOperationsAtForce = mustForce ? pending : 0;
|
|
771
|
+
if (mustForce) {
|
|
772
|
+
lifetimeAbort.abort();
|
|
773
|
+
const forceAbort = new AbortController;
|
|
774
|
+
const forceTimer = setTimeout(() => forceAbort.abort(), Math.max(0, forceDeadlineAt - performance.now()));
|
|
775
|
+
await Promise.all(reverse.map(async (entry) => {
|
|
776
|
+
const record = records.get(entry.id);
|
|
777
|
+
if (!record?.attempted || record.closed)
|
|
778
|
+
return;
|
|
779
|
+
try {
|
|
780
|
+
let cleanup;
|
|
781
|
+
if (entry.resource.force) {
|
|
782
|
+
cleanup = Promise.resolve(entry.resource.force(contextFor(record, {
|
|
783
|
+
signal: forceAbort.signal,
|
|
784
|
+
deadlineAt: graceDeadlineAt,
|
|
785
|
+
forceDeadlineAt
|
|
786
|
+
})));
|
|
787
|
+
} else if (entry.resource.close && !record.closeInvoked) {
|
|
788
|
+
record.closeInvoked = true;
|
|
789
|
+
cleanup = Promise.resolve(entry.resource.close(contextFor(record, {
|
|
790
|
+
signal: forceAbort.signal,
|
|
791
|
+
deadlineAt: graceDeadlineAt,
|
|
792
|
+
forceDeadlineAt
|
|
793
|
+
})));
|
|
794
|
+
} else if (entry.resource.close) {
|
|
795
|
+
record.failures.push("force");
|
|
796
|
+
reportFailure(entry.id, "force", new Error(`[stitchkit] resource "${entry.id}" was already closing and did not settle before the force deadline`));
|
|
797
|
+
return;
|
|
798
|
+
}
|
|
799
|
+
const forced = await untilDeadline(cleanup ?? Promise.resolve(), forceAbort.signal);
|
|
800
|
+
if (!forced.settled || forced.error !== undefined) {
|
|
801
|
+
record.failures.push("force");
|
|
802
|
+
reportFailure(entry.id, "force", forced.settled ? forced.error : new Error("[stitchkit] forced cleanup did not settle in time"));
|
|
803
|
+
return;
|
|
804
|
+
}
|
|
805
|
+
record.closed = true;
|
|
806
|
+
record.state = "stopped";
|
|
807
|
+
} catch (error) {
|
|
808
|
+
record.failures.push("force");
|
|
809
|
+
reportFailure(entry.id, "force", error);
|
|
810
|
+
}
|
|
811
|
+
}));
|
|
812
|
+
clearTimeout(forceTimer);
|
|
813
|
+
}
|
|
814
|
+
const cleanupComplete = [...records.values()].every((record) => !record.attempted || record.closed);
|
|
815
|
+
lifecycle = cleanupComplete ? "stopped" : "failed";
|
|
816
|
+
publish();
|
|
817
|
+
return ApplicationShutdownResultSchema.parse({
|
|
818
|
+
outcome: mustForce ? "forced" : "clean",
|
|
819
|
+
...forcedReason && { reason: forcedReason },
|
|
820
|
+
cleanupComplete,
|
|
821
|
+
acceptedOperations: accepted,
|
|
822
|
+
completedOperations: completed,
|
|
823
|
+
pendingOperations: pending,
|
|
824
|
+
pendingOperationsAtForce,
|
|
825
|
+
resources: ordered.map((entry) => {
|
|
826
|
+
const record = records.get(entry.id);
|
|
827
|
+
if (!record)
|
|
828
|
+
throw new Error("Managed resource record disappeared");
|
|
829
|
+
return {
|
|
830
|
+
id: entry.id,
|
|
831
|
+
state: !record.attempted ? "not-started" : record.closed ? "closed" : "force-failed",
|
|
832
|
+
failures: record.failures
|
|
833
|
+
};
|
|
834
|
+
}),
|
|
835
|
+
durationMs: performance.now() - startedAt
|
|
836
|
+
});
|
|
837
|
+
})();
|
|
838
|
+
shutdownPromise.catch(() => {
|
|
839
|
+
return;
|
|
840
|
+
});
|
|
841
|
+
return shutdownPromise;
|
|
842
|
+
};
|
|
843
|
+
return {
|
|
844
|
+
id,
|
|
845
|
+
admission: { acquire, run },
|
|
846
|
+
start,
|
|
847
|
+
getSnapshot: snapshot,
|
|
848
|
+
subscribe(listener) {
|
|
849
|
+
listeners.add(listener);
|
|
850
|
+
listener(snapshot());
|
|
851
|
+
return () => listeners.delete(listener);
|
|
852
|
+
},
|
|
853
|
+
shutdown,
|
|
854
|
+
restart(input) {
|
|
855
|
+
const queued = restarting.then(() => runRestart(input));
|
|
856
|
+
restarting = queued.then(() => {
|
|
857
|
+
return;
|
|
858
|
+
}, () => {
|
|
859
|
+
return;
|
|
860
|
+
});
|
|
861
|
+
return queued;
|
|
862
|
+
}
|
|
863
|
+
};
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
export { ApplicationShutdownOptionsSchema, ApplicationShutdownBudgetSchema, ApplicationRestartInputSchema, ApplicationRestartOutcomeSchema, ApplicationRestartResultSchema, ApplicationAdmissionError, createApplication };
|