@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/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;
@@ -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":"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"}
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-Ds44uuND.js";
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 over atob/btoa instead of
201
- * Buffer so this module is safe inside the celld worker bundle (workerd has
202
- * no Buffer global); atob/btoa are global in both workerd and Node >= 16.
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
- function b64encode(bytes) {
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
- const CHUNK = 32768;
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
- function b64decode(text) {
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 goes through atob/btoa.
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 > 32) throw protocolError(`batch exceeds 32 chunks`);
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 > 32) throw protocolError(`maxChunks must be between 0 and 32`);
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 > 32) throw protocolError("write count is out of range");
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 > 32 || chunkBytes > 8388736) throw protocolError("read result exceeds configured bounds");
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 = 8389632;
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: { "content-type": STREAM_BATCH_CONTENT_TYPE }
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, 32),
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
- if (await txn.get(expiredKey(runId)) !== void 0) return { stored: false };
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; one delete accepts at most 128 keys. */
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 = 64;
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.ctx.storage.get(sizeKeys);
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.ctx.storage.get(payloadKeys);
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 = { [META_KEY]: nextMeta };
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)] = storedChunks[offset];
3022
- entries[chunkSizeKey(index)] = storedChunks[offset].byteLength;
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
- if (keys.length > 0) await txn.delete(keys);
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
- const STREAM_CHUNKS_PER_CLEANUP_PAGE = STORAGE_BATCH_SIZE;
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 values = await storage.get([TOMBSTONE_KEY, CLEANUP_RECORD_KEY]);
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
- let eventSequence = await txn.get("event_sequence") ?? 0;
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("event_sequence", eventSequence);
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,