stitchkit 0.100.0 → 0.100.2
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 +93 -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/process-lifecycle-transitions.d.ts +13 -2
- package/dist/application/process-lifecycle-transitions.d.ts.map +1 -1
- package/dist/application/process-lifecycle.d.ts +2 -1
- package/dist/application/process-lifecycle.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 +2 -2
- package/dist/entrypoints/application.d.ts.map +1 -1
- package/dist/entrypoints/application.js +285 -193
- 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 +47 -6
- package/llms/application.txt +18 -27
- 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 +118 -29
- 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;
|
|
@@ -1559,7 +1613,8 @@ function previousExitOf(previous, run, overlap) {
|
|
|
1559
1613
|
return ended;
|
|
1560
1614
|
if (previous.pid === run.pid)
|
|
1561
1615
|
return "hot-reload";
|
|
1562
|
-
|
|
1616
|
+
const declared = isVersionChange(previous.version, run.version) ? overlap.versionChange : overlap.sameVersion;
|
|
1617
|
+
if (declared === "handoff")
|
|
1563
1618
|
return "handoff";
|
|
1564
1619
|
return previous.readyAt === null ? "startup-failed" : "abnormal";
|
|
1565
1620
|
}
|
|
@@ -1569,7 +1624,8 @@ function closeAbandoned(candidate, previous, previousExit, run, overlap) {
|
|
|
1569
1624
|
const closed = { ...candidate, stoppedAt: run.startedAt };
|
|
1570
1625
|
if (candidate.pid === run.pid)
|
|
1571
1626
|
return { ...closed, termination: "hot-reload" };
|
|
1572
|
-
|
|
1627
|
+
const overlapsThisBuild = overlap.versionChange === "handoff" || !isVersionChange(candidate.version, run.version);
|
|
1628
|
+
if (overlap.sameVersion === "handoff" && overlapsThisBuild)
|
|
1573
1629
|
return candidate;
|
|
1574
1630
|
if (candidate.runId === previous?.runId && previousExit === "handoff")
|
|
1575
1631
|
return candidate;
|
|
@@ -1580,7 +1636,10 @@ function closeAbandoned(candidate, previous, previousExit, run, overlap) {
|
|
|
1580
1636
|
}
|
|
1581
1637
|
function transitionProcessStart(source, input) {
|
|
1582
1638
|
const retain = RetainSchema.parse(input.retain ?? 20);
|
|
1583
|
-
const overlap =
|
|
1639
|
+
const overlap = {
|
|
1640
|
+
sameVersion: input.sameVersionOverlap ?? "abnormal",
|
|
1641
|
+
versionChange: input.versionChangeOverlap ?? "handoff"
|
|
1642
|
+
};
|
|
1584
1643
|
const state = normalizeLifecycleState(source, 1000);
|
|
1585
1644
|
const run = LifecycleRunSchema.parse({
|
|
1586
1645
|
runId: input.runId,
|
|
@@ -1761,7 +1820,8 @@ function createProcessLifecycleLedger(config) {
|
|
|
1761
1820
|
version,
|
|
1762
1821
|
now: clock().toISOString(),
|
|
1763
1822
|
retain,
|
|
1764
|
-
sameVersionOverlap: config.sameVersionOverlap
|
|
1823
|
+
sameVersionOverlap: config.sameVersionOverlap,
|
|
1824
|
+
versionChangeOverlap: config.versionChangeOverlap
|
|
1765
1825
|
});
|
|
1766
1826
|
return { state: transition.state, result: transition.fact };
|
|
1767
1827
|
});
|
|
@@ -2088,6 +2148,78 @@ function managedServerResource(config) {
|
|
|
2088
2148
|
}
|
|
2089
2149
|
});
|
|
2090
2150
|
}
|
|
2151
|
+
// src/application/watch-hub-source.ts
|
|
2152
|
+
function remember(source, deltaMemoryBytes, revision, value, fingerprint) {
|
|
2153
|
+
if (deltaMemoryBytes === 0)
|
|
2154
|
+
return;
|
|
2155
|
+
const bytes = (JSON.stringify(value) ?? "null").length;
|
|
2156
|
+
if (bytes > deltaMemoryBytes)
|
|
2157
|
+
return;
|
|
2158
|
+
source.history.set(revision, { value, fingerprint, bytes });
|
|
2159
|
+
source.historyBytes += bytes;
|
|
2160
|
+
for (const [oldest, entry] of source.history) {
|
|
2161
|
+
if (source.historyBytes <= deltaMemoryBytes)
|
|
2162
|
+
break;
|
|
2163
|
+
source.history.delete(oldest);
|
|
2164
|
+
source.historyBytes -= entry.bytes;
|
|
2165
|
+
}
|
|
2166
|
+
}
|
|
2167
|
+
function fullFrame(source) {
|
|
2168
|
+
const value = source.value;
|
|
2169
|
+
return {
|
|
2170
|
+
kind: "full",
|
|
2171
|
+
key: source.key,
|
|
2172
|
+
revision: source.revision,
|
|
2173
|
+
fingerprint: source.fingerprint ?? valueFingerprint(value),
|
|
2174
|
+
value
|
|
2175
|
+
};
|
|
2176
|
+
}
|
|
2177
|
+
function frameFor(source, subscriber) {
|
|
2178
|
+
const value = source.value;
|
|
2179
|
+
const full = fullFrame(source);
|
|
2180
|
+
const fingerprint = full.fingerprint;
|
|
2181
|
+
const base = source.baselines.get(subscriber);
|
|
2182
|
+
if (base === undefined)
|
|
2183
|
+
return full;
|
|
2184
|
+
if (base === source.revision) {
|
|
2185
|
+
return { kind: "unchanged", key: source.key, revision: source.revision, fingerprint };
|
|
2186
|
+
}
|
|
2187
|
+
const held = source.history.get(base);
|
|
2188
|
+
if (!held)
|
|
2189
|
+
return full;
|
|
2190
|
+
const delta = diff(held.value, value);
|
|
2191
|
+
if (delta === undefined || !deltaWins(delta, value))
|
|
2192
|
+
return full;
|
|
2193
|
+
return {
|
|
2194
|
+
kind: "delta",
|
|
2195
|
+
key: source.key,
|
|
2196
|
+
revision: source.revision,
|
|
2197
|
+
fingerprint,
|
|
2198
|
+
base,
|
|
2199
|
+
delta
|
|
2200
|
+
};
|
|
2201
|
+
}
|
|
2202
|
+
function adoptBaseline(source, subscriber, have) {
|
|
2203
|
+
if (source.fingerprint === have.fingerprint) {
|
|
2204
|
+
source.baselines.set(subscriber, source.revision);
|
|
2205
|
+
return;
|
|
2206
|
+
}
|
|
2207
|
+
const named = source.history.get(have.revision);
|
|
2208
|
+
if (named?.fingerprint === have.fingerprint) {
|
|
2209
|
+
source.baselines.set(subscriber, have.revision);
|
|
2210
|
+
return;
|
|
2211
|
+
}
|
|
2212
|
+
for (const [revision, entry] of source.history) {
|
|
2213
|
+
if (entry.fingerprint !== have.fingerprint)
|
|
2214
|
+
continue;
|
|
2215
|
+
source.baselines.set(subscriber, revision);
|
|
2216
|
+
return;
|
|
2217
|
+
}
|
|
2218
|
+
}
|
|
2219
|
+
function valueFingerprint(value) {
|
|
2220
|
+
return argumentsDigest({ value });
|
|
2221
|
+
}
|
|
2222
|
+
|
|
2091
2223
|
// src/application/watch-hub-core.ts
|
|
2092
2224
|
var DEFAULT_BACKOFF = { minDelayMs: 250, maxDelayMs: 30000, jitter: 0.2 };
|
|
2093
2225
|
|
|
@@ -2137,82 +2269,24 @@ class WatchHubCore {
|
|
|
2137
2269
|
announceState(source, state) {
|
|
2138
2270
|
source.state = state;
|
|
2139
2271
|
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;
|
|
2272
|
+
for (const key of source.routes.get(subscriber)?.values() ?? []) {
|
|
2273
|
+
this.tell(key, () => subscriber.state({ ...state, key }));
|
|
2274
|
+
}
|
|
2204
2275
|
}
|
|
2205
2276
|
}
|
|
2206
2277
|
deliver(source, subscriber) {
|
|
2207
|
-
const frame =
|
|
2208
|
-
|
|
2278
|
+
const frame = frameFor(source, subscriber);
|
|
2279
|
+
let delivered = true;
|
|
2280
|
+
for (const key of source.routes.get(subscriber)?.values() ?? []) {
|
|
2281
|
+
delivered = this.tell(key, () => subscriber.value({ ...frame, key })) && delivered;
|
|
2282
|
+
}
|
|
2209
2283
|
if (delivered)
|
|
2210
2284
|
source.baselines.set(subscriber, source.revision);
|
|
2211
2285
|
return delivered;
|
|
2212
2286
|
}
|
|
2213
2287
|
publish(source, value, fingerprint) {
|
|
2214
2288
|
if (source.fingerprint !== undefined) {
|
|
2215
|
-
|
|
2289
|
+
remember(source, this.deltaMemoryBytes, source.revision, source.value, source.fingerprint);
|
|
2216
2290
|
}
|
|
2217
2291
|
source.revision += 1;
|
|
2218
2292
|
source.value = value;
|
|
@@ -2234,21 +2308,20 @@ class WatchHubCore {
|
|
|
2234
2308
|
this.announceState(source, { key: source.key, phase: "live" });
|
|
2235
2309
|
}
|
|
2236
2310
|
readFailed(source, error) {
|
|
2237
|
-
const code = readErrorCode(error);
|
|
2238
2311
|
this.announceState(source, {
|
|
2239
2312
|
key: source.key,
|
|
2240
2313
|
phase: "unavailable",
|
|
2241
2314
|
reason: "source-error",
|
|
2242
|
-
...code
|
|
2243
|
-
message: error instanceof Error ? error.message : String(error)
|
|
2315
|
+
...AppError.is(error) ? { code: error.code, message: error.message } : { message: "The watched read failed" }
|
|
2244
2316
|
});
|
|
2245
2317
|
this.config.logger?.warn?.("[stitchkit] watched read failed", {
|
|
2246
2318
|
key: watchKeyString(source.key),
|
|
2247
2319
|
error
|
|
2248
2320
|
});
|
|
2249
2321
|
}
|
|
2250
|
-
acquire(operation,
|
|
2251
|
-
const id =
|
|
2322
|
+
acquire(operation, routed, args, scope) {
|
|
2323
|
+
const id = sourceId(scope, routed);
|
|
2324
|
+
const key = sharedKey(routed);
|
|
2252
2325
|
const existing = this.sources.get(id);
|
|
2253
2326
|
if (existing) {
|
|
2254
2327
|
clearTimeout(existing.release);
|
|
@@ -2262,6 +2335,7 @@ class WatchHubCore {
|
|
|
2262
2335
|
operation,
|
|
2263
2336
|
args,
|
|
2264
2337
|
subscribers: new Set,
|
|
2338
|
+
routes: new Map,
|
|
2265
2339
|
unsubscribes: [],
|
|
2266
2340
|
revision: 0,
|
|
2267
2341
|
history: new Map,
|
|
@@ -2305,12 +2379,32 @@ class WatchHubCore {
|
|
|
2305
2379
|
source.release = setTimeout(drop, this.holdMs);
|
|
2306
2380
|
source.release.unref?.();
|
|
2307
2381
|
}
|
|
2382
|
+
unroute(source, subscriber, route) {
|
|
2383
|
+
const routes = source.routes.get(subscriber);
|
|
2384
|
+
routes?.delete(route);
|
|
2385
|
+
if (routes && routes.size > 0)
|
|
2386
|
+
return;
|
|
2387
|
+
source.routes.delete(subscriber);
|
|
2388
|
+
source.subscribers.delete(subscriber);
|
|
2389
|
+
source.baselines.delete(subscriber);
|
|
2390
|
+
this.release(source);
|
|
2391
|
+
}
|
|
2308
2392
|
attach(subscriber, scope) {
|
|
2309
|
-
if (this.held.size >= (this.config.maxSubscribers ?? 1024))
|
|
2310
|
-
|
|
2393
|
+
if (this.held.size >= (this.config.maxSubscribers ?? 1024)) {
|
|
2394
|
+
const refused = {
|
|
2395
|
+
open: () => ({ accepted: false, reason: "Watch subscriber capacity exceeded" }),
|
|
2396
|
+
close: () => {
|
|
2397
|
+
return;
|
|
2398
|
+
},
|
|
2399
|
+
detach: () => {
|
|
2400
|
+
return;
|
|
2401
|
+
}
|
|
2402
|
+
};
|
|
2403
|
+
return refused;
|
|
2404
|
+
}
|
|
2311
2405
|
if (scope && (!scope.key || scope.key.length > 512))
|
|
2312
2406
|
throw new Error("Invalid watch admission scope");
|
|
2313
|
-
const keys = new
|
|
2407
|
+
const keys = new Map;
|
|
2314
2408
|
this.held.set(subscriber, keys);
|
|
2315
2409
|
let detached = false;
|
|
2316
2410
|
const attached = {
|
|
@@ -2326,8 +2420,9 @@ class WatchHubCore {
|
|
|
2326
2420
|
reason: `${key.service}.${key.action} is not watchable`
|
|
2327
2421
|
};
|
|
2328
2422
|
}
|
|
2329
|
-
const
|
|
2330
|
-
|
|
2423
|
+
const route = watchKeyString(key);
|
|
2424
|
+
const id = sourceId(scope?.key, key);
|
|
2425
|
+
if (keys.has(route))
|
|
2331
2426
|
return { accepted: true };
|
|
2332
2427
|
if (keys.size >= this.maxWatches) {
|
|
2333
2428
|
return {
|
|
@@ -2338,52 +2433,50 @@ class WatchHubCore {
|
|
|
2338
2433
|
if (!this.sources.has(id) && this.sources.size >= (this.config.maxSources ?? 1024))
|
|
2339
2434
|
return { accepted: false, reason: "Watch source capacity exceeded" };
|
|
2340
2435
|
const source = this.acquire(operation, key, args, scope?.key);
|
|
2436
|
+
const alreadyRouted = source.routes.has(subscriber);
|
|
2341
2437
|
source.subscribers.add(subscriber);
|
|
2342
|
-
|
|
2438
|
+
const routes = source.routes.get(subscriber) ?? new Map;
|
|
2439
|
+
routes.set(route, key);
|
|
2440
|
+
source.routes.set(subscriber, routes);
|
|
2441
|
+
keys.set(route, id);
|
|
2343
2442
|
if (have)
|
|
2344
|
-
|
|
2443
|
+
adoptBaseline(source, subscriber, have);
|
|
2345
2444
|
try {
|
|
2346
2445
|
if (source.signature !== undefined) {
|
|
2347
|
-
const frame =
|
|
2348
|
-
subscriber.value(frame);
|
|
2446
|
+
const frame = alreadyRouted && !have ? fullFrame(source) : frameFor(source, subscriber);
|
|
2447
|
+
subscriber.value({ ...frame, key });
|
|
2349
2448
|
source.baselines.set(subscriber, source.revision);
|
|
2350
2449
|
}
|
|
2351
|
-
subscriber.state(source.state);
|
|
2450
|
+
subscriber.state({ ...source.state, key });
|
|
2352
2451
|
} catch (error) {
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
keys.delete(id);
|
|
2356
|
-
this.release(source);
|
|
2452
|
+
keys.delete(route);
|
|
2453
|
+
this.unroute(source, subscriber, route);
|
|
2357
2454
|
return {
|
|
2358
2455
|
accepted: false,
|
|
2359
2456
|
reason: error instanceof Error ? error.message : String(error)
|
|
2360
2457
|
};
|
|
2361
2458
|
}
|
|
2362
|
-
if (!source.task.running && (source.signature === undefined || source.state.phase !== "live"))
|
|
2459
|
+
if (!source.task.running && (source.signature === undefined || source.state.phase !== "live" || source.task.pending))
|
|
2363
2460
|
source.task.trigger();
|
|
2364
2461
|
return { accepted: true };
|
|
2365
2462
|
},
|
|
2366
2463
|
close: (key) => {
|
|
2367
|
-
const
|
|
2368
|
-
|
|
2464
|
+
const route = watchKeyString(key);
|
|
2465
|
+
const id = keys.get(route);
|
|
2466
|
+
if (id === undefined)
|
|
2369
2467
|
return;
|
|
2468
|
+
keys.delete(route);
|
|
2370
2469
|
const source = this.sources.get(id);
|
|
2371
|
-
if (
|
|
2372
|
-
|
|
2373
|
-
source.subscribers.delete(subscriber);
|
|
2374
|
-
source.baselines.delete(subscriber);
|
|
2375
|
-
this.release(source);
|
|
2470
|
+
if (source)
|
|
2471
|
+
this.unroute(source, subscriber, route);
|
|
2376
2472
|
},
|
|
2377
2473
|
detach: () => {
|
|
2378
2474
|
detached = true;
|
|
2379
2475
|
scope?.signal.removeEventListener("abort", onAbort);
|
|
2380
|
-
for (const id of keys) {
|
|
2476
|
+
for (const [route, id] of keys) {
|
|
2381
2477
|
const source = this.sources.get(id);
|
|
2382
|
-
if (
|
|
2383
|
-
|
|
2384
|
-
source.subscribers.delete(subscriber);
|
|
2385
|
-
source.baselines.delete(subscriber);
|
|
2386
|
-
this.release(source);
|
|
2478
|
+
if (source)
|
|
2479
|
+
this.unroute(source, subscriber, route);
|
|
2387
2480
|
}
|
|
2388
2481
|
keys.clear();
|
|
2389
2482
|
this.held.delete(subscriber);
|
|
@@ -2416,14 +2509,12 @@ class WatchHubCore {
|
|
|
2416
2509
|
}
|
|
2417
2510
|
}
|
|
2418
2511
|
}
|
|
2419
|
-
function
|
|
2420
|
-
|
|
2512
|
+
function sharedKey(key) {
|
|
2513
|
+
const { instance: _routing, ...shared } = key;
|
|
2514
|
+
return shared;
|
|
2421
2515
|
}
|
|
2422
|
-
function
|
|
2423
|
-
|
|
2424
|
-
return;
|
|
2425
|
-
const code = Reflect.get(error, "code");
|
|
2426
|
-
return typeof code === "string" ? code : undefined;
|
|
2516
|
+
function sourceId(scope, key) {
|
|
2517
|
+
return JSON.stringify([scope ?? null, watchKeyString(sharedKey(key))]);
|
|
2427
2518
|
}
|
|
2428
2519
|
|
|
2429
2520
|
// src/application/watch-hub.ts
|
|
@@ -2487,6 +2578,7 @@ export {
|
|
|
2487
2578
|
RevisionSignalAdvanceResultSchema,
|
|
2488
2579
|
PreviousExitSchema,
|
|
2489
2580
|
PolicyDecisionSchema,
|
|
2581
|
+
NotificationPlanVersionError,
|
|
2490
2582
|
NotificationDeliveryStateSchema,
|
|
2491
2583
|
ManagedScheduleStatusSchema,
|
|
2492
2584
|
ManagedScheduleOverlapSchema,
|