@ewhauser/world-celld 0.8.0 → 0.9.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/testing.js CHANGED
@@ -1,4 +1,4 @@
1
- import { A as isPositiveSafeInteger, E as MAX_RUN_INDEX_PUBLICATION_LIFETIME_MS, F as MAX_STREAM_CHUNK_BYTES, H as STREAM_CHUNK_LIMIT_HEADER, J as encodeStreamWriteResult, K as encodeStreamReadResult, L as MAX_STREAM_LONG_POLL_MS, P as MAX_STREAM_BATCH_BYTES, Q as validateStreamWriteChunks, R as MAX_STREAM_READ_BYTES, S as LIFECYCLE_COMPACTION_RETRY_MS, V as STREAM_BATCH_CONTENT_TYPE, W as decodeStreamWriteBatch, X as normalizeStreamError, Z as validateStreamReadRequest, a as isInlineRunBody, c as queueOrphanName, d as validateNativeQueueEnvelope, f as validateNativeQueueSendOptions, g as parse, h as rpcStringify, i as QUEUE_PAYLOAD_REGISTRY_PREFIX, j as isRecord$1, k as isNonNegativeSafeInteger, l as queuePayloadObjectKey, m as rpcParse, n as listRunsPage, p as compact, r as NATIVE_QUEUE_MAX_MESSAGE_BYTES, s as queueClaimName, t as MAX_RUN_LIST_LIMIT, u as queuePayloadRegistryKey, v as createWorkflowIndex, x as HOOK_CLAIM_LEASE_MS, y as runIndexMetadata } from "./run-list-C987PkJ2.js";
1
+ import { $ as normalizeStreamError, B as MAX_STREAM_LONG_POLL_MS, C as HOOK_CLAIM_LEASE_MS, F as queueDelayDeadline, G as STREAM_CHUNK_LIMIT_HEADER, L as MAX_STREAM_BATCH_BYTES, M as isNonNegativeSafeInteger, N as isPositiveSafeInteger, O as MAX_RUN_INDEX_PUBLICATION_LIFETIME_MS, P as isRecord$1, R as MAX_STREAM_CHUNK_BYTES, V as MAX_STREAM_READ_BYTES, W as STREAM_BATCH_CONTENT_TYPE, Y as encodeStreamReadResult, Z as encodeStreamWriteResult, _ as rpcStringify, a as QUEUE_PUBLICATION_LEASE_MS, b as createWorkflowIndex, c as nativeQueueDelaySeconds, d as queuePayloadObjectKey, et as validateStreamReadRequest, f as queuePayloadRegistryKey, g as rpcParse, h as compact, i as QUEUE_PAYLOAD_REGISTRY_PREFIX, k as QUEUE_CLAIM_STALE_MS, l as queueClaimName, m as validateNativeQueueSendOptions, n as listRunsPage, o as QUEUE_RESERVATION_GRACE_MS, p as validateNativeQueueEnvelope, q as decodeStreamWriteBatch, r as NATIVE_QUEUE_MAX_MESSAGE_BYTES, s as isInlineRunBody, t as MAX_RUN_LIST_LIMIT, tt as validateStreamWriteChunks, u as queueOrphanName, v as parse, w as LIFECYCLE_COMPACTION_RETRY_MS, x as runIndexMetadata } from "./run-list-C4B4SBYW.js";
2
2
  import { CreateEventSchema, EventSchema, HookSchema, SPEC_VERSION_CURRENT, StepSchema, WaitSchema, WorkflowRunSchema, applyAttributeChanges, eventIdToSlot, getMaxEventsPerRun, isChildEntityCreationEvent, isTerminalStepStatus, isTerminalWorkflowRunStatus, slotToEventId, validateAttributeChanges } from "@workflow/world";
3
3
  import http from "node:http";
4
4
  //#region src/testing/fake-cell.ts
@@ -546,8 +546,8 @@ function readStringProp(value, key) {
546
546
  }
547
547
  }
548
548
  /**
549
- * Map a failure eventData.error (which may be a string, an Error-shaped
550
- * object, or anything else) to the structured error stored on entities.
549
+ * Map a run failure eventData.error (which may be a string, an Error-shaped
550
+ * object, or anything else) to the structured error stored on runs.
551
551
  * Matches upstream: string errors keep their text, errorCode is preserved.
552
552
  */
553
553
  function toStructuredError(error, opts) {
@@ -1023,14 +1023,17 @@ async function applyEvent(store, ctx) {
1023
1023
  }
1024
1024
  if (!step) return failure("STEP_NOT_FOUND", `Step "${data.correlationId}" not found`);
1025
1025
  if (step.retryAfter && step.retryAfter.getTime() > now.getTime()) return failure("TOO_EARLY", `Cannot start step "${data.correlationId}": retryAfter timestamp has not been reached yet`, { retryAfterSeconds: Math.ceil((step.retryAfter.getTime() - now.getTime()) / 1e3) });
1026
- const updated = StepSchema.parse(compact({
1027
- ...step,
1028
- status: "running",
1029
- startedAt: step.startedAt ?? now,
1030
- attempt: step.attempt + 1,
1031
- retryAfter: void 0,
1032
- updatedAt: now
1033
- }));
1026
+ const updated = StepSchema.parse({
1027
+ ...compact({
1028
+ ...step,
1029
+ status: "running",
1030
+ startedAt: step.startedAt ?? now,
1031
+ attempt: step.attempt + 1,
1032
+ retryAfter: void 0,
1033
+ updatedAt: now
1034
+ }),
1035
+ error: step.error
1036
+ });
1034
1037
  const event = buildEvent({ ...data });
1035
1038
  await store.put(`${STEP_KEY_PREFIX}${data.correlationId}`, updated);
1036
1039
  await putEvent(event);
@@ -1045,13 +1048,16 @@ async function applyEvent(store, ctx) {
1045
1048
  case "step_completed": {
1046
1049
  const step = validatedStep;
1047
1050
  if (!step) return failure("STEP_NOT_FOUND", `Step "${data.correlationId}" not found`);
1048
- const updated = StepSchema.parse(compact({
1049
- ...step,
1050
- status: "completed",
1051
- output: data.eventData.result,
1052
- completedAt: now,
1053
- updatedAt: now
1054
- }));
1051
+ const updated = StepSchema.parse({
1052
+ ...compact({
1053
+ ...step,
1054
+ status: "completed",
1055
+ output: data.eventData.result,
1056
+ completedAt: now,
1057
+ updatedAt: now
1058
+ }),
1059
+ error: step.error
1060
+ });
1055
1061
  const event = buildEvent({ ...data });
1056
1062
  await store.put(`${STEP_KEY_PREFIX}${data.correlationId}`, updated);
1057
1063
  await putEvent(event);
@@ -1065,13 +1071,13 @@ async function applyEvent(store, ctx) {
1065
1071
  case "step_failed": {
1066
1072
  const step = validatedStep;
1067
1073
  if (!step) return failure("STEP_NOT_FOUND", `Step "${data.correlationId}" not found`);
1068
- const updated = StepSchema.parse(compact({
1074
+ const updated = StepSchema.parse({
1069
1075
  ...step,
1070
1076
  status: "failed",
1071
- error: toStructuredError(data.eventData.error),
1077
+ error: data.eventData.error,
1072
1078
  completedAt: now,
1073
1079
  updatedAt: now
1074
- }));
1080
+ });
1075
1081
  const event = buildEvent({ ...data });
1076
1082
  await store.put(`${STEP_KEY_PREFIX}${data.correlationId}`, updated);
1077
1083
  await putEvent(event);
@@ -1085,13 +1091,13 @@ async function applyEvent(store, ctx) {
1085
1091
  case "step_retrying": {
1086
1092
  const step = validatedStep;
1087
1093
  if (!step) return failure("STEP_NOT_FOUND", `Step "${data.correlationId}" not found`);
1088
- const updated = StepSchema.parse(compact({
1094
+ const updated = StepSchema.parse({
1089
1095
  ...step,
1090
1096
  status: "pending",
1091
- error: toStructuredError(data.eventData.error),
1097
+ error: data.eventData.error,
1092
1098
  retryAfter: data.eventData.retryAfter ? new Date(data.eventData.retryAfter) : void 0,
1093
1099
  updatedAt: now
1094
- }));
1100
+ });
1095
1101
  const event = buildEvent({ ...data });
1096
1102
  await store.put(`${STEP_KEY_PREFIX}${data.correlationId}`, updated);
1097
1103
  await putEvent(event);
@@ -1127,7 +1133,7 @@ async function applyEvent(store, ctx) {
1127
1133
  }
1128
1134
  if (await store.get(markerKey) !== void 0) {
1129
1135
  const existingHook = await store.get(hookKey);
1130
- if (existingHook && !holder) return {
1136
+ if (existingHook && !holder && existingHook.token === token) return {
1131
1137
  ok: true,
1132
1138
  hook: existingHook,
1133
1139
  hookToIndex: existingHook,
@@ -1355,6 +1361,11 @@ async function authenticate(request, secret) {
1355
1361
  }
1356
1362
  //#endregion
1357
1363
  //#region src/worker/queue-claims.ts
1364
+ /**
1365
+ * Idempotency claims for Queue messages. A claim lives either in its own
1366
+ * claim cell (named by queue and idempotency key) or, for run-bearing
1367
+ * messages whose envelope says so, as an entry in the run's own cell.
1368
+ */
1358
1369
  function cellQueueClaim(cell) {
1359
1370
  return {
1360
1371
  claim: (messageId, staleMs) => cell.claimInflight({
@@ -1363,6 +1374,11 @@ function cellQueueClaim(cell) {
1363
1374
  }),
1364
1375
  hold: (params) => cell.holdInflight(params),
1365
1376
  release: (messageId) => cell.releaseInflight(messageId),
1377
+ abandonReservation: (messageId) => cell.abandonQueueMessageReservation(messageId),
1378
+ confirmPublication: (messageId, expiresAt) => cell.confirmQueueMessagePublication({
1379
+ messageId,
1380
+ expiresAt
1381
+ }),
1366
1382
  complete: (messageId) => cell.completeQueueMessage(messageId)
1367
1383
  };
1368
1384
  }
@@ -1387,12 +1403,27 @@ function runQueueClaim(run, claimName) {
1387
1403
  claimName,
1388
1404
  messageId
1389
1405
  }),
1406
+ abandonReservation: (messageId) => run.abandonRunQueueMessageReservation({
1407
+ claimName,
1408
+ messageId
1409
+ }),
1410
+ confirmPublication: (messageId, expiresAt) => run.confirmRunQueueMessagePublication({
1411
+ claimName,
1412
+ messageId,
1413
+ expiresAt
1414
+ }),
1390
1415
  complete: (messageId) => run.completeRunQueueMessage({
1391
1416
  claimName,
1392
1417
  messageId
1393
1418
  })
1394
1419
  };
1395
1420
  }
1421
+ /** The claim a delivered envelope was reserved under, if it has an idempotency key. */
1422
+ function deliveryQueueClaim(envelope, runStub) {
1423
+ if (!envelope.idempotencyKey) return void 0;
1424
+ const claimName = queueClaimName(envelope.queueName, envelope.idempotencyKey);
1425
+ return envelope.claimScope === "run" && envelope.runId ? runQueueClaim(runStub(envelope.runId), claimName) : cellQueueClaim(runStub(claimName));
1426
+ }
1396
1427
  //#endregion
1397
1428
  //#region src/worker/index-validation.ts
1398
1429
  function invalid(message) {
@@ -1708,7 +1739,8 @@ const BINDINGS = {
1708
1739
  "scheduleCleanup",
1709
1740
  "cleanupNow",
1710
1741
  "rearmCleanup",
1711
- "resolveHookTokenClaim"
1742
+ "resolveHookTokenClaim",
1743
+ "resolveExpiredHookClaim"
1712
1744
  ])
1713
1745
  },
1714
1746
  streams: {
@@ -1735,7 +1767,6 @@ const BINDINGS = {
1735
1767
  /** Request body cap: oversize payloads get a clear 413 instead of an OOM. */
1736
1768
  const MAX_BODY_BYTES = 33554432;
1737
1769
  const MAX_STREAM_WRITE_BODY_BYTES = MAX_STREAM_BATCH_BYTES + 2048 + 1024;
1738
- const QUEUE_ORPHAN_GRACE_MS = 432e6;
1739
1770
  function errorResponse(status, name, message) {
1740
1771
  return Response.json({ error: {
1741
1772
  name,
@@ -1888,6 +1919,7 @@ function createRouter(env) {
1888
1919
  } catch (error) {
1889
1920
  return errorResponse(400, "BadRequest", error.message);
1890
1921
  }
1922
+ let enqueueClaim;
1891
1923
  try {
1892
1924
  const namespace = env.WORKFLOW_DB;
1893
1925
  const payloadStore = env.WORKFLOW_QUEUE_PAYLOADS;
@@ -1904,26 +1936,26 @@ function createRouter(env) {
1904
1936
  });
1905
1937
  if (new TextEncoder().encode(sized).byteLength > 128e3) return errorResponse(413, "PayloadTooLarge", `native queue envelope exceeds ${NATIVE_QUEUE_MAX_MESSAGE_BYTES} bytes`);
1906
1938
  const now = Date.now();
1907
- const reservationExpiresAt = Math.max(now, envelope.notBefore ?? now) + QUEUE_ORPHAN_GRACE_MS;
1939
+ const reservationExpiresAt = Math.max(now, envelope.notBefore ?? now) + QUEUE_RESERVATION_GRACE_MS;
1908
1940
  const run = envelope.runId ? namespace.get(namespace.idFromName(envelope.runId)) : void 0;
1909
- let claim;
1910
1941
  let admission;
1911
1942
  if (envelope.idempotencyKey) {
1912
1943
  const claimName = queueClaimName(envelope.queueName, envelope.idempotencyKey);
1913
1944
  let scope = run && envelope.runId ? runClaimScopes.get(envelope.runId) : "cell";
1914
1945
  let reservation;
1915
1946
  if (run && envelope.runId && scope !== "cell") {
1947
+ enqueueClaim = runQueueClaim(run, claimName);
1916
1948
  const reserved = await run.reserveRunQueueMessage({
1917
1949
  claimName,
1918
1950
  messageId: envelope.messageId,
1919
- expiresAt: reservationExpiresAt
1951
+ expiresAt: now + QUEUE_PUBLICATION_LEASE_MS,
1952
+ publicationPending: true
1920
1953
  });
1921
1954
  if (!reserved.ok) return errorResponse(410, "RunExpiredError", reserved.message);
1922
1955
  scope = reserved.scope;
1923
1956
  rememberRunClaimScope(envelope.runId, reserved.scope);
1924
1957
  if (reserved.scope === "run") {
1925
1958
  reservation = reserved;
1926
- claim = runQueueClaim(run, claimName);
1927
1959
  brokerEnvelope = {
1928
1960
  ...brokerEnvelope,
1929
1961
  claimScope: "run"
@@ -1935,27 +1967,34 @@ function createRouter(env) {
1935
1967
  }
1936
1968
  if (!reservation) {
1937
1969
  const cell = namespace.get(namespace.idFromName(claimName));
1970
+ enqueueClaim = cellQueueClaim(cell);
1938
1971
  reservation = await cell.reserveQueueMessage({
1939
1972
  messageId: envelope.messageId,
1940
- expiresAt: reservationExpiresAt
1973
+ expiresAt: now + QUEUE_PUBLICATION_LEASE_MS,
1974
+ publicationPending: true
1975
+ });
1976
+ }
1977
+ if (!reservation.admitted) {
1978
+ if (reservation.publicationPending) return errorResponse(503, "QueuePublicationPending", "the reserved Queue message has not been confirmed by the broker; retry publication");
1979
+ return new Response(rpcStringify({ messageId: reservation.messageId }), {
1980
+ status: 200,
1981
+ headers: { "content-type": "application/json" }
1941
1982
  });
1942
- claim = cellQueueClaim(cell);
1943
1983
  }
1944
- if (!reservation.admitted) return new Response(rpcStringify({ messageId: reservation.messageId }), {
1945
- status: 200,
1946
- headers: { "content-type": "application/json" }
1947
- });
1948
1984
  } else if (inlineRunBody && run) admission = run.getQueueAdmission();
1949
1985
  const encoded = JSON.stringify(brokerEnvelope);
1950
1986
  if (admission) {
1951
1987
  const admitted = await admission;
1952
1988
  if (!admitted.ok) {
1953
- await claim?.complete(envelope.messageId);
1989
+ await enqueueClaim?.complete(envelope.messageId);
1954
1990
  return errorResponse(410, "RunExpiredError", admitted.message);
1955
1991
  }
1956
1992
  }
1957
1993
  if (run && envelope.runId && payloadKey) {
1958
- if (!payloadStore) return errorResponse(500, "WorldMisconfigured", "missing binding: WORKFLOW_QUEUE_PAYLOADS");
1994
+ if (!payloadStore) {
1995
+ await enqueueClaim?.abandonReservation(envelope.messageId);
1996
+ return errorResponse(500, "WorldMisconfigured", "missing binding: WORKFLOW_QUEUE_PAYLOADS");
1997
+ }
1959
1998
  const orphanExpiresAt = reservationExpiresAt;
1960
1999
  const registered = await run.registerQueuePayload({
1961
2000
  messageId: envelope.messageId,
@@ -1963,7 +2002,7 @@ function createRouter(env) {
1963
2002
  orphanExpiresAt
1964
2003
  });
1965
2004
  if (!registered.ok) {
1966
- await claim?.complete(envelope.messageId);
2005
+ await enqueueClaim?.complete(envelope.messageId);
1967
2006
  return errorResponse(410, "RunExpiredError", registered.message);
1968
2007
  }
1969
2008
  const orphan = namespace.get(namespace.idFromName(queueOrphanName(envelope.messageId)));
@@ -1976,30 +2015,18 @@ function createRouter(env) {
1976
2015
  });
1977
2016
  } catch (error) {
1978
2017
  await run.unregisterQueuePayload(envelope.messageId);
1979
- await claim?.complete(envelope.messageId);
1980
- throw error;
1981
- }
1982
- try {
1983
- await payloadStore.write(payloadKey, envelope.body, {
1984
- runId: envelope.runId,
1985
- messageId: envelope.messageId
1986
- });
1987
- } catch (error) {
1988
- await claim?.complete(envelope.messageId);
1989
- throw error;
1990
- }
1991
- let finalized;
1992
- try {
1993
- finalized = await run.finalizeQueuePayload(envelope.messageId);
1994
- } catch (error) {
1995
- await claim?.complete(envelope.messageId);
1996
2018
  throw error;
1997
2019
  }
2020
+ await payloadStore.write(payloadKey, envelope.body, {
2021
+ runId: envelope.runId,
2022
+ messageId: envelope.messageId
2023
+ });
2024
+ const finalized = await run.finalizeQueuePayload(envelope.messageId);
1998
2025
  if (!finalized.ok) {
1999
2026
  await payloadStore.delete(payloadKey);
2000
2027
  await run.unregisterQueuePayload(envelope.messageId);
2001
2028
  await orphan.cancelQueuePayloadOrphan(envelope.messageId);
2002
- await claim?.complete(envelope.messageId);
2029
+ await enqueueClaim?.complete(envelope.messageId);
2003
2030
  return errorResponse(410, "RunExpiredError", finalized.message);
2004
2031
  }
2005
2032
  }
@@ -2007,11 +2034,17 @@ function createRouter(env) {
2007
2034
  contentType: "text",
2008
2035
  ...options
2009
2036
  });
2037
+ await enqueueClaim?.confirmPublication(envelope.messageId, reservationExpiresAt);
2010
2038
  return new Response(rpcStringify({ messageId: envelope.messageId }), {
2011
2039
  status: 200,
2012
2040
  headers: { "content-type": "application/json" }
2013
2041
  });
2014
2042
  } catch (error) {
2043
+ try {
2044
+ await enqueueClaim?.abandonReservation(envelope.messageId);
2045
+ } catch (cleanupError) {
2046
+ console.error("world-celld could not abandon a failed Queue reservation", cleanupError);
2047
+ }
2015
2048
  const err = error;
2016
2049
  return errorResponse(500, err.name ?? "Error", err.message ?? String(error));
2017
2050
  }
@@ -2244,6 +2277,16 @@ async function runIsActive(env, runId) {
2244
2277
  if (!namespace) throw new Error("world-celld hook index missing WORKFLOW_DB binding");
2245
2278
  return await namespace.get(namespace.idFromName(runId)).getLifecycleStatus() === "active";
2246
2279
  }
2280
+ /** Resolve a stale shard reservation in its authoritative event writer. */
2281
+ async function resolveExpiredHookClaim(env, owner, claimId, token) {
2282
+ const namespace = env.WORKFLOW_DB;
2283
+ if (!namespace) throw new Error("world-celld hook index missing WORKFLOW_DB binding");
2284
+ return await namespace.get(namespace.idFromName(owner.runId)).resolveExpiredHookClaim({
2285
+ hookId: owner.hookId,
2286
+ claimId,
2287
+ token
2288
+ });
2289
+ }
2247
2290
  //#endregion
2248
2291
  //#region src/worker/durable-objects/HookIdDO.ts
2249
2292
  const MAX_RELEASE_BATCH$1 = 64;
@@ -2272,52 +2315,69 @@ var HookIdDO = class extends DurableObject {
2272
2315
  return await runIsActive(this.env, owner.runId) ? raw : null;
2273
2316
  }
2274
2317
  async reserve(hookId, owner, proposedClaimId) {
2275
- return await this.ctx.storage.transaction(async (txn) => {
2276
- const record = recordKey$1(hookId);
2277
- const claim = claimKey$1(hookId);
2278
- const values = await txn.get([record, claim]);
2279
- const indexed = values.get(record);
2280
- if (indexed !== void 0) return {
2281
- claimed: false,
2282
- holder: ownerFromSerializedHook(indexed)
2283
- };
2284
- let existing = values.get(claim);
2285
- const now = this.now();
2286
- if (existing && existing.expiresAt <= now) {
2287
- await txn.delete([claim, existing.deadlineKey]);
2288
- existing = void 0;
2289
- }
2290
- if (existing !== void 0 && !sameOwner(existing.owner, owner)) return {
2291
- claimed: false,
2292
- holder: existing.owner
2293
- };
2294
- if (existing === void 0 || existing.claimId !== proposedClaimId) {
2295
- if (existing) await txn.delete(existing.deadlineKey);
2296
- const expiresAt = now + HOOK_CLAIM_LEASE_MS;
2297
- const deadlineKey = claimDeadlineKey$1(hookId, expiresAt);
2298
- await txn.put(claim, {
2299
- owner,
2300
- claimId: proposedClaimId,
2301
- reservedAt: now,
2302
- expiresAt,
2303
- deadlineKey
2304
- });
2305
- await txn.put(deadlineKey, {
2306
- hookId,
2307
- claimId: proposedClaimId,
2308
- expiresAt
2309
- });
2310
- await this.armAtMost(txn, expiresAt);
2318
+ for (;;) {
2319
+ const observed = await this.ctx.storage.get(claimKey$1(hookId));
2320
+ const expired = observed && observed.expiresAt <= this.now() ? observed : void 0;
2321
+ const committed = expired ? await resolveExpiredHookClaim(this.env, expired.owner, expired.claimId) : void 0;
2322
+ const result = await this.ctx.storage.transaction(async (txn) => {
2323
+ const record = recordKey$1(hookId);
2324
+ const claim = claimKey$1(hookId);
2325
+ const values = await txn.get([record, claim]);
2326
+ const indexed = values.get(record);
2327
+ if (indexed !== void 0) return {
2328
+ claimed: false,
2329
+ holder: ownerFromSerializedHook(indexed)
2330
+ };
2331
+ let existing = values.get(claim);
2332
+ const now = this.now();
2333
+ if (existing && existing.expiresAt <= now) {
2334
+ if (existing.claimId !== expired?.claimId || existing.expiresAt !== expired.expiresAt) return null;
2335
+ if (committed) {
2336
+ await this.renewClaim(txn, hookId, existing, now);
2337
+ return sameOwner(existing.owner, owner) ? {
2338
+ claimed: true,
2339
+ claimId: existing.claimId
2340
+ } : {
2341
+ claimed: false,
2342
+ holder: existing.owner
2343
+ };
2344
+ }
2345
+ await txn.delete([claim, existing.deadlineKey]);
2346
+ existing = void 0;
2347
+ }
2348
+ if (existing !== void 0 && !sameOwner(existing.owner, owner)) return {
2349
+ claimed: false,
2350
+ holder: existing.owner
2351
+ };
2352
+ if (existing === void 0 || existing.claimId !== proposedClaimId) {
2353
+ if (existing) await txn.delete(existing.deadlineKey);
2354
+ const expiresAt = now + HOOK_CLAIM_LEASE_MS;
2355
+ const deadlineKey = claimDeadlineKey$1(hookId, expiresAt);
2356
+ await txn.put(claim, {
2357
+ owner,
2358
+ claimId: proposedClaimId,
2359
+ reservedAt: now,
2360
+ expiresAt,
2361
+ deadlineKey
2362
+ });
2363
+ await txn.put(deadlineKey, {
2364
+ hookId,
2365
+ claimId: proposedClaimId,
2366
+ expiresAt
2367
+ });
2368
+ await this.armAtMost(txn, expiresAt);
2369
+ return {
2370
+ claimed: true,
2371
+ claimId: proposedClaimId
2372
+ };
2373
+ }
2311
2374
  return {
2312
2375
  claimed: true,
2313
- claimId: proposedClaimId
2376
+ claimId: existing.claimId
2314
2377
  };
2315
- }
2316
- return {
2317
- claimed: true,
2318
- claimId: existing.claimId
2319
- };
2320
- });
2378
+ });
2379
+ if (result) return result;
2380
+ }
2321
2381
  }
2322
2382
  async publish(hookId, serializedHook, owner, claimId) {
2323
2383
  if (!await runIsActive(this.env, owner.runId)) {
@@ -2334,10 +2394,7 @@ var HookIdDO = class extends DurableObject {
2334
2394
  const indexed = values.get(record);
2335
2395
  if (indexed !== void 0 && !sameOwner(ownerFromSerializedHook(indexed), owner)) throw new Error(`Hook id ${hookId} is owned by another hook`);
2336
2396
  const existingClaim = values.get(claim);
2337
- if (existingClaim && existingClaim.expiresAt <= this.now()) {
2338
- await txn.delete([claim, existingClaim.deadlineKey]);
2339
- return { stored: false };
2340
- }
2397
+ if (existingClaim && existingClaim.expiresAt <= this.now()) throw new Error(`Hook id ${hookId} reservation expired`);
2341
2398
  if (existingClaim !== void 0 && (!sameOwner(existingClaim.owner, owner) || existingClaim.claimId !== claimId)) throw new Error(`Hook id ${hookId} is reserved by another hook`);
2342
2399
  if (indexed === void 0 && existingClaim === void 0) throw new Error(`Hook id ${hookId} has no active reservation`);
2343
2400
  await txn.put(record, serializedHook);
@@ -2387,21 +2444,51 @@ var HookIdDO = class extends DurableObject {
2387
2444
  const current = await storage.getAlarm();
2388
2445
  if (current === null || at < current) await storage.setAlarm(at);
2389
2446
  }
2447
+ async renewClaim(txn, hookId, claim, now) {
2448
+ const expiresAt = now + HOOK_CLAIM_LEASE_MS;
2449
+ const deadlineKey = claimDeadlineKey$1(hookId, expiresAt);
2450
+ await txn.delete(claim.deadlineKey);
2451
+ await txn.put(claimKey$1(hookId), {
2452
+ ...claim,
2453
+ expiresAt,
2454
+ deadlineKey
2455
+ });
2456
+ await txn.put(deadlineKey, {
2457
+ hookId,
2458
+ claimId: claim.claimId,
2459
+ expiresAt
2460
+ });
2461
+ await this.armAtMost(txn, expiresAt);
2462
+ }
2390
2463
  async compactClaims() {
2391
2464
  const now = this.now();
2465
+ const due = await this.ctx.storage.list({
2466
+ prefix: CLAIM_DEADLINE_PREFIX$1,
2467
+ end: `${CLAIM_DEADLINE_PREFIX$1}${pad$2(now + 1)}`,
2468
+ limit: 129
2469
+ });
2470
+ const page = Array.from(due).slice(0, 128);
2471
+ const observed = await this.ctx.storage.get(page.map(([, item]) => claimKey$1(item.hookId)));
2472
+ const committed = /* @__PURE__ */ new Map();
2473
+ await Promise.all(page.map(async ([, item]) => {
2474
+ const claim = observed.get(claimKey$1(item.hookId));
2475
+ if (!claim || claim.claimId !== item.claimId || claim.expiresAt > now) return;
2476
+ const hook = await resolveExpiredHookClaim(this.env, claim.owner, claim.claimId);
2477
+ committed.set(claimKey$1(item.hookId), hook !== null);
2478
+ }));
2392
2479
  await this.ctx.storage.transaction(async (txn) => {
2393
- const due = await txn.list({
2394
- prefix: CLAIM_DEADLINE_PREFIX$1,
2395
- end: `${CLAIM_DEADLINE_PREFIX$1}${pad$2(now + 1)}`,
2396
- limit: 129
2397
- });
2398
- const page = Array.from(due).slice(0, 128);
2399
2480
  const claims = await txn.get(page.map(([, item]) => claimKey$1(item.hookId)));
2400
- const deletes = page.flatMap(([deadline, item]) => {
2481
+ const deletes = [];
2482
+ for (const [deadline, item] of page) {
2401
2483
  const key = claimKey$1(item.hookId);
2402
2484
  const claim = claims.get(key);
2403
- return claim?.claimId === item.claimId && claim.expiresAt <= now ? [deadline, key] : [deadline];
2404
- });
2485
+ const snapshot = observed.get(key);
2486
+ if (claim?.claimId !== item.claimId || claim.expiresAt > now) deletes.push(deadline);
2487
+ else if (claim.claimId === snapshot?.claimId && claim.expiresAt === snapshot.expiresAt) {
2488
+ if (committed.get(key)) await this.renewClaim(txn, item.hookId, claim, now);
2489
+ else deletes.push(deadline, key);
2490
+ }
2491
+ }
2405
2492
  for (let offset = 0; offset < deletes.length; offset += 128) await txn.delete(deletes.slice(offset, offset + 128));
2406
2493
  if (due.size > 128) {
2407
2494
  await txn.setAlarm(now + 1);
@@ -2444,51 +2531,68 @@ var HookTokenDO = class extends DurableObject {
2444
2531
  return await runIsActive(this.env, owner.runId) ? raw : null;
2445
2532
  }
2446
2533
  async reserve(token, owner, proposedClaimId) {
2447
- return await this.ctx.storage.transaction(async (txn) => {
2448
- const record = recordKey(token);
2449
- const claim = claimKey(token);
2450
- const values = await txn.get([record, claim]);
2451
- const indexed = values.get(record);
2452
- if (indexed !== void 0) return {
2453
- claimed: false,
2454
- holder: ownerFromSerializedHook(indexed)
2455
- };
2456
- let existing = values.get(claim);
2457
- const now = this.now();
2458
- if (existing && existing.expiresAt <= now) {
2459
- await txn.delete([claim, existing.deadlineKey]);
2460
- existing = void 0;
2461
- }
2462
- if (existing !== void 0 && !sameOwner(existing.owner, owner)) return {
2463
- claimed: false,
2464
- holder: existing.owner
2465
- };
2466
- if (existing === void 0) {
2467
- const expiresAt = now + HOOK_CLAIM_LEASE_MS;
2468
- const deadlineKey = claimDeadlineKey(token, expiresAt);
2469
- await txn.put(claim, {
2470
- owner,
2471
- claimId: proposedClaimId,
2472
- reservedAt: now,
2473
- expiresAt,
2474
- deadlineKey
2475
- });
2476
- await txn.put(deadlineKey, {
2477
- token,
2478
- claimId: proposedClaimId,
2479
- expiresAt
2480
- });
2481
- await this.armAtMost(txn, expiresAt);
2534
+ for (;;) {
2535
+ const observed = await this.ctx.storage.get(claimKey(token));
2536
+ const expired = observed && observed.expiresAt <= this.now() ? observed : void 0;
2537
+ const committed = expired ? await resolveExpiredHookClaim(this.env, expired.owner, expired.claimId, token) : void 0;
2538
+ const result = await this.ctx.storage.transaction(async (txn) => {
2539
+ const record = recordKey(token);
2540
+ const claim = claimKey(token);
2541
+ const values = await txn.get([record, claim]);
2542
+ const indexed = values.get(record);
2543
+ if (indexed !== void 0) return {
2544
+ claimed: false,
2545
+ holder: ownerFromSerializedHook(indexed)
2546
+ };
2547
+ let existing = values.get(claim);
2548
+ const now = this.now();
2549
+ if (existing && existing.expiresAt <= now) {
2550
+ if (existing.claimId !== expired?.claimId || existing.expiresAt !== expired.expiresAt) return null;
2551
+ if (committed) {
2552
+ await this.renewClaim(txn, token, existing, now);
2553
+ return sameOwner(existing.owner, owner) ? {
2554
+ claimed: true,
2555
+ claimId: existing.claimId
2556
+ } : {
2557
+ claimed: false,
2558
+ holder: existing.owner
2559
+ };
2560
+ }
2561
+ await txn.delete([claim, existing.deadlineKey]);
2562
+ existing = void 0;
2563
+ }
2564
+ if (existing !== void 0 && !sameOwner(existing.owner, owner)) return {
2565
+ claimed: false,
2566
+ holder: existing.owner
2567
+ };
2568
+ if (existing === void 0) {
2569
+ const expiresAt = now + HOOK_CLAIM_LEASE_MS;
2570
+ const deadlineKey = claimDeadlineKey(token, expiresAt);
2571
+ await txn.put(claim, {
2572
+ owner,
2573
+ claimId: proposedClaimId,
2574
+ reservedAt: now,
2575
+ expiresAt,
2576
+ deadlineKey
2577
+ });
2578
+ await txn.put(deadlineKey, {
2579
+ token,
2580
+ claimId: proposedClaimId,
2581
+ expiresAt
2582
+ });
2583
+ await this.armAtMost(txn, expiresAt);
2584
+ return {
2585
+ claimed: true,
2586
+ claimId: proposedClaimId
2587
+ };
2588
+ }
2482
2589
  return {
2483
2590
  claimed: true,
2484
- claimId: proposedClaimId
2591
+ claimId: existing.claimId
2485
2592
  };
2486
- }
2487
- return {
2488
- claimed: true,
2489
- claimId: existing.claimId
2490
- };
2491
- });
2593
+ });
2594
+ if (result) return result;
2595
+ }
2492
2596
  }
2493
2597
  async finalize(token, serializedHook, owner, claimId) {
2494
2598
  if (!await runIsActive(this.env, owner.runId)) {
@@ -2505,10 +2609,7 @@ var HookTokenDO = class extends DurableObject {
2505
2609
  const indexed = values.get(record);
2506
2610
  if (indexed !== void 0 && !sameOwner(ownerFromSerializedHook(indexed), owner)) throw new Error(`Hook token ${token} is owned by another hook`);
2507
2611
  const existingClaim = values.get(claim);
2508
- if (existingClaim && existingClaim.expiresAt <= this.now()) {
2509
- await txn.delete([claim, existingClaim.deadlineKey]);
2510
- return { stored: false };
2511
- }
2612
+ if (existingClaim && existingClaim.expiresAt <= this.now()) throw new Error(`Hook token ${token} reservation expired`);
2512
2613
  if (existingClaim !== void 0 && (!sameOwner(existingClaim.owner, owner) || existingClaim.claimId !== claimId)) throw new Error(`Hook token ${token} is reserved by another hook`);
2513
2614
  if (indexed === void 0 && existingClaim === void 0) throw new Error(`Hook token ${token} has no active reservation`);
2514
2615
  await txn.put(record, serializedHook);
@@ -2558,21 +2659,51 @@ var HookTokenDO = class extends DurableObject {
2558
2659
  const current = await storage.getAlarm();
2559
2660
  if (current === null || at < current) await storage.setAlarm(at);
2560
2661
  }
2662
+ async renewClaim(txn, token, claim, now) {
2663
+ const expiresAt = now + HOOK_CLAIM_LEASE_MS;
2664
+ const deadlineKey = claimDeadlineKey(token, expiresAt);
2665
+ await txn.delete(claim.deadlineKey);
2666
+ await txn.put(claimKey(token), {
2667
+ ...claim,
2668
+ expiresAt,
2669
+ deadlineKey
2670
+ });
2671
+ await txn.put(deadlineKey, {
2672
+ token,
2673
+ claimId: claim.claimId,
2674
+ expiresAt
2675
+ });
2676
+ await this.armAtMost(txn, expiresAt);
2677
+ }
2561
2678
  async compactClaims() {
2562
2679
  const now = this.now();
2680
+ const due = await this.ctx.storage.list({
2681
+ prefix: CLAIM_DEADLINE_PREFIX,
2682
+ end: `${CLAIM_DEADLINE_PREFIX}${pad$1(now + 1)}`,
2683
+ limit: 129
2684
+ });
2685
+ const page = Array.from(due).slice(0, 128);
2686
+ const observed = await this.ctx.storage.get(page.map(([, item]) => claimKey(item.token)));
2687
+ const committed = /* @__PURE__ */ new Map();
2688
+ await Promise.all(page.map(async ([, item]) => {
2689
+ const claim = observed.get(claimKey(item.token));
2690
+ if (!claim || claim.claimId !== item.claimId || claim.expiresAt > now) return;
2691
+ const hook = await resolveExpiredHookClaim(this.env, claim.owner, claim.claimId, item.token);
2692
+ committed.set(claimKey(item.token), hook !== null);
2693
+ }));
2563
2694
  await this.ctx.storage.transaction(async (txn) => {
2564
- const due = await txn.list({
2565
- prefix: CLAIM_DEADLINE_PREFIX,
2566
- end: `${CLAIM_DEADLINE_PREFIX}${pad$1(now + 1)}`,
2567
- limit: 129
2568
- });
2569
- const page = Array.from(due).slice(0, 128);
2570
2695
  const claims = await txn.get(page.map(([, item]) => claimKey(item.token)));
2571
- const deletes = page.flatMap(([deadline, item]) => {
2696
+ const deletes = [];
2697
+ for (const [deadline, item] of page) {
2572
2698
  const key = claimKey(item.token);
2573
2699
  const claim = claims.get(key);
2574
- return claim?.claimId === item.claimId && claim.expiresAt <= now ? [deadline, key] : [deadline];
2575
- });
2700
+ const snapshot = observed.get(key);
2701
+ if (claim?.claimId !== item.claimId || claim.expiresAt > now) deletes.push(deadline);
2702
+ else if (claim.claimId === snapshot?.claimId && claim.expiresAt === snapshot.expiresAt) {
2703
+ if (committed.get(key)) await this.renewClaim(txn, item.token, claim, now);
2704
+ else deletes.push(deadline, key);
2705
+ }
2706
+ }
2576
2707
  for (let offset = 0; offset < deletes.length; offset += 128) await txn.delete(deletes.slice(offset, offset + 128));
2577
2708
  if (due.size > 128) {
2578
2709
  await txn.setAlarm(now + 1);
@@ -3049,6 +3180,15 @@ var StreamDO = class extends DurableObject {
3049
3180
  size: validateSegmentSize(entry[1], start)
3050
3181
  });
3051
3182
  }
3183
+ let covered = startIndex;
3184
+ for (let index = 0; index < planned.length; index++) {
3185
+ const { start, size } = planned[index];
3186
+ if (start > covered) throw new Error(`Missing persisted stream segment at index ${covered}`);
3187
+ if (index > 0 && start !== covered) throw new Error(`Overlapping persisted stream segment at index ${start}`);
3188
+ if (start + size.count <= covered) throw new Error(`Missing persisted stream segment at index ${covered}`);
3189
+ covered = start + size.count;
3190
+ }
3191
+ if (covered < endIndex) throw new Error(`Missing persisted stream segment at index ${covered}`);
3052
3192
  const selected = [];
3053
3193
  let plannedBytes = 0;
3054
3194
  for (const segment of planned) {
@@ -3659,6 +3799,28 @@ var WorkflowRunDO = class extends DurableObject {
3659
3799
  async getRun() {
3660
3800
  return this.readKey("run");
3661
3801
  }
3802
+ /** Fence both supported event-claim forms before an expired shard claim is released. */
3803
+ async resolveExpiredHookClaim(request) {
3804
+ if (typeof request?.hookId !== "string" || request.hookId.length === 0 || typeof request.claimId !== "string" || request.claimId.length === 0 || request.token !== void 0 && typeof request.token !== "string") throw new TypeError("world-celld expired hook claim resolution is invalid");
3805
+ return await this.ctx.storage.transaction(async (txn) => {
3806
+ const { tombstone } = await this.retentionState(txn);
3807
+ if (tombstone) return null;
3808
+ const run = await txn.get("run");
3809
+ if (run && isTerminalWorkflowRunStatus(run.status)) return null;
3810
+ const hook = await txn.get(`${HOOK_KEY_PREFIX}${request.hookId}`);
3811
+ if (hook && (request.token === void 0 || hook.token === request.token)) return hook;
3812
+ const cancellation = {
3813
+ hookId: request.hookId,
3814
+ token: request.token,
3815
+ canceledAt: this.now()
3816
+ };
3817
+ await txn.put({
3818
+ [hookClaimCancellationKey(`${request.claimId}:${request.claimId}`)]: cancellation,
3819
+ [hookClaimCancellationKey(`-:${request.claimId}`)]: cancellation
3820
+ });
3821
+ return null;
3822
+ });
3823
+ }
3662
3824
  /** Single authoritative lifecycle answer used after derivative compaction. */
3663
3825
  async getLifecycleStatus() {
3664
3826
  return await this.ctx.storage.transaction(async (txn) => {
@@ -4290,7 +4452,7 @@ var WorkflowRunDO = class extends DurableObject {
4290
4452
  * the run expiry check.
4291
4453
  */
4292
4454
  async reserveRunQueueMessage(params) {
4293
- if (!validRunQueueClaimName(params?.claimName) || typeof params.messageId !== "string" || params.messageId.length === 0 || !isNonNegativeSafeInteger(params.expiresAt)) throw new TypeError("world-celld run queue reservation is invalid");
4455
+ if (!validRunQueueClaimName(params?.claimName) || typeof params.messageId !== "string" || params.messageId.length === 0 || !isNonNegativeSafeInteger(params.expiresAt) || params.publicationPending !== void 0 && typeof params.publicationPending !== "boolean") throw new TypeError("world-celld run queue reservation is invalid");
4294
4456
  return await this.ctx.storage.transaction(async (txn) => {
4295
4457
  const key = runQueueClaimKey(params.claimName);
4296
4458
  const values = await txn.get([
@@ -4310,17 +4472,19 @@ var WorkflowRunDO = class extends DurableObject {
4310
4472
  };
4311
4473
  const state = values.get(key) ?? {};
4312
4474
  const existing = state.reservation;
4313
- if (existing && existing.expiresAt > this.now() && existing.messageId !== params.messageId) return {
4475
+ if (existing && existing.expiresAt > this.now() && (existing.messageId !== params.messageId || params.publicationPending)) return {
4314
4476
  ok: true,
4315
4477
  scope: "run",
4316
4478
  admitted: false,
4317
- messageId: existing.messageId
4479
+ messageId: existing.messageId,
4480
+ ...existing.publicationPending ? { publicationPending: true } : {}
4318
4481
  };
4319
4482
  await txn.put(key, {
4320
4483
  ...state,
4321
4484
  reservation: {
4322
4485
  messageId: params.messageId,
4323
- expiresAt: Math.max(existing?.expiresAt ?? 0, params.expiresAt)
4486
+ expiresAt: Math.max(existing?.expiresAt ?? 0, params.expiresAt),
4487
+ ...params.publicationPending ? { publicationPending: true } : {}
4324
4488
  }
4325
4489
  });
4326
4490
  return {
@@ -4331,6 +4495,21 @@ var WorkflowRunDO = class extends DurableObject {
4331
4495
  };
4332
4496
  });
4333
4497
  }
4498
+ async confirmRunQueueMessagePublication(params) {
4499
+ if (!validRunQueueClaimName(params?.claimName) || typeof params.messageId !== "string" || params.messageId.length === 0 || !isNonNegativeSafeInteger(params.expiresAt)) throw new TypeError("world-celld run queue publication confirmation is invalid");
4500
+ await this.ctx.storage.transaction(async (txn) => {
4501
+ const key = runQueueClaimKey(params.claimName);
4502
+ const state = await txn.get(key);
4503
+ if (state?.reservation?.messageId !== params.messageId) return;
4504
+ await txn.put(key, {
4505
+ ...state,
4506
+ reservation: {
4507
+ messageId: params.messageId,
4508
+ expiresAt: Math.max(state.reservation.expiresAt, params.expiresAt)
4509
+ }
4510
+ });
4511
+ });
4512
+ }
4334
4513
  /** Delivery-side run expiry check and in-flight claim in one transaction. */
4335
4514
  async claimRunQueueMessage(params) {
4336
4515
  if (!validRunQueueClaimName(params?.claimName) || typeof params.messageId !== "string" || params.messageId.length === 0 || !isPositiveSafeInteger(params.staleMs)) throw new TypeError("world-celld run queue claim is invalid");
@@ -4342,9 +4521,19 @@ var WorkflowRunDO = class extends DurableObject {
4342
4521
  key
4343
4522
  ]);
4344
4523
  if (this.tombstoneFrom(values)) return { expired: true };
4345
- const state = values.get(key) ?? {};
4346
- const existing = state.claim;
4524
+ let state = values.get(key) ?? {};
4347
4525
  const now = this.now();
4526
+ if (state.reservation?.publicationPending && state.reservation.messageId === params.messageId) {
4527
+ state = {
4528
+ ...state,
4529
+ reservation: {
4530
+ messageId: params.messageId,
4531
+ expiresAt: Math.max(state.reservation.expiresAt, now + QUEUE_RESERVATION_GRACE_MS)
4532
+ }
4533
+ };
4534
+ await txn.put(key, state);
4535
+ }
4536
+ const existing = state.claim;
4348
4537
  if (existing && existing.expiresAt > now) {
4349
4538
  if (existing.messageId !== params.messageId) return {
4350
4539
  expired: false,
@@ -4416,6 +4605,18 @@ var WorkflowRunDO = class extends DurableObject {
4416
4605
  else await txn.delete(key);
4417
4606
  });
4418
4607
  }
4608
+ /** Failed enqueue cleanup must preserve delivery already accepted by the broker. */
4609
+ async abandonRunQueueMessageReservation(params) {
4610
+ if (!validRunQueueClaimName(params?.claimName) || typeof params.messageId !== "string" || params.messageId.length === 0) throw new TypeError("world-celld run queue reservation abandonment is invalid");
4611
+ await this.ctx.storage.transaction(async (txn) => {
4612
+ const key = runQueueClaimKey(params.claimName);
4613
+ const state = await txn.get(key);
4614
+ if (state?.reservation?.messageId !== params.messageId) return;
4615
+ const { reservation: _abandoned, ...rest } = state;
4616
+ if (rest.claim) await txn.put(key, rest);
4617
+ else await txn.delete(key);
4618
+ });
4619
+ }
4419
4620
  async registerQueuePayload(registration) {
4420
4621
  if (!registration || typeof registration.messageId !== "string" || registration.messageId.length === 0 || typeof registration.key !== "string" || registration.key.length === 0 || !isNonNegativeSafeInteger(registration.orphanExpiresAt)) throw new TypeError("world-celld queue payload registration is invalid");
4421
4622
  return await this.ctx.storage.transaction(async (txn) => {
@@ -4472,22 +4673,24 @@ var WorkflowRunDO = class extends DurableObject {
4472
4673
  });
4473
4674
  }
4474
4675
  async reserveQueueMessage(params) {
4475
- if (typeof params?.messageId !== "string" || params.messageId.length === 0 || !isNonNegativeSafeInteger(params.expiresAt)) throw new TypeError("world-celld queue message reservation is invalid");
4676
+ if (typeof params?.messageId !== "string" || params.messageId.length === 0 || !isNonNegativeSafeInteger(params.expiresAt) || params.publicationPending !== void 0 && typeof params.publicationPending !== "boolean") throw new TypeError("world-celld queue message reservation is invalid");
4476
4677
  return await this.ctx.storage.transaction(async (txn) => {
4477
4678
  const values = await txn.get([QUEUE_RESERVATION_KEY, "claim"]);
4478
4679
  const existing = values.get(QUEUE_RESERVATION_KEY);
4479
4680
  const claim = values.get("claim");
4480
4681
  const now = this.now();
4481
- if (existing && existing.expiresAt > now && existing.messageId !== params.messageId) {
4682
+ if (existing && existing.expiresAt > now && (existing.messageId !== params.messageId || params.publicationPending)) {
4482
4683
  await txn.setAlarm(Math.min(existing.expiresAt, claim?.expiresAt ?? existing.expiresAt));
4483
4684
  return {
4484
4685
  admitted: false,
4485
- messageId: existing.messageId
4686
+ messageId: existing.messageId,
4687
+ ...existing.publicationPending ? { publicationPending: true } : {}
4486
4688
  };
4487
4689
  }
4488
4690
  const reservation = {
4489
4691
  messageId: params.messageId,
4490
- expiresAt: Math.max(existing?.expiresAt ?? 0, params.expiresAt)
4692
+ expiresAt: Math.max(existing?.expiresAt ?? 0, params.expiresAt),
4693
+ ...params.publicationPending ? { publicationPending: true } : {}
4491
4694
  };
4492
4695
  await txn.put(QUEUE_RESERVATION_KEY, reservation);
4493
4696
  await txn.setAlarm(Math.min(reservation.expiresAt, claim?.expiresAt ?? reservation.expiresAt));
@@ -4497,6 +4700,20 @@ var WorkflowRunDO = class extends DurableObject {
4497
4700
  };
4498
4701
  });
4499
4702
  }
4703
+ async confirmQueueMessagePublication(params) {
4704
+ if (typeof params?.messageId !== "string" || params.messageId.length === 0 || !isNonNegativeSafeInteger(params.expiresAt)) throw new TypeError("world-celld queue publication confirmation is invalid");
4705
+ await this.ctx.storage.transaction(async (txn) => {
4706
+ const values = await txn.get([QUEUE_RESERVATION_KEY, "claim"]);
4707
+ const existing = values.get(QUEUE_RESERVATION_KEY);
4708
+ if (existing?.messageId !== params.messageId) return;
4709
+ const reservation = {
4710
+ messageId: params.messageId,
4711
+ expiresAt: Math.max(existing.expiresAt, params.expiresAt)
4712
+ };
4713
+ await txn.put(QUEUE_RESERVATION_KEY, reservation);
4714
+ await txn.setAlarm(Math.min(reservation.expiresAt, values.get("claim")?.expiresAt ?? reservation.expiresAt));
4715
+ });
4716
+ }
4500
4717
  async completeQueueMessage(messageId) {
4501
4718
  if (typeof messageId !== "string" || messageId.length === 0) throw new TypeError("world-celld queue messageId must be a non-empty string");
4502
4719
  await this.ctx.storage.transaction(async (txn) => {
@@ -4516,13 +4733,32 @@ var WorkflowRunDO = class extends DurableObject {
4516
4733
  else await txn.deleteAlarm();
4517
4734
  });
4518
4735
  }
4736
+ /** Abandon the enqueue reservation without releasing an active delivery claim. */
4737
+ async abandonQueueMessageReservation(messageId) {
4738
+ if (typeof messageId !== "string" || messageId.length === 0) throw new TypeError("world-celld queue reservation messageId must be a non-empty string");
4739
+ await this.ctx.storage.transaction(async (txn) => {
4740
+ const values = await txn.get([QUEUE_RESERVATION_KEY, "claim"]);
4741
+ if (values.get(QUEUE_RESERVATION_KEY)?.messageId !== messageId) return;
4742
+ await txn.delete(QUEUE_RESERVATION_KEY);
4743
+ const claim = values.get("claim");
4744
+ if (claim) await txn.setAlarm(claim.expiresAt);
4745
+ else await txn.deleteAlarm();
4746
+ });
4747
+ }
4519
4748
  async claimInflight(params) {
4520
4749
  if (typeof params?.messageId !== "string" || params.messageId.length === 0 || !isPositiveSafeInteger(params.staleMs)) throw new TypeError("world-celld queue claim is invalid");
4521
4750
  return await this.ctx.storage.transaction(async (txn) => {
4522
4751
  const values = await txn.get(["claim", QUEUE_RESERVATION_KEY]);
4523
4752
  const existing = values.get("claim");
4524
- const reservation = values.get(QUEUE_RESERVATION_KEY);
4753
+ let reservation = values.get(QUEUE_RESERVATION_KEY);
4525
4754
  const now = this.now();
4755
+ if (reservation?.publicationPending && reservation.messageId === params.messageId) {
4756
+ reservation = {
4757
+ messageId: params.messageId,
4758
+ expiresAt: Math.max(reservation.expiresAt, now + QUEUE_RESERVATION_GRACE_MS)
4759
+ };
4760
+ await txn.put(QUEUE_RESERVATION_KEY, reservation);
4761
+ }
4526
4762
  if (existing && existing.expiresAt > now) {
4527
4763
  if (existing.messageId !== params.messageId) return { claimed: false };
4528
4764
  if (existing.retryAt === void 0 || existing.retryAt > now) return {
@@ -4576,6 +4812,146 @@ var WorkflowRunDO = class extends DurableObject {
4576
4812
  }
4577
4813
  };
4578
4814
  //#endregion
4815
+ //#region src/worker/queue-delivery.ts
4816
+ /** Internal delivery contract. No Request/Response objects cross the RPC boundary. */
4817
+ const MAX_QUEUE_DELIVERY_RESPONSE_BYTES = 65536;
4818
+ const QUEUE_ORPHAN_GRACE_MS = 432e6;
4819
+ const PERMANENT_QUEUE_STATUSES = /* @__PURE__ */ new Set([
4820
+ 404,
4821
+ 409,
4822
+ 410,
4823
+ 422
4824
+ ]);
4825
+ async function readResponseText(response) {
4826
+ if (!response.body) return "";
4827
+ const reader = response.body.getReader();
4828
+ const decoder = new TextDecoder();
4829
+ let total = 0;
4830
+ let text = "";
4831
+ try {
4832
+ for (;;) {
4833
+ const { done, value } = await reader.read();
4834
+ if (done) break;
4835
+ total += value.byteLength;
4836
+ if (total > MAX_QUEUE_DELIVERY_RESPONSE_BYTES) {
4837
+ await reader.cancel("queue delivery response is too large").catch(() => void 0);
4838
+ throw new Error("workflow queue handler response exceeds configured limit");
4839
+ }
4840
+ text += decoder.decode(value, { stream: true });
4841
+ }
4842
+ return text + decoder.decode();
4843
+ } finally {
4844
+ reader.releaseLock();
4845
+ }
4846
+ }
4847
+ /** Validate at the service boundary; callers cannot bypass auth via RPC. */
4848
+ async function deliverQueueMessage(env, secret, input, attempt) {
4849
+ if (!env.WORLD_SECRET) throw new Error("WORLD_SECRET is not configured");
4850
+ if (typeof secret !== "string" || !secret || !await timingSafeEqual(secret, env.WORLD_SECRET)) throw new Error("Unauthorized Queue delivery");
4851
+ const envelope = validateNativeQueueEnvelope(input);
4852
+ if (!Number.isSafeInteger(attempt) || attempt < 1) throw new TypeError("queue delivery attempt must be a positive safe integer");
4853
+ if (!env.WORKFLOW_DB) throw new Error("missing binding: WORKFLOW_DB");
4854
+ const namespace = env.WORKFLOW_DB;
4855
+ const payloadStore = env.WORKFLOW_QUEUE_PAYLOADS;
4856
+ const runStub = (name) => namespace.get(namespace.idFromName(name));
4857
+ const claim = deliveryQueueClaim(envelope, runStub);
4858
+ let claimed = false;
4859
+ async function releaseClaim() {
4860
+ if (claimed) await claim.release(envelope.messageId);
4861
+ }
4862
+ async function unregisterPayload() {
4863
+ if (envelope.runId) {
4864
+ await runStub(envelope.runId).unregisterQueuePayload(envelope.messageId);
4865
+ await runStub(queueOrphanName(envelope.messageId)).cancelQueuePayloadOrphan(envelope.messageId);
4866
+ }
4867
+ }
4868
+ try {
4869
+ let admission;
4870
+ if (envelope.runId && !envelope.payloadKey && envelope.claimScope !== "run") {
4871
+ admission = runStub(envelope.runId).getQueueAdmission();
4872
+ admission.catch(() => void 0);
4873
+ }
4874
+ if (claim) {
4875
+ const result = await claim.claim(envelope.messageId, QUEUE_CLAIM_STALE_MS);
4876
+ if (result.expired) return { kind: "complete" };
4877
+ if (!result.claimed) {
4878
+ const now = Date.now();
4879
+ if (result.retryAt !== void 0 && result.retryAt > now) return {
4880
+ kind: "suspend",
4881
+ timeoutSeconds: Math.max(1, Math.ceil((result.retryAt - now) / 1e3))
4882
+ };
4883
+ return { kind: "complete" };
4884
+ }
4885
+ claimed = true;
4886
+ }
4887
+ if (admission && !(await admission).ok) {
4888
+ await claim?.complete(envelope.messageId);
4889
+ return { kind: "complete" };
4890
+ }
4891
+ let body = envelope.body;
4892
+ if (envelope.payloadKey) {
4893
+ if (!payloadStore) throw new Error("missing binding: WORKFLOW_QUEUE_PAYLOADS");
4894
+ body = await payloadStore.read(envelope.payloadKey) ?? void 0;
4895
+ if (body === void 0) {
4896
+ await unregisterPayload();
4897
+ await claim?.complete(envelope.messageId);
4898
+ return { kind: "complete" };
4899
+ }
4900
+ }
4901
+ const headers = {
4902
+ "content-type": "application/json",
4903
+ "x-vqs-queue-name": envelope.queueName,
4904
+ "x-vqs-message-id": envelope.messageId,
4905
+ "x-vqs-message-attempt": String(attempt)
4906
+ };
4907
+ if (env.WORKFLOW_CALLBACK_SECRET) headers["x-workflow-callback-secret"] = env.WORKFLOW_CALLBACK_SECRET;
4908
+ const callback = await fetch(`${envelope.targetBaseUrl.replace(/\/$/, "")}/.well-known/workflow/v1/flow`, {
4909
+ method: "POST",
4910
+ headers,
4911
+ body,
4912
+ signal: AbortSignal.timeout(3e5)
4913
+ });
4914
+ if (callback.ok || PERMANENT_QUEUE_STATUSES.has(callback.status)) {
4915
+ await callback.body?.cancel().catch(() => void 0);
4916
+ if (envelope.payloadKey) {
4917
+ await payloadStore.delete(envelope.payloadKey);
4918
+ await unregisterPayload();
4919
+ }
4920
+ await claim?.complete(envelope.messageId);
4921
+ return { kind: "complete" };
4922
+ }
4923
+ if (callback.status === 503) {
4924
+ const responseBody = await readResponseText(callback);
4925
+ const timeoutSeconds = JSON.parse(responseBody).timeoutSeconds;
4926
+ const notBefore = queueDelayDeadline(Date.now(), timeoutSeconds, 1, QUEUE_CLAIM_STALE_MS);
4927
+ if (notBefore === null) throw new Error("workflow queue suspension deadline is invalid");
4928
+ if (envelope.payloadKey && envelope.runId) await runStub(queueOrphanName(envelope.messageId)).scheduleQueuePayloadOrphan({
4929
+ messageId: envelope.messageId,
4930
+ runId: envelope.runId,
4931
+ key: envelope.payloadKey,
4932
+ expiresAt: notBefore + QUEUE_ORPHAN_GRACE_MS
4933
+ });
4934
+ if (claim) await claim.hold({
4935
+ messageId: envelope.messageId,
4936
+ retryAt: notBefore,
4937
+ expiresAt: notBefore + QUEUE_CLAIM_STALE_MS,
4938
+ reservationExpiresAt: notBefore + QUEUE_ORPHAN_GRACE_MS
4939
+ });
4940
+ return {
4941
+ kind: "suspend",
4942
+ timeoutSeconds
4943
+ };
4944
+ }
4945
+ await callback.body?.cancel().catch(() => void 0);
4946
+ await releaseClaim();
4947
+ claimed = false;
4948
+ return { kind: "retry" };
4949
+ } catch (error) {
4950
+ await releaseClaim();
4951
+ throw error;
4952
+ }
4953
+ }
4954
+ //#endregion
4579
4955
  //#region src/testing/http-harness.ts
4580
4956
  /**
4581
4957
  * Boots the real worker router over real cell classes (on FakeFleet state)
@@ -4584,6 +4960,7 @@ var WorkflowRunDO = class extends DurableObject {
4584
4960
  async function startHarness(options = {}) {
4585
4961
  const cellEnv = { ...options.cellEnv };
4586
4962
  const queueMessages = [];
4963
+ const queuePublications = [];
4587
4964
  const queuePayloads = /* @__PURE__ */ new Map();
4588
4965
  const queuePayloadBucket = {
4589
4966
  async put(key, value) {
@@ -4597,8 +4974,12 @@ async function startHarness(options = {}) {
4597
4974
  for (const key of Array.isArray(keys) ? keys : [keys]) queuePayloads.delete(key);
4598
4975
  }
4599
4976
  };
4600
- const nativeQueue = { async send(body) {
4977
+ const nativeQueue = { async send(body, sendOptions) {
4601
4978
  queueMessages.push(body);
4979
+ queuePublications.push({
4980
+ body,
4981
+ delaySeconds: sendOptions?.delaySeconds ?? 0
4982
+ });
4602
4983
  } };
4603
4984
  const fleet = new FakeFleet({
4604
4985
  runs: WorkflowRunDO,
@@ -4617,7 +4998,7 @@ async function startHarness(options = {}) {
4617
4998
  WORKFLOW_HOOK_IDS: fleet.namespace("hook-ids"),
4618
4999
  WORKFLOW_QUEUE_PAYLOADS: queuePayloadBucket
4619
5000
  });
4620
- const router = createRouter({
5001
+ const env = {
4621
5002
  WORKFLOW_DB: fleet.namespace("runs"),
4622
5003
  WORKFLOW_STREAMS: fleet.namespace("streams"),
4623
5004
  WORKFLOW_RUN_CATALOG: fleet.namespace("run-catalog"),
@@ -4626,7 +5007,8 @@ async function startHarness(options = {}) {
4626
5007
  WORKFLOW_QUEUE: nativeQueue,
4627
5008
  WORKFLOW_QUEUE_PAYLOADS: createQueuePayloadStore(queuePayloadBucket),
4628
5009
  WORLD_SECRET: options.secret
4629
- });
5010
+ };
5011
+ const router = createRouter(env);
4630
5012
  const server = http.createServer(async (req, res) => {
4631
5013
  const requestAbort = new AbortController();
4632
5014
  const abortRequest = () => {
@@ -4669,7 +5051,9 @@ async function startHarness(options = {}) {
4669
5051
  url: `http://127.0.0.1:${port}`,
4670
5052
  fleet,
4671
5053
  queueMessages,
5054
+ queuePublications,
4672
5055
  queuePayloads,
5056
+ deliverQueueMessage: (secret, envelope, attempt) => deliverQueueMessage(env, secret, envelope, attempt),
4673
5057
  close: () => new Promise((resolve, reject) => {
4674
5058
  server.close((err) => err ? reject(err) : resolve());
4675
5059
  server.closeAllConnections();
@@ -4677,11 +5061,148 @@ async function startHarness(options = {}) {
4677
5061
  };
4678
5062
  }
4679
5063
  //#endregion
5064
+ //#region src/worker/queue-consumer.ts
5065
+ function backoffSeconds(attempt) {
5066
+ return Math.min(60, 2 ** Math.min(attempt, 30));
5067
+ }
5068
+ async function republish(env, envelope, now = Date.now()) {
5069
+ await env.WORKFLOW_QUEUE.send(JSON.stringify(envelope), {
5070
+ contentType: "text",
5071
+ delaySeconds: nativeQueueDelaySeconds(envelope.notBefore, now)
5072
+ });
5073
+ }
5074
+ async function consume(message, env) {
5075
+ let envelope;
5076
+ try {
5077
+ if (typeof message.body !== "string") throw new TypeError("native Queue message body must be text");
5078
+ envelope = validateNativeQueueEnvelope(JSON.parse(message.body));
5079
+ } catch (error) {
5080
+ console.error("world-celld native Queue rejected malformed broker message", error);
5081
+ message.retry({ delaySeconds: backoffSeconds(message.attempts) });
5082
+ return;
5083
+ }
5084
+ const now = Date.now();
5085
+ if (envelope.notBefore !== void 0 && envelope.notBefore > now) {
5086
+ try {
5087
+ await republish(env, envelope, now);
5088
+ message.ack();
5089
+ } catch (error) {
5090
+ console.error("world-celld native Queue failed to continue a long delay", error);
5091
+ message.retry({ delaySeconds: backoffSeconds(message.attempts) });
5092
+ }
5093
+ return;
5094
+ }
5095
+ const attempt = (envelope.deliveryFailures ?? 0) + message.attempts;
5096
+ if (!env.WORLD_SECRET) {
5097
+ console.error("world-celld Queue consumer missing WORLD_SECRET");
5098
+ message.retry({ delaySeconds: backoffSeconds(attempt) });
5099
+ return;
5100
+ }
5101
+ let response;
5102
+ try {
5103
+ response = await env.WORLD_SERVICE.deliver(env.WORLD_SECRET, envelope, attempt);
5104
+ } catch (error) {
5105
+ console.error("world-celld Queue consumer could not reach the world service", error);
5106
+ message.retry({ delaySeconds: backoffSeconds(attempt) });
5107
+ return;
5108
+ }
5109
+ if (response?.kind === "complete") {
5110
+ message.ack();
5111
+ return;
5112
+ }
5113
+ if (response?.kind === "suspend") try {
5114
+ const notBefore = queueDelayDeadline(Date.now(), response.timeoutSeconds, 1, QUEUE_CLAIM_STALE_MS);
5115
+ const suspensionCount = (envelope.suspensionCount ?? 0) + 1;
5116
+ if (notBefore !== null && suspensionCount <= 256) {
5117
+ await republish(env, {
5118
+ ...envelope,
5119
+ notBefore,
5120
+ deliveryFailures: attempt - 1,
5121
+ suspensionCount
5122
+ });
5123
+ message.ack();
5124
+ return;
5125
+ }
5126
+ } catch (error) {
5127
+ console.error("world-celld Queue consumer received an invalid suspension response", error);
5128
+ }
5129
+ message.retry({ delaySeconds: backoffSeconds(attempt) });
5130
+ }
5131
+ var queue_consumer_default = { async queue(batch, env) {
5132
+ await Promise.all(batch.messages.map((message) => consume(message, env)));
5133
+ } };
5134
+ //#endregion
4680
5135
  //#region src/testing/index.ts
4681
5136
  /** Alarm-pump interval for the dev fleet (real time). */
4682
5137
  const PUMP_INTERVAL_MS = 25;
4683
5138
  async function startDevFleet(options = {}) {
4684
5139
  const harness = await startHarness(options);
5140
+ const pending = [];
5141
+ const deliveries = /* @__PURE__ */ new Set();
5142
+ let publicationCursor = 0;
5143
+ let stopped = false;
5144
+ const queueEnv = {
5145
+ WORKFLOW_QUEUE: { async send(body, sendOptions) {
5146
+ harness.queueMessages.push(body);
5147
+ harness.queuePublications.push({
5148
+ body,
5149
+ delaySeconds: sendOptions?.delaySeconds ?? 0
5150
+ });
5151
+ } },
5152
+ WORLD_SERVICE: { deliver: (secret, envelope, attempt) => harness.deliverQueueMessage(secret, envelope, attempt) },
5153
+ WORLD_SECRET: options.secret
5154
+ };
5155
+ function dispatch(message) {
5156
+ let settled = false;
5157
+ const delivery = queue_consumer_default.queue({ messages: [{
5158
+ body: message.body,
5159
+ attempts: message.attempts,
5160
+ ack() {
5161
+ settled = true;
5162
+ },
5163
+ retry(retryOptions) {
5164
+ settled = true;
5165
+ if (!stopped) pending.push({
5166
+ body: message.body,
5167
+ attempts: message.attempts + 1,
5168
+ dueAt: Date.now() + (retryOptions?.delaySeconds ?? 0) * 1e3
5169
+ });
5170
+ }
5171
+ }] }, queueEnv).then(() => {
5172
+ if (!settled) {
5173
+ console.error("[world-celld dev fleet] Queue delivery was not settled");
5174
+ if (!stopped) pending.push({
5175
+ body: message.body,
5176
+ attempts: message.attempts + 1,
5177
+ dueAt: Date.now() + 1e3
5178
+ });
5179
+ }
5180
+ }).catch((error) => {
5181
+ console.error("[world-celld dev fleet] Queue consumer failed", error);
5182
+ if (!settled && !stopped) pending.push({
5183
+ body: message.body,
5184
+ attempts: message.attempts + 1,
5185
+ dueAt: Date.now() + 1e3
5186
+ });
5187
+ });
5188
+ deliveries.add(delivery);
5189
+ delivery.finally(() => deliveries.delete(delivery));
5190
+ }
5191
+ const queuePump = setInterval(() => {
5192
+ if (stopped) return;
5193
+ while (publicationCursor < harness.queuePublications.length) {
5194
+ const publication = harness.queuePublications[publicationCursor++];
5195
+ pending.push({
5196
+ body: publication.body,
5197
+ attempts: 1,
5198
+ dueAt: Date.now() + publication.delaySeconds * 1e3
5199
+ });
5200
+ }
5201
+ const now = Date.now();
5202
+ for (let index = 0; index < pending.length;) if (pending[index].dueAt <= now) dispatch(pending.splice(index, 1)[0]);
5203
+ else index++;
5204
+ }, PUMP_INTERVAL_MS);
5205
+ queuePump.unref?.();
4685
5206
  let pumping = false;
4686
5207
  const pump = setInterval(async () => {
4687
5208
  if (pumping) return;
@@ -4699,7 +5220,10 @@ async function startDevFleet(options = {}) {
4699
5220
  return {
4700
5221
  ...harness,
4701
5222
  async stop() {
5223
+ stopped = true;
5224
+ clearInterval(queuePump);
4702
5225
  clearInterval(pump);
5226
+ await Promise.allSettled(deliveries);
4703
5227
  await harness.close();
4704
5228
  }
4705
5229
  };