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