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.
Files changed (84) hide show
  1. package/dist/agent-runtime-browser.js +53 -581
  2. package/dist/agent-runtime-coding-tools.js +6 -5
  3. package/dist/agent-runtime-harness.js +21 -94
  4. package/dist/agent-runtime-openrouter.js +1 -1
  5. package/dist/agent-runtime.js +68 -64
  6. package/dist/application/kernel.d.ts.map +1 -1
  7. package/dist/application/schemas.d.ts +2 -0
  8. package/dist/application/schemas.d.ts.map +1 -1
  9. package/dist/application/watch-hub.d.ts.map +1 -1
  10. package/dist/application-diagnostic-journal.js +14 -96
  11. package/dist/application-opentelemetry.js +6 -92
  12. package/dist/application-schemas.js +2 -1
  13. package/dist/application.js +158 -1802
  14. package/dist/cli.js +13 -11
  15. package/dist/contract/index.js +7 -4
  16. package/dist/index-2qzskm8h.js +63 -0
  17. package/dist/{index-93xp5tgg.js → index-2rwwmr7v.js} +3 -3
  18. package/dist/{index-kw12fsbc.js → index-3gye9wzb.js} +7 -3
  19. package/dist/{index-1ckavx4h.js → index-3z73fh2c.js} +1 -49
  20. package/dist/index-4fpa9dy2.js +51 -0
  21. package/dist/{index-cwp38wxn.js → index-5mtc969c.js} +2 -2
  22. package/dist/index-5s3zajp8.js +192 -0
  23. package/dist/index-6s7n2v50.js +583 -0
  24. package/dist/{index-qaajjfp6.js → index-758zx2ha.js} +15 -94
  25. package/dist/{index-qyrqwr4c.js → index-77fekveh.js} +4 -1
  26. package/dist/{index-k45qqrdh.js → index-7ag81v2f.js} +13 -166
  27. package/dist/{index-n1g2xm6a.js → index-7gnkny4z.js} +124 -1
  28. package/dist/index-7zbps32p.js +89 -0
  29. package/dist/index-85vfqd7m.js +135 -0
  30. package/dist/{index-4ntp63ps.js → index-9s6z8vgf.js} +18 -142
  31. package/dist/index-a916km3r.js +45 -0
  32. package/dist/index-b9aefrqd.js +20 -0
  33. package/dist/index-dre2ywck.js +48 -0
  34. package/dist/{index-s5tn4kdv.js → index-dwf1f2ap.js} +3 -3
  35. package/dist/{index-wwst0td5.js → index-dx9xehwt.js} +1 -1
  36. package/dist/{index-5r0ak8fr.js → index-dxesg10d.js} +4 -4
  37. package/dist/{index-aczggrty.js → index-dxjjv399.js} +2 -2
  38. package/dist/{index-j6mfaw26.js → index-egaabxkz.js} +3 -3
  39. package/dist/index-evcnmsc3.js +866 -0
  40. package/dist/{index-efm026pw.js → index-fxz0v7vg.js} +86 -86
  41. package/dist/{index-56fm73re.js → index-hb5pf3s4.js} +1 -1
  42. package/dist/{index-n7apa6sy.js → index-k9ay6v6d.js} +24 -275
  43. package/dist/index-kzfs85xp.js +9 -0
  44. package/dist/{index-vbf2p6me.js → index-m9kzax52.js} +91 -89
  45. package/dist/{index-22by16v6.js → index-mgtxfe6b.js} +1 -1
  46. package/dist/index-p7hgdyhb.js +22 -0
  47. package/dist/index-pq95qkbf.js +0 -0
  48. package/dist/index-t0z5p5w6.js +318 -0
  49. package/dist/{index-31k1ns1s.js → index-vwp69871.js} +13 -11
  50. package/dist/index-wc4x00bn.js +103 -0
  51. package/dist/{index-aewy3x5j.js → index-xcetq6j3.js} +10 -8
  52. package/dist/{index-7z69kyrh.js → index-y01f4223.js} +24 -40
  53. package/dist/{index-dmfn9m4x.js → index-ywd0y6m1.js} +20 -341
  54. package/dist/index.js +46 -407
  55. package/dist/live.js +6 -3
  56. package/dist/node.js +17 -14
  57. package/dist/observability/index.js +8 -7
  58. package/dist/primitives.js +4 -2
  59. package/dist/react.js +1 -1
  60. package/dist/remote.js +7 -3
  61. package/dist/server/index.js +32 -45
  62. package/dist/telegram.js +1 -1
  63. package/dist/testing.js +90 -1576
  64. package/dist/tool-invoker.js +11 -9
  65. package/dist/tools/async-operation-contract.d.ts +93 -0
  66. package/dist/tools/async-operation-contract.d.ts.map +1 -0
  67. package/dist/tools/async-operation.d.ts +2 -47
  68. package/dist/tools/async-operation.d.ts.map +1 -1
  69. package/dist/tools/view-file-contract.d.ts +98 -0
  70. package/dist/tools/view-file-contract.d.ts.map +1 -0
  71. package/dist/tools/view-file.d.ts +4 -83
  72. package/dist/tools/view-file.d.ts.map +1 -1
  73. package/dist/tools-contract.d.ts +29 -0
  74. package/dist/tools-contract.d.ts.map +1 -0
  75. package/dist/tools-contract.js +24 -0
  76. package/dist/tools.d.ts +3 -3
  77. package/dist/tools.d.ts.map +1 -1
  78. package/dist/tools.js +61 -140
  79. package/llms-full.txt +86 -3
  80. package/package.json +9 -6
  81. package/dist/index-anyqnqcv.js +0 -79
  82. package/dist/index-ksp6e2ye.js +0 -24
  83. package/dist/index-vj3vvpaa.js +0 -307
  84. package/dist/index-wfq8zerm.js +0 -92
@@ -1,26 +1,46 @@
1
1
  import {
2
- createBackoff
3
- } from "./index-h4pj6fta.js";
4
- import {
5
- argumentsDigest,
6
- stableValue,
7
- watchKeyString
8
- } from "./index-wfq8zerm.js";
2
+ ApplicationAdmissionError,
3
+ ApplicationRestartInputSchema,
4
+ ApplicationRestartOutcomeSchema,
5
+ ApplicationRestartResultSchema,
6
+ ApplicationShutdownBudgetSchema,
7
+ ApplicationShutdownOptionsSchema,
8
+ createApplication
9
+ } from "./index-evcnmsc3.js";
9
10
  import {
10
- BoundedChannelPolicySchema,
11
- BoundedChannelReaderError,
12
- BoundedChannelSnapshotSchema,
13
- BoundedChannelStateSchema,
14
- CreditWindowSnapshotSchema,
15
- createBoundedChannel,
16
- createCreditWindow
17
- } from "./index-dmfn9m4x.js";
11
+ defineManagedResource,
12
+ managedResourceDependencyId
13
+ } from "./index-2k4yrqkc.js";
18
14
  import {
19
- AppError
20
- } from "./index-0w9abg87.js";
15
+ ActivityIdSchema,
16
+ ActivityLiveStateSchema,
17
+ ActivitySnapshotSchema,
18
+ ActivityStageIdSchema,
19
+ ActivityStageSnapshotSchema,
20
+ ActivityTokenBrand,
21
+ ApplicationSnapshotSinkStatusSchema,
22
+ ManagedScheduleDescriptorSchema,
23
+ ManagedScheduleErrorPolicySchema,
24
+ ManagedScheduleOverlapSchema,
25
+ ManagedScheduleStatusSchema,
26
+ createActivityProjection,
27
+ createApplicationSnapshotSink,
28
+ createManagedSchedule
29
+ } from "./index-6s7n2v50.js";
21
30
  import {
22
- isRecord
23
- } from "./index-ksp6e2ye.js";
31
+ DiagnosticJournalCloseResultSchema,
32
+ DiagnosticJournalFailurePhaseSchema,
33
+ DiagnosticJournalFrameSchema,
34
+ DiagnosticJournalLimitsSchema,
35
+ DiagnosticJournalLockPolicySchema,
36
+ DiagnosticJournalRefusalReasonSchema,
37
+ DiagnosticJournalStateSchema,
38
+ DiagnosticJournalStatusSchema,
39
+ DiagnosticJournalSubmitResultSchema,
40
+ DiagnosticJournalWaitResultSchema,
41
+ readDiagnosticJournalLockDiagnosis
42
+ } from "./index-wc4x00bn.js";
43
+ import"./index-pq95qkbf.js";
24
44
  import {
25
45
  ApplicationAdmissionSnapshotSchema,
26
46
  ApplicationHealthSchema,
@@ -33,315 +53,70 @@ import {
33
53
  ManagedResourceSnapshotSchema,
34
54
  ManagedResourceStateSchema,
35
55
  projectApplicationStatus
36
- } from "./index-kw12fsbc.js";
37
-
38
- // src/application/activity.ts
39
- import { z as z2 } from "zod";
40
-
41
- // src/application/latest-sink.ts
42
- import { z } from "zod";
43
- var SnapshotRevisionSchema = z.number().int().nonnegative();
44
- var ApplicationSnapshotSinkStatusSchema = z.object({
45
- accepting: z.boolean(),
46
- received: z.number().int().nonnegative(),
47
- accepted: z.number().int().nonnegative(),
48
- rejected: z.number().int().nonnegative(),
49
- delivered: z.number().int().nonnegative(),
50
- coalesced: z.number().int().nonnegative(),
51
- failed: z.number().int().nonnegative(),
52
- inFlight: z.boolean(),
53
- pending: z.boolean(),
54
- lastAcceptedRevision: SnapshotRevisionSchema.optional(),
55
- lastDeliveredRevision: SnapshotRevisionSchema.optional()
56
- }).strict().readonly();
57
- function createApplicationSnapshotSink(config) {
58
- let accepting = true;
59
- let received = 0;
60
- let accepted = 0;
61
- let rejected = 0;
62
- let delivered = 0;
63
- let coalesced = 0;
64
- let failed = 0;
65
- let lastAcceptedRevision;
66
- let lastDeliveredRevision;
67
- let inFlight;
68
- let closePromise;
69
- let resolveClose;
70
- const pending = createBoundedChannel({
71
- policy: "latest",
72
- maxItems: 1,
73
- maxBytes: 1,
74
- sizeOf: () => 1
75
- });
76
- const getStatus = () => ApplicationSnapshotSinkStatusSchema.parse({
77
- accepting,
78
- received,
79
- accepted,
80
- rejected,
81
- delivered,
82
- coalesced,
83
- failed,
84
- inFlight: inFlight !== undefined,
85
- pending: pending.getSnapshot().queuedItems > 0,
86
- ...lastAcceptedRevision !== undefined && { lastAcceptedRevision },
87
- ...lastDeliveredRevision !== undefined && { lastDeliveredRevision }
88
- });
89
- const reportFailure = (error, snapshot) => {
90
- if (!config.onSinkError)
91
- return;
92
- Promise.resolve().then(() => config.onSinkError?.({ error, snapshot })).catch(() => {
93
- return;
94
- });
95
- };
96
- const settleCloseIfIdle = () => {
97
- if (accepting || inFlight || pending.getSnapshot().queuedItems > 0 || !resolveClose)
98
- return;
99
- const resolve = resolveClose;
100
- resolveClose = undefined;
101
- resolve(getStatus());
102
- };
103
- const startWrite = (snapshot) => {
104
- const write = Promise.resolve().then(() => config.write(snapshot)).then(() => {
105
- delivered += 1;
106
- lastDeliveredRevision = snapshot.revision;
107
- }).catch((error) => {
108
- failed += 1;
109
- reportFailure(error, snapshot);
110
- }).finally(() => {
111
- if (pending.getSnapshot().queuedItems > 0) {
112
- pending.next().then((next) => {
113
- if (next.done) {
114
- inFlight = undefined;
115
- settleCloseIfIdle();
116
- } else {
117
- startWrite(next.value);
118
- }
119
- });
120
- } else {
121
- inFlight = undefined;
122
- settleCloseIfIdle();
123
- }
124
- });
125
- inFlight = write;
126
- };
127
- return {
128
- publish(snapshot) {
129
- received += 1;
130
- const revision = SnapshotRevisionSchema.parse(snapshot.revision);
131
- if (!accepting || lastAcceptedRevision !== undefined && revision <= lastAcceptedRevision) {
132
- rejected += 1;
133
- return false;
134
- }
135
- accepted += 1;
136
- lastAcceptedRevision = revision;
137
- if (!inFlight) {
138
- startWrite(snapshot);
139
- } else {
140
- const offered = pending.offer(snapshot);
141
- if (offered.outcome === "coalesced")
142
- coalesced += offered.replaced;
143
- }
144
- return true;
145
- },
146
- getStatus,
147
- close() {
148
- if (closePromise)
149
- return closePromise;
150
- accepting = false;
151
- pending.close({ mode: "drain" });
152
- closePromise = new Promise((resolve) => {
153
- resolveClose = resolve;
154
- settleCloseIfIdle();
155
- });
156
- return closePromise;
157
- }
158
- };
159
- }
160
-
161
- // src/application/activity.ts
162
- var BoundedOperationalIdSchema = z2.string().min(1).max(64).regex(/^[a-zA-Z0-9][a-zA-Z0-9._:-]*$/);
163
- var NonNegativeIntegerSchema = z2.number().int().nonnegative();
164
- var ActivityIdSchema = BoundedOperationalIdSchema;
165
- var ActivityStageIdSchema = BoundedOperationalIdSchema;
166
- var ActivityStageSnapshotSchema = z2.object({
167
- id: ActivityStageIdSchema,
168
- active: NonNegativeIntegerSchema,
169
- queued: NonNegativeIntegerSchema,
170
- completed: NonNegativeIntegerSchema,
171
- failed: NonNegativeIntegerSchema
172
- }).strict().readonly();
173
- var ActivityTotalsSchema = z2.object({
174
- active: NonNegativeIntegerSchema,
175
- queued: NonNegativeIntegerSchema,
176
- completed: NonNegativeIntegerSchema,
177
- failed: NonNegativeIntegerSchema
178
- }).strict().readonly();
179
- var ActivitySnapshotSchema = z2.object({
180
- id: ActivityIdSchema,
181
- epoch: z2.string().uuid(),
182
- revision: NonNegativeIntegerSchema,
183
- capturedAt: z2.string().datetime({ offset: true }),
184
- changedAt: z2.string().datetime({ offset: true }),
185
- stages: z2.array(ActivityStageSnapshotSchema).min(1).max(64).readonly(),
186
- totals: ActivityTotalsSchema
187
- }).strict().readonly();
188
- var ActivityLiveStateSchema = z2.enum(["active", "queued"]);
189
- var ActivityTokenBrand = Symbol("stitchkit.application.activity");
190
- function emptyCounters() {
191
- return { active: 0, queued: 0, completed: 0, failed: 0 };
192
- }
193
- function totalsOf(stages) {
194
- return stages.reduce((totals, stage) => ({
195
- active: totals.active + stage.active,
196
- queued: totals.queued + stage.queued,
197
- completed: totals.completed + stage.completed,
198
- failed: totals.failed + stage.failed
199
- }), emptyCounters());
200
- }
201
- function createActivityProjection(config) {
202
- const id = ActivityIdSchema.parse(config.id);
203
- const declaredStages = z2.array(ActivityStageIdSchema).min(1).max(64).parse(config.stages);
204
- const uniqueStages = new Set(declaredStages);
205
- if (uniqueStages.size !== declaredStages.length) {
206
- throw new Error("[stitchkit] createActivityProjection: stage ids must be unique");
207
- }
208
- const epoch = z2.string().uuid().parse(config.epoch ?? crypto.randomUUID());
209
- const now = config.now ?? (() => new Date);
210
- const counters = new Map(declaredStages.map((stage) => [stage, emptyCounters()]));
211
- const activities = new WeakMap;
212
- const subscribers = new Set;
213
- let revision = 0;
214
- let changedAt = now().toISOString();
215
- const countersFor = (stage) => {
216
- const value = counters.get(stage);
217
- if (!value) {
218
- throw new Error(`[stitchkit] createActivityProjection: undeclared stage "${stage}"`);
219
- }
220
- return value;
221
- };
222
- const getSnapshot = () => {
223
- const stages = declaredStages.map((stage) => ({ id: stage, ...countersFor(stage) }));
224
- return ActivitySnapshotSchema.parse({
225
- id,
226
- epoch,
227
- revision,
228
- capturedAt: now().toISOString(),
229
- changedAt,
230
- stages,
231
- totals: totalsOf(stages)
232
- });
233
- };
234
- const publishChange = () => {
235
- revision += 1;
236
- changedAt = now().toISOString();
237
- const snapshot = getSnapshot();
238
- for (const subscriber of subscribers)
239
- subscriber.publish(snapshot);
240
- };
241
- const requireActivity = (token) => {
242
- const activity = activities.get(token);
243
- if (!activity) {
244
- throw new Error("[stitchkit] activity token belongs to another projection");
245
- }
246
- return activity;
247
- };
248
- const leaveLiveState = (activity) => {
249
- if (activity.state === "active" || activity.state === "queued") {
250
- countersFor(activity.stage)[activity.state] -= 1;
251
- }
252
- };
253
- const settle = (token, terminal) => {
254
- const activity = requireActivity(token);
255
- if (activity.state === "completed" || activity.state === "failed")
256
- return false;
257
- leaveLiveState(activity);
258
- countersFor(activity.stage)[terminal] += 1;
259
- activity.state = terminal;
260
- publishChange();
261
- return true;
262
- };
263
- return {
264
- open(stage, rawState = "active") {
265
- const parsedStage = ActivityStageIdSchema.parse(stage);
266
- const state = ActivityLiveStateSchema.parse(rawState);
267
- countersFor(parsedStage)[state] += 1;
268
- const token = Object.freeze({
269
- get [ActivityTokenBrand]() {
270
- return true;
271
- }
272
- });
273
- activities.set(token, { stage: parsedStage, state });
274
- publishChange();
275
- return token;
276
- },
277
- transition(token, input) {
278
- const activity = requireActivity(token);
279
- if (activity.state === "completed" || activity.state === "failed") {
280
- throw new Error("[stitchkit] cannot transition a terminal activity");
281
- }
282
- const stage = ActivityStageIdSchema.parse(input.stage);
283
- const state = ActivityLiveStateSchema.parse(input.state);
284
- countersFor(stage);
285
- if (activity.stage === stage && activity.state === state)
286
- return false;
287
- leaveLiveState(activity);
288
- countersFor(stage)[state] += 1;
289
- activity.stage = stage;
290
- activity.state = state;
291
- publishChange();
292
- return true;
293
- },
294
- complete: (token) => settle(token, "completed"),
295
- fail: (token) => settle(token, "failed"),
296
- getSnapshot,
297
- subscribe(listener) {
298
- const sink = createApplicationSnapshotSink({
299
- write: listener,
300
- ...config.onSubscriberError && { onSinkError: config.onSubscriberError }
301
- });
302
- subscribers.add(sink);
303
- sink.publish(getSnapshot());
304
- return () => {
305
- subscribers.delete(sink);
306
- sink.close();
307
- };
308
- }
309
- };
310
- }
56
+ } from "./index-3gye9wzb.js";
57
+ import {
58
+ withDeadline
59
+ } from "./index-b9aefrqd.js";
60
+ import"./index-3z73fh2c.js";
61
+ import {
62
+ createBoundedSinkManager
63
+ } from "./index-a59da114.js";
64
+ import {
65
+ createBackoff
66
+ } from "./index-h4pj6fta.js";
67
+ import {
68
+ watchKeyString
69
+ } from "./index-dre2ywck.js";
70
+ import"./index-ywd0y6m1.js";
71
+ import {
72
+ BoundedChannelPolicySchema,
73
+ BoundedChannelReaderError,
74
+ BoundedChannelSnapshotSchema,
75
+ BoundedChannelStateSchema,
76
+ CreditWindowSnapshotSchema,
77
+ createBoundedChannel,
78
+ createCreditWindow
79
+ } from "./index-7gnkny4z.js";
80
+ import {
81
+ argumentsDigest,
82
+ stableValue
83
+ } from "./index-a916km3r.js";
84
+ import"./index-0w9abg87.js";
85
+ import"./index-77fekveh.js";
311
86
  // src/application/admission.ts
312
- import { z as z3 } from "zod";
313
- var PositiveSafeIntegerSchema = z3.number().int().positive().safe();
314
- var BoundedRateBudgetSchema = z3.object({
87
+ import { z } from "zod";
88
+ var PositiveSafeIntegerSchema = z.number().int().positive().safe();
89
+ var BoundedRateBudgetSchema = z.object({
315
90
  limit: PositiveSafeIntegerSchema,
316
91
  intervalMs: PositiveSafeIntegerSchema
317
92
  }).strict().readonly();
318
- var BoundedAdmissionPerKeyLimitsSchema = z3.object({
93
+ var BoundedAdmissionPerKeyLimitsSchema = z.object({
319
94
  maxConcurrent: PositiveSafeIntegerSchema,
320
95
  rate: BoundedRateBudgetSchema.optional()
321
96
  }).strict().readonly();
322
- var PerKeyLimitResolverSchema = z3.custom((value) => typeof value === "function", { message: "perKey.limits must be a function of the key" });
323
- var BoundedAdmissionPolicySchema = z3.object({
324
- global: z3.object({
97
+ var PerKeyLimitResolverSchema = z.custom((value) => typeof value === "function", { message: "perKey.limits must be a function of the key" });
98
+ var BoundedAdmissionPolicySchema = z.object({
99
+ global: z.object({
325
100
  maxConcurrent: PositiveSafeIntegerSchema,
326
101
  rate: BoundedRateBudgetSchema.optional()
327
102
  }).strict(),
328
- perKey: z3.union([
329
- z3.object({
103
+ perKey: z.union([
104
+ z.object({
330
105
  maxConcurrent: PositiveSafeIntegerSchema,
331
106
  maxKeys: PositiveSafeIntegerSchema,
332
107
  rate: BoundedRateBudgetSchema.optional(),
333
- limits: z3.never().optional()
108
+ limits: z.never().optional()
334
109
  }).strict(),
335
- z3.object({
110
+ z.object({
336
111
  maxKeys: PositiveSafeIntegerSchema,
337
112
  limits: PerKeyLimitResolverSchema,
338
- maxConcurrent: z3.never().optional(),
339
- rate: z3.never().optional()
113
+ maxConcurrent: z.never().optional(),
114
+ rate: z.never().optional()
340
115
  }).strict()
341
116
  ]).optional()
342
117
  }).strict().readonly();
343
- var BoundedAdmissionStateSchema = z3.enum(["accepting", "draining", "closed"]);
344
- var BoundedAdmissionRefusalReasonSchema = z3.enum([
118
+ var BoundedAdmissionStateSchema = z.enum(["accepting", "draining", "closed"]);
119
+ var BoundedAdmissionRefusalReasonSchema = z.enum([
345
120
  "not-accepting",
346
121
  "global-concurrency",
347
122
  "key-required",
@@ -351,25 +126,25 @@ var BoundedAdmissionRefusalReasonSchema = z3.enum([
351
126
  "key-rate",
352
127
  "upstream"
353
128
  ]);
354
- var RefusalCountersSchema = z3.object({
355
- "not-accepting": z3.number().int().nonnegative(),
356
- "global-concurrency": z3.number().int().nonnegative(),
357
- "key-required": z3.number().int().nonnegative(),
358
- "key-capacity": z3.number().int().nonnegative(),
359
- "key-concurrency": z3.number().int().nonnegative(),
360
- "global-rate": z3.number().int().nonnegative(),
361
- "key-rate": z3.number().int().nonnegative(),
362
- upstream: z3.number().int().nonnegative()
129
+ var RefusalCountersSchema = z.object({
130
+ "not-accepting": z.number().int().nonnegative(),
131
+ "global-concurrency": z.number().int().nonnegative(),
132
+ "key-required": z.number().int().nonnegative(),
133
+ "key-capacity": z.number().int().nonnegative(),
134
+ "key-concurrency": z.number().int().nonnegative(),
135
+ "global-rate": z.number().int().nonnegative(),
136
+ "key-rate": z.number().int().nonnegative(),
137
+ upstream: z.number().int().nonnegative()
363
138
  }).strict().readonly();
364
- var BoundedAdmissionSnapshotSchema = z3.object({
139
+ var BoundedAdmissionSnapshotSchema = z.object({
365
140
  state: BoundedAdmissionStateSchema,
366
- active: z3.number().int().nonnegative(),
367
- accepted: z3.number().int().nonnegative(),
368
- released: z3.number().int().nonnegative(),
369
- refused: z3.number().int().nonnegative(),
370
- keys: z3.number().int().nonnegative(),
371
- globalRateSamples: z3.number().int().nonnegative(),
372
- keyRateSamples: z3.number().int().nonnegative(),
141
+ active: z.number().int().nonnegative(),
142
+ accepted: z.number().int().nonnegative(),
143
+ released: z.number().int().nonnegative(),
144
+ refused: z.number().int().nonnegative(),
145
+ keys: z.number().int().nonnegative(),
146
+ globalRateSamples: z.number().int().nonnegative(),
147
+ keyRateSamples: z.number().int().nonnegative(),
373
148
  refusals: RefusalCountersSchema
374
149
  }).strict().readonly();
375
150
 
@@ -682,31 +457,12 @@ function createBoundedAdmission(config) {
682
457
  getSnapshot
683
458
  };
684
459
  }
685
- // src/internal/deadline.ts
686
- async function withDeadline(value, timeoutMs) {
687
- if (!(value instanceof Promise))
688
- return { settled: true, value };
689
- let timer;
690
- const expiry = new Promise((resolve) => {
691
- timer = setTimeout(() => resolve({ settled: false }), timeoutMs);
692
- });
693
- try {
694
- return await Promise.race([
695
- value.then((settledValue) => ({ settled: true, value: settledValue })),
696
- expiry
697
- ]);
698
- } finally {
699
- if (timer !== undefined)
700
- clearTimeout(timer);
701
- }
702
- }
703
-
704
460
  // src/internal/decision.ts
705
- import { z as z4 } from "zod";
706
- var PolicyDecisionSchema = z4.discriminatedUnion("outcome", [
707
- z4.object({ outcome: z4.literal("allow") }).strict(),
708
- z4.object({ outcome: z4.literal("deny"), reason: z4.string().min(1) }).strict(),
709
- z4.object({ outcome: z4.literal("defer") }).strict()
461
+ import { z as z2 } from "zod";
462
+ var PolicyDecisionSchema = z2.discriminatedUnion("outcome", [
463
+ z2.object({ outcome: z2.literal("allow") }).strict(),
464
+ z2.object({ outcome: z2.literal("deny"), reason: z2.string().min(1) }).strict(),
465
+ z2.object({ outcome: z2.literal("defer") }).strict()
710
466
  ]);
711
467
 
712
468
  // src/application/decisions.ts
@@ -779,249 +535,18 @@ function createDecisionPipeline(policies, config) {
779
535
  }
780
536
  };
781
537
  }
782
- // src/application/diagnostic-journal-contract.ts
783
- import { z as z5 } from "zod";
784
- var PositiveSafeIntegerSchema2 = z5.number().int().positive().safe();
785
- var FileModeSchema = z5.number().int().min(0).max(511);
786
- var DiagnosticJournalLimitsSchema = z5.object({
787
- maxEventBytes: PositiveSafeIntegerSchema2,
788
- maxPendingItems: PositiveSafeIntegerSchema2,
789
- maxPendingBytes: PositiveSafeIntegerSchema2,
790
- maxFileBytes: PositiveSafeIntegerSchema2,
791
- maxFiles: PositiveSafeIntegerSchema2
792
- }).strict().readonly();
793
- var DiagnosticJournalLockPolicySchema = z5.enum(["refuse", "reclaim-stale"]);
794
- var DiagnosticJournalStateSchema = z5.enum(["open", "draining", "closed", "failed"]);
795
- var DiagnosticJournalRefusalReasonSchema = z5.enum([
796
- "closed",
797
- "failed",
798
- "invalid",
799
- "oversized",
800
- "item-capacity",
801
- "byte-capacity"
802
- ]);
803
- var DiagnosticJournalRefusalCountersSchema = z5.object({
804
- closed: z5.number().int().nonnegative(),
805
- failed: z5.number().int().nonnegative(),
806
- invalid: z5.number().int().nonnegative(),
807
- oversized: z5.number().int().nonnegative(),
808
- "item-capacity": z5.number().int().nonnegative(),
809
- "byte-capacity": z5.number().int().nonnegative()
810
- }).strict().readonly();
811
- var DiagnosticJournalFailurePhaseSchema = z5.enum(["write", "rotation", "close"]);
812
- var DiagnosticJournalStatusSchema = z5.object({
813
- state: DiagnosticJournalStateSchema,
814
- epoch: z5.uuid(),
815
- limits: DiagnosticJournalLimitsSchema,
816
- lock: z5.object({
817
- policy: DiagnosticJournalLockPolicySchema,
818
- reclaimedStale: z5.boolean()
819
- }).strict().readonly(),
820
- received: z5.number().int().nonnegative(),
821
- accepted: z5.number().int().nonnegative(),
822
- refused: z5.number().int().nonnegative(),
823
- refusals: DiagnosticJournalRefusalCountersSchema,
824
- written: z5.number().int().nonnegative(),
825
- failedRecords: z5.number().int().nonnegative(),
826
- pendingItems: z5.number().int().nonnegative(),
827
- pendingBytes: z5.number().int().nonnegative(),
828
- inFlight: z5.boolean(),
829
- rotations: z5.number().int().nonnegative(),
830
- rotationFailures: z5.number().int().nonnegative(),
831
- partialTails: z5.number().int().nonnegative(),
832
- currentFileBytes: z5.number().int().nonnegative(),
833
- retainedFiles: z5.number().int().nonnegative(),
834
- lastAcceptedSequence: z5.number().int().positive().optional(),
835
- lastWrittenSequence: z5.number().int().positive().optional(),
836
- lastSettledSequence: z5.number().int().positive().optional(),
837
- lastFailure: z5.object({
838
- phase: DiagnosticJournalFailurePhaseSchema,
839
- sequence: z5.number().int().positive().optional()
840
- }).strict().readonly().optional()
841
- }).strict().readonly();
842
- var DiagnosticJournalSubmitResultSchema = z5.discriminatedUnion("outcome", [
843
- z5.object({
844
- outcome: z5.literal("accepted"),
845
- epoch: z5.uuid(),
846
- sequence: z5.number().int().positive()
847
- }).strict().readonly(),
848
- z5.object({
849
- outcome: z5.literal("refused"),
850
- reason: DiagnosticJournalRefusalReasonSchema
851
- }).strict().readonly()
852
- ]);
853
- var DiagnosticJournalWaitResultSchema = z5.object({
854
- outcome: z5.enum(["settled", "timed-out", "cancelled"]),
855
- state: DiagnosticJournalStateSchema,
856
- throughSequence: z5.number().int().nonnegative(),
857
- settledSequence: z5.number().int().nonnegative()
858
- }).strict().readonly();
859
- var DiagnosticJournalCloseResultSchema = z5.object({
860
- outcome: z5.enum(["closed", "timed-out", "cancelled"]),
861
- state: DiagnosticJournalStateSchema,
862
- pendingItems: z5.number().int().nonnegative()
863
- }).strict().readonly();
864
- var DiagnosticJournalFrameSchema = z5.object({
865
- schemaVersion: z5.literal(1),
866
- epoch: z5.uuid(),
867
- sequence: z5.number().int().positive(),
868
- event: z5.json()
869
- }).strict().readonly();
870
- function readDiagnosticJournalLockDiagnosis(error) {
871
- if (!isRecord(error))
872
- return;
873
- const diagnosis = error.journalLock;
874
- return isRecord(diagnosis) ? diagnosis : undefined;
875
- }
876
538
  // src/application/events.ts
877
- import { z as z7 } from "zod";
878
-
879
- // src/observability/status.ts
880
- import { z as z6 } from "zod";
881
- var NonNegativeIntegerSchema2 = z6.number().int().nonnegative();
882
- var ObservabilitySinkStatusSchema = z6.object({
883
- capacity: NonNegativeIntegerSchema2,
884
- received: NonNegativeIntegerSchema2,
885
- accepted: NonNegativeIntegerSchema2,
886
- filtered: NonNegativeIntegerSchema2,
887
- completed: NonNegativeIntegerSchema2,
888
- dropped: NonNegativeIntegerSchema2,
889
- failed: NonNegativeIntegerSchema2,
890
- preparationFailed: NonNegativeIntegerSchema2,
891
- preparing: NonNegativeIntegerSchema2,
892
- pending: NonNegativeIntegerSchema2,
893
- closed: z6.boolean()
894
- }).readonly();
895
- var ObservabilityStatusObjectSchema = z6.object({
896
- request: ObservabilitySinkStatusSchema.optional(),
897
- tools: ObservabilitySinkStatusSchema.optional(),
898
- total: ObservabilitySinkStatusSchema
899
- });
900
- var ObservabilityStatusSchema = ObservabilityStatusObjectSchema.readonly();
901
- var ObservabilityDrainReportSchema = ObservabilityStatusObjectSchema.extend({
902
- durationMs: z6.number().nonnegative()
903
- }).readonly();
904
-
905
- // src/internal/observability-sink.ts
906
- var DEFAULT_MAX_PENDING = 1000;
907
- function invokeIsolated(callback) {
908
- if (!callback)
909
- return;
910
- Promise.resolve().then(callback).catch(() => {});
911
- }
912
- function createBoundedSinkManager(config) {
913
- const maxPending = config.maxPending ?? DEFAULT_MAX_PENDING;
914
- if (!Number.isSafeInteger(maxPending) || maxPending <= 0) {
915
- throw new TypeError("Observability maxPending must be a positive safe integer");
916
- }
917
- let sequence = 0;
918
- let closed = false;
919
- let received = 0;
920
- let accepted = 0;
921
- let filtered = 0;
922
- let completed = 0;
923
- let dropped = 0;
924
- let failed = 0;
925
- let preparationFailed = 0;
926
- let closePromise;
927
- const preparing = new Map;
928
- const writes = new Map;
929
- const reportError = (error, event) => {
930
- invokeIsolated(config.onSinkError ? () => config.onSinkError?.({ error, ...event !== undefined && { event } }) : undefined);
931
- };
932
- const reportDrop = (reason, event) => {
933
- invokeIsolated(config.onDrop ? () => config.onDrop?.({ reason, event, pending: writes.size }) : undefined);
934
- };
935
- const admit = (id, event) => {
936
- try {
937
- if (config.filter && !config.filter(event)) {
938
- filtered += 1;
939
- return;
940
- }
941
- } catch (error) {
942
- preparationFailed += 1;
943
- reportError(error, event);
944
- return;
945
- }
946
- if (writes.size >= maxPending) {
947
- dropped += 1;
948
- reportDrop("capacity", event);
949
- return;
950
- }
951
- accepted += 1;
952
- const write = Promise.resolve().then(() => config.write(event)).then(() => {
953
- completed += 1;
954
- }).catch((error) => {
955
- failed += 1;
956
- reportError(error, event);
957
- }).finally(() => writes.delete(id));
958
- writes.set(id, write);
959
- };
960
- const awaitGeneration = async (boundary) => {
961
- const through = (values) => [...values].filter(([id]) => id <= boundary).map(([, value]) => value);
962
- await Promise.allSettled(through(preparing));
963
- await Promise.allSettled(through(writes));
964
- };
965
- return {
966
- submit(produce) {
967
- received += 1;
968
- const id = ++sequence;
969
- if (closed) {
970
- Promise.resolve().then(produce).then((event) => {
971
- dropped += 1;
972
- reportDrop("closed", event);
973
- }).catch((error) => {
974
- preparationFailed += 1;
975
- reportError(error);
976
- });
977
- return;
978
- }
979
- const preparation = Promise.resolve().then(produce).then((event) => admit(id, event)).catch((error) => {
980
- preparationFailed += 1;
981
- reportError(error);
982
- }).finally(() => preparing.delete(id));
983
- preparing.set(id, preparation);
984
- },
985
- flush() {
986
- return awaitGeneration(sequence);
987
- },
988
- getStatus,
989
- close() {
990
- if (closePromise)
991
- return closePromise;
992
- closed = true;
993
- closePromise = awaitGeneration(sequence).then(getStatus);
994
- return closePromise;
995
- }
996
- };
997
- function getStatus() {
998
- return ObservabilitySinkStatusSchema.parse({
999
- capacity: maxPending,
1000
- received,
1001
- accepted,
1002
- filtered,
1003
- completed,
1004
- dropped,
1005
- failed,
1006
- preparationFailed,
1007
- preparing: preparing.size,
1008
- pending: writes.size,
1009
- closed
1010
- });
1011
- }
1012
- }
1013
-
1014
- // src/application/events.ts
1015
- var ApplicationLifecycleEventSchema = z7.object({
1016
- type: z7.literal("application-state"),
539
+ import { z as z3 } from "zod";
540
+ var ApplicationLifecycleEventSchema = z3.object({
541
+ type: z3.literal("application-state"),
1017
542
  applicationId: ApplicationIdSchema,
1018
- epoch: z7.string().uuid(),
1019
- revision: z7.number().int().nonnegative(),
543
+ epoch: z3.string().uuid(),
544
+ revision: z3.number().int().nonnegative(),
1020
545
  lifecycle: ApplicationLifecycleSchema,
1021
546
  health: ApplicationHealthSchema,
1022
- ready: z7.boolean(),
1023
- capturedAt: z7.string().datetime({ offset: true }),
1024
- resources: z7.array(ManagedResourceSnapshotSchema).readonly()
547
+ ready: z3.boolean(),
548
+ capturedAt: z3.string().datetime({ offset: true }),
549
+ resources: z3.array(ManagedResourceSnapshotSchema).readonly()
1025
550
  }).strict().readonly();
1026
551
  function applicationLifecycleEvent(snapshot) {
1027
552
  return ApplicationLifecycleEventSchema.parse({
@@ -1052,13 +577,13 @@ function createApplicationEventSink(config) {
1052
577
  };
1053
578
  }
1054
579
  // src/application/health.ts
1055
- import { z as z8 } from "zod";
1056
- var ApplicationHealthHandlerOptionsSchema = z8.object({
1057
- kind: z8.enum(["liveness", "readiness"]),
1058
- retryAfterSeconds: z8.number().int().nonnegative().default(5)
580
+ import { z as z4 } from "zod";
581
+ var ApplicationHealthHandlerOptionsSchema = z4.object({
582
+ kind: z4.enum(["liveness", "readiness"]),
583
+ retryAfterSeconds: z4.number().int().nonnegative().default(5)
1059
584
  });
1060
- var ApplicationOperationalHandlersOptionsSchema = z8.object({
1061
- retryAfterSeconds: z8.number().int().nonnegative().default(5)
585
+ var ApplicationOperationalHandlersOptionsSchema = z4.object({
586
+ retryAfterSeconds: z4.number().int().nonnegative().default(5)
1062
587
  });
1063
588
  function createApplicationHealthHandler(application, options) {
1064
589
  const parsed = ApplicationHealthHandlerOptionsSchema.parse(options);
@@ -1085,901 +610,6 @@ function createApplicationOperationalHandlers(application, options = {}) {
1085
610
  })
1086
611
  };
1087
612
  }
1088
- // src/application/kernel.ts
1089
- import { z as z10 } from "zod";
1090
-
1091
- // src/server/shutdown.ts
1092
- import { z as z9 } from "zod";
1093
-
1094
- // src/server/http-stream-lifetime.ts
1095
- var requests = new WeakMap;
1096
-
1097
- // src/server/shutdown.ts
1098
- var ShutdownStateSchema = z9.enum([
1099
- "running",
1100
- "draining-http",
1101
- "closing-realtime",
1102
- "stopping-runtime",
1103
- "clean",
1104
- "forced"
1105
- ]);
1106
- var ShutdownOptionsSchema = z9.object({
1107
- gracePeriodMs: z9.number().int().nonnegative().default(30000),
1108
- realtimeCloseTimeoutMs: z9.number().int().nonnegative().default(1000),
1109
- forceTimeoutMs: z9.number().int().nonnegative().default(5000),
1110
- retryAfterSeconds: z9.number().int().nonnegative().default(5),
1111
- signal: z9.custom((value) => typeof value === "object" && value !== null && ("aborted" in value) && ("addEventListener" in value), "Expected an AbortSignal").optional()
1112
- });
1113
- var ShutdownStatusSchema = z9.object({
1114
- state: ShutdownStateSchema,
1115
- acceptedRequests: z9.number().int().nonnegative(),
1116
- completedRequests: z9.number().int().nonnegative(),
1117
- pendingRequests: z9.number().int().nonnegative(),
1118
- pendingWebSockets: z9.number().int().nonnegative()
1119
- });
1120
- var ShutdownResultSchema = z9.object({
1121
- outcome: z9.enum(["clean", "forced"]),
1122
- reason: z9.enum(["deadline", "signal"]).optional(),
1123
- acceptedRequests: z9.number().int().nonnegative(),
1124
- completedRequests: z9.number().int().nonnegative(),
1125
- pendingRequests: z9.number().int().nonnegative(),
1126
- pendingWebSockets: z9.number().int().nonnegative(),
1127
- pendingRequestsAtForce: z9.number().int().nonnegative(),
1128
- pendingWebSocketsAtForce: z9.number().int().nonnegative(),
1129
- abortedRequests: z9.number().int().nonnegative(),
1130
- forcedWebSockets: z9.number().int().nonnegative(),
1131
- durationMs: z9.number().nonnegative()
1132
- });
1133
-
1134
- // src/application/resource.ts
1135
- function managedResourceDependencyId(dependency) {
1136
- return typeof dependency === "string" ? dependency : dependency.id;
1137
- }
1138
- function defineManagedResource(resource) {
1139
- return resource;
1140
- }
1141
-
1142
- // src/application/graph.ts
1143
- function resolveResourceGraph(resources) {
1144
- const entries = resources.map((resource, declarationIndex) => ({
1145
- resource,
1146
- id: ApplicationIdSchema.parse(resource.id),
1147
- dependsOn: [...resource.dependsOn ?? []].map((dependency) => ApplicationIdSchema.parse(managedResourceDependencyId(dependency))),
1148
- required: resource.required ?? true,
1149
- declarationIndex
1150
- }));
1151
- const byId = new Map;
1152
- for (const entry of entries) {
1153
- if (byId.has(entry.id)) {
1154
- throw new Error(`[stitchkit] createApplication: duplicate resource id "${entry.id}"`);
1155
- }
1156
- byId.set(entry.id, entry);
1157
- }
1158
- for (const entry of entries) {
1159
- for (const dependencyId of entry.dependsOn) {
1160
- const dependency = byId.get(dependencyId);
1161
- if (!dependency) {
1162
- throw new Error(`[stitchkit] createApplication: resource "${entry.id}" depends on missing resource "${dependencyId}"`);
1163
- }
1164
- if (entry.required && !dependency.required) {
1165
- throw new Error(`[stitchkit] createApplication: required resource "${entry.id}" cannot depend on optional resource "${dependencyId}"`);
1166
- }
1167
- }
1168
- }
1169
- const pending = new Map(entries.map((entry) => [entry.id, entry]));
1170
- const resolved = new Set;
1171
- const ordered = [];
1172
- while (pending.size > 0) {
1173
- const ready = [...pending.values()].filter((entry) => entry.dependsOn.every((id) => resolved.has(id))).sort((left, right) => left.declarationIndex - right.declarationIndex);
1174
- if (ready.length === 0) {
1175
- throw new Error(`[stitchkit] createApplication: resource dependency cycle: ${[...pending.keys()].join(", ")}`);
1176
- }
1177
- for (const entry of ready) {
1178
- pending.delete(entry.id);
1179
- resolved.add(entry.id);
1180
- ordered.push(entry);
1181
- }
1182
- }
1183
- return ordered;
1184
- }
1185
-
1186
- // src/application/kernel.ts
1187
- class ResourceCompletionBeforeReadyError extends Error {
1188
- constructor(resourceId, cause) {
1189
- super(`[stitchkit] resource "${resourceId}" completed before reaching readiness`, {
1190
- ...cause !== undefined && { cause }
1191
- });
1192
- this.name = "ResourceCompletionBeforeReadyError";
1193
- }
1194
- }
1195
-
1196
- class ApplicationStartupInterruptedError extends Error {
1197
- constructor() {
1198
- super("[stitchkit] application startup interrupted by shutdown");
1199
- this.name = "ApplicationStartupInterruptedError";
1200
- }
1201
- }
1202
- var ApplicationShutdownOptionsSchema = ShutdownOptionsSchema.pick({
1203
- gracePeriodMs: true,
1204
- forceTimeoutMs: true,
1205
- signal: true
1206
- });
1207
- var ApplicationShutdownBudgetSchema = ShutdownOptionsSchema.pick({
1208
- gracePeriodMs: true,
1209
- forceTimeoutMs: true
1210
- });
1211
- var ApplicationRestartInputSchema = z10.object({
1212
- resourceId: z10.string().min(1),
1213
- gracePeriodMs: z10.number().int().nonnegative().optional(),
1214
- forceTimeoutMs: z10.number().int().nonnegative().optional()
1215
- }).strict().readonly();
1216
- var ApplicationRestartOutcomeSchema = z10.enum(["restarted", "failed", "refused"]);
1217
- var ApplicationRestartResultSchema = z10.object({
1218
- resourceId: z10.string(),
1219
- affected: z10.array(z10.string()).readonly(),
1220
- outcome: ApplicationRestartOutcomeSchema,
1221
- reason: z10.string().optional(),
1222
- durationMs: z10.number().int().nonnegative()
1223
- }).strict().readonly();
1224
-
1225
- class ApplicationAdmissionError extends AppError {
1226
- constructor() {
1227
- super("APPLICATION_NOT_ACCEPTING", "Application is not accepting new operations", 503);
1228
- this.name = "ApplicationAdmissionError";
1229
- }
1230
- }
1231
- function isStartResult(value) {
1232
- return typeof value === "object" && value !== null;
1233
- }
1234
- function waitForAbort(signal) {
1235
- if (signal.aborted)
1236
- return Promise.resolve();
1237
- return new Promise((resolve) => signal.addEventListener("abort", () => resolve(), { once: true }));
1238
- }
1239
- async function untilDeadline(work, signal) {
1240
- const result = await Promise.race([
1241
- work.then((value) => ({ settled: true, value }), (error) => ({ settled: true, error })),
1242
- waitForAbort(signal).then(() => ({ settled: false }))
1243
- ]);
1244
- return result;
1245
- }
1246
- function createApplication(config) {
1247
- const id = ApplicationIdSchema.parse(config.id);
1248
- const ordered = resolveResourceGraph(config.resources ?? []);
1249
- const shutdownBudget = ApplicationShutdownBudgetSchema.parse(config.shutdown ?? {});
1250
- const reverse = [...ordered].reverse();
1251
- const reportFailure = (resourceId, phase, error) => {
1252
- if (!config.onResourceFailure)
1253
- return;
1254
- try {
1255
- Promise.resolve(config.onResourceFailure({ resourceId, phase, error })).catch(() => {
1256
- return;
1257
- });
1258
- } catch {}
1259
- };
1260
- const published = new Map;
1261
- const records = new Map;
1262
- for (const entry of ordered) {
1263
- records.set(entry.id, {
1264
- entry,
1265
- state: "registered",
1266
- health: "unknown",
1267
- healthReported: false,
1268
- everHealthy: false,
1269
- attempted: false,
1270
- activated: false,
1271
- closeInvoked: false,
1272
- closed: false,
1273
- failures: []
1274
- });
1275
- }
1276
- const epoch = crypto.randomUUID();
1277
- const listeners = new Set;
1278
- const lifetimeAbort = new AbortController;
1279
- const startupAbort = new AbortController;
1280
- let lifecycle = "created";
1281
- let revision = 0;
1282
- let changedAt = new Date().toISOString();
1283
- let accepting = false;
1284
- let accepted = 0;
1285
- let completed = 0;
1286
- let pending = 0;
1287
- let startPromise;
1288
- let shutdownPromise;
1289
- let shutdownRequested = false;
1290
- let activationComplete = false;
1291
- const pendingWaiters = new Set;
1292
- const aggregateHealth = () => {
1293
- if (lifecycle === "created" || lifecycle === "starting")
1294
- return "unknown";
1295
- let optionalUnhealthy = false;
1296
- for (const record of records.values()) {
1297
- if (record.entry.required && (record.state !== "ready" || record.health !== "healthy")) {
1298
- return "unhealthy";
1299
- }
1300
- if (!record.entry.required && (record.state !== "ready" || record.health !== "healthy")) {
1301
- optionalUnhealthy = true;
1302
- }
1303
- }
1304
- return optionalUnhealthy ? "degraded" : "healthy";
1305
- };
1306
- const isReady = () => lifecycle === "ready" && [...records.values()].every((record) => !record.entry.required || record.state === "ready" && record.health === "healthy");
1307
- const snapshot = () => ApplicationSnapshotSchema.parse({
1308
- id,
1309
- epoch,
1310
- revision,
1311
- lifecycle,
1312
- health: aggregateHealth(),
1313
- ready: isReady(),
1314
- capturedAt: new Date().toISOString(),
1315
- changedAt,
1316
- admission: { accepting, accepted, completed, pending },
1317
- resources: ordered.map((entry) => {
1318
- const record = records.get(entry.id);
1319
- if (!record)
1320
- throw new Error("Managed resource record disappeared");
1321
- return {
1322
- id: entry.id,
1323
- required: entry.required,
1324
- dependsOn: entry.dependsOn,
1325
- state: record.state,
1326
- health: record.health,
1327
- ready: record.state === "ready" && record.health === "healthy"
1328
- };
1329
- })
1330
- });
1331
- const publish = () => {
1332
- revision += 1;
1333
- changedAt = new Date().toISOString();
1334
- const value = snapshot();
1335
- for (const listener of listeners) {
1336
- try {
1337
- listener(value);
1338
- } catch {}
1339
- }
1340
- if (config.onSnapshot) {
1341
- Promise.resolve().then(() => config.onSnapshot?.(value)).catch(() => {});
1342
- }
1343
- };
1344
- const contextFor = (record, options = {}) => ({
1345
- applicationId: id,
1346
- signal: options.signal ?? lifetimeAbort.signal,
1347
- ...options.deadlineAt !== undefined && { deadlineAt: options.deadlineAt },
1348
- ...options.forceDeadlineAt !== undefined && {
1349
- forceDeadlineAt: options.forceDeadlineAt
1350
- },
1351
- now: () => performance.now(),
1352
- use(resource) {
1353
- const dependencyId = resource?.id;
1354
- if (typeof dependencyId !== "string" || dependencyId.length === 0) {
1355
- throw new Error(`[stitchkit] resource "${record.entry.id}": use() takes a managed resource, and this one has no id`);
1356
- }
1357
- if (!record.entry.dependsOn.includes(dependencyId)) {
1358
- throw new Error(`[stitchkit] resource "${record.entry.id}" used "${dependencyId}" without declaring it in dependsOn`);
1359
- }
1360
- if (!published.has(dependencyId)) {
1361
- throw new Error(`[stitchkit] resource "${record.entry.id}" used "${dependencyId}", which published no value from start()`);
1362
- }
1363
- return published.get(dependencyId);
1364
- },
1365
- reportHealth(health) {
1366
- if (record.state === "stopped")
1367
- return;
1368
- record.health = health;
1369
- record.healthReported = true;
1370
- if (health === "healthy")
1371
- record.everHealthy = true;
1372
- accepting = activationComplete && !shutdownRequested && isReady();
1373
- publish();
1374
- }
1375
- });
1376
- const markLateCompletion = (record, failure) => {
1377
- if (shutdownRequested || record.state === "stopping" || record.state === "stopped")
1378
- return;
1379
- if (failure) {
1380
- record.failures.push("completion");
1381
- reportFailure(record.entry.id, "completion", failure.error);
1382
- }
1383
- record.state = "failed";
1384
- record.health = "unhealthy";
1385
- accepting = activationComplete && isReady();
1386
- publish();
1387
- };
1388
- const closeAttempted = async () => {
1389
- const errors = [];
1390
- const rollbackStartedAt = performance.now();
1391
- const rollbackDeadlineAt = rollbackStartedAt + shutdownBudget.gracePeriodMs;
1392
- const rollbackForceDeadlineAt = rollbackDeadlineAt + shutdownBudget.forceTimeoutMs;
1393
- const rollbackAbort = new AbortController;
1394
- const rollbackTimer = setTimeout(() => rollbackAbort.abort(), Math.max(0, rollbackForceDeadlineAt - performance.now()));
1395
- try {
1396
- await closeEachAttempted(rollbackAbort.signal, errors, {
1397
- deadlineAt: rollbackDeadlineAt,
1398
- forceDeadlineAt: rollbackForceDeadlineAt
1399
- });
1400
- } finally {
1401
- clearTimeout(rollbackTimer);
1402
- }
1403
- return errors;
1404
- };
1405
- const closeEachAttempted = async (bound, errors, deadlines) => {
1406
- for (const entry of reverse) {
1407
- const record = records.get(entry.id);
1408
- if (!record?.attempted || record.closed)
1409
- continue;
1410
- record.state = "stopping";
1411
- try {
1412
- record.closeInvoked = true;
1413
- const settled = await untilDeadline(Promise.resolve(entry.resource.close?.(contextFor(record, { signal: startupAbort.signal, ...deadlines }))), bound);
1414
- if (!settled.settled) {
1415
- record.failures.push("close");
1416
- record.state = "failed";
1417
- const timedOut = new Error(`[stitchkit] resource "${entry.id}" did not finish closing during rollback`);
1418
- reportFailure(entry.id, "close", timedOut);
1419
- errors.push(timedOut);
1420
- publish();
1421
- break;
1422
- }
1423
- if (settled.error !== undefined)
1424
- throw settled.error;
1425
- record.closed = true;
1426
- record.state = "stopped";
1427
- } catch (error) {
1428
- record.failures.push("close");
1429
- record.state = "failed";
1430
- reportFailure(entry.id, "close", error);
1431
- errors.push(error);
1432
- }
1433
- publish();
1434
- }
1435
- };
1436
- const startEach = async (entries, signal) => {
1437
- for (const entry of entries) {
1438
- if (shutdownRequested || signal.aborted) {
1439
- throw new ApplicationStartupInterruptedError;
1440
- }
1441
- const record = records.get(entry.id);
1442
- if (!record)
1443
- throw new Error("Managed resource record disappeared");
1444
- const dependencyFailed = entry.dependsOn.some((dependencyId) => records.get(dependencyId)?.state !== "ready");
1445
- if (dependencyFailed) {
1446
- record.state = "failed";
1447
- record.health = "unhealthy";
1448
- record.failures.push("start");
1449
- publish();
1450
- if (entry.required) {
1451
- throw new Error(`[stitchkit] required resource "${entry.id}" has an unavailable dependency`);
1452
- }
1453
- continue;
1454
- }
1455
- record.attempted = true;
1456
- record.state = "starting";
1457
- publish();
1458
- try {
1459
- const started = await entry.resource.start(contextFor(record, { signal }));
1460
- if (shutdownRequested || signal.aborted) {
1461
- throw new ApplicationStartupInterruptedError;
1462
- }
1463
- if (isStartResult(started)) {
1464
- record.runtime = started;
1465
- if (started.value !== undefined)
1466
- published.set(entry.id, started.value);
1467
- let resourceReady = started.ready === undefined;
1468
- let completionSettled = false;
1469
- let completionFailure;
1470
- const completion = started.completion?.then(() => {
1471
- completionSettled = true;
1472
- if (resourceReady)
1473
- markLateCompletion(record);
1474
- }, (error) => {
1475
- completionSettled = true;
1476
- completionFailure = error;
1477
- if (resourceReady)
1478
- markLateCompletion(record, { error });
1479
- });
1480
- if (started.ready && completion) {
1481
- const readiness = started.ready.then(() => "ready");
1482
- const completionBeforeReady = completion.then(() => "completion");
1483
- const first = await Promise.race([readiness, completionBeforeReady]);
1484
- if (first === "completion") {
1485
- throw new ResourceCompletionBeforeReadyError(entry.id, completionFailure);
1486
- }
1487
- resourceReady = true;
1488
- if (completionSettled) {
1489
- throw new ResourceCompletionBeforeReadyError(entry.id, completionFailure);
1490
- }
1491
- } else if (started.ready) {
1492
- await started.ready;
1493
- resourceReady = true;
1494
- } else if (completion) {}
1495
- }
1496
- if (shutdownRequested || signal.aborted) {
1497
- throw new ApplicationStartupInterruptedError;
1498
- }
1499
- record.state = "ready";
1500
- if (!record.healthReported) {
1501
- record.health = "healthy";
1502
- record.everHealthy = true;
1503
- }
1504
- publish();
1505
- } catch (error) {
1506
- const interrupted = shutdownRequested || signal.aborted;
1507
- if (!(error instanceof ApplicationStartupInterruptedError)) {
1508
- record.failures.push(error instanceof ResourceCompletionBeforeReadyError ? "completion" : record.runtime?.ready ? "ready" : "start");
1509
- record.state = "failed";
1510
- record.health = "unhealthy";
1511
- reportFailure(entry.id, record.failures[record.failures.length - 1] ?? "start", error);
1512
- publish();
1513
- }
1514
- if (interrupted || entry.required)
1515
- throw error;
1516
- }
1517
- }
1518
- };
1519
- const activateEach = async (entries, signal) => {
1520
- for (const entry of entries) {
1521
- if (shutdownRequested || signal.aborted) {
1522
- throw new ApplicationStartupInterruptedError;
1523
- }
1524
- const record = records.get(entry.id);
1525
- if (record?.state !== "ready")
1526
- continue;
1527
- const dependencyUnavailable = entry.dependsOn.some((dependencyId) => {
1528
- const dependency = records.get(dependencyId);
1529
- return dependency?.state !== "ready" || !dependency.activated;
1530
- });
1531
- if (dependencyUnavailable) {
1532
- record.failures.push("start");
1533
- record.state = "failed";
1534
- record.health = "unhealthy";
1535
- publish();
1536
- if (entry.required) {
1537
- throw new Error(`[stitchkit] required resource "${entry.id}" has an unavailable activation dependency`);
1538
- }
1539
- continue;
1540
- }
1541
- try {
1542
- await entry.resource.activate?.(contextFor(record));
1543
- if (shutdownRequested || signal.aborted) {
1544
- throw new ApplicationStartupInterruptedError;
1545
- }
1546
- record.activated = true;
1547
- if (entry.required && (record.state !== "ready" || record.health !== "healthy")) {
1548
- const observed = `${record.state}/${record.health}`;
1549
- 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\`.`);
1550
- }
1551
- } catch (error) {
1552
- const interrupted = shutdownRequested || signal.aborted;
1553
- if (!(error instanceof ApplicationStartupInterruptedError)) {
1554
- record.failures.push("start");
1555
- record.state = "failed";
1556
- record.health = "unhealthy";
1557
- reportFailure(entry.id, "start", error);
1558
- publish();
1559
- }
1560
- if (interrupted || entry.required)
1561
- throw error;
1562
- }
1563
- }
1564
- };
1565
- const subtreeOf = (resourceId) => {
1566
- const affected = new Set([resourceId]);
1567
- for (const entry of ordered) {
1568
- if (entry.dependsOn.some((dependencyId) => affected.has(dependencyId))) {
1569
- affected.add(entry.id);
1570
- }
1571
- }
1572
- return ordered.filter((entry) => affected.has(entry.id));
1573
- };
1574
- const closeOne = async (entry, record, signal, deadlines) => {
1575
- const context = () => contextFor(record, { ...deadlines, signal });
1576
- if (record.attempted && !record.closed) {
1577
- const phase = async (work, what) => {
1578
- const settled = await untilDeadline(Promise.resolve(work), signal);
1579
- if (!settled.settled) {
1580
- throw new Error(`[stitchkit] resource "${entry.id}" did not ${what} within the restart budget`);
1581
- }
1582
- if (settled.error !== undefined)
1583
- throw settled.error;
1584
- };
1585
- await phase(entry.resource.stopAdmission?.(context()), "stop admitting");
1586
- await phase(entry.resource.drain?.(context()), "drain");
1587
- record.closeInvoked = true;
1588
- await phase(entry.resource.close?.(context()), "close");
1589
- }
1590
- record.closed = false;
1591
- record.closeInvoked = false;
1592
- record.attempted = false;
1593
- record.activated = false;
1594
- record.state = "registered";
1595
- record.health = "unknown";
1596
- record.healthReported = false;
1597
- record.runtime = undefined;
1598
- published.delete(entry.id);
1599
- };
1600
- let restarting = Promise.resolve();
1601
- const runRestart = async (input) => {
1602
- const startedAt = Date.now();
1603
- const parsed = ApplicationRestartInputSchema.parse(input);
1604
- const affected = subtreeOf(parsed.resourceId);
1605
- const affectedIds = affected.map((entry) => entry.id);
1606
- const refuse = (reason) => ({
1607
- resourceId: parsed.resourceId,
1608
- affected: affectedIds,
1609
- outcome: "refused",
1610
- reason,
1611
- durationMs: Date.now() - startedAt
1612
- });
1613
- if (!records.has(parsed.resourceId)) {
1614
- return refuse(`no resource is registered as "${parsed.resourceId}"`);
1615
- }
1616
- if (shutdownRequested) {
1617
- return refuse("the application is shutting down");
1618
- }
1619
- if (lifecycle !== "ready") {
1620
- return refuse(`the application is ${lifecycle}, not ready`);
1621
- }
1622
- const budget = {
1623
- gracePeriodMs: parsed.gracePeriodMs ?? shutdownBudget.gracePeriodMs,
1624
- forceTimeoutMs: parsed.forceTimeoutMs ?? shutdownBudget.forceTimeoutMs
1625
- };
1626
- const closeDeadlineAt = performance.now() + budget.gracePeriodMs;
1627
- const deadlines = {
1628
- deadlineAt: closeDeadlineAt,
1629
- forceDeadlineAt: closeDeadlineAt + budget.forceTimeoutMs
1630
- };
1631
- const restartAbort = new AbortController;
1632
- const closeTimer = setTimeout(() => restartAbort.abort(), Math.max(0, deadlines.forceDeadlineAt - performance.now()));
1633
- const stillFailed = () => affectedIds.filter((id2) => records.get(id2)?.state === "failed");
1634
- try {
1635
- for (const entry of [...affected].reverse()) {
1636
- const record = records.get(entry.id);
1637
- if (record)
1638
- await closeOne(entry, record, restartAbort.signal, deadlines);
1639
- }
1640
- publish();
1641
- await startEach(affected, restartAbort.signal);
1642
- await activateEach(affected, restartAbort.signal);
1643
- publish();
1644
- const failed = stillFailed();
1645
- if (failed.length > 0) {
1646
- return {
1647
- resourceId: parsed.resourceId,
1648
- affected: affectedIds,
1649
- outcome: "failed",
1650
- reason: `did not come back: ${failed.join(", ")}`,
1651
- durationMs: Date.now() - startedAt
1652
- };
1653
- }
1654
- return {
1655
- resourceId: parsed.resourceId,
1656
- affected: affectedIds,
1657
- outcome: "restarted",
1658
- durationMs: Date.now() - startedAt
1659
- };
1660
- } catch (error) {
1661
- restartAbort.abort();
1662
- publish();
1663
- return {
1664
- resourceId: parsed.resourceId,
1665
- affected: affectedIds,
1666
- outcome: "failed",
1667
- reason: error instanceof Error ? error.message : String(error),
1668
- durationMs: Date.now() - startedAt
1669
- };
1670
- } finally {
1671
- clearTimeout(closeTimer);
1672
- }
1673
- };
1674
- const runStart = async () => {
1675
- lifecycle = "starting";
1676
- publish();
1677
- let startFailure;
1678
- try {
1679
- await startEach(ordered, startupAbort.signal);
1680
- lifecycle = "ready";
1681
- publish();
1682
- await activateEach(ordered, startupAbort.signal);
1683
- if (shutdownRequested) {
1684
- throw new ApplicationStartupInterruptedError;
1685
- }
1686
- if (!isReady()) {
1687
- const blocking = [...records.values()].filter((record) => record.entry.required && (record.state !== "ready" || record.health !== "healthy")).map((record) => `${record.entry.id} (${record.state}/${record.health})`);
1688
- if (blocking.length === 0)
1689
- blocking.push("(none identified)");
1690
- const chosen = [...records.values()].some((record) => record.entry.required && record.health !== "healthy" && !record.everHealthy);
1691
- 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."}`);
1692
- }
1693
- activationComplete = true;
1694
- accepting = isReady();
1695
- publish();
1696
- return snapshot();
1697
- } catch (error) {
1698
- startFailure = error;
1699
- }
1700
- if (!shutdownRequested) {
1701
- const rollbackErrors = await closeAttempted();
1702
- lifecycle = "failed";
1703
- accepting = false;
1704
- publish();
1705
- if (rollbackErrors.length > 0) {
1706
- throw new AggregateError([startFailure, ...rollbackErrors], "[stitchkit] application startup and rollback failed", { cause: startFailure });
1707
- }
1708
- }
1709
- throw startFailure;
1710
- };
1711
- const start = () => {
1712
- if (startPromise)
1713
- return startPromise;
1714
- if (lifecycle !== "created") {
1715
- return Promise.reject(new Error(`[stitchkit] application cannot start from lifecycle "${lifecycle}"`));
1716
- }
1717
- startPromise = runStart();
1718
- startPromise.catch(() => {
1719
- return;
1720
- });
1721
- return startPromise;
1722
- };
1723
- const acquire = () => {
1724
- if (!accepting || !isReady())
1725
- return null;
1726
- accepted += 1;
1727
- pending += 1;
1728
- publish();
1729
- let released = false;
1730
- return {
1731
- get released() {
1732
- return released;
1733
- },
1734
- release() {
1735
- if (released)
1736
- return;
1737
- released = true;
1738
- pending -= 1;
1739
- completed += 1;
1740
- if (pending === 0) {
1741
- for (const waiter of pendingWaiters)
1742
- waiter();
1743
- pendingWaiters.clear();
1744
- }
1745
- publish();
1746
- }
1747
- };
1748
- };
1749
- const run = async (work) => {
1750
- const lease = acquire();
1751
- if (!lease)
1752
- throw new ApplicationAdmissionError;
1753
- try {
1754
- return await work();
1755
- } finally {
1756
- lease.release();
1757
- }
1758
- };
1759
- const waitForPending = () => {
1760
- if (pending === 0)
1761
- return Promise.resolve();
1762
- return new Promise((resolve) => pendingWaiters.add(resolve));
1763
- };
1764
- const shutdown = (options) => {
1765
- if (shutdownPromise)
1766
- return shutdownPromise;
1767
- const requested = options ?? {};
1768
- const parsed = ApplicationShutdownOptionsSchema.parse({
1769
- gracePeriodMs: requested.gracePeriodMs ?? shutdownBudget.gracePeriodMs,
1770
- forceTimeoutMs: requested.forceTimeoutMs ?? shutdownBudget.forceTimeoutMs,
1771
- ...requested.signal !== undefined && { signal: requested.signal }
1772
- });
1773
- const startedAt = performance.now();
1774
- const graceDeadlineAt = startedAt + parsed.gracePeriodMs;
1775
- const forceDeadlineAt = graceDeadlineAt + parsed.forceTimeoutMs;
1776
- shutdownRequested = true;
1777
- accepting = false;
1778
- startupAbort.abort();
1779
- lifecycle = lifecycle === "created" ? "stopping" : "draining";
1780
- publish();
1781
- shutdownPromise = (async () => {
1782
- const gracefulAbort = new AbortController;
1783
- let forcedReason;
1784
- let gracefulFailed = [...records.values()].some((record) => record.attempted && record.closeInvoked && !record.closed);
1785
- const force = (reason) => {
1786
- if (forcedReason)
1787
- return;
1788
- forcedReason = reason;
1789
- gracefulAbort.abort();
1790
- lifetimeAbort.abort();
1791
- };
1792
- const graceTimer = setTimeout(() => force("deadline"), Math.max(0, graceDeadlineAt - performance.now()));
1793
- const onExternalAbort = () => force("signal");
1794
- parsed.signal?.addEventListener("abort", onExternalAbort, { once: true });
1795
- if (parsed.signal?.aborted)
1796
- force("signal");
1797
- if (startPromise) {
1798
- await untilDeadline(startPromise.catch(() => {
1799
- return;
1800
- }), gracefulAbort.signal);
1801
- }
1802
- const gracefulContext = (record) => contextFor(record, {
1803
- signal: gracefulAbort.signal,
1804
- deadlineAt: graceDeadlineAt,
1805
- forceDeadlineAt
1806
- });
1807
- for (const entry of reverse) {
1808
- if (gracefulAbort.signal.aborted)
1809
- break;
1810
- const record = records.get(entry.id);
1811
- if (!record?.attempted || record.closed)
1812
- continue;
1813
- try {
1814
- const result = await untilDeadline(Promise.resolve(entry.resource.stopAdmission?.(gracefulContext(record))), gracefulAbort.signal);
1815
- if (!result.settled)
1816
- break;
1817
- if (result.error !== undefined)
1818
- throw result.error;
1819
- } catch (error) {
1820
- record.failures.push("admission");
1821
- reportFailure(entry.id, "admission", error);
1822
- gracefulFailed = true;
1823
- lifetimeAbort.abort();
1824
- }
1825
- }
1826
- if (!gracefulAbort.signal.aborted && !gracefulFailed) {
1827
- const result = await untilDeadline(waitForPending(), gracefulAbort.signal);
1828
- if (!result.settled)
1829
- force("deadline");
1830
- }
1831
- for (const entry of reverse) {
1832
- if (gracefulAbort.signal.aborted || gracefulFailed)
1833
- break;
1834
- const record = records.get(entry.id);
1835
- if (!record?.attempted || record.closed)
1836
- continue;
1837
- record.state = "stopping";
1838
- publish();
1839
- try {
1840
- const drained = await untilDeadline(Promise.resolve(entry.resource.drain?.(gracefulContext(record))), gracefulAbort.signal);
1841
- if (!drained.settled) {
1842
- force("deadline");
1843
- break;
1844
- }
1845
- if (drained.error !== undefined)
1846
- throw drained.error;
1847
- } catch (error) {
1848
- record.failures.push("drain");
1849
- reportFailure(entry.id, "drain", error);
1850
- gracefulFailed = true;
1851
- lifetimeAbort.abort();
1852
- break;
1853
- }
1854
- }
1855
- lifecycle = "stopping";
1856
- publish();
1857
- for (const entry of reverse) {
1858
- if (gracefulAbort.signal.aborted || gracefulFailed)
1859
- break;
1860
- const record = records.get(entry.id);
1861
- if (!record?.attempted || record.closed || record.closeInvoked)
1862
- continue;
1863
- try {
1864
- record.closeInvoked = true;
1865
- const closed = await untilDeadline(Promise.resolve(entry.resource.close?.(gracefulContext(record))), gracefulAbort.signal);
1866
- if (!closed.settled) {
1867
- force("deadline");
1868
- break;
1869
- }
1870
- if (closed.error !== undefined)
1871
- throw closed.error;
1872
- record.closed = true;
1873
- record.state = "stopped";
1874
- publish();
1875
- } catch (error) {
1876
- record.failures.push("close");
1877
- reportFailure(entry.id, "close", error);
1878
- record.state = "failed";
1879
- gracefulFailed = true;
1880
- lifetimeAbort.abort();
1881
- publish();
1882
- break;
1883
- }
1884
- }
1885
- clearTimeout(graceTimer);
1886
- parsed.signal?.removeEventListener("abort", onExternalAbort);
1887
- const mustForce = forcedReason !== undefined || gracefulFailed;
1888
- const pendingOperationsAtForce = mustForce ? pending : 0;
1889
- if (mustForce) {
1890
- lifetimeAbort.abort();
1891
- const forceAbort = new AbortController;
1892
- const forceTimer = setTimeout(() => forceAbort.abort(), Math.max(0, forceDeadlineAt - performance.now()));
1893
- await Promise.all(reverse.map(async (entry) => {
1894
- const record = records.get(entry.id);
1895
- if (!record?.attempted || record.closed)
1896
- return;
1897
- try {
1898
- let cleanup;
1899
- if (entry.resource.force) {
1900
- cleanup = Promise.resolve(entry.resource.force(contextFor(record, {
1901
- signal: forceAbort.signal,
1902
- deadlineAt: graceDeadlineAt,
1903
- forceDeadlineAt
1904
- })));
1905
- } else if (entry.resource.close && !record.closeInvoked) {
1906
- record.closeInvoked = true;
1907
- cleanup = Promise.resolve(entry.resource.close(contextFor(record, {
1908
- signal: forceAbort.signal,
1909
- deadlineAt: graceDeadlineAt,
1910
- forceDeadlineAt
1911
- })));
1912
- } else if (entry.resource.close) {
1913
- record.failures.push("force");
1914
- reportFailure(entry.id, "force", new Error(`[stitchkit] resource "${entry.id}" was already closing and did not settle before the force deadline`));
1915
- return;
1916
- }
1917
- const forced = await untilDeadline(cleanup ?? Promise.resolve(), forceAbort.signal);
1918
- if (!forced.settled || forced.error !== undefined) {
1919
- record.failures.push("force");
1920
- reportFailure(entry.id, "force", forced.settled ? forced.error : new Error("[stitchkit] forced cleanup did not settle in time"));
1921
- return;
1922
- }
1923
- record.closed = true;
1924
- record.state = "stopped";
1925
- } catch (error) {
1926
- record.failures.push("force");
1927
- reportFailure(entry.id, "force", error);
1928
- }
1929
- }));
1930
- clearTimeout(forceTimer);
1931
- }
1932
- const cleanupComplete = [...records.values()].every((record) => !record.attempted || record.closed);
1933
- lifecycle = cleanupComplete ? "stopped" : "failed";
1934
- publish();
1935
- return ApplicationShutdownResultSchema.parse({
1936
- outcome: mustForce ? "forced" : "clean",
1937
- ...forcedReason && { reason: forcedReason },
1938
- cleanupComplete,
1939
- acceptedOperations: accepted,
1940
- completedOperations: completed,
1941
- pendingOperations: pending,
1942
- pendingOperationsAtForce,
1943
- resources: ordered.map((entry) => {
1944
- const record = records.get(entry.id);
1945
- if (!record)
1946
- throw new Error("Managed resource record disappeared");
1947
- return {
1948
- id: entry.id,
1949
- state: !record.attempted ? "not-started" : record.closed ? "closed" : "force-failed",
1950
- failures: record.failures
1951
- };
1952
- }),
1953
- durationMs: performance.now() - startedAt
1954
- });
1955
- })();
1956
- shutdownPromise.catch(() => {
1957
- return;
1958
- });
1959
- return shutdownPromise;
1960
- };
1961
- return {
1962
- id,
1963
- admission: { acquire, run },
1964
- start,
1965
- getSnapshot: snapshot,
1966
- subscribe(listener) {
1967
- listeners.add(listener);
1968
- listener(snapshot());
1969
- return () => listeners.delete(listener);
1970
- },
1971
- shutdown,
1972
- restart(input) {
1973
- const queued = restarting.then(() => runRestart(input));
1974
- restarting = queued.then(() => {
1975
- return;
1976
- }, () => {
1977
- return;
1978
- });
1979
- return queued;
1980
- }
1981
- };
1982
- }
1983
613
  // src/application/keyspace.ts
1984
614
  function defineKeyspace(name, declaration) {
1985
615
  if (name.trim() === "") {
@@ -2148,308 +778,6 @@ function sqliteKeyspaceBackend(declaration, config) {
2148
778
  }
2149
779
  };
2150
780
  }
2151
- // src/application/schedule.ts
2152
- import { z as z11 } from "zod";
2153
- var NonNegativeSafeIntegerSchema = z11.number().int().nonnegative().refine(Number.isSafeInteger, "Expected a safe integer");
2154
- var PositiveSafeIntegerSchema3 = NonNegativeSafeIntegerSchema.refine((value) => value > 0, "Expected a positive integer");
2155
- var ManagedScheduleOverlapSchema = z11.discriminatedUnion("mode", [
2156
- z11.object({ mode: z11.literal("skip") }).readonly(),
2157
- z11.object({ mode: z11.literal("queue-one") }).readonly(),
2158
- z11.object({
2159
- mode: z11.literal("parallel"),
2160
- maxConcurrent: PositiveSafeIntegerSchema3
2161
- }).readonly()
2162
- ]);
2163
- var ManagedScheduleErrorPolicySchema = z11.enum(["continue", "stop-schedule"]);
2164
- var ManagedScheduleDescriptorSchema = z11.object({
2165
- id: ApplicationIdSchema,
2166
- everyMs: PositiveSafeIntegerSchema3,
2167
- startAfterMs: NonNegativeSafeIntegerSchema,
2168
- overlap: ManagedScheduleOverlapSchema,
2169
- errorPolicy: ManagedScheduleErrorPolicySchema
2170
- }).readonly();
2171
- var ManagedScheduleStatusSchema = z11.object({
2172
- descriptor: ManagedScheduleDescriptorSchema,
2173
- state: z11.enum(["inactive", "scheduled", "running", "draining", "stopped"]),
2174
- revision: NonNegativeSafeIntegerSchema,
2175
- capturedAt: z11.string().datetime({ offset: true }),
2176
- changedAt: z11.string().datetime({ offset: true }),
2177
- accepting: z11.boolean(),
2178
- active: NonNegativeSafeIntegerSchema,
2179
- queued: z11.boolean(),
2180
- runsStarted: NonNegativeSafeIntegerSchema,
2181
- runsCompleted: NonNegativeSafeIntegerSchema,
2182
- runsFailed: NonNegativeSafeIntegerSchema,
2183
- ticksSkipped: NonNegativeSafeIntegerSchema,
2184
- nextRunAt: z11.string().datetime({ offset: true }).nullable(),
2185
- lastScheduledAt: z11.string().datetime({ offset: true }).nullable(),
2186
- lastStartedAt: z11.string().datetime({ offset: true }).nullable(),
2187
- lastFinishedAt: z11.string().datetime({ offset: true }).nullable()
2188
- }).readonly();
2189
- var systemClock = {
2190
- now: () => performance.now(),
2191
- wallNow: () => new Date,
2192
- schedule(callback, delayMs) {
2193
- const timer = setTimeout(callback, delayMs);
2194
- return { cancel: () => clearTimeout(timer) };
2195
- }
2196
- };
2197
- var DEFAULT_OVERLAP = { mode: "skip" };
2198
- function createManagedSchedule(config) {
2199
- const descriptor = ManagedScheduleDescriptorSchema.parse({
2200
- id: config.id,
2201
- everyMs: config.everyMs,
2202
- startAfterMs: config.startAfterMs ?? config.everyMs,
2203
- overlap: config.overlap ?? DEFAULT_OVERLAP,
2204
- errorPolicy: config.errorPolicy ?? "continue"
2205
- });
2206
- const dependsOn = config.dependsOn?.map((id) => ApplicationIdSchema.parse(id));
2207
- const clock = config.clock ?? systemClock;
2208
- let revision = 0;
2209
- let accepting = false;
2210
- let activated = false;
2211
- let stopped = false;
2212
- let timer = null;
2213
- let activationContext = null;
2214
- let nextRunAt = null;
2215
- let queuedAt = null;
2216
- let runsStarted = 0;
2217
- let runsCompleted = 0;
2218
- let runsFailed = 0;
2219
- let ticksSkipped = 0;
2220
- let lastScheduledAt = null;
2221
- let lastStartedAt = null;
2222
- let lastFinishedAt = null;
2223
- let changedAt = clock.wallNow().toISOString();
2224
- const active = new Set;
2225
- const changed = () => {
2226
- revision += 1;
2227
- changedAt = clock.wallNow().toISOString();
2228
- };
2229
- const cancelFuture = () => {
2230
- timer?.cancel();
2231
- timer = null;
2232
- nextRunAt = null;
2233
- queuedAt = null;
2234
- };
2235
- const stopAdmission = () => {
2236
- if (!accepting && stopped)
2237
- return;
2238
- accepting = false;
2239
- stopped = true;
2240
- cancelFuture();
2241
- changed();
2242
- };
2243
- const reportError = (error, context) => {
2244
- if (!config.onError)
2245
- return;
2246
- Promise.resolve().then(() => config.onError?.(error, context)).catch(() => {});
2247
- };
2248
- const state = () => {
2249
- if (!activated)
2250
- return stopped ? "stopped" : "inactive";
2251
- if (!accepting)
2252
- return active.size > 0 ? "draining" : "stopped";
2253
- return active.size > 0 ? "running" : "scheduled";
2254
- };
2255
- const wallAt = (monotonicAt, anchor) => {
2256
- if (monotonicAt === null)
2257
- return null;
2258
- return new Date(anchor.wallNow + monotonicAt - anchor.monotonicNow).toISOString();
2259
- };
2260
- const status = () => {
2261
- const captured = clock.wallNow();
2262
- const anchor = { monotonicNow: clock.now(), wallNow: captured.getTime() };
2263
- return ManagedScheduleStatusSchema.parse({
2264
- descriptor,
2265
- state: state(),
2266
- revision,
2267
- capturedAt: captured.toISOString(),
2268
- changedAt,
2269
- accepting,
2270
- active: active.size,
2271
- queued: queuedAt !== null,
2272
- runsStarted,
2273
- runsCompleted,
2274
- runsFailed,
2275
- ticksSkipped,
2276
- nextRunAt: wallAt(nextRunAt, anchor),
2277
- lastScheduledAt,
2278
- lastStartedAt,
2279
- lastFinishedAt
2280
- });
2281
- };
2282
- let startExecution = () => {
2283
- return;
2284
- };
2285
- const settleExecution = () => {
2286
- if (!accepting || queuedAt === null || active.size !== 0)
2287
- return;
2288
- const successorAt = queuedAt;
2289
- queuedAt = null;
2290
- changed();
2291
- startExecution(successorAt);
2292
- };
2293
- startExecution = (scheduledAt) => {
2294
- const resourceContext = activationContext;
2295
- if (!accepting || !resourceContext)
2296
- return;
2297
- const startedAt = clock.now();
2298
- const wallStartedAt = clock.wallNow().getTime();
2299
- const runContext = {
2300
- applicationId: resourceContext.applicationId,
2301
- signal: resourceContext.signal,
2302
- scheduledAt,
2303
- startedAt,
2304
- now: () => clock.now()
2305
- };
2306
- runsStarted += 1;
2307
- lastScheduledAt = new Date(wallStartedAt + scheduledAt - startedAt).toISOString();
2308
- lastStartedAt = new Date(wallStartedAt).toISOString();
2309
- changed();
2310
- let tracked;
2311
- tracked = Promise.resolve().then(() => config.run(runContext)).then(() => {
2312
- runsCompleted += 1;
2313
- lastFinishedAt = clock.wallNow().toISOString();
2314
- if (descriptor.errorPolicy === "continue" && accepting) {
2315
- resourceContext.reportHealth("healthy");
2316
- }
2317
- }, (error) => {
2318
- runsFailed += 1;
2319
- lastFinishedAt = clock.wallNow().toISOString();
2320
- reportError(error, runContext);
2321
- if (descriptor.errorPolicy === "stop-schedule") {
2322
- resourceContext.reportHealth("unhealthy");
2323
- stopAdmission();
2324
- } else {
2325
- resourceContext.reportHealth("degraded");
2326
- }
2327
- }).finally(() => {
2328
- active.delete(tracked);
2329
- changed();
2330
- settleExecution();
2331
- });
2332
- active.add(tracked);
2333
- };
2334
- const dispatchTick = (scheduledAt) => {
2335
- if (!accepting)
2336
- return;
2337
- if (descriptor.overlap.mode === "skip") {
2338
- if (active.size > 0) {
2339
- ticksSkipped += 1;
2340
- changed();
2341
- return;
2342
- }
2343
- startExecution(scheduledAt);
2344
- return;
2345
- }
2346
- if (descriptor.overlap.mode === "queue-one") {
2347
- if (active.size > 0) {
2348
- queuedAt = scheduledAt;
2349
- changed();
2350
- return;
2351
- }
2352
- startExecution(scheduledAt);
2353
- return;
2354
- }
2355
- if (active.size >= descriptor.overlap.maxConcurrent) {
2356
- ticksSkipped += 1;
2357
- changed();
2358
- return;
2359
- }
2360
- startExecution(scheduledAt);
2361
- };
2362
- const arm = () => {
2363
- if (!accepting || nextRunAt === null)
2364
- return;
2365
- const delayMs = Math.max(0, nextRunAt - clock.now());
2366
- timer = clock.schedule(() => {
2367
- timer = null;
2368
- if (!accepting || nextRunAt === null)
2369
- return;
2370
- const scheduledAt = nextRunAt;
2371
- const observedAt = clock.now();
2372
- const elapsedIntervals = Math.max(1, Math.floor((observedAt - scheduledAt) / descriptor.everyMs) + 1);
2373
- if (elapsedIntervals > 1) {
2374
- ticksSkipped += elapsedIntervals - 1;
2375
- changed();
2376
- }
2377
- nextRunAt = scheduledAt + elapsedIntervals * descriptor.everyMs;
2378
- arm();
2379
- dispatchTick(scheduledAt);
2380
- }, delayMs);
2381
- };
2382
- const waitForActive = async (context, deadlineAt) => {
2383
- if (active.size === 0 || context.signal.aborted)
2384
- return;
2385
- await new Promise((resolve) => {
2386
- let settled = false;
2387
- let deadlineTimer = null;
2388
- const finish = () => {
2389
- if (settled)
2390
- return;
2391
- settled = true;
2392
- deadlineTimer?.cancel();
2393
- context.signal.removeEventListener("abort", finish);
2394
- resolve();
2395
- };
2396
- context.signal.addEventListener("abort", finish, { once: true });
2397
- if (deadlineAt !== undefined) {
2398
- deadlineTimer = clock.schedule(finish, Math.max(0, deadlineAt - context.now()));
2399
- }
2400
- Promise.allSettled([...active]).then(finish);
2401
- });
2402
- };
2403
- const drain = async (context) => {
2404
- stopAdmission();
2405
- await waitForActive(context, context.deadlineAt);
2406
- };
2407
- return {
2408
- id: descriptor.id,
2409
- ...dependsOn && { dependsOn },
2410
- ...config.required !== undefined && { required: config.required },
2411
- get status() {
2412
- return status();
2413
- },
2414
- start() {
2415
- if (active.size > 0) {
2416
- throw new Error(`[stitchkit] schedule "${descriptor.id}" still has executions from its previous generation`);
2417
- }
2418
- cancelFuture();
2419
- stopped = false;
2420
- accepting = false;
2421
- activated = false;
2422
- activationContext = null;
2423
- queuedAt = null;
2424
- changed();
2425
- },
2426
- activate(context) {
2427
- if (activated || stopped)
2428
- return;
2429
- activated = true;
2430
- accepting = true;
2431
- activationContext = context;
2432
- nextRunAt = clock.now() + descriptor.startAfterMs;
2433
- context.reportHealth("healthy");
2434
- changed();
2435
- arm();
2436
- },
2437
- stopAdmission() {
2438
- stopAdmission();
2439
- },
2440
- drain,
2441
- close() {
2442
- stopAdmission();
2443
- },
2444
- async force(context) {
2445
- stopAdmission();
2446
- await waitForActive(context, context.forceDeadlineAt);
2447
- if (active.size > 0) {
2448
- throw new Error(`[stitchkit] schedule "${descriptor.id}" still has active executions`);
2449
- }
2450
- }
2451
- };
2452
- }
2453
781
  // src/application/server-resource.ts
2454
782
  function managedServerResource(config) {
2455
783
  let shutdownPromise;
@@ -2532,17 +860,29 @@ function createWatchHub(config) {
2532
860
  const same = config.same ?? ((previous, next) => JSON.stringify(stableValue(previous)) === JSON.stringify(stableValue(next)));
2533
861
  let reads = 0;
2534
862
  let closed = false;
863
+ function tell(key, deliver) {
864
+ try {
865
+ deliver();
866
+ } catch (error) {
867
+ config.logger?.warn?.("[stitchkit] watch subscriber threw", {
868
+ key: watchKeyString(key),
869
+ error: error instanceof Error ? error.message : String(error)
870
+ });
871
+ }
872
+ }
2535
873
  function announceState(source, state) {
2536
874
  source.state = state;
2537
- for (const subscriber of source.subscribers)
2538
- subscriber.state(state);
875
+ for (const subscriber of source.subscribers) {
876
+ tell(source.key, () => subscriber.state(state));
877
+ }
2539
878
  }
2540
879
  function publish(source, value) {
2541
880
  source.revision += 1;
2542
881
  source.value = value;
2543
882
  const frame = { key: source.key, revision: source.revision, value };
2544
- for (const subscriber of source.subscribers)
2545
- subscriber.value(frame);
883
+ for (const subscriber of source.subscribers) {
884
+ tell(source.key, () => subscriber.value(frame));
885
+ }
2546
886
  }
2547
887
  async function pump(source) {
2548
888
  if (source.reading || closed)
@@ -2672,10 +1012,20 @@ function createWatchHub(config) {
2672
1012
  const source = acquire(operation, key, args);
2673
1013
  source.subscribers.add(subscriber);
2674
1014
  keys.add(id);
2675
- if (source.signature !== undefined) {
2676
- subscriber.value({ key, revision: source.revision, value: source.value });
1015
+ try {
1016
+ if (source.signature !== undefined) {
1017
+ subscriber.value({ key, revision: source.revision, value: source.value });
1018
+ }
1019
+ subscriber.state(source.state);
1020
+ } catch (error) {
1021
+ source.subscribers.delete(subscriber);
1022
+ keys.delete(id);
1023
+ release(source);
1024
+ return {
1025
+ accepted: false,
1026
+ reason: error instanceof Error ? error.message : String(error)
1027
+ };
2677
1028
  }
2678
- subscriber.state(source.state);
2679
1029
  pump(source);
2680
1030
  return { accepted: true };
2681
1031
  },
@@ -2706,14 +1056,20 @@ function createWatchHub(config) {
2706
1056
  size: () => sources.size,
2707
1057
  close() {
2708
1058
  closed = true;
2709
- for (const source of sources.values()) {
1059
+ const teardown = [...sources.values()];
1060
+ sources.clear();
1061
+ held.clear();
1062
+ for (const source of teardown) {
2710
1063
  clearTimeout(source.retry);
2711
1064
  clearTimeout(source.release);
1065
+ announceState(source, {
1066
+ key: source.key,
1067
+ phase: "unavailable",
1068
+ reason: "source-unavailable"
1069
+ });
2712
1070
  for (const unsubscribe of source.unsubscribes)
2713
1071
  unsubscribe();
2714
1072
  }
2715
- sources.clear();
2716
- held.clear();
2717
1073
  }
2718
1074
  };
2719
1075
  }