@ewhauser/world-celld 0.7.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/CHANGELOG.md +26 -0
- package/README.md +5 -0
- package/dist/index.d.ts +15 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +54 -37
- package/dist/index.js.map +1 -1
- package/dist/queue-consumer.d.ts +1 -1
- package/dist/queue-consumer.js +1 -1
- package/dist/{queue-delivery-DzNOtCYA.d.ts → queue-delivery-wvPp6_Hh.d.ts} +30 -3
- package/dist/queue-delivery-wvPp6_Hh.d.ts.map +1 -0
- package/dist/{queue-protocol-CtH7UUCO.js → queue-protocol-C1zbXwa_.js} +28 -2
- package/dist/queue-protocol-C1zbXwa_.js.map +1 -0
- package/dist/{run-list-B__jKuE7.js → run-list-DGsdv6Jf.js} +28 -2
- package/dist/{run-list-B__jKuE7.js.map → run-list-DGsdv6Jf.js.map} +1 -1
- package/dist/testing.d.ts +2 -0
- package/dist/testing.d.ts.map +1 -1
- package/dist/testing.js +945 -255
- package/dist/testing.js.map +1 -1
- package/dist/worker.d.ts +98 -1
- package/dist/worker.d.ts.map +1 -1
- package/dist/worker.js +972 -269
- package/dist/worker.js.map +1 -1
- package/package.json +1 -1
- package/dist/queue-delivery-DzNOtCYA.d.ts.map +0 -1
- package/dist/queue-protocol-CtH7UUCO.js.map +0 -1
package/dist/testing.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
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
|
|
@@ -373,6 +373,10 @@ var FakeFleet = class {
|
|
|
373
373
|
}
|
|
374
374
|
}, this.cellEnv);
|
|
375
375
|
}
|
|
376
|
+
/** Whether a cell has been activated, without activating it. */
|
|
377
|
+
hasCell(bindingKey, name) {
|
|
378
|
+
return this.cells.has(`${bindingKey}\0${name}`);
|
|
379
|
+
}
|
|
376
380
|
cell(bindingKey, name) {
|
|
377
381
|
const key = `${bindingKey}\0${name}`;
|
|
378
382
|
let slot = this.cells.get(key);
|
|
@@ -542,8 +546,8 @@ function readStringProp(value, key) {
|
|
|
542
546
|
}
|
|
543
547
|
}
|
|
544
548
|
/**
|
|
545
|
-
* Map a failure eventData.error (which may be a string, an Error-shaped
|
|
546
|
-
* object, or anything else) to the structured error stored on
|
|
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.
|
|
547
551
|
* Matches upstream: string errors keep their text, errorCode is preserved.
|
|
548
552
|
*/
|
|
549
553
|
function toStructuredError(error, opts) {
|
|
@@ -1019,14 +1023,17 @@ async function applyEvent(store, ctx) {
|
|
|
1019
1023
|
}
|
|
1020
1024
|
if (!step) return failure("STEP_NOT_FOUND", `Step "${data.correlationId}" not found`);
|
|
1021
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) });
|
|
1022
|
-
const updated = StepSchema.parse(
|
|
1023
|
-
...
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
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
|
+
});
|
|
1030
1037
|
const event = buildEvent({ ...data });
|
|
1031
1038
|
await store.put(`${STEP_KEY_PREFIX}${data.correlationId}`, updated);
|
|
1032
1039
|
await putEvent(event);
|
|
@@ -1041,13 +1048,16 @@ async function applyEvent(store, ctx) {
|
|
|
1041
1048
|
case "step_completed": {
|
|
1042
1049
|
const step = validatedStep;
|
|
1043
1050
|
if (!step) return failure("STEP_NOT_FOUND", `Step "${data.correlationId}" not found`);
|
|
1044
|
-
const updated = StepSchema.parse(
|
|
1045
|
-
...
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
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
|
+
});
|
|
1051
1061
|
const event = buildEvent({ ...data });
|
|
1052
1062
|
await store.put(`${STEP_KEY_PREFIX}${data.correlationId}`, updated);
|
|
1053
1063
|
await putEvent(event);
|
|
@@ -1061,13 +1071,13 @@ async function applyEvent(store, ctx) {
|
|
|
1061
1071
|
case "step_failed": {
|
|
1062
1072
|
const step = validatedStep;
|
|
1063
1073
|
if (!step) return failure("STEP_NOT_FOUND", `Step "${data.correlationId}" not found`);
|
|
1064
|
-
const updated = StepSchema.parse(
|
|
1074
|
+
const updated = StepSchema.parse({
|
|
1065
1075
|
...step,
|
|
1066
1076
|
status: "failed",
|
|
1067
|
-
error:
|
|
1077
|
+
error: data.eventData.error,
|
|
1068
1078
|
completedAt: now,
|
|
1069
1079
|
updatedAt: now
|
|
1070
|
-
})
|
|
1080
|
+
});
|
|
1071
1081
|
const event = buildEvent({ ...data });
|
|
1072
1082
|
await store.put(`${STEP_KEY_PREFIX}${data.correlationId}`, updated);
|
|
1073
1083
|
await putEvent(event);
|
|
@@ -1081,13 +1091,13 @@ async function applyEvent(store, ctx) {
|
|
|
1081
1091
|
case "step_retrying": {
|
|
1082
1092
|
const step = validatedStep;
|
|
1083
1093
|
if (!step) return failure("STEP_NOT_FOUND", `Step "${data.correlationId}" not found`);
|
|
1084
|
-
const updated = StepSchema.parse(
|
|
1094
|
+
const updated = StepSchema.parse({
|
|
1085
1095
|
...step,
|
|
1086
1096
|
status: "pending",
|
|
1087
|
-
error:
|
|
1097
|
+
error: data.eventData.error,
|
|
1088
1098
|
retryAfter: data.eventData.retryAfter ? new Date(data.eventData.retryAfter) : void 0,
|
|
1089
1099
|
updatedAt: now
|
|
1090
|
-
})
|
|
1100
|
+
});
|
|
1091
1101
|
const event = buildEvent({ ...data });
|
|
1092
1102
|
await store.put(`${STEP_KEY_PREFIX}${data.correlationId}`, updated);
|
|
1093
1103
|
await putEvent(event);
|
|
@@ -1123,7 +1133,7 @@ async function applyEvent(store, ctx) {
|
|
|
1123
1133
|
}
|
|
1124
1134
|
if (await store.get(markerKey) !== void 0) {
|
|
1125
1135
|
const existingHook = await store.get(hookKey);
|
|
1126
|
-
if (existingHook && !holder) return {
|
|
1136
|
+
if (existingHook && !holder && existingHook.token === token) return {
|
|
1127
1137
|
ok: true,
|
|
1128
1138
|
hook: existingHook,
|
|
1129
1139
|
hookToIndex: existingHook,
|
|
@@ -1350,6 +1360,60 @@ async function authenticate(request, secret) {
|
|
|
1350
1360
|
return { ok: true };
|
|
1351
1361
|
}
|
|
1352
1362
|
//#endregion
|
|
1363
|
+
//#region src/worker/queue-claims.ts
|
|
1364
|
+
function cellQueueClaim(cell) {
|
|
1365
|
+
return {
|
|
1366
|
+
claim: (messageId, staleMs) => cell.claimInflight({
|
|
1367
|
+
messageId,
|
|
1368
|
+
staleMs
|
|
1369
|
+
}),
|
|
1370
|
+
hold: (params) => cell.holdInflight(params),
|
|
1371
|
+
release: (messageId) => cell.releaseInflight(messageId),
|
|
1372
|
+
abandonReservation: (messageId) => cell.abandonQueueMessageReservation(messageId),
|
|
1373
|
+
confirmPublication: (messageId, expiresAt) => cell.confirmQueueMessagePublication({
|
|
1374
|
+
messageId,
|
|
1375
|
+
expiresAt
|
|
1376
|
+
}),
|
|
1377
|
+
complete: (messageId) => cell.completeQueueMessage(messageId)
|
|
1378
|
+
};
|
|
1379
|
+
}
|
|
1380
|
+
function runQueueClaim(run, claimName) {
|
|
1381
|
+
return {
|
|
1382
|
+
claim: async (messageId, staleMs) => {
|
|
1383
|
+
const result = await run.claimRunQueueMessage({
|
|
1384
|
+
claimName,
|
|
1385
|
+
messageId,
|
|
1386
|
+
staleMs
|
|
1387
|
+
});
|
|
1388
|
+
return result.expired ? {
|
|
1389
|
+
expired: true,
|
|
1390
|
+
claimed: false
|
|
1391
|
+
} : result;
|
|
1392
|
+
},
|
|
1393
|
+
hold: (params) => run.holdRunQueueMessage({
|
|
1394
|
+
claimName,
|
|
1395
|
+
...params
|
|
1396
|
+
}),
|
|
1397
|
+
release: (messageId) => run.releaseRunQueueMessage({
|
|
1398
|
+
claimName,
|
|
1399
|
+
messageId
|
|
1400
|
+
}),
|
|
1401
|
+
abandonReservation: (messageId) => run.abandonRunQueueMessageReservation({
|
|
1402
|
+
claimName,
|
|
1403
|
+
messageId
|
|
1404
|
+
}),
|
|
1405
|
+
confirmPublication: (messageId, expiresAt) => run.confirmRunQueueMessagePublication({
|
|
1406
|
+
claimName,
|
|
1407
|
+
messageId,
|
|
1408
|
+
expiresAt
|
|
1409
|
+
}),
|
|
1410
|
+
complete: (messageId) => run.completeRunQueueMessage({
|
|
1411
|
+
claimName,
|
|
1412
|
+
messageId
|
|
1413
|
+
})
|
|
1414
|
+
};
|
|
1415
|
+
}
|
|
1416
|
+
//#endregion
|
|
1353
1417
|
//#region src/worker/index-validation.ts
|
|
1354
1418
|
function invalid(message) {
|
|
1355
1419
|
const error = /* @__PURE__ */ new Error(`invalid index RPC arguments: ${message}`);
|
|
@@ -1598,6 +1662,18 @@ const INDEX_OPERATIONS = /* @__PURE__ */ new Set([
|
|
|
1598
1662
|
*/
|
|
1599
1663
|
const WORLD_NAME = "world-celld";
|
|
1600
1664
|
const WORLD_VERSION = "0.1.0";
|
|
1665
|
+
/**
|
|
1666
|
+
* Per-isolate record of runs whose Queue idempotency claims stay in claim
|
|
1667
|
+
* cells (created before run-scoped claims) or live in the run cell. A run's
|
|
1668
|
+
* scope never changes, so the cache only saves the run-cell question.
|
|
1669
|
+
*/
|
|
1670
|
+
const runClaimScopes = /* @__PURE__ */ new Map();
|
|
1671
|
+
const MAX_REMEMBERED_RUN_CLAIM_SCOPES = 4096;
|
|
1672
|
+
function rememberRunClaimScope(runId, scope) {
|
|
1673
|
+
if (runClaimScopes.has(runId)) return;
|
|
1674
|
+
if (runClaimScopes.size >= MAX_REMEMBERED_RUN_CLAIM_SCOPES) runClaimScopes.delete(runClaimScopes.keys().next().value);
|
|
1675
|
+
runClaimScopes.set(runId, scope);
|
|
1676
|
+
}
|
|
1601
1677
|
function workflowIndex(env) {
|
|
1602
1678
|
return createWorkflowIndex({
|
|
1603
1679
|
runCatalog: env.WORKFLOW_RUN_CATALOG,
|
|
@@ -1652,7 +1728,8 @@ const BINDINGS = {
|
|
|
1652
1728
|
"scheduleCleanup",
|
|
1653
1729
|
"cleanupNow",
|
|
1654
1730
|
"rearmCleanup",
|
|
1655
|
-
"resolveHookTokenClaim"
|
|
1731
|
+
"resolveHookTokenClaim",
|
|
1732
|
+
"resolveExpiredHookClaim"
|
|
1656
1733
|
])
|
|
1657
1734
|
},
|
|
1658
1735
|
streams: {
|
|
@@ -1679,7 +1756,6 @@ const BINDINGS = {
|
|
|
1679
1756
|
/** Request body cap: oversize payloads get a clear 413 instead of an OOM. */
|
|
1680
1757
|
const MAX_BODY_BYTES = 33554432;
|
|
1681
1758
|
const MAX_STREAM_WRITE_BODY_BYTES = MAX_STREAM_BATCH_BYTES + 2048 + 1024;
|
|
1682
|
-
const QUEUE_ORPHAN_GRACE_MS = 432e6;
|
|
1683
1759
|
function errorResponse(status, name, message) {
|
|
1684
1760
|
return Response.json({ error: {
|
|
1685
1761
|
name,
|
|
@@ -1832,34 +1908,82 @@ function createRouter(env) {
|
|
|
1832
1908
|
} catch (error) {
|
|
1833
1909
|
return errorResponse(400, "BadRequest", error.message);
|
|
1834
1910
|
}
|
|
1911
|
+
let enqueueClaim;
|
|
1835
1912
|
try {
|
|
1836
1913
|
const namespace = env.WORKFLOW_DB;
|
|
1837
1914
|
const payloadStore = env.WORKFLOW_QUEUE_PAYLOADS;
|
|
1838
|
-
const
|
|
1839
|
-
const
|
|
1915
|
+
const inlineRunBody = envelope.runId !== void 0 && isInlineRunBody(envelope.body);
|
|
1916
|
+
const payloadKey = envelope.runId && !inlineRunBody ? queuePayloadObjectKey(envelope.runId, envelope.messageId) : void 0;
|
|
1917
|
+
let brokerEnvelope = payloadKey ? {
|
|
1840
1918
|
...envelope,
|
|
1841
1919
|
payloadKey,
|
|
1842
1920
|
body: void 0
|
|
1843
1921
|
} : envelope;
|
|
1844
|
-
const
|
|
1845
|
-
|
|
1922
|
+
const sized = JSON.stringify({
|
|
1923
|
+
...brokerEnvelope,
|
|
1924
|
+
claimScope: "run"
|
|
1925
|
+
});
|
|
1926
|
+
if (new TextEncoder().encode(sized).byteLength > 128e3) return errorResponse(413, "PayloadTooLarge", `native queue envelope exceeds ${NATIVE_QUEUE_MAX_MESSAGE_BYTES} bytes`);
|
|
1846
1927
|
const now = Date.now();
|
|
1847
|
-
const reservationExpiresAt = Math.max(now, envelope.notBefore ?? now) +
|
|
1848
|
-
|
|
1928
|
+
const reservationExpiresAt = Math.max(now, envelope.notBefore ?? now) + QUEUE_RESERVATION_GRACE_MS;
|
|
1929
|
+
const run = envelope.runId ? namespace.get(namespace.idFromName(envelope.runId)) : void 0;
|
|
1930
|
+
let admission;
|
|
1849
1931
|
if (envelope.idempotencyKey) {
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1932
|
+
const claimName = queueClaimName(envelope.queueName, envelope.idempotencyKey);
|
|
1933
|
+
let scope = run && envelope.runId ? runClaimScopes.get(envelope.runId) : "cell";
|
|
1934
|
+
let reservation;
|
|
1935
|
+
if (run && envelope.runId && scope !== "cell") {
|
|
1936
|
+
enqueueClaim = runQueueClaim(run, claimName);
|
|
1937
|
+
const reserved = await run.reserveRunQueueMessage({
|
|
1938
|
+
claimName,
|
|
1939
|
+
messageId: envelope.messageId,
|
|
1940
|
+
expiresAt: now + QUEUE_PUBLICATION_LEASE_MS,
|
|
1941
|
+
publicationPending: true
|
|
1942
|
+
});
|
|
1943
|
+
if (!reserved.ok) return errorResponse(410, "RunExpiredError", reserved.message);
|
|
1944
|
+
scope = reserved.scope;
|
|
1945
|
+
rememberRunClaimScope(envelope.runId, reserved.scope);
|
|
1946
|
+
if (reserved.scope === "run") {
|
|
1947
|
+
reservation = reserved;
|
|
1948
|
+
brokerEnvelope = {
|
|
1949
|
+
...brokerEnvelope,
|
|
1950
|
+
claimScope: "run"
|
|
1951
|
+
};
|
|
1952
|
+
}
|
|
1953
|
+
} else if (inlineRunBody && run) {
|
|
1954
|
+
admission = run.getQueueAdmission();
|
|
1955
|
+
admission.catch(() => void 0);
|
|
1956
|
+
}
|
|
1957
|
+
if (!reservation) {
|
|
1958
|
+
const cell = namespace.get(namespace.idFromName(claimName));
|
|
1959
|
+
enqueueClaim = cellQueueClaim(cell);
|
|
1960
|
+
reservation = await cell.reserveQueueMessage({
|
|
1961
|
+
messageId: envelope.messageId,
|
|
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" }
|
|
1971
|
+
});
|
|
1972
|
+
}
|
|
1973
|
+
} else if (inlineRunBody && run) admission = run.getQueueAdmission();
|
|
1974
|
+
const encoded = JSON.stringify(brokerEnvelope);
|
|
1975
|
+
if (admission) {
|
|
1976
|
+
const admitted = await admission;
|
|
1977
|
+
if (!admitted.ok) {
|
|
1978
|
+
await enqueueClaim?.complete(envelope.messageId);
|
|
1979
|
+
return errorResponse(410, "RunExpiredError", admitted.message);
|
|
1980
|
+
}
|
|
1859
1981
|
}
|
|
1860
|
-
if (envelope.runId && payloadKey) {
|
|
1861
|
-
if (!payloadStore)
|
|
1862
|
-
|
|
1982
|
+
if (run && envelope.runId && payloadKey) {
|
|
1983
|
+
if (!payloadStore) {
|
|
1984
|
+
await enqueueClaim?.abandonReservation(envelope.messageId);
|
|
1985
|
+
return errorResponse(500, "WorldMisconfigured", "missing binding: WORKFLOW_QUEUE_PAYLOADS");
|
|
1986
|
+
}
|
|
1863
1987
|
const orphanExpiresAt = reservationExpiresAt;
|
|
1864
1988
|
const registered = await run.registerQueuePayload({
|
|
1865
1989
|
messageId: envelope.messageId,
|
|
@@ -1867,7 +1991,7 @@ function createRouter(env) {
|
|
|
1867
1991
|
orphanExpiresAt
|
|
1868
1992
|
});
|
|
1869
1993
|
if (!registered.ok) {
|
|
1870
|
-
await
|
|
1994
|
+
await enqueueClaim?.complete(envelope.messageId);
|
|
1871
1995
|
return errorResponse(410, "RunExpiredError", registered.message);
|
|
1872
1996
|
}
|
|
1873
1997
|
const orphan = namespace.get(namespace.idFromName(queueOrphanName(envelope.messageId)));
|
|
@@ -1880,30 +2004,18 @@ function createRouter(env) {
|
|
|
1880
2004
|
});
|
|
1881
2005
|
} catch (error) {
|
|
1882
2006
|
await run.unregisterQueuePayload(envelope.messageId);
|
|
1883
|
-
await claim?.completeQueueMessage(envelope.messageId);
|
|
1884
|
-
throw error;
|
|
1885
|
-
}
|
|
1886
|
-
try {
|
|
1887
|
-
await payloadStore.write(payloadKey, envelope.body, {
|
|
1888
|
-
runId: envelope.runId,
|
|
1889
|
-
messageId: envelope.messageId
|
|
1890
|
-
});
|
|
1891
|
-
} catch (error) {
|
|
1892
|
-
await claim?.completeQueueMessage(envelope.messageId);
|
|
1893
|
-
throw error;
|
|
1894
|
-
}
|
|
1895
|
-
let finalized;
|
|
1896
|
-
try {
|
|
1897
|
-
finalized = await run.finalizeQueuePayload(envelope.messageId);
|
|
1898
|
-
} catch (error) {
|
|
1899
|
-
await claim?.completeQueueMessage(envelope.messageId);
|
|
1900
2007
|
throw error;
|
|
1901
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);
|
|
1902
2014
|
if (!finalized.ok) {
|
|
1903
2015
|
await payloadStore.delete(payloadKey);
|
|
1904
2016
|
await run.unregisterQueuePayload(envelope.messageId);
|
|
1905
2017
|
await orphan.cancelQueuePayloadOrphan(envelope.messageId);
|
|
1906
|
-
await
|
|
2018
|
+
await enqueueClaim?.complete(envelope.messageId);
|
|
1907
2019
|
return errorResponse(410, "RunExpiredError", finalized.message);
|
|
1908
2020
|
}
|
|
1909
2021
|
}
|
|
@@ -1911,11 +2023,17 @@ function createRouter(env) {
|
|
|
1911
2023
|
contentType: "text",
|
|
1912
2024
|
...options
|
|
1913
2025
|
});
|
|
2026
|
+
await enqueueClaim?.confirmPublication(envelope.messageId, reservationExpiresAt);
|
|
1914
2027
|
return new Response(rpcStringify({ messageId: envelope.messageId }), {
|
|
1915
2028
|
status: 200,
|
|
1916
2029
|
headers: { "content-type": "application/json" }
|
|
1917
2030
|
});
|
|
1918
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
|
+
}
|
|
1919
2037
|
const err = error;
|
|
1920
2038
|
return errorResponse(500, err.name ?? "Error", err.message ?? String(error));
|
|
1921
2039
|
}
|
|
@@ -2148,6 +2266,16 @@ async function runIsActive(env, runId) {
|
|
|
2148
2266
|
if (!namespace) throw new Error("world-celld hook index missing WORKFLOW_DB binding");
|
|
2149
2267
|
return await namespace.get(namespace.idFromName(runId)).getLifecycleStatus() === "active";
|
|
2150
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
|
+
}
|
|
2151
2279
|
//#endregion
|
|
2152
2280
|
//#region src/worker/durable-objects/HookIdDO.ts
|
|
2153
2281
|
const MAX_RELEASE_BATCH$1 = 64;
|
|
@@ -2176,52 +2304,69 @@ var HookIdDO = class extends DurableObject {
|
|
|
2176
2304
|
return await runIsActive(this.env, owner.runId) ? raw : null;
|
|
2177
2305
|
}
|
|
2178
2306
|
async reserve(hookId, owner, proposedClaimId) {
|
|
2179
|
-
|
|
2180
|
-
const
|
|
2181
|
-
const
|
|
2182
|
-
const
|
|
2183
|
-
const
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
existing =
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
}
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
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
|
+
}
|
|
2215
2363
|
return {
|
|
2216
2364
|
claimed: true,
|
|
2217
|
-
claimId:
|
|
2365
|
+
claimId: existing.claimId
|
|
2218
2366
|
};
|
|
2219
|
-
}
|
|
2220
|
-
return
|
|
2221
|
-
|
|
2222
|
-
claimId: existing.claimId
|
|
2223
|
-
};
|
|
2224
|
-
});
|
|
2367
|
+
});
|
|
2368
|
+
if (result) return result;
|
|
2369
|
+
}
|
|
2225
2370
|
}
|
|
2226
2371
|
async publish(hookId, serializedHook, owner, claimId) {
|
|
2227
2372
|
if (!await runIsActive(this.env, owner.runId)) {
|
|
@@ -2238,10 +2383,7 @@ var HookIdDO = class extends DurableObject {
|
|
|
2238
2383
|
const indexed = values.get(record);
|
|
2239
2384
|
if (indexed !== void 0 && !sameOwner(ownerFromSerializedHook(indexed), owner)) throw new Error(`Hook id ${hookId} is owned by another hook`);
|
|
2240
2385
|
const existingClaim = values.get(claim);
|
|
2241
|
-
if (existingClaim && existingClaim.expiresAt <= this.now()) {
|
|
2242
|
-
await txn.delete([claim, existingClaim.deadlineKey]);
|
|
2243
|
-
return { stored: false };
|
|
2244
|
-
}
|
|
2386
|
+
if (existingClaim && existingClaim.expiresAt <= this.now()) throw new Error(`Hook id ${hookId} reservation expired`);
|
|
2245
2387
|
if (existingClaim !== void 0 && (!sameOwner(existingClaim.owner, owner) || existingClaim.claimId !== claimId)) throw new Error(`Hook id ${hookId} is reserved by another hook`);
|
|
2246
2388
|
if (indexed === void 0 && existingClaim === void 0) throw new Error(`Hook id ${hookId} has no active reservation`);
|
|
2247
2389
|
await txn.put(record, serializedHook);
|
|
@@ -2291,21 +2433,51 @@ var HookIdDO = class extends DurableObject {
|
|
|
2291
2433
|
const current = await storage.getAlarm();
|
|
2292
2434
|
if (current === null || at < current) await storage.setAlarm(at);
|
|
2293
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
|
+
}
|
|
2294
2452
|
async compactClaims() {
|
|
2295
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
|
+
}));
|
|
2296
2468
|
await this.ctx.storage.transaction(async (txn) => {
|
|
2297
|
-
const due = await txn.list({
|
|
2298
|
-
prefix: CLAIM_DEADLINE_PREFIX$1,
|
|
2299
|
-
end: `${CLAIM_DEADLINE_PREFIX$1}${pad$2(now + 1)}`,
|
|
2300
|
-
limit: 129
|
|
2301
|
-
});
|
|
2302
|
-
const page = Array.from(due).slice(0, 128);
|
|
2303
2469
|
const claims = await txn.get(page.map(([, item]) => claimKey$1(item.hookId)));
|
|
2304
|
-
const deletes =
|
|
2470
|
+
const deletes = [];
|
|
2471
|
+
for (const [deadline, item] of page) {
|
|
2305
2472
|
const key = claimKey$1(item.hookId);
|
|
2306
2473
|
const claim = claims.get(key);
|
|
2307
|
-
|
|
2308
|
-
|
|
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
|
+
}
|
|
2309
2481
|
for (let offset = 0; offset < deletes.length; offset += 128) await txn.delete(deletes.slice(offset, offset + 128));
|
|
2310
2482
|
if (due.size > 128) {
|
|
2311
2483
|
await txn.setAlarm(now + 1);
|
|
@@ -2348,51 +2520,68 @@ var HookTokenDO = class extends DurableObject {
|
|
|
2348
2520
|
return await runIsActive(this.env, owner.runId) ? raw : null;
|
|
2349
2521
|
}
|
|
2350
2522
|
async reserve(token, owner, proposedClaimId) {
|
|
2351
|
-
|
|
2352
|
-
const
|
|
2353
|
-
const
|
|
2354
|
-
const
|
|
2355
|
-
const
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
existing =
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
deadlineKey
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
}
|
|
2385
|
-
|
|
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
|
+
}
|
|
2386
2578
|
return {
|
|
2387
2579
|
claimed: true,
|
|
2388
|
-
claimId:
|
|
2580
|
+
claimId: existing.claimId
|
|
2389
2581
|
};
|
|
2390
|
-
}
|
|
2391
|
-
return
|
|
2392
|
-
|
|
2393
|
-
claimId: existing.claimId
|
|
2394
|
-
};
|
|
2395
|
-
});
|
|
2582
|
+
});
|
|
2583
|
+
if (result) return result;
|
|
2584
|
+
}
|
|
2396
2585
|
}
|
|
2397
2586
|
async finalize(token, serializedHook, owner, claimId) {
|
|
2398
2587
|
if (!await runIsActive(this.env, owner.runId)) {
|
|
@@ -2409,10 +2598,7 @@ var HookTokenDO = class extends DurableObject {
|
|
|
2409
2598
|
const indexed = values.get(record);
|
|
2410
2599
|
if (indexed !== void 0 && !sameOwner(ownerFromSerializedHook(indexed), owner)) throw new Error(`Hook token ${token} is owned by another hook`);
|
|
2411
2600
|
const existingClaim = values.get(claim);
|
|
2412
|
-
if (existingClaim && existingClaim.expiresAt <= this.now()) {
|
|
2413
|
-
await txn.delete([claim, existingClaim.deadlineKey]);
|
|
2414
|
-
return { stored: false };
|
|
2415
|
-
}
|
|
2601
|
+
if (existingClaim && existingClaim.expiresAt <= this.now()) throw new Error(`Hook token ${token} reservation expired`);
|
|
2416
2602
|
if (existingClaim !== void 0 && (!sameOwner(existingClaim.owner, owner) || existingClaim.claimId !== claimId)) throw new Error(`Hook token ${token} is reserved by another hook`);
|
|
2417
2603
|
if (indexed === void 0 && existingClaim === void 0) throw new Error(`Hook token ${token} has no active reservation`);
|
|
2418
2604
|
await txn.put(record, serializedHook);
|
|
@@ -2462,21 +2648,51 @@ var HookTokenDO = class extends DurableObject {
|
|
|
2462
2648
|
const current = await storage.getAlarm();
|
|
2463
2649
|
if (current === null || at < current) await storage.setAlarm(at);
|
|
2464
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
|
+
}
|
|
2465
2667
|
async compactClaims() {
|
|
2466
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
|
+
}));
|
|
2467
2683
|
await this.ctx.storage.transaction(async (txn) => {
|
|
2468
|
-
const due = await txn.list({
|
|
2469
|
-
prefix: CLAIM_DEADLINE_PREFIX,
|
|
2470
|
-
end: `${CLAIM_DEADLINE_PREFIX}${pad$1(now + 1)}`,
|
|
2471
|
-
limit: 129
|
|
2472
|
-
});
|
|
2473
|
-
const page = Array.from(due).slice(0, 128);
|
|
2474
2684
|
const claims = await txn.get(page.map(([, item]) => claimKey(item.token)));
|
|
2475
|
-
const deletes =
|
|
2685
|
+
const deletes = [];
|
|
2686
|
+
for (const [deadline, item] of page) {
|
|
2476
2687
|
const key = claimKey(item.token);
|
|
2477
2688
|
const claim = claims.get(key);
|
|
2478
|
-
|
|
2479
|
-
|
|
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
|
+
}
|
|
2480
2696
|
for (let offset = 0; offset < deletes.length; offset += 128) await txn.delete(deletes.slice(offset, offset + 128));
|
|
2481
2697
|
if (due.size > 128) {
|
|
2482
2698
|
await txn.setAlarm(now + 1);
|
|
@@ -2655,6 +2871,13 @@ const DELETE_BATCH_KEYS = 128;
|
|
|
2655
2871
|
const DEFAULT_EXPIRE_BYTE_LIMIT = 16777216;
|
|
2656
2872
|
const MAX_EXPIRE_BYTE_LIMIT = DEFAULT_EXPIRE_BYTE_LIMIT;
|
|
2657
2873
|
const MAX_STREAM_INDEX = 2147483647;
|
|
2874
|
+
const SEGMENT_LAYOUT = 2;
|
|
2875
|
+
const SEGMENT_KEY_PREFIX = "seg:";
|
|
2876
|
+
const SEGMENT_SIZE_KEY_PREFIX = "segsize:";
|
|
2877
|
+
/** Payload bytes per segment row, well below the 2 MiB per-value storage limit. */
|
|
2878
|
+
const MAX_SEGMENT_BYTES = 1048576;
|
|
2879
|
+
/** Layout of a stream's first write. Existing streams keep the layout they started with. */
|
|
2880
|
+
const NEW_STREAM_LAYOUT = SEGMENT_LAYOUT;
|
|
2658
2881
|
/** One storage get or put call accepts at most 128 keys. */
|
|
2659
2882
|
const STORAGE_BATCH_KEYS = 128;
|
|
2660
2883
|
function boundedLimit(value, fallback, maximum) {
|
|
@@ -2675,7 +2898,7 @@ function validateMeta(meta) {
|
|
|
2675
2898
|
"closed",
|
|
2676
2899
|
"errored",
|
|
2677
2900
|
"expired"
|
|
2678
|
-
].includes(candidate.state) || candidate.ownerRunId !== void 0 && typeof candidate.ownerRunId !== "string" || candidate.payloadDeleted !== void 0 && typeof candidate.payloadDeleted !== "boolean") throw new Error("Invalid persisted stream metadata");
|
|
2901
|
+
].includes(candidate.state) || candidate.ownerRunId !== void 0 && typeof candidate.ownerRunId !== "string" || candidate.payloadDeleted !== void 0 && typeof candidate.payloadDeleted !== "boolean" || candidate.layout !== void 0 && candidate.layout !== SEGMENT_LAYOUT) throw new Error("Invalid persisted stream metadata");
|
|
2679
2902
|
const error = candidate.error;
|
|
2680
2903
|
if (error !== void 0 && (!error || typeof error !== "object" || typeof error.name !== "string" || typeof error.message !== "string") || candidate.state === "errored" !== (error !== void 0)) throw new Error("Invalid persisted stream error metadata");
|
|
2681
2904
|
return candidate;
|
|
@@ -2687,6 +2910,80 @@ function chunkKey(index) {
|
|
|
2687
2910
|
function chunkSizeKey(index) {
|
|
2688
2911
|
return `${CHUNK_SIZE_KEY_PREFIX}${index.toString().padStart(12, "0")}`;
|
|
2689
2912
|
}
|
|
2913
|
+
function segmentKey(start) {
|
|
2914
|
+
return `${SEGMENT_KEY_PREFIX}${start.toString().padStart(12, "0")}`;
|
|
2915
|
+
}
|
|
2916
|
+
function segmentSizeKey(start) {
|
|
2917
|
+
return `${SEGMENT_SIZE_KEY_PREFIX}${start.toString().padStart(12, "0")}`;
|
|
2918
|
+
}
|
|
2919
|
+
function startFromSegmentKey(key, prefix) {
|
|
2920
|
+
const suffix = key.slice(prefix.length);
|
|
2921
|
+
const start = Number(suffix);
|
|
2922
|
+
if (!key.startsWith(prefix) || !/^\d{12}$/.test(suffix) || !Number.isSafeInteger(start) || start > MAX_STREAM_INDEX) throw new Error(`Invalid persisted stream segment key "${key}"`);
|
|
2923
|
+
return start;
|
|
2924
|
+
}
|
|
2925
|
+
function validateSegmentSize(value, start) {
|
|
2926
|
+
const size = value;
|
|
2927
|
+
if (!size || !Number.isSafeInteger(size.count) || size.count < 1 || !Number.isSafeInteger(size.bytes) || size.bytes < 0) throw new Error(`Invalid persisted stream segment size at index ${start}`);
|
|
2928
|
+
return size;
|
|
2929
|
+
}
|
|
2930
|
+
/** Pack chunks as `[u32 count][u32 length]*count` followed by the payloads. */
|
|
2931
|
+
function encodeSegment(chunks) {
|
|
2932
|
+
const payloadBytes = chunks.reduce((total, chunk) => total + chunk.byteLength, 0);
|
|
2933
|
+
const header = 4 + 4 * chunks.length;
|
|
2934
|
+
const encoded = new Uint8Array(header + payloadBytes);
|
|
2935
|
+
const view = new DataView(encoded.buffer);
|
|
2936
|
+
view.setUint32(0, chunks.length);
|
|
2937
|
+
let offset = header;
|
|
2938
|
+
for (let index = 0; index < chunks.length; index++) {
|
|
2939
|
+
view.setUint32(4 + 4 * index, chunks[index].byteLength);
|
|
2940
|
+
encoded.set(chunks[index], offset);
|
|
2941
|
+
offset += chunks[index].byteLength;
|
|
2942
|
+
}
|
|
2943
|
+
return encoded;
|
|
2944
|
+
}
|
|
2945
|
+
function decodeSegment(value, start, size) {
|
|
2946
|
+
if (!(value instanceof Uint8Array) || value.byteLength < 4) throw new Error(`Invalid persisted stream segment at index ${start}`);
|
|
2947
|
+
const view = new DataView(value.buffer, value.byteOffset, value.byteLength);
|
|
2948
|
+
const count = view.getUint32(0);
|
|
2949
|
+
const header = 4 + 4 * count;
|
|
2950
|
+
if (count !== size.count || header + size.bytes !== value.byteLength) throw new Error(`Invalid persisted stream segment at index ${start}`);
|
|
2951
|
+
const chunks = [];
|
|
2952
|
+
let offset = header;
|
|
2953
|
+
for (let index = 0; index < count; index++) {
|
|
2954
|
+
const length = view.getUint32(4 + 4 * index);
|
|
2955
|
+
if (offset + length > value.byteLength) throw new Error(`Invalid persisted stream segment at index ${start}`);
|
|
2956
|
+
chunks.push(value.subarray(offset, offset + length));
|
|
2957
|
+
offset += length;
|
|
2958
|
+
}
|
|
2959
|
+
if (offset !== value.byteLength) throw new Error(`Invalid persisted stream segment at index ${start}`);
|
|
2960
|
+
return chunks;
|
|
2961
|
+
}
|
|
2962
|
+
/** Group one write's chunks into segments of at most MAX_SEGMENT_BYTES payload bytes. */
|
|
2963
|
+
function packSegments(startIndex, chunks) {
|
|
2964
|
+
const segments = [];
|
|
2965
|
+
let current = [];
|
|
2966
|
+
let bytes = 0;
|
|
2967
|
+
let start = startIndex;
|
|
2968
|
+
for (const chunk of chunks) {
|
|
2969
|
+
if (current.length > 0 && bytes + chunk.byteLength > MAX_SEGMENT_BYTES) {
|
|
2970
|
+
segments.push({
|
|
2971
|
+
start,
|
|
2972
|
+
chunks: current
|
|
2973
|
+
});
|
|
2974
|
+
start += current.length;
|
|
2975
|
+
current = [];
|
|
2976
|
+
bytes = 0;
|
|
2977
|
+
}
|
|
2978
|
+
current.push(chunk);
|
|
2979
|
+
bytes += chunk.byteLength;
|
|
2980
|
+
}
|
|
2981
|
+
if (current.length > 0) segments.push({
|
|
2982
|
+
start,
|
|
2983
|
+
chunks: current
|
|
2984
|
+
});
|
|
2985
|
+
return segments;
|
|
2986
|
+
}
|
|
2690
2987
|
function indexFromChunkSizeKey(key) {
|
|
2691
2988
|
const suffix = key.slice(11);
|
|
2692
2989
|
const index = Number(suffix);
|
|
@@ -2703,6 +3000,9 @@ function validateChunkSize(size, index) {
|
|
|
2703
3000
|
if (typeof size !== "number" || !Number.isSafeInteger(size) || size < 0 || size > 1048576) throw new Error(`Invalid persisted stream chunk size at index ${index}`);
|
|
2704
3001
|
return size;
|
|
2705
3002
|
}
|
|
3003
|
+
async function deleteInBatches(txn, keys) {
|
|
3004
|
+
for (let offset = 0; offset < keys.length; offset += DELETE_BATCH_KEYS) await txn.delete(keys.slice(offset, offset + DELETE_BATCH_KEYS));
|
|
3005
|
+
}
|
|
2706
3006
|
function compactChunk(chunk) {
|
|
2707
3007
|
return chunk.byteOffset === 0 && chunk.byteLength === chunk.buffer.byteLength ? chunk : new Uint8Array(chunk);
|
|
2708
3008
|
}
|
|
@@ -2799,29 +3099,8 @@ var StreamDO = class extends DurableObject {
|
|
|
2799
3099
|
const meta = await this.getMeta();
|
|
2800
3100
|
this.assertOwner(meta, request.runId);
|
|
2801
3101
|
const available = meta.state === "expired" ? 0 : Math.max(0, Math.min(request.maxChunks, meta.count - request.startIndex));
|
|
2802
|
-
|
|
2803
|
-
if (available > 0)
|
|
2804
|
-
const indexes = Array.from({ length: available }, (_, offset) => request.startIndex + offset);
|
|
2805
|
-
const sizeKeys = indexes.map(chunkSizeKey);
|
|
2806
|
-
const storedSizes = await this.getMany(sizeKeys);
|
|
2807
|
-
const selectedIndexes = [];
|
|
2808
|
-
const selectedSizes = [];
|
|
2809
|
-
let bytes = 0;
|
|
2810
|
-
for (const index of indexes) {
|
|
2811
|
-
const size = validateChunkSize(storedSizes.get(chunkSizeKey(index)), index);
|
|
2812
|
-
if (bytes + size > request.maxBytes) break;
|
|
2813
|
-
selectedIndexes.push(index);
|
|
2814
|
-
selectedSizes.push(size);
|
|
2815
|
-
bytes += size;
|
|
2816
|
-
}
|
|
2817
|
-
const payloadKeys = selectedIndexes.map(chunkKey);
|
|
2818
|
-
const storedChunks = await this.getMany(payloadKeys);
|
|
2819
|
-
for (let offset = 0; offset < payloadKeys.length; offset++) {
|
|
2820
|
-
const chunk = storedChunks.get(payloadKeys[offset]);
|
|
2821
|
-
if (!(chunk instanceof Uint8Array) || chunk.byteLength !== selectedSizes[offset]) throw new Error(`Invalid persisted stream chunk at index ${selectedIndexes[offset]}`);
|
|
2822
|
-
chunks.push(chunk);
|
|
2823
|
-
}
|
|
2824
|
-
}
|
|
3102
|
+
let chunks = [];
|
|
3103
|
+
if (available > 0) chunks = meta.layout === SEGMENT_LAYOUT ? await this.readSegmentRows(request.startIndex, available, request.maxBytes) : await this.readChunkRows(request.startIndex, available, request.maxBytes);
|
|
2825
3104
|
return {
|
|
2826
3105
|
startIndex: request.startIndex,
|
|
2827
3106
|
tailIndex: meta.count - 1,
|
|
@@ -2831,10 +3110,100 @@ var StreamDO = class extends DurableObject {
|
|
|
2831
3110
|
error: meta.error
|
|
2832
3111
|
};
|
|
2833
3112
|
}
|
|
3113
|
+
async readChunkRows(startIndex, available, maxBytes) {
|
|
3114
|
+
const indexes = Array.from({ length: available }, (_, offset) => startIndex + offset);
|
|
3115
|
+
const sizeKeys = indexes.map(chunkSizeKey);
|
|
3116
|
+
const storedSizes = await this.getMany(sizeKeys);
|
|
3117
|
+
const selectedIndexes = [];
|
|
3118
|
+
const selectedSizes = [];
|
|
3119
|
+
let bytes = 0;
|
|
3120
|
+
for (const index of indexes) {
|
|
3121
|
+
const size = validateChunkSize(storedSizes.get(chunkSizeKey(index)), index);
|
|
3122
|
+
if (bytes + size > maxBytes) break;
|
|
3123
|
+
selectedIndexes.push(index);
|
|
3124
|
+
selectedSizes.push(size);
|
|
3125
|
+
bytes += size;
|
|
3126
|
+
}
|
|
3127
|
+
const payloadKeys = selectedIndexes.map(chunkKey);
|
|
3128
|
+
const storedChunks = await this.getMany(payloadKeys);
|
|
3129
|
+
const chunks = [];
|
|
3130
|
+
for (let offset = 0; offset < payloadKeys.length; offset++) {
|
|
3131
|
+
const chunk = storedChunks.get(payloadKeys[offset]);
|
|
3132
|
+
if (!(chunk instanceof Uint8Array) || chunk.byteLength !== selectedSizes[offset]) throw new Error(`Invalid persisted stream chunk at index ${selectedIndexes[offset]}`);
|
|
3133
|
+
chunks.push(chunk);
|
|
3134
|
+
}
|
|
3135
|
+
return chunks;
|
|
3136
|
+
}
|
|
3137
|
+
/**
|
|
3138
|
+
* Plan from the small segment size rows, then load only the segments that
|
|
3139
|
+
* cover the requested range. A read that starts on a segment boundary (the
|
|
3140
|
+
* common sequential case) needs one list and one multi-get.
|
|
3141
|
+
*/
|
|
3142
|
+
async readSegmentRows(startIndex, available, maxBytes) {
|
|
3143
|
+
const endIndex = startIndex + available;
|
|
3144
|
+
const listed = await this.ctx.storage.list({
|
|
3145
|
+
prefix: SEGMENT_SIZE_KEY_PREFIX,
|
|
3146
|
+
start: segmentSizeKey(startIndex),
|
|
3147
|
+
end: segmentSizeKey(endIndex),
|
|
3148
|
+
limit: available
|
|
3149
|
+
});
|
|
3150
|
+
const planned = [];
|
|
3151
|
+
for (const [key, value] of listed) {
|
|
3152
|
+
const start = startFromSegmentKey(key, SEGMENT_SIZE_KEY_PREFIX);
|
|
3153
|
+
planned.push({
|
|
3154
|
+
start,
|
|
3155
|
+
size: validateSegmentSize(value, start)
|
|
3156
|
+
});
|
|
3157
|
+
}
|
|
3158
|
+
if (planned[0]?.start !== startIndex) {
|
|
3159
|
+
const entry = (await this.ctx.storage.list({
|
|
3160
|
+
prefix: SEGMENT_SIZE_KEY_PREFIX,
|
|
3161
|
+
end: segmentSizeKey(startIndex + 1),
|
|
3162
|
+
reverse: true,
|
|
3163
|
+
limit: 1
|
|
3164
|
+
})).entries().next().value;
|
|
3165
|
+
if (!entry) throw new Error(`Missing persisted stream segment at index ${startIndex}`);
|
|
3166
|
+
const start = startFromSegmentKey(entry[0], SEGMENT_SIZE_KEY_PREFIX);
|
|
3167
|
+
planned.unshift({
|
|
3168
|
+
start,
|
|
3169
|
+
size: validateSegmentSize(entry[1], start)
|
|
3170
|
+
});
|
|
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}`);
|
|
3181
|
+
const selected = [];
|
|
3182
|
+
let plannedBytes = 0;
|
|
3183
|
+
for (const segment of planned) {
|
|
3184
|
+
if (selected.length > 0 && plannedBytes >= maxBytes) break;
|
|
3185
|
+
selected.push(segment);
|
|
3186
|
+
plannedBytes += segment.size.bytes;
|
|
3187
|
+
}
|
|
3188
|
+
const values = await this.getMany(selected.map(({ start }) => segmentKey(start)));
|
|
3189
|
+
const chunks = [];
|
|
3190
|
+
let bytes = 0;
|
|
3191
|
+
let expected = startIndex;
|
|
3192
|
+
for (const { start, size } of selected) {
|
|
3193
|
+
if (start > expected) throw new Error(`Missing persisted stream segment at index ${expected}`);
|
|
3194
|
+
const segment = decodeSegment(values.get(segmentKey(start)), start, size);
|
|
3195
|
+
for (let offset = expected - start; offset < segment.length; offset++) {
|
|
3196
|
+
if (expected >= endIndex || bytes + segment[offset].byteLength > maxBytes) return chunks;
|
|
3197
|
+
chunks.push(segment[offset]);
|
|
3198
|
+
bytes += segment[offset].byteLength;
|
|
3199
|
+
expected++;
|
|
3200
|
+
}
|
|
3201
|
+
}
|
|
3202
|
+
return chunks;
|
|
3203
|
+
}
|
|
2834
3204
|
/** Append one bounded binary batch with contiguous indexes. */
|
|
2835
3205
|
async writeChunks(runId, chunks) {
|
|
2836
3206
|
validateStreamWriteChunks(chunks);
|
|
2837
|
-
const storedChunks = chunks.map(compactChunk);
|
|
2838
3207
|
return await this.runMutation(async () => {
|
|
2839
3208
|
const committed = await this.ctx.storage.transaction(async (txn) => {
|
|
2840
3209
|
const stored = await txn.get(META_KEY);
|
|
@@ -2842,16 +3211,28 @@ var StreamDO = class extends DurableObject {
|
|
|
2842
3211
|
this.assertWritable(meta, runId);
|
|
2843
3212
|
const startIndex = meta.count;
|
|
2844
3213
|
if (startIndex + chunks.length > MAX_STREAM_INDEX) throw new Error("Stream offset limit exceeded");
|
|
3214
|
+
const layout = stored === void 0 ? NEW_STREAM_LAYOUT : meta.layout;
|
|
2845
3215
|
const nextMeta = {
|
|
2846
3216
|
count: startIndex + chunks.length,
|
|
2847
3217
|
state: "open",
|
|
2848
|
-
ownerRunId: runId
|
|
3218
|
+
ownerRunId: runId,
|
|
3219
|
+
...layout === void 0 ? {} : { layout }
|
|
2849
3220
|
};
|
|
2850
3221
|
const entries = [[META_KEY, nextMeta]];
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
entries.push([
|
|
2854
|
-
|
|
3222
|
+
if (layout === SEGMENT_LAYOUT) for (const segment of packSegments(startIndex, chunks)) {
|
|
3223
|
+
entries.push([segmentKey(segment.start), encodeSegment(segment.chunks)]);
|
|
3224
|
+
entries.push([segmentSizeKey(segment.start), {
|
|
3225
|
+
count: segment.chunks.length,
|
|
3226
|
+
bytes: segment.chunks.reduce((total, chunk) => total + chunk.byteLength, 0)
|
|
3227
|
+
}]);
|
|
3228
|
+
}
|
|
3229
|
+
else {
|
|
3230
|
+
const storedChunks = chunks.map(compactChunk);
|
|
3231
|
+
for (let offset = 0; offset < storedChunks.length; offset++) {
|
|
3232
|
+
const index = startIndex + offset;
|
|
3233
|
+
entries.push([chunkKey(index), storedChunks[offset]]);
|
|
3234
|
+
entries.push([chunkSizeKey(index), storedChunks[offset].byteLength]);
|
|
3235
|
+
}
|
|
2855
3236
|
}
|
|
2856
3237
|
for (let offset = 0; offset < entries.length; offset += STORAGE_BATCH_KEYS) await txn.put(Object.fromEntries(entries.slice(offset, offset + STORAGE_BATCH_KEYS)));
|
|
2857
3238
|
return {
|
|
@@ -2987,6 +3368,102 @@ var StreamDO = class extends DurableObject {
|
|
|
2987
3368
|
};
|
|
2988
3369
|
});
|
|
2989
3370
|
}
|
|
3371
|
+
/** Delete one page of per-chunk rows, listing size rows to avoid loading payloads. */
|
|
3372
|
+
async expireChunkPage(txn, limit, byteLimit) {
|
|
3373
|
+
const candidates = await txn.list({
|
|
3374
|
+
prefix: CHUNK_SIZE_KEY_PREFIX,
|
|
3375
|
+
limit: limit + 1
|
|
3376
|
+
});
|
|
3377
|
+
const page = [];
|
|
3378
|
+
let bytes = 0;
|
|
3379
|
+
for (const [key, storedSize] of Array.from(candidates).slice(0, limit)) {
|
|
3380
|
+
const index = indexFromChunkSizeKey(key);
|
|
3381
|
+
const size = validateChunkSize(storedSize, index);
|
|
3382
|
+
if (page.length > 0 && bytes + size > byteLimit) break;
|
|
3383
|
+
page.push({
|
|
3384
|
+
index,
|
|
3385
|
+
size
|
|
3386
|
+
});
|
|
3387
|
+
bytes += size;
|
|
3388
|
+
}
|
|
3389
|
+
let payloadFallback = false;
|
|
3390
|
+
if (candidates.size === 0) {
|
|
3391
|
+
const first = (await txn.list({
|
|
3392
|
+
prefix: CHUNK_KEY_PREFIX,
|
|
3393
|
+
limit: 1
|
|
3394
|
+
})).entries().next().value;
|
|
3395
|
+
if (first) {
|
|
3396
|
+
const [key, payload] = first;
|
|
3397
|
+
page.push({
|
|
3398
|
+
index: indexFromChunkKey(key),
|
|
3399
|
+
size: payload instanceof Uint8Array ? payload.byteLength : 0
|
|
3400
|
+
});
|
|
3401
|
+
bytes = page[0].size;
|
|
3402
|
+
payloadFallback = true;
|
|
3403
|
+
}
|
|
3404
|
+
}
|
|
3405
|
+
await deleteInBatches(txn, page.flatMap(({ index }) => [chunkKey(index), chunkSizeKey(index)]));
|
|
3406
|
+
let done = candidates.size === page.length;
|
|
3407
|
+
if (payloadFallback) done = (await txn.list({
|
|
3408
|
+
prefix: CHUNK_KEY_PREFIX,
|
|
3409
|
+
limit: 1
|
|
3410
|
+
})).size === 0;
|
|
3411
|
+
else if (done && candidates.size > 0) done = (await txn.list({
|
|
3412
|
+
prefix: CHUNK_KEY_PREFIX,
|
|
3413
|
+
limit: 1
|
|
3414
|
+
})).size === 0;
|
|
3415
|
+
return {
|
|
3416
|
+
chunks: page.length,
|
|
3417
|
+
bytes,
|
|
3418
|
+
done
|
|
3419
|
+
};
|
|
3420
|
+
}
|
|
3421
|
+
/**
|
|
3422
|
+
* Delete one page of segment rows, listing segment size rows to avoid
|
|
3423
|
+
* loading payloads. One segment always makes progress, even when it holds
|
|
3424
|
+
* more chunks or bytes than the page allows.
|
|
3425
|
+
*/
|
|
3426
|
+
async expireSegmentPage(txn, limit, byteLimit) {
|
|
3427
|
+
const candidates = await txn.list({
|
|
3428
|
+
prefix: SEGMENT_SIZE_KEY_PREFIX,
|
|
3429
|
+
limit: limit + 1
|
|
3430
|
+
});
|
|
3431
|
+
const page = [];
|
|
3432
|
+
let chunks = 0;
|
|
3433
|
+
let bytes = 0;
|
|
3434
|
+
for (const [key, value] of candidates) {
|
|
3435
|
+
const start = startFromSegmentKey(key, SEGMENT_SIZE_KEY_PREFIX);
|
|
3436
|
+
const size = validateSegmentSize(value, start);
|
|
3437
|
+
if (page.length > 0 && (chunks + size.count > limit || bytes + size.bytes > byteLimit)) break;
|
|
3438
|
+
page.push(start);
|
|
3439
|
+
chunks += size.count;
|
|
3440
|
+
bytes += size.bytes;
|
|
3441
|
+
}
|
|
3442
|
+
let orphan = false;
|
|
3443
|
+
if (candidates.size === 0) {
|
|
3444
|
+
const first = (await txn.list({
|
|
3445
|
+
prefix: SEGMENT_KEY_PREFIX,
|
|
3446
|
+
limit: 1
|
|
3447
|
+
})).entries().next().value;
|
|
3448
|
+
if (first) {
|
|
3449
|
+
page.push(startFromSegmentKey(first[0], SEGMENT_KEY_PREFIX));
|
|
3450
|
+
bytes = first[1] instanceof Uint8Array ? first[1].byteLength : 0;
|
|
3451
|
+
chunks = 1;
|
|
3452
|
+
orphan = true;
|
|
3453
|
+
}
|
|
3454
|
+
}
|
|
3455
|
+
await deleteInBatches(txn, page.flatMap((start) => [segmentKey(start), segmentSizeKey(start)]));
|
|
3456
|
+
let done = !orphan && candidates.size === page.length;
|
|
3457
|
+
if (orphan || done) done = (await txn.list({
|
|
3458
|
+
prefix: SEGMENT_KEY_PREFIX,
|
|
3459
|
+
limit: 1
|
|
3460
|
+
})).size === 0;
|
|
3461
|
+
return {
|
|
3462
|
+
chunks,
|
|
3463
|
+
bytes,
|
|
3464
|
+
done
|
|
3465
|
+
};
|
|
3466
|
+
}
|
|
2990
3467
|
/**
|
|
2991
3468
|
* Fence the stream and delete one bounded page. Listing size keys avoids
|
|
2992
3469
|
* loading payloads, and the transaction makes a crash retry resume safely.
|
|
@@ -3000,50 +3477,8 @@ var StreamDO = class extends DurableObject {
|
|
|
3000
3477
|
const meta = stored === void 0 ? emptyMeta() : validateMeta(stored);
|
|
3001
3478
|
this.assertOwner(meta, runId);
|
|
3002
3479
|
const firstFence = meta.state !== "expired";
|
|
3003
|
-
const
|
|
3004
|
-
|
|
3005
|
-
limit: limit + 1
|
|
3006
|
-
});
|
|
3007
|
-
const page = [];
|
|
3008
|
-
let bytes = 0;
|
|
3009
|
-
for (const [key, storedSize] of Array.from(candidates).slice(0, limit)) {
|
|
3010
|
-
const index = indexFromChunkSizeKey(key);
|
|
3011
|
-
const size = validateChunkSize(storedSize, index);
|
|
3012
|
-
if (page.length > 0 && bytes + size > byteLimit) break;
|
|
3013
|
-
page.push({
|
|
3014
|
-
index,
|
|
3015
|
-
size
|
|
3016
|
-
});
|
|
3017
|
-
bytes += size;
|
|
3018
|
-
}
|
|
3019
|
-
let payloadFallback = false;
|
|
3020
|
-
if (candidates.size === 0) {
|
|
3021
|
-
const first = (await txn.list({
|
|
3022
|
-
prefix: CHUNK_KEY_PREFIX,
|
|
3023
|
-
limit: 1
|
|
3024
|
-
})).entries().next().value;
|
|
3025
|
-
if (first) {
|
|
3026
|
-
const [key, payload] = first;
|
|
3027
|
-
page.push({
|
|
3028
|
-
index: indexFromChunkKey(key),
|
|
3029
|
-
size: payload instanceof Uint8Array ? payload.byteLength : 0
|
|
3030
|
-
});
|
|
3031
|
-
bytes = page[0].size;
|
|
3032
|
-
payloadFallback = true;
|
|
3033
|
-
}
|
|
3034
|
-
}
|
|
3035
|
-
const keys = page.flatMap(({ index }) => [chunkKey(index), chunkSizeKey(index)]);
|
|
3036
|
-
for (let offset = 0; offset < keys.length; offset += DELETE_BATCH_KEYS) await txn.delete(keys.slice(offset, offset + DELETE_BATCH_KEYS));
|
|
3037
|
-
let done = candidates.size === page.length;
|
|
3038
|
-
if (payloadFallback) done = (await txn.list({
|
|
3039
|
-
prefix: CHUNK_KEY_PREFIX,
|
|
3040
|
-
limit: 1
|
|
3041
|
-
})).size === 0;
|
|
3042
|
-
else if (done && candidates.size > 0) done = (await txn.list({
|
|
3043
|
-
prefix: CHUNK_KEY_PREFIX,
|
|
3044
|
-
limit: 1
|
|
3045
|
-
})).size === 0;
|
|
3046
|
-
if (done && page.length === 0 && meta.state === "expired" && meta.payloadDeleted) return {
|
|
3480
|
+
const { chunks, bytes, done } = meta.layout === SEGMENT_LAYOUT ? await this.expireSegmentPage(txn, limit, byteLimit) : await this.expireChunkPage(txn, limit, byteLimit);
|
|
3481
|
+
if (done && chunks === 0 && meta.state === "expired" && meta.payloadDeleted) return {
|
|
3047
3482
|
meta,
|
|
3048
3483
|
result: {
|
|
3049
3484
|
deleted: false,
|
|
@@ -3067,7 +3502,7 @@ var StreamDO = class extends DurableObject {
|
|
|
3067
3502
|
meta: nextMeta,
|
|
3068
3503
|
result: {
|
|
3069
3504
|
deleted: firstFence,
|
|
3070
|
-
chunks
|
|
3505
|
+
chunks,
|
|
3071
3506
|
bytes,
|
|
3072
3507
|
done
|
|
3073
3508
|
}
|
|
@@ -3152,6 +3587,14 @@ const CLEANUP_RETRY_MAX_MS = 36e5;
|
|
|
3152
3587
|
const MAX_DATE_MS = 864e13;
|
|
3153
3588
|
const QUEUE_ORPHAN_KEY = "queue-orphan";
|
|
3154
3589
|
const QUEUE_RESERVATION_KEY = "queue-reservation";
|
|
3590
|
+
/** Run-scoped Queue idempotency state, one key per claim name, in the run's own cell. */
|
|
3591
|
+
const RUN_QUEUE_CLAIM_PREFIX = "queue-claim:";
|
|
3592
|
+
/**
|
|
3593
|
+
* Present when every idempotency claim of this run lives in the run's own
|
|
3594
|
+
* cell. Written only when the run is created, so a run keeps one claim scope
|
|
3595
|
+
* for its whole life and claims made before the marker existed stay valid.
|
|
3596
|
+
*/
|
|
3597
|
+
const RUN_QUEUE_CLAIM_SCOPE_KEY = "queue-claim-scope";
|
|
3155
3598
|
function validateCleanupRequest(request, allowDisabled) {
|
|
3156
3599
|
if (!isRecord$1(request)) throw new Error("world-celld retention request must be an object");
|
|
3157
3600
|
if (!(allowDisabled ? isNonNegativeSafeInteger(request.retentionMs) : isPositiveSafeInteger(request.retentionMs))) throw new Error(`world-celld retentionMs must be a ${allowDisabled ? "non-negative" : "positive"} safe integer`);
|
|
@@ -3205,6 +3648,12 @@ async function putStorageEntries(storage, entries) {
|
|
|
3205
3648
|
const all = Array.from(entries);
|
|
3206
3649
|
for (let offset = 0; offset < all.length; offset += STORAGE_BATCH_SIZE) await storage.put(Object.fromEntries(all.slice(offset, offset + STORAGE_BATCH_SIZE)));
|
|
3207
3650
|
}
|
|
3651
|
+
function runQueueClaimKey(claimName) {
|
|
3652
|
+
return `${RUN_QUEUE_CLAIM_PREFIX}${claimName}`;
|
|
3653
|
+
}
|
|
3654
|
+
function validRunQueueClaimName(claimName) {
|
|
3655
|
+
return typeof claimName === "string" && claimName.startsWith("claim:");
|
|
3656
|
+
}
|
|
3208
3657
|
/**
|
|
3209
3658
|
* Durable Object for managing a single workflow run's state.
|
|
3210
3659
|
*
|
|
@@ -3285,6 +3734,7 @@ var WorkflowRunDO = class extends DurableObject {
|
|
|
3285
3734
|
});
|
|
3286
3735
|
if (!outcome.ok) return outcome;
|
|
3287
3736
|
if (eventSequence !== storedSequence) await txn.put(EVENT_SEQUENCE_KEY, eventSequence);
|
|
3737
|
+
if (outcome.runCreated) await txn.put(RUN_QUEUE_CLAIM_SCOPE_KEY, "run");
|
|
3288
3738
|
await putStorageEntries(txn, (outcome.hookToIndex ? [{
|
|
3289
3739
|
hookId: outcome.hookToIndex.hookId,
|
|
3290
3740
|
token: outcome.hookToIndex.token
|
|
@@ -3338,6 +3788,28 @@ var WorkflowRunDO = class extends DurableObject {
|
|
|
3338
3788
|
async getRun() {
|
|
3339
3789
|
return this.readKey("run");
|
|
3340
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
|
+
}
|
|
3341
3813
|
/** Single authoritative lifecycle answer used after derivative compaction. */
|
|
3342
3814
|
async getLifecycleStatus() {
|
|
3343
3815
|
return await this.ctx.storage.transaction(async (txn) => {
|
|
@@ -3951,6 +4423,189 @@ var WorkflowRunDO = class extends DurableObject {
|
|
|
3951
4423
|
await txn.setAlarm(this.now() + delay);
|
|
3952
4424
|
});
|
|
3953
4425
|
}
|
|
4426
|
+
/**
|
|
4427
|
+
* Read-only admission for a run-bearing Queue message whose body travels
|
|
4428
|
+
* inline in the broker envelope. It answers what payload registration does
|
|
4429
|
+
* for offloaded bodies, without writing anything.
|
|
4430
|
+
*/
|
|
4431
|
+
async getQueueAdmission() {
|
|
4432
|
+
const { tombstone } = await this.retentionState(this.ctx.storage);
|
|
4433
|
+
return tombstone ? {
|
|
4434
|
+
ok: false,
|
|
4435
|
+
message: `Workflow run "${tombstone.runId}" expired at ${tombstone.expiredAt.toISOString()}`
|
|
4436
|
+
} : { ok: true };
|
|
4437
|
+
}
|
|
4438
|
+
/**
|
|
4439
|
+
* Enqueue-side admission and idempotency reservation in one transaction,
|
|
4440
|
+
* for a run-scoped claim. Replaces a separate claim cell's reservation plus
|
|
4441
|
+
* the run expiry check.
|
|
4442
|
+
*/
|
|
4443
|
+
async reserveRunQueueMessage(params) {
|
|
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");
|
|
4445
|
+
return await this.ctx.storage.transaction(async (txn) => {
|
|
4446
|
+
const key = runQueueClaimKey(params.claimName);
|
|
4447
|
+
const values = await txn.get([
|
|
4448
|
+
TOMBSTONE_KEY,
|
|
4449
|
+
CLEANUP_RECORD_KEY,
|
|
4450
|
+
RUN_QUEUE_CLAIM_SCOPE_KEY,
|
|
4451
|
+
key
|
|
4452
|
+
]);
|
|
4453
|
+
const tombstone = this.tombstoneFrom(values);
|
|
4454
|
+
if (tombstone) return {
|
|
4455
|
+
ok: false,
|
|
4456
|
+
message: `Workflow run "${tombstone.runId}" expired at ${tombstone.expiredAt.toISOString()}`
|
|
4457
|
+
};
|
|
4458
|
+
if (values.get("queue-claim-scope") !== "run") return {
|
|
4459
|
+
ok: true,
|
|
4460
|
+
scope: "cell"
|
|
4461
|
+
};
|
|
4462
|
+
const state = values.get(key) ?? {};
|
|
4463
|
+
const existing = state.reservation;
|
|
4464
|
+
if (existing && existing.expiresAt > this.now() && (existing.messageId !== params.messageId || params.publicationPending)) return {
|
|
4465
|
+
ok: true,
|
|
4466
|
+
scope: "run",
|
|
4467
|
+
admitted: false,
|
|
4468
|
+
messageId: existing.messageId,
|
|
4469
|
+
...existing.publicationPending ? { publicationPending: true } : {}
|
|
4470
|
+
};
|
|
4471
|
+
await txn.put(key, {
|
|
4472
|
+
...state,
|
|
4473
|
+
reservation: {
|
|
4474
|
+
messageId: params.messageId,
|
|
4475
|
+
expiresAt: Math.max(existing?.expiresAt ?? 0, params.expiresAt),
|
|
4476
|
+
...params.publicationPending ? { publicationPending: true } : {}
|
|
4477
|
+
}
|
|
4478
|
+
});
|
|
4479
|
+
return {
|
|
4480
|
+
ok: true,
|
|
4481
|
+
scope: "run",
|
|
4482
|
+
admitted: true,
|
|
4483
|
+
messageId: params.messageId
|
|
4484
|
+
};
|
|
4485
|
+
});
|
|
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
|
+
}
|
|
4502
|
+
/** Delivery-side run expiry check and in-flight claim in one transaction. */
|
|
4503
|
+
async claimRunQueueMessage(params) {
|
|
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");
|
|
4505
|
+
return await this.ctx.storage.transaction(async (txn) => {
|
|
4506
|
+
const key = runQueueClaimKey(params.claimName);
|
|
4507
|
+
const values = await txn.get([
|
|
4508
|
+
TOMBSTONE_KEY,
|
|
4509
|
+
CLEANUP_RECORD_KEY,
|
|
4510
|
+
key
|
|
4511
|
+
]);
|
|
4512
|
+
if (this.tombstoneFrom(values)) return { expired: true };
|
|
4513
|
+
let state = values.get(key) ?? {};
|
|
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;
|
|
4526
|
+
if (existing && existing.expiresAt > now) {
|
|
4527
|
+
if (existing.messageId !== params.messageId) return {
|
|
4528
|
+
expired: false,
|
|
4529
|
+
claimed: false
|
|
4530
|
+
};
|
|
4531
|
+
if (existing.retryAt === void 0 || existing.retryAt > now) return {
|
|
4532
|
+
expired: false,
|
|
4533
|
+
claimed: false,
|
|
4534
|
+
retryAt: existing.retryAt ?? existing.expiresAt
|
|
4535
|
+
};
|
|
4536
|
+
}
|
|
4537
|
+
await txn.put(key, {
|
|
4538
|
+
...state,
|
|
4539
|
+
claim: {
|
|
4540
|
+
messageId: params.messageId,
|
|
4541
|
+
expiresAt: now + params.staleMs
|
|
4542
|
+
}
|
|
4543
|
+
});
|
|
4544
|
+
return {
|
|
4545
|
+
expired: false,
|
|
4546
|
+
claimed: true
|
|
4547
|
+
};
|
|
4548
|
+
});
|
|
4549
|
+
}
|
|
4550
|
+
async holdRunQueueMessage(params) {
|
|
4551
|
+
if (!validRunQueueClaimName(params?.claimName) || typeof params.messageId !== "string" || params.messageId.length === 0 || !isNonNegativeSafeInteger(params.retryAt) || !isNonNegativeSafeInteger(params.expiresAt) || params.reservationExpiresAt !== void 0 && !isNonNegativeSafeInteger(params.reservationExpiresAt)) throw new TypeError("world-celld run queue claim hold is invalid");
|
|
4552
|
+
return await this.ctx.storage.transaction(async (txn) => {
|
|
4553
|
+
const key = runQueueClaimKey(params.claimName);
|
|
4554
|
+
const state = await txn.get(key) ?? {};
|
|
4555
|
+
const existing = state.claim;
|
|
4556
|
+
if (!existing || existing.messageId !== params.messageId) return { held: false };
|
|
4557
|
+
const next = {
|
|
4558
|
+
...state,
|
|
4559
|
+
claim: {
|
|
4560
|
+
...existing,
|
|
4561
|
+
retryAt: params.retryAt,
|
|
4562
|
+
expiresAt: Math.max(existing.expiresAt, params.expiresAt)
|
|
4563
|
+
}
|
|
4564
|
+
};
|
|
4565
|
+
if (state.reservation?.messageId === params.messageId && params.reservationExpiresAt !== void 0 && params.reservationExpiresAt > state.reservation.expiresAt) next.reservation = {
|
|
4566
|
+
...state.reservation,
|
|
4567
|
+
expiresAt: params.reservationExpiresAt
|
|
4568
|
+
};
|
|
4569
|
+
await txn.put(key, next);
|
|
4570
|
+
return { held: true };
|
|
4571
|
+
});
|
|
4572
|
+
}
|
|
4573
|
+
async releaseRunQueueMessage(params) {
|
|
4574
|
+
if (!validRunQueueClaimName(params?.claimName) || typeof params.messageId !== "string") throw new TypeError("world-celld run queue claim release is invalid");
|
|
4575
|
+
await this.ctx.storage.transaction(async (txn) => {
|
|
4576
|
+
const key = runQueueClaimKey(params.claimName);
|
|
4577
|
+
const state = await txn.get(key);
|
|
4578
|
+
if (state?.claim?.messageId !== params.messageId) return;
|
|
4579
|
+
const { claim: _released, ...rest } = state;
|
|
4580
|
+
if (rest.reservation) await txn.put(key, rest);
|
|
4581
|
+
else await txn.delete(key);
|
|
4582
|
+
});
|
|
4583
|
+
}
|
|
4584
|
+
async completeRunQueueMessage(params) {
|
|
4585
|
+
if (!validRunQueueClaimName(params?.claimName) || typeof params.messageId !== "string") throw new TypeError("world-celld run queue completion is invalid");
|
|
4586
|
+
await this.ctx.storage.transaction(async (txn) => {
|
|
4587
|
+
const key = runQueueClaimKey(params.claimName);
|
|
4588
|
+
const state = await txn.get(key);
|
|
4589
|
+
if (!state) return;
|
|
4590
|
+
const next = {};
|
|
4591
|
+
if (state.reservation && state.reservation.messageId !== params.messageId) next.reservation = state.reservation;
|
|
4592
|
+
if (state.claim && state.claim.messageId !== params.messageId) next.claim = state.claim;
|
|
4593
|
+
if (next.reservation || next.claim) await txn.put(key, next);
|
|
4594
|
+
else await txn.delete(key);
|
|
4595
|
+
});
|
|
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
|
+
}
|
|
3954
4609
|
async registerQueuePayload(registration) {
|
|
3955
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");
|
|
3956
4611
|
return await this.ctx.storage.transaction(async (txn) => {
|
|
@@ -4007,22 +4662,24 @@ var WorkflowRunDO = class extends DurableObject {
|
|
|
4007
4662
|
});
|
|
4008
4663
|
}
|
|
4009
4664
|
async reserveQueueMessage(params) {
|
|
4010
|
-
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");
|
|
4011
4666
|
return await this.ctx.storage.transaction(async (txn) => {
|
|
4012
4667
|
const values = await txn.get([QUEUE_RESERVATION_KEY, "claim"]);
|
|
4013
4668
|
const existing = values.get(QUEUE_RESERVATION_KEY);
|
|
4014
4669
|
const claim = values.get("claim");
|
|
4015
4670
|
const now = this.now();
|
|
4016
|
-
if (existing && existing.expiresAt > now && existing.messageId !== params.messageId) {
|
|
4671
|
+
if (existing && existing.expiresAt > now && (existing.messageId !== params.messageId || params.publicationPending)) {
|
|
4017
4672
|
await txn.setAlarm(Math.min(existing.expiresAt, claim?.expiresAt ?? existing.expiresAt));
|
|
4018
4673
|
return {
|
|
4019
4674
|
admitted: false,
|
|
4020
|
-
messageId: existing.messageId
|
|
4675
|
+
messageId: existing.messageId,
|
|
4676
|
+
...existing.publicationPending ? { publicationPending: true } : {}
|
|
4021
4677
|
};
|
|
4022
4678
|
}
|
|
4023
4679
|
const reservation = {
|
|
4024
4680
|
messageId: params.messageId,
|
|
4025
|
-
expiresAt: Math.max(existing?.expiresAt ?? 0, params.expiresAt)
|
|
4681
|
+
expiresAt: Math.max(existing?.expiresAt ?? 0, params.expiresAt),
|
|
4682
|
+
...params.publicationPending ? { publicationPending: true } : {}
|
|
4026
4683
|
};
|
|
4027
4684
|
await txn.put(QUEUE_RESERVATION_KEY, reservation);
|
|
4028
4685
|
await txn.setAlarm(Math.min(reservation.expiresAt, claim?.expiresAt ?? reservation.expiresAt));
|
|
@@ -4032,6 +4689,20 @@ var WorkflowRunDO = class extends DurableObject {
|
|
|
4032
4689
|
};
|
|
4033
4690
|
});
|
|
4034
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
|
+
}
|
|
4035
4706
|
async completeQueueMessage(messageId) {
|
|
4036
4707
|
if (typeof messageId !== "string" || messageId.length === 0) throw new TypeError("world-celld queue messageId must be a non-empty string");
|
|
4037
4708
|
await this.ctx.storage.transaction(async (txn) => {
|
|
@@ -4051,13 +4722,32 @@ var WorkflowRunDO = class extends DurableObject {
|
|
|
4051
4722
|
else await txn.deleteAlarm();
|
|
4052
4723
|
});
|
|
4053
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
|
+
}
|
|
4054
4737
|
async claimInflight(params) {
|
|
4055
4738
|
if (typeof params?.messageId !== "string" || params.messageId.length === 0 || !isPositiveSafeInteger(params.staleMs)) throw new TypeError("world-celld queue claim is invalid");
|
|
4056
4739
|
return await this.ctx.storage.transaction(async (txn) => {
|
|
4057
4740
|
const values = await txn.get(["claim", QUEUE_RESERVATION_KEY]);
|
|
4058
4741
|
const existing = values.get("claim");
|
|
4059
|
-
|
|
4742
|
+
let reservation = values.get(QUEUE_RESERVATION_KEY);
|
|
4060
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
|
+
}
|
|
4061
4751
|
if (existing && existing.expiresAt > now) {
|
|
4062
4752
|
if (existing.messageId !== params.messageId) return { claimed: false };
|
|
4063
4753
|
if (existing.retryAt === void 0 || existing.retryAt > now) return {
|