stitchkit 0.59.3 → 0.60.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 +14 -4
- package/dist/agent-runtime/admission-lanes.d.ts +35 -0
- package/dist/agent-runtime/admission-lanes.d.ts.map +1 -0
- package/dist/agent-runtime/coordinator.d.ts +36 -2
- package/dist/agent-runtime/coordinator.d.ts.map +1 -1
- package/dist/agent-runtime/events.d.ts +7 -0
- package/dist/agent-runtime/events.d.ts.map +1 -1
- package/dist/agent-runtime/history.d.ts +7 -0
- package/dist/agent-runtime/history.d.ts.map +1 -1
- package/dist/agent-runtime/models.d.ts +2 -3
- package/dist/agent-runtime/models.d.ts.map +1 -1
- package/dist/agent-runtime/observability.d.ts.map +1 -1
- package/dist/agent-runtime/run-execution.d.ts +31 -0
- package/dist/agent-runtime/run-execution.d.ts.map +1 -0
- package/dist/agent-runtime/runtime-internals.d.ts +25 -0
- package/dist/agent-runtime/runtime-internals.d.ts.map +1 -0
- package/dist/agent-runtime/runtime-result.d.ts +11 -0
- package/dist/agent-runtime/runtime-result.d.ts.map +1 -0
- package/dist/agent-runtime/runtime.d.ts +16 -15
- package/dist/agent-runtime/runtime.d.ts.map +1 -1
- package/dist/agent-runtime/schemas.d.ts.map +1 -1
- package/dist/agent-runtime/store-driver.d.ts.map +1 -1
- package/dist/agent-runtime/store.d.ts +9 -2
- package/dist/agent-runtime/store.d.ts.map +1 -1
- package/dist/agent-runtime/terminal-commit.d.ts.map +1 -1
- package/dist/agent-runtime/terminal-status.d.ts +13 -0
- package/dist/agent-runtime/terminal-status.d.ts.map +1 -0
- package/dist/agent-runtime.d.ts +5 -3
- package/dist/agent-runtime.d.ts.map +1 -1
- package/dist/agent-runtime.js +211 -156
- package/dist/application/activity.d.ts +7 -2
- package/dist/application/activity.d.ts.map +1 -1
- package/dist/application/grammy.d.ts +6 -0
- package/dist/application/grammy.d.ts.map +1 -1
- package/dist/application/health.d.ts +20 -1
- package/dist/application/health.d.ts.map +1 -1
- package/dist/application/kernel.d.ts +49 -3
- package/dist/application/kernel.d.ts.map +1 -1
- package/dist/application/opentelemetry.d.ts +25 -0
- package/dist/application/opentelemetry.d.ts.map +1 -0
- package/dist/application/schemas.d.ts +42 -0
- package/dist/application/schemas.d.ts.map +1 -1
- package/dist/application-opentelemetry.d.ts +2 -0
- package/dist/application-opentelemetry.d.ts.map +1 -0
- package/dist/application-opentelemetry.js +248 -0
- package/dist/application.d.ts +4 -4
- package/dist/application.d.ts.map +1 -1
- package/dist/application.js +69 -1243
- package/dist/cli.js +6 -6
- package/dist/contract/errors.d.ts +6 -0
- package/dist/contract/errors.d.ts.map +1 -1
- package/dist/contract/index.js +1 -1
- package/dist/declaration.d.ts +345 -0
- package/dist/declaration.d.ts.map +1 -0
- package/dist/declaration.js +183 -0
- package/dist/index-1zencgcb.js +651 -0
- package/dist/{index-sa2mbwa7.js → index-2akar0rq.js} +2 -2
- package/dist/{index-41wm56v0.js → index-3hn10n4c.js} +1 -1
- package/dist/{index-f6pymtqe.js → index-3w74r08v.js} +2 -2
- package/dist/{index-7c0gkyvj.js → index-6fbp58g8.js} +2 -2
- package/dist/{index-gnvzyw0k.js → index-8es1hrv4.js} +2 -2
- package/dist/index-8eywc9zv.js +100 -0
- package/dist/{index-3nwng7hs.js → index-8pc5s3pz.js} +4 -4
- package/dist/{index-28cqssm3.js → index-9h1vba8n.js} +6 -0
- package/dist/{index-0nc0cddp.js → index-a59da114.js} +17 -28
- package/dist/{index-0tszzfxp.js → index-f7521k0x.js} +25 -19
- package/dist/{index-y2rb7dwx.js → index-pzyt11ch.js} +5 -1
- package/dist/{index-mr617n62.js → index-svqhcrrj.js} +1 -1
- package/dist/{index-ps5pxp3h.js → index-v6e8hnkx.js} +7 -1
- package/dist/{index-1tkyng3g.js → index-vkk06pv1.js} +1 -9
- package/dist/index-x8v8b7rc.js +560 -0
- package/dist/index.js +1 -1
- package/dist/internal/observability-sink.d.ts.map +1 -1
- package/dist/internal/route-pattern.d.ts +9 -0
- package/dist/internal/route-pattern.d.ts.map +1 -1
- package/dist/node.d.ts +1 -1
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js +12 -12
- package/dist/observability/index.js +5 -5
- package/dist/observability/sanitize.d.ts +3 -1
- package/dist/observability/sanitize.d.ts.map +1 -1
- package/dist/remote.js +3 -3
- package/dist/server/error-hook.d.ts +10 -3
- package/dist/server/error-hook.d.ts.map +1 -1
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +16 -19
- package/dist/server/middleware/cors.d.ts +5 -2
- package/dist/server/middleware/cors.d.ts.map +1 -1
- package/dist/server/openapi.d.ts.map +1 -1
- package/dist/server/router.d.ts.map +1 -1
- package/dist/server/socket-io-config.d.ts +68 -3
- package/dist/server/socket-io-config.d.ts.map +1 -1
- package/dist/server/socket-io-node.d.ts +1 -1
- package/dist/server/socket-io-node.d.ts.map +1 -1
- package/dist/server/socket-io.d.ts +1 -1
- package/dist/server/socket-io.d.ts.map +1 -1
- package/dist/testing/managed-resource-conformance-contract.d.ts +114 -0
- package/dist/testing/managed-resource-conformance-contract.d.ts.map +1 -0
- package/dist/testing/managed-resource-conformance-driver.d.ts +20 -0
- package/dist/testing/managed-resource-conformance-driver.d.ts.map +1 -0
- package/dist/testing/managed-resource-conformance-scenarios.d.ts +33 -0
- package/dist/testing/managed-resource-conformance-scenarios.d.ts.map +1 -0
- package/dist/testing/managed-resource-conformance.d.ts +5 -0
- package/dist/testing/managed-resource-conformance.d.ts.map +1 -0
- package/dist/testing/surface-manifest.d.ts.map +1 -1
- package/dist/testing.d.ts +1 -0
- package/dist/testing.d.ts.map +1 -1
- package/dist/testing.js +664 -78
- package/dist/tools/flatten-join.d.ts +2 -0
- package/dist/tools/flatten-join.d.ts.map +1 -1
- package/dist/tools/flatten.d.ts.map +1 -1
- package/dist/tools/mcp-prepare.d.ts +0 -2
- package/dist/tools/mcp-prepare.d.ts.map +1 -1
- package/dist/tools/runtime-tool.d.ts +0 -3
- package/dist/tools/runtime-tool.d.ts.map +1 -1
- package/dist/tools/schema.d.ts +0 -4
- package/dist/tools/schema.d.ts.map +1 -1
- package/dist/tools.js +9 -9
- package/llms-full.txt +1212 -67
- package/llms.txt +2 -0
- package/package.json +16 -3
package/dist/application.js
CHANGED
|
@@ -2,352 +2,55 @@ import {
|
|
|
2
2
|
defineManagedResource
|
|
3
3
|
} from "./index-yr276yz0.js";
|
|
4
4
|
import {
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
ActivityIdSchema,
|
|
6
|
+
ActivityLiveStateSchema,
|
|
7
|
+
ActivitySnapshotSchema,
|
|
8
|
+
ActivityStageIdSchema,
|
|
9
|
+
ActivityStageSnapshotSchema,
|
|
10
|
+
ActivityTokenBrand,
|
|
11
|
+
ApplicationSnapshotSinkStatusSchema,
|
|
12
|
+
ManagedScheduleDescriptorSchema,
|
|
13
|
+
ManagedScheduleErrorPolicySchema,
|
|
14
|
+
ManagedScheduleOverlapSchema,
|
|
15
|
+
ManagedScheduleStatusSchema,
|
|
16
|
+
createActivityProjection,
|
|
17
|
+
createApplicationSnapshotSink,
|
|
18
|
+
createManagedSchedule
|
|
19
|
+
} from "./index-x8v8b7rc.js";
|
|
20
|
+
import {
|
|
21
|
+
ApplicationAdmissionError,
|
|
22
|
+
ApplicationShutdownOptionsSchema,
|
|
23
|
+
createApplication
|
|
24
|
+
} from "./index-1zencgcb.js";
|
|
25
|
+
import {
|
|
26
|
+
ApplicationAdmissionSnapshotSchema,
|
|
27
|
+
ApplicationHealthSchema,
|
|
28
|
+
ApplicationIdSchema,
|
|
29
|
+
ApplicationLifecycleSchema,
|
|
30
|
+
ApplicationResourceShutdownSchema,
|
|
31
|
+
ApplicationShutdownResultSchema,
|
|
32
|
+
ApplicationSnapshotSchema,
|
|
33
|
+
ApplicationStatusProjectionSchema,
|
|
34
|
+
ManagedResourceSnapshotSchema,
|
|
35
|
+
ManagedResourceStateSchema,
|
|
36
|
+
projectApplicationStatus
|
|
37
|
+
} from "./index-8eywc9zv.js";
|
|
38
|
+
import"./index-dk6e56g0.js";
|
|
7
39
|
import {
|
|
8
40
|
createBoundedSinkManager
|
|
9
|
-
} from "./index-
|
|
10
|
-
|
|
11
|
-
// src/application/activity.ts
|
|
12
|
-
import { z as z2 } from "zod";
|
|
13
|
-
|
|
14
|
-
// src/application/latest-sink.ts
|
|
15
|
-
import { z } from "zod";
|
|
16
|
-
var SnapshotRevisionSchema = z.number().int().nonnegative();
|
|
17
|
-
var ApplicationSnapshotSinkStatusSchema = z.object({
|
|
18
|
-
accepting: z.boolean(),
|
|
19
|
-
received: z.number().int().nonnegative(),
|
|
20
|
-
accepted: z.number().int().nonnegative(),
|
|
21
|
-
rejected: z.number().int().nonnegative(),
|
|
22
|
-
delivered: z.number().int().nonnegative(),
|
|
23
|
-
coalesced: z.number().int().nonnegative(),
|
|
24
|
-
failed: z.number().int().nonnegative(),
|
|
25
|
-
inFlight: z.boolean(),
|
|
26
|
-
pending: z.boolean(),
|
|
27
|
-
lastAcceptedRevision: SnapshotRevisionSchema.optional(),
|
|
28
|
-
lastDeliveredRevision: SnapshotRevisionSchema.optional()
|
|
29
|
-
}).strict().readonly();
|
|
30
|
-
function createApplicationSnapshotSink(config) {
|
|
31
|
-
let accepting = true;
|
|
32
|
-
let received = 0;
|
|
33
|
-
let accepted = 0;
|
|
34
|
-
let rejected = 0;
|
|
35
|
-
let delivered = 0;
|
|
36
|
-
let coalesced = 0;
|
|
37
|
-
let failed = 0;
|
|
38
|
-
let lastAcceptedRevision;
|
|
39
|
-
let lastDeliveredRevision;
|
|
40
|
-
let inFlight;
|
|
41
|
-
let pending;
|
|
42
|
-
let closePromise;
|
|
43
|
-
let resolveClose;
|
|
44
|
-
const getStatus = () => ApplicationSnapshotSinkStatusSchema.parse({
|
|
45
|
-
accepting,
|
|
46
|
-
received,
|
|
47
|
-
accepted,
|
|
48
|
-
rejected,
|
|
49
|
-
delivered,
|
|
50
|
-
coalesced,
|
|
51
|
-
failed,
|
|
52
|
-
inFlight: inFlight !== undefined,
|
|
53
|
-
pending: pending !== undefined,
|
|
54
|
-
...lastAcceptedRevision !== undefined && { lastAcceptedRevision },
|
|
55
|
-
...lastDeliveredRevision !== undefined && { lastDeliveredRevision }
|
|
56
|
-
});
|
|
57
|
-
const reportFailure = (error, snapshot) => {
|
|
58
|
-
if (!config.onSinkError)
|
|
59
|
-
return;
|
|
60
|
-
Promise.resolve().then(() => config.onSinkError?.({ error, snapshot })).catch(() => {
|
|
61
|
-
return;
|
|
62
|
-
});
|
|
63
|
-
};
|
|
64
|
-
const settleCloseIfIdle = () => {
|
|
65
|
-
if (accepting || inFlight || pending || !resolveClose)
|
|
66
|
-
return;
|
|
67
|
-
const resolve = resolveClose;
|
|
68
|
-
resolveClose = undefined;
|
|
69
|
-
resolve(getStatus());
|
|
70
|
-
};
|
|
71
|
-
const startWrite = (snapshot) => {
|
|
72
|
-
const write = Promise.resolve().then(() => config.write(snapshot)).then(() => {
|
|
73
|
-
delivered += 1;
|
|
74
|
-
lastDeliveredRevision = snapshot.revision;
|
|
75
|
-
}).catch((error) => {
|
|
76
|
-
failed += 1;
|
|
77
|
-
reportFailure(error, snapshot);
|
|
78
|
-
}).finally(() => {
|
|
79
|
-
inFlight = undefined;
|
|
80
|
-
const next = pending;
|
|
81
|
-
pending = undefined;
|
|
82
|
-
if (next)
|
|
83
|
-
startWrite(next);
|
|
84
|
-
else
|
|
85
|
-
settleCloseIfIdle();
|
|
86
|
-
});
|
|
87
|
-
inFlight = write;
|
|
88
|
-
};
|
|
89
|
-
return {
|
|
90
|
-
publish(snapshot) {
|
|
91
|
-
received += 1;
|
|
92
|
-
const revision = SnapshotRevisionSchema.parse(snapshot.revision);
|
|
93
|
-
if (!accepting || lastAcceptedRevision !== undefined && revision <= lastAcceptedRevision) {
|
|
94
|
-
rejected += 1;
|
|
95
|
-
return false;
|
|
96
|
-
}
|
|
97
|
-
accepted += 1;
|
|
98
|
-
lastAcceptedRevision = revision;
|
|
99
|
-
if (!inFlight) {
|
|
100
|
-
startWrite(snapshot);
|
|
101
|
-
} else {
|
|
102
|
-
if (pending)
|
|
103
|
-
coalesced += 1;
|
|
104
|
-
pending = snapshot;
|
|
105
|
-
}
|
|
106
|
-
return true;
|
|
107
|
-
},
|
|
108
|
-
getStatus,
|
|
109
|
-
close() {
|
|
110
|
-
if (closePromise)
|
|
111
|
-
return closePromise;
|
|
112
|
-
accepting = false;
|
|
113
|
-
closePromise = new Promise((resolve) => {
|
|
114
|
-
resolveClose = resolve;
|
|
115
|
-
settleCloseIfIdle();
|
|
116
|
-
});
|
|
117
|
-
return closePromise;
|
|
118
|
-
}
|
|
119
|
-
};
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// src/application/activity.ts
|
|
123
|
-
var BoundedOperationalIdSchema = z2.string().min(1).max(64).regex(/^[a-zA-Z0-9][a-zA-Z0-9._:-]*$/);
|
|
124
|
-
var NonNegativeIntegerSchema = z2.number().int().nonnegative();
|
|
125
|
-
var ActivityIdSchema = BoundedOperationalIdSchema;
|
|
126
|
-
var ActivityStageIdSchema = BoundedOperationalIdSchema;
|
|
127
|
-
var ActivityStageSnapshotSchema = z2.object({
|
|
128
|
-
id: ActivityStageIdSchema,
|
|
129
|
-
active: NonNegativeIntegerSchema,
|
|
130
|
-
queued: NonNegativeIntegerSchema,
|
|
131
|
-
completed: NonNegativeIntegerSchema,
|
|
132
|
-
failed: NonNegativeIntegerSchema
|
|
133
|
-
}).strict().readonly();
|
|
134
|
-
var ActivityTotalsSchema = z2.object({
|
|
135
|
-
active: NonNegativeIntegerSchema,
|
|
136
|
-
queued: NonNegativeIntegerSchema,
|
|
137
|
-
completed: NonNegativeIntegerSchema,
|
|
138
|
-
failed: NonNegativeIntegerSchema
|
|
139
|
-
}).strict().readonly();
|
|
140
|
-
var ActivitySnapshotSchema = z2.object({
|
|
141
|
-
id: ActivityIdSchema,
|
|
142
|
-
epoch: z2.string().uuid(),
|
|
143
|
-
revision: NonNegativeIntegerSchema,
|
|
144
|
-
capturedAt: z2.string().datetime({ offset: true }),
|
|
145
|
-
changedAt: z2.string().datetime({ offset: true }),
|
|
146
|
-
stages: z2.array(ActivityStageSnapshotSchema).min(1).max(64).readonly(),
|
|
147
|
-
totals: ActivityTotalsSchema
|
|
148
|
-
}).strict().readonly();
|
|
149
|
-
var ActivityLiveStateSchema = z2.enum(["active", "queued"]);
|
|
150
|
-
var ActivityTokenBrand = Symbol("stitchkit.application.activity");
|
|
151
|
-
function emptyCounters() {
|
|
152
|
-
return { active: 0, queued: 0, completed: 0, failed: 0 };
|
|
153
|
-
}
|
|
154
|
-
function totalsOf(stages) {
|
|
155
|
-
return stages.reduce((totals, stage) => ({
|
|
156
|
-
active: totals.active + stage.active,
|
|
157
|
-
queued: totals.queued + stage.queued,
|
|
158
|
-
completed: totals.completed + stage.completed,
|
|
159
|
-
failed: totals.failed + stage.failed
|
|
160
|
-
}), emptyCounters());
|
|
161
|
-
}
|
|
162
|
-
function createActivityProjection(config) {
|
|
163
|
-
const id = ActivityIdSchema.parse(config.id);
|
|
164
|
-
const declaredStages = z2.array(ActivityStageIdSchema).min(1).max(64).parse(config.stages);
|
|
165
|
-
const uniqueStages = new Set(declaredStages);
|
|
166
|
-
if (uniqueStages.size !== declaredStages.length) {
|
|
167
|
-
throw new Error("[stitchkit] createActivityProjection: stage ids must be unique");
|
|
168
|
-
}
|
|
169
|
-
const epoch = z2.string().uuid().parse(config.epoch ?? crypto.randomUUID());
|
|
170
|
-
const now = config.now ?? (() => new Date);
|
|
171
|
-
const counters = new Map(declaredStages.map((stage) => [stage, emptyCounters()]));
|
|
172
|
-
const activities = new WeakMap;
|
|
173
|
-
const subscribers = new Set;
|
|
174
|
-
let revision = 0;
|
|
175
|
-
let changedAt = now().toISOString();
|
|
176
|
-
const countersFor = (stage) => {
|
|
177
|
-
const value = counters.get(stage);
|
|
178
|
-
if (!value) {
|
|
179
|
-
throw new Error(`[stitchkit] createActivityProjection: undeclared stage "${stage}"`);
|
|
180
|
-
}
|
|
181
|
-
return value;
|
|
182
|
-
};
|
|
183
|
-
const getSnapshot = () => {
|
|
184
|
-
const stages = declaredStages.map((stage) => ({ id: stage, ...countersFor(stage) }));
|
|
185
|
-
return ActivitySnapshotSchema.parse({
|
|
186
|
-
id,
|
|
187
|
-
epoch,
|
|
188
|
-
revision,
|
|
189
|
-
capturedAt: now().toISOString(),
|
|
190
|
-
changedAt,
|
|
191
|
-
stages,
|
|
192
|
-
totals: totalsOf(stages)
|
|
193
|
-
});
|
|
194
|
-
};
|
|
195
|
-
const publishChange = () => {
|
|
196
|
-
revision += 1;
|
|
197
|
-
changedAt = now().toISOString();
|
|
198
|
-
const snapshot = getSnapshot();
|
|
199
|
-
for (const subscriber of subscribers)
|
|
200
|
-
subscriber.publish(snapshot);
|
|
201
|
-
};
|
|
202
|
-
const requireActivity = (token) => {
|
|
203
|
-
const activity = activities.get(token);
|
|
204
|
-
if (!activity) {
|
|
205
|
-
throw new Error("[stitchkit] activity token belongs to another projection");
|
|
206
|
-
}
|
|
207
|
-
return activity;
|
|
208
|
-
};
|
|
209
|
-
const leaveLiveState = (activity) => {
|
|
210
|
-
if (activity.state === "active" || activity.state === "queued") {
|
|
211
|
-
countersFor(activity.stage)[activity.state] -= 1;
|
|
212
|
-
}
|
|
213
|
-
};
|
|
214
|
-
const settle = (token, terminal) => {
|
|
215
|
-
const activity = requireActivity(token);
|
|
216
|
-
if (activity.state === "completed" || activity.state === "failed")
|
|
217
|
-
return false;
|
|
218
|
-
leaveLiveState(activity);
|
|
219
|
-
countersFor(activity.stage)[terminal] += 1;
|
|
220
|
-
activity.state = terminal;
|
|
221
|
-
publishChange();
|
|
222
|
-
return true;
|
|
223
|
-
};
|
|
224
|
-
return {
|
|
225
|
-
open(stage, rawState = "active") {
|
|
226
|
-
const parsedStage = ActivityStageIdSchema.parse(stage);
|
|
227
|
-
const state = ActivityLiveStateSchema.parse(rawState);
|
|
228
|
-
countersFor(parsedStage)[state] += 1;
|
|
229
|
-
const token = Object.freeze({
|
|
230
|
-
get [ActivityTokenBrand]() {
|
|
231
|
-
return true;
|
|
232
|
-
}
|
|
233
|
-
});
|
|
234
|
-
activities.set(token, { stage: parsedStage, state });
|
|
235
|
-
publishChange();
|
|
236
|
-
return token;
|
|
237
|
-
},
|
|
238
|
-
transition(token, input) {
|
|
239
|
-
const activity = requireActivity(token);
|
|
240
|
-
if (activity.state === "completed" || activity.state === "failed") {
|
|
241
|
-
throw new Error("[stitchkit] cannot transition a terminal activity");
|
|
242
|
-
}
|
|
243
|
-
const stage = ActivityStageIdSchema.parse(input.stage);
|
|
244
|
-
const state = ActivityLiveStateSchema.parse(input.state);
|
|
245
|
-
countersFor(stage);
|
|
246
|
-
if (activity.stage === stage && activity.state === state)
|
|
247
|
-
return false;
|
|
248
|
-
leaveLiveState(activity);
|
|
249
|
-
countersFor(stage)[state] += 1;
|
|
250
|
-
activity.stage = stage;
|
|
251
|
-
activity.state = state;
|
|
252
|
-
publishChange();
|
|
253
|
-
return true;
|
|
254
|
-
},
|
|
255
|
-
complete: (token) => settle(token, "completed"),
|
|
256
|
-
fail: (token) => settle(token, "failed"),
|
|
257
|
-
getSnapshot,
|
|
258
|
-
subscribe(listener) {
|
|
259
|
-
const sink = createApplicationSnapshotSink({
|
|
260
|
-
write: listener,
|
|
261
|
-
...config.onSubscriberError && { onSinkError: config.onSubscriberError }
|
|
262
|
-
});
|
|
263
|
-
subscribers.add(sink);
|
|
264
|
-
sink.publish(getSnapshot());
|
|
265
|
-
return () => {
|
|
266
|
-
subscribers.delete(sink);
|
|
267
|
-
sink.close();
|
|
268
|
-
};
|
|
269
|
-
}
|
|
270
|
-
};
|
|
271
|
-
}
|
|
272
|
-
// src/application/events.ts
|
|
273
|
-
import { z as z4 } from "zod";
|
|
274
|
-
|
|
275
|
-
// src/application/schemas.ts
|
|
276
|
-
import { z as z3 } from "zod";
|
|
277
|
-
var ApplicationIdSchema = z3.string().min(1).max(128).regex(/^[a-zA-Z0-9][a-zA-Z0-9._:-]*$/);
|
|
278
|
-
var ApplicationLifecycleSchema = z3.enum([
|
|
279
|
-
"created",
|
|
280
|
-
"starting",
|
|
281
|
-
"ready",
|
|
282
|
-
"draining",
|
|
283
|
-
"stopping",
|
|
284
|
-
"stopped",
|
|
285
|
-
"failed"
|
|
286
|
-
]);
|
|
287
|
-
var ApplicationHealthSchema = z3.enum(["unknown", "healthy", "degraded", "unhealthy"]);
|
|
288
|
-
var ManagedResourceStateSchema = z3.enum([
|
|
289
|
-
"registered",
|
|
290
|
-
"starting",
|
|
291
|
-
"ready",
|
|
292
|
-
"failed",
|
|
293
|
-
"stopping",
|
|
294
|
-
"stopped"
|
|
295
|
-
]);
|
|
296
|
-
var NonNegativeIntegerSchema2 = z3.number().int().nonnegative();
|
|
297
|
-
var ApplicationAdmissionSnapshotSchema = z3.object({
|
|
298
|
-
accepting: z3.boolean(),
|
|
299
|
-
accepted: NonNegativeIntegerSchema2,
|
|
300
|
-
completed: NonNegativeIntegerSchema2,
|
|
301
|
-
pending: NonNegativeIntegerSchema2
|
|
302
|
-
}).readonly();
|
|
303
|
-
var ManagedResourceSnapshotSchema = z3.object({
|
|
304
|
-
id: ApplicationIdSchema,
|
|
305
|
-
required: z3.boolean(),
|
|
306
|
-
dependsOn: z3.array(ApplicationIdSchema).readonly(),
|
|
307
|
-
state: ManagedResourceStateSchema,
|
|
308
|
-
health: ApplicationHealthSchema,
|
|
309
|
-
ready: z3.boolean()
|
|
310
|
-
}).readonly();
|
|
311
|
-
var ApplicationSnapshotSchema = z3.object({
|
|
312
|
-
id: ApplicationIdSchema,
|
|
313
|
-
epoch: z3.string().uuid(),
|
|
314
|
-
revision: NonNegativeIntegerSchema2,
|
|
315
|
-
lifecycle: ApplicationLifecycleSchema,
|
|
316
|
-
health: ApplicationHealthSchema,
|
|
317
|
-
ready: z3.boolean(),
|
|
318
|
-
capturedAt: z3.string().datetime({ offset: true }),
|
|
319
|
-
changedAt: z3.string().datetime({ offset: true }),
|
|
320
|
-
admission: ApplicationAdmissionSnapshotSchema,
|
|
321
|
-
resources: z3.array(ManagedResourceSnapshotSchema).readonly()
|
|
322
|
-
}).readonly();
|
|
323
|
-
var ApplicationResourceShutdownSchema = z3.object({
|
|
324
|
-
id: ApplicationIdSchema,
|
|
325
|
-
state: z3.enum(["not-started", "closed", "force-failed"]),
|
|
326
|
-
failures: z3.array(z3.enum(["start", "ready", "completion", "admission", "drain", "close", "force"])).readonly()
|
|
327
|
-
}).readonly();
|
|
328
|
-
var ApplicationShutdownResultSchema = z3.object({
|
|
329
|
-
outcome: z3.enum(["clean", "forced"]),
|
|
330
|
-
reason: z3.enum(["deadline", "signal"]).optional(),
|
|
331
|
-
cleanupComplete: z3.boolean(),
|
|
332
|
-
acceptedOperations: NonNegativeIntegerSchema2,
|
|
333
|
-
completedOperations: NonNegativeIntegerSchema2,
|
|
334
|
-
pendingOperations: NonNegativeIntegerSchema2,
|
|
335
|
-
pendingOperationsAtForce: NonNegativeIntegerSchema2,
|
|
336
|
-
resources: z3.array(ApplicationResourceShutdownSchema).readonly(),
|
|
337
|
-
durationMs: z3.number().nonnegative()
|
|
338
|
-
}).readonly();
|
|
339
|
-
|
|
41
|
+
} from "./index-a59da114.js";
|
|
340
42
|
// src/application/events.ts
|
|
341
|
-
|
|
342
|
-
|
|
43
|
+
import { z } from "zod";
|
|
44
|
+
var ApplicationLifecycleEventSchema = z.object({
|
|
45
|
+
type: z.literal("application-state"),
|
|
343
46
|
applicationId: ApplicationIdSchema,
|
|
344
|
-
epoch:
|
|
345
|
-
revision:
|
|
47
|
+
epoch: z.string().uuid(),
|
|
48
|
+
revision: z.number().int().nonnegative(),
|
|
346
49
|
lifecycle: ApplicationLifecycleSchema,
|
|
347
50
|
health: ApplicationHealthSchema,
|
|
348
|
-
ready:
|
|
349
|
-
capturedAt:
|
|
350
|
-
resources:
|
|
51
|
+
ready: z.boolean(),
|
|
52
|
+
capturedAt: z.string().datetime({ offset: true }),
|
|
53
|
+
resources: z.array(ManagedResourceSnapshotSchema).readonly()
|
|
351
54
|
}).strict().readonly();
|
|
352
55
|
function applicationLifecycleEvent(snapshot) {
|
|
353
56
|
return ApplicationLifecycleEventSchema.parse({
|
|
@@ -378,920 +81,37 @@ function createApplicationEventSink(config) {
|
|
|
378
81
|
};
|
|
379
82
|
}
|
|
380
83
|
// src/application/health.ts
|
|
381
|
-
import { z as
|
|
382
|
-
var ApplicationHealthHandlerOptionsSchema =
|
|
383
|
-
kind:
|
|
384
|
-
retryAfterSeconds:
|
|
84
|
+
import { z as z2 } from "zod";
|
|
85
|
+
var ApplicationHealthHandlerOptionsSchema = z2.object({
|
|
86
|
+
kind: z2.enum(["liveness", "readiness"]),
|
|
87
|
+
retryAfterSeconds: z2.number().int().nonnegative().default(5)
|
|
88
|
+
});
|
|
89
|
+
var ApplicationOperationalHandlersOptionsSchema = z2.object({
|
|
90
|
+
retryAfterSeconds: z2.number().int().nonnegative().default(5)
|
|
385
91
|
});
|
|
386
92
|
function createApplicationHealthHandler(application, options) {
|
|
387
93
|
const parsed = ApplicationHealthHandlerOptionsSchema.parse(options);
|
|
388
94
|
return () => {
|
|
389
95
|
const snapshot = application.getSnapshot();
|
|
390
96
|
const healthy = parsed.kind === "readiness" ? snapshot.ready : snapshot.lifecycle !== "failed" && snapshot.lifecycle !== "stopped";
|
|
391
|
-
return Response.json(snapshot, {
|
|
97
|
+
return Response.json(projectApplicationStatus(snapshot), {
|
|
392
98
|
status: healthy ? 200 : 503,
|
|
393
99
|
...healthy ? {} : { headers: { "Retry-After": String(parsed.retryAfterSeconds) } }
|
|
394
100
|
});
|
|
395
101
|
};
|
|
396
102
|
}
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
const entries = resources.map((resource, declarationIndex) => ({
|
|
400
|
-
resource,
|
|
401
|
-
id: ApplicationIdSchema.parse(resource.id),
|
|
402
|
-
dependsOn: [...resource.dependsOn ?? []].map((id) => ApplicationIdSchema.parse(id)),
|
|
403
|
-
required: resource.required ?? true,
|
|
404
|
-
declarationIndex
|
|
405
|
-
}));
|
|
406
|
-
const byId = new Map;
|
|
407
|
-
for (const entry of entries) {
|
|
408
|
-
if (byId.has(entry.id)) {
|
|
409
|
-
throw new Error(`[stitchkit] createApplication: duplicate resource id "${entry.id}"`);
|
|
410
|
-
}
|
|
411
|
-
byId.set(entry.id, entry);
|
|
412
|
-
}
|
|
413
|
-
for (const entry of entries) {
|
|
414
|
-
for (const dependencyId of entry.dependsOn) {
|
|
415
|
-
const dependency = byId.get(dependencyId);
|
|
416
|
-
if (!dependency) {
|
|
417
|
-
throw new Error(`[stitchkit] createApplication: resource "${entry.id}" depends on missing resource "${dependencyId}"`);
|
|
418
|
-
}
|
|
419
|
-
if (entry.required && !dependency.required) {
|
|
420
|
-
throw new Error(`[stitchkit] createApplication: required resource "${entry.id}" cannot depend on optional resource "${dependencyId}"`);
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
const pending = new Map(entries.map((entry) => [entry.id, entry]));
|
|
425
|
-
const resolved = new Set;
|
|
426
|
-
const ordered = [];
|
|
427
|
-
while (pending.size > 0) {
|
|
428
|
-
const ready = [...pending.values()].filter((entry) => entry.dependsOn.every((id) => resolved.has(id))).sort((left, right) => left.declarationIndex - right.declarationIndex);
|
|
429
|
-
if (ready.length === 0) {
|
|
430
|
-
throw new Error(`[stitchkit] createApplication: resource dependency cycle: ${[...pending.keys()].join(", ")}`);
|
|
431
|
-
}
|
|
432
|
-
for (const entry of ready) {
|
|
433
|
-
pending.delete(entry.id);
|
|
434
|
-
resolved.add(entry.id);
|
|
435
|
-
ordered.push(entry);
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
return ordered;
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
// src/application/kernel.ts
|
|
442
|
-
class ResourceCompletionBeforeReadyError extends Error {
|
|
443
|
-
constructor(resourceId, cause) {
|
|
444
|
-
super(`[stitchkit] resource "${resourceId}" completed before reaching readiness`, {
|
|
445
|
-
...cause !== undefined && { cause }
|
|
446
|
-
});
|
|
447
|
-
this.name = "ResourceCompletionBeforeReadyError";
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
class ApplicationAdmissionError extends Error {
|
|
452
|
-
code = "APPLICATION_NOT_ACCEPTING";
|
|
453
|
-
constructor() {
|
|
454
|
-
super("Application is not accepting new operations");
|
|
455
|
-
this.name = "ApplicationAdmissionError";
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
function isStartResult(value) {
|
|
459
|
-
return typeof value === "object" && value !== null;
|
|
460
|
-
}
|
|
461
|
-
function waitForAbort(signal) {
|
|
462
|
-
if (signal.aborted)
|
|
463
|
-
return Promise.resolve();
|
|
464
|
-
return new Promise((resolve) => signal.addEventListener("abort", () => resolve(), { once: true }));
|
|
465
|
-
}
|
|
466
|
-
async function untilDeadline(work, signal) {
|
|
467
|
-
const result = await Promise.race([
|
|
468
|
-
work.then((value) => ({ settled: true, value }), (error) => ({ settled: true, error })),
|
|
469
|
-
waitForAbort(signal).then(() => ({ settled: false }))
|
|
470
|
-
]);
|
|
471
|
-
return result;
|
|
472
|
-
}
|
|
473
|
-
function createApplication(config) {
|
|
474
|
-
const id = ApplicationIdSchema.parse(config.id);
|
|
475
|
-
const ordered = resolveResourceGraph(config.resources ?? []);
|
|
476
|
-
const reverse = [...ordered].reverse();
|
|
477
|
-
const records = new Map;
|
|
478
|
-
for (const entry of ordered) {
|
|
479
|
-
records.set(entry.id, {
|
|
480
|
-
entry,
|
|
481
|
-
state: "registered",
|
|
482
|
-
health: "unknown",
|
|
483
|
-
attempted: false,
|
|
484
|
-
activated: false,
|
|
485
|
-
closeInvoked: false,
|
|
486
|
-
closed: false,
|
|
487
|
-
failures: []
|
|
488
|
-
});
|
|
489
|
-
}
|
|
490
|
-
const epoch = crypto.randomUUID();
|
|
491
|
-
const listeners = new Set;
|
|
492
|
-
const lifetimeAbort = new AbortController;
|
|
493
|
-
const startupAbort = new AbortController;
|
|
494
|
-
let lifecycle = "created";
|
|
495
|
-
let revision = 0;
|
|
496
|
-
let changedAt = new Date().toISOString();
|
|
497
|
-
let accepting = false;
|
|
498
|
-
let accepted = 0;
|
|
499
|
-
let completed = 0;
|
|
500
|
-
let pending = 0;
|
|
501
|
-
let startPromise;
|
|
502
|
-
let shutdownPromise;
|
|
503
|
-
let shutdownRequested = false;
|
|
504
|
-
let activationComplete = false;
|
|
505
|
-
const pendingWaiters = new Set;
|
|
506
|
-
const aggregateHealth = () => {
|
|
507
|
-
if (lifecycle === "created" || lifecycle === "starting")
|
|
508
|
-
return "unknown";
|
|
509
|
-
let optionalUnhealthy = false;
|
|
510
|
-
for (const record of records.values()) {
|
|
511
|
-
if (record.entry.required && (record.state !== "ready" || record.health !== "healthy")) {
|
|
512
|
-
return "unhealthy";
|
|
513
|
-
}
|
|
514
|
-
if (!record.entry.required && (record.state !== "ready" || record.health !== "healthy")) {
|
|
515
|
-
optionalUnhealthy = true;
|
|
516
|
-
}
|
|
517
|
-
}
|
|
518
|
-
return optionalUnhealthy ? "degraded" : "healthy";
|
|
519
|
-
};
|
|
520
|
-
const isReady = () => lifecycle === "ready" && [...records.values()].every((record) => !record.entry.required || record.state === "ready" && record.health === "healthy");
|
|
521
|
-
const snapshot = () => ApplicationSnapshotSchema.parse({
|
|
522
|
-
id,
|
|
523
|
-
epoch,
|
|
524
|
-
revision,
|
|
525
|
-
lifecycle,
|
|
526
|
-
health: aggregateHealth(),
|
|
527
|
-
ready: isReady(),
|
|
528
|
-
capturedAt: new Date().toISOString(),
|
|
529
|
-
changedAt,
|
|
530
|
-
admission: { accepting, accepted, completed, pending },
|
|
531
|
-
resources: ordered.map((entry) => {
|
|
532
|
-
const record = records.get(entry.id);
|
|
533
|
-
if (!record)
|
|
534
|
-
throw new Error("Managed resource record disappeared");
|
|
535
|
-
return {
|
|
536
|
-
id: entry.id,
|
|
537
|
-
required: entry.required,
|
|
538
|
-
dependsOn: entry.dependsOn,
|
|
539
|
-
state: record.state,
|
|
540
|
-
health: record.health,
|
|
541
|
-
ready: record.state === "ready" && record.health === "healthy"
|
|
542
|
-
};
|
|
543
|
-
})
|
|
544
|
-
});
|
|
545
|
-
const publish = () => {
|
|
546
|
-
revision += 1;
|
|
547
|
-
changedAt = new Date().toISOString();
|
|
548
|
-
const value = snapshot();
|
|
549
|
-
for (const listener of listeners) {
|
|
550
|
-
try {
|
|
551
|
-
listener(value);
|
|
552
|
-
} catch {}
|
|
553
|
-
}
|
|
554
|
-
if (config.onSnapshot) {
|
|
555
|
-
Promise.resolve().then(() => config.onSnapshot?.(value)).catch(() => {});
|
|
556
|
-
}
|
|
557
|
-
};
|
|
558
|
-
const contextFor = (record, options = {}) => ({
|
|
559
|
-
applicationId: id,
|
|
560
|
-
signal: options.signal ?? lifetimeAbort.signal,
|
|
561
|
-
...options.deadlineAt !== undefined && { deadlineAt: options.deadlineAt },
|
|
562
|
-
...options.forceDeadlineAt !== undefined && {
|
|
563
|
-
forceDeadlineAt: options.forceDeadlineAt
|
|
564
|
-
},
|
|
565
|
-
now: () => performance.now(),
|
|
566
|
-
reportHealth(health) {
|
|
567
|
-
if (record.state === "stopped")
|
|
568
|
-
return;
|
|
569
|
-
record.health = health;
|
|
570
|
-
accepting = activationComplete && !shutdownRequested && isReady();
|
|
571
|
-
publish();
|
|
572
|
-
}
|
|
573
|
-
});
|
|
574
|
-
const markLateCompletion = (record, failure) => {
|
|
575
|
-
if (shutdownRequested || record.state === "stopping" || record.state === "stopped")
|
|
576
|
-
return;
|
|
577
|
-
if (failure)
|
|
578
|
-
record.failures.push("completion");
|
|
579
|
-
record.state = "failed";
|
|
580
|
-
record.health = "unhealthy";
|
|
581
|
-
accepting = activationComplete && isReady();
|
|
582
|
-
publish();
|
|
583
|
-
};
|
|
584
|
-
const closeAttempted = async () => {
|
|
585
|
-
const errors = [];
|
|
586
|
-
for (const entry of reverse) {
|
|
587
|
-
const record = records.get(entry.id);
|
|
588
|
-
if (!record?.attempted || record.closed)
|
|
589
|
-
continue;
|
|
590
|
-
record.state = "stopping";
|
|
591
|
-
try {
|
|
592
|
-
record.closeInvoked = true;
|
|
593
|
-
await entry.resource.close?.(contextFor(record, { signal: startupAbort.signal }));
|
|
594
|
-
record.closed = true;
|
|
595
|
-
record.state = "stopped";
|
|
596
|
-
} catch (error) {
|
|
597
|
-
record.failures.push("close");
|
|
598
|
-
record.state = "failed";
|
|
599
|
-
errors.push(error);
|
|
600
|
-
}
|
|
601
|
-
publish();
|
|
602
|
-
}
|
|
603
|
-
return errors;
|
|
604
|
-
};
|
|
605
|
-
const runStart = async () => {
|
|
606
|
-
lifecycle = "starting";
|
|
607
|
-
publish();
|
|
608
|
-
let startFailure;
|
|
609
|
-
try {
|
|
610
|
-
for (const entry of ordered) {
|
|
611
|
-
if (shutdownRequested || startupAbort.signal.aborted) {
|
|
612
|
-
throw new Error("[stitchkit] application startup interrupted by shutdown");
|
|
613
|
-
}
|
|
614
|
-
const record = records.get(entry.id);
|
|
615
|
-
if (!record)
|
|
616
|
-
throw new Error("Managed resource record disappeared");
|
|
617
|
-
const dependencyFailed = entry.dependsOn.some((dependencyId) => records.get(dependencyId)?.state !== "ready");
|
|
618
|
-
if (dependencyFailed) {
|
|
619
|
-
record.state = "failed";
|
|
620
|
-
record.health = "unhealthy";
|
|
621
|
-
record.failures.push("start");
|
|
622
|
-
publish();
|
|
623
|
-
if (entry.required) {
|
|
624
|
-
throw new Error(`[stitchkit] required resource "${entry.id}" has an unavailable dependency`);
|
|
625
|
-
}
|
|
626
|
-
continue;
|
|
627
|
-
}
|
|
628
|
-
record.attempted = true;
|
|
629
|
-
record.state = "starting";
|
|
630
|
-
publish();
|
|
631
|
-
try {
|
|
632
|
-
const started = await entry.resource.start(contextFor(record, { signal: startupAbort.signal }));
|
|
633
|
-
if (shutdownRequested || startupAbort.signal.aborted) {
|
|
634
|
-
throw new Error("[stitchkit] application startup interrupted by shutdown");
|
|
635
|
-
}
|
|
636
|
-
if (isStartResult(started)) {
|
|
637
|
-
record.runtime = started;
|
|
638
|
-
let resourceReady = started.ready === undefined;
|
|
639
|
-
let completionSettled = false;
|
|
640
|
-
let completionFailure;
|
|
641
|
-
const completion = started.completion?.then(() => {
|
|
642
|
-
completionSettled = true;
|
|
643
|
-
if (resourceReady)
|
|
644
|
-
markLateCompletion(record, false);
|
|
645
|
-
}, (error) => {
|
|
646
|
-
completionSettled = true;
|
|
647
|
-
completionFailure = error;
|
|
648
|
-
if (resourceReady)
|
|
649
|
-
markLateCompletion(record, true);
|
|
650
|
-
});
|
|
651
|
-
if (started.ready && completion) {
|
|
652
|
-
const readiness = started.ready.then(() => "ready");
|
|
653
|
-
const completionBeforeReady = completion.then(() => "completion");
|
|
654
|
-
const first = await Promise.race([readiness, completionBeforeReady]);
|
|
655
|
-
if (first === "completion") {
|
|
656
|
-
throw new ResourceCompletionBeforeReadyError(entry.id, completionFailure);
|
|
657
|
-
}
|
|
658
|
-
resourceReady = true;
|
|
659
|
-
if (completionSettled) {
|
|
660
|
-
throw new ResourceCompletionBeforeReadyError(entry.id, completionFailure);
|
|
661
|
-
}
|
|
662
|
-
} else if (started.ready) {
|
|
663
|
-
await started.ready;
|
|
664
|
-
resourceReady = true;
|
|
665
|
-
} else if (completion) {}
|
|
666
|
-
}
|
|
667
|
-
if (shutdownRequested || startupAbort.signal.aborted) {
|
|
668
|
-
throw new Error("[stitchkit] application startup interrupted by shutdown");
|
|
669
|
-
}
|
|
670
|
-
record.state = "ready";
|
|
671
|
-
record.health = "healthy";
|
|
672
|
-
publish();
|
|
673
|
-
} catch (error) {
|
|
674
|
-
if (shutdownRequested || startupAbort.signal.aborted)
|
|
675
|
-
throw error;
|
|
676
|
-
record.failures.push(error instanceof ResourceCompletionBeforeReadyError ? "completion" : record.runtime?.ready ? "ready" : "start");
|
|
677
|
-
record.state = "failed";
|
|
678
|
-
record.health = "unhealthy";
|
|
679
|
-
publish();
|
|
680
|
-
if (entry.required)
|
|
681
|
-
throw error;
|
|
682
|
-
}
|
|
683
|
-
}
|
|
684
|
-
lifecycle = "ready";
|
|
685
|
-
publish();
|
|
686
|
-
for (const entry of ordered) {
|
|
687
|
-
if (shutdownRequested || startupAbort.signal.aborted) {
|
|
688
|
-
throw new Error("[stitchkit] application startup interrupted by shutdown");
|
|
689
|
-
}
|
|
690
|
-
const record = records.get(entry.id);
|
|
691
|
-
if (record?.state !== "ready")
|
|
692
|
-
continue;
|
|
693
|
-
const dependencyUnavailable = entry.dependsOn.some((dependencyId) => {
|
|
694
|
-
const dependency = records.get(dependencyId);
|
|
695
|
-
return dependency?.state !== "ready" || !dependency.activated;
|
|
696
|
-
});
|
|
697
|
-
if (dependencyUnavailable) {
|
|
698
|
-
record.failures.push("start");
|
|
699
|
-
record.state = "failed";
|
|
700
|
-
record.health = "unhealthy";
|
|
701
|
-
publish();
|
|
702
|
-
if (entry.required) {
|
|
703
|
-
throw new Error(`[stitchkit] required resource "${entry.id}" has an unavailable activation dependency`);
|
|
704
|
-
}
|
|
705
|
-
continue;
|
|
706
|
-
}
|
|
707
|
-
try {
|
|
708
|
-
await entry.resource.activate?.(contextFor(record));
|
|
709
|
-
if (shutdownRequested || startupAbort.signal.aborted) {
|
|
710
|
-
throw new Error("[stitchkit] application startup interrupted by shutdown");
|
|
711
|
-
}
|
|
712
|
-
record.activated = true;
|
|
713
|
-
if (entry.required && (record.state !== "ready" || record.health !== "healthy")) {
|
|
714
|
-
throw new Error(`[stitchkit] required resource "${entry.id}" lost readiness during activation`);
|
|
715
|
-
}
|
|
716
|
-
} catch (error) {
|
|
717
|
-
if (shutdownRequested || startupAbort.signal.aborted)
|
|
718
|
-
throw error;
|
|
719
|
-
record.failures.push("start");
|
|
720
|
-
record.state = "failed";
|
|
721
|
-
record.health = "unhealthy";
|
|
722
|
-
publish();
|
|
723
|
-
if (entry.required)
|
|
724
|
-
throw error;
|
|
725
|
-
}
|
|
726
|
-
}
|
|
727
|
-
if (shutdownRequested) {
|
|
728
|
-
throw new Error("[stitchkit] application startup interrupted by shutdown");
|
|
729
|
-
}
|
|
730
|
-
if (!isReady()) {
|
|
731
|
-
throw new Error("[stitchkit] a required resource lost readiness during startup");
|
|
732
|
-
}
|
|
733
|
-
activationComplete = true;
|
|
734
|
-
accepting = isReady();
|
|
735
|
-
publish();
|
|
736
|
-
return snapshot();
|
|
737
|
-
} catch (error) {
|
|
738
|
-
startFailure = error;
|
|
739
|
-
}
|
|
740
|
-
if (!shutdownRequested) {
|
|
741
|
-
const rollbackErrors = await closeAttempted();
|
|
742
|
-
lifecycle = "failed";
|
|
743
|
-
accepting = false;
|
|
744
|
-
publish();
|
|
745
|
-
if (rollbackErrors.length > 0) {
|
|
746
|
-
throw new AggregateError([startFailure, ...rollbackErrors], "[stitchkit] application startup and rollback failed", { cause: startFailure });
|
|
747
|
-
}
|
|
748
|
-
}
|
|
749
|
-
throw startFailure;
|
|
750
|
-
};
|
|
751
|
-
const start = () => {
|
|
752
|
-
if (startPromise)
|
|
753
|
-
return startPromise;
|
|
754
|
-
if (lifecycle !== "created") {
|
|
755
|
-
return Promise.reject(new Error(`[stitchkit] application cannot start from lifecycle "${lifecycle}"`));
|
|
756
|
-
}
|
|
757
|
-
startPromise = runStart();
|
|
758
|
-
startPromise.catch(() => {
|
|
759
|
-
return;
|
|
760
|
-
});
|
|
761
|
-
return startPromise;
|
|
762
|
-
};
|
|
763
|
-
const acquire = () => {
|
|
764
|
-
if (!accepting || !isReady())
|
|
765
|
-
return null;
|
|
766
|
-
accepted += 1;
|
|
767
|
-
pending += 1;
|
|
768
|
-
publish();
|
|
769
|
-
let released = false;
|
|
770
|
-
return {
|
|
771
|
-
get released() {
|
|
772
|
-
return released;
|
|
773
|
-
},
|
|
774
|
-
release() {
|
|
775
|
-
if (released)
|
|
776
|
-
return;
|
|
777
|
-
released = true;
|
|
778
|
-
pending -= 1;
|
|
779
|
-
completed += 1;
|
|
780
|
-
if (pending === 0) {
|
|
781
|
-
for (const waiter of pendingWaiters)
|
|
782
|
-
waiter();
|
|
783
|
-
pendingWaiters.clear();
|
|
784
|
-
}
|
|
785
|
-
publish();
|
|
786
|
-
}
|
|
787
|
-
};
|
|
788
|
-
};
|
|
789
|
-
const run = async (work) => {
|
|
790
|
-
const lease = acquire();
|
|
791
|
-
if (!lease)
|
|
792
|
-
throw new ApplicationAdmissionError;
|
|
793
|
-
try {
|
|
794
|
-
return await work();
|
|
795
|
-
} finally {
|
|
796
|
-
lease.release();
|
|
797
|
-
}
|
|
798
|
-
};
|
|
799
|
-
const waitForPending = () => {
|
|
800
|
-
if (pending === 0)
|
|
801
|
-
return Promise.resolve();
|
|
802
|
-
return new Promise((resolve) => pendingWaiters.add(resolve));
|
|
803
|
-
};
|
|
804
|
-
const shutdown = (options) => {
|
|
805
|
-
if (shutdownPromise)
|
|
806
|
-
return shutdownPromise;
|
|
807
|
-
const parsed = ShutdownOptionsSchema.parse(options ?? {});
|
|
808
|
-
const startedAt = performance.now();
|
|
809
|
-
const graceDeadlineAt = startedAt + parsed.gracePeriodMs;
|
|
810
|
-
const forceDeadlineAt = graceDeadlineAt + parsed.forceTimeoutMs;
|
|
811
|
-
shutdownRequested = true;
|
|
812
|
-
accepting = false;
|
|
813
|
-
startupAbort.abort();
|
|
814
|
-
lifecycle = lifecycle === "created" ? "stopping" : "draining";
|
|
815
|
-
publish();
|
|
816
|
-
shutdownPromise = (async () => {
|
|
817
|
-
const gracefulAbort = new AbortController;
|
|
818
|
-
let forcedReason;
|
|
819
|
-
let gracefulFailed = [...records.values()].some((record) => record.attempted && record.closeInvoked && !record.closed);
|
|
820
|
-
const force = (reason) => {
|
|
821
|
-
if (forcedReason)
|
|
822
|
-
return;
|
|
823
|
-
forcedReason = reason;
|
|
824
|
-
gracefulAbort.abort();
|
|
825
|
-
lifetimeAbort.abort();
|
|
826
|
-
};
|
|
827
|
-
const graceTimer = setTimeout(() => force("deadline"), Math.max(0, graceDeadlineAt - performance.now()));
|
|
828
|
-
const onExternalAbort = () => force("signal");
|
|
829
|
-
parsed.signal?.addEventListener("abort", onExternalAbort, { once: true });
|
|
830
|
-
if (parsed.signal?.aborted)
|
|
831
|
-
force("signal");
|
|
832
|
-
if (startPromise) {
|
|
833
|
-
await untilDeadline(startPromise.catch(() => {
|
|
834
|
-
return;
|
|
835
|
-
}), gracefulAbort.signal);
|
|
836
|
-
}
|
|
837
|
-
const gracefulContext = (record) => contextFor(record, {
|
|
838
|
-
signal: gracefulAbort.signal,
|
|
839
|
-
deadlineAt: graceDeadlineAt,
|
|
840
|
-
forceDeadlineAt
|
|
841
|
-
});
|
|
842
|
-
for (const entry of reverse) {
|
|
843
|
-
if (gracefulAbort.signal.aborted)
|
|
844
|
-
break;
|
|
845
|
-
const record = records.get(entry.id);
|
|
846
|
-
if (!record?.attempted || record.closed)
|
|
847
|
-
continue;
|
|
848
|
-
try {
|
|
849
|
-
const result = await untilDeadline(Promise.resolve(entry.resource.stopAdmission?.(gracefulContext(record))), gracefulAbort.signal);
|
|
850
|
-
if (!result.settled)
|
|
851
|
-
break;
|
|
852
|
-
if (result.error !== undefined)
|
|
853
|
-
throw result.error;
|
|
854
|
-
} catch {
|
|
855
|
-
record.failures.push("admission");
|
|
856
|
-
gracefulFailed = true;
|
|
857
|
-
lifetimeAbort.abort();
|
|
858
|
-
}
|
|
859
|
-
}
|
|
860
|
-
if (!gracefulAbort.signal.aborted && !gracefulFailed) {
|
|
861
|
-
const result = await untilDeadline(waitForPending(), gracefulAbort.signal);
|
|
862
|
-
if (!result.settled)
|
|
863
|
-
force("deadline");
|
|
864
|
-
}
|
|
865
|
-
for (const entry of reverse) {
|
|
866
|
-
if (gracefulAbort.signal.aborted || gracefulFailed)
|
|
867
|
-
break;
|
|
868
|
-
const record = records.get(entry.id);
|
|
869
|
-
if (!record?.attempted || record.closed)
|
|
870
|
-
continue;
|
|
871
|
-
record.state = "stopping";
|
|
872
|
-
publish();
|
|
873
|
-
try {
|
|
874
|
-
const drained = await untilDeadline(Promise.resolve(entry.resource.drain?.(gracefulContext(record))), gracefulAbort.signal);
|
|
875
|
-
if (!drained.settled) {
|
|
876
|
-
force("deadline");
|
|
877
|
-
break;
|
|
878
|
-
}
|
|
879
|
-
if (drained.error !== undefined)
|
|
880
|
-
throw drained.error;
|
|
881
|
-
} catch {
|
|
882
|
-
record.failures.push("drain");
|
|
883
|
-
gracefulFailed = true;
|
|
884
|
-
lifetimeAbort.abort();
|
|
885
|
-
break;
|
|
886
|
-
}
|
|
887
|
-
}
|
|
888
|
-
lifecycle = "stopping";
|
|
889
|
-
publish();
|
|
890
|
-
for (const entry of reverse) {
|
|
891
|
-
if (gracefulAbort.signal.aborted || gracefulFailed)
|
|
892
|
-
break;
|
|
893
|
-
const record = records.get(entry.id);
|
|
894
|
-
if (!record?.attempted || record.closed || record.closeInvoked)
|
|
895
|
-
continue;
|
|
896
|
-
try {
|
|
897
|
-
record.closeInvoked = true;
|
|
898
|
-
const closed = await untilDeadline(Promise.resolve(entry.resource.close?.(gracefulContext(record))), gracefulAbort.signal);
|
|
899
|
-
if (!closed.settled) {
|
|
900
|
-
force("deadline");
|
|
901
|
-
break;
|
|
902
|
-
}
|
|
903
|
-
if (closed.error !== undefined)
|
|
904
|
-
throw closed.error;
|
|
905
|
-
record.closed = true;
|
|
906
|
-
record.state = "stopped";
|
|
907
|
-
publish();
|
|
908
|
-
} catch {
|
|
909
|
-
record.failures.push("close");
|
|
910
|
-
record.state = "failed";
|
|
911
|
-
gracefulFailed = true;
|
|
912
|
-
lifetimeAbort.abort();
|
|
913
|
-
publish();
|
|
914
|
-
break;
|
|
915
|
-
}
|
|
916
|
-
}
|
|
917
|
-
clearTimeout(graceTimer);
|
|
918
|
-
parsed.signal?.removeEventListener("abort", onExternalAbort);
|
|
919
|
-
const mustForce = forcedReason !== undefined || gracefulFailed;
|
|
920
|
-
const pendingOperationsAtForce = mustForce ? pending : 0;
|
|
921
|
-
if (mustForce) {
|
|
922
|
-
lifetimeAbort.abort();
|
|
923
|
-
const forceAbort = new AbortController;
|
|
924
|
-
const forceTimer = setTimeout(() => forceAbort.abort(), Math.max(0, forceDeadlineAt - performance.now()));
|
|
925
|
-
await Promise.all(reverse.map(async (entry) => {
|
|
926
|
-
const record = records.get(entry.id);
|
|
927
|
-
if (!record?.attempted || record.closed)
|
|
928
|
-
return;
|
|
929
|
-
try {
|
|
930
|
-
let cleanup;
|
|
931
|
-
if (entry.resource.force) {
|
|
932
|
-
cleanup = Promise.resolve(entry.resource.force(contextFor(record, {
|
|
933
|
-
signal: forceAbort.signal,
|
|
934
|
-
deadlineAt: graceDeadlineAt,
|
|
935
|
-
forceDeadlineAt
|
|
936
|
-
})));
|
|
937
|
-
} else if (entry.resource.close && !record.closeInvoked) {
|
|
938
|
-
record.closeInvoked = true;
|
|
939
|
-
cleanup = Promise.resolve(entry.resource.close(contextFor(record, {
|
|
940
|
-
signal: forceAbort.signal,
|
|
941
|
-
deadlineAt: graceDeadlineAt,
|
|
942
|
-
forceDeadlineAt
|
|
943
|
-
})));
|
|
944
|
-
} else if (entry.resource.close) {
|
|
945
|
-
record.failures.push("force");
|
|
946
|
-
return;
|
|
947
|
-
}
|
|
948
|
-
const forced = await untilDeadline(cleanup ?? Promise.resolve(), forceAbort.signal);
|
|
949
|
-
if (!forced.settled || forced.error !== undefined) {
|
|
950
|
-
record.failures.push("force");
|
|
951
|
-
return;
|
|
952
|
-
}
|
|
953
|
-
record.closed = true;
|
|
954
|
-
record.state = "stopped";
|
|
955
|
-
} catch {
|
|
956
|
-
record.failures.push("force");
|
|
957
|
-
}
|
|
958
|
-
}));
|
|
959
|
-
clearTimeout(forceTimer);
|
|
960
|
-
}
|
|
961
|
-
const cleanupComplete = [...records.values()].every((record) => !record.attempted || record.closed);
|
|
962
|
-
lifecycle = cleanupComplete ? "stopped" : "failed";
|
|
963
|
-
publish();
|
|
964
|
-
return ApplicationShutdownResultSchema.parse({
|
|
965
|
-
outcome: mustForce ? "forced" : "clean",
|
|
966
|
-
...forcedReason && { reason: forcedReason },
|
|
967
|
-
cleanupComplete,
|
|
968
|
-
acceptedOperations: accepted,
|
|
969
|
-
completedOperations: completed,
|
|
970
|
-
pendingOperations: pending,
|
|
971
|
-
pendingOperationsAtForce,
|
|
972
|
-
resources: ordered.map((entry) => {
|
|
973
|
-
const record = records.get(entry.id);
|
|
974
|
-
if (!record)
|
|
975
|
-
throw new Error("Managed resource record disappeared");
|
|
976
|
-
return {
|
|
977
|
-
id: entry.id,
|
|
978
|
-
state: !record.attempted ? "not-started" : record.closed ? "closed" : "force-failed",
|
|
979
|
-
failures: record.failures
|
|
980
|
-
};
|
|
981
|
-
}),
|
|
982
|
-
durationMs: performance.now() - startedAt
|
|
983
|
-
});
|
|
984
|
-
})();
|
|
985
|
-
shutdownPromise.catch(() => {
|
|
986
|
-
return;
|
|
987
|
-
});
|
|
988
|
-
return shutdownPromise;
|
|
989
|
-
};
|
|
990
|
-
return {
|
|
991
|
-
id,
|
|
992
|
-
admission: { acquire, run },
|
|
993
|
-
start,
|
|
994
|
-
getSnapshot: snapshot,
|
|
995
|
-
subscribe(listener) {
|
|
996
|
-
listeners.add(listener);
|
|
997
|
-
listener(snapshot());
|
|
998
|
-
return () => listeners.delete(listener);
|
|
999
|
-
},
|
|
1000
|
-
shutdown
|
|
1001
|
-
};
|
|
1002
|
-
}
|
|
1003
|
-
// src/application/schedule.ts
|
|
1004
|
-
import { z as z6 } from "zod";
|
|
1005
|
-
var NonNegativeSafeIntegerSchema = z6.number().int().nonnegative().refine(Number.isSafeInteger, "Expected a safe integer");
|
|
1006
|
-
var PositiveSafeIntegerSchema = NonNegativeSafeIntegerSchema.refine((value) => value > 0, "Expected a positive integer");
|
|
1007
|
-
var ManagedScheduleOverlapSchema = z6.discriminatedUnion("mode", [
|
|
1008
|
-
z6.object({ mode: z6.literal("skip") }).readonly(),
|
|
1009
|
-
z6.object({ mode: z6.literal("queue-one") }).readonly(),
|
|
1010
|
-
z6.object({
|
|
1011
|
-
mode: z6.literal("parallel"),
|
|
1012
|
-
maxConcurrent: PositiveSafeIntegerSchema
|
|
1013
|
-
}).readonly()
|
|
1014
|
-
]);
|
|
1015
|
-
var ManagedScheduleErrorPolicySchema = z6.enum(["continue", "stop-schedule"]);
|
|
1016
|
-
var ManagedScheduleDescriptorSchema = z6.object({
|
|
1017
|
-
id: ApplicationIdSchema,
|
|
1018
|
-
everyMs: PositiveSafeIntegerSchema,
|
|
1019
|
-
startAfterMs: NonNegativeSafeIntegerSchema,
|
|
1020
|
-
overlap: ManagedScheduleOverlapSchema,
|
|
1021
|
-
errorPolicy: ManagedScheduleErrorPolicySchema
|
|
1022
|
-
}).readonly();
|
|
1023
|
-
var ManagedScheduleStatusSchema = z6.object({
|
|
1024
|
-
descriptor: ManagedScheduleDescriptorSchema,
|
|
1025
|
-
state: z6.enum(["inactive", "scheduled", "running", "draining", "stopped"]),
|
|
1026
|
-
revision: NonNegativeSafeIntegerSchema,
|
|
1027
|
-
capturedAt: z6.string().datetime({ offset: true }),
|
|
1028
|
-
changedAt: z6.string().datetime({ offset: true }),
|
|
1029
|
-
accepting: z6.boolean(),
|
|
1030
|
-
active: NonNegativeSafeIntegerSchema,
|
|
1031
|
-
queued: z6.boolean(),
|
|
1032
|
-
runsStarted: NonNegativeSafeIntegerSchema,
|
|
1033
|
-
runsCompleted: NonNegativeSafeIntegerSchema,
|
|
1034
|
-
runsFailed: NonNegativeSafeIntegerSchema,
|
|
1035
|
-
ticksSkipped: NonNegativeSafeIntegerSchema,
|
|
1036
|
-
nextRunAt: z6.string().datetime({ offset: true }).nullable(),
|
|
1037
|
-
lastScheduledAt: z6.string().datetime({ offset: true }).nullable(),
|
|
1038
|
-
lastStartedAt: z6.string().datetime({ offset: true }).nullable(),
|
|
1039
|
-
lastFinishedAt: z6.string().datetime({ offset: true }).nullable()
|
|
1040
|
-
}).readonly();
|
|
1041
|
-
var systemClock = {
|
|
1042
|
-
now: () => performance.now(),
|
|
1043
|
-
wallNow: () => new Date,
|
|
1044
|
-
schedule(callback, delayMs) {
|
|
1045
|
-
const timer = setTimeout(callback, delayMs);
|
|
1046
|
-
return { cancel: () => clearTimeout(timer) };
|
|
1047
|
-
}
|
|
1048
|
-
};
|
|
1049
|
-
var DEFAULT_OVERLAP = { mode: "skip" };
|
|
1050
|
-
function createManagedSchedule(config) {
|
|
1051
|
-
const descriptor = ManagedScheduleDescriptorSchema.parse({
|
|
1052
|
-
id: config.id,
|
|
1053
|
-
everyMs: config.everyMs,
|
|
1054
|
-
startAfterMs: config.startAfterMs ?? config.everyMs,
|
|
1055
|
-
overlap: config.overlap ?? DEFAULT_OVERLAP,
|
|
1056
|
-
errorPolicy: config.errorPolicy ?? "continue"
|
|
1057
|
-
});
|
|
1058
|
-
const dependsOn = config.dependsOn?.map((id) => ApplicationIdSchema.parse(id));
|
|
1059
|
-
const clock = config.clock ?? systemClock;
|
|
1060
|
-
let revision = 0;
|
|
1061
|
-
let accepting = false;
|
|
1062
|
-
let activated = false;
|
|
1063
|
-
let stopped = false;
|
|
1064
|
-
let timer = null;
|
|
1065
|
-
let activationContext = null;
|
|
1066
|
-
let nextRunAt = null;
|
|
1067
|
-
let queuedAt = null;
|
|
1068
|
-
let runsStarted = 0;
|
|
1069
|
-
let runsCompleted = 0;
|
|
1070
|
-
let runsFailed = 0;
|
|
1071
|
-
let ticksSkipped = 0;
|
|
1072
|
-
let lastScheduledAt = null;
|
|
1073
|
-
let lastStartedAt = null;
|
|
1074
|
-
let lastFinishedAt = null;
|
|
1075
|
-
let changedAt = clock.wallNow().toISOString();
|
|
1076
|
-
const active = new Set;
|
|
1077
|
-
const changed = () => {
|
|
1078
|
-
revision += 1;
|
|
1079
|
-
changedAt = clock.wallNow().toISOString();
|
|
1080
|
-
};
|
|
1081
|
-
const cancelFuture = () => {
|
|
1082
|
-
timer?.cancel();
|
|
1083
|
-
timer = null;
|
|
1084
|
-
nextRunAt = null;
|
|
1085
|
-
queuedAt = null;
|
|
1086
|
-
};
|
|
1087
|
-
const stopAdmission = () => {
|
|
1088
|
-
if (!accepting && stopped)
|
|
1089
|
-
return;
|
|
1090
|
-
accepting = false;
|
|
1091
|
-
stopped = true;
|
|
1092
|
-
cancelFuture();
|
|
1093
|
-
changed();
|
|
1094
|
-
};
|
|
1095
|
-
const reportError = (error, context) => {
|
|
1096
|
-
if (!config.onError)
|
|
1097
|
-
return;
|
|
1098
|
-
Promise.resolve().then(() => config.onError?.(error, context)).catch(() => {});
|
|
1099
|
-
};
|
|
1100
|
-
const state = () => {
|
|
1101
|
-
if (!activated)
|
|
1102
|
-
return stopped ? "stopped" : "inactive";
|
|
1103
|
-
if (!accepting)
|
|
1104
|
-
return active.size > 0 ? "draining" : "stopped";
|
|
1105
|
-
return active.size > 0 ? "running" : "scheduled";
|
|
1106
|
-
};
|
|
1107
|
-
const wallAt = (monotonicAt, anchor) => {
|
|
1108
|
-
if (monotonicAt === null)
|
|
1109
|
-
return null;
|
|
1110
|
-
return new Date(anchor.wallNow + monotonicAt - anchor.monotonicNow).toISOString();
|
|
1111
|
-
};
|
|
1112
|
-
const status = () => {
|
|
1113
|
-
const captured = clock.wallNow();
|
|
1114
|
-
const anchor = { monotonicNow: clock.now(), wallNow: captured.getTime() };
|
|
1115
|
-
return ManagedScheduleStatusSchema.parse({
|
|
1116
|
-
descriptor,
|
|
1117
|
-
state: state(),
|
|
1118
|
-
revision,
|
|
1119
|
-
capturedAt: captured.toISOString(),
|
|
1120
|
-
changedAt,
|
|
1121
|
-
accepting,
|
|
1122
|
-
active: active.size,
|
|
1123
|
-
queued: queuedAt !== null,
|
|
1124
|
-
runsStarted,
|
|
1125
|
-
runsCompleted,
|
|
1126
|
-
runsFailed,
|
|
1127
|
-
ticksSkipped,
|
|
1128
|
-
nextRunAt: wallAt(nextRunAt, anchor),
|
|
1129
|
-
lastScheduledAt,
|
|
1130
|
-
lastStartedAt,
|
|
1131
|
-
lastFinishedAt
|
|
1132
|
-
});
|
|
1133
|
-
};
|
|
1134
|
-
let startExecution = () => {
|
|
1135
|
-
return;
|
|
1136
|
-
};
|
|
1137
|
-
const settleExecution = () => {
|
|
1138
|
-
if (!accepting || queuedAt === null || active.size !== 0)
|
|
1139
|
-
return;
|
|
1140
|
-
const successorAt = queuedAt;
|
|
1141
|
-
queuedAt = null;
|
|
1142
|
-
changed();
|
|
1143
|
-
startExecution(successorAt);
|
|
1144
|
-
};
|
|
1145
|
-
startExecution = (scheduledAt) => {
|
|
1146
|
-
const resourceContext = activationContext;
|
|
1147
|
-
if (!accepting || !resourceContext)
|
|
1148
|
-
return;
|
|
1149
|
-
const startedAt = clock.now();
|
|
1150
|
-
const wallStartedAt = clock.wallNow().getTime();
|
|
1151
|
-
const runContext = {
|
|
1152
|
-
applicationId: resourceContext.applicationId,
|
|
1153
|
-
signal: resourceContext.signal,
|
|
1154
|
-
scheduledAt,
|
|
1155
|
-
startedAt,
|
|
1156
|
-
now: () => clock.now()
|
|
1157
|
-
};
|
|
1158
|
-
runsStarted += 1;
|
|
1159
|
-
lastScheduledAt = new Date(wallStartedAt + scheduledAt - startedAt).toISOString();
|
|
1160
|
-
lastStartedAt = new Date(wallStartedAt).toISOString();
|
|
1161
|
-
changed();
|
|
1162
|
-
let tracked;
|
|
1163
|
-
tracked = Promise.resolve().then(() => config.run(runContext)).then(() => {
|
|
1164
|
-
runsCompleted += 1;
|
|
1165
|
-
lastFinishedAt = clock.wallNow().toISOString();
|
|
1166
|
-
if (descriptor.errorPolicy === "continue" && accepting) {
|
|
1167
|
-
resourceContext.reportHealth("healthy");
|
|
1168
|
-
}
|
|
1169
|
-
}, (error) => {
|
|
1170
|
-
runsFailed += 1;
|
|
1171
|
-
lastFinishedAt = clock.wallNow().toISOString();
|
|
1172
|
-
reportError(error, runContext);
|
|
1173
|
-
if (descriptor.errorPolicy === "stop-schedule") {
|
|
1174
|
-
resourceContext.reportHealth("unhealthy");
|
|
1175
|
-
stopAdmission();
|
|
1176
|
-
} else {
|
|
1177
|
-
resourceContext.reportHealth("degraded");
|
|
1178
|
-
}
|
|
1179
|
-
}).finally(() => {
|
|
1180
|
-
active.delete(tracked);
|
|
1181
|
-
changed();
|
|
1182
|
-
settleExecution();
|
|
1183
|
-
});
|
|
1184
|
-
active.add(tracked);
|
|
1185
|
-
};
|
|
1186
|
-
const dispatchTick = (scheduledAt) => {
|
|
1187
|
-
if (!accepting)
|
|
1188
|
-
return;
|
|
1189
|
-
if (descriptor.overlap.mode === "skip") {
|
|
1190
|
-
if (active.size > 0) {
|
|
1191
|
-
ticksSkipped += 1;
|
|
1192
|
-
changed();
|
|
1193
|
-
return;
|
|
1194
|
-
}
|
|
1195
|
-
startExecution(scheduledAt);
|
|
1196
|
-
return;
|
|
1197
|
-
}
|
|
1198
|
-
if (descriptor.overlap.mode === "queue-one") {
|
|
1199
|
-
if (active.size > 0) {
|
|
1200
|
-
queuedAt = scheduledAt;
|
|
1201
|
-
changed();
|
|
1202
|
-
return;
|
|
1203
|
-
}
|
|
1204
|
-
startExecution(scheduledAt);
|
|
1205
|
-
return;
|
|
1206
|
-
}
|
|
1207
|
-
if (active.size >= descriptor.overlap.maxConcurrent) {
|
|
1208
|
-
ticksSkipped += 1;
|
|
1209
|
-
changed();
|
|
1210
|
-
return;
|
|
1211
|
-
}
|
|
1212
|
-
startExecution(scheduledAt);
|
|
1213
|
-
};
|
|
1214
|
-
const arm = () => {
|
|
1215
|
-
if (!accepting || nextRunAt === null)
|
|
1216
|
-
return;
|
|
1217
|
-
const delayMs = Math.max(0, nextRunAt - clock.now());
|
|
1218
|
-
timer = clock.schedule(() => {
|
|
1219
|
-
timer = null;
|
|
1220
|
-
if (!accepting || nextRunAt === null)
|
|
1221
|
-
return;
|
|
1222
|
-
const scheduledAt = nextRunAt;
|
|
1223
|
-
const observedAt = clock.now();
|
|
1224
|
-
const elapsedIntervals = Math.max(1, Math.floor((observedAt - scheduledAt) / descriptor.everyMs) + 1);
|
|
1225
|
-
if (elapsedIntervals > 1) {
|
|
1226
|
-
ticksSkipped += elapsedIntervals - 1;
|
|
1227
|
-
changed();
|
|
1228
|
-
}
|
|
1229
|
-
nextRunAt = scheduledAt + elapsedIntervals * descriptor.everyMs;
|
|
1230
|
-
arm();
|
|
1231
|
-
dispatchTick(scheduledAt);
|
|
1232
|
-
}, delayMs);
|
|
1233
|
-
};
|
|
1234
|
-
const waitForActive = async (context, deadlineAt) => {
|
|
1235
|
-
if (active.size === 0 || context.signal.aborted)
|
|
1236
|
-
return;
|
|
1237
|
-
await new Promise((resolve) => {
|
|
1238
|
-
let settled = false;
|
|
1239
|
-
let deadlineTimer = null;
|
|
1240
|
-
const finish = () => {
|
|
1241
|
-
if (settled)
|
|
1242
|
-
return;
|
|
1243
|
-
settled = true;
|
|
1244
|
-
deadlineTimer?.cancel();
|
|
1245
|
-
context.signal.removeEventListener("abort", finish);
|
|
1246
|
-
resolve();
|
|
1247
|
-
};
|
|
1248
|
-
context.signal.addEventListener("abort", finish, { once: true });
|
|
1249
|
-
if (deadlineAt !== undefined) {
|
|
1250
|
-
deadlineTimer = clock.schedule(finish, Math.max(0, deadlineAt - context.now()));
|
|
1251
|
-
}
|
|
1252
|
-
Promise.allSettled([...active]).then(finish);
|
|
1253
|
-
});
|
|
1254
|
-
};
|
|
1255
|
-
const drain = async (context) => {
|
|
1256
|
-
stopAdmission();
|
|
1257
|
-
await waitForActive(context, context.deadlineAt);
|
|
1258
|
-
};
|
|
103
|
+
function createApplicationOperationalHandlers(application, options = {}) {
|
|
104
|
+
const parsed = ApplicationOperationalHandlersOptionsSchema.parse(options);
|
|
1259
105
|
return {
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
},
|
|
1270
|
-
activate(context) {
|
|
1271
|
-
if (activated || stopped)
|
|
1272
|
-
return;
|
|
1273
|
-
activated = true;
|
|
1274
|
-
accepting = true;
|
|
1275
|
-
activationContext = context;
|
|
1276
|
-
nextRunAt = clock.now() + descriptor.startAfterMs;
|
|
1277
|
-
context.reportHealth("healthy");
|
|
1278
|
-
changed();
|
|
1279
|
-
arm();
|
|
1280
|
-
},
|
|
1281
|
-
stopAdmission() {
|
|
1282
|
-
stopAdmission();
|
|
1283
|
-
},
|
|
1284
|
-
drain,
|
|
1285
|
-
close() {
|
|
1286
|
-
stopAdmission();
|
|
1287
|
-
},
|
|
1288
|
-
async force(context) {
|
|
1289
|
-
stopAdmission();
|
|
1290
|
-
await waitForActive(context, context.forceDeadlineAt);
|
|
1291
|
-
if (active.size > 0) {
|
|
1292
|
-
throw new Error(`[stitchkit] schedule "${descriptor.id}" still has active executions`);
|
|
1293
|
-
}
|
|
1294
|
-
}
|
|
106
|
+
status: () => Response.json(projectApplicationStatus(application.getSnapshot())),
|
|
107
|
+
readiness: createApplicationHealthHandler(application, {
|
|
108
|
+
kind: "readiness",
|
|
109
|
+
retryAfterSeconds: parsed.retryAfterSeconds
|
|
110
|
+
}),
|
|
111
|
+
liveness: createApplicationHealthHandler(application, {
|
|
112
|
+
kind: "liveness",
|
|
113
|
+
retryAfterSeconds: parsed.retryAfterSeconds
|
|
114
|
+
})
|
|
1295
115
|
};
|
|
1296
116
|
}
|
|
1297
117
|
// src/application/server-resource.ts
|
|
@@ -1340,6 +160,7 @@ export {
|
|
|
1340
160
|
ActivitySnapshotSchema,
|
|
1341
161
|
ActivityStageIdSchema,
|
|
1342
162
|
ActivityStageSnapshotSchema,
|
|
163
|
+
ActivityTokenBrand,
|
|
1343
164
|
ApplicationAdmissionError,
|
|
1344
165
|
ApplicationAdmissionSnapshotSchema,
|
|
1345
166
|
ApplicationHealthHandlerOptionsSchema,
|
|
@@ -1347,10 +168,13 @@ export {
|
|
|
1347
168
|
ApplicationIdSchema,
|
|
1348
169
|
ApplicationLifecycleEventSchema,
|
|
1349
170
|
ApplicationLifecycleSchema,
|
|
171
|
+
ApplicationOperationalHandlersOptionsSchema,
|
|
1350
172
|
ApplicationResourceShutdownSchema,
|
|
173
|
+
ApplicationShutdownOptionsSchema,
|
|
1351
174
|
ApplicationShutdownResultSchema,
|
|
1352
175
|
ApplicationSnapshotSchema,
|
|
1353
176
|
ApplicationSnapshotSinkStatusSchema,
|
|
177
|
+
ApplicationStatusProjectionSchema,
|
|
1354
178
|
ManagedResourceSnapshotSchema,
|
|
1355
179
|
ManagedResourceStateSchema,
|
|
1356
180
|
ManagedScheduleDescriptorSchema,
|
|
@@ -1362,8 +186,10 @@ export {
|
|
|
1362
186
|
createApplication,
|
|
1363
187
|
createApplicationEventSink,
|
|
1364
188
|
createApplicationHealthHandler,
|
|
189
|
+
createApplicationOperationalHandlers,
|
|
1365
190
|
createApplicationSnapshotSink,
|
|
1366
191
|
createManagedSchedule,
|
|
1367
192
|
defineManagedResource,
|
|
1368
|
-
managedServerResource
|
|
193
|
+
managedServerResource,
|
|
194
|
+
projectApplicationStatus
|
|
1369
195
|
};
|