stitchkit 0.76.2 → 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 (74) 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 +89 -0
  5. package/dist/application/decisions.d.ts.map +1 -0
  6. package/dist/application/kernel.d.ts +35 -1
  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.d.ts +15 -0
  17. package/dist/application-schemas.d.ts.map +1 -0
  18. package/dist/application-schemas.js +26 -0
  19. package/dist/application.d.ts +3 -3
  20. package/dist/application.d.ts.map +1 -1
  21. package/dist/application.js +1832 -844
  22. package/dist/cli.js +7 -7
  23. package/dist/contract/index.js +9 -7
  24. package/dist/files/boundary.d.ts.map +1 -1
  25. package/dist/files.js +1 -1
  26. package/dist/{index-3z73fh2c.js → index-1ckavx4h.js} +49 -1
  27. package/dist/{index-s2rchahr.js → index-31k1ns1s.js} +4 -4
  28. package/dist/{index-m668wzyc.js → index-4ntp63ps.js} +4 -4
  29. package/dist/{index-1rxswfbv.js → index-56fm73re.js} +1 -1
  30. package/dist/{index-ezmn6ac6.js → index-5r0ak8fr.js} +3 -3
  31. package/dist/{index-f6n5n7nz.js → index-7z69kyrh.js} +10 -87
  32. package/dist/{index-k2zczx1g.js → index-93xp5tgg.js} +1 -1
  33. package/dist/{index-bfcpjw20.js → index-actkayzn.js} +7 -3
  34. package/dist/index-anyqnqcv.js +79 -0
  35. package/dist/{index-7qy2ex0m.js → index-cwp38wxn.js} +1 -1
  36. package/dist/{index-35z5h2ty.js → index-dmfn9m4x.js} +125 -4
  37. package/dist/index-h4pj6fta.js +87 -0
  38. package/dist/{index-jqtsc9mj.js → index-j6mfaw26.js} +2 -2
  39. package/dist/{index-58jzmnn4.js → index-k45qqrdh.js} +513 -39
  40. package/dist/{index-8eywc9zv.js → index-kw12fsbc.js} +0 -1
  41. package/dist/index-n1g2xm6a.js +203 -0
  42. package/dist/{index-t8qyvrvg.js → index-n7apa6sy.js} +12 -12
  43. package/dist/{index-s4c8wy8m.js → index-qaajjfp6.js} +8 -8
  44. package/dist/{index-nemjkxjp.js → index-s5tn4kdv.js} +14 -10
  45. package/dist/index-wfq8zerm.js +92 -0
  46. package/dist/index.js +53 -1215
  47. package/dist/internal/deadline.d.ts +17 -0
  48. package/dist/internal/deadline.d.ts.map +1 -0
  49. package/dist/internal/decision.d.ts +31 -0
  50. package/dist/internal/decision.d.ts.map +1 -0
  51. package/dist/live/events.d.ts +8 -13
  52. package/dist/live/events.d.ts.map +1 -1
  53. package/dist/live.d.ts +1 -1
  54. package/dist/live.d.ts.map +1 -1
  55. package/dist/live.js +12 -89
  56. package/dist/node.js +5 -6
  57. package/dist/observability/context.d.ts.map +1 -1
  58. package/dist/observability/index.js +3 -3
  59. package/dist/primitives.js +2 -1
  60. package/dist/remote.js +3 -6
  61. package/dist/server/event-bus.d.ts +3 -2
  62. package/dist/server/event-bus.d.ts.map +1 -1
  63. package/dist/server/index.js +31 -30
  64. package/dist/testing.js +1562 -87
  65. package/dist/tool-invoker.js +6 -6
  66. package/dist/tools.js +66 -23
  67. package/llms-full.txt +276 -6
  68. package/package.json +12 -4
  69. package/dist/index-3cwck0rm.js +0 -897
  70. package/dist/index-7b188kmz.js +0 -49
  71. package/dist/index-a916km3r.js +0 -45
  72. package/dist/index-hvftzz91.js +0 -721
  73. package/dist/{index-da1aqnhb.js → index-aewy3x5j.js} +3 -3
  74. package/dist/{index-nt1mp8km.js → index-efm026pw.js} +3 -3
@@ -1,43 +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
- ApplicationShutdownBudgetSchema,
34
- ApplicationShutdownOptionsSchema,
35
- createApplication
36
- } from "./index-hvftzz91.js";
37
- import {
38
- defineManagedResource,
39
- managedResourceDependencyId
40
- } from "./index-2k4yrqkc.js";
22
+ isRecord
23
+ } from "./index-ksp6e2ye.js";
41
24
  import {
42
25
  ApplicationAdmissionSnapshotSchema,
43
26
  ApplicationHealthSchema,
@@ -50,46 +33,315 @@ import {
50
33
  ManagedResourceSnapshotSchema,
51
34
  ManagedResourceStateSchema,
52
35
  projectApplicationStatus
53
- } from "./index-8eywc9zv.js";
54
- import"./index-3z73fh2c.js";
55
- import"./index-0w9abg87.js";
56
- import {
57
- isRecord
58
- } from "./index-qyrqwr4c.js";
59
- // 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
60
42
  import { z } from "zod";
61
- var PositiveSafeIntegerSchema = z.number().int().positive().safe();
62
- 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({
63
315
  limit: PositiveSafeIntegerSchema,
64
316
  intervalMs: PositiveSafeIntegerSchema
65
317
  }).strict().readonly();
66
- var BoundedAdmissionPerKeyLimitsSchema = z.object({
318
+ var BoundedAdmissionPerKeyLimitsSchema = z3.object({
67
319
  maxConcurrent: PositiveSafeIntegerSchema,
68
320
  rate: BoundedRateBudgetSchema.optional()
69
321
  }).strict().readonly();
70
- var PerKeyLimitResolverSchema = z.custom((value) => typeof value === "function", { message: "perKey.limits must be a function of the key" });
71
- var BoundedAdmissionPolicySchema = z.object({
72
- 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({
73
325
  maxConcurrent: PositiveSafeIntegerSchema,
74
326
  rate: BoundedRateBudgetSchema.optional()
75
327
  }).strict(),
76
- perKey: z.union([
77
- z.object({
328
+ perKey: z3.union([
329
+ z3.object({
78
330
  maxConcurrent: PositiveSafeIntegerSchema,
79
331
  maxKeys: PositiveSafeIntegerSchema,
80
332
  rate: BoundedRateBudgetSchema.optional(),
81
- limits: z.never().optional()
333
+ limits: z3.never().optional()
82
334
  }).strict(),
83
- z.object({
335
+ z3.object({
84
336
  maxKeys: PositiveSafeIntegerSchema,
85
337
  limits: PerKeyLimitResolverSchema,
86
- maxConcurrent: z.never().optional(),
87
- rate: z.never().optional()
338
+ maxConcurrent: z3.never().optional(),
339
+ rate: z3.never().optional()
88
340
  }).strict()
89
341
  ]).optional()
90
342
  }).strict().readonly();
91
- var BoundedAdmissionStateSchema = z.enum(["accepting", "draining", "closed"]);
92
- var BoundedAdmissionRefusalReasonSchema = z.enum([
343
+ var BoundedAdmissionStateSchema = z3.enum(["accepting", "draining", "closed"]);
344
+ var BoundedAdmissionRefusalReasonSchema = z3.enum([
93
345
  "not-accepting",
94
346
  "global-concurrency",
95
347
  "key-required",
@@ -99,25 +351,25 @@ var BoundedAdmissionRefusalReasonSchema = z.enum([
99
351
  "key-rate",
100
352
  "upstream"
101
353
  ]);
102
- var RefusalCountersSchema = z.object({
103
- "not-accepting": z.number().int().nonnegative(),
104
- "global-concurrency": z.number().int().nonnegative(),
105
- "key-required": z.number().int().nonnegative(),
106
- "key-capacity": z.number().int().nonnegative(),
107
- "key-concurrency": z.number().int().nonnegative(),
108
- "global-rate": z.number().int().nonnegative(),
109
- "key-rate": z.number().int().nonnegative(),
110
- 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()
111
363
  }).strict().readonly();
112
- var BoundedAdmissionSnapshotSchema = z.object({
364
+ var BoundedAdmissionSnapshotSchema = z3.object({
113
365
  state: BoundedAdmissionStateSchema,
114
- active: z.number().int().nonnegative(),
115
- accepted: z.number().int().nonnegative(),
116
- released: z.number().int().nonnegative(),
117
- refused: z.number().int().nonnegative(),
118
- keys: z.number().int().nonnegative(),
119
- globalRateSamples: z.number().int().nonnegative(),
120
- 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(),
121
373
  refusals: RefusalCountersSchema
122
374
  }).strict().readonly();
123
375
 
@@ -430,23 +682,117 @@ function createBoundedAdmission(config) {
430
682
  getSnapshot
431
683
  };
432
684
  }
433
- // src/application/diagnostic-journal.ts
434
- import { randomUUID } from "node:crypto";
685
+ // src/internal/deadline.ts
686
+ async function withDeadline(value, timeoutMs) {
687
+ if (!(value instanceof Promise))
688
+ return { settled: true, value };
689
+ let timer;
690
+ const expiry = new Promise((resolve) => {
691
+ timer = setTimeout(() => resolve({ settled: false }), timeoutMs);
692
+ });
693
+ try {
694
+ return await Promise.race([
695
+ value.then((settledValue) => ({ settled: true, value: settledValue })),
696
+ expiry
697
+ ]);
698
+ } finally {
699
+ if (timer !== undefined)
700
+ clearTimeout(timer);
701
+ }
702
+ }
703
+
704
+ // src/internal/decision.ts
705
+ import { z as z4 } from "zod";
706
+ var PolicyDecisionSchema = z4.discriminatedUnion("outcome", [
707
+ z4.object({ outcome: z4.literal("allow") }).strict(),
708
+ z4.object({ outcome: z4.literal("deny"), reason: z4.string().min(1) }).strict(),
709
+ z4.object({ outcome: z4.literal("defer") }).strict()
710
+ ]);
711
+
712
+ // src/application/decisions.ts
713
+ class DecisionUndecidedError extends Error {
714
+ trace;
715
+ constructor(trace) {
716
+ const ran = trace.length === 0 ? "no policy ran" : trace.map((e) => e.id).join(" → ");
717
+ super(`Decision pipeline reached its end with no terminal verdict (${ran}). A pipeline needs a policy that answers allow or deny.`);
718
+ this.name = "DecisionUndecidedError";
719
+ this.trace = trace;
720
+ }
721
+ }
435
722
 
723
+ class DecisionPolicyError extends Error {
724
+ policyId;
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.`);
728
+ this.name = "DecisionPolicyError";
729
+ this.policyId = policyId;
730
+ this.trace = trace;
731
+ }
732
+ }
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
+ }
738
+ const seen = new Set;
739
+ for (const policy of policies) {
740
+ if (policy.id.trim() === "") {
741
+ throw new Error("[stitchkit] decision pipeline: a policy needs a non-empty id.");
742
+ }
743
+ if (seen.has(policy.id)) {
744
+ throw new Error(`[stitchkit] decision pipeline: two policies share the id "${policy.id}". The trace names policies by id, and a duplicate makes it ambiguous exactly when it is read to explain a refusal.`);
745
+ }
746
+ seen.add(policy.id);
747
+ }
748
+ return {
749
+ policyIds: policies.map((policy) => policy.id),
750
+ async decide(input) {
751
+ const trace = [];
752
+ for (const policy of policies) {
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
+ }
765
+ const parsed = PolicyDecisionSchema.safeParse(answer);
766
+ if (!parsed.success) {
767
+ throw new DecisionPolicyError(policy.id, parsed.error.issues[0]?.message ?? "invalid", trace);
768
+ }
769
+ const decision = parsed.data;
770
+ trace.push(decision.outcome === "deny" ? { id: policy.id, outcome: "deny", reason: decision.reason } : { id: policy.id, outcome: decision.outcome });
771
+ if (decision.outcome === "deny") {
772
+ return { outcome: "deny", reason: decision.reason, trace };
773
+ }
774
+ if (decision.outcome === "allow") {
775
+ return { outcome: "allow", trace };
776
+ }
777
+ }
778
+ throw new DecisionUndecidedError(trace);
779
+ }
780
+ };
781
+ }
436
782
  // src/application/diagnostic-journal-contract.ts
437
- import { z as z2 } from "zod";
438
- var PositiveSafeIntegerSchema2 = z2.number().int().positive().safe();
439
- var FileModeSchema = z2.number().int().min(0).max(511);
440
- var DiagnosticJournalLimitsSchema = z2.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({
441
787
  maxEventBytes: PositiveSafeIntegerSchema2,
442
788
  maxPendingItems: PositiveSafeIntegerSchema2,
443
789
  maxPendingBytes: PositiveSafeIntegerSchema2,
444
790
  maxFileBytes: PositiveSafeIntegerSchema2,
445
791
  maxFiles: PositiveSafeIntegerSchema2
446
792
  }).strict().readonly();
447
- var DiagnosticJournalLockPolicySchema = z2.enum(["refuse", "reclaim-stale"]);
448
- var DiagnosticJournalStateSchema = z2.enum(["open", "draining", "closed", "failed"]);
449
- var DiagnosticJournalRefusalReasonSchema = z2.enum([
793
+ var DiagnosticJournalLockPolicySchema = z5.enum(["refuse", "reclaim-stale"]);
794
+ var DiagnosticJournalStateSchema = z5.enum(["open", "draining", "closed", "failed"]);
795
+ var DiagnosticJournalRefusalReasonSchema = z5.enum([
450
796
  "closed",
451
797
  "failed",
452
798
  "invalid",
@@ -454,704 +800,228 @@ var DiagnosticJournalRefusalReasonSchema = z2.enum([
454
800
  "item-capacity",
455
801
  "byte-capacity"
456
802
  ]);
457
- var DiagnosticJournalRefusalCountersSchema = z2.object({
458
- closed: z2.number().int().nonnegative(),
459
- failed: z2.number().int().nonnegative(),
460
- invalid: z2.number().int().nonnegative(),
461
- oversized: z2.number().int().nonnegative(),
462
- "item-capacity": z2.number().int().nonnegative(),
463
- "byte-capacity": z2.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()
464
810
  }).strict().readonly();
465
- var DiagnosticJournalFailurePhaseSchema = z2.enum(["write", "rotation", "close"]);
466
- var DiagnosticJournalStatusSchema = z2.object({
811
+ var DiagnosticJournalFailurePhaseSchema = z5.enum(["write", "rotation", "close"]);
812
+ var DiagnosticJournalStatusSchema = z5.object({
467
813
  state: DiagnosticJournalStateSchema,
468
- epoch: z2.uuid(),
814
+ epoch: z5.uuid(),
469
815
  limits: DiagnosticJournalLimitsSchema,
470
- lock: z2.object({
816
+ lock: z5.object({
471
817
  policy: DiagnosticJournalLockPolicySchema,
472
- reclaimedStale: z2.boolean()
818
+ reclaimedStale: z5.boolean()
473
819
  }).strict().readonly(),
474
- received: z2.number().int().nonnegative(),
475
- accepted: z2.number().int().nonnegative(),
476
- refused: z2.number().int().nonnegative(),
820
+ received: z5.number().int().nonnegative(),
821
+ accepted: z5.number().int().nonnegative(),
822
+ refused: z5.number().int().nonnegative(),
477
823
  refusals: DiagnosticJournalRefusalCountersSchema,
478
- written: z2.number().int().nonnegative(),
479
- failedRecords: z2.number().int().nonnegative(),
480
- pendingItems: z2.number().int().nonnegative(),
481
- pendingBytes: z2.number().int().nonnegative(),
482
- inFlight: z2.boolean(),
483
- rotations: z2.number().int().nonnegative(),
484
- rotationFailures: z2.number().int().nonnegative(),
485
- partialTails: z2.number().int().nonnegative(),
486
- currentFileBytes: z2.number().int().nonnegative(),
487
- retainedFiles: z2.number().int().nonnegative(),
488
- lastAcceptedSequence: z2.number().int().positive().optional(),
489
- lastWrittenSequence: z2.number().int().positive().optional(),
490
- lastSettledSequence: z2.number().int().positive().optional(),
491
- lastFailure: z2.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({
492
838
  phase: DiagnosticJournalFailurePhaseSchema,
493
- sequence: z2.number().int().positive().optional()
839
+ sequence: z5.number().int().positive().optional()
494
840
  }).strict().readonly().optional()
495
841
  }).strict().readonly();
496
- var DiagnosticJournalSubmitResultSchema = z2.discriminatedUnion("outcome", [
497
- z2.object({
498
- outcome: z2.literal("accepted"),
499
- epoch: z2.uuid(),
500
- sequence: z2.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()
501
847
  }).strict().readonly(),
502
- z2.object({
503
- outcome: z2.literal("refused"),
848
+ z5.object({
849
+ outcome: z5.literal("refused"),
504
850
  reason: DiagnosticJournalRefusalReasonSchema
505
851
  }).strict().readonly()
506
852
  ]);
507
- var DiagnosticJournalWaitResultSchema = z2.object({
508
- outcome: z2.enum(["settled", "timed-out", "cancelled"]),
853
+ var DiagnosticJournalWaitResultSchema = z5.object({
854
+ outcome: z5.enum(["settled", "timed-out", "cancelled"]),
509
855
  state: DiagnosticJournalStateSchema,
510
- throughSequence: z2.number().int().nonnegative(),
511
- settledSequence: z2.number().int().nonnegative()
856
+ throughSequence: z5.number().int().nonnegative(),
857
+ settledSequence: z5.number().int().nonnegative()
512
858
  }).strict().readonly();
513
- var DiagnosticJournalCloseResultSchema = z2.object({
514
- outcome: z2.enum(["closed", "timed-out", "cancelled"]),
859
+ var DiagnosticJournalCloseResultSchema = z5.object({
860
+ outcome: z5.enum(["closed", "timed-out", "cancelled"]),
515
861
  state: DiagnosticJournalStateSchema,
516
- pendingItems: z2.number().int().nonnegative()
862
+ pendingItems: z5.number().int().nonnegative()
517
863
  }).strict().readonly();
518
- var DiagnosticJournalFrameSchema = z2.object({
519
- schemaVersion: z2.literal(1),
520
- epoch: z2.uuid(),
521
- sequence: z2.number().int().positive(),
522
- event: z2.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()
523
869
  }).strict().readonly();
524
- function parseDiagnosticJournalMode(mode) {
525
- return FileModeSchema.parse(mode ?? 384);
526
- }
527
870
  function readDiagnosticJournalLockDiagnosis(error) {
528
871
  if (!isRecord(error))
529
872
  return;
530
873
  const diagnosis = error.journalLock;
531
874
  return isRecord(diagnosis) ? diagnosis : undefined;
532
875
  }
876
+ // src/application/events.ts
877
+ import { z as z7 } from "zod";
533
878
 
534
- // src/application/diagnostic-journal-manager.ts
535
- import { z as z3 } from "zod";
536
-
537
- // src/application/diagnostic-journal-storage.ts
538
- import { constants as constants2 } from "node:fs";
539
- import { lstat, open as open2, readdir, realpath, rename, unlink as unlink2 } from "node:fs/promises";
540
- import { basename, dirname, isAbsolute, resolve } from "node:path";
541
-
542
- // src/application/diagnostic-journal-lock.ts
543
- import { execFile } from "node:child_process";
544
- import { constants } from "node:fs";
545
- import { open, readFile, unlink } from "node:fs/promises";
546
- import { hostname, platform } from "node:os";
547
- import { promisify } from "node:util";
548
- function isExisting(error) {
549
- return isRecord(error) && error.code === "EEXIST";
550
- }
551
- function readOwner(text) {
552
- let parsed;
553
- try {
554
- parsed = JSON.parse(text);
555
- } catch {
556
- return;
557
- }
558
- if (!isRecord(parsed))
559
- return;
560
- const { pid, host, acquiredAt, machine } = parsed;
561
- if (typeof pid !== "number" || !Number.isSafeInteger(pid) || pid <= 0)
562
- return;
563
- if (typeof host !== "string" || host.length === 0)
564
- return;
565
- if (typeof acquiredAt !== "string")
566
- return;
567
- const identified = typeof machine === "string" && machine.length > 0 ? { machine } : {};
568
- return { pid, host, acquiredAt, ...identified };
569
- }
570
- var detectedIdentity;
571
- async function firstReadableLine(paths) {
572
- for (const path of paths) {
573
- try {
574
- const text = (await readFile(path, "utf8")).trim();
575
- if (text.length > 0)
576
- return text;
577
- } catch {}
578
- }
579
- return null;
580
- }
581
- var run = promisify(execFile);
582
- function parsePlatformUuid(dump) {
583
- return /"IOPlatformUUID"\s*=\s*"([^"]+)"/.exec(dump)?.[1] ?? null;
584
- }
585
- async function readDarwinPlatformUuid(command = "ioreg") {
586
- try {
587
- const { stdout } = await run(command, ["-rd1", "-c", "IOPlatformExpertDevice"], {
588
- timeout: 2000,
589
- maxBuffer: 1 << 20,
590
- encoding: "utf8"
591
- });
592
- return parsePlatformUuid(stdout);
593
- } catch {
594
- return null;
595
- }
596
- }
597
- async function machineIdentity(declared) {
598
- if (declared !== undefined)
599
- return declared;
600
- if (detectedIdentity !== undefined)
601
- return detectedIdentity;
602
- detectedIdentity = platform() === "darwin" ? await readDarwinPlatformUuid() : await firstReadableLine(["/etc/machine-id", "/var/lib/dbus/machine-id"]);
603
- return detectedIdentity;
604
- }
605
- function attribute(owner, identity) {
606
- if (identity !== null && owner.machine !== undefined) {
607
- return owner.machine === identity ? "this-machine" : "another-machine";
608
- }
609
- return owner.host === hostname() ? "this-machine" : "unattributable";
610
- }
611
- async function probeLiveness(owner) {
612
- try {
613
- process.kill(owner.pid, 0);
614
- } catch (error) {
615
- return isRecord(error) && error.code === "ESRCH" ? "gone" : "alive";
616
- }
617
- return await isZombieProcess(owner.pid) ? "gone" : "alive";
618
- }
619
- async function isZombieProcess(pid, seam = {}) {
620
- const { procRoot = "/proc", psCommand = "ps" } = seam;
621
- try {
622
- const stat = await readFile(`${procRoot}/${pid}/stat`, "utf8");
623
- return stat.slice(stat.lastIndexOf(")") + 1).trimStart().startsWith("Z");
624
- } catch {}
625
- try {
626
- const { stdout } = await run(psCommand, ["-o", "state=", "-p", String(pid)], {
627
- timeout: 2000,
628
- maxBuffer: 1 << 16,
629
- encoding: "utf8"
630
- });
631
- return stdout.trim().startsWith("Z");
632
- } catch {
633
- return false;
634
- }
635
- }
636
- async function diagnose(owner, declaredIdentity) {
637
- if (!owner)
638
- return { attribution: "unattributable", liveness: "not-probed", owner: null };
639
- const attribution = attribute(owner, await machineIdentity(declaredIdentity));
640
- const record = {
641
- pid: owner.pid,
642
- host: owner.host,
643
- acquiredAt: owner.acquiredAt,
644
- ...owner.machine !== undefined && { machine: owner.machine }
645
- };
646
- if (attribution !== "this-machine")
647
- return { attribution, liveness: "not-probed", owner: record };
648
- return { attribution, liveness: await probeLiveness(owner), owner: record };
649
- }
650
- async function readLockOwner(lockPath) {
651
- let handle;
652
- try {
653
- handle = await open(lockPath, constants.O_RDONLY | constants.O_NOFOLLOW);
654
- return readOwner(await handle.readFile("utf8"));
655
- } catch {
656
- return;
657
- } finally {
658
- await handle?.close().catch(() => {
659
- return;
660
- });
661
- }
662
- }
663
- async function createExclusive(lockPath, mode, declaredIdentity) {
664
- const handle = await open(lockPath, "wx", mode);
665
- const identity = await machineIdentity(declaredIdentity);
666
- const owner = {
667
- pid: process.pid,
668
- host: hostname(),
669
- acquiredAt: new Date().toISOString(),
670
- ...identity !== null && { machine: identity }
671
- };
672
- await handle.writeFile(`${JSON.stringify(owner)}
673
- `, "utf8");
674
- return handle;
675
- }
676
- async function acquireDiagnosticJournalLock(lockPath, mode, policy, declaredIdentity) {
677
- try {
678
- return {
679
- handle: await createExclusive(lockPath, mode, declaredIdentity),
680
- reclaimedStale: false
681
- };
682
- } catch (error) {
683
- if (policy !== "reclaim-stale" || !isExisting(error))
684
- throw error;
685
- const owner = await readLockOwner(lockPath);
686
- const diagnosis = await diagnose(owner, declaredIdentity);
687
- if (!owner || diagnosis.liveness !== "gone") {
688
- if (isRecord(error))
689
- Object.assign(error, { journalLock: diagnosis });
690
- throw error;
691
- }
692
- await unlink(lockPath).catch(() => {
693
- return;
694
- });
695
- return {
696
- handle: await createExclusive(lockPath, mode, declaredIdentity),
697
- reclaimedStale: true
698
- };
699
- }
700
- }
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();
701
904
 
702
- // src/application/diagnostic-journal-storage.ts
703
- class DiagnosticJournalStorageError extends Error {
704
- phase;
705
- constructor(phase, message, options) {
706
- super(message, options);
707
- this.phase = phase;
708
- this.name = "DiagnosticJournalStorageError";
709
- }
710
- }
711
- function isMissing(error) {
712
- return isRecord(error) && error.code === "ENOENT";
713
- }
714
- async function removeIfPresent(path) {
715
- try {
716
- const info = await lstat(path);
717
- if (info.isSymbolicLink() || !info.isFile()) {
718
- throw new Error("Diagnostic journal generations must be regular files");
719
- }
720
- await unlink2(path);
721
- } catch (error) {
722
- if (!isMissing(error))
723
- throw error;
724
- }
725
- }
726
- async function moveIfPresent(from, to) {
727
- try {
728
- const info = await lstat(from);
729
- if (info.isSymbolicLink() || !info.isFile()) {
730
- throw new Error("Diagnostic journal generations must be regular files");
731
- }
732
- await rename(from, to);
733
- return true;
734
- } catch (error) {
735
- if (isMissing(error))
736
- return false;
737
- throw error;
738
- }
739
- }
740
- function generation(path, index) {
741
- return `${path}.${index}`;
742
- }
743
- function generationIndex(name, prefix) {
744
- if (!name.startsWith(prefix))
905
+ // src/internal/observability-sink.ts
906
+ var DEFAULT_MAX_PENDING = 1000;
907
+ function invokeIsolated(callback) {
908
+ if (!callback)
745
909
  return;
746
- const suffix = name.slice(prefix.length);
747
- if (!/^\d+$/.test(suffix))
748
- return;
749
- const value = Number(suffix);
750
- return Number.isSafeInteger(value) && value > 0 ? value : undefined;
910
+ Promise.resolve().then(callback).catch(() => {});
751
911
  }
752
- async function createRotatingDiagnosticJournalStorage(config) {
753
- if (!isAbsolute(config.path) || resolve(config.path) !== config.path) {
754
- 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");
755
916
  }
756
- const parent = await realpath(dirname(config.path));
757
- const journalPath = resolve(parent, basename(config.path));
758
- const lockPath = `${journalPath}.lock`;
759
- const { handle: lock, reclaimedStale } = await acquireDiagnosticJournalLock(lockPath, config.mode, config.lock, config.machineIdentity);
760
- let handle;
761
- let currentFileBytes = 0;
762
- let retainedFiles = 1;
763
- let rotations = 0;
764
- let partialTails = 0;
917
+ let sequence = 0;
765
918
  let closed = false;
766
- const openCurrent = async () => {
767
- handle = await open2(journalPath, constants2.O_APPEND | constants2.O_CREAT | constants2.O_RDWR | constants2.O_NOFOLLOW, config.mode);
768
- const info = await handle.stat();
769
- if (!info.isFile()) {
770
- await handle.close();
771
- handle = undefined;
772
- throw new Error("Diagnostic journal path must be a regular file");
773
- }
774
- 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);
775
931
  };
776
- 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) => {
777
936
  try {
778
- await handle?.close();
779
- handle = undefined;
780
- if (config.maxFiles === 1) {
781
- await removeIfPresent(journalPath);
782
- } else {
783
- await removeIfPresent(generation(journalPath, config.maxFiles - 1));
784
- for (let index = config.maxFiles - 2;index >= 1; index -= 1) {
785
- await moveIfPresent(generation(journalPath, index), generation(journalPath, index + 1));
786
- }
787
- await moveIfPresent(journalPath, generation(journalPath, 1));
937
+ if (config.filter && !config.filter(event)) {
938
+ filtered += 1;
939
+ return;
788
940
  }
789
- retainedFiles = Math.min(config.maxFiles, retainedFiles + 1);
790
- rotations += 1;
791
- await openCurrent();
792
941
  } catch (error) {
793
- throw new DiagnosticJournalStorageError("rotation", "Diagnostic journal rotation failed", { cause: error });
794
- }
795
- };
796
- try {
797
- const names = await readdir(parent);
798
- const prefix = `${basename(journalPath)}.`;
799
- let existingGenerations = 0;
800
- for (const name of names) {
801
- const index = generationIndex(name, prefix);
802
- if (index === undefined)
803
- continue;
804
- if (index >= config.maxFiles) {
805
- await removeIfPresent(resolve(parent, name));
806
- } else {
807
- const info = await lstat(resolve(parent, name));
808
- if (info.isSymbolicLink() || !info.isFile()) {
809
- throw new Error("Diagnostic journal generations must be regular files");
810
- }
811
- existingGenerations += 1;
812
- }
813
- }
814
- retainedFiles = Math.min(config.maxFiles, existingGenerations + 1);
815
- await openCurrent();
816
- if (currentFileBytes > 0) {
817
- const tail = new Uint8Array(1);
818
- const read = await handle?.read(tail, 0, 1, currentFileBytes - 1);
819
- if (read?.bytesRead === 1 && tail[0] !== 10) {
820
- partialTails += 1;
821
- await rotate();
822
- }
823
- }
824
- } catch (error) {
825
- await handle?.close().catch(() => {
942
+ preparationFailed += 1;
943
+ reportError(error, event);
826
944
  return;
827
- });
828
- await lock.close().catch(() => {
829
- return;
830
- });
831
- await unlink2(lockPath).catch(() => {
945
+ }
946
+ if (writes.size >= maxPending) {
947
+ dropped += 1;
948
+ reportDrop("capacity", event);
832
949
  return;
833
- });
834
- throw error;
835
- }
836
- const snapshot = () => ({
837
- currentFileBytes,
838
- retainedFiles,
839
- rotations,
840
- partialTails
841
- });
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
+ };
842
965
  return {
843
- reclaimedStale,
844
- async append(bytes) {
845
- if (closed || !handle) {
846
- throw new DiagnosticJournalStorageError("write", "Diagnostic journal storage is closed");
847
- }
848
- let rotated = false;
849
- if (currentFileBytes > 0 && currentFileBytes + bytes.byteLength > config.maxFileBytes) {
850
- await rotate();
851
- rotated = true;
852
- }
853
- try {
854
- await handle.writeFile(bytes);
855
- currentFileBytes += bytes.byteLength;
856
- return { ...snapshot(), rotated };
857
- } catch (error) {
858
- throw new DiagnosticJournalStorageError("write", "Diagnostic journal write failed", {
859
- 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);
860
976
  });
977
+ return;
861
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);
862
984
  },
863
- snapshot,
864
- async close() {
865
- if (closed)
866
- return;
985
+ flush() {
986
+ return awaitGeneration(sequence);
987
+ },
988
+ getStatus,
989
+ close() {
990
+ if (closePromise)
991
+ return closePromise;
867
992
  closed = true;
868
- let failure;
869
- try {
870
- await handle?.close();
871
- } catch (error) {
872
- failure = error;
873
- }
874
- try {
875
- await lock.close();
876
- await unlink2(lockPath);
877
- } catch (error) {
878
- failure ??= error;
879
- }
880
- if (failure !== undefined) {
881
- throw new DiagnosticJournalStorageError("close", "Diagnostic journal close failed", {
882
- cause: failure
883
- });
884
- }
993
+ closePromise = awaitGeneration(sequence).then(getStatus);
994
+ return closePromise;
885
995
  }
886
996
  };
887
- }
888
-
889
- // src/application/diagnostic-journal-manager.ts
890
- function waitBudget(options) {
891
- if (options.timeoutMs === undefined)
892
- return;
893
- if (!Number.isSafeInteger(options.timeoutMs) || options.timeoutMs <= 0) {
894
- 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
+ });
895
1011
  }
896
- return options.timeoutMs;
897
- }
898
- function createDiagnosticJournalManager(config, storage) {
899
- const limits = DiagnosticJournalLimitsSchema.parse(config.limits);
900
- const encoder = new TextEncoder;
901
- const channel = createBoundedChannel({
902
- policy: "ordered",
903
- maxItems: limits.maxPendingItems,
904
- maxBytes: limits.maxPendingBytes,
905
- sizeOf: (frame) => frame.bytes.byteLength
906
- });
907
- const refusals = {
908
- closed: 0,
909
- failed: 0,
910
- invalid: 0,
911
- oversized: 0,
912
- "item-capacity": 0,
913
- "byte-capacity": 0
914
- };
915
- const progressWaiters = new Set;
916
- let state = "open";
917
- let received = 0;
918
- let accepted = 0;
919
- let refused = 0;
920
- let written = 0;
921
- let failedRecords = 0;
922
- let pendingItems = 0;
923
- let pendingBytes = 0;
924
- let inFlight = false;
925
- let rotationFailures = 0;
926
- let lastAcceptedSequence = 0;
927
- let lastWrittenSequence = 0;
928
- let lastSettledSequence = 0;
929
- let lastFailure;
930
- let physicalClosed = false;
931
- const isFailed = () => state === "failed";
932
- const notifyProgress = () => {
933
- for (const waiter of progressWaiters)
934
- waiter();
935
- };
936
- const reportFailure = (failure) => {
937
- if (!config.onFailure)
938
- return;
939
- Promise.resolve().then(() => config.onFailure?.(failure)).catch(() => {
940
- return;
941
- });
942
- };
943
- const markFailure = (error, fallbackPhase, sequence) => {
944
- const phase = error instanceof DiagnosticJournalStorageError ? error.phase : fallbackPhase;
945
- if (phase === "rotation")
946
- rotationFailures += 1;
947
- lastFailure = { phase, ...sequence !== undefined && { sequence } };
948
- state = "failed";
949
- channel.close({ mode: "drain" });
950
- reportFailure({ phase, error, ...sequence !== undefined && { sequence } });
951
- };
952
- const worker = (async () => {
953
- for await (const frame of channel) {
954
- inFlight = true;
955
- if (isFailed()) {
956
- failedRecords += 1;
957
- } else {
958
- try {
959
- await storage.append(frame.bytes);
960
- written += 1;
961
- lastWrittenSequence = frame.sequence;
962
- } catch (error) {
963
- failedRecords += 1;
964
- markFailure(error, "write", frame.sequence);
965
- }
966
- }
967
- pendingItems -= 1;
968
- pendingBytes -= frame.bytes.byteLength;
969
- lastSettledSequence = frame.sequence;
970
- inFlight = false;
971
- notifyProgress();
972
- }
973
- try {
974
- await storage.close();
975
- } catch (error) {
976
- markFailure(error, "close");
977
- }
978
- physicalClosed = true;
979
- if (!isFailed())
980
- state = "closed";
981
- notifyProgress();
982
- })();
983
- worker.catch((error) => {
984
- markFailure(error, "close");
985
- physicalClosed = true;
986
- notifyProgress();
987
- });
988
- const refuse = (reason) => {
989
- refused += 1;
990
- refusals[reason] += 1;
991
- return DiagnosticJournalSubmitResultSchema.parse({ outcome: "refused", reason });
992
- };
993
- const waitUntil = (predicate, options) => {
994
- const timeoutMs = waitBudget(options);
995
- if (predicate())
996
- return Promise.resolve("settled");
997
- if (options.signal?.aborted)
998
- return Promise.resolve("cancelled");
999
- return new Promise((resolve2) => {
1000
- let settled = false;
1001
- let timer;
1002
- const finish = (outcome) => {
1003
- if (settled)
1004
- return;
1005
- settled = true;
1006
- progressWaiters.delete(onProgress);
1007
- options.signal?.removeEventListener("abort", onAbort);
1008
- if (timer)
1009
- clearTimeout(timer);
1010
- resolve2(outcome);
1011
- };
1012
- const onProgress = () => {
1013
- if (predicate())
1014
- finish("settled");
1015
- };
1016
- const onAbort = () => finish("cancelled");
1017
- progressWaiters.add(onProgress);
1018
- options.signal?.addEventListener("abort", onAbort, { once: true });
1019
- if (timeoutMs !== undefined)
1020
- timer = setTimeout(() => finish("timed-out"), timeoutMs);
1021
- onProgress();
1022
- });
1023
- };
1024
- const getStatus = () => {
1025
- const file = storage.snapshot();
1026
- return DiagnosticJournalStatusSchema.parse({
1027
- state,
1028
- epoch: config.epoch,
1029
- limits,
1030
- lock: config.lock,
1031
- received,
1032
- accepted,
1033
- refused,
1034
- refusals,
1035
- written,
1036
- failedRecords,
1037
- pendingItems,
1038
- pendingBytes,
1039
- inFlight,
1040
- rotations: file.rotations,
1041
- rotationFailures,
1042
- partialTails: file.partialTails,
1043
- currentFileBytes: file.currentFileBytes,
1044
- retainedFiles: file.retainedFiles,
1045
- ...lastAcceptedSequence > 0 && { lastAcceptedSequence },
1046
- ...lastWrittenSequence > 0 && { lastWrittenSequence },
1047
- ...lastSettledSequence > 0 && { lastSettledSequence },
1048
- ...lastFailure && { lastFailure }
1049
- });
1050
- };
1051
- return {
1052
- submit(event) {
1053
- received += 1;
1054
- if (state === "failed")
1055
- return refuse("failed");
1056
- if (state !== "open")
1057
- return refuse("closed");
1058
- const parsed = config.eventSchema.safeParse(event);
1059
- if (!parsed.success)
1060
- return refuse("invalid");
1061
- const json = z3.json().safeParse(parsed.data);
1062
- if (!json.success)
1063
- return refuse("invalid");
1064
- const payload = encoder.encode(JSON.stringify(json.data));
1065
- if (payload.byteLength > limits.maxEventBytes)
1066
- return refuse("oversized");
1067
- if (pendingItems >= limits.maxPendingItems)
1068
- return refuse("item-capacity");
1069
- const sequence = lastAcceptedSequence + 1;
1070
- const frame = DiagnosticJournalFrameSchema.parse({
1071
- schemaVersion: 1,
1072
- epoch: config.epoch,
1073
- sequence,
1074
- event: json.data
1075
- });
1076
- const bytes = encoder.encode(`${JSON.stringify(frame)}
1077
- `);
1078
- if (bytes.byteLength > limits.maxFileBytes)
1079
- return refuse("oversized");
1080
- if (pendingBytes + bytes.byteLength > limits.maxPendingBytes) {
1081
- return refuse("byte-capacity");
1082
- }
1083
- const offered = channel.offer({ sequence, bytes });
1084
- if (offered.outcome === "refused") {
1085
- return refuse(offered.reason === "not-open" ? "closed" : "item-capacity");
1086
- }
1087
- accepted += 1;
1088
- pendingItems += 1;
1089
- pendingBytes += bytes.byteLength;
1090
- lastAcceptedSequence = sequence;
1091
- return DiagnosticJournalSubmitResultSchema.parse({
1092
- outcome: "accepted",
1093
- epoch: config.epoch,
1094
- sequence
1095
- });
1096
- },
1097
- async flush(options = {}) {
1098
- const throughSequence = lastAcceptedSequence;
1099
- const outcome = await waitUntil(() => lastSettledSequence >= throughSequence, options);
1100
- return DiagnosticJournalWaitResultSchema.parse({
1101
- outcome,
1102
- state,
1103
- throughSequence,
1104
- settledSequence: lastSettledSequence
1105
- });
1106
- },
1107
- getStatus,
1108
- async close(options = {}) {
1109
- if (state === "open") {
1110
- state = "draining";
1111
- channel.close({ mode: "drain" });
1112
- }
1113
- const wait = await waitUntil(() => physicalClosed, options);
1114
- return DiagnosticJournalCloseResultSchema.parse({
1115
- outcome: wait === "settled" ? "closed" : wait,
1116
- state,
1117
- pendingItems
1118
- });
1119
- }
1120
- };
1121
1012
  }
1122
1013
 
1123
- // src/application/diagnostic-journal.ts
1124
- async function createDiagnosticJournal(config) {
1125
- const limits = DiagnosticJournalLimitsSchema.parse(config.limits);
1126
- const lock = DiagnosticJournalLockPolicySchema.parse(config.lock ?? "refuse");
1127
- const storage = await createRotatingDiagnosticJournalStorage({
1128
- path: config.path,
1129
- maxFileBytes: limits.maxFileBytes,
1130
- maxFiles: limits.maxFiles,
1131
- mode: parseDiagnosticJournalMode(config.mode),
1132
- lock,
1133
- ...config.machineIdentity !== undefined && { machineIdentity: config.machineIdentity }
1134
- });
1135
- return createDiagnosticJournalManager({
1136
- eventSchema: config.eventSchema,
1137
- epoch: randomUUID(),
1138
- limits,
1139
- lock: { policy: lock, reclaimedStale: storage.reclaimedStale },
1140
- ...config.onFailure && { onFailure: config.onFailure }
1141
- }, storage);
1142
- }
1143
1014
  // src/application/events.ts
1144
- import { z as z4 } from "zod";
1145
- var ApplicationLifecycleEventSchema = z4.object({
1146
- type: z4.literal("application-state"),
1015
+ var ApplicationLifecycleEventSchema = z7.object({
1016
+ type: z7.literal("application-state"),
1147
1017
  applicationId: ApplicationIdSchema,
1148
- epoch: z4.string().uuid(),
1149
- revision: z4.number().int().nonnegative(),
1018
+ epoch: z7.string().uuid(),
1019
+ revision: z7.number().int().nonnegative(),
1150
1020
  lifecycle: ApplicationLifecycleSchema,
1151
1021
  health: ApplicationHealthSchema,
1152
- ready: z4.boolean(),
1153
- capturedAt: z4.string().datetime({ offset: true }),
1154
- resources: z4.array(ManagedResourceSnapshotSchema).readonly()
1022
+ ready: z7.boolean(),
1023
+ capturedAt: z7.string().datetime({ offset: true }),
1024
+ resources: z7.array(ManagedResourceSnapshotSchema).readonly()
1155
1025
  }).strict().readonly();
1156
1026
  function applicationLifecycleEvent(snapshot) {
1157
1027
  return ApplicationLifecycleEventSchema.parse({
@@ -1182,13 +1052,13 @@ function createApplicationEventSink(config) {
1182
1052
  };
1183
1053
  }
1184
1054
  // src/application/health.ts
1185
- import { z as z5 } from "zod";
1186
- var ApplicationHealthHandlerOptionsSchema = z5.object({
1187
- kind: z5.enum(["liveness", "readiness"]),
1188
- retryAfterSeconds: z5.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)
1189
1059
  });
1190
- var ApplicationOperationalHandlersOptionsSchema = z5.object({
1191
- retryAfterSeconds: z5.number().int().nonnegative().default(5)
1060
+ var ApplicationOperationalHandlersOptionsSchema = z8.object({
1061
+ retryAfterSeconds: z8.number().int().nonnegative().default(5)
1192
1062
  });
1193
1063
  function createApplicationHealthHandler(application, options) {
1194
1064
  const parsed = ApplicationHealthHandlerOptionsSchema.parse(options);
@@ -1215,6 +1085,901 @@ function createApplicationOperationalHandlers(application, options = {}) {
1215
1085
  })
1216
1086
  };
1217
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
+ }
1218
1983
  // src/application/keyspace.ts
1219
1984
  function defineKeyspace(name, declaration) {
1220
1985
  if (name.trim() === "") {
@@ -1238,7 +2003,7 @@ function keyspaceResource(declaration, config) {
1238
2003
  id: config.id ?? `keyspace:${declaration.name}`,
1239
2004
  ...config.dependsOn && { dependsOn: config.dependsOn },
1240
2005
  async start(_context) {
1241
- await machine.load();
2006
+ await machine.open();
1242
2007
  return { value: machine.keyspace };
1243
2008
  },
1244
2009
  stopAdmission: machine.stopAdmission,
@@ -1251,9 +2016,14 @@ function keyspaceMachine(declaration, config) {
1251
2016
  const maxPending = config.maxPendingWrites ?? 1024;
1252
2017
  const records = new Map;
1253
2018
  let admitting = true;
2019
+ let backend;
2020
+ const currentBackend = () => {
2021
+ backend ??= typeof config.backend === "function" ? config.backend() : config.backend;
2022
+ return backend;
2023
+ };
1254
2024
  let pending = 0;
1255
2025
  let chain = Promise.resolve();
1256
- function run2(work) {
2026
+ function run(work) {
1257
2027
  if (!admitting) {
1258
2028
  return Promise.reject(new Error(`[stitchkit] keyspace "${declaration.name}" is shutting down and is not accepting writes.`));
1259
2029
  }
@@ -1280,8 +2050,8 @@ function keyspaceMachine(declaration, config) {
1280
2050
  put(value) {
1281
2051
  const parsed = declaration.schema.parse(value);
1282
2052
  const key = declaration.key(parsed);
1283
- return run2(async () => {
1284
- await config.backend.put(key, parsed);
2053
+ return run(async () => {
2054
+ await currentBackend().put(key, parsed);
1285
2055
  records.set(key, parsed);
1286
2056
  config.onChanged?.({
1287
2057
  keyspace: declaration.name,
@@ -1292,8 +2062,8 @@ function keyspaceMachine(declaration, config) {
1292
2062
  });
1293
2063
  },
1294
2064
  delete(key) {
1295
- return run2(async () => {
1296
- await config.backend.delete(key);
2065
+ return run(async () => {
2066
+ await currentBackend().delete(key);
1297
2067
  records.delete(key);
1298
2068
  config.onChanged?.({ keyspace: declaration.name, key, change: "delete" });
1299
2069
  });
@@ -1307,11 +2077,19 @@ function keyspaceMachine(declaration, config) {
1307
2077
  return {
1308
2078
  keyspace,
1309
2079
  async load() {
1310
- for (const stored of await config.backend.load()) {
2080
+ for (const stored of await currentBackend().load()) {
1311
2081
  const value = declaration.schema.parse(stored);
1312
2082
  records.set(declaration.key(value), value);
1313
2083
  }
1314
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
+ },
1315
2093
  stopAdmission() {
1316
2094
  admitting = false;
1317
2095
  },
@@ -1320,7 +2098,7 @@ function keyspaceMachine(declaration, config) {
1320
2098
  },
1321
2099
  async close() {
1322
2100
  reportUnwritten();
1323
- await config.backend.close?.();
2101
+ await backend?.close?.();
1324
2102
  },
1325
2103
  force() {
1326
2104
  reportUnwritten();
@@ -1370,6 +2148,308 @@ function sqliteKeyspaceBackend(declaration, config) {
1370
2148
  }
1371
2149
  };
1372
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
+ }
1373
2453
  // src/application/server-resource.ts
1374
2454
  function managedServerResource(config) {
1375
2455
  let shutdownPromise;
@@ -1411,6 +2491,11 @@ function managedServerResource(config) {
1411
2491
  ...config.dependsOn && { dependsOn: config.dependsOn },
1412
2492
  ...config.required !== undefined && { required: config.required },
1413
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;
1414
2499
  startAttempted = true;
1415
2500
  started = await resolveServer(context);
1416
2501
  return { value: started };
@@ -1429,109 +2514,6 @@ function managedServerResource(config) {
1429
2514
  }
1430
2515
  });
1431
2516
  }
1432
- // src/browser/resumable.ts
1433
- import { z as z6 } from "zod";
1434
- var PositiveSafeIntegerSchema3 = z6.number().int().positive().safe();
1435
- var BackoffPolicySchema = z6.object({
1436
- minDelayMs: PositiveSafeIntegerSchema3,
1437
- maxDelayMs: PositiveSafeIntegerSchema3,
1438
- jitter: z6.number().min(0).max(1)
1439
- }).strict().readonly().refine((policy) => policy.maxDelayMs >= policy.minDelayMs, {
1440
- message: "maxDelayMs must be at least minDelayMs"
1441
- });
1442
- function createBackoff(policy, random = Math.random) {
1443
- const { minDelayMs, maxDelayMs, jitter } = BackoffPolicySchema.parse(policy);
1444
- let attempt = 0;
1445
- return {
1446
- next() {
1447
- const exponential = Math.min(maxDelayMs, minDelayMs * 2 ** attempt);
1448
- attempt += 1;
1449
- const spread = exponential * jitter * random();
1450
- return Math.max(1, Math.round(exponential - spread));
1451
- },
1452
- reset() {
1453
- attempt = 0;
1454
- }
1455
- };
1456
- }
1457
-
1458
- // src/live/watch-contract.ts
1459
- import { z as z8 } from "zod";
1460
-
1461
- // src/browser/live-state.ts
1462
- import { z as z7 } from "zod";
1463
- var PositiveSafeIntegerSchema4 = z7.number().int().positive().safe();
1464
- var LiveStatePhaseSchema = z7.enum([
1465
- "idle",
1466
- "opening",
1467
- "live",
1468
- "resync-required",
1469
- "unavailable",
1470
- "closed"
1471
- ]);
1472
- var LiveStateStopReasonSchema = z7.enum([
1473
- "gap",
1474
- "buffer-overflow",
1475
- "source-unavailable",
1476
- "source-error",
1477
- "controller-error",
1478
- "controller-capacity"
1479
- ]);
1480
- var LiveStateControllerStatusSchema = z7.object({
1481
- phase: LiveStatePhaseSchema,
1482
- generation: z7.number().int().nonnegative(),
1483
- hasValue: z7.boolean(),
1484
- bufferedEvents: z7.number().int().nonnegative(),
1485
- bufferedBytes: z7.number().int().nonnegative(),
1486
- receivedEvents: z7.number().int().nonnegative(),
1487
- appliedEvents: z7.number().int().nonnegative(),
1488
- duplicateEvents: z7.number().int().nonnegative(),
1489
- gapEvents: z7.number().int().nonnegative(),
1490
- refusedEvents: z7.number().int().nonnegative(),
1491
- reason: LiveStateStopReasonSchema.optional()
1492
- }).strict().readonly();
1493
-
1494
- // src/live/watch-contract.ts
1495
- var WatchKeySchema = z8.object({
1496
- service: z8.string().min(1),
1497
- action: z8.string().min(1),
1498
- digest: z8.string().min(1)
1499
- }).strict().readonly();
1500
- var WatchOpenSchema = z8.object({ key: WatchKeySchema, args: z8.unknown() }).readonly();
1501
- var WatchAcceptedSchema = z8.object({
1502
- accepted: z8.boolean(),
1503
- reason: z8.string().optional()
1504
- }).strict().readonly();
1505
- var WatchValueSchema = z8.object({
1506
- key: WatchKeySchema,
1507
- revision: z8.number().int().nonnegative(),
1508
- value: z8.unknown()
1509
- }).readonly();
1510
- var WatchStateSchema = z8.object({
1511
- key: WatchKeySchema,
1512
- phase: LiveStatePhaseSchema,
1513
- reason: LiveStateStopReasonSchema.optional(),
1514
- code: z8.string().optional(),
1515
- message: z8.string().optional()
1516
- }).readonly();
1517
- var WATCH_OPEN = "stitchkit.watch.open";
1518
- var WATCH_CLOSE = "stitchkit.watch.close";
1519
- var WATCH_VALUE = "stitchkit.watch.value";
1520
- var WATCH_STATE = "stitchkit.watch.state";
1521
- var watchContract = {
1522
- serverToClient: {
1523
- [WATCH_VALUE]: { args: z8.tuple([WatchValueSchema]) },
1524
- [WATCH_STATE]: { args: z8.tuple([WatchStateSchema]) }
1525
- },
1526
- clientToServer: {
1527
- [WATCH_OPEN]: { args: z8.tuple([WatchOpenSchema]), ack: WatchAcceptedSchema },
1528
- [WATCH_CLOSE]: { args: z8.tuple([z8.object({ key: WatchKeySchema }).readonly()]) }
1529
- }
1530
- };
1531
- function watchKeyString(key) {
1532
- return `${key.service}/${key.action}/${key.digest}`;
1533
- }
1534
-
1535
2517
  // src/application/watch-hub.ts
1536
2518
  var DEFAULT_BACKOFF = { minDelayMs: 250, maxDelayMs: 30000, jitter: 0.2 };
1537
2519
  function watchKey(operation, args) {
@@ -1755,7 +2737,7 @@ export {
1755
2737
  defineKeyspace,
1756
2738
  createWatchHub,
1757
2739
  createManagedSchedule,
1758
- createDiagnosticJournal,
2740
+ createDecisionPipeline,
1759
2741
  createCreditWindow,
1760
2742
  createBoundedChannel,
1761
2743
  createBoundedAdmission,
@@ -1766,6 +2748,7 @@ export {
1766
2748
  createApplication,
1767
2749
  createActivityProjection,
1768
2750
  applicationLifecycleEvent,
2751
+ PolicyDecisionSchema,
1769
2752
  ManagedScheduleStatusSchema,
1770
2753
  ManagedScheduleOverlapSchema,
1771
2754
  ManagedScheduleErrorPolicySchema,
@@ -1782,6 +2765,8 @@ export {
1782
2765
  DiagnosticJournalFrameSchema,
1783
2766
  DiagnosticJournalFailurePhaseSchema,
1784
2767
  DiagnosticJournalCloseResultSchema,
2768
+ DecisionUndecidedError,
2769
+ DecisionPolicyError,
1785
2770
  CreditWindowSnapshotSchema,
1786
2771
  BoundedRateBudgetSchema,
1787
2772
  BoundedOperationWaitError,
@@ -1801,6 +2786,9 @@ export {
1801
2786
  ApplicationShutdownResultSchema,
1802
2787
  ApplicationShutdownOptionsSchema,
1803
2788
  ApplicationShutdownBudgetSchema,
2789
+ ApplicationRestartResultSchema,
2790
+ ApplicationRestartOutcomeSchema,
2791
+ ApplicationRestartInputSchema,
1804
2792
  ApplicationResourceShutdownSchema,
1805
2793
  ApplicationOperationalHandlersOptionsSchema,
1806
2794
  ApplicationLifecycleSchema,