@ewhauser/world-celld 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -0
- package/README.md +43 -15
- package/celld-queue-worker/README.md +2 -2
- package/celld-queue-worker/wrangler.jsonc +3 -1
- package/celld-worker/README.md +3 -3
- package/dist/index.d.ts +44 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +113 -99
- package/dist/index.js.map +1 -1
- package/dist/queue-consumer.js +1 -1
- package/dist/{queue-protocol-Ds44uuND.js → queue-protocol-CtH7UUCO.js} +2 -2
- package/dist/{queue-protocol-Ds44uuND.js.map → queue-protocol-CtH7UUCO.js.map} +1 -1
- package/dist/{queue-protocol-CmJNMH_5.js → run-list-B__jKuE7.js} +124 -18
- package/dist/run-list-B__jKuE7.js.map +1 -0
- package/dist/testing.d.ts +2 -2
- package/dist/testing.d.ts.map +1 -1
- package/dist/testing.js +211 -25
- package/dist/testing.js.map +1 -1
- package/dist/worker.d.ts +19 -0
- package/dist/worker.d.ts.map +1 -1
- package/dist/worker.js +313 -33
- package/dist/worker.js.map +1 -1
- package/docs/celld-v0.6-upgrade.md +121 -0
- package/package.json +7 -5
- package/dist/queue-protocol-CmJNMH_5.js.map +0 -1
- package/docs/celld-v0.5-upgrade.md +0 -130
package/dist/worker.d.ts
CHANGED
|
@@ -224,6 +224,15 @@ interface ApplyEventSuccess {
|
|
|
224
224
|
maxEvents?: number;
|
|
225
225
|
/** Deadline for publishing this authoritative mutation to derivative indexes. */
|
|
226
226
|
indexPublicationExpiresAt?: number;
|
|
227
|
+
/** Set by the worker router after it published `run` to the run catalog. */
|
|
228
|
+
runIndexPublished?: true;
|
|
229
|
+
/** Set by the worker router after it released `releasedHooks` from the hook indexes. */
|
|
230
|
+
hookIndexesReleased?: true;
|
|
231
|
+
/**
|
|
232
|
+
* The unchanged run returned with a `preloadEvents` hook_received replay
|
|
233
|
+
* log. It is not a run mutation, so it carries no index publication.
|
|
234
|
+
*/
|
|
235
|
+
replayRun?: WorkflowRun;
|
|
227
236
|
}
|
|
228
237
|
type ApplyEventOutcome = ApplyEventSuccess | ApplyEventFailure;
|
|
229
238
|
//#endregion
|
|
@@ -383,6 +392,8 @@ declare class StreamDO extends DurableObject {
|
|
|
383
392
|
private mutationTail;
|
|
384
393
|
private changeVersion;
|
|
385
394
|
private readonly waiters;
|
|
395
|
+
/** Multi-key read split into calls within the per-call key limit. */
|
|
396
|
+
private getMany;
|
|
386
397
|
private getMeta;
|
|
387
398
|
private assertOwner;
|
|
388
399
|
private assertWritable;
|
|
@@ -503,6 +514,14 @@ declare class WorkflowRunDO extends DurableObject {
|
|
|
503
514
|
private executeScheduledCleanupPage;
|
|
504
515
|
private executeCleanup;
|
|
505
516
|
private executeTerminalCleanup;
|
|
517
|
+
/**
|
|
518
|
+
* Most runs end with a handful of hooks and waits. When every terminal
|
|
519
|
+
* phase fits in one page, release the hook indexes once and delete hooks,
|
|
520
|
+
* markers, and waits in a single transaction instead of one alarm and one
|
|
521
|
+
* transaction per phase. Returns false, having changed nothing locally, when
|
|
522
|
+
* any phase needs paging; the paged phases then run as before.
|
|
523
|
+
*/
|
|
524
|
+
private completeSmallTerminalCleanup;
|
|
506
525
|
private deleteTerminalHookPage;
|
|
507
526
|
private deleteTerminalHookMarkerPage;
|
|
508
527
|
private deleteTerminalWaitPage;
|
package/dist/worker.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"worker.d.ts","names":[],"sources":["../src/worker/queue-payload-store.ts","../src/worker/router.ts","../src/worker/do-base.ts","../src/retention.ts","../src/apply-event.ts","../src/stream-protocol.ts","../src/indexes.ts","../src/config.ts","../src/worker/durable-objects/hook-index-shared.ts","../src/worker/durable-objects/HookIdDO.ts","../src/worker/durable-objects/HookTokenDO.ts","../src/worker/durable-objects/RunCatalogDO.ts","../src/worker/durable-objects/StreamDO.ts","../src/worker/durable-objects/WorkflowRunDO.ts","../src/worker/worker.ts"],"mappings":";;;;;;;;;;UAOiB;EACf,MAAM,aAAa,eAAe;IAAY;IAAe;MAAsB;EACnF,KAAK,cAAc;EACnB,OAAO,0BAA0B;;UAGlB;EACf,IACE,aACA,eACA;IAAY,iBAAiB;MAC5B;EACH,IAAI,cAAc;IAAU,QAAQ;;EACpC,OAAO,0BAA0B;;;;;
|
|
1
|
+
{"version":3,"file":"worker.d.ts","names":[],"sources":["../src/worker/queue-payload-store.ts","../src/worker/router.ts","../src/worker/do-base.ts","../src/retention.ts","../src/apply-event.ts","../src/stream-protocol.ts","../src/indexes.ts","../src/config.ts","../src/worker/durable-objects/hook-index-shared.ts","../src/worker/durable-objects/HookIdDO.ts","../src/worker/durable-objects/HookTokenDO.ts","../src/worker/durable-objects/RunCatalogDO.ts","../src/worker/durable-objects/StreamDO.ts","../src/worker/durable-objects/WorkflowRunDO.ts","../src/worker/worker.ts"],"mappings":"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"}
|
package/dist/worker.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { _ as isRecord$1, a as queueOrphanName, c as validateNativeQueueEnvelope, d as HOOK_CLAIM_LEASE_MS, f as LIFECYCLE_COMPACTION_RETRY_MS, g as isPositiveSafeInteger, h as isNonNegativeSafeInteger, i as queueClaimName, l as validateNativeQueueSendOptions, m as QUEUE_CLAIM_STALE_MS, n as QUEUE_PAYLOAD_REGISTRY_PREFIX, o as queuePayloadObjectKey, p as MAX_RUN_INDEX_PUBLICATION_LIFETIME_MS, s as queuePayloadRegistryKey, t as NATIVE_QUEUE_MAX_MESSAGE_BYTES, v as queueDelayDeadline, y as strictIntegerSetting } from "./queue-protocol-
|
|
1
|
+
import { _ as isRecord$1, a as queueOrphanName, c as validateNativeQueueEnvelope, d as HOOK_CLAIM_LEASE_MS, f as LIFECYCLE_COMPACTION_RETRY_MS, g as isPositiveSafeInteger, h as isNonNegativeSafeInteger, i as queueClaimName, l as validateNativeQueueSendOptions, m as QUEUE_CLAIM_STALE_MS, n as QUEUE_PAYLOAD_REGISTRY_PREFIX, o as queuePayloadObjectKey, p as MAX_RUN_INDEX_PUBLICATION_LIFETIME_MS, s as queuePayloadRegistryKey, t as NATIVE_QUEUE_MAX_MESSAGE_BYTES, v as queueDelayDeadline, y as strictIntegerSetting } from "./queue-protocol-CtH7UUCO.js";
|
|
2
2
|
import { DurableObject, WorkerEntrypoint } from "cloudflare:workers";
|
|
3
3
|
import { CreateEventSchema, EventSchema, HookSchema, SPEC_VERSION_CURRENT, StepSchema, WaitSchema, WorkflowRunSchema, applyAttributeChanges, eventIdToSlot, getMaxEventsPerRun, isChildEntityCreationEvent, isTerminalStepStatus, isTerminalWorkflowRunStatus, slotToEventId, validateAttributeChanges } from "@workflow/world";
|
|
4
4
|
//#region src/worker/auth.ts
|
|
@@ -197,22 +197,34 @@ function dateReviver(key, value) {
|
|
|
197
197
|
return value;
|
|
198
198
|
}
|
|
199
199
|
/**
|
|
200
|
-
* Base64 helpers. Modified vs upstream: implemented
|
|
201
|
-
*
|
|
202
|
-
*
|
|
200
|
+
* Base64 helpers. Modified vs upstream: implemented without Buffer so this
|
|
201
|
+
* module is safe inside the celld worker bundle (workerd has no Buffer
|
|
202
|
+
* global). Uses the native Uint8Array base64 methods where the runtime has
|
|
203
|
+
* them (Node >= 25, current V8) and falls back to atob/btoa elsewhere.
|
|
203
204
|
*/
|
|
204
|
-
|
|
205
|
+
const nativeToBase64 = Uint8Array.prototype.toBase64;
|
|
206
|
+
const nativeFromBase64 = Uint8Array.fromBase64;
|
|
207
|
+
/** Small enough to stay far below engine argument-count limits and fast to spread. */
|
|
208
|
+
const FALLBACK_ENCODE_CHUNK = 4096;
|
|
209
|
+
/** @internal Exported for tests; use b64encode. */
|
|
210
|
+
function b64encodeFallback(bytes) {
|
|
205
211
|
let bin = "";
|
|
206
|
-
|
|
207
|
-
for (let i = 0; i < bytes.length; i += CHUNK) bin += String.fromCharCode(...bytes.subarray(i, i + CHUNK));
|
|
212
|
+
for (let i = 0; i < bytes.length; i += FALLBACK_ENCODE_CHUNK) bin += String.fromCharCode.apply(null, bytes.subarray(i, i + FALLBACK_ENCODE_CHUNK));
|
|
208
213
|
return btoa(bin);
|
|
209
214
|
}
|
|
210
|
-
|
|
215
|
+
/** @internal Exported for tests; use b64decode. */
|
|
216
|
+
function b64decodeFallback(text) {
|
|
211
217
|
const bin = atob(text);
|
|
212
218
|
const out = new Uint8Array(bin.length);
|
|
213
219
|
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
|
|
214
220
|
return out;
|
|
215
221
|
}
|
|
222
|
+
function b64encode(bytes) {
|
|
223
|
+
return nativeToBase64 ? nativeToBase64.call(bytes) : b64encodeFallback(bytes);
|
|
224
|
+
}
|
|
225
|
+
function b64decode(text) {
|
|
226
|
+
return nativeFromBase64 ? nativeFromBase64(text) : b64decodeFallback(text);
|
|
227
|
+
}
|
|
216
228
|
/**
|
|
217
229
|
* JSON reviver that decodes tagged Uint8Array objects and ISO date strings.
|
|
218
230
|
* Supports both:
|
|
@@ -1068,6 +1080,7 @@ async function finalizeEventPage(store, outcome, params) {
|
|
|
1068
1080
|
maxEvents: getMaxEventsPerRun()
|
|
1069
1081
|
} : outcome;
|
|
1070
1082
|
if (!result.event) return result;
|
|
1083
|
+
if (params?.preloadEvents && result.event.eventType === "hook_received") return await attachHookReplayPreload(store, result);
|
|
1071
1084
|
if (typeof params?.sinceCursor === "string") {
|
|
1072
1085
|
const page = await listByPrefix(store, EVENT_KEY_PREFIX, {
|
|
1073
1086
|
limit: 100,
|
|
@@ -1097,6 +1110,32 @@ async function finalizeEventPage(store, outcome, params) {
|
|
|
1097
1110
|
hasMore: events.length < skippedCount
|
|
1098
1111
|
};
|
|
1099
1112
|
}
|
|
1113
|
+
/**
|
|
1114
|
+
* Lazy hook resume preload: return the run and its complete replay log with
|
|
1115
|
+
* the hook_received write, read in the same transaction, so the runtime can
|
|
1116
|
+
* skip its run_started write and the index publication that follows it. The
|
|
1117
|
+
* runtime trusts only a complete log with a non-null cursor, so a run that is
|
|
1118
|
+
* not started, not running, or over the event ceiling gets the plain result.
|
|
1119
|
+
*/
|
|
1120
|
+
async function attachHookReplayPreload(store, result) {
|
|
1121
|
+
const run = await store.get(RUN_KEY);
|
|
1122
|
+
if (!run?.startedAt || run.status !== "running") return result;
|
|
1123
|
+
const maxEvents = getMaxEventsPerRun();
|
|
1124
|
+
const log = await listByPrefix(store, EVENT_KEY_PREFIX, {
|
|
1125
|
+
limit: maxEvents,
|
|
1126
|
+
sortOrder: "asc"
|
|
1127
|
+
}, (event) => event.eventId);
|
|
1128
|
+
const last = log.data.at(-1);
|
|
1129
|
+
if (log.hasMore || last === void 0) return result;
|
|
1130
|
+
return {
|
|
1131
|
+
...result,
|
|
1132
|
+
replayRun: run,
|
|
1133
|
+
maxEvents,
|
|
1134
|
+
events: log.data,
|
|
1135
|
+
cursor: last.eventId,
|
|
1136
|
+
hasMore: false
|
|
1137
|
+
};
|
|
1138
|
+
}
|
|
1100
1139
|
//#endregion
|
|
1101
1140
|
//#region src/codec.ts
|
|
1102
1141
|
/**
|
|
@@ -1110,7 +1149,8 @@ async function finalizeEventPage(store, outcome, params) {
|
|
|
1110
1149
|
* `{__type:'Date', iso}`.
|
|
1111
1150
|
*
|
|
1112
1151
|
* Runs on both sides of the wire, including inside the celld worker — so no
|
|
1113
|
-
* Node built-ins (Buffer): base64
|
|
1152
|
+
* Node built-ins (Buffer): base64 uses the native Uint8Array methods or
|
|
1153
|
+
* atob/btoa.
|
|
1114
1154
|
*/
|
|
1115
1155
|
function rpcReplacer(key, value) {
|
|
1116
1156
|
const original = this && typeof this === "object" ? this[key] : void 0;
|
|
@@ -1150,6 +1190,79 @@ function rpcStringify(value) {
|
|
|
1150
1190
|
function rpcParse(text) {
|
|
1151
1191
|
return JSON.parse(text, rpcReviver);
|
|
1152
1192
|
}
|
|
1193
|
+
/** Largest page the fleet-side list accepts, matching the catalog page limit. */
|
|
1194
|
+
const MAX_RUN_LIST_LIMIT = 1e3;
|
|
1195
|
+
/**
|
|
1196
|
+
* Run statuses only move pending -> running -> terminal, and terminal statuses
|
|
1197
|
+
* are immutable. An older index value may therefore safely exclude a filter
|
|
1198
|
+
* only when it is already terminal, or when the caller asks for pending and
|
|
1199
|
+
* the index has advanced beyond pending. Earlier non-terminal metadata cannot
|
|
1200
|
+
* exclude a later status because a post-commit index write may need replay.
|
|
1201
|
+
*/
|
|
1202
|
+
function indexStatusExcludes(indexed, requested) {
|
|
1203
|
+
if (requested === void 0 || indexed === requested) return false;
|
|
1204
|
+
return isTerminalWorkflowRunStatus(indexed) || requested === "pending";
|
|
1205
|
+
}
|
|
1206
|
+
function stripRunData(run) {
|
|
1207
|
+
const { input: _input, output: _output, ...rest } = run;
|
|
1208
|
+
return rest;
|
|
1209
|
+
}
|
|
1210
|
+
/**
|
|
1211
|
+
* Page through the run catalog, verifying every candidate against its
|
|
1212
|
+
* authoritative run cell. Runs on the client against remote bindings, or in
|
|
1213
|
+
* the worker router against fleet bindings.
|
|
1214
|
+
*/
|
|
1215
|
+
async function listRunsPage(deps, request) {
|
|
1216
|
+
const { statuses, limit } = request;
|
|
1217
|
+
const prefix = request.workflowName ? `run:${request.workflowName}:` : "runall:";
|
|
1218
|
+
const reverse = request.sortOrder === "desc";
|
|
1219
|
+
const matches = [];
|
|
1220
|
+
let scanCursor = request.cursor;
|
|
1221
|
+
let exhausted = false;
|
|
1222
|
+
while (matches.length <= limit && !exhausted) {
|
|
1223
|
+
const kvList = await deps.listRuns({
|
|
1224
|
+
prefix,
|
|
1225
|
+
limit: Math.min(MAX_RUN_LIST_LIMIT, Math.max(50, limit)),
|
|
1226
|
+
cursor: scanCursor,
|
|
1227
|
+
reverse
|
|
1228
|
+
});
|
|
1229
|
+
if (kvList.keys.length === 0) {
|
|
1230
|
+
exhausted = true;
|
|
1231
|
+
break;
|
|
1232
|
+
}
|
|
1233
|
+
const candidates = [];
|
|
1234
|
+
for (const key of kvList.keys) {
|
|
1235
|
+
const metadata = JSON.parse(key.value);
|
|
1236
|
+
if (statuses?.every((status) => indexStatusExcludes(metadata.status, status))) continue;
|
|
1237
|
+
candidates.push({
|
|
1238
|
+
key: key.name,
|
|
1239
|
+
metadata
|
|
1240
|
+
});
|
|
1241
|
+
}
|
|
1242
|
+
for (let offset = 0; offset < candidates.length && matches.length <= limit;) {
|
|
1243
|
+
const needed = limit + 1 - matches.length;
|
|
1244
|
+
const batch = candidates.slice(offset, offset + Math.min(8, needed));
|
|
1245
|
+
const resolved = await Promise.all(batch.map(async ({ key, metadata }) => {
|
|
1246
|
+
const run = await deps.readRun(metadata.runId);
|
|
1247
|
+
return run && (statuses === void 0 || statuses.includes(run.status)) ? {
|
|
1248
|
+
key,
|
|
1249
|
+
run
|
|
1250
|
+
} : null;
|
|
1251
|
+
}));
|
|
1252
|
+
for (const match of resolved) if (match) matches.push(match);
|
|
1253
|
+
offset += batch.length;
|
|
1254
|
+
}
|
|
1255
|
+
exhausted = kvList.list_complete;
|
|
1256
|
+
scanCursor = kvList.cursor ?? kvList.keys.at(-1)?.name;
|
|
1257
|
+
}
|
|
1258
|
+
const hasMore = matches.length > limit;
|
|
1259
|
+
const page = matches.slice(0, limit);
|
|
1260
|
+
return {
|
|
1261
|
+
data: page.map(({ run }) => request.resolveData === "none" ? stripRunData(run) : run),
|
|
1262
|
+
cursor: hasMore ? page.at(-1)?.key ?? null : null,
|
|
1263
|
+
hasMore
|
|
1264
|
+
};
|
|
1265
|
+
}
|
|
1153
1266
|
const INDEX_PROTOCOL_VERSION = "v1";
|
|
1154
1267
|
const HOOK_RELEASE_BATCH = 64;
|
|
1155
1268
|
/** Stable FNV-1a routing hash. Collisions share a shard but never a storage key. */
|
|
@@ -1178,6 +1291,14 @@ function hookIdShardName(hookId) {
|
|
|
1178
1291
|
function allRunCatalogShardNames() {
|
|
1179
1292
|
return Array.from({ length: 16 }, (_, shard) => `run-catalog:${INDEX_PROTOCOL_VERSION}:${shard.toString(16).padStart(2, "0")}`);
|
|
1180
1293
|
}
|
|
1294
|
+
/** Catalog metadata for a run; the client and the worker router must agree on it. */
|
|
1295
|
+
function runIndexMetadata(run) {
|
|
1296
|
+
return JSON.stringify({
|
|
1297
|
+
runId: run.runId,
|
|
1298
|
+
createdAt: run.createdAt.toISOString(),
|
|
1299
|
+
status: run.status
|
|
1300
|
+
});
|
|
1301
|
+
}
|
|
1181
1302
|
function stub(namespace, name) {
|
|
1182
1303
|
return namespace.get(namespace.idFromName(name));
|
|
1183
1304
|
}
|
|
@@ -1343,6 +1464,8 @@ const STREAM_BATCH_CONTENT_TYPE = "application/vnd.world-celld.stream-batch.v1";
|
|
|
1343
1464
|
const MAX_STREAM_CHUNK_BYTES = 1048576;
|
|
1344
1465
|
/** Keeps serialized RPC bodies well below the 32 MiB Workers RPC limit. */
|
|
1345
1466
|
const MAX_STREAM_BATCH_BYTES = 8388608;
|
|
1467
|
+
/** Response header on stream requests: the per-request chunk limit this worker accepts. */
|
|
1468
|
+
const STREAM_CHUNK_LIMIT_HEADER = "x-world-celld-stream-chunks";
|
|
1346
1469
|
const MAX_STREAM_READ_BYTES = 8388608;
|
|
1347
1470
|
const MAX_STREAM_ERROR_BYTES = 16384;
|
|
1348
1471
|
/** Must remain below the default 30 second fleet request deadline. */
|
|
@@ -1370,7 +1493,7 @@ function requireSafeIndex(value, field) {
|
|
|
1370
1493
|
}
|
|
1371
1494
|
function validateStreamWriteChunks(chunks) {
|
|
1372
1495
|
if (chunks.length === 0) throw protocolError("batch must contain at least one chunk");
|
|
1373
|
-
if (chunks.length >
|
|
1496
|
+
if (chunks.length > 512) throw protocolError(`batch exceeds 512 chunks`);
|
|
1374
1497
|
let totalBytes = 0;
|
|
1375
1498
|
for (const chunk of chunks) {
|
|
1376
1499
|
if (!(chunk instanceof Uint8Array)) throw protocolError("chunk is not binary data");
|
|
@@ -1383,7 +1506,7 @@ function validateStreamWriteChunks(chunks) {
|
|
|
1383
1506
|
function validateStreamReadRequest(request) {
|
|
1384
1507
|
if (!request.runId) throw protocolError("runId is required");
|
|
1385
1508
|
if (!Number.isSafeInteger(request.startIndex) || request.startIndex < 0 || request.startIndex > 2147483647) throw protocolError("startIndex must be between 0 and 2147483647");
|
|
1386
|
-
if (!Number.isSafeInteger(request.maxChunks) || request.maxChunks < 0 || request.maxChunks >
|
|
1509
|
+
if (!Number.isSafeInteger(request.maxChunks) || request.maxChunks < 0 || request.maxChunks > 512) throw protocolError(`maxChunks must be between 0 and 512`);
|
|
1387
1510
|
if (!Number.isSafeInteger(request.maxBytes) || request.maxBytes < 1048576 || request.maxBytes > 8388608) throw protocolError(`maxBytes must be between ${MAX_STREAM_CHUNK_BYTES} and ${MAX_STREAM_READ_BYTES}`);
|
|
1388
1511
|
if (!Number.isSafeInteger(request.waitMs) || request.waitMs < 0 || request.waitMs > 2e4) throw protocolError(`waitMs must be between 0 and ${MAX_STREAM_LONG_POLL_MS}`);
|
|
1389
1512
|
}
|
|
@@ -1410,7 +1533,7 @@ function decodeStreamWriteBatch(encoded) {
|
|
|
1410
1533
|
function encodeStreamWriteResult(result) {
|
|
1411
1534
|
requireSafeIndex(result.startIndex, "startIndex");
|
|
1412
1535
|
requireSafeIndex(result.tailIndex, "tailIndex");
|
|
1413
|
-
if (!Number.isSafeInteger(result.count) || result.count < 1 || result.count >
|
|
1536
|
+
if (!Number.isSafeInteger(result.count) || result.count < 1 || result.count > 512) throw protocolError("write count is out of range");
|
|
1414
1537
|
const encoded = new Uint8Array(WRITE_ACK_BYTES);
|
|
1415
1538
|
const view = new DataView(encoded.buffer);
|
|
1416
1539
|
view.setUint32(0, WRITE_ACK_MAGIC);
|
|
@@ -1425,7 +1548,7 @@ function encodeStreamReadResult(result) {
|
|
|
1425
1548
|
requireSafeIndex(result.tailIndex, "tailIndex");
|
|
1426
1549
|
let chunkBytes = 0;
|
|
1427
1550
|
for (const chunk of result.chunks) chunkBytes += 4 + chunk.byteLength;
|
|
1428
|
-
if (result.chunks.length >
|
|
1551
|
+
if (result.chunks.length > 512 || chunkBytes > 8390656) throw protocolError("read result exceeds configured bounds");
|
|
1429
1552
|
const encoder = new TextEncoder();
|
|
1430
1553
|
const errorName = result.error ? encoder.encode(result.error.name) : /* @__PURE__ */ new Uint8Array();
|
|
1431
1554
|
const errorMessage = result.error ? encoder.encode(result.error.message) : /* @__PURE__ */ new Uint8Array();
|
|
@@ -1577,6 +1700,34 @@ function listOptions(value) {
|
|
|
1577
1700
|
}
|
|
1578
1701
|
return parsed;
|
|
1579
1702
|
}
|
|
1703
|
+
const RUN_STATUSES = /* @__PURE__ */ new Set([
|
|
1704
|
+
"pending",
|
|
1705
|
+
"running",
|
|
1706
|
+
"completed",
|
|
1707
|
+
"failed",
|
|
1708
|
+
"cancelled"
|
|
1709
|
+
]);
|
|
1710
|
+
function runListRequest(value) {
|
|
1711
|
+
if (!isRecord$1(value)) invalid("run list request must be an object");
|
|
1712
|
+
const limit = value.limit;
|
|
1713
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 1e3) invalid(`request.limit must be an integer between 1 and ${MAX_RUN_LIST_LIMIT}`);
|
|
1714
|
+
const parsed = { limit };
|
|
1715
|
+
if (value.workflowName !== void 0) parsed.workflowName = nonEmptyString(value.workflowName, "request.workflowName");
|
|
1716
|
+
if (value.cursor !== void 0) parsed.cursor = stringValue(value.cursor, "request.cursor");
|
|
1717
|
+
if (value.statuses !== void 0) {
|
|
1718
|
+
if (!Array.isArray(value.statuses) || value.statuses.length === 0 || !value.statuses.every((status) => RUN_STATUSES.has(status))) invalid("request.statuses must be a non-empty array of run statuses");
|
|
1719
|
+
parsed.statuses = value.statuses;
|
|
1720
|
+
}
|
|
1721
|
+
if (value.sortOrder !== void 0) {
|
|
1722
|
+
if (value.sortOrder !== "asc" && value.sortOrder !== "desc") invalid("request.sortOrder must be 'asc' or 'desc'");
|
|
1723
|
+
parsed.sortOrder = value.sortOrder;
|
|
1724
|
+
}
|
|
1725
|
+
if (value.resolveData !== void 0) {
|
|
1726
|
+
if (value.resolveData !== "none" && value.resolveData !== "all") invalid("request.resolveData must be 'none' or 'all'");
|
|
1727
|
+
parsed.resolveData = value.resolveData;
|
|
1728
|
+
}
|
|
1729
|
+
return parsed;
|
|
1730
|
+
}
|
|
1580
1731
|
function runCommit(args) {
|
|
1581
1732
|
exactArgs(args, 3);
|
|
1582
1733
|
const parsedRun = WorkflowRunSchema.safeParse(args[0]);
|
|
@@ -1637,6 +1788,12 @@ function validateIndexRequest(operation, args) {
|
|
|
1637
1788
|
operation,
|
|
1638
1789
|
args: [listOptions(args[0])]
|
|
1639
1790
|
};
|
|
1791
|
+
case "runs.list-resolved":
|
|
1792
|
+
exactArgs(args, 1);
|
|
1793
|
+
return {
|
|
1794
|
+
operation,
|
|
1795
|
+
args: [runListRequest(args[0])]
|
|
1796
|
+
};
|
|
1640
1797
|
case "runs.commit": return runCommit(args);
|
|
1641
1798
|
case "runs.expire":
|
|
1642
1799
|
exactArgs(args, 1);
|
|
@@ -1672,6 +1829,7 @@ function validateIndexRequest(operation, args) {
|
|
|
1672
1829
|
}
|
|
1673
1830
|
const INDEX_OPERATIONS = /* @__PURE__ */ new Set([
|
|
1674
1831
|
"runs.list",
|
|
1832
|
+
"runs.list-resolved",
|
|
1675
1833
|
"runs.commit",
|
|
1676
1834
|
"runs.expire",
|
|
1677
1835
|
"hooks.reserve",
|
|
@@ -1703,6 +1861,37 @@ function workflowIndex(env) {
|
|
|
1703
1861
|
hookIds: env.WORKFLOW_HOOK_IDS
|
|
1704
1862
|
});
|
|
1705
1863
|
}
|
|
1864
|
+
/**
|
|
1865
|
+
* Publish a committed applyEvent outcome to the derivative indexes inside the
|
|
1866
|
+
* fleet, sparing the client a round trip per run mutation. Each index write is
|
|
1867
|
+
* idempotent under the outcome's publication lease. A failed write leaves its
|
|
1868
|
+
* flag unset, so the client publishes it itself exactly as it would without
|
|
1869
|
+
* this step; older clients ignore the flags and publish again.
|
|
1870
|
+
*/
|
|
1871
|
+
async function publishApplyEventIndexes(env, runId, outcome) {
|
|
1872
|
+
if (typeof outcome !== "object" || outcome === null) return;
|
|
1873
|
+
const success = outcome;
|
|
1874
|
+
if (success.ok !== true) return;
|
|
1875
|
+
const index = workflowIndex(env);
|
|
1876
|
+
const publications = [];
|
|
1877
|
+
if (success.run && typeof success.indexPublicationExpiresAt === "number") {
|
|
1878
|
+
const run = success.run;
|
|
1879
|
+
const lease = success.indexPublicationExpiresAt;
|
|
1880
|
+
publications.push((async () => {
|
|
1881
|
+
await index.commitRun(run, runIndexMetadata(run), lease);
|
|
1882
|
+
success.runIndexPublished = true;
|
|
1883
|
+
})());
|
|
1884
|
+
}
|
|
1885
|
+
const releasedHooks = success.releasedHooks;
|
|
1886
|
+
if (Array.isArray(releasedHooks) && releasedHooks.length > 0) publications.push((async () => {
|
|
1887
|
+
await index.releaseHookIndexes({
|
|
1888
|
+
runId,
|
|
1889
|
+
hooks: releasedHooks
|
|
1890
|
+
});
|
|
1891
|
+
success.hookIndexesReleased = true;
|
|
1892
|
+
})());
|
|
1893
|
+
await Promise.allSettled(publications);
|
|
1894
|
+
}
|
|
1706
1895
|
const BINDINGS = {
|
|
1707
1896
|
runs: {
|
|
1708
1897
|
env: "WORKFLOW_DB",
|
|
@@ -1745,7 +1934,7 @@ const BINDINGS = {
|
|
|
1745
1934
|
};
|
|
1746
1935
|
/** Request body cap: oversize payloads get a clear 413 instead of an OOM. */
|
|
1747
1936
|
const MAX_BODY_BYTES = 33554432;
|
|
1748
|
-
const MAX_STREAM_WRITE_BODY_BYTES =
|
|
1937
|
+
const MAX_STREAM_WRITE_BODY_BYTES = 8391680;
|
|
1749
1938
|
const QUEUE_ORPHAN_GRACE_MS = 432e6;
|
|
1750
1939
|
function errorResponse(status, name, message) {
|
|
1751
1940
|
return Response.json({ error: {
|
|
@@ -1862,7 +2051,10 @@ function parseBoundedInteger(url, name, minimum, maximum) {
|
|
|
1862
2051
|
function streamResponse(body) {
|
|
1863
2052
|
return new Response(body, {
|
|
1864
2053
|
status: 200,
|
|
1865
|
-
headers: {
|
|
2054
|
+
headers: {
|
|
2055
|
+
"content-type": STREAM_BATCH_CONTENT_TYPE,
|
|
2056
|
+
[STREAM_CHUNK_LIMIT_HEADER]: String(512)
|
|
2057
|
+
}
|
|
1866
2058
|
});
|
|
1867
2059
|
}
|
|
1868
2060
|
function createRouter(env) {
|
|
@@ -2026,6 +2218,18 @@ function createRouter(env) {
|
|
|
2026
2218
|
case "runs.list":
|
|
2027
2219
|
result = await index.listRuns(...validated.args);
|
|
2028
2220
|
break;
|
|
2221
|
+
case "runs.list-resolved": {
|
|
2222
|
+
const runs = env.WORKFLOW_DB;
|
|
2223
|
+
if (!runs) return errorResponse(500, "WorldMisconfigured", "missing binding: WORKFLOW_DB");
|
|
2224
|
+
result = await listRunsPage({
|
|
2225
|
+
listRuns: (options) => index.listRuns(options),
|
|
2226
|
+
readRun: async (runId) => {
|
|
2227
|
+
const outcome = await runs.get(runs.idFromName(runId)).getRun();
|
|
2228
|
+
return outcome.ok ? outcome.value : null;
|
|
2229
|
+
}
|
|
2230
|
+
}, ...validated.args);
|
|
2231
|
+
break;
|
|
2232
|
+
}
|
|
2029
2233
|
case "runs.commit":
|
|
2030
2234
|
result = await index.commitRun(...validated.args);
|
|
2031
2235
|
break;
|
|
@@ -2081,7 +2285,7 @@ function createRouter(env) {
|
|
|
2081
2285
|
const readRequest = {
|
|
2082
2286
|
runId,
|
|
2083
2287
|
startIndex: parseBoundedInteger(url, "startIndex", 0, 2147483647),
|
|
2084
|
-
maxChunks: parseBoundedInteger(url, "maxChunks", 0,
|
|
2288
|
+
maxChunks: parseBoundedInteger(url, "maxChunks", 0, 512),
|
|
2085
2289
|
maxBytes: parseBoundedInteger(url, "maxBytes", MAX_STREAM_CHUNK_BYTES, MAX_STREAM_READ_BYTES),
|
|
2086
2290
|
waitMs: parseBoundedInteger(url, "waitMs", 0, MAX_STREAM_LONG_POLL_MS)
|
|
2087
2291
|
};
|
|
@@ -2144,6 +2348,7 @@ function createRouter(env) {
|
|
|
2144
2348
|
}
|
|
2145
2349
|
try {
|
|
2146
2350
|
const result = await namespace.get(namespace.idFromName(name))[method](...args);
|
|
2351
|
+
if (bindingKey === "runs" && method === "applyEvent") await publishApplyEventIndexes(env, name, result);
|
|
2147
2352
|
return new Response(rpcStringify(result ?? null), {
|
|
2148
2353
|
status: 200,
|
|
2149
2354
|
headers: { "content-type": "application/json" }
|
|
@@ -2713,9 +2918,18 @@ var RunCatalogDO = class extends DurableObject {
|
|
|
2713
2918
|
async upsertRun(runId, keys, serializedMetadata, publicationExpiresAt) {
|
|
2714
2919
|
const canonicalKeys = canonicalCatalogKeys(runId, keys);
|
|
2715
2920
|
return await this.ctx.storage.transaction(async (txn) => {
|
|
2716
|
-
|
|
2921
|
+
const recordKey = catalogKeysKey(runId);
|
|
2922
|
+
const existing = await txn.get([
|
|
2923
|
+
expiredKey(runId),
|
|
2924
|
+
canonicalKeys[0],
|
|
2925
|
+
canonicalKeys[1],
|
|
2926
|
+
recordKey
|
|
2927
|
+
]);
|
|
2928
|
+
if (existing.get(expiredKey(runId)) !== void 0) return { stored: false };
|
|
2717
2929
|
const now = this.now();
|
|
2718
2930
|
if (!Number.isSafeInteger(publicationExpiresAt) || publicationExpiresAt <= now || publicationExpiresAt > now + 1200900) return { stored: false };
|
|
2931
|
+
const record = existing.get(recordKey);
|
|
2932
|
+
if (existing.get(canonicalKeys[0]) === serializedMetadata && existing.get(canonicalKeys[1]) === serializedMetadata && record?.keys.length === canonicalKeys.length && record.keys.every((key, index) => key === canonicalKeys[index])) return { stored: true };
|
|
2719
2933
|
await txn.put({
|
|
2720
2934
|
[canonicalKeys[0]]: serializedMetadata,
|
|
2721
2935
|
[canonicalKeys[1]]: serializedMetadata,
|
|
@@ -2827,13 +3041,16 @@ const CHUNK_SIZE_KEY_PREFIX = "chunk-size:";
|
|
|
2827
3041
|
const STREAM_REGISTRY_PREFIX = "stream:";
|
|
2828
3042
|
const REGISTRY_OWNER_KEY = "registry:owner";
|
|
2829
3043
|
const REGISTRY_EXPIRED_KEY = "registry:expired";
|
|
2830
|
-
/** Each chunk occupies a payload key and a size key
|
|
3044
|
+
/** Each chunk occupies a payload key and a size key, deleted in calls of at most 128 keys. */
|
|
2831
3045
|
const DEFAULT_EXPIRE_CHUNK_LIMIT = 64;
|
|
2832
|
-
const MAX_EXPIRE_CHUNK_LIMIT =
|
|
3046
|
+
const MAX_EXPIRE_CHUNK_LIMIT = 256;
|
|
3047
|
+
const DELETE_BATCH_KEYS = 128;
|
|
2833
3048
|
/** Bound storage deletion work as well as item count; one oversized chunk still makes progress. */
|
|
2834
3049
|
const DEFAULT_EXPIRE_BYTE_LIMIT = 16777216;
|
|
2835
3050
|
const MAX_EXPIRE_BYTE_LIMIT = DEFAULT_EXPIRE_BYTE_LIMIT;
|
|
2836
3051
|
const MAX_STREAM_INDEX = 2147483647;
|
|
3052
|
+
/** One storage get or put call accepts at most 128 keys. */
|
|
3053
|
+
const STORAGE_BATCH_KEYS = 128;
|
|
2837
3054
|
function boundedLimit(value, fallback, maximum) {
|
|
2838
3055
|
if (value === void 0 || !Number.isFinite(value)) return fallback;
|
|
2839
3056
|
return Math.max(1, Math.min(maximum, Math.floor(value)));
|
|
@@ -2899,6 +3116,15 @@ var StreamDO = class extends DurableObject {
|
|
|
2899
3116
|
mutationTail = Promise.resolve();
|
|
2900
3117
|
changeVersion = 0;
|
|
2901
3118
|
waiters = /* @__PURE__ */ new Set();
|
|
3119
|
+
/** Multi-key read split into calls within the per-call key limit. */
|
|
3120
|
+
async getMany(keys) {
|
|
3121
|
+
if (keys.length <= STORAGE_BATCH_KEYS) return await this.ctx.storage.get(keys);
|
|
3122
|
+
const groups = [];
|
|
3123
|
+
for (let offset = 0; offset < keys.length; offset += STORAGE_BATCH_KEYS) groups.push(keys.slice(offset, offset + STORAGE_BATCH_KEYS));
|
|
3124
|
+
const merged = /* @__PURE__ */ new Map();
|
|
3125
|
+
for (const result of await Promise.all(groups.map((group) => this.ctx.storage.get(group)))) for (const [key, value] of result) merged.set(key, value);
|
|
3126
|
+
return merged;
|
|
3127
|
+
}
|
|
2902
3128
|
async getMeta() {
|
|
2903
3129
|
if (this.meta) return this.meta;
|
|
2904
3130
|
this.metaLoad ??= this.ctx.storage.get(META_KEY).then((meta) => {
|
|
@@ -2971,7 +3197,7 @@ var StreamDO = class extends DurableObject {
|
|
|
2971
3197
|
if (available > 0) {
|
|
2972
3198
|
const indexes = Array.from({ length: available }, (_, offset) => request.startIndex + offset);
|
|
2973
3199
|
const sizeKeys = indexes.map(chunkSizeKey);
|
|
2974
|
-
const storedSizes = await this.
|
|
3200
|
+
const storedSizes = await this.getMany(sizeKeys);
|
|
2975
3201
|
const selectedIndexes = [];
|
|
2976
3202
|
const selectedSizes = [];
|
|
2977
3203
|
let bytes = 0;
|
|
@@ -2983,7 +3209,7 @@ var StreamDO = class extends DurableObject {
|
|
|
2983
3209
|
bytes += size;
|
|
2984
3210
|
}
|
|
2985
3211
|
const payloadKeys = selectedIndexes.map(chunkKey);
|
|
2986
|
-
const storedChunks = await this.
|
|
3212
|
+
const storedChunks = await this.getMany(payloadKeys);
|
|
2987
3213
|
for (let offset = 0; offset < payloadKeys.length; offset++) {
|
|
2988
3214
|
const chunk = storedChunks.get(payloadKeys[offset]);
|
|
2989
3215
|
if (!(chunk instanceof Uint8Array) || chunk.byteLength !== selectedSizes[offset]) throw new Error(`Invalid persisted stream chunk at index ${selectedIndexes[offset]}`);
|
|
@@ -3015,13 +3241,13 @@ var StreamDO = class extends DurableObject {
|
|
|
3015
3241
|
state: "open",
|
|
3016
3242
|
ownerRunId: runId
|
|
3017
3243
|
};
|
|
3018
|
-
const entries =
|
|
3244
|
+
const entries = [[META_KEY, nextMeta]];
|
|
3019
3245
|
for (let offset = 0; offset < storedChunks.length; offset++) {
|
|
3020
3246
|
const index = startIndex + offset;
|
|
3021
|
-
entries[chunkKey(index)
|
|
3022
|
-
entries[chunkSizeKey(index)
|
|
3247
|
+
entries.push([chunkKey(index), storedChunks[offset]]);
|
|
3248
|
+
entries.push([chunkSizeKey(index), storedChunks[offset].byteLength]);
|
|
3023
3249
|
}
|
|
3024
|
-
await txn.put(entries);
|
|
3250
|
+
for (let offset = 0; offset < entries.length; offset += STORAGE_BATCH_KEYS) await txn.put(Object.fromEntries(entries.slice(offset, offset + STORAGE_BATCH_KEYS)));
|
|
3025
3251
|
return {
|
|
3026
3252
|
meta: nextMeta,
|
|
3027
3253
|
result: {
|
|
@@ -3201,7 +3427,7 @@ var StreamDO = class extends DurableObject {
|
|
|
3201
3427
|
}
|
|
3202
3428
|
}
|
|
3203
3429
|
const keys = page.flatMap(({ index }) => [chunkKey(index), chunkSizeKey(index)]);
|
|
3204
|
-
|
|
3430
|
+
for (let offset = 0; offset < keys.length; offset += DELETE_BATCH_KEYS) await txn.delete(keys.slice(offset, offset + DELETE_BATCH_KEYS));
|
|
3205
3431
|
let done = candidates.size === page.length;
|
|
3206
3432
|
if (payloadFallback) done = (await txn.list({
|
|
3207
3433
|
prefix: CHUNK_KEY_PREFIX,
|
|
@@ -3250,13 +3476,15 @@ var StreamDO = class extends DurableObject {
|
|
|
3250
3476
|
};
|
|
3251
3477
|
//#endregion
|
|
3252
3478
|
//#region src/worker/durable-objects/WorkflowRunDO.ts
|
|
3479
|
+
const EVENT_SEQUENCE_KEY = "event_sequence";
|
|
3253
3480
|
const STORAGE_BATCH_SIZE = 128;
|
|
3254
3481
|
const PAYLOAD_DELETE_BATCH = STORAGE_BATCH_SIZE;
|
|
3255
3482
|
const HOOK_MARKER_PAGE_SIZE = 64;
|
|
3256
3483
|
const TERMINAL_HOOK_PAGE_SIZE = 64;
|
|
3257
3484
|
const TERMINAL_WAIT_PAGE_SIZE = STORAGE_BATCH_SIZE;
|
|
3258
3485
|
const STREAMS_PER_CLEANUP_PAGE = 16;
|
|
3259
|
-
|
|
3486
|
+
/** StreamDO deletes a page in 128-key calls within one transaction. */
|
|
3487
|
+
const STREAM_CHUNKS_PER_CLEANUP_PAGE = 256;
|
|
3260
3488
|
const STREAM_BYTES_PER_CLEANUP_PAGE = 16777216;
|
|
3261
3489
|
const QUEUE_PAYLOADS_PER_CLEANUP_PAGE = STORAGE_BATCH_SIZE;
|
|
3262
3490
|
const CLEANUP_RETRY_MAX_MS = 36e5;
|
|
@@ -3338,11 +3566,14 @@ var WorkflowRunDO = class extends DurableObject {
|
|
|
3338
3566
|
if (!cleanup || cleanup.phase === "retained" && cleanup.dueAt.getTime() > now) return null;
|
|
3339
3567
|
return cleanupTombstone(cleanup, cleanup.tombstonedAt ?? cleanup.dueAt);
|
|
3340
3568
|
}
|
|
3341
|
-
async retentionState(storage, now = this.now()) {
|
|
3342
|
-
const
|
|
3569
|
+
async retentionState(storage, now = this.now(), options) {
|
|
3570
|
+
const keys = [TOMBSTONE_KEY, CLEANUP_RECORD_KEY];
|
|
3571
|
+
if (options?.withEventSequence) keys.push(EVENT_SEQUENCE_KEY);
|
|
3572
|
+
const values = await storage.get(keys);
|
|
3343
3573
|
return {
|
|
3344
3574
|
cleanup: values.get(CLEANUP_RECORD_KEY),
|
|
3345
|
-
tombstone: this.tombstoneFrom(values, now)
|
|
3575
|
+
tombstone: this.tombstoneFrom(values, now),
|
|
3576
|
+
eventSequence: values.get(EVENT_SEQUENCE_KEY) ?? 0
|
|
3346
3577
|
};
|
|
3347
3578
|
}
|
|
3348
3579
|
async read(reader) {
|
|
@@ -3377,21 +3608,22 @@ var WorkflowRunDO = class extends DurableObject {
|
|
|
3377
3608
|
if (request.cleanup !== void 0) validateCleanupRequest(request.cleanup, true);
|
|
3378
3609
|
return await this.ctx.storage.transaction(async (txn) => {
|
|
3379
3610
|
const now = new Date(this.now());
|
|
3380
|
-
const retention = await this.retentionState(txn, now.getTime());
|
|
3611
|
+
const retention = await this.retentionState(txn, now.getTime(), { withEventSequence: true });
|
|
3381
3612
|
const { tombstone } = retention;
|
|
3382
3613
|
if (tombstone) return {
|
|
3383
3614
|
ok: false,
|
|
3384
3615
|
code: "RUN_EXPIRED",
|
|
3385
3616
|
message: `Workflow run "${request.runId}" expired at ${tombstone.expiredAt.toISOString()}`
|
|
3386
3617
|
};
|
|
3387
|
-
|
|
3618
|
+
const storedSequence = retention.eventSequence;
|
|
3619
|
+
let eventSequence = storedSequence;
|
|
3388
3620
|
const outcome = await applyEvent(storeFrom(txn), {
|
|
3389
3621
|
...request,
|
|
3390
3622
|
nextEventId: () => slotToEventId(++eventSequence),
|
|
3391
3623
|
now
|
|
3392
3624
|
});
|
|
3393
3625
|
if (!outcome.ok) return outcome;
|
|
3394
|
-
await txn.put(
|
|
3626
|
+
if (eventSequence !== storedSequence) await txn.put(EVENT_SEQUENCE_KEY, eventSequence);
|
|
3395
3627
|
await putStorageEntries(txn, (outcome.hookToIndex ? [{
|
|
3396
3628
|
hookId: outcome.hookToIndex.hookId,
|
|
3397
3629
|
token: outcome.hookToIndex.token
|
|
@@ -3771,10 +4003,58 @@ var WorkflowRunDO = class extends DurableObject {
|
|
|
3771
4003
|
if (cleanup.phase === "payload") await this.deletePayloadBatch();
|
|
3772
4004
|
}
|
|
3773
4005
|
async executeTerminalCleanup(cleanup) {
|
|
4006
|
+
if (cleanup.phase === "hooks" && await this.completeSmallTerminalCleanup(cleanup)) return;
|
|
3774
4007
|
if (cleanup.phase === "hooks") await this.deleteTerminalHookPage(cleanup);
|
|
3775
4008
|
else if (cleanup.phase === "markers") await this.deleteTerminalHookMarkerPage(cleanup);
|
|
3776
4009
|
else await this.deleteTerminalWaitPage(cleanup);
|
|
3777
4010
|
}
|
|
4011
|
+
/**
|
|
4012
|
+
* Most runs end with a handful of hooks and waits. When every terminal
|
|
4013
|
+
* phase fits in one page, release the hook indexes once and delete hooks,
|
|
4014
|
+
* markers, and waits in a single transaction instead of one alarm and one
|
|
4015
|
+
* transaction per phase. Returns false, having changed nothing locally, when
|
|
4016
|
+
* any phase needs paging; the paged phases then run as before.
|
|
4017
|
+
*/
|
|
4018
|
+
async completeSmallTerminalCleanup(cleanup) {
|
|
4019
|
+
const [hookEntries, markerEntries] = await Promise.all([this.ctx.storage.list({
|
|
4020
|
+
prefix: HOOK_KEY_PREFIX,
|
|
4021
|
+
limit: 65
|
|
4022
|
+
}), this.ctx.storage.list({
|
|
4023
|
+
prefix: HOOK_MARKER_PREFIX,
|
|
4024
|
+
limit: 65
|
|
4025
|
+
})]);
|
|
4026
|
+
if (hookEntries.size > TERMINAL_HOOK_PAGE_SIZE || markerEntries.size > HOOK_MARKER_PAGE_SIZE) return false;
|
|
4027
|
+
const hooks = Array.from(hookEntries.values());
|
|
4028
|
+
const references = /* @__PURE__ */ new Map();
|
|
4029
|
+
for (const reference of [...hooks.map((hook) => ({
|
|
4030
|
+
hookId: hook.hookId,
|
|
4031
|
+
token: hook.token
|
|
4032
|
+
})), ...markerEntries.values()]) references.set(`${reference.hookId}\u0000${reference.token}`, reference);
|
|
4033
|
+
if (references.size > 0) await this.workflowIndex().releaseHookIndexes({
|
|
4034
|
+
runId: cleanup.runId,
|
|
4035
|
+
hooks: Array.from(references.values())
|
|
4036
|
+
});
|
|
4037
|
+
return await this.ctx.storage.transaction(async (txn) => {
|
|
4038
|
+
const current = await txn.get(TERMINAL_CLEANUP_KEY);
|
|
4039
|
+
if (!current || current.phase !== cleanup.phase || current.generation !== cleanup.generation) return true;
|
|
4040
|
+
const waits = await txn.list({
|
|
4041
|
+
prefix: WAIT_KEY_PREFIX,
|
|
4042
|
+
limit: 129
|
|
4043
|
+
});
|
|
4044
|
+
if (waits.size > TERMINAL_WAIT_PAGE_SIZE) return false;
|
|
4045
|
+
const keys = [
|
|
4046
|
+
...hooks.flatMap((hook) => [`${HOOK_KEY_PREFIX}${hook.hookId}`, hookCreationIndexKey(hook)]),
|
|
4047
|
+
...markerEntries.keys(),
|
|
4048
|
+
...waits.keys()
|
|
4049
|
+
];
|
|
4050
|
+
for (let offset = 0; offset < keys.length; offset += STORAGE_BATCH_SIZE) await txn.delete(keys.slice(offset, offset + STORAGE_BATCH_SIZE));
|
|
4051
|
+
await txn.delete(TERMINAL_CLEANUP_KEY);
|
|
4052
|
+
const retention = await txn.get(CLEANUP_RECORD_KEY);
|
|
4053
|
+
if (retention && retention.phase !== "tombstoned") await txn.setAlarm(Math.max(this.now() + 1, retention.dueAt.getTime()));
|
|
4054
|
+
else await txn.deleteAlarm();
|
|
4055
|
+
return true;
|
|
4056
|
+
});
|
|
4057
|
+
}
|
|
3778
4058
|
async deleteTerminalHookPage(cleanup) {
|
|
3779
4059
|
const entries = await this.ctx.storage.list({
|
|
3780
4060
|
prefix: HOOK_KEY_PREFIX,
|