stitchkit 0.77.0 → 0.78.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 (65) hide show
  1. package/dist/agent-runtime-coding-tools.js +1 -1
  2. package/dist/agent-runtime-harness.js +6 -6
  3. package/dist/agent-runtime.js +11 -11
  4. package/dist/application/decisions.d.ts +28 -25
  5. package/dist/application/decisions.d.ts.map +1 -1
  6. package/dist/application/kernel.d.ts +2 -0
  7. package/dist/application/kernel.d.ts.map +1 -1
  8. package/dist/application/keyspace.d.ts +11 -1
  9. package/dist/application/keyspace.d.ts.map +1 -1
  10. package/dist/application/schedule.d.ts.map +1 -1
  11. package/dist/application/server-resource.d.ts.map +1 -1
  12. package/dist/application-diagnostic-journal.d.ts +21 -0
  13. package/dist/application-diagnostic-journal.d.ts.map +1 -0
  14. package/dist/application-diagnostic-journal.js +714 -0
  15. package/dist/application-opentelemetry.js +92 -5
  16. package/dist/application-schemas.js +13 -98
  17. package/dist/application.d.ts +1 -2
  18. package/dist/application.d.ts.map +1 -1
  19. package/dist/application.js +1775 -858
  20. package/dist/cli.js +7 -7
  21. package/dist/contract/index.js +9 -7
  22. package/dist/files/boundary.d.ts.map +1 -1
  23. package/dist/files.js +1 -1
  24. package/dist/{index-3z73fh2c.js → index-1ckavx4h.js} +49 -1
  25. package/dist/{index-s2rchahr.js → index-31k1ns1s.js} +4 -4
  26. package/dist/{index-m668wzyc.js → index-4ntp63ps.js} +4 -4
  27. package/dist/{index-1rxswfbv.js → index-56fm73re.js} +1 -1
  28. package/dist/{index-ezmn6ac6.js → index-5r0ak8fr.js} +3 -3
  29. package/dist/{index-f6n5n7nz.js → index-7z69kyrh.js} +10 -87
  30. package/dist/{index-k2zczx1g.js → index-93xp5tgg.js} +1 -1
  31. package/dist/{index-bfcpjw20.js → index-actkayzn.js} +7 -3
  32. package/dist/index-anyqnqcv.js +79 -0
  33. package/dist/{index-7qy2ex0m.js → index-cwp38wxn.js} +1 -1
  34. package/dist/{index-35z5h2ty.js → index-dmfn9m4x.js} +125 -4
  35. package/dist/index-h4pj6fta.js +87 -0
  36. package/dist/{index-jqtsc9mj.js → index-j6mfaw26.js} +2 -2
  37. package/dist/{index-58jzmnn4.js → index-k45qqrdh.js} +513 -39
  38. package/dist/{index-8eywc9zv.js → index-kw12fsbc.js} +0 -1
  39. package/dist/index-n1g2xm6a.js +203 -0
  40. package/dist/{index-t8qyvrvg.js → index-n7apa6sy.js} +12 -12
  41. package/dist/{index-s4c8wy8m.js → index-qaajjfp6.js} +8 -8
  42. package/dist/{index-nemjkxjp.js → index-s5tn4kdv.js} +14 -10
  43. package/dist/index-wfq8zerm.js +92 -0
  44. package/dist/index.js +53 -1215
  45. package/dist/internal/deadline.d.ts +17 -0
  46. package/dist/internal/deadline.d.ts.map +1 -0
  47. package/dist/live.js +12 -89
  48. package/dist/node.js +5 -6
  49. package/dist/observability/context.d.ts.map +1 -1
  50. package/dist/observability/index.js +3 -3
  51. package/dist/primitives.js +2 -1
  52. package/dist/remote.js +3 -6
  53. package/dist/server/event-bus.d.ts.map +1 -1
  54. package/dist/server/index.js +31 -30
  55. package/dist/testing.js +1562 -87
  56. package/dist/tool-invoker.js +6 -6
  57. package/dist/tools.js +66 -23
  58. package/llms-full.txt +110 -13
  59. package/package.json +8 -4
  60. package/dist/index-3cwck0rm.js +0 -897
  61. package/dist/index-7b188kmz.js +0 -49
  62. package/dist/index-a916km3r.js +0 -45
  63. package/dist/index-vc1b0b1b.js +0 -825
  64. package/dist/{index-da1aqnhb.js → index-aewy3x5j.js} +3 -3
  65. package/dist/{index-nt1mp8km.js → index-efm026pw.js} +3 -3
@@ -1,46 +1,26 @@
1
1
  import {
2
- ActivityIdSchema,
3
- ActivityLiveStateSchema,
4
- ActivitySnapshotSchema,
5
- ActivityStageIdSchema,
6
- ActivityStageSnapshotSchema,
7
- ActivityTokenBrand,
8
- ApplicationSnapshotSinkStatusSchema,
2
+ createBackoff
3
+ } from "./index-h4pj6fta.js";
4
+ import {
5
+ argumentsDigest,
6
+ stableValue,
7
+ watchKeyString
8
+ } from "./index-wfq8zerm.js";
9
+ import {
9
10
  BoundedChannelPolicySchema,
10
11
  BoundedChannelReaderError,
11
12
  BoundedChannelSnapshotSchema,
12
13
  BoundedChannelStateSchema,
13
14
  CreditWindowSnapshotSchema,
14
- ManagedScheduleDescriptorSchema,
15
- ManagedScheduleErrorPolicySchema,
16
- ManagedScheduleOverlapSchema,
17
- ManagedScheduleStatusSchema,
18
- createActivityProjection,
19
- createApplicationSnapshotSink,
20
15
  createBoundedChannel,
21
- createCreditWindow,
22
- createManagedSchedule
23
- } from "./index-3cwck0rm.js";
16
+ createCreditWindow
17
+ } from "./index-dmfn9m4x.js";
24
18
  import {
25
- createBoundedSinkManager
26
- } from "./index-a59da114.js";
19
+ AppError
20
+ } from "./index-0w9abg87.js";
27
21
  import {
28
- argumentsDigest,
29
- stableValue
30
- } from "./index-a916km3r.js";
31
- import {
32
- ApplicationAdmissionError,
33
- ApplicationRestartInputSchema,
34
- ApplicationRestartOutcomeSchema,
35
- ApplicationRestartResultSchema,
36
- ApplicationShutdownBudgetSchema,
37
- ApplicationShutdownOptionsSchema,
38
- createApplication
39
- } from "./index-vc1b0b1b.js";
40
- import {
41
- defineManagedResource,
42
- managedResourceDependencyId
43
- } from "./index-2k4yrqkc.js";
22
+ isRecord
23
+ } from "./index-ksp6e2ye.js";
44
24
  import {
45
25
  ApplicationAdmissionSnapshotSchema,
46
26
  ApplicationHealthSchema,
@@ -53,46 +33,315 @@ import {
53
33
  ManagedResourceSnapshotSchema,
54
34
  ManagedResourceStateSchema,
55
35
  projectApplicationStatus
56
- } from "./index-8eywc9zv.js";
57
- import"./index-3z73fh2c.js";
58
- import"./index-0w9abg87.js";
59
- import {
60
- isRecord
61
- } from "./index-qyrqwr4c.js";
62
- // src/application/admission.ts
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
63
42
  import { z } from "zod";
64
- var PositiveSafeIntegerSchema = z.number().int().positive().safe();
65
- var BoundedRateBudgetSchema = z.object({
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
+ }
311
+ // src/application/admission.ts
312
+ import { z as z3 } from "zod";
313
+ var PositiveSafeIntegerSchema = z3.number().int().positive().safe();
314
+ var BoundedRateBudgetSchema = z3.object({
66
315
  limit: PositiveSafeIntegerSchema,
67
316
  intervalMs: PositiveSafeIntegerSchema
68
317
  }).strict().readonly();
69
- var BoundedAdmissionPerKeyLimitsSchema = z.object({
318
+ var BoundedAdmissionPerKeyLimitsSchema = z3.object({
70
319
  maxConcurrent: PositiveSafeIntegerSchema,
71
320
  rate: BoundedRateBudgetSchema.optional()
72
321
  }).strict().readonly();
73
- var PerKeyLimitResolverSchema = z.custom((value) => typeof value === "function", { message: "perKey.limits must be a function of the key" });
74
- var BoundedAdmissionPolicySchema = z.object({
75
- global: z.object({
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({
76
325
  maxConcurrent: PositiveSafeIntegerSchema,
77
326
  rate: BoundedRateBudgetSchema.optional()
78
327
  }).strict(),
79
- perKey: z.union([
80
- z.object({
328
+ perKey: z3.union([
329
+ z3.object({
81
330
  maxConcurrent: PositiveSafeIntegerSchema,
82
331
  maxKeys: PositiveSafeIntegerSchema,
83
332
  rate: BoundedRateBudgetSchema.optional(),
84
- limits: z.never().optional()
333
+ limits: z3.never().optional()
85
334
  }).strict(),
86
- z.object({
335
+ z3.object({
87
336
  maxKeys: PositiveSafeIntegerSchema,
88
337
  limits: PerKeyLimitResolverSchema,
89
- maxConcurrent: z.never().optional(),
90
- rate: z.never().optional()
338
+ maxConcurrent: z3.never().optional(),
339
+ rate: z3.never().optional()
91
340
  }).strict()
92
341
  ]).optional()
93
342
  }).strict().readonly();
94
- var BoundedAdmissionStateSchema = z.enum(["accepting", "draining", "closed"]);
95
- var BoundedAdmissionRefusalReasonSchema = z.enum([
343
+ var BoundedAdmissionStateSchema = z3.enum(["accepting", "draining", "closed"]);
344
+ var BoundedAdmissionRefusalReasonSchema = z3.enum([
96
345
  "not-accepting",
97
346
  "global-concurrency",
98
347
  "key-required",
@@ -102,25 +351,25 @@ var BoundedAdmissionRefusalReasonSchema = z.enum([
102
351
  "key-rate",
103
352
  "upstream"
104
353
  ]);
105
- var RefusalCountersSchema = z.object({
106
- "not-accepting": z.number().int().nonnegative(),
107
- "global-concurrency": z.number().int().nonnegative(),
108
- "key-required": z.number().int().nonnegative(),
109
- "key-capacity": z.number().int().nonnegative(),
110
- "key-concurrency": z.number().int().nonnegative(),
111
- "global-rate": z.number().int().nonnegative(),
112
- "key-rate": z.number().int().nonnegative(),
113
- upstream: z.number().int().nonnegative()
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()
114
363
  }).strict().readonly();
115
- var BoundedAdmissionSnapshotSchema = z.object({
364
+ var BoundedAdmissionSnapshotSchema = z3.object({
116
365
  state: BoundedAdmissionStateSchema,
117
- active: z.number().int().nonnegative(),
118
- accepted: z.number().int().nonnegative(),
119
- released: z.number().int().nonnegative(),
120
- refused: z.number().int().nonnegative(),
121
- keys: z.number().int().nonnegative(),
122
- globalRateSamples: z.number().int().nonnegative(),
123
- keyRateSamples: z.number().int().nonnegative(),
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(),
124
373
  refusals: RefusalCountersSchema
125
374
  }).strict().readonly();
126
375
 
@@ -433,12 +682,31 @@ function createBoundedAdmission(config) {
433
682
  getSnapshot
434
683
  };
435
684
  }
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
+
436
704
  // src/internal/decision.ts
437
- import { z as z2 } from "zod";
438
- var PolicyDecisionSchema = z2.discriminatedUnion("outcome", [
439
- z2.object({ outcome: z2.literal("allow") }).strict(),
440
- z2.object({ outcome: z2.literal("deny"), reason: z2.string().min(1) }).strict(),
441
- z2.object({ outcome: z2.literal("defer") }).strict()
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()
442
710
  ]);
443
711
 
444
712
  // src/application/decisions.ts
@@ -454,13 +722,19 @@ class DecisionUndecidedError extends Error {
454
722
 
455
723
  class DecisionPolicyError extends Error {
456
724
  policyId;
457
- constructor(policyId, detail) {
458
- super(`Policy "${policyId}" did not return a decision (${detail}). A policy returns { outcome: "allow" | "deny" | "defer" }, and a deny carries a reason.`);
725
+ trace;
726
+ constructor(policyId, detail, trace = []) {
727
+ super(`Policy "${policyId}" did not answer (${detail}). A policy returns { outcome: "allow" | "deny" | "defer" } within its deadline, and a deny carries a reason.`);
459
728
  this.name = "DecisionPolicyError";
460
729
  this.policyId = policyId;
730
+ this.trace = trace;
461
731
  }
462
732
  }
463
- function createDecisionPipeline(policies) {
733
+ function createDecisionPipeline(policies, config) {
734
+ const { policyTimeoutMs } = config;
735
+ if (!Number.isInteger(policyTimeoutMs) || policyTimeoutMs <= 0) {
736
+ throw new Error("[stitchkit] decision pipeline: policyTimeoutMs must be a positive integer number of milliseconds.");
737
+ }
464
738
  const seen = new Set;
465
739
  for (const policy of policies) {
466
740
  if (policy.id.trim() === "") {
@@ -476,10 +750,21 @@ function createDecisionPipeline(policies) {
476
750
  async decide(input) {
477
751
  const trace = [];
478
752
  for (const policy of policies) {
479
- const answer = await policy.decide(input);
753
+ let answer;
754
+ try {
755
+ const outcome = await withDeadline(policy.decide(input), policyTimeoutMs);
756
+ if (!outcome.settled) {
757
+ throw new DecisionPolicyError(policy.id, `did not answer within ${policyTimeoutMs}ms`, trace);
758
+ }
759
+ answer = outcome.value;
760
+ } catch (error) {
761
+ if (error instanceof DecisionPolicyError)
762
+ throw error;
763
+ throw new DecisionPolicyError(policy.id, error instanceof Error ? `threw: ${error.message}` : "threw", trace);
764
+ }
480
765
  const parsed = PolicyDecisionSchema.safeParse(answer);
481
766
  if (!parsed.success) {
482
- throw new DecisionPolicyError(policy.id, parsed.error.issues[0]?.message ?? "invalid");
767
+ throw new DecisionPolicyError(policy.id, parsed.error.issues[0]?.message ?? "invalid", trace);
483
768
  }
484
769
  const decision = parsed.data;
485
770
  trace.push(decision.outcome === "deny" ? { id: policy.id, outcome: "deny", reason: decision.reason } : { id: policy.id, outcome: decision.outcome });
@@ -494,23 +779,20 @@ function createDecisionPipeline(policies) {
494
779
  }
495
780
  };
496
781
  }
497
- // src/application/diagnostic-journal.ts
498
- import { randomUUID } from "node:crypto";
499
-
500
782
  // src/application/diagnostic-journal-contract.ts
501
- import { z as z3 } from "zod";
502
- var PositiveSafeIntegerSchema2 = z3.number().int().positive().safe();
503
- var FileModeSchema = z3.number().int().min(0).max(511);
504
- var DiagnosticJournalLimitsSchema = z3.object({
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({
505
787
  maxEventBytes: PositiveSafeIntegerSchema2,
506
788
  maxPendingItems: PositiveSafeIntegerSchema2,
507
789
  maxPendingBytes: PositiveSafeIntegerSchema2,
508
790
  maxFileBytes: PositiveSafeIntegerSchema2,
509
791
  maxFiles: PositiveSafeIntegerSchema2
510
792
  }).strict().readonly();
511
- var DiagnosticJournalLockPolicySchema = z3.enum(["refuse", "reclaim-stale"]);
512
- var DiagnosticJournalStateSchema = z3.enum(["open", "draining", "closed", "failed"]);
513
- var DiagnosticJournalRefusalReasonSchema = z3.enum([
793
+ var DiagnosticJournalLockPolicySchema = z5.enum(["refuse", "reclaim-stale"]);
794
+ var DiagnosticJournalStateSchema = z5.enum(["open", "draining", "closed", "failed"]);
795
+ var DiagnosticJournalRefusalReasonSchema = z5.enum([
514
796
  "closed",
515
797
  "failed",
516
798
  "invalid",
@@ -518,704 +800,228 @@ var DiagnosticJournalRefusalReasonSchema = z3.enum([
518
800
  "item-capacity",
519
801
  "byte-capacity"
520
802
  ]);
521
- var DiagnosticJournalRefusalCountersSchema = z3.object({
522
- closed: z3.number().int().nonnegative(),
523
- failed: z3.number().int().nonnegative(),
524
- invalid: z3.number().int().nonnegative(),
525
- oversized: z3.number().int().nonnegative(),
526
- "item-capacity": z3.number().int().nonnegative(),
527
- "byte-capacity": z3.number().int().nonnegative()
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()
528
810
  }).strict().readonly();
529
- var DiagnosticJournalFailurePhaseSchema = z3.enum(["write", "rotation", "close"]);
530
- var DiagnosticJournalStatusSchema = z3.object({
811
+ var DiagnosticJournalFailurePhaseSchema = z5.enum(["write", "rotation", "close"]);
812
+ var DiagnosticJournalStatusSchema = z5.object({
531
813
  state: DiagnosticJournalStateSchema,
532
- epoch: z3.uuid(),
814
+ epoch: z5.uuid(),
533
815
  limits: DiagnosticJournalLimitsSchema,
534
- lock: z3.object({
816
+ lock: z5.object({
535
817
  policy: DiagnosticJournalLockPolicySchema,
536
- reclaimedStale: z3.boolean()
818
+ reclaimedStale: z5.boolean()
537
819
  }).strict().readonly(),
538
- received: z3.number().int().nonnegative(),
539
- accepted: z3.number().int().nonnegative(),
540
- refused: z3.number().int().nonnegative(),
820
+ received: z5.number().int().nonnegative(),
821
+ accepted: z5.number().int().nonnegative(),
822
+ refused: z5.number().int().nonnegative(),
541
823
  refusals: DiagnosticJournalRefusalCountersSchema,
542
- written: z3.number().int().nonnegative(),
543
- failedRecords: z3.number().int().nonnegative(),
544
- pendingItems: z3.number().int().nonnegative(),
545
- pendingBytes: z3.number().int().nonnegative(),
546
- inFlight: z3.boolean(),
547
- rotations: z3.number().int().nonnegative(),
548
- rotationFailures: z3.number().int().nonnegative(),
549
- partialTails: z3.number().int().nonnegative(),
550
- currentFileBytes: z3.number().int().nonnegative(),
551
- retainedFiles: z3.number().int().nonnegative(),
552
- lastAcceptedSequence: z3.number().int().positive().optional(),
553
- lastWrittenSequence: z3.number().int().positive().optional(),
554
- lastSettledSequence: z3.number().int().positive().optional(),
555
- lastFailure: z3.object({
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({
556
838
  phase: DiagnosticJournalFailurePhaseSchema,
557
- sequence: z3.number().int().positive().optional()
839
+ sequence: z5.number().int().positive().optional()
558
840
  }).strict().readonly().optional()
559
841
  }).strict().readonly();
560
- var DiagnosticJournalSubmitResultSchema = z3.discriminatedUnion("outcome", [
561
- z3.object({
562
- outcome: z3.literal("accepted"),
563
- epoch: z3.uuid(),
564
- sequence: z3.number().int().positive()
842
+ var DiagnosticJournalSubmitResultSchema = z5.discriminatedUnion("outcome", [
843
+ z5.object({
844
+ outcome: z5.literal("accepted"),
845
+ epoch: z5.uuid(),
846
+ sequence: z5.number().int().positive()
565
847
  }).strict().readonly(),
566
- z3.object({
567
- outcome: z3.literal("refused"),
848
+ z5.object({
849
+ outcome: z5.literal("refused"),
568
850
  reason: DiagnosticJournalRefusalReasonSchema
569
851
  }).strict().readonly()
570
852
  ]);
571
- var DiagnosticJournalWaitResultSchema = z3.object({
572
- outcome: z3.enum(["settled", "timed-out", "cancelled"]),
853
+ var DiagnosticJournalWaitResultSchema = z5.object({
854
+ outcome: z5.enum(["settled", "timed-out", "cancelled"]),
573
855
  state: DiagnosticJournalStateSchema,
574
- throughSequence: z3.number().int().nonnegative(),
575
- settledSequence: z3.number().int().nonnegative()
856
+ throughSequence: z5.number().int().nonnegative(),
857
+ settledSequence: z5.number().int().nonnegative()
576
858
  }).strict().readonly();
577
- var DiagnosticJournalCloseResultSchema = z3.object({
578
- outcome: z3.enum(["closed", "timed-out", "cancelled"]),
859
+ var DiagnosticJournalCloseResultSchema = z5.object({
860
+ outcome: z5.enum(["closed", "timed-out", "cancelled"]),
579
861
  state: DiagnosticJournalStateSchema,
580
- pendingItems: z3.number().int().nonnegative()
862
+ pendingItems: z5.number().int().nonnegative()
581
863
  }).strict().readonly();
582
- var DiagnosticJournalFrameSchema = z3.object({
583
- schemaVersion: z3.literal(1),
584
- epoch: z3.uuid(),
585
- sequence: z3.number().int().positive(),
586
- event: z3.json()
864
+ var DiagnosticJournalFrameSchema = z5.object({
865
+ schemaVersion: z5.literal(1),
866
+ epoch: z5.uuid(),
867
+ sequence: z5.number().int().positive(),
868
+ event: z5.json()
587
869
  }).strict().readonly();
588
- function parseDiagnosticJournalMode(mode) {
589
- return FileModeSchema.parse(mode ?? 384);
590
- }
591
870
  function readDiagnosticJournalLockDiagnosis(error) {
592
871
  if (!isRecord(error))
593
872
  return;
594
873
  const diagnosis = error.journalLock;
595
874
  return isRecord(diagnosis) ? diagnosis : undefined;
596
875
  }
876
+ // src/application/events.ts
877
+ import { z as z7 } from "zod";
597
878
 
598
- // src/application/diagnostic-journal-manager.ts
599
- import { z as z4 } from "zod";
600
-
601
- // src/application/diagnostic-journal-storage.ts
602
- import { constants as constants2 } from "node:fs";
603
- import { lstat, open as open2, readdir, realpath, rename, unlink as unlink2 } from "node:fs/promises";
604
- import { basename, dirname, isAbsolute, resolve } from "node:path";
605
-
606
- // src/application/diagnostic-journal-lock.ts
607
- import { execFile } from "node:child_process";
608
- import { constants } from "node:fs";
609
- import { open, readFile, unlink } from "node:fs/promises";
610
- import { hostname, platform } from "node:os";
611
- import { promisify } from "node:util";
612
- function isExisting(error) {
613
- return isRecord(error) && error.code === "EEXIST";
614
- }
615
- function readOwner(text) {
616
- let parsed;
617
- try {
618
- parsed = JSON.parse(text);
619
- } catch {
620
- return;
621
- }
622
- if (!isRecord(parsed))
623
- return;
624
- const { pid, host, acquiredAt, machine } = parsed;
625
- if (typeof pid !== "number" || !Number.isSafeInteger(pid) || pid <= 0)
626
- return;
627
- if (typeof host !== "string" || host.length === 0)
628
- return;
629
- if (typeof acquiredAt !== "string")
630
- return;
631
- const identified = typeof machine === "string" && machine.length > 0 ? { machine } : {};
632
- return { pid, host, acquiredAt, ...identified };
633
- }
634
- var detectedIdentity;
635
- async function firstReadableLine(paths) {
636
- for (const path of paths) {
637
- try {
638
- const text = (await readFile(path, "utf8")).trim();
639
- if (text.length > 0)
640
- return text;
641
- } catch {}
642
- }
643
- return null;
644
- }
645
- var run = promisify(execFile);
646
- function parsePlatformUuid(dump) {
647
- return /"IOPlatformUUID"\s*=\s*"([^"]+)"/.exec(dump)?.[1] ?? null;
648
- }
649
- async function readDarwinPlatformUuid(command = "ioreg") {
650
- try {
651
- const { stdout } = await run(command, ["-rd1", "-c", "IOPlatformExpertDevice"], {
652
- timeout: 2000,
653
- maxBuffer: 1 << 20,
654
- encoding: "utf8"
655
- });
656
- return parsePlatformUuid(stdout);
657
- } catch {
658
- return null;
659
- }
660
- }
661
- async function machineIdentity(declared) {
662
- if (declared !== undefined)
663
- return declared;
664
- if (detectedIdentity !== undefined)
665
- return detectedIdentity;
666
- detectedIdentity = platform() === "darwin" ? await readDarwinPlatformUuid() : await firstReadableLine(["/etc/machine-id", "/var/lib/dbus/machine-id"]);
667
- return detectedIdentity;
668
- }
669
- function attribute(owner, identity) {
670
- if (identity !== null && owner.machine !== undefined) {
671
- return owner.machine === identity ? "this-machine" : "another-machine";
672
- }
673
- return owner.host === hostname() ? "this-machine" : "unattributable";
674
- }
675
- async function probeLiveness(owner) {
676
- try {
677
- process.kill(owner.pid, 0);
678
- } catch (error) {
679
- return isRecord(error) && error.code === "ESRCH" ? "gone" : "alive";
680
- }
681
- return await isZombieProcess(owner.pid) ? "gone" : "alive";
682
- }
683
- async function isZombieProcess(pid, seam = {}) {
684
- const { procRoot = "/proc", psCommand = "ps" } = seam;
685
- try {
686
- const stat = await readFile(`${procRoot}/${pid}/stat`, "utf8");
687
- return stat.slice(stat.lastIndexOf(")") + 1).trimStart().startsWith("Z");
688
- } catch {}
689
- try {
690
- const { stdout } = await run(psCommand, ["-o", "state=", "-p", String(pid)], {
691
- timeout: 2000,
692
- maxBuffer: 1 << 16,
693
- encoding: "utf8"
694
- });
695
- return stdout.trim().startsWith("Z");
696
- } catch {
697
- return false;
698
- }
699
- }
700
- async function diagnose(owner, declaredIdentity) {
701
- if (!owner)
702
- return { attribution: "unattributable", liveness: "not-probed", owner: null };
703
- const attribution = attribute(owner, await machineIdentity(declaredIdentity));
704
- const record = {
705
- pid: owner.pid,
706
- host: owner.host,
707
- acquiredAt: owner.acquiredAt,
708
- ...owner.machine !== undefined && { machine: owner.machine }
709
- };
710
- if (attribution !== "this-machine")
711
- return { attribution, liveness: "not-probed", owner: record };
712
- return { attribution, liveness: await probeLiveness(owner), owner: record };
713
- }
714
- async function readLockOwner(lockPath) {
715
- let handle;
716
- try {
717
- handle = await open(lockPath, constants.O_RDONLY | constants.O_NOFOLLOW);
718
- return readOwner(await handle.readFile("utf8"));
719
- } catch {
720
- return;
721
- } finally {
722
- await handle?.close().catch(() => {
723
- return;
724
- });
725
- }
726
- }
727
- async function createExclusive(lockPath, mode, declaredIdentity) {
728
- const handle = await open(lockPath, "wx", mode);
729
- const identity = await machineIdentity(declaredIdentity);
730
- const owner = {
731
- pid: process.pid,
732
- host: hostname(),
733
- acquiredAt: new Date().toISOString(),
734
- ...identity !== null && { machine: identity }
735
- };
736
- await handle.writeFile(`${JSON.stringify(owner)}
737
- `, "utf8");
738
- return handle;
739
- }
740
- async function acquireDiagnosticJournalLock(lockPath, mode, policy, declaredIdentity) {
741
- try {
742
- return {
743
- handle: await createExclusive(lockPath, mode, declaredIdentity),
744
- reclaimedStale: false
745
- };
746
- } catch (error) {
747
- if (policy !== "reclaim-stale" || !isExisting(error))
748
- throw error;
749
- const owner = await readLockOwner(lockPath);
750
- const diagnosis = await diagnose(owner, declaredIdentity);
751
- if (!owner || diagnosis.liveness !== "gone") {
752
- if (isRecord(error))
753
- Object.assign(error, { journalLock: diagnosis });
754
- throw error;
755
- }
756
- await unlink(lockPath).catch(() => {
757
- return;
758
- });
759
- return {
760
- handle: await createExclusive(lockPath, mode, declaredIdentity),
761
- reclaimedStale: true
762
- };
763
- }
764
- }
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();
765
904
 
766
- // src/application/diagnostic-journal-storage.ts
767
- class DiagnosticJournalStorageError extends Error {
768
- phase;
769
- constructor(phase, message, options) {
770
- super(message, options);
771
- this.phase = phase;
772
- this.name = "DiagnosticJournalStorageError";
773
- }
774
- }
775
- function isMissing(error) {
776
- return isRecord(error) && error.code === "ENOENT";
777
- }
778
- async function removeIfPresent(path) {
779
- try {
780
- const info = await lstat(path);
781
- if (info.isSymbolicLink() || !info.isFile()) {
782
- throw new Error("Diagnostic journal generations must be regular files");
783
- }
784
- await unlink2(path);
785
- } catch (error) {
786
- if (!isMissing(error))
787
- throw error;
788
- }
789
- }
790
- async function moveIfPresent(from, to) {
791
- try {
792
- const info = await lstat(from);
793
- if (info.isSymbolicLink() || !info.isFile()) {
794
- throw new Error("Diagnostic journal generations must be regular files");
795
- }
796
- await rename(from, to);
797
- return true;
798
- } catch (error) {
799
- if (isMissing(error))
800
- return false;
801
- throw error;
802
- }
803
- }
804
- function generation(path, index) {
805
- return `${path}.${index}`;
806
- }
807
- function generationIndex(name, prefix) {
808
- if (!name.startsWith(prefix))
905
+ // src/internal/observability-sink.ts
906
+ var DEFAULT_MAX_PENDING = 1000;
907
+ function invokeIsolated(callback) {
908
+ if (!callback)
809
909
  return;
810
- const suffix = name.slice(prefix.length);
811
- if (!/^\d+$/.test(suffix))
812
- return;
813
- const value = Number(suffix);
814
- return Number.isSafeInteger(value) && value > 0 ? value : undefined;
910
+ Promise.resolve().then(callback).catch(() => {});
815
911
  }
816
- async function createRotatingDiagnosticJournalStorage(config) {
817
- if (!isAbsolute(config.path) || resolve(config.path) !== config.path) {
818
- throw new TypeError("Diagnostic journal path must be normalized and absolute");
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");
819
916
  }
820
- const parent = await realpath(dirname(config.path));
821
- const journalPath = resolve(parent, basename(config.path));
822
- const lockPath = `${journalPath}.lock`;
823
- const { handle: lock, reclaimedStale } = await acquireDiagnosticJournalLock(lockPath, config.mode, config.lock, config.machineIdentity);
824
- let handle;
825
- let currentFileBytes = 0;
826
- let retainedFiles = 1;
827
- let rotations = 0;
828
- let partialTails = 0;
917
+ let sequence = 0;
829
918
  let closed = false;
830
- const openCurrent = async () => {
831
- handle = await open2(journalPath, constants2.O_APPEND | constants2.O_CREAT | constants2.O_RDWR | constants2.O_NOFOLLOW, config.mode);
832
- const info = await handle.stat();
833
- if (!info.isFile()) {
834
- await handle.close();
835
- handle = undefined;
836
- throw new Error("Diagnostic journal path must be a regular file");
837
- }
838
- currentFileBytes = Number(info.size);
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);
839
931
  };
840
- const rotate = async () => {
932
+ const reportDrop = (reason, event) => {
933
+ invokeIsolated(config.onDrop ? () => config.onDrop?.({ reason, event, pending: writes.size }) : undefined);
934
+ };
935
+ const admit = (id, event) => {
841
936
  try {
842
- await handle?.close();
843
- handle = undefined;
844
- if (config.maxFiles === 1) {
845
- await removeIfPresent(journalPath);
846
- } else {
847
- await removeIfPresent(generation(journalPath, config.maxFiles - 1));
848
- for (let index = config.maxFiles - 2;index >= 1; index -= 1) {
849
- await moveIfPresent(generation(journalPath, index), generation(journalPath, index + 1));
850
- }
851
- await moveIfPresent(journalPath, generation(journalPath, 1));
937
+ if (config.filter && !config.filter(event)) {
938
+ filtered += 1;
939
+ return;
852
940
  }
853
- retainedFiles = Math.min(config.maxFiles, retainedFiles + 1);
854
- rotations += 1;
855
- await openCurrent();
856
941
  } catch (error) {
857
- throw new DiagnosticJournalStorageError("rotation", "Diagnostic journal rotation failed", { cause: error });
858
- }
859
- };
860
- try {
861
- const names = await readdir(parent);
862
- const prefix = `${basename(journalPath)}.`;
863
- let existingGenerations = 0;
864
- for (const name of names) {
865
- const index = generationIndex(name, prefix);
866
- if (index === undefined)
867
- continue;
868
- if (index >= config.maxFiles) {
869
- await removeIfPresent(resolve(parent, name));
870
- } else {
871
- const info = await lstat(resolve(parent, name));
872
- if (info.isSymbolicLink() || !info.isFile()) {
873
- throw new Error("Diagnostic journal generations must be regular files");
874
- }
875
- existingGenerations += 1;
876
- }
877
- }
878
- retainedFiles = Math.min(config.maxFiles, existingGenerations + 1);
879
- await openCurrent();
880
- if (currentFileBytes > 0) {
881
- const tail = new Uint8Array(1);
882
- const read = await handle?.read(tail, 0, 1, currentFileBytes - 1);
883
- if (read?.bytesRead === 1 && tail[0] !== 10) {
884
- partialTails += 1;
885
- await rotate();
886
- }
887
- }
888
- } catch (error) {
889
- await handle?.close().catch(() => {
890
- return;
891
- });
892
- await lock.close().catch(() => {
942
+ preparationFailed += 1;
943
+ reportError(error, event);
893
944
  return;
894
- });
895
- await unlink2(lockPath).catch(() => {
945
+ }
946
+ if (writes.size >= maxPending) {
947
+ dropped += 1;
948
+ reportDrop("capacity", event);
896
949
  return;
897
- });
898
- throw error;
899
- }
900
- const snapshot = () => ({
901
- currentFileBytes,
902
- retainedFiles,
903
- rotations,
904
- partialTails
905
- });
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
+ };
906
965
  return {
907
- reclaimedStale,
908
- async append(bytes) {
909
- if (closed || !handle) {
910
- throw new DiagnosticJournalStorageError("write", "Diagnostic journal storage is closed");
911
- }
912
- let rotated = false;
913
- if (currentFileBytes > 0 && currentFileBytes + bytes.byteLength > config.maxFileBytes) {
914
- await rotate();
915
- rotated = true;
916
- }
917
- try {
918
- await handle.writeFile(bytes);
919
- currentFileBytes += bytes.byteLength;
920
- return { ...snapshot(), rotated };
921
- } catch (error) {
922
- throw new DiagnosticJournalStorageError("write", "Diagnostic journal write failed", {
923
- cause: error
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);
924
976
  });
977
+ return;
925
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);
926
984
  },
927
- snapshot,
928
- async close() {
929
- if (closed)
930
- return;
985
+ flush() {
986
+ return awaitGeneration(sequence);
987
+ },
988
+ getStatus,
989
+ close() {
990
+ if (closePromise)
991
+ return closePromise;
931
992
  closed = true;
932
- let failure;
933
- try {
934
- await handle?.close();
935
- } catch (error) {
936
- failure = error;
937
- }
938
- try {
939
- await lock.close();
940
- await unlink2(lockPath);
941
- } catch (error) {
942
- failure ??= error;
943
- }
944
- if (failure !== undefined) {
945
- throw new DiagnosticJournalStorageError("close", "Diagnostic journal close failed", {
946
- cause: failure
947
- });
948
- }
993
+ closePromise = awaitGeneration(sequence).then(getStatus);
994
+ return closePromise;
949
995
  }
950
996
  };
951
- }
952
-
953
- // src/application/diagnostic-journal-manager.ts
954
- function waitBudget(options) {
955
- if (options.timeoutMs === undefined)
956
- return;
957
- if (!Number.isSafeInteger(options.timeoutMs) || options.timeoutMs <= 0) {
958
- throw new TypeError("Diagnostic journal timeoutMs must be a positive safe integer");
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
+ });
959
1011
  }
960
- return options.timeoutMs;
961
- }
962
- function createDiagnosticJournalManager(config, storage) {
963
- const limits = DiagnosticJournalLimitsSchema.parse(config.limits);
964
- const encoder = new TextEncoder;
965
- const channel = createBoundedChannel({
966
- policy: "ordered",
967
- maxItems: limits.maxPendingItems,
968
- maxBytes: limits.maxPendingBytes,
969
- sizeOf: (frame) => frame.bytes.byteLength
970
- });
971
- const refusals = {
972
- closed: 0,
973
- failed: 0,
974
- invalid: 0,
975
- oversized: 0,
976
- "item-capacity": 0,
977
- "byte-capacity": 0
978
- };
979
- const progressWaiters = new Set;
980
- let state = "open";
981
- let received = 0;
982
- let accepted = 0;
983
- let refused = 0;
984
- let written = 0;
985
- let failedRecords = 0;
986
- let pendingItems = 0;
987
- let pendingBytes = 0;
988
- let inFlight = false;
989
- let rotationFailures = 0;
990
- let lastAcceptedSequence = 0;
991
- let lastWrittenSequence = 0;
992
- let lastSettledSequence = 0;
993
- let lastFailure;
994
- let physicalClosed = false;
995
- const isFailed = () => state === "failed";
996
- const notifyProgress = () => {
997
- for (const waiter of progressWaiters)
998
- waiter();
999
- };
1000
- const reportFailure = (failure) => {
1001
- if (!config.onFailure)
1002
- return;
1003
- Promise.resolve().then(() => config.onFailure?.(failure)).catch(() => {
1004
- return;
1005
- });
1006
- };
1007
- const markFailure = (error, fallbackPhase, sequence) => {
1008
- const phase = error instanceof DiagnosticJournalStorageError ? error.phase : fallbackPhase;
1009
- if (phase === "rotation")
1010
- rotationFailures += 1;
1011
- lastFailure = { phase, ...sequence !== undefined && { sequence } };
1012
- state = "failed";
1013
- channel.close({ mode: "drain" });
1014
- reportFailure({ phase, error, ...sequence !== undefined && { sequence } });
1015
- };
1016
- const worker = (async () => {
1017
- for await (const frame of channel) {
1018
- inFlight = true;
1019
- if (isFailed()) {
1020
- failedRecords += 1;
1021
- } else {
1022
- try {
1023
- await storage.append(frame.bytes);
1024
- written += 1;
1025
- lastWrittenSequence = frame.sequence;
1026
- } catch (error) {
1027
- failedRecords += 1;
1028
- markFailure(error, "write", frame.sequence);
1029
- }
1030
- }
1031
- pendingItems -= 1;
1032
- pendingBytes -= frame.bytes.byteLength;
1033
- lastSettledSequence = frame.sequence;
1034
- inFlight = false;
1035
- notifyProgress();
1036
- }
1037
- try {
1038
- await storage.close();
1039
- } catch (error) {
1040
- markFailure(error, "close");
1041
- }
1042
- physicalClosed = true;
1043
- if (!isFailed())
1044
- state = "closed";
1045
- notifyProgress();
1046
- })();
1047
- worker.catch((error) => {
1048
- markFailure(error, "close");
1049
- physicalClosed = true;
1050
- notifyProgress();
1051
- });
1052
- const refuse = (reason) => {
1053
- refused += 1;
1054
- refusals[reason] += 1;
1055
- return DiagnosticJournalSubmitResultSchema.parse({ outcome: "refused", reason });
1056
- };
1057
- const waitUntil = (predicate, options) => {
1058
- const timeoutMs = waitBudget(options);
1059
- if (predicate())
1060
- return Promise.resolve("settled");
1061
- if (options.signal?.aborted)
1062
- return Promise.resolve("cancelled");
1063
- return new Promise((resolve2) => {
1064
- let settled = false;
1065
- let timer;
1066
- const finish = (outcome) => {
1067
- if (settled)
1068
- return;
1069
- settled = true;
1070
- progressWaiters.delete(onProgress);
1071
- options.signal?.removeEventListener("abort", onAbort);
1072
- if (timer)
1073
- clearTimeout(timer);
1074
- resolve2(outcome);
1075
- };
1076
- const onProgress = () => {
1077
- if (predicate())
1078
- finish("settled");
1079
- };
1080
- const onAbort = () => finish("cancelled");
1081
- progressWaiters.add(onProgress);
1082
- options.signal?.addEventListener("abort", onAbort, { once: true });
1083
- if (timeoutMs !== undefined)
1084
- timer = setTimeout(() => finish("timed-out"), timeoutMs);
1085
- onProgress();
1086
- });
1087
- };
1088
- const getStatus = () => {
1089
- const file = storage.snapshot();
1090
- return DiagnosticJournalStatusSchema.parse({
1091
- state,
1092
- epoch: config.epoch,
1093
- limits,
1094
- lock: config.lock,
1095
- received,
1096
- accepted,
1097
- refused,
1098
- refusals,
1099
- written,
1100
- failedRecords,
1101
- pendingItems,
1102
- pendingBytes,
1103
- inFlight,
1104
- rotations: file.rotations,
1105
- rotationFailures,
1106
- partialTails: file.partialTails,
1107
- currentFileBytes: file.currentFileBytes,
1108
- retainedFiles: file.retainedFiles,
1109
- ...lastAcceptedSequence > 0 && { lastAcceptedSequence },
1110
- ...lastWrittenSequence > 0 && { lastWrittenSequence },
1111
- ...lastSettledSequence > 0 && { lastSettledSequence },
1112
- ...lastFailure && { lastFailure }
1113
- });
1114
- };
1115
- return {
1116
- submit(event) {
1117
- received += 1;
1118
- if (state === "failed")
1119
- return refuse("failed");
1120
- if (state !== "open")
1121
- return refuse("closed");
1122
- const parsed = config.eventSchema.safeParse(event);
1123
- if (!parsed.success)
1124
- return refuse("invalid");
1125
- const json = z4.json().safeParse(parsed.data);
1126
- if (!json.success)
1127
- return refuse("invalid");
1128
- const payload = encoder.encode(JSON.stringify(json.data));
1129
- if (payload.byteLength > limits.maxEventBytes)
1130
- return refuse("oversized");
1131
- if (pendingItems >= limits.maxPendingItems)
1132
- return refuse("item-capacity");
1133
- const sequence = lastAcceptedSequence + 1;
1134
- const frame = DiagnosticJournalFrameSchema.parse({
1135
- schemaVersion: 1,
1136
- epoch: config.epoch,
1137
- sequence,
1138
- event: json.data
1139
- });
1140
- const bytes = encoder.encode(`${JSON.stringify(frame)}
1141
- `);
1142
- if (bytes.byteLength > limits.maxFileBytes)
1143
- return refuse("oversized");
1144
- if (pendingBytes + bytes.byteLength > limits.maxPendingBytes) {
1145
- return refuse("byte-capacity");
1146
- }
1147
- const offered = channel.offer({ sequence, bytes });
1148
- if (offered.outcome === "refused") {
1149
- return refuse(offered.reason === "not-open" ? "closed" : "item-capacity");
1150
- }
1151
- accepted += 1;
1152
- pendingItems += 1;
1153
- pendingBytes += bytes.byteLength;
1154
- lastAcceptedSequence = sequence;
1155
- return DiagnosticJournalSubmitResultSchema.parse({
1156
- outcome: "accepted",
1157
- epoch: config.epoch,
1158
- sequence
1159
- });
1160
- },
1161
- async flush(options = {}) {
1162
- const throughSequence = lastAcceptedSequence;
1163
- const outcome = await waitUntil(() => lastSettledSequence >= throughSequence, options);
1164
- return DiagnosticJournalWaitResultSchema.parse({
1165
- outcome,
1166
- state,
1167
- throughSequence,
1168
- settledSequence: lastSettledSequence
1169
- });
1170
- },
1171
- getStatus,
1172
- async close(options = {}) {
1173
- if (state === "open") {
1174
- state = "draining";
1175
- channel.close({ mode: "drain" });
1176
- }
1177
- const wait = await waitUntil(() => physicalClosed, options);
1178
- return DiagnosticJournalCloseResultSchema.parse({
1179
- outcome: wait === "settled" ? "closed" : wait,
1180
- state,
1181
- pendingItems
1182
- });
1183
- }
1184
- };
1185
1012
  }
1186
1013
 
1187
- // src/application/diagnostic-journal.ts
1188
- async function createDiagnosticJournal(config) {
1189
- const limits = DiagnosticJournalLimitsSchema.parse(config.limits);
1190
- const lock = DiagnosticJournalLockPolicySchema.parse(config.lock ?? "refuse");
1191
- const storage = await createRotatingDiagnosticJournalStorage({
1192
- path: config.path,
1193
- maxFileBytes: limits.maxFileBytes,
1194
- maxFiles: limits.maxFiles,
1195
- mode: parseDiagnosticJournalMode(config.mode),
1196
- lock,
1197
- ...config.machineIdentity !== undefined && { machineIdentity: config.machineIdentity }
1198
- });
1199
- return createDiagnosticJournalManager({
1200
- eventSchema: config.eventSchema,
1201
- epoch: randomUUID(),
1202
- limits,
1203
- lock: { policy: lock, reclaimedStale: storage.reclaimedStale },
1204
- ...config.onFailure && { onFailure: config.onFailure }
1205
- }, storage);
1206
- }
1207
1014
  // src/application/events.ts
1208
- import { z as z5 } from "zod";
1209
- var ApplicationLifecycleEventSchema = z5.object({
1210
- type: z5.literal("application-state"),
1015
+ var ApplicationLifecycleEventSchema = z7.object({
1016
+ type: z7.literal("application-state"),
1211
1017
  applicationId: ApplicationIdSchema,
1212
- epoch: z5.string().uuid(),
1213
- revision: z5.number().int().nonnegative(),
1018
+ epoch: z7.string().uuid(),
1019
+ revision: z7.number().int().nonnegative(),
1214
1020
  lifecycle: ApplicationLifecycleSchema,
1215
1021
  health: ApplicationHealthSchema,
1216
- ready: z5.boolean(),
1217
- capturedAt: z5.string().datetime({ offset: true }),
1218
- resources: z5.array(ManagedResourceSnapshotSchema).readonly()
1022
+ ready: z7.boolean(),
1023
+ capturedAt: z7.string().datetime({ offset: true }),
1024
+ resources: z7.array(ManagedResourceSnapshotSchema).readonly()
1219
1025
  }).strict().readonly();
1220
1026
  function applicationLifecycleEvent(snapshot) {
1221
1027
  return ApplicationLifecycleEventSchema.parse({
@@ -1246,13 +1052,13 @@ function createApplicationEventSink(config) {
1246
1052
  };
1247
1053
  }
1248
1054
  // src/application/health.ts
1249
- import { z as z6 } from "zod";
1250
- var ApplicationHealthHandlerOptionsSchema = z6.object({
1251
- kind: z6.enum(["liveness", "readiness"]),
1252
- retryAfterSeconds: z6.number().int().nonnegative().default(5)
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)
1253
1059
  });
1254
- var ApplicationOperationalHandlersOptionsSchema = z6.object({
1255
- retryAfterSeconds: z6.number().int().nonnegative().default(5)
1060
+ var ApplicationOperationalHandlersOptionsSchema = z8.object({
1061
+ retryAfterSeconds: z8.number().int().nonnegative().default(5)
1256
1062
  });
1257
1063
  function createApplicationHealthHandler(application, options) {
1258
1064
  const parsed = ApplicationHealthHandlerOptionsSchema.parse(options);
@@ -1279,6 +1085,901 @@ function createApplicationOperationalHandlers(application, options = {}) {
1279
1085
  })
1280
1086
  };
1281
1087
  }
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
+ }
1282
1983
  // src/application/keyspace.ts
1283
1984
  function defineKeyspace(name, declaration) {
1284
1985
  if (name.trim() === "") {
@@ -1302,7 +2003,7 @@ function keyspaceResource(declaration, config) {
1302
2003
  id: config.id ?? `keyspace:${declaration.name}`,
1303
2004
  ...config.dependsOn && { dependsOn: config.dependsOn },
1304
2005
  async start(_context) {
1305
- await machine.load();
2006
+ await machine.open();
1306
2007
  return { value: machine.keyspace };
1307
2008
  },
1308
2009
  stopAdmission: machine.stopAdmission,
@@ -1315,9 +2016,14 @@ function keyspaceMachine(declaration, config) {
1315
2016
  const maxPending = config.maxPendingWrites ?? 1024;
1316
2017
  const records = new Map;
1317
2018
  let admitting = true;
2019
+ let backend;
2020
+ const currentBackend = () => {
2021
+ backend ??= typeof config.backend === "function" ? config.backend() : config.backend;
2022
+ return backend;
2023
+ };
1318
2024
  let pending = 0;
1319
2025
  let chain = Promise.resolve();
1320
- function run2(work) {
2026
+ function run(work) {
1321
2027
  if (!admitting) {
1322
2028
  return Promise.reject(new Error(`[stitchkit] keyspace "${declaration.name}" is shutting down and is not accepting writes.`));
1323
2029
  }
@@ -1344,8 +2050,8 @@ function keyspaceMachine(declaration, config) {
1344
2050
  put(value) {
1345
2051
  const parsed = declaration.schema.parse(value);
1346
2052
  const key = declaration.key(parsed);
1347
- return run2(async () => {
1348
- await config.backend.put(key, parsed);
2053
+ return run(async () => {
2054
+ await currentBackend().put(key, parsed);
1349
2055
  records.set(key, parsed);
1350
2056
  config.onChanged?.({
1351
2057
  keyspace: declaration.name,
@@ -1356,8 +2062,8 @@ function keyspaceMachine(declaration, config) {
1356
2062
  });
1357
2063
  },
1358
2064
  delete(key) {
1359
- return run2(async () => {
1360
- await config.backend.delete(key);
2065
+ return run(async () => {
2066
+ await currentBackend().delete(key);
1361
2067
  records.delete(key);
1362
2068
  config.onChanged?.({ keyspace: declaration.name, key, change: "delete" });
1363
2069
  });
@@ -1371,11 +2077,19 @@ function keyspaceMachine(declaration, config) {
1371
2077
  return {
1372
2078
  keyspace,
1373
2079
  async load() {
1374
- for (const stored of await config.backend.load()) {
2080
+ for (const stored of await currentBackend().load()) {
1375
2081
  const value = declaration.schema.parse(stored);
1376
2082
  records.set(declaration.key(value), value);
1377
2083
  }
1378
2084
  },
2085
+ async open() {
2086
+ backend = undefined;
2087
+ records.clear();
2088
+ chain = Promise.resolve();
2089
+ pending = 0;
2090
+ admitting = true;
2091
+ await this.load();
2092
+ },
1379
2093
  stopAdmission() {
1380
2094
  admitting = false;
1381
2095
  },
@@ -1384,7 +2098,7 @@ function keyspaceMachine(declaration, config) {
1384
2098
  },
1385
2099
  async close() {
1386
2100
  reportUnwritten();
1387
- await config.backend.close?.();
2101
+ await backend?.close?.();
1388
2102
  },
1389
2103
  force() {
1390
2104
  reportUnwritten();
@@ -1434,6 +2148,308 @@ function sqliteKeyspaceBackend(declaration, config) {
1434
2148
  }
1435
2149
  };
1436
2150
  }
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
+ }
1437
2453
  // src/application/server-resource.ts
1438
2454
  function managedServerResource(config) {
1439
2455
  let shutdownPromise;
@@ -1475,6 +2491,11 @@ function managedServerResource(config) {
1475
2491
  ...config.dependsOn && { dependsOn: config.dependsOn },
1476
2492
  ...config.required !== undefined && { required: config.required },
1477
2493
  async start(context) {
2494
+ if (startAttempted && typeof config.server !== "function") {
2495
+ throw new Error(`[stitchkit] managed server "${config.id}" cannot be restarted: it was given a server instance, and that one has been shut down. Pass a factory — \`server: (context) => …\` — so each generation gets its own.`);
2496
+ }
2497
+ shutdownPromise = undefined;
2498
+ started = undefined;
1478
2499
  startAttempted = true;
1479
2500
  started = await resolveServer(context);
1480
2501
  return { value: started };
@@ -1493,109 +2514,6 @@ function managedServerResource(config) {
1493
2514
  }
1494
2515
  });
1495
2516
  }
1496
- // src/browser/resumable.ts
1497
- import { z as z7 } from "zod";
1498
- var PositiveSafeIntegerSchema3 = z7.number().int().positive().safe();
1499
- var BackoffPolicySchema = z7.object({
1500
- minDelayMs: PositiveSafeIntegerSchema3,
1501
- maxDelayMs: PositiveSafeIntegerSchema3,
1502
- jitter: z7.number().min(0).max(1)
1503
- }).strict().readonly().refine((policy) => policy.maxDelayMs >= policy.minDelayMs, {
1504
- message: "maxDelayMs must be at least minDelayMs"
1505
- });
1506
- function createBackoff(policy, random = Math.random) {
1507
- const { minDelayMs, maxDelayMs, jitter } = BackoffPolicySchema.parse(policy);
1508
- let attempt = 0;
1509
- return {
1510
- next() {
1511
- const exponential = Math.min(maxDelayMs, minDelayMs * 2 ** attempt);
1512
- attempt += 1;
1513
- const spread = exponential * jitter * random();
1514
- return Math.max(1, Math.round(exponential - spread));
1515
- },
1516
- reset() {
1517
- attempt = 0;
1518
- }
1519
- };
1520
- }
1521
-
1522
- // src/live/watch-contract.ts
1523
- import { z as z9 } from "zod";
1524
-
1525
- // src/browser/live-state.ts
1526
- import { z as z8 } from "zod";
1527
- var PositiveSafeIntegerSchema4 = z8.number().int().positive().safe();
1528
- var LiveStatePhaseSchema = z8.enum([
1529
- "idle",
1530
- "opening",
1531
- "live",
1532
- "resync-required",
1533
- "unavailable",
1534
- "closed"
1535
- ]);
1536
- var LiveStateStopReasonSchema = z8.enum([
1537
- "gap",
1538
- "buffer-overflow",
1539
- "source-unavailable",
1540
- "source-error",
1541
- "controller-error",
1542
- "controller-capacity"
1543
- ]);
1544
- var LiveStateControllerStatusSchema = z8.object({
1545
- phase: LiveStatePhaseSchema,
1546
- generation: z8.number().int().nonnegative(),
1547
- hasValue: z8.boolean(),
1548
- bufferedEvents: z8.number().int().nonnegative(),
1549
- bufferedBytes: z8.number().int().nonnegative(),
1550
- receivedEvents: z8.number().int().nonnegative(),
1551
- appliedEvents: z8.number().int().nonnegative(),
1552
- duplicateEvents: z8.number().int().nonnegative(),
1553
- gapEvents: z8.number().int().nonnegative(),
1554
- refusedEvents: z8.number().int().nonnegative(),
1555
- reason: LiveStateStopReasonSchema.optional()
1556
- }).strict().readonly();
1557
-
1558
- // src/live/watch-contract.ts
1559
- var WatchKeySchema = z9.object({
1560
- service: z9.string().min(1),
1561
- action: z9.string().min(1),
1562
- digest: z9.string().min(1)
1563
- }).strict().readonly();
1564
- var WatchOpenSchema = z9.object({ key: WatchKeySchema, args: z9.unknown() }).readonly();
1565
- var WatchAcceptedSchema = z9.object({
1566
- accepted: z9.boolean(),
1567
- reason: z9.string().optional()
1568
- }).strict().readonly();
1569
- var WatchValueSchema = z9.object({
1570
- key: WatchKeySchema,
1571
- revision: z9.number().int().nonnegative(),
1572
- value: z9.unknown()
1573
- }).readonly();
1574
- var WatchStateSchema = z9.object({
1575
- key: WatchKeySchema,
1576
- phase: LiveStatePhaseSchema,
1577
- reason: LiveStateStopReasonSchema.optional(),
1578
- code: z9.string().optional(),
1579
- message: z9.string().optional()
1580
- }).readonly();
1581
- var WATCH_OPEN = "stitchkit.watch.open";
1582
- var WATCH_CLOSE = "stitchkit.watch.close";
1583
- var WATCH_VALUE = "stitchkit.watch.value";
1584
- var WATCH_STATE = "stitchkit.watch.state";
1585
- var watchContract = {
1586
- serverToClient: {
1587
- [WATCH_VALUE]: { args: z9.tuple([WatchValueSchema]) },
1588
- [WATCH_STATE]: { args: z9.tuple([WatchStateSchema]) }
1589
- },
1590
- clientToServer: {
1591
- [WATCH_OPEN]: { args: z9.tuple([WatchOpenSchema]), ack: WatchAcceptedSchema },
1592
- [WATCH_CLOSE]: { args: z9.tuple([z9.object({ key: WatchKeySchema }).readonly()]) }
1593
- }
1594
- };
1595
- function watchKeyString(key) {
1596
- return `${key.service}/${key.action}/${key.digest}`;
1597
- }
1598
-
1599
2517
  // src/application/watch-hub.ts
1600
2518
  var DEFAULT_BACKOFF = { minDelayMs: 250, maxDelayMs: 30000, jitter: 0.2 };
1601
2519
  function watchKey(operation, args) {
@@ -1819,7 +2737,6 @@ export {
1819
2737
  defineKeyspace,
1820
2738
  createWatchHub,
1821
2739
  createManagedSchedule,
1822
- createDiagnosticJournal,
1823
2740
  createDecisionPipeline,
1824
2741
  createCreditWindow,
1825
2742
  createBoundedChannel,