@ewhauser/world-celld 0.8.0 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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 validateStreamReadRequest, B as MAX_STREAM_READ_BYTES, C as HOOK_CLAIM_LEASE_MS, I as MAX_STREAM_BATCH_BYTES, J as encodeStreamReadResult, K as decodeStreamWriteBatch, L as MAX_STREAM_CHUNK_BYTES, M as isPositiveSafeInteger, N as isRecord$1, O as MAX_RUN_INDEX_PUBLICATION_LIFETIME_MS, Q as normalizeStreamError, U as STREAM_BATCH_CONTENT_TYPE, W as STREAM_CHUNK_LIMIT_HEADER, X as encodeStreamWriteResult, _ as rpcStringify, a as QUEUE_PUBLICATION_LEASE_MS, b as createWorkflowIndex, d as queuePayloadObjectKey, et as validateStreamWriteChunks, f as queuePayloadRegistryKey, g as rpcParse, h as compact, i as QUEUE_PAYLOAD_REGISTRY_PREFIX, j as isNonNegativeSafeInteger, l as queueClaimName, m as validateNativeQueueSendOptions, n as listRunsPage, o as QUEUE_RESERVATION_GRACE_MS, p as validateNativeQueueEnvelope, r as NATIVE_QUEUE_MAX_MESSAGE_BYTES, s as isInlineRunBody, t as MAX_RUN_LIST_LIMIT, u as queueOrphanName, v as parse, w as LIFECYCLE_COMPACTION_RETRY_MS, x as runIndexMetadata, z as MAX_STREAM_LONG_POLL_MS } from "./run-list-DGsdv6Jf.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,
@@ -1363,6 +1369,11 @@ function cellQueueClaim(cell) {
1363
1369
  }),
1364
1370
  hold: (params) => cell.holdInflight(params),
1365
1371
  release: (messageId) => cell.releaseInflight(messageId),
1372
+ abandonReservation: (messageId) => cell.abandonQueueMessageReservation(messageId),
1373
+ confirmPublication: (messageId, expiresAt) => cell.confirmQueueMessagePublication({
1374
+ messageId,
1375
+ expiresAt
1376
+ }),
1366
1377
  complete: (messageId) => cell.completeQueueMessage(messageId)
1367
1378
  };
1368
1379
  }
@@ -1387,6 +1398,15 @@ function runQueueClaim(run, claimName) {
1387
1398
  claimName,
1388
1399
  messageId
1389
1400
  }),
1401
+ abandonReservation: (messageId) => run.abandonRunQueueMessageReservation({
1402
+ claimName,
1403
+ messageId
1404
+ }),
1405
+ confirmPublication: (messageId, expiresAt) => run.confirmRunQueueMessagePublication({
1406
+ claimName,
1407
+ messageId,
1408
+ expiresAt
1409
+ }),
1390
1410
  complete: (messageId) => run.completeRunQueueMessage({
1391
1411
  claimName,
1392
1412
  messageId
@@ -1708,7 +1728,8 @@ const BINDINGS = {
1708
1728
  "scheduleCleanup",
1709
1729
  "cleanupNow",
1710
1730
  "rearmCleanup",
1711
- "resolveHookTokenClaim"
1731
+ "resolveHookTokenClaim",
1732
+ "resolveExpiredHookClaim"
1712
1733
  ])
1713
1734
  },
1714
1735
  streams: {
@@ -1735,7 +1756,6 @@ const BINDINGS = {
1735
1756
  /** Request body cap: oversize payloads get a clear 413 instead of an OOM. */
1736
1757
  const MAX_BODY_BYTES = 33554432;
1737
1758
  const MAX_STREAM_WRITE_BODY_BYTES = MAX_STREAM_BATCH_BYTES + 2048 + 1024;
1738
- const QUEUE_ORPHAN_GRACE_MS = 432e6;
1739
1759
  function errorResponse(status, name, message) {
1740
1760
  return Response.json({ error: {
1741
1761
  name,
@@ -1888,6 +1908,7 @@ function createRouter(env) {
1888
1908
  } catch (error) {
1889
1909
  return errorResponse(400, "BadRequest", error.message);
1890
1910
  }
1911
+ let enqueueClaim;
1891
1912
  try {
1892
1913
  const namespace = env.WORKFLOW_DB;
1893
1914
  const payloadStore = env.WORKFLOW_QUEUE_PAYLOADS;
@@ -1904,26 +1925,26 @@ function createRouter(env) {
1904
1925
  });
1905
1926
  if (new TextEncoder().encode(sized).byteLength > 128e3) return errorResponse(413, "PayloadTooLarge", `native queue envelope exceeds ${NATIVE_QUEUE_MAX_MESSAGE_BYTES} bytes`);
1906
1927
  const now = Date.now();
1907
- const reservationExpiresAt = Math.max(now, envelope.notBefore ?? now) + QUEUE_ORPHAN_GRACE_MS;
1928
+ const reservationExpiresAt = Math.max(now, envelope.notBefore ?? now) + QUEUE_RESERVATION_GRACE_MS;
1908
1929
  const run = envelope.runId ? namespace.get(namespace.idFromName(envelope.runId)) : void 0;
1909
- let claim;
1910
1930
  let admission;
1911
1931
  if (envelope.idempotencyKey) {
1912
1932
  const claimName = queueClaimName(envelope.queueName, envelope.idempotencyKey);
1913
1933
  let scope = run && envelope.runId ? runClaimScopes.get(envelope.runId) : "cell";
1914
1934
  let reservation;
1915
1935
  if (run && envelope.runId && scope !== "cell") {
1936
+ enqueueClaim = runQueueClaim(run, claimName);
1916
1937
  const reserved = await run.reserveRunQueueMessage({
1917
1938
  claimName,
1918
1939
  messageId: envelope.messageId,
1919
- expiresAt: reservationExpiresAt
1940
+ expiresAt: now + QUEUE_PUBLICATION_LEASE_MS,
1941
+ publicationPending: true
1920
1942
  });
1921
1943
  if (!reserved.ok) return errorResponse(410, "RunExpiredError", reserved.message);
1922
1944
  scope = reserved.scope;
1923
1945
  rememberRunClaimScope(envelope.runId, reserved.scope);
1924
1946
  if (reserved.scope === "run") {
1925
1947
  reservation = reserved;
1926
- claim = runQueueClaim(run, claimName);
1927
1948
  brokerEnvelope = {
1928
1949
  ...brokerEnvelope,
1929
1950
  claimScope: "run"
@@ -1935,27 +1956,34 @@ function createRouter(env) {
1935
1956
  }
1936
1957
  if (!reservation) {
1937
1958
  const cell = namespace.get(namespace.idFromName(claimName));
1959
+ enqueueClaim = cellQueueClaim(cell);
1938
1960
  reservation = await cell.reserveQueueMessage({
1939
1961
  messageId: envelope.messageId,
1940
- expiresAt: reservationExpiresAt
1962
+ expiresAt: now + QUEUE_PUBLICATION_LEASE_MS,
1963
+ publicationPending: true
1964
+ });
1965
+ }
1966
+ if (!reservation.admitted) {
1967
+ if (reservation.publicationPending) return errorResponse(503, "QueuePublicationPending", "the reserved Queue message has not been confirmed by the broker; retry publication");
1968
+ return new Response(rpcStringify({ messageId: reservation.messageId }), {
1969
+ status: 200,
1970
+ headers: { "content-type": "application/json" }
1941
1971
  });
1942
- claim = cellQueueClaim(cell);
1943
1972
  }
1944
- if (!reservation.admitted) return new Response(rpcStringify({ messageId: reservation.messageId }), {
1945
- status: 200,
1946
- headers: { "content-type": "application/json" }
1947
- });
1948
1973
  } else if (inlineRunBody && run) admission = run.getQueueAdmission();
1949
1974
  const encoded = JSON.stringify(brokerEnvelope);
1950
1975
  if (admission) {
1951
1976
  const admitted = await admission;
1952
1977
  if (!admitted.ok) {
1953
- await claim?.complete(envelope.messageId);
1978
+ await enqueueClaim?.complete(envelope.messageId);
1954
1979
  return errorResponse(410, "RunExpiredError", admitted.message);
1955
1980
  }
1956
1981
  }
1957
1982
  if (run && envelope.runId && payloadKey) {
1958
- if (!payloadStore) return errorResponse(500, "WorldMisconfigured", "missing binding: WORKFLOW_QUEUE_PAYLOADS");
1983
+ if (!payloadStore) {
1984
+ await enqueueClaim?.abandonReservation(envelope.messageId);
1985
+ return errorResponse(500, "WorldMisconfigured", "missing binding: WORKFLOW_QUEUE_PAYLOADS");
1986
+ }
1959
1987
  const orphanExpiresAt = reservationExpiresAt;
1960
1988
  const registered = await run.registerQueuePayload({
1961
1989
  messageId: envelope.messageId,
@@ -1963,7 +1991,7 @@ function createRouter(env) {
1963
1991
  orphanExpiresAt
1964
1992
  });
1965
1993
  if (!registered.ok) {
1966
- await claim?.complete(envelope.messageId);
1994
+ await enqueueClaim?.complete(envelope.messageId);
1967
1995
  return errorResponse(410, "RunExpiredError", registered.message);
1968
1996
  }
1969
1997
  const orphan = namespace.get(namespace.idFromName(queueOrphanName(envelope.messageId)));
@@ -1976,30 +2004,18 @@ function createRouter(env) {
1976
2004
  });
1977
2005
  } catch (error) {
1978
2006
  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
2007
  throw error;
1997
2008
  }
2009
+ await payloadStore.write(payloadKey, envelope.body, {
2010
+ runId: envelope.runId,
2011
+ messageId: envelope.messageId
2012
+ });
2013
+ const finalized = await run.finalizeQueuePayload(envelope.messageId);
1998
2014
  if (!finalized.ok) {
1999
2015
  await payloadStore.delete(payloadKey);
2000
2016
  await run.unregisterQueuePayload(envelope.messageId);
2001
2017
  await orphan.cancelQueuePayloadOrphan(envelope.messageId);
2002
- await claim?.complete(envelope.messageId);
2018
+ await enqueueClaim?.complete(envelope.messageId);
2003
2019
  return errorResponse(410, "RunExpiredError", finalized.message);
2004
2020
  }
2005
2021
  }
@@ -2007,11 +2023,17 @@ function createRouter(env) {
2007
2023
  contentType: "text",
2008
2024
  ...options
2009
2025
  });
2026
+ await enqueueClaim?.confirmPublication(envelope.messageId, reservationExpiresAt);
2010
2027
  return new Response(rpcStringify({ messageId: envelope.messageId }), {
2011
2028
  status: 200,
2012
2029
  headers: { "content-type": "application/json" }
2013
2030
  });
2014
2031
  } catch (error) {
2032
+ try {
2033
+ await enqueueClaim?.abandonReservation(envelope.messageId);
2034
+ } catch (cleanupError) {
2035
+ console.error("world-celld could not abandon a failed Queue reservation", cleanupError);
2036
+ }
2015
2037
  const err = error;
2016
2038
  return errorResponse(500, err.name ?? "Error", err.message ?? String(error));
2017
2039
  }
@@ -2244,6 +2266,16 @@ async function runIsActive(env, runId) {
2244
2266
  if (!namespace) throw new Error("world-celld hook index missing WORKFLOW_DB binding");
2245
2267
  return await namespace.get(namespace.idFromName(runId)).getLifecycleStatus() === "active";
2246
2268
  }
2269
+ /** Resolve a stale shard reservation in its authoritative event writer. */
2270
+ async function resolveExpiredHookClaim(env, owner, claimId, token) {
2271
+ const namespace = env.WORKFLOW_DB;
2272
+ if (!namespace) throw new Error("world-celld hook index missing WORKFLOW_DB binding");
2273
+ return await namespace.get(namespace.idFromName(owner.runId)).resolveExpiredHookClaim({
2274
+ hookId: owner.hookId,
2275
+ claimId,
2276
+ token
2277
+ });
2278
+ }
2247
2279
  //#endregion
2248
2280
  //#region src/worker/durable-objects/HookIdDO.ts
2249
2281
  const MAX_RELEASE_BATCH$1 = 64;
@@ -2272,52 +2304,69 @@ var HookIdDO = class extends DurableObject {
2272
2304
  return await runIsActive(this.env, owner.runId) ? raw : null;
2273
2305
  }
2274
2306
  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);
2307
+ for (;;) {
2308
+ const observed = await this.ctx.storage.get(claimKey$1(hookId));
2309
+ const expired = observed && observed.expiresAt <= this.now() ? observed : void 0;
2310
+ const committed = expired ? await resolveExpiredHookClaim(this.env, expired.owner, expired.claimId) : void 0;
2311
+ const result = await this.ctx.storage.transaction(async (txn) => {
2312
+ const record = recordKey$1(hookId);
2313
+ const claim = claimKey$1(hookId);
2314
+ const values = await txn.get([record, claim]);
2315
+ const indexed = values.get(record);
2316
+ if (indexed !== void 0) return {
2317
+ claimed: false,
2318
+ holder: ownerFromSerializedHook(indexed)
2319
+ };
2320
+ let existing = values.get(claim);
2321
+ const now = this.now();
2322
+ if (existing && existing.expiresAt <= now) {
2323
+ if (existing.claimId !== expired?.claimId || existing.expiresAt !== expired.expiresAt) return null;
2324
+ if (committed) {
2325
+ await this.renewClaim(txn, hookId, existing, now);
2326
+ return sameOwner(existing.owner, owner) ? {
2327
+ claimed: true,
2328
+ claimId: existing.claimId
2329
+ } : {
2330
+ claimed: false,
2331
+ holder: existing.owner
2332
+ };
2333
+ }
2334
+ await txn.delete([claim, existing.deadlineKey]);
2335
+ existing = void 0;
2336
+ }
2337
+ if (existing !== void 0 && !sameOwner(existing.owner, owner)) return {
2338
+ claimed: false,
2339
+ holder: existing.owner
2340
+ };
2341
+ if (existing === void 0 || existing.claimId !== proposedClaimId) {
2342
+ if (existing) await txn.delete(existing.deadlineKey);
2343
+ const expiresAt = now + HOOK_CLAIM_LEASE_MS;
2344
+ const deadlineKey = claimDeadlineKey$1(hookId, expiresAt);
2345
+ await txn.put(claim, {
2346
+ owner,
2347
+ claimId: proposedClaimId,
2348
+ reservedAt: now,
2349
+ expiresAt,
2350
+ deadlineKey
2351
+ });
2352
+ await txn.put(deadlineKey, {
2353
+ hookId,
2354
+ claimId: proposedClaimId,
2355
+ expiresAt
2356
+ });
2357
+ await this.armAtMost(txn, expiresAt);
2358
+ return {
2359
+ claimed: true,
2360
+ claimId: proposedClaimId
2361
+ };
2362
+ }
2311
2363
  return {
2312
2364
  claimed: true,
2313
- claimId: proposedClaimId
2365
+ claimId: existing.claimId
2314
2366
  };
2315
- }
2316
- return {
2317
- claimed: true,
2318
- claimId: existing.claimId
2319
- };
2320
- });
2367
+ });
2368
+ if (result) return result;
2369
+ }
2321
2370
  }
2322
2371
  async publish(hookId, serializedHook, owner, claimId) {
2323
2372
  if (!await runIsActive(this.env, owner.runId)) {
@@ -2334,10 +2383,7 @@ var HookIdDO = class extends DurableObject {
2334
2383
  const indexed = values.get(record);
2335
2384
  if (indexed !== void 0 && !sameOwner(ownerFromSerializedHook(indexed), owner)) throw new Error(`Hook id ${hookId} is owned by another hook`);
2336
2385
  const existingClaim = values.get(claim);
2337
- if (existingClaim && existingClaim.expiresAt <= this.now()) {
2338
- await txn.delete([claim, existingClaim.deadlineKey]);
2339
- return { stored: false };
2340
- }
2386
+ if (existingClaim && existingClaim.expiresAt <= this.now()) throw new Error(`Hook id ${hookId} reservation expired`);
2341
2387
  if (existingClaim !== void 0 && (!sameOwner(existingClaim.owner, owner) || existingClaim.claimId !== claimId)) throw new Error(`Hook id ${hookId} is reserved by another hook`);
2342
2388
  if (indexed === void 0 && existingClaim === void 0) throw new Error(`Hook id ${hookId} has no active reservation`);
2343
2389
  await txn.put(record, serializedHook);
@@ -2387,21 +2433,51 @@ var HookIdDO = class extends DurableObject {
2387
2433
  const current = await storage.getAlarm();
2388
2434
  if (current === null || at < current) await storage.setAlarm(at);
2389
2435
  }
2436
+ async renewClaim(txn, hookId, claim, now) {
2437
+ const expiresAt = now + HOOK_CLAIM_LEASE_MS;
2438
+ const deadlineKey = claimDeadlineKey$1(hookId, expiresAt);
2439
+ await txn.delete(claim.deadlineKey);
2440
+ await txn.put(claimKey$1(hookId), {
2441
+ ...claim,
2442
+ expiresAt,
2443
+ deadlineKey
2444
+ });
2445
+ await txn.put(deadlineKey, {
2446
+ hookId,
2447
+ claimId: claim.claimId,
2448
+ expiresAt
2449
+ });
2450
+ await this.armAtMost(txn, expiresAt);
2451
+ }
2390
2452
  async compactClaims() {
2391
2453
  const now = this.now();
2454
+ const due = await this.ctx.storage.list({
2455
+ prefix: CLAIM_DEADLINE_PREFIX$1,
2456
+ end: `${CLAIM_DEADLINE_PREFIX$1}${pad$2(now + 1)}`,
2457
+ limit: 129
2458
+ });
2459
+ const page = Array.from(due).slice(0, 128);
2460
+ const observed = await this.ctx.storage.get(page.map(([, item]) => claimKey$1(item.hookId)));
2461
+ const committed = /* @__PURE__ */ new Map();
2462
+ await Promise.all(page.map(async ([, item]) => {
2463
+ const claim = observed.get(claimKey$1(item.hookId));
2464
+ if (!claim || claim.claimId !== item.claimId || claim.expiresAt > now) return;
2465
+ const hook = await resolveExpiredHookClaim(this.env, claim.owner, claim.claimId);
2466
+ committed.set(claimKey$1(item.hookId), hook !== null);
2467
+ }));
2392
2468
  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
2469
  const claims = await txn.get(page.map(([, item]) => claimKey$1(item.hookId)));
2400
- const deletes = page.flatMap(([deadline, item]) => {
2470
+ const deletes = [];
2471
+ for (const [deadline, item] of page) {
2401
2472
  const key = claimKey$1(item.hookId);
2402
2473
  const claim = claims.get(key);
2403
- return claim?.claimId === item.claimId && claim.expiresAt <= now ? [deadline, key] : [deadline];
2404
- });
2474
+ const snapshot = observed.get(key);
2475
+ if (claim?.claimId !== item.claimId || claim.expiresAt > now) deletes.push(deadline);
2476
+ else if (claim.claimId === snapshot?.claimId && claim.expiresAt === snapshot.expiresAt) {
2477
+ if (committed.get(key)) await this.renewClaim(txn, item.hookId, claim, now);
2478
+ else deletes.push(deadline, key);
2479
+ }
2480
+ }
2405
2481
  for (let offset = 0; offset < deletes.length; offset += 128) await txn.delete(deletes.slice(offset, offset + 128));
2406
2482
  if (due.size > 128) {
2407
2483
  await txn.setAlarm(now + 1);
@@ -2444,51 +2520,68 @@ var HookTokenDO = class extends DurableObject {
2444
2520
  return await runIsActive(this.env, owner.runId) ? raw : null;
2445
2521
  }
2446
2522
  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);
2523
+ for (;;) {
2524
+ const observed = await this.ctx.storage.get(claimKey(token));
2525
+ const expired = observed && observed.expiresAt <= this.now() ? observed : void 0;
2526
+ const committed = expired ? await resolveExpiredHookClaim(this.env, expired.owner, expired.claimId, token) : void 0;
2527
+ const result = await this.ctx.storage.transaction(async (txn) => {
2528
+ const record = recordKey(token);
2529
+ const claim = claimKey(token);
2530
+ const values = await txn.get([record, claim]);
2531
+ const indexed = values.get(record);
2532
+ if (indexed !== void 0) return {
2533
+ claimed: false,
2534
+ holder: ownerFromSerializedHook(indexed)
2535
+ };
2536
+ let existing = values.get(claim);
2537
+ const now = this.now();
2538
+ if (existing && existing.expiresAt <= now) {
2539
+ if (existing.claimId !== expired?.claimId || existing.expiresAt !== expired.expiresAt) return null;
2540
+ if (committed) {
2541
+ await this.renewClaim(txn, token, existing, now);
2542
+ return sameOwner(existing.owner, owner) ? {
2543
+ claimed: true,
2544
+ claimId: existing.claimId
2545
+ } : {
2546
+ claimed: false,
2547
+ holder: existing.owner
2548
+ };
2549
+ }
2550
+ await txn.delete([claim, existing.deadlineKey]);
2551
+ existing = void 0;
2552
+ }
2553
+ if (existing !== void 0 && !sameOwner(existing.owner, owner)) return {
2554
+ claimed: false,
2555
+ holder: existing.owner
2556
+ };
2557
+ if (existing === void 0) {
2558
+ const expiresAt = now + HOOK_CLAIM_LEASE_MS;
2559
+ const deadlineKey = claimDeadlineKey(token, expiresAt);
2560
+ await txn.put(claim, {
2561
+ owner,
2562
+ claimId: proposedClaimId,
2563
+ reservedAt: now,
2564
+ expiresAt,
2565
+ deadlineKey
2566
+ });
2567
+ await txn.put(deadlineKey, {
2568
+ token,
2569
+ claimId: proposedClaimId,
2570
+ expiresAt
2571
+ });
2572
+ await this.armAtMost(txn, expiresAt);
2573
+ return {
2574
+ claimed: true,
2575
+ claimId: proposedClaimId
2576
+ };
2577
+ }
2482
2578
  return {
2483
2579
  claimed: true,
2484
- claimId: proposedClaimId
2580
+ claimId: existing.claimId
2485
2581
  };
2486
- }
2487
- return {
2488
- claimed: true,
2489
- claimId: existing.claimId
2490
- };
2491
- });
2582
+ });
2583
+ if (result) return result;
2584
+ }
2492
2585
  }
2493
2586
  async finalize(token, serializedHook, owner, claimId) {
2494
2587
  if (!await runIsActive(this.env, owner.runId)) {
@@ -2505,10 +2598,7 @@ var HookTokenDO = class extends DurableObject {
2505
2598
  const indexed = values.get(record);
2506
2599
  if (indexed !== void 0 && !sameOwner(ownerFromSerializedHook(indexed), owner)) throw new Error(`Hook token ${token} is owned by another hook`);
2507
2600
  const existingClaim = values.get(claim);
2508
- if (existingClaim && existingClaim.expiresAt <= this.now()) {
2509
- await txn.delete([claim, existingClaim.deadlineKey]);
2510
- return { stored: false };
2511
- }
2601
+ if (existingClaim && existingClaim.expiresAt <= this.now()) throw new Error(`Hook token ${token} reservation expired`);
2512
2602
  if (existingClaim !== void 0 && (!sameOwner(existingClaim.owner, owner) || existingClaim.claimId !== claimId)) throw new Error(`Hook token ${token} is reserved by another hook`);
2513
2603
  if (indexed === void 0 && existingClaim === void 0) throw new Error(`Hook token ${token} has no active reservation`);
2514
2604
  await txn.put(record, serializedHook);
@@ -2558,21 +2648,51 @@ var HookTokenDO = class extends DurableObject {
2558
2648
  const current = await storage.getAlarm();
2559
2649
  if (current === null || at < current) await storage.setAlarm(at);
2560
2650
  }
2651
+ async renewClaim(txn, token, claim, now) {
2652
+ const expiresAt = now + HOOK_CLAIM_LEASE_MS;
2653
+ const deadlineKey = claimDeadlineKey(token, expiresAt);
2654
+ await txn.delete(claim.deadlineKey);
2655
+ await txn.put(claimKey(token), {
2656
+ ...claim,
2657
+ expiresAt,
2658
+ deadlineKey
2659
+ });
2660
+ await txn.put(deadlineKey, {
2661
+ token,
2662
+ claimId: claim.claimId,
2663
+ expiresAt
2664
+ });
2665
+ await this.armAtMost(txn, expiresAt);
2666
+ }
2561
2667
  async compactClaims() {
2562
2668
  const now = this.now();
2669
+ const due = await this.ctx.storage.list({
2670
+ prefix: CLAIM_DEADLINE_PREFIX,
2671
+ end: `${CLAIM_DEADLINE_PREFIX}${pad$1(now + 1)}`,
2672
+ limit: 129
2673
+ });
2674
+ const page = Array.from(due).slice(0, 128);
2675
+ const observed = await this.ctx.storage.get(page.map(([, item]) => claimKey(item.token)));
2676
+ const committed = /* @__PURE__ */ new Map();
2677
+ await Promise.all(page.map(async ([, item]) => {
2678
+ const claim = observed.get(claimKey(item.token));
2679
+ if (!claim || claim.claimId !== item.claimId || claim.expiresAt > now) return;
2680
+ const hook = await resolveExpiredHookClaim(this.env, claim.owner, claim.claimId, item.token);
2681
+ committed.set(claimKey(item.token), hook !== null);
2682
+ }));
2563
2683
  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
2684
  const claims = await txn.get(page.map(([, item]) => claimKey(item.token)));
2571
- const deletes = page.flatMap(([deadline, item]) => {
2685
+ const deletes = [];
2686
+ for (const [deadline, item] of page) {
2572
2687
  const key = claimKey(item.token);
2573
2688
  const claim = claims.get(key);
2574
- return claim?.claimId === item.claimId && claim.expiresAt <= now ? [deadline, key] : [deadline];
2575
- });
2689
+ const snapshot = observed.get(key);
2690
+ if (claim?.claimId !== item.claimId || claim.expiresAt > now) deletes.push(deadline);
2691
+ else if (claim.claimId === snapshot?.claimId && claim.expiresAt === snapshot.expiresAt) {
2692
+ if (committed.get(key)) await this.renewClaim(txn, item.token, claim, now);
2693
+ else deletes.push(deadline, key);
2694
+ }
2695
+ }
2576
2696
  for (let offset = 0; offset < deletes.length; offset += 128) await txn.delete(deletes.slice(offset, offset + 128));
2577
2697
  if (due.size > 128) {
2578
2698
  await txn.setAlarm(now + 1);
@@ -3049,6 +3169,15 @@ var StreamDO = class extends DurableObject {
3049
3169
  size: validateSegmentSize(entry[1], start)
3050
3170
  });
3051
3171
  }
3172
+ let covered = startIndex;
3173
+ for (let index = 0; index < planned.length; index++) {
3174
+ const { start, size } = planned[index];
3175
+ if (start > covered) throw new Error(`Missing persisted stream segment at index ${covered}`);
3176
+ if (index > 0 && start !== covered) throw new Error(`Overlapping persisted stream segment at index ${start}`);
3177
+ if (start + size.count <= covered) throw new Error(`Missing persisted stream segment at index ${covered}`);
3178
+ covered = start + size.count;
3179
+ }
3180
+ if (covered < endIndex) throw new Error(`Missing persisted stream segment at index ${covered}`);
3052
3181
  const selected = [];
3053
3182
  let plannedBytes = 0;
3054
3183
  for (const segment of planned) {
@@ -3659,6 +3788,28 @@ var WorkflowRunDO = class extends DurableObject {
3659
3788
  async getRun() {
3660
3789
  return this.readKey("run");
3661
3790
  }
3791
+ /** Fence both supported event-claim forms before an expired shard claim is released. */
3792
+ async resolveExpiredHookClaim(request) {
3793
+ 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");
3794
+ return await this.ctx.storage.transaction(async (txn) => {
3795
+ const { tombstone } = await this.retentionState(txn);
3796
+ if (tombstone) return null;
3797
+ const run = await txn.get("run");
3798
+ if (run && isTerminalWorkflowRunStatus(run.status)) return null;
3799
+ const hook = await txn.get(`${HOOK_KEY_PREFIX}${request.hookId}`);
3800
+ if (hook && (request.token === void 0 || hook.token === request.token)) return hook;
3801
+ const cancellation = {
3802
+ hookId: request.hookId,
3803
+ token: request.token,
3804
+ canceledAt: this.now()
3805
+ };
3806
+ await txn.put({
3807
+ [hookClaimCancellationKey(`${request.claimId}:${request.claimId}`)]: cancellation,
3808
+ [hookClaimCancellationKey(`-:${request.claimId}`)]: cancellation
3809
+ });
3810
+ return null;
3811
+ });
3812
+ }
3662
3813
  /** Single authoritative lifecycle answer used after derivative compaction. */
3663
3814
  async getLifecycleStatus() {
3664
3815
  return await this.ctx.storage.transaction(async (txn) => {
@@ -4290,7 +4441,7 @@ var WorkflowRunDO = class extends DurableObject {
4290
4441
  * the run expiry check.
4291
4442
  */
4292
4443
  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");
4444
+ 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
4445
  return await this.ctx.storage.transaction(async (txn) => {
4295
4446
  const key = runQueueClaimKey(params.claimName);
4296
4447
  const values = await txn.get([
@@ -4310,17 +4461,19 @@ var WorkflowRunDO = class extends DurableObject {
4310
4461
  };
4311
4462
  const state = values.get(key) ?? {};
4312
4463
  const existing = state.reservation;
4313
- if (existing && existing.expiresAt > this.now() && existing.messageId !== params.messageId) return {
4464
+ if (existing && existing.expiresAt > this.now() && (existing.messageId !== params.messageId || params.publicationPending)) return {
4314
4465
  ok: true,
4315
4466
  scope: "run",
4316
4467
  admitted: false,
4317
- messageId: existing.messageId
4468
+ messageId: existing.messageId,
4469
+ ...existing.publicationPending ? { publicationPending: true } : {}
4318
4470
  };
4319
4471
  await txn.put(key, {
4320
4472
  ...state,
4321
4473
  reservation: {
4322
4474
  messageId: params.messageId,
4323
- expiresAt: Math.max(existing?.expiresAt ?? 0, params.expiresAt)
4475
+ expiresAt: Math.max(existing?.expiresAt ?? 0, params.expiresAt),
4476
+ ...params.publicationPending ? { publicationPending: true } : {}
4324
4477
  }
4325
4478
  });
4326
4479
  return {
@@ -4331,6 +4484,21 @@ var WorkflowRunDO = class extends DurableObject {
4331
4484
  };
4332
4485
  });
4333
4486
  }
4487
+ async confirmRunQueueMessagePublication(params) {
4488
+ 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");
4489
+ await this.ctx.storage.transaction(async (txn) => {
4490
+ const key = runQueueClaimKey(params.claimName);
4491
+ const state = await txn.get(key);
4492
+ if (state?.reservation?.messageId !== params.messageId) return;
4493
+ await txn.put(key, {
4494
+ ...state,
4495
+ reservation: {
4496
+ messageId: params.messageId,
4497
+ expiresAt: Math.max(state.reservation.expiresAt, params.expiresAt)
4498
+ }
4499
+ });
4500
+ });
4501
+ }
4334
4502
  /** Delivery-side run expiry check and in-flight claim in one transaction. */
4335
4503
  async claimRunQueueMessage(params) {
4336
4504
  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 +4510,19 @@ var WorkflowRunDO = class extends DurableObject {
4342
4510
  key
4343
4511
  ]);
4344
4512
  if (this.tombstoneFrom(values)) return { expired: true };
4345
- const state = values.get(key) ?? {};
4346
- const existing = state.claim;
4513
+ let state = values.get(key) ?? {};
4347
4514
  const now = this.now();
4515
+ if (state.reservation?.publicationPending && state.reservation.messageId === params.messageId) {
4516
+ state = {
4517
+ ...state,
4518
+ reservation: {
4519
+ messageId: params.messageId,
4520
+ expiresAt: Math.max(state.reservation.expiresAt, now + QUEUE_RESERVATION_GRACE_MS)
4521
+ }
4522
+ };
4523
+ await txn.put(key, state);
4524
+ }
4525
+ const existing = state.claim;
4348
4526
  if (existing && existing.expiresAt > now) {
4349
4527
  if (existing.messageId !== params.messageId) return {
4350
4528
  expired: false,
@@ -4416,6 +4594,18 @@ var WorkflowRunDO = class extends DurableObject {
4416
4594
  else await txn.delete(key);
4417
4595
  });
4418
4596
  }
4597
+ /** Failed enqueue cleanup must preserve delivery already accepted by the broker. */
4598
+ async abandonRunQueueMessageReservation(params) {
4599
+ if (!validRunQueueClaimName(params?.claimName) || typeof params.messageId !== "string" || params.messageId.length === 0) throw new TypeError("world-celld run queue reservation abandonment is invalid");
4600
+ await this.ctx.storage.transaction(async (txn) => {
4601
+ const key = runQueueClaimKey(params.claimName);
4602
+ const state = await txn.get(key);
4603
+ if (state?.reservation?.messageId !== params.messageId) return;
4604
+ const { reservation: _abandoned, ...rest } = state;
4605
+ if (rest.claim) await txn.put(key, rest);
4606
+ else await txn.delete(key);
4607
+ });
4608
+ }
4419
4609
  async registerQueuePayload(registration) {
4420
4610
  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
4611
  return await this.ctx.storage.transaction(async (txn) => {
@@ -4472,22 +4662,24 @@ var WorkflowRunDO = class extends DurableObject {
4472
4662
  });
4473
4663
  }
4474
4664
  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");
4665
+ 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
4666
  return await this.ctx.storage.transaction(async (txn) => {
4477
4667
  const values = await txn.get([QUEUE_RESERVATION_KEY, "claim"]);
4478
4668
  const existing = values.get(QUEUE_RESERVATION_KEY);
4479
4669
  const claim = values.get("claim");
4480
4670
  const now = this.now();
4481
- if (existing && existing.expiresAt > now && existing.messageId !== params.messageId) {
4671
+ if (existing && existing.expiresAt > now && (existing.messageId !== params.messageId || params.publicationPending)) {
4482
4672
  await txn.setAlarm(Math.min(existing.expiresAt, claim?.expiresAt ?? existing.expiresAt));
4483
4673
  return {
4484
4674
  admitted: false,
4485
- messageId: existing.messageId
4675
+ messageId: existing.messageId,
4676
+ ...existing.publicationPending ? { publicationPending: true } : {}
4486
4677
  };
4487
4678
  }
4488
4679
  const reservation = {
4489
4680
  messageId: params.messageId,
4490
- expiresAt: Math.max(existing?.expiresAt ?? 0, params.expiresAt)
4681
+ expiresAt: Math.max(existing?.expiresAt ?? 0, params.expiresAt),
4682
+ ...params.publicationPending ? { publicationPending: true } : {}
4491
4683
  };
4492
4684
  await txn.put(QUEUE_RESERVATION_KEY, reservation);
4493
4685
  await txn.setAlarm(Math.min(reservation.expiresAt, claim?.expiresAt ?? reservation.expiresAt));
@@ -4497,6 +4689,20 @@ var WorkflowRunDO = class extends DurableObject {
4497
4689
  };
4498
4690
  });
4499
4691
  }
4692
+ async confirmQueueMessagePublication(params) {
4693
+ if (typeof params?.messageId !== "string" || params.messageId.length === 0 || !isNonNegativeSafeInteger(params.expiresAt)) throw new TypeError("world-celld queue publication confirmation is invalid");
4694
+ await this.ctx.storage.transaction(async (txn) => {
4695
+ const values = await txn.get([QUEUE_RESERVATION_KEY, "claim"]);
4696
+ const existing = values.get(QUEUE_RESERVATION_KEY);
4697
+ if (existing?.messageId !== params.messageId) return;
4698
+ const reservation = {
4699
+ messageId: params.messageId,
4700
+ expiresAt: Math.max(existing.expiresAt, params.expiresAt)
4701
+ };
4702
+ await txn.put(QUEUE_RESERVATION_KEY, reservation);
4703
+ await txn.setAlarm(Math.min(reservation.expiresAt, values.get("claim")?.expiresAt ?? reservation.expiresAt));
4704
+ });
4705
+ }
4500
4706
  async completeQueueMessage(messageId) {
4501
4707
  if (typeof messageId !== "string" || messageId.length === 0) throw new TypeError("world-celld queue messageId must be a non-empty string");
4502
4708
  await this.ctx.storage.transaction(async (txn) => {
@@ -4516,13 +4722,32 @@ var WorkflowRunDO = class extends DurableObject {
4516
4722
  else await txn.deleteAlarm();
4517
4723
  });
4518
4724
  }
4725
+ /** Abandon the enqueue reservation without releasing an active delivery claim. */
4726
+ async abandonQueueMessageReservation(messageId) {
4727
+ if (typeof messageId !== "string" || messageId.length === 0) throw new TypeError("world-celld queue reservation messageId must be a non-empty string");
4728
+ await this.ctx.storage.transaction(async (txn) => {
4729
+ const values = await txn.get([QUEUE_RESERVATION_KEY, "claim"]);
4730
+ if (values.get(QUEUE_RESERVATION_KEY)?.messageId !== messageId) return;
4731
+ await txn.delete(QUEUE_RESERVATION_KEY);
4732
+ const claim = values.get("claim");
4733
+ if (claim) await txn.setAlarm(claim.expiresAt);
4734
+ else await txn.deleteAlarm();
4735
+ });
4736
+ }
4519
4737
  async claimInflight(params) {
4520
4738
  if (typeof params?.messageId !== "string" || params.messageId.length === 0 || !isPositiveSafeInteger(params.staleMs)) throw new TypeError("world-celld queue claim is invalid");
4521
4739
  return await this.ctx.storage.transaction(async (txn) => {
4522
4740
  const values = await txn.get(["claim", QUEUE_RESERVATION_KEY]);
4523
4741
  const existing = values.get("claim");
4524
- const reservation = values.get(QUEUE_RESERVATION_KEY);
4742
+ let reservation = values.get(QUEUE_RESERVATION_KEY);
4525
4743
  const now = this.now();
4744
+ if (reservation?.publicationPending && reservation.messageId === params.messageId) {
4745
+ reservation = {
4746
+ messageId: params.messageId,
4747
+ expiresAt: Math.max(reservation.expiresAt, now + QUEUE_RESERVATION_GRACE_MS)
4748
+ };
4749
+ await txn.put(QUEUE_RESERVATION_KEY, reservation);
4750
+ }
4526
4751
  if (existing && existing.expiresAt > now) {
4527
4752
  if (existing.messageId !== params.messageId) return { claimed: false };
4528
4753
  if (existing.retryAt === void 0 || existing.retryAt > now) return {