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.
- package/CHANGELOG.md +80 -1
- package/dist/agent-runtime/invocation-attempt.d.ts +6 -0
- package/dist/agent-runtime/invocation-attempt.d.ts.map +1 -1
- package/dist/agent-runtime/invocation-payload.d.ts +2 -0
- package/dist/agent-runtime/invocation-payload.d.ts.map +1 -1
- package/dist/agent-runtime/invocation-read.d.ts.map +1 -1
- package/dist/agent-runtime/invocations.d.ts +6 -0
- package/dist/agent-runtime/invocations.d.ts.map +1 -1
- package/dist/agent-runtime/projections.d.ts.map +1 -1
- package/dist/agent-runtime/schedule-dispatch.d.ts +9 -2
- package/dist/agent-runtime/schedule-dispatch.d.ts.map +1 -1
- package/dist/agent-runtime/schedule-records.d.ts +8 -2
- package/dist/agent-runtime/schedule-records.d.ts.map +1 -1
- package/dist/agent-runtime/schedules.d.ts +5 -0
- package/dist/agent-runtime/schedules.d.ts.map +1 -1
- package/dist/agent-runtime/sqlite-events.d.ts.map +1 -1
- package/dist/agent-runtime/store-create.d.ts.map +1 -1
- package/dist/agent-runtime/store-driver-contract.d.ts +9 -0
- package/dist/agent-runtime/store-driver-contract.d.ts.map +1 -1
- package/dist/agent-runtime/store-event-once.d.ts +9 -1
- package/dist/agent-runtime/store-event-once.d.ts.map +1 -1
- package/dist/agent-runtime/store-memory.d.ts.map +1 -1
- package/dist/agent-runtime/store.d.ts +6 -0
- package/dist/agent-runtime/store.d.ts.map +1 -1
- package/dist/application/change-subscription.d.ts.map +1 -1
- package/dist/application/notification-delivery.d.ts +24 -1
- package/dist/application/notification-delivery.d.ts.map +1 -1
- package/dist/application/notification-outbox-state.d.ts +4 -0
- package/dist/application/notification-outbox-state.d.ts.map +1 -1
- package/dist/application/notification-outbox.d.ts.map +1 -1
- package/dist/application/watch-hub-core.d.ts +14 -79
- package/dist/application/watch-hub-core.d.ts.map +1 -1
- package/dist/application/watch-hub-source.d.ts +98 -0
- package/dist/application/watch-hub-source.d.ts.map +1 -0
- package/dist/browser/session-credentials.d.ts +6 -0
- package/dist/browser/session-credentials.d.ts.map +1 -1
- package/dist/browser/session.d.ts +3 -1
- package/dist/browser/session.d.ts.map +1 -1
- package/dist/entrypoints/agent-runtime/harness.js +1 -1
- package/dist/entrypoints/agent-runtime/sqlite/bun.js +2 -2
- package/dist/entrypoints/agent-runtime/sqlite/node.js +2 -2
- package/dist/entrypoints/agent-runtime/testing.js +2 -2
- package/dist/entrypoints/agent-runtime.js +53 -19
- package/dist/entrypoints/application/diagnostic-journal.js +1 -1
- package/dist/entrypoints/application.d.ts +1 -1
- package/dist/entrypoints/application.d.ts.map +1 -1
- package/dist/entrypoints/application.js +275 -189
- package/dist/entrypoints/files.js +8 -2
- package/dist/entrypoints/index.js +33 -6
- package/dist/entrypoints/live.js +16 -12
- package/dist/entrypoints/node.js +1 -1
- package/dist/entrypoints/server.js +4 -4
- package/dist/{index-s84he4v7.js → index-10x2hbx0.js} +4 -1
- package/dist/{index-js7gqrb1.js → index-c6bxbsnc.js} +5 -1
- package/dist/{index-y9qn426m.js → index-eaysmdtq.js} +10 -1
- package/dist/{index-xwhjjw2x.js → index-f4fx2tza.js} +119 -49
- package/dist/{index-p3vsv1fb.js → index-hadmp193.js} +14 -5
- package/dist/{index-0b1cnze2.js → index-vqr7f7v8.js} +31 -15
- package/dist/internal/coalesced-task.d.ts +2 -0
- package/dist/internal/coalesced-task.d.ts.map +1 -1
- package/dist/internal/process-instance.d.ts.map +1 -1
- package/dist/internal/with-exclusive-lock.d.ts.map +1 -1
- package/dist/server/realtime.d.ts.map +1 -1
- package/llms/agent-runtime.2.txt +13 -5
- package/llms/agent-runtime.3.txt +9 -4
- package/llms/agent-runtime.txt +8 -3
- package/llms/application.2.txt +21 -5
- package/llms/live.txt +2 -1
- package/llms/server.3.txt +22 -0
- package/llms/stitchkit.3.txt +9 -5
- package/llms/upgrading-0.74.0-0.100.0.txt +15 -3
- package/llms-full.txt +99 -26
- package/llms.txt +5 -5
- 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
|
|
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).
|
|
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
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
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
|
|
1131
|
-
throw new Error("Notification action plan
|
|
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((
|
|
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
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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 =
|
|
2208
|
-
|
|
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
|
-
|
|
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
|
|
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,
|
|
2251
|
-
const id =
|
|
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
|
-
|
|
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
|
|
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
|
|
2330
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2437
|
+
adoptBaseline(source, subscriber, have);
|
|
2345
2438
|
try {
|
|
2346
2439
|
if (source.signature !== undefined) {
|
|
2347
|
-
const frame =
|
|
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
|
-
|
|
2354
|
-
|
|
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
|
|
2368
|
-
|
|
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 (
|
|
2372
|
-
|
|
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 (
|
|
2383
|
-
|
|
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
|
|
2420
|
-
|
|
2506
|
+
function sharedKey(key) {
|
|
2507
|
+
const { instance: _routing, ...shared } = key;
|
|
2508
|
+
return shared;
|
|
2421
2509
|
}
|
|
2422
|
-
function
|
|
2423
|
-
|
|
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,
|