stitchkit 0.100.0 → 0.100.1

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/CHANGELOG.md +80 -1
  2. package/dist/agent-runtime/invocation-attempt.d.ts +6 -0
  3. package/dist/agent-runtime/invocation-attempt.d.ts.map +1 -1
  4. package/dist/agent-runtime/invocation-payload.d.ts +2 -0
  5. package/dist/agent-runtime/invocation-payload.d.ts.map +1 -1
  6. package/dist/agent-runtime/invocation-read.d.ts.map +1 -1
  7. package/dist/agent-runtime/invocations.d.ts +6 -0
  8. package/dist/agent-runtime/invocations.d.ts.map +1 -1
  9. package/dist/agent-runtime/projections.d.ts.map +1 -1
  10. package/dist/agent-runtime/schedule-dispatch.d.ts +9 -2
  11. package/dist/agent-runtime/schedule-dispatch.d.ts.map +1 -1
  12. package/dist/agent-runtime/schedule-records.d.ts +8 -2
  13. package/dist/agent-runtime/schedule-records.d.ts.map +1 -1
  14. package/dist/agent-runtime/schedules.d.ts +5 -0
  15. package/dist/agent-runtime/schedules.d.ts.map +1 -1
  16. package/dist/agent-runtime/sqlite-events.d.ts.map +1 -1
  17. package/dist/agent-runtime/store-create.d.ts.map +1 -1
  18. package/dist/agent-runtime/store-driver-contract.d.ts +9 -0
  19. package/dist/agent-runtime/store-driver-contract.d.ts.map +1 -1
  20. package/dist/agent-runtime/store-event-once.d.ts +9 -1
  21. package/dist/agent-runtime/store-event-once.d.ts.map +1 -1
  22. package/dist/agent-runtime/store-memory.d.ts.map +1 -1
  23. package/dist/agent-runtime/store.d.ts +6 -0
  24. package/dist/agent-runtime/store.d.ts.map +1 -1
  25. package/dist/application/change-subscription.d.ts.map +1 -1
  26. package/dist/application/notification-delivery.d.ts +24 -1
  27. package/dist/application/notification-delivery.d.ts.map +1 -1
  28. package/dist/application/notification-outbox-state.d.ts +4 -0
  29. package/dist/application/notification-outbox-state.d.ts.map +1 -1
  30. package/dist/application/notification-outbox.d.ts.map +1 -1
  31. package/dist/application/watch-hub-core.d.ts +14 -79
  32. package/dist/application/watch-hub-core.d.ts.map +1 -1
  33. package/dist/application/watch-hub-source.d.ts +98 -0
  34. package/dist/application/watch-hub-source.d.ts.map +1 -0
  35. package/dist/browser/session-credentials.d.ts +6 -0
  36. package/dist/browser/session-credentials.d.ts.map +1 -1
  37. package/dist/browser/session.d.ts +3 -1
  38. package/dist/browser/session.d.ts.map +1 -1
  39. package/dist/entrypoints/agent-runtime/harness.js +1 -1
  40. package/dist/entrypoints/agent-runtime/sqlite/bun.js +2 -2
  41. package/dist/entrypoints/agent-runtime/sqlite/node.js +2 -2
  42. package/dist/entrypoints/agent-runtime/testing.js +2 -2
  43. package/dist/entrypoints/agent-runtime.js +53 -19
  44. package/dist/entrypoints/application/diagnostic-journal.js +1 -1
  45. package/dist/entrypoints/application.d.ts +1 -1
  46. package/dist/entrypoints/application.d.ts.map +1 -1
  47. package/dist/entrypoints/application.js +275 -189
  48. package/dist/entrypoints/files.js +8 -2
  49. package/dist/entrypoints/index.js +33 -6
  50. package/dist/entrypoints/live.js +16 -12
  51. package/dist/entrypoints/node.js +1 -1
  52. package/dist/entrypoints/server.js +4 -4
  53. package/dist/{index-s84he4v7.js → index-10x2hbx0.js} +4 -1
  54. package/dist/{index-js7gqrb1.js → index-c6bxbsnc.js} +5 -1
  55. package/dist/{index-y9qn426m.js → index-eaysmdtq.js} +10 -1
  56. package/dist/{index-xwhjjw2x.js → index-f4fx2tza.js} +119 -49
  57. package/dist/{index-p3vsv1fb.js → index-hadmp193.js} +14 -5
  58. package/dist/{index-0b1cnze2.js → index-vqr7f7v8.js} +31 -15
  59. package/dist/internal/coalesced-task.d.ts +2 -0
  60. package/dist/internal/coalesced-task.d.ts.map +1 -1
  61. package/dist/internal/process-instance.d.ts.map +1 -1
  62. package/dist/internal/with-exclusive-lock.d.ts.map +1 -1
  63. package/dist/server/realtime.d.ts.map +1 -1
  64. package/llms/agent-runtime.2.txt +13 -5
  65. package/llms/agent-runtime.3.txt +9 -4
  66. package/llms/agent-runtime.txt +8 -3
  67. package/llms/application.2.txt +21 -5
  68. package/llms/live.txt +2 -1
  69. package/llms/server.3.txt +22 -0
  70. package/llms/stitchkit.3.txt +9 -5
  71. package/llms/upgrading-0.74.0-0.100.0.txt +15 -3
  72. package/llms-full.txt +99 -26
  73. package/llms.txt +5 -5
  74. package/package.json +1 -1
@@ -58,9 +58,6 @@ import {
58
58
  defineManagedResource,
59
59
  managedResourceDependencyId
60
60
  } from "../index-2k4yrqkc.js";
61
- import {
62
- withDeadline
63
- } from "../index-b9aefrqd.js";
64
61
  import"../index-3z73fh2c.js";
65
62
  import {
66
63
  createBoundedSinkManager
@@ -69,6 +66,9 @@ import {
69
66
  backoffDelay,
70
67
  createBackoff
71
68
  } from "../index-ngfhvfpd.js";
69
+ import {
70
+ withDeadline
71
+ } from "../index-b9aefrqd.js";
72
72
  import {
73
73
  deltaWins,
74
74
  diff,
@@ -90,7 +90,9 @@ import {
90
90
  import {
91
91
  argumentsDigest
92
92
  } from "../index-0b1ess5e.js";
93
- import"../index-2vtcmrj9.js";
93
+ import {
94
+ AppError
95
+ } from "../index-2vtcmrj9.js";
94
96
  import {
95
97
  serializeCanonicalJson
96
98
  } from "../index-k5hsfc3j.js";
@@ -528,6 +530,9 @@ function createCoalescedTask(config) {
528
530
  get running() {
529
531
  return running;
530
532
  },
533
+ get pending() {
534
+ return dirty;
535
+ },
531
536
  trigger() {
532
537
  if (closed)
533
538
  return;
@@ -651,7 +656,7 @@ class ChangeSubscription {
651
656
  this.connection = undefined;
652
657
  this.attempt?.abort();
653
658
  this.report(error);
654
- this.release(handle).then(() => this.schedule(), (cause) => this.report(cause));
659
+ this.release(handle).catch((cause) => this.report(cause)).then(() => this.schedule());
655
660
  }
656
661
  open() {
657
662
  if (this.opening)
@@ -722,7 +727,6 @@ class ChangeSubscription {
722
727
  await this.release(handle);
723
728
  } catch (cause) {
724
729
  this.report(cause);
725
- return;
726
730
  }
727
731
  }
728
732
  if (!lifetime.signal.aborted) {
@@ -1110,13 +1114,97 @@ function sqliteKeyspaceBackend(declaration, config) {
1110
1114
  };
1111
1115
  }
1112
1116
  // src/application/notification-delivery.ts
1117
+ import { z as z7 } from "zod";
1118
+
1119
+ // src/application/notification-outbox-state.ts
1113
1120
  import { z as z6 } from "zod";
1114
- var NotificationDeliveryStateSchema = z6.object({
1115
- version: z6.string().min(1).max(128),
1116
- actions: z6.array(z6.string().min(1).max(128)).min(1).max(128),
1117
- receipts: z6.array(z6.object({ action: z6.string(), receipt: z6.json() }).strict()).max(128),
1118
- projectedAt: z6.string().datetime({ offset: true }).optional()
1121
+ var sizeOf = (value) => measureSize(value).responseBytes;
1122
+ var DEFAULT_OUTBOX_BACKOFF = {
1123
+ minDelayMs: 1000,
1124
+ maxDelayMs: 60000,
1125
+ jitter: 0
1126
+ };
1127
+ function resolveOutboxLimits(config) {
1128
+ const pollIntervalMs = z6.number().int().min(10).parse(config.pollIntervalMs ?? 1000);
1129
+ const leaseMs = z6.number().int().min(100).parse(config.leaseMs ?? 30000);
1130
+ const maxAttempts = z6.number().int().min(1).max(1e4).parse(config.maxAttempts ?? 100);
1131
+ const maxQueue = z6.number().int().min(1).max(1e5).parse(config.maxQueue ?? 1000);
1132
+ const maxStateBytes = z6.number().int().min(1024).parse(config.maxStateBytes ?? 1024 * 1024);
1133
+ const retainReceipts = z6.number().int().min(0).max(1e5).parse(config.retainReceipts ?? 1000);
1134
+ return { pollIntervalMs, leaseMs, maxAttempts, maxQueue, maxStateBytes, retainReceipts };
1135
+ }
1136
+
1137
+ class OutboxStateLimitError extends Error {
1138
+ constructor(maxStateBytes) {
1139
+ super(`[stitchkit] notification outbox state limit (${maxStateBytes} bytes) exceeded`);
1140
+ this.name = "OutboxStateLimitError";
1141
+ }
1142
+ }
1143
+ function boundedOutboxState(parsed, maxQueue, retainReceipts, maxStateBytes) {
1144
+ if (parsed.queue.length > maxQueue) {
1145
+ throw new Error(`[stitchkit] notification outbox queue limit (${maxQueue}) exceeded`);
1146
+ }
1147
+ let receipts = parsed.receipts.slice(0, retainReceipts);
1148
+ let next = { ...parsed, receipts };
1149
+ while (sizeOf(next) > maxStateBytes && receipts.length > 0) {
1150
+ receipts = receipts.slice(0, -1);
1151
+ next = { ...parsed, receipts };
1152
+ }
1153
+ if (sizeOf(next) > maxStateBytes)
1154
+ throw new OutboxStateLimitError(maxStateBytes);
1155
+ return next;
1156
+ }
1157
+ function dropReason(classification, attempts, maxAttempts) {
1158
+ if (classification.recipientUnreachable === true)
1159
+ return "recipient-unreachable";
1160
+ if (!classification.retryable)
1161
+ return "terminal";
1162
+ return attempts >= maxAttempts ? "attempt-limit" : null;
1163
+ }
1164
+ var RetryDelaySchema = z6.number().nonnegative().max(24 * 60 * 60 * 1000);
1165
+ var NO_LEASE = { leaseOwner: null, leaseId: null, leaseUntil: null };
1166
+ function claimDue(queue, now, ownerId, leaseMs) {
1167
+ const candidate = [...queue].sort((left, right) => Date.parse(left.createdAt) - Date.parse(right.createdAt)).find((item) => Date.parse(item.nextAttemptAt) <= now.getTime() && (item.leaseUntil === null || Date.parse(item.leaseUntil) <= now.getTime()));
1168
+ if (!candidate)
1169
+ return null;
1170
+ return {
1171
+ ...candidate,
1172
+ attempts: candidate.attempts + 1,
1173
+ leaseOwner: ownerId,
1174
+ leaseId: crypto.randomUUID(),
1175
+ leaseUntil: new Date(now.getTime() + leaseMs).toISOString()
1176
+ };
1177
+ }
1178
+
1179
+ // src/application/notification-delivery.ts
1180
+ var NotificationDeliveryStateSchema = z7.object({
1181
+ version: z7.string().min(1).max(128),
1182
+ actions: z7.array(z7.string().min(1).max(128)).min(1).max(128),
1183
+ receipts: z7.array(z7.object({
1184
+ action: z7.string(),
1185
+ receipt: z7.json(),
1186
+ unrecorded: z7.enum(["not-json", "too-large"]).optional()
1187
+ }).strict()).max(128),
1188
+ projectedAt: z7.string().datetime({ offset: true }).optional()
1119
1189
  }).strict().refine((value) => new Set(value.actions).size === value.actions.length && new Set(value.receipts.map((receipt) => receipt.action)).size === value.receipts.length && value.receipts.every((receipt) => value.actions.includes(receipt.action)), "Invalid action plan or receipts");
1190
+
1191
+ class NotificationPlanVersionError extends Error {
1192
+ version;
1193
+ constructor(version) {
1194
+ super(`Notification action plan version "${version}" has no executor`);
1195
+ this.name = "NotificationPlanVersionError";
1196
+ this.version = version;
1197
+ }
1198
+ }
1199
+ function projectionFor(config, version) {
1200
+ if (version === config.version)
1201
+ return config.project;
1202
+ return config.retiredVersions && Object.hasOwn(config.retiredVersions, version) ? config.retiredVersions[version] : undefined;
1203
+ }
1204
+ function recordable(result) {
1205
+ const parsed = z7.json().safeParse(result ?? null);
1206
+ return parsed.success ? { receipt: parsed.data } : { receipt: null, unrecorded: "not-json" };
1207
+ }
1120
1208
  function notificationDeliveryPlan(config, payload) {
1121
1209
  return NotificationDeliveryStateSchema.parse({
1122
1210
  version: config.version,
@@ -1127,8 +1215,11 @@ function notificationDeliveryPlan(config, payload) {
1127
1215
  async function deliverNotification(input) {
1128
1216
  const { item, config } = input;
1129
1217
  const plan = item.delivery;
1130
- if (!plan || plan.version !== config.version)
1131
- throw new Error("Notification action plan version does not match the executor");
1218
+ if (!plan)
1219
+ throw new Error("Notification has no action plan");
1220
+ const project = projectionFor(config, plan.version);
1221
+ if (!project)
1222
+ throw new NotificationPlanVersionError(plan.version);
1132
1223
  const owns = (current2) => current2.key === item.key && current2.leaseId === item.leaseId;
1133
1224
  const checkpoint = async (change) => input.store.update((state) => {
1134
1225
  if (!state)
@@ -1153,7 +1244,7 @@ async function deliverNotification(input) {
1153
1244
  for (const action of plan.actions) {
1154
1245
  if (!input.active())
1155
1246
  return false;
1156
- if (plan.receipts.some((receipt2) => receipt2.action === action))
1247
+ if (plan.receipts.some((receipt) => receipt.action === action))
1157
1248
  continue;
1158
1249
  if (!await checkpoint((value) => value))
1159
1250
  return false;
@@ -1162,11 +1253,24 @@ async function deliverNotification(input) {
1162
1253
  action,
1163
1254
  idempotencyKey: JSON.stringify([item.key, plan.version, action])
1164
1255
  });
1165
- const receipt = z6.json().parse(result ?? null);
1166
- if (!await checkpoint((value) => ({
1167
- ...value,
1168
- receipts: [...value.receipts, { action, receipt }]
1169
- })))
1256
+ const entry = recordable(result);
1257
+ const confirm = (receipt) => checkpoint((value) => ({ ...value, receipts: [...value.receipts, receipt] }));
1258
+ let confirmed;
1259
+ let dropped;
1260
+ try {
1261
+ confirmed = await confirm({ action, ...entry });
1262
+ } catch (error) {
1263
+ if (entry.unrecorded || !(error instanceof OutboxStateLimitError))
1264
+ throw error;
1265
+ dropped = error;
1266
+ confirmed = await confirm({ action, receipt: null, unrecorded: "too-large" });
1267
+ }
1268
+ if (dropped)
1269
+ await input.report(dropped);
1270
+ if (entry.unrecorded) {
1271
+ await input.report(new Error(`Notification action "${action}" returned a receipt that is not JSON`));
1272
+ }
1273
+ if (!confirmed)
1170
1274
  return false;
1171
1275
  }
1172
1276
  if (!input.active())
@@ -1176,7 +1280,7 @@ async function deliverNotification(input) {
1176
1280
  if (!live?.delivery)
1177
1281
  return false;
1178
1282
  if (live.delivery.projectedAt === undefined) {
1179
- await config.project(notification, live.delivery.receipts);
1283
+ await project(notification, live.delivery.receipts);
1180
1284
  if (!await checkpoint((value) => ({ ...value, projectedAt: input.clock().toISOString() })))
1181
1285
  return false;
1182
1286
  }
@@ -1184,62 +1288,6 @@ async function deliverNotification(input) {
1184
1288
  }
1185
1289
  // src/application/notification-outbox.ts
1186
1290
  import { z as z8 } from "zod";
1187
-
1188
- // src/application/notification-outbox-state.ts
1189
- import { z as z7 } from "zod";
1190
- var sizeOf = (value) => measureSize(value).responseBytes;
1191
- var DEFAULT_OUTBOX_BACKOFF = {
1192
- minDelayMs: 1000,
1193
- maxDelayMs: 60000,
1194
- jitter: 0
1195
- };
1196
- function resolveOutboxLimits(config) {
1197
- const pollIntervalMs = z7.number().int().min(10).parse(config.pollIntervalMs ?? 1000);
1198
- const leaseMs = z7.number().int().min(100).parse(config.leaseMs ?? 30000);
1199
- const maxAttempts = z7.number().int().min(1).max(1e4).parse(config.maxAttempts ?? 100);
1200
- const maxQueue = z7.number().int().min(1).max(1e5).parse(config.maxQueue ?? 1000);
1201
- const maxStateBytes = z7.number().int().min(1024).parse(config.maxStateBytes ?? 1024 * 1024);
1202
- const retainReceipts = z7.number().int().min(0).max(1e5).parse(config.retainReceipts ?? 1000);
1203
- return { pollIntervalMs, leaseMs, maxAttempts, maxQueue, maxStateBytes, retainReceipts };
1204
- }
1205
- function boundedOutboxState(parsed, maxQueue, retainReceipts, maxStateBytes) {
1206
- if (parsed.queue.length > maxQueue) {
1207
- throw new Error(`[stitchkit] notification outbox queue limit (${maxQueue}) exceeded`);
1208
- }
1209
- let receipts = parsed.receipts.slice(0, retainReceipts);
1210
- let next = { ...parsed, receipts };
1211
- while (sizeOf(next) > maxStateBytes && receipts.length > 0) {
1212
- receipts = receipts.slice(0, -1);
1213
- next = { ...parsed, receipts };
1214
- }
1215
- if (sizeOf(next) > maxStateBytes) {
1216
- throw new Error(`[stitchkit] notification outbox state limit (${maxStateBytes} bytes) exceeded`);
1217
- }
1218
- return next;
1219
- }
1220
- function dropReason(classification, attempts, maxAttempts) {
1221
- if (classification.recipientUnreachable === true)
1222
- return "recipient-unreachable";
1223
- if (!classification.retryable)
1224
- return "terminal";
1225
- return attempts >= maxAttempts ? "attempt-limit" : null;
1226
- }
1227
- var RetryDelaySchema = z7.number().nonnegative().max(24 * 60 * 60 * 1000);
1228
- var NO_LEASE = { leaseOwner: null, leaseId: null, leaseUntil: null };
1229
- function claimDue(queue, now, ownerId, leaseMs) {
1230
- const candidate = [...queue].sort((left, right) => Date.parse(left.createdAt) - Date.parse(right.createdAt)).find((item) => Date.parse(item.nextAttemptAt) <= now.getTime() && (item.leaseUntil === null || Date.parse(item.leaseUntil) <= now.getTime()));
1231
- if (!candidate)
1232
- return null;
1233
- return {
1234
- ...candidate,
1235
- attempts: candidate.attempts + 1,
1236
- leaseOwner: ownerId,
1237
- leaseId: crypto.randomUUID(),
1238
- leaseUntil: new Date(now.getTime() + leaseMs).toISOString()
1239
- };
1240
- }
1241
-
1242
- // src/application/notification-outbox.ts
1243
1291
  function notificationOutboxStateSchema(payload) {
1244
1292
  return z8.object({
1245
1293
  schemaVersion: z8.literal(1),
@@ -1271,16 +1319,19 @@ async function reportOutboxError(config, error) {
1271
1319
  await config.onError?.(error);
1272
1320
  } catch {}
1273
1321
  }
1274
- function notificationRejection(config, parse, bounded, clock, backoffMs) {
1322
+ function notificationRejection(config, parse, bounded, clock, backoffMs, pollIntervalMs) {
1275
1323
  const maxAttempts = config.maxAttempts ?? 100;
1276
1324
  return async (claimed, error) => {
1277
- const classification = await config.classify(error);
1325
+ const unknownPlan = error instanceof NotificationPlanVersionError;
1326
+ if (unknownPlan)
1327
+ await reportOutboxError(config, error);
1328
+ const classification = unknownPlan ? { retryable: true } : await config.classify(error);
1278
1329
  return config.store.update((current) => {
1279
1330
  const state = parse(current);
1280
1331
  const live = state.queue.find((item) => item.key === claimed.key && item.leaseId === claimed.leaseId);
1281
1332
  if (!live)
1282
1333
  return { state, result: null };
1283
- const attempts = live.attempts;
1334
+ const attempts = unknownPlan ? Math.max(0, live.attempts - 1) : live.attempts;
1284
1335
  const reason = dropReason(classification, attempts, maxAttempts);
1285
1336
  if (reason) {
1286
1337
  return {
@@ -1291,9 +1342,11 @@ function notificationRejection(config, parse, bounded, clock, backoffMs) {
1291
1342
  result: { item: { ...live, attempts }, error, reason }
1292
1343
  };
1293
1344
  }
1294
- const wait = RetryDelaySchema.parse(backoffMs(attempts));
1345
+ const backoff = RetryDelaySchema.parse(backoffMs(Math.max(1, attempts)));
1346
+ const wait = unknownPlan ? Math.max(backoff, pollIntervalMs) : backoff;
1295
1347
  const retry = {
1296
1348
  ...live,
1349
+ attempts,
1297
1350
  nextAttemptAt: new Date(clock().getTime() + wait).toISOString(),
1298
1351
  ...NO_LEASE
1299
1352
  };
@@ -1387,7 +1440,7 @@ function createNotificationOutbox(config) {
1387
1440
  };
1388
1441
  });
1389
1442
  };
1390
- const reject = notificationRejection(config, parse, bounded, clock, backoffMs);
1443
+ const reject = notificationRejection(config, parse, bounded, clock, backoffMs, pollIntervalMs);
1391
1444
  const flushPass = async () => {
1392
1445
  let delivered = 0;
1393
1446
  while (!interrupted) {
@@ -1404,7 +1457,8 @@ function createNotificationOutbox(config) {
1404
1457
  clock,
1405
1458
  bounded,
1406
1459
  leaseMs,
1407
- active: () => !interrupted
1460
+ active: () => !interrupted,
1461
+ report: (error) => reportOutboxError(config, error)
1408
1462
  });
1409
1463
  if (!finished)
1410
1464
  return delivered;
@@ -2088,6 +2142,78 @@ function managedServerResource(config) {
2088
2142
  }
2089
2143
  });
2090
2144
  }
2145
+ // src/application/watch-hub-source.ts
2146
+ function remember(source, deltaMemoryBytes, revision, value, fingerprint) {
2147
+ if (deltaMemoryBytes === 0)
2148
+ return;
2149
+ const bytes = (JSON.stringify(value) ?? "null").length;
2150
+ if (bytes > deltaMemoryBytes)
2151
+ return;
2152
+ source.history.set(revision, { value, fingerprint, bytes });
2153
+ source.historyBytes += bytes;
2154
+ for (const [oldest, entry] of source.history) {
2155
+ if (source.historyBytes <= deltaMemoryBytes)
2156
+ break;
2157
+ source.history.delete(oldest);
2158
+ source.historyBytes -= entry.bytes;
2159
+ }
2160
+ }
2161
+ function fullFrame(source) {
2162
+ const value = source.value;
2163
+ return {
2164
+ kind: "full",
2165
+ key: source.key,
2166
+ revision: source.revision,
2167
+ fingerprint: source.fingerprint ?? valueFingerprint(value),
2168
+ value
2169
+ };
2170
+ }
2171
+ function frameFor(source, subscriber) {
2172
+ const value = source.value;
2173
+ const full = fullFrame(source);
2174
+ const fingerprint = full.fingerprint;
2175
+ const base = source.baselines.get(subscriber);
2176
+ if (base === undefined)
2177
+ return full;
2178
+ if (base === source.revision) {
2179
+ return { kind: "unchanged", key: source.key, revision: source.revision, fingerprint };
2180
+ }
2181
+ const held = source.history.get(base);
2182
+ if (!held)
2183
+ return full;
2184
+ const delta = diff(held.value, value);
2185
+ if (delta === undefined || !deltaWins(delta, value))
2186
+ return full;
2187
+ return {
2188
+ kind: "delta",
2189
+ key: source.key,
2190
+ revision: source.revision,
2191
+ fingerprint,
2192
+ base,
2193
+ delta
2194
+ };
2195
+ }
2196
+ function adoptBaseline(source, subscriber, have) {
2197
+ if (source.fingerprint === have.fingerprint) {
2198
+ source.baselines.set(subscriber, source.revision);
2199
+ return;
2200
+ }
2201
+ const named = source.history.get(have.revision);
2202
+ if (named?.fingerprint === have.fingerprint) {
2203
+ source.baselines.set(subscriber, have.revision);
2204
+ return;
2205
+ }
2206
+ for (const [revision, entry] of source.history) {
2207
+ if (entry.fingerprint !== have.fingerprint)
2208
+ continue;
2209
+ source.baselines.set(subscriber, revision);
2210
+ return;
2211
+ }
2212
+ }
2213
+ function valueFingerprint(value) {
2214
+ return argumentsDigest({ value });
2215
+ }
2216
+
2091
2217
  // src/application/watch-hub-core.ts
2092
2218
  var DEFAULT_BACKOFF = { minDelayMs: 250, maxDelayMs: 30000, jitter: 0.2 };
2093
2219
 
@@ -2137,82 +2263,24 @@ class WatchHubCore {
2137
2263
  announceState(source, state) {
2138
2264
  source.state = state;
2139
2265
  for (const subscriber of source.subscribers) {
2140
- this.tell(source.key, () => subscriber.state(state));
2141
- }
2142
- }
2143
- remember(source, revision, value, fingerprint) {
2144
- if (this.deltaMemoryBytes === 0)
2145
- return;
2146
- const bytes = (JSON.stringify(value) ?? "null").length;
2147
- if (bytes > this.deltaMemoryBytes)
2148
- return;
2149
- source.history.set(revision, { value, fingerprint, bytes });
2150
- source.historyBytes += bytes;
2151
- for (const [oldest, entry] of source.history) {
2152
- if (source.historyBytes <= this.deltaMemoryBytes)
2153
- break;
2154
- source.history.delete(oldest);
2155
- source.historyBytes -= entry.bytes;
2156
- }
2157
- }
2158
- frameFor(source, subscriber) {
2159
- const value = source.value;
2160
- const fingerprint = source.fingerprint ?? valueFingerprint(value);
2161
- const full = {
2162
- kind: "full",
2163
- key: source.key,
2164
- revision: source.revision,
2165
- fingerprint,
2166
- value
2167
- };
2168
- const base = source.baselines.get(subscriber);
2169
- if (base === undefined)
2170
- return full;
2171
- if (base === source.revision) {
2172
- return { kind: "unchanged", key: source.key, revision: source.revision, fingerprint };
2173
- }
2174
- const held = source.history.get(base);
2175
- if (!held)
2176
- return full;
2177
- const delta = diff(held.value, value);
2178
- if (delta === undefined || !deltaWins(delta, value))
2179
- return full;
2180
- return {
2181
- kind: "delta",
2182
- key: source.key,
2183
- revision: source.revision,
2184
- fingerprint,
2185
- base,
2186
- delta
2187
- };
2188
- }
2189
- adoptBaseline(source, subscriber, have) {
2190
- if (source.fingerprint === have.fingerprint) {
2191
- source.baselines.set(subscriber, source.revision);
2192
- return;
2193
- }
2194
- const named = source.history.get(have.revision);
2195
- if (named?.fingerprint === have.fingerprint) {
2196
- source.baselines.set(subscriber, have.revision);
2197
- return;
2198
- }
2199
- for (const [revision, entry] of source.history) {
2200
- if (entry.fingerprint !== have.fingerprint)
2201
- continue;
2202
- source.baselines.set(subscriber, revision);
2203
- return;
2266
+ for (const key of source.routes.get(subscriber)?.values() ?? []) {
2267
+ this.tell(key, () => subscriber.state({ ...state, key }));
2268
+ }
2204
2269
  }
2205
2270
  }
2206
2271
  deliver(source, subscriber) {
2207
- const frame = this.frameFor(source, subscriber);
2208
- const delivered = this.tell(source.key, () => subscriber.value(frame));
2272
+ const frame = frameFor(source, subscriber);
2273
+ let delivered = true;
2274
+ for (const key of source.routes.get(subscriber)?.values() ?? []) {
2275
+ delivered = this.tell(key, () => subscriber.value({ ...frame, key })) && delivered;
2276
+ }
2209
2277
  if (delivered)
2210
2278
  source.baselines.set(subscriber, source.revision);
2211
2279
  return delivered;
2212
2280
  }
2213
2281
  publish(source, value, fingerprint) {
2214
2282
  if (source.fingerprint !== undefined) {
2215
- this.remember(source, source.revision, source.value, source.fingerprint);
2283
+ remember(source, this.deltaMemoryBytes, source.revision, source.value, source.fingerprint);
2216
2284
  }
2217
2285
  source.revision += 1;
2218
2286
  source.value = value;
@@ -2234,21 +2302,20 @@ class WatchHubCore {
2234
2302
  this.announceState(source, { key: source.key, phase: "live" });
2235
2303
  }
2236
2304
  readFailed(source, error) {
2237
- const code = readErrorCode(error);
2238
2305
  this.announceState(source, {
2239
2306
  key: source.key,
2240
2307
  phase: "unavailable",
2241
2308
  reason: "source-error",
2242
- ...code !== undefined && { code },
2243
- message: error instanceof Error ? error.message : String(error)
2309
+ ...AppError.is(error) ? { code: error.code, message: error.message } : { message: "The watched read failed" }
2244
2310
  });
2245
2311
  this.config.logger?.warn?.("[stitchkit] watched read failed", {
2246
2312
  key: watchKeyString(source.key),
2247
2313
  error
2248
2314
  });
2249
2315
  }
2250
- acquire(operation, key, args, scope) {
2251
- const id = JSON.stringify([scope ?? null, watchKeyString(key)]);
2316
+ acquire(operation, routed, args, scope) {
2317
+ const id = sourceId(scope, routed);
2318
+ const key = sharedKey(routed);
2252
2319
  const existing = this.sources.get(id);
2253
2320
  if (existing) {
2254
2321
  clearTimeout(existing.release);
@@ -2262,6 +2329,7 @@ class WatchHubCore {
2262
2329
  operation,
2263
2330
  args,
2264
2331
  subscribers: new Set,
2332
+ routes: new Map,
2265
2333
  unsubscribes: [],
2266
2334
  revision: 0,
2267
2335
  history: new Map,
@@ -2305,12 +2373,32 @@ class WatchHubCore {
2305
2373
  source.release = setTimeout(drop, this.holdMs);
2306
2374
  source.release.unref?.();
2307
2375
  }
2376
+ unroute(source, subscriber, route) {
2377
+ const routes = source.routes.get(subscriber);
2378
+ routes?.delete(route);
2379
+ if (routes && routes.size > 0)
2380
+ return;
2381
+ source.routes.delete(subscriber);
2382
+ source.subscribers.delete(subscriber);
2383
+ source.baselines.delete(subscriber);
2384
+ this.release(source);
2385
+ }
2308
2386
  attach(subscriber, scope) {
2309
- if (this.held.size >= (this.config.maxSubscribers ?? 1024))
2310
- throw new Error("Watch subscriber capacity exceeded");
2387
+ if (this.held.size >= (this.config.maxSubscribers ?? 1024)) {
2388
+ const refused = {
2389
+ open: () => ({ accepted: false, reason: "Watch subscriber capacity exceeded" }),
2390
+ close: () => {
2391
+ return;
2392
+ },
2393
+ detach: () => {
2394
+ return;
2395
+ }
2396
+ };
2397
+ return refused;
2398
+ }
2311
2399
  if (scope && (!scope.key || scope.key.length > 512))
2312
2400
  throw new Error("Invalid watch admission scope");
2313
- const keys = new Set;
2401
+ const keys = new Map;
2314
2402
  this.held.set(subscriber, keys);
2315
2403
  let detached = false;
2316
2404
  const attached = {
@@ -2326,8 +2414,9 @@ class WatchHubCore {
2326
2414
  reason: `${key.service}.${key.action} is not watchable`
2327
2415
  };
2328
2416
  }
2329
- const id = JSON.stringify([scope?.key ?? null, watchKeyString(key)]);
2330
- if (keys.has(id))
2417
+ const route = watchKeyString(key);
2418
+ const id = sourceId(scope?.key, key);
2419
+ if (keys.has(route))
2331
2420
  return { accepted: true };
2332
2421
  if (keys.size >= this.maxWatches) {
2333
2422
  return {
@@ -2338,52 +2427,50 @@ class WatchHubCore {
2338
2427
  if (!this.sources.has(id) && this.sources.size >= (this.config.maxSources ?? 1024))
2339
2428
  return { accepted: false, reason: "Watch source capacity exceeded" };
2340
2429
  const source = this.acquire(operation, key, args, scope?.key);
2430
+ const alreadyRouted = source.routes.has(subscriber);
2341
2431
  source.subscribers.add(subscriber);
2342
- keys.add(id);
2432
+ const routes = source.routes.get(subscriber) ?? new Map;
2433
+ routes.set(route, key);
2434
+ source.routes.set(subscriber, routes);
2435
+ keys.set(route, id);
2343
2436
  if (have)
2344
- this.adoptBaseline(source, subscriber, have);
2437
+ adoptBaseline(source, subscriber, have);
2345
2438
  try {
2346
2439
  if (source.signature !== undefined) {
2347
- const frame = this.frameFor(source, subscriber);
2348
- subscriber.value(frame);
2440
+ const frame = alreadyRouted && !have ? fullFrame(source) : frameFor(source, subscriber);
2441
+ subscriber.value({ ...frame, key });
2349
2442
  source.baselines.set(subscriber, source.revision);
2350
2443
  }
2351
- subscriber.state(source.state);
2444
+ subscriber.state({ ...source.state, key });
2352
2445
  } catch (error) {
2353
- source.subscribers.delete(subscriber);
2354
- source.baselines.delete(subscriber);
2355
- keys.delete(id);
2356
- this.release(source);
2446
+ keys.delete(route);
2447
+ this.unroute(source, subscriber, route);
2357
2448
  return {
2358
2449
  accepted: false,
2359
2450
  reason: error instanceof Error ? error.message : String(error)
2360
2451
  };
2361
2452
  }
2362
- if (!source.task.running && (source.signature === undefined || source.state.phase !== "live"))
2453
+ if (!source.task.running && (source.signature === undefined || source.state.phase !== "live" || source.task.pending))
2363
2454
  source.task.trigger();
2364
2455
  return { accepted: true };
2365
2456
  },
2366
2457
  close: (key) => {
2367
- const id = JSON.stringify([scope?.key ?? null, watchKeyString(key)]);
2368
- if (!keys.delete(id))
2458
+ const route = watchKeyString(key);
2459
+ const id = keys.get(route);
2460
+ if (id === undefined)
2369
2461
  return;
2462
+ keys.delete(route);
2370
2463
  const source = this.sources.get(id);
2371
- if (!source)
2372
- return;
2373
- source.subscribers.delete(subscriber);
2374
- source.baselines.delete(subscriber);
2375
- this.release(source);
2464
+ if (source)
2465
+ this.unroute(source, subscriber, route);
2376
2466
  },
2377
2467
  detach: () => {
2378
2468
  detached = true;
2379
2469
  scope?.signal.removeEventListener("abort", onAbort);
2380
- for (const id of keys) {
2470
+ for (const [route, id] of keys) {
2381
2471
  const source = this.sources.get(id);
2382
- if (!source)
2383
- continue;
2384
- source.subscribers.delete(subscriber);
2385
- source.baselines.delete(subscriber);
2386
- this.release(source);
2472
+ if (source)
2473
+ this.unroute(source, subscriber, route);
2387
2474
  }
2388
2475
  keys.clear();
2389
2476
  this.held.delete(subscriber);
@@ -2416,14 +2503,12 @@ class WatchHubCore {
2416
2503
  }
2417
2504
  }
2418
2505
  }
2419
- function valueFingerprint(value) {
2420
- return argumentsDigest({ value });
2506
+ function sharedKey(key) {
2507
+ const { instance: _routing, ...shared } = key;
2508
+ return shared;
2421
2509
  }
2422
- function readErrorCode(error) {
2423
- if (typeof error !== "object" || error === null)
2424
- return;
2425
- const code = Reflect.get(error, "code");
2426
- return typeof code === "string" ? code : undefined;
2510
+ function sourceId(scope, key) {
2511
+ return JSON.stringify([scope ?? null, watchKeyString(sharedKey(key))]);
2427
2512
  }
2428
2513
 
2429
2514
  // src/application/watch-hub.ts
@@ -2487,6 +2572,7 @@ export {
2487
2572
  RevisionSignalAdvanceResultSchema,
2488
2573
  PreviousExitSchema,
2489
2574
  PolicyDecisionSchema,
2575
+ NotificationPlanVersionError,
2490
2576
  NotificationDeliveryStateSchema,
2491
2577
  ManagedScheduleStatusSchema,
2492
2578
  ManagedScheduleOverlapSchema,