@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/worker.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { _ as
|
|
1
|
+
import { S as strictIntegerSetting, _ as QUEUE_CLAIM_STALE_MS, a as isInlineRunBody, b as isRecord$1, c as queueOrphanName, d as validateNativeQueueEnvelope, f as validateNativeQueueSendOptions, g as MAX_RUN_INDEX_PUBLICATION_LIFETIME_MS, h as LIFECYCLE_COMPACTION_RETRY_MS, i as QUEUE_RESERVATION_GRACE_MS, l as queuePayloadObjectKey, m as HOOK_CLAIM_LEASE_MS, n as QUEUE_PAYLOAD_REGISTRY_PREFIX, r as QUEUE_PUBLICATION_LEASE_MS, s as queueClaimName, t as NATIVE_QUEUE_MAX_MESSAGE_BYTES, u as queuePayloadRegistryKey, v as isNonNegativeSafeInteger, x as queueDelayDeadline, y as isPositiveSafeInteger } from "./queue-protocol-C1zbXwa_.js";
|
|
2
2
|
import { DurableObject, WorkerEntrypoint } from "cloudflare:workers";
|
|
3
3
|
import { CreateEventSchema, EventSchema, HookSchema, SPEC_VERSION_CURRENT, StepSchema, WaitSchema, WorkflowRunSchema, applyAttributeChanges, eventIdToSlot, getMaxEventsPerRun, isChildEntityCreationEvent, isTerminalStepStatus, isTerminalWorkflowRunStatus, slotToEventId, validateAttributeChanges } from "@workflow/world";
|
|
4
4
|
//#region src/worker/auth.ts
|
|
@@ -42,10 +42,75 @@ async function authenticate(request, secret) {
|
|
|
42
42
|
return { ok: true };
|
|
43
43
|
}
|
|
44
44
|
//#endregion
|
|
45
|
+
//#region src/worker/queue-claims.ts
|
|
46
|
+
/**
|
|
47
|
+
* Idempotency claims for Queue messages. A claim lives either in its own
|
|
48
|
+
* claim cell (named by queue and idempotency key) or, for run-bearing
|
|
49
|
+
* messages whose envelope says so, as an entry in the run's own cell.
|
|
50
|
+
*/
|
|
51
|
+
function cellQueueClaim(cell) {
|
|
52
|
+
return {
|
|
53
|
+
claim: (messageId, staleMs) => cell.claimInflight({
|
|
54
|
+
messageId,
|
|
55
|
+
staleMs
|
|
56
|
+
}),
|
|
57
|
+
hold: (params) => cell.holdInflight(params),
|
|
58
|
+
release: (messageId) => cell.releaseInflight(messageId),
|
|
59
|
+
abandonReservation: (messageId) => cell.abandonQueueMessageReservation(messageId),
|
|
60
|
+
confirmPublication: (messageId, expiresAt) => cell.confirmQueueMessagePublication({
|
|
61
|
+
messageId,
|
|
62
|
+
expiresAt
|
|
63
|
+
}),
|
|
64
|
+
complete: (messageId) => cell.completeQueueMessage(messageId)
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
function runQueueClaim(run, claimName) {
|
|
68
|
+
return {
|
|
69
|
+
claim: async (messageId, staleMs) => {
|
|
70
|
+
const result = await run.claimRunQueueMessage({
|
|
71
|
+
claimName,
|
|
72
|
+
messageId,
|
|
73
|
+
staleMs
|
|
74
|
+
});
|
|
75
|
+
return result.expired ? {
|
|
76
|
+
expired: true,
|
|
77
|
+
claimed: false
|
|
78
|
+
} : result;
|
|
79
|
+
},
|
|
80
|
+
hold: (params) => run.holdRunQueueMessage({
|
|
81
|
+
claimName,
|
|
82
|
+
...params
|
|
83
|
+
}),
|
|
84
|
+
release: (messageId) => run.releaseRunQueueMessage({
|
|
85
|
+
claimName,
|
|
86
|
+
messageId
|
|
87
|
+
}),
|
|
88
|
+
abandonReservation: (messageId) => run.abandonRunQueueMessageReservation({
|
|
89
|
+
claimName,
|
|
90
|
+
messageId
|
|
91
|
+
}),
|
|
92
|
+
confirmPublication: (messageId, expiresAt) => run.confirmRunQueueMessagePublication({
|
|
93
|
+
claimName,
|
|
94
|
+
messageId,
|
|
95
|
+
expiresAt
|
|
96
|
+
}),
|
|
97
|
+
complete: (messageId) => run.completeRunQueueMessage({
|
|
98
|
+
claimName,
|
|
99
|
+
messageId
|
|
100
|
+
})
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
/** The claim a delivered envelope was reserved under, if it has an idempotency key. */
|
|
104
|
+
function deliveryQueueClaim(envelope, runStub) {
|
|
105
|
+
if (!envelope.idempotencyKey) return void 0;
|
|
106
|
+
const claimName = queueClaimName(envelope.queueName, envelope.idempotencyKey);
|
|
107
|
+
return envelope.claimScope === "run" && envelope.runId ? runQueueClaim(runStub(envelope.runId), claimName) : cellQueueClaim(runStub(claimName));
|
|
108
|
+
}
|
|
109
|
+
//#endregion
|
|
45
110
|
//#region src/worker/queue-delivery.ts
|
|
46
111
|
/** Internal delivery contract. No Request/Response objects cross the RPC boundary. */
|
|
47
112
|
const MAX_QUEUE_DELIVERY_RESPONSE_BYTES = 65536;
|
|
48
|
-
const QUEUE_ORPHAN_GRACE_MS
|
|
113
|
+
const QUEUE_ORPHAN_GRACE_MS = 432e6;
|
|
49
114
|
const PERMANENT_QUEUE_STATUSES = /* @__PURE__ */ new Set([
|
|
50
115
|
404,
|
|
51
116
|
409,
|
|
@@ -84,10 +149,10 @@ async function deliverQueueMessage(env, secret, input, attempt) {
|
|
|
84
149
|
const namespace = env.WORKFLOW_DB;
|
|
85
150
|
const payloadStore = env.WORKFLOW_QUEUE_PAYLOADS;
|
|
86
151
|
const runStub = (name) => namespace.get(namespace.idFromName(name));
|
|
87
|
-
|
|
152
|
+
const claim = deliveryQueueClaim(envelope, runStub);
|
|
88
153
|
let claimed = false;
|
|
89
154
|
async function releaseClaim() {
|
|
90
|
-
if (claimed) await claim.
|
|
155
|
+
if (claimed) await claim.release(envelope.messageId);
|
|
91
156
|
}
|
|
92
157
|
async function unregisterPayload() {
|
|
93
158
|
if (envelope.runId) {
|
|
@@ -96,12 +161,14 @@ async function deliverQueueMessage(env, secret, input, attempt) {
|
|
|
96
161
|
}
|
|
97
162
|
}
|
|
98
163
|
try {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
164
|
+
let admission;
|
|
165
|
+
if (envelope.runId && !envelope.payloadKey && envelope.claimScope !== "run") {
|
|
166
|
+
admission = runStub(envelope.runId).getQueueAdmission();
|
|
167
|
+
admission.catch(() => void 0);
|
|
168
|
+
}
|
|
169
|
+
if (claim) {
|
|
170
|
+
const result = await claim.claim(envelope.messageId, QUEUE_CLAIM_STALE_MS);
|
|
171
|
+
if (result.expired) return { kind: "complete" };
|
|
105
172
|
if (!result.claimed) {
|
|
106
173
|
const now = Date.now();
|
|
107
174
|
if (result.retryAt !== void 0 && result.retryAt > now) return {
|
|
@@ -112,13 +179,17 @@ async function deliverQueueMessage(env, secret, input, attempt) {
|
|
|
112
179
|
}
|
|
113
180
|
claimed = true;
|
|
114
181
|
}
|
|
182
|
+
if (admission && !(await admission).ok) {
|
|
183
|
+
await claim?.complete(envelope.messageId);
|
|
184
|
+
return { kind: "complete" };
|
|
185
|
+
}
|
|
115
186
|
let body = envelope.body;
|
|
116
187
|
if (envelope.payloadKey) {
|
|
117
188
|
if (!payloadStore) throw new Error("missing binding: WORKFLOW_QUEUE_PAYLOADS");
|
|
118
189
|
body = await payloadStore.read(envelope.payloadKey) ?? void 0;
|
|
119
190
|
if (body === void 0) {
|
|
120
191
|
await unregisterPayload();
|
|
121
|
-
await claim?.
|
|
192
|
+
await claim?.complete(envelope.messageId);
|
|
122
193
|
return { kind: "complete" };
|
|
123
194
|
}
|
|
124
195
|
}
|
|
@@ -141,7 +212,7 @@ async function deliverQueueMessage(env, secret, input, attempt) {
|
|
|
141
212
|
await payloadStore.delete(envelope.payloadKey);
|
|
142
213
|
await unregisterPayload();
|
|
143
214
|
}
|
|
144
|
-
await claim?.
|
|
215
|
+
await claim?.complete(envelope.messageId);
|
|
145
216
|
return { kind: "complete" };
|
|
146
217
|
}
|
|
147
218
|
if (callback.status === 503) {
|
|
@@ -153,13 +224,13 @@ async function deliverQueueMessage(env, secret, input, attempt) {
|
|
|
153
224
|
messageId: envelope.messageId,
|
|
154
225
|
runId: envelope.runId,
|
|
155
226
|
key: envelope.payloadKey,
|
|
156
|
-
expiresAt: notBefore + QUEUE_ORPHAN_GRACE_MS
|
|
227
|
+
expiresAt: notBefore + QUEUE_ORPHAN_GRACE_MS
|
|
157
228
|
});
|
|
158
|
-
if (claim) await claim.
|
|
229
|
+
if (claim) await claim.hold({
|
|
159
230
|
messageId: envelope.messageId,
|
|
160
231
|
retryAt: notBefore,
|
|
161
232
|
expiresAt: notBefore + QUEUE_CLAIM_STALE_MS,
|
|
162
|
-
reservationExpiresAt: notBefore + QUEUE_ORPHAN_GRACE_MS
|
|
233
|
+
reservationExpiresAt: notBefore + QUEUE_ORPHAN_GRACE_MS
|
|
163
234
|
});
|
|
164
235
|
return {
|
|
165
236
|
kind: "suspend",
|
|
@@ -370,8 +441,8 @@ function readStringProp(value, key) {
|
|
|
370
441
|
}
|
|
371
442
|
}
|
|
372
443
|
/**
|
|
373
|
-
* Map a failure eventData.error (which may be a string, an Error-shaped
|
|
374
|
-
* object, or anything else) to the structured error stored on
|
|
444
|
+
* Map a run failure eventData.error (which may be a string, an Error-shaped
|
|
445
|
+
* object, or anything else) to the structured error stored on runs.
|
|
375
446
|
* Matches upstream: string errors keep their text, errorCode is preserved.
|
|
376
447
|
*/
|
|
377
448
|
function toStructuredError(error, opts) {
|
|
@@ -847,14 +918,17 @@ async function applyEvent(store, ctx) {
|
|
|
847
918
|
}
|
|
848
919
|
if (!step) return failure("STEP_NOT_FOUND", `Step "${data.correlationId}" not found`);
|
|
849
920
|
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) });
|
|
850
|
-
const updated = StepSchema.parse(
|
|
851
|
-
...
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
921
|
+
const updated = StepSchema.parse({
|
|
922
|
+
...compact({
|
|
923
|
+
...step,
|
|
924
|
+
status: "running",
|
|
925
|
+
startedAt: step.startedAt ?? now,
|
|
926
|
+
attempt: step.attempt + 1,
|
|
927
|
+
retryAfter: void 0,
|
|
928
|
+
updatedAt: now
|
|
929
|
+
}),
|
|
930
|
+
error: step.error
|
|
931
|
+
});
|
|
858
932
|
const event = buildEvent({ ...data });
|
|
859
933
|
await store.put(`${STEP_KEY_PREFIX}${data.correlationId}`, updated);
|
|
860
934
|
await putEvent(event);
|
|
@@ -869,13 +943,16 @@ async function applyEvent(store, ctx) {
|
|
|
869
943
|
case "step_completed": {
|
|
870
944
|
const step = validatedStep;
|
|
871
945
|
if (!step) return failure("STEP_NOT_FOUND", `Step "${data.correlationId}" not found`);
|
|
872
|
-
const updated = StepSchema.parse(
|
|
873
|
-
...
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
946
|
+
const updated = StepSchema.parse({
|
|
947
|
+
...compact({
|
|
948
|
+
...step,
|
|
949
|
+
status: "completed",
|
|
950
|
+
output: data.eventData.result,
|
|
951
|
+
completedAt: now,
|
|
952
|
+
updatedAt: now
|
|
953
|
+
}),
|
|
954
|
+
error: step.error
|
|
955
|
+
});
|
|
879
956
|
const event = buildEvent({ ...data });
|
|
880
957
|
await store.put(`${STEP_KEY_PREFIX}${data.correlationId}`, updated);
|
|
881
958
|
await putEvent(event);
|
|
@@ -889,13 +966,13 @@ async function applyEvent(store, ctx) {
|
|
|
889
966
|
case "step_failed": {
|
|
890
967
|
const step = validatedStep;
|
|
891
968
|
if (!step) return failure("STEP_NOT_FOUND", `Step "${data.correlationId}" not found`);
|
|
892
|
-
const updated = StepSchema.parse(
|
|
969
|
+
const updated = StepSchema.parse({
|
|
893
970
|
...step,
|
|
894
971
|
status: "failed",
|
|
895
|
-
error:
|
|
972
|
+
error: data.eventData.error,
|
|
896
973
|
completedAt: now,
|
|
897
974
|
updatedAt: now
|
|
898
|
-
})
|
|
975
|
+
});
|
|
899
976
|
const event = buildEvent({ ...data });
|
|
900
977
|
await store.put(`${STEP_KEY_PREFIX}${data.correlationId}`, updated);
|
|
901
978
|
await putEvent(event);
|
|
@@ -909,13 +986,13 @@ async function applyEvent(store, ctx) {
|
|
|
909
986
|
case "step_retrying": {
|
|
910
987
|
const step = validatedStep;
|
|
911
988
|
if (!step) return failure("STEP_NOT_FOUND", `Step "${data.correlationId}" not found`);
|
|
912
|
-
const updated = StepSchema.parse(
|
|
989
|
+
const updated = StepSchema.parse({
|
|
913
990
|
...step,
|
|
914
991
|
status: "pending",
|
|
915
|
-
error:
|
|
992
|
+
error: data.eventData.error,
|
|
916
993
|
retryAfter: data.eventData.retryAfter ? new Date(data.eventData.retryAfter) : void 0,
|
|
917
994
|
updatedAt: now
|
|
918
|
-
})
|
|
995
|
+
});
|
|
919
996
|
const event = buildEvent({ ...data });
|
|
920
997
|
await store.put(`${STEP_KEY_PREFIX}${data.correlationId}`, updated);
|
|
921
998
|
await putEvent(event);
|
|
@@ -951,7 +1028,7 @@ async function applyEvent(store, ctx) {
|
|
|
951
1028
|
}
|
|
952
1029
|
if (await store.get(markerKey) !== void 0) {
|
|
953
1030
|
const existingHook = await store.get(hookKey);
|
|
954
|
-
if (existingHook && !holder) return {
|
|
1031
|
+
if (existingHook && !holder && existingHook.token === token) return {
|
|
955
1032
|
ok: true,
|
|
956
1033
|
hook: existingHook,
|
|
957
1034
|
hookToIndex: existingHook,
|
|
@@ -1854,6 +1931,18 @@ const INDEX_OPERATIONS = /* @__PURE__ */ new Set([
|
|
|
1854
1931
|
*/
|
|
1855
1932
|
const WORLD_NAME = "world-celld";
|
|
1856
1933
|
const WORLD_VERSION = "0.1.0";
|
|
1934
|
+
/**
|
|
1935
|
+
* Per-isolate record of runs whose Queue idempotency claims stay in claim
|
|
1936
|
+
* cells (created before run-scoped claims) or live in the run cell. A run's
|
|
1937
|
+
* scope never changes, so the cache only saves the run-cell question.
|
|
1938
|
+
*/
|
|
1939
|
+
const runClaimScopes = /* @__PURE__ */ new Map();
|
|
1940
|
+
const MAX_REMEMBERED_RUN_CLAIM_SCOPES = 4096;
|
|
1941
|
+
function rememberRunClaimScope(runId, scope) {
|
|
1942
|
+
if (runClaimScopes.has(runId)) return;
|
|
1943
|
+
if (runClaimScopes.size >= MAX_REMEMBERED_RUN_CLAIM_SCOPES) runClaimScopes.delete(runClaimScopes.keys().next().value);
|
|
1944
|
+
runClaimScopes.set(runId, scope);
|
|
1945
|
+
}
|
|
1857
1946
|
function workflowIndex(env) {
|
|
1858
1947
|
return createWorkflowIndex({
|
|
1859
1948
|
runCatalog: env.WORKFLOW_RUN_CATALOG,
|
|
@@ -1908,7 +1997,8 @@ const BINDINGS = {
|
|
|
1908
1997
|
"scheduleCleanup",
|
|
1909
1998
|
"cleanupNow",
|
|
1910
1999
|
"rearmCleanup",
|
|
1911
|
-
"resolveHookTokenClaim"
|
|
2000
|
+
"resolveHookTokenClaim",
|
|
2001
|
+
"resolveExpiredHookClaim"
|
|
1912
2002
|
])
|
|
1913
2003
|
},
|
|
1914
2004
|
streams: {
|
|
@@ -1935,7 +2025,6 @@ const BINDINGS = {
|
|
|
1935
2025
|
/** Request body cap: oversize payloads get a clear 413 instead of an OOM. */
|
|
1936
2026
|
const MAX_BODY_BYTES = 33554432;
|
|
1937
2027
|
const MAX_STREAM_WRITE_BODY_BYTES = 8391680;
|
|
1938
|
-
const QUEUE_ORPHAN_GRACE_MS = 432e6;
|
|
1939
2028
|
function errorResponse(status, name, message) {
|
|
1940
2029
|
return Response.json({ error: {
|
|
1941
2030
|
name,
|
|
@@ -2088,34 +2177,82 @@ function createRouter(env) {
|
|
|
2088
2177
|
} catch (error) {
|
|
2089
2178
|
return errorResponse(400, "BadRequest", error.message);
|
|
2090
2179
|
}
|
|
2180
|
+
let enqueueClaim;
|
|
2091
2181
|
try {
|
|
2092
2182
|
const namespace = env.WORKFLOW_DB;
|
|
2093
2183
|
const payloadStore = env.WORKFLOW_QUEUE_PAYLOADS;
|
|
2094
|
-
const
|
|
2095
|
-
const
|
|
2184
|
+
const inlineRunBody = envelope.runId !== void 0 && isInlineRunBody(envelope.body);
|
|
2185
|
+
const payloadKey = envelope.runId && !inlineRunBody ? queuePayloadObjectKey(envelope.runId, envelope.messageId) : void 0;
|
|
2186
|
+
let brokerEnvelope = payloadKey ? {
|
|
2096
2187
|
...envelope,
|
|
2097
2188
|
payloadKey,
|
|
2098
2189
|
body: void 0
|
|
2099
2190
|
} : envelope;
|
|
2100
|
-
const
|
|
2101
|
-
|
|
2191
|
+
const sized = JSON.stringify({
|
|
2192
|
+
...brokerEnvelope,
|
|
2193
|
+
claimScope: "run"
|
|
2194
|
+
});
|
|
2195
|
+
if (new TextEncoder().encode(sized).byteLength > 128e3) return errorResponse(413, "PayloadTooLarge", `native queue envelope exceeds ${NATIVE_QUEUE_MAX_MESSAGE_BYTES} bytes`);
|
|
2102
2196
|
const now = Date.now();
|
|
2103
|
-
const reservationExpiresAt = Math.max(now, envelope.notBefore ?? now) +
|
|
2104
|
-
|
|
2197
|
+
const reservationExpiresAt = Math.max(now, envelope.notBefore ?? now) + QUEUE_RESERVATION_GRACE_MS;
|
|
2198
|
+
const run = envelope.runId ? namespace.get(namespace.idFromName(envelope.runId)) : void 0;
|
|
2199
|
+
let admission;
|
|
2105
2200
|
if (envelope.idempotencyKey) {
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2201
|
+
const claimName = queueClaimName(envelope.queueName, envelope.idempotencyKey);
|
|
2202
|
+
let scope = run && envelope.runId ? runClaimScopes.get(envelope.runId) : "cell";
|
|
2203
|
+
let reservation;
|
|
2204
|
+
if (run && envelope.runId && scope !== "cell") {
|
|
2205
|
+
enqueueClaim = runQueueClaim(run, claimName);
|
|
2206
|
+
const reserved = await run.reserveRunQueueMessage({
|
|
2207
|
+
claimName,
|
|
2208
|
+
messageId: envelope.messageId,
|
|
2209
|
+
expiresAt: now + QUEUE_PUBLICATION_LEASE_MS,
|
|
2210
|
+
publicationPending: true
|
|
2211
|
+
});
|
|
2212
|
+
if (!reserved.ok) return errorResponse(410, "RunExpiredError", reserved.message);
|
|
2213
|
+
scope = reserved.scope;
|
|
2214
|
+
rememberRunClaimScope(envelope.runId, reserved.scope);
|
|
2215
|
+
if (reserved.scope === "run") {
|
|
2216
|
+
reservation = reserved;
|
|
2217
|
+
brokerEnvelope = {
|
|
2218
|
+
...brokerEnvelope,
|
|
2219
|
+
claimScope: "run"
|
|
2220
|
+
};
|
|
2221
|
+
}
|
|
2222
|
+
} else if (inlineRunBody && run) {
|
|
2223
|
+
admission = run.getQueueAdmission();
|
|
2224
|
+
admission.catch(() => void 0);
|
|
2225
|
+
}
|
|
2226
|
+
if (!reservation) {
|
|
2227
|
+
const cell = namespace.get(namespace.idFromName(claimName));
|
|
2228
|
+
enqueueClaim = cellQueueClaim(cell);
|
|
2229
|
+
reservation = await cell.reserveQueueMessage({
|
|
2230
|
+
messageId: envelope.messageId,
|
|
2231
|
+
expiresAt: now + QUEUE_PUBLICATION_LEASE_MS,
|
|
2232
|
+
publicationPending: true
|
|
2233
|
+
});
|
|
2234
|
+
}
|
|
2235
|
+
if (!reservation.admitted) {
|
|
2236
|
+
if (reservation.publicationPending) return errorResponse(503, "QueuePublicationPending", "the reserved Queue message has not been confirmed by the broker; retry publication");
|
|
2237
|
+
return new Response(rpcStringify({ messageId: reservation.messageId }), {
|
|
2238
|
+
status: 200,
|
|
2239
|
+
headers: { "content-type": "application/json" }
|
|
2240
|
+
});
|
|
2241
|
+
}
|
|
2242
|
+
} else if (inlineRunBody && run) admission = run.getQueueAdmission();
|
|
2243
|
+
const encoded = JSON.stringify(brokerEnvelope);
|
|
2244
|
+
if (admission) {
|
|
2245
|
+
const admitted = await admission;
|
|
2246
|
+
if (!admitted.ok) {
|
|
2247
|
+
await enqueueClaim?.complete(envelope.messageId);
|
|
2248
|
+
return errorResponse(410, "RunExpiredError", admitted.message);
|
|
2249
|
+
}
|
|
2115
2250
|
}
|
|
2116
|
-
if (envelope.runId && payloadKey) {
|
|
2117
|
-
if (!payloadStore)
|
|
2118
|
-
|
|
2251
|
+
if (run && envelope.runId && payloadKey) {
|
|
2252
|
+
if (!payloadStore) {
|
|
2253
|
+
await enqueueClaim?.abandonReservation(envelope.messageId);
|
|
2254
|
+
return errorResponse(500, "WorldMisconfigured", "missing binding: WORKFLOW_QUEUE_PAYLOADS");
|
|
2255
|
+
}
|
|
2119
2256
|
const orphanExpiresAt = reservationExpiresAt;
|
|
2120
2257
|
const registered = await run.registerQueuePayload({
|
|
2121
2258
|
messageId: envelope.messageId,
|
|
@@ -2123,7 +2260,7 @@ function createRouter(env) {
|
|
|
2123
2260
|
orphanExpiresAt
|
|
2124
2261
|
});
|
|
2125
2262
|
if (!registered.ok) {
|
|
2126
|
-
await
|
|
2263
|
+
await enqueueClaim?.complete(envelope.messageId);
|
|
2127
2264
|
return errorResponse(410, "RunExpiredError", registered.message);
|
|
2128
2265
|
}
|
|
2129
2266
|
const orphan = namespace.get(namespace.idFromName(queueOrphanName(envelope.messageId)));
|
|
@@ -2136,30 +2273,18 @@ function createRouter(env) {
|
|
|
2136
2273
|
});
|
|
2137
2274
|
} catch (error) {
|
|
2138
2275
|
await run.unregisterQueuePayload(envelope.messageId);
|
|
2139
|
-
await claim?.completeQueueMessage(envelope.messageId);
|
|
2140
|
-
throw error;
|
|
2141
|
-
}
|
|
2142
|
-
try {
|
|
2143
|
-
await payloadStore.write(payloadKey, envelope.body, {
|
|
2144
|
-
runId: envelope.runId,
|
|
2145
|
-
messageId: envelope.messageId
|
|
2146
|
-
});
|
|
2147
|
-
} catch (error) {
|
|
2148
|
-
await claim?.completeQueueMessage(envelope.messageId);
|
|
2149
|
-
throw error;
|
|
2150
|
-
}
|
|
2151
|
-
let finalized;
|
|
2152
|
-
try {
|
|
2153
|
-
finalized = await run.finalizeQueuePayload(envelope.messageId);
|
|
2154
|
-
} catch (error) {
|
|
2155
|
-
await claim?.completeQueueMessage(envelope.messageId);
|
|
2156
2276
|
throw error;
|
|
2157
2277
|
}
|
|
2278
|
+
await payloadStore.write(payloadKey, envelope.body, {
|
|
2279
|
+
runId: envelope.runId,
|
|
2280
|
+
messageId: envelope.messageId
|
|
2281
|
+
});
|
|
2282
|
+
const finalized = await run.finalizeQueuePayload(envelope.messageId);
|
|
2158
2283
|
if (!finalized.ok) {
|
|
2159
2284
|
await payloadStore.delete(payloadKey);
|
|
2160
2285
|
await run.unregisterQueuePayload(envelope.messageId);
|
|
2161
2286
|
await orphan.cancelQueuePayloadOrphan(envelope.messageId);
|
|
2162
|
-
await
|
|
2287
|
+
await enqueueClaim?.complete(envelope.messageId);
|
|
2163
2288
|
return errorResponse(410, "RunExpiredError", finalized.message);
|
|
2164
2289
|
}
|
|
2165
2290
|
}
|
|
@@ -2167,11 +2292,17 @@ function createRouter(env) {
|
|
|
2167
2292
|
contentType: "text",
|
|
2168
2293
|
...options
|
|
2169
2294
|
});
|
|
2295
|
+
await enqueueClaim?.confirmPublication(envelope.messageId, reservationExpiresAt);
|
|
2170
2296
|
return new Response(rpcStringify({ messageId: envelope.messageId }), {
|
|
2171
2297
|
status: 200,
|
|
2172
2298
|
headers: { "content-type": "application/json" }
|
|
2173
2299
|
});
|
|
2174
2300
|
} catch (error) {
|
|
2301
|
+
try {
|
|
2302
|
+
await enqueueClaim?.abandonReservation(envelope.messageId);
|
|
2303
|
+
} catch (cleanupError) {
|
|
2304
|
+
console.error("world-celld could not abandon a failed Queue reservation", cleanupError);
|
|
2305
|
+
}
|
|
2175
2306
|
const err = error;
|
|
2176
2307
|
return errorResponse(500, err.name ?? "Error", err.message ?? String(error));
|
|
2177
2308
|
}
|
|
@@ -2542,6 +2673,16 @@ async function runIsActive(env, runId) {
|
|
|
2542
2673
|
if (!namespace) throw new Error("world-celld hook index missing WORKFLOW_DB binding");
|
|
2543
2674
|
return await namespace.get(namespace.idFromName(runId)).getLifecycleStatus() === "active";
|
|
2544
2675
|
}
|
|
2676
|
+
/** Resolve a stale shard reservation in its authoritative event writer. */
|
|
2677
|
+
async function resolveExpiredHookClaim(env, owner, claimId, token) {
|
|
2678
|
+
const namespace = env.WORKFLOW_DB;
|
|
2679
|
+
if (!namespace) throw new Error("world-celld hook index missing WORKFLOW_DB binding");
|
|
2680
|
+
return await namespace.get(namespace.idFromName(owner.runId)).resolveExpiredHookClaim({
|
|
2681
|
+
hookId: owner.hookId,
|
|
2682
|
+
claimId,
|
|
2683
|
+
token
|
|
2684
|
+
});
|
|
2685
|
+
}
|
|
2545
2686
|
//#endregion
|
|
2546
2687
|
//#region src/worker/durable-objects/HookIdDO.ts
|
|
2547
2688
|
const MAX_RELEASE_BATCH$1 = 64;
|
|
@@ -2570,52 +2711,69 @@ var HookIdDO = class extends DurableObject {
|
|
|
2570
2711
|
return await runIsActive(this.env, owner.runId) ? raw : null;
|
|
2571
2712
|
}
|
|
2572
2713
|
async reserve(hookId, owner, proposedClaimId) {
|
|
2573
|
-
|
|
2574
|
-
const
|
|
2575
|
-
const
|
|
2576
|
-
const
|
|
2577
|
-
const
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
existing =
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
}
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2714
|
+
for (;;) {
|
|
2715
|
+
const observed = await this.ctx.storage.get(claimKey$1(hookId));
|
|
2716
|
+
const expired = observed && observed.expiresAt <= this.now() ? observed : void 0;
|
|
2717
|
+
const committed = expired ? await resolveExpiredHookClaim(this.env, expired.owner, expired.claimId) : void 0;
|
|
2718
|
+
const result = await this.ctx.storage.transaction(async (txn) => {
|
|
2719
|
+
const record = recordKey$1(hookId);
|
|
2720
|
+
const claim = claimKey$1(hookId);
|
|
2721
|
+
const values = await txn.get([record, claim]);
|
|
2722
|
+
const indexed = values.get(record);
|
|
2723
|
+
if (indexed !== void 0) return {
|
|
2724
|
+
claimed: false,
|
|
2725
|
+
holder: ownerFromSerializedHook(indexed)
|
|
2726
|
+
};
|
|
2727
|
+
let existing = values.get(claim);
|
|
2728
|
+
const now = this.now();
|
|
2729
|
+
if (existing && existing.expiresAt <= now) {
|
|
2730
|
+
if (existing.claimId !== expired?.claimId || existing.expiresAt !== expired.expiresAt) return null;
|
|
2731
|
+
if (committed) {
|
|
2732
|
+
await this.renewClaim(txn, hookId, existing, now);
|
|
2733
|
+
return sameOwner(existing.owner, owner) ? {
|
|
2734
|
+
claimed: true,
|
|
2735
|
+
claimId: existing.claimId
|
|
2736
|
+
} : {
|
|
2737
|
+
claimed: false,
|
|
2738
|
+
holder: existing.owner
|
|
2739
|
+
};
|
|
2740
|
+
}
|
|
2741
|
+
await txn.delete([claim, existing.deadlineKey]);
|
|
2742
|
+
existing = void 0;
|
|
2743
|
+
}
|
|
2744
|
+
if (existing !== void 0 && !sameOwner(existing.owner, owner)) return {
|
|
2745
|
+
claimed: false,
|
|
2746
|
+
holder: existing.owner
|
|
2747
|
+
};
|
|
2748
|
+
if (existing === void 0 || existing.claimId !== proposedClaimId) {
|
|
2749
|
+
if (existing) await txn.delete(existing.deadlineKey);
|
|
2750
|
+
const expiresAt = now + HOOK_CLAIM_LEASE_MS;
|
|
2751
|
+
const deadlineKey = claimDeadlineKey$1(hookId, expiresAt);
|
|
2752
|
+
await txn.put(claim, {
|
|
2753
|
+
owner,
|
|
2754
|
+
claimId: proposedClaimId,
|
|
2755
|
+
reservedAt: now,
|
|
2756
|
+
expiresAt,
|
|
2757
|
+
deadlineKey
|
|
2758
|
+
});
|
|
2759
|
+
await txn.put(deadlineKey, {
|
|
2760
|
+
hookId,
|
|
2761
|
+
claimId: proposedClaimId,
|
|
2762
|
+
expiresAt
|
|
2763
|
+
});
|
|
2764
|
+
await this.armAtMost(txn, expiresAt);
|
|
2765
|
+
return {
|
|
2766
|
+
claimed: true,
|
|
2767
|
+
claimId: proposedClaimId
|
|
2768
|
+
};
|
|
2769
|
+
}
|
|
2609
2770
|
return {
|
|
2610
2771
|
claimed: true,
|
|
2611
|
-
claimId:
|
|
2772
|
+
claimId: existing.claimId
|
|
2612
2773
|
};
|
|
2613
|
-
}
|
|
2614
|
-
return
|
|
2615
|
-
|
|
2616
|
-
claimId: existing.claimId
|
|
2617
|
-
};
|
|
2618
|
-
});
|
|
2774
|
+
});
|
|
2775
|
+
if (result) return result;
|
|
2776
|
+
}
|
|
2619
2777
|
}
|
|
2620
2778
|
async publish(hookId, serializedHook, owner, claimId) {
|
|
2621
2779
|
if (!await runIsActive(this.env, owner.runId)) {
|
|
@@ -2632,10 +2790,7 @@ var HookIdDO = class extends DurableObject {
|
|
|
2632
2790
|
const indexed = values.get(record);
|
|
2633
2791
|
if (indexed !== void 0 && !sameOwner(ownerFromSerializedHook(indexed), owner)) throw new Error(`Hook id ${hookId} is owned by another hook`);
|
|
2634
2792
|
const existingClaim = values.get(claim);
|
|
2635
|
-
if (existingClaim && existingClaim.expiresAt <= this.now()) {
|
|
2636
|
-
await txn.delete([claim, existingClaim.deadlineKey]);
|
|
2637
|
-
return { stored: false };
|
|
2638
|
-
}
|
|
2793
|
+
if (existingClaim && existingClaim.expiresAt <= this.now()) throw new Error(`Hook id ${hookId} reservation expired`);
|
|
2639
2794
|
if (existingClaim !== void 0 && (!sameOwner(existingClaim.owner, owner) || existingClaim.claimId !== claimId)) throw new Error(`Hook id ${hookId} is reserved by another hook`);
|
|
2640
2795
|
if (indexed === void 0 && existingClaim === void 0) throw new Error(`Hook id ${hookId} has no active reservation`);
|
|
2641
2796
|
await txn.put(record, serializedHook);
|
|
@@ -2685,21 +2840,51 @@ var HookIdDO = class extends DurableObject {
|
|
|
2685
2840
|
const current = await storage.getAlarm();
|
|
2686
2841
|
if (current === null || at < current) await storage.setAlarm(at);
|
|
2687
2842
|
}
|
|
2843
|
+
async renewClaim(txn, hookId, claim, now) {
|
|
2844
|
+
const expiresAt = now + HOOK_CLAIM_LEASE_MS;
|
|
2845
|
+
const deadlineKey = claimDeadlineKey$1(hookId, expiresAt);
|
|
2846
|
+
await txn.delete(claim.deadlineKey);
|
|
2847
|
+
await txn.put(claimKey$1(hookId), {
|
|
2848
|
+
...claim,
|
|
2849
|
+
expiresAt,
|
|
2850
|
+
deadlineKey
|
|
2851
|
+
});
|
|
2852
|
+
await txn.put(deadlineKey, {
|
|
2853
|
+
hookId,
|
|
2854
|
+
claimId: claim.claimId,
|
|
2855
|
+
expiresAt
|
|
2856
|
+
});
|
|
2857
|
+
await this.armAtMost(txn, expiresAt);
|
|
2858
|
+
}
|
|
2688
2859
|
async compactClaims() {
|
|
2689
2860
|
const now = this.now();
|
|
2861
|
+
const due = await this.ctx.storage.list({
|
|
2862
|
+
prefix: CLAIM_DEADLINE_PREFIX$1,
|
|
2863
|
+
end: `${CLAIM_DEADLINE_PREFIX$1}${pad$2(now + 1)}`,
|
|
2864
|
+
limit: 129
|
|
2865
|
+
});
|
|
2866
|
+
const page = Array.from(due).slice(0, 128);
|
|
2867
|
+
const observed = await this.ctx.storage.get(page.map(([, item]) => claimKey$1(item.hookId)));
|
|
2868
|
+
const committed = /* @__PURE__ */ new Map();
|
|
2869
|
+
await Promise.all(page.map(async ([, item]) => {
|
|
2870
|
+
const claim = observed.get(claimKey$1(item.hookId));
|
|
2871
|
+
if (!claim || claim.claimId !== item.claimId || claim.expiresAt > now) return;
|
|
2872
|
+
const hook = await resolveExpiredHookClaim(this.env, claim.owner, claim.claimId);
|
|
2873
|
+
committed.set(claimKey$1(item.hookId), hook !== null);
|
|
2874
|
+
}));
|
|
2690
2875
|
await this.ctx.storage.transaction(async (txn) => {
|
|
2691
|
-
const due = await txn.list({
|
|
2692
|
-
prefix: CLAIM_DEADLINE_PREFIX$1,
|
|
2693
|
-
end: `${CLAIM_DEADLINE_PREFIX$1}${pad$2(now + 1)}`,
|
|
2694
|
-
limit: 129
|
|
2695
|
-
});
|
|
2696
|
-
const page = Array.from(due).slice(0, 128);
|
|
2697
2876
|
const claims = await txn.get(page.map(([, item]) => claimKey$1(item.hookId)));
|
|
2698
|
-
const deletes =
|
|
2877
|
+
const deletes = [];
|
|
2878
|
+
for (const [deadline, item] of page) {
|
|
2699
2879
|
const key = claimKey$1(item.hookId);
|
|
2700
2880
|
const claim = claims.get(key);
|
|
2701
|
-
|
|
2702
|
-
|
|
2881
|
+
const snapshot = observed.get(key);
|
|
2882
|
+
if (claim?.claimId !== item.claimId || claim.expiresAt > now) deletes.push(deadline);
|
|
2883
|
+
else if (claim.claimId === snapshot?.claimId && claim.expiresAt === snapshot.expiresAt) {
|
|
2884
|
+
if (committed.get(key)) await this.renewClaim(txn, item.hookId, claim, now);
|
|
2885
|
+
else deletes.push(deadline, key);
|
|
2886
|
+
}
|
|
2887
|
+
}
|
|
2703
2888
|
for (let offset = 0; offset < deletes.length; offset += 128) await txn.delete(deletes.slice(offset, offset + 128));
|
|
2704
2889
|
if (due.size > 128) {
|
|
2705
2890
|
await txn.setAlarm(now + 1);
|
|
@@ -2742,51 +2927,68 @@ var HookTokenDO = class extends DurableObject {
|
|
|
2742
2927
|
return await runIsActive(this.env, owner.runId) ? raw : null;
|
|
2743
2928
|
}
|
|
2744
2929
|
async reserve(token, owner, proposedClaimId) {
|
|
2745
|
-
|
|
2746
|
-
const
|
|
2747
|
-
const
|
|
2748
|
-
const
|
|
2749
|
-
const
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
existing =
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
deadlineKey
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
}
|
|
2779
|
-
|
|
2930
|
+
for (;;) {
|
|
2931
|
+
const observed = await this.ctx.storage.get(claimKey(token));
|
|
2932
|
+
const expired = observed && observed.expiresAt <= this.now() ? observed : void 0;
|
|
2933
|
+
const committed = expired ? await resolveExpiredHookClaim(this.env, expired.owner, expired.claimId, token) : void 0;
|
|
2934
|
+
const result = await this.ctx.storage.transaction(async (txn) => {
|
|
2935
|
+
const record = recordKey(token);
|
|
2936
|
+
const claim = claimKey(token);
|
|
2937
|
+
const values = await txn.get([record, claim]);
|
|
2938
|
+
const indexed = values.get(record);
|
|
2939
|
+
if (indexed !== void 0) return {
|
|
2940
|
+
claimed: false,
|
|
2941
|
+
holder: ownerFromSerializedHook(indexed)
|
|
2942
|
+
};
|
|
2943
|
+
let existing = values.get(claim);
|
|
2944
|
+
const now = this.now();
|
|
2945
|
+
if (existing && existing.expiresAt <= now) {
|
|
2946
|
+
if (existing.claimId !== expired?.claimId || existing.expiresAt !== expired.expiresAt) return null;
|
|
2947
|
+
if (committed) {
|
|
2948
|
+
await this.renewClaim(txn, token, existing, now);
|
|
2949
|
+
return sameOwner(existing.owner, owner) ? {
|
|
2950
|
+
claimed: true,
|
|
2951
|
+
claimId: existing.claimId
|
|
2952
|
+
} : {
|
|
2953
|
+
claimed: false,
|
|
2954
|
+
holder: existing.owner
|
|
2955
|
+
};
|
|
2956
|
+
}
|
|
2957
|
+
await txn.delete([claim, existing.deadlineKey]);
|
|
2958
|
+
existing = void 0;
|
|
2959
|
+
}
|
|
2960
|
+
if (existing !== void 0 && !sameOwner(existing.owner, owner)) return {
|
|
2961
|
+
claimed: false,
|
|
2962
|
+
holder: existing.owner
|
|
2963
|
+
};
|
|
2964
|
+
if (existing === void 0) {
|
|
2965
|
+
const expiresAt = now + HOOK_CLAIM_LEASE_MS;
|
|
2966
|
+
const deadlineKey = claimDeadlineKey(token, expiresAt);
|
|
2967
|
+
await txn.put(claim, {
|
|
2968
|
+
owner,
|
|
2969
|
+
claimId: proposedClaimId,
|
|
2970
|
+
reservedAt: now,
|
|
2971
|
+
expiresAt,
|
|
2972
|
+
deadlineKey
|
|
2973
|
+
});
|
|
2974
|
+
await txn.put(deadlineKey, {
|
|
2975
|
+
token,
|
|
2976
|
+
claimId: proposedClaimId,
|
|
2977
|
+
expiresAt
|
|
2978
|
+
});
|
|
2979
|
+
await this.armAtMost(txn, expiresAt);
|
|
2980
|
+
return {
|
|
2981
|
+
claimed: true,
|
|
2982
|
+
claimId: proposedClaimId
|
|
2983
|
+
};
|
|
2984
|
+
}
|
|
2780
2985
|
return {
|
|
2781
2986
|
claimed: true,
|
|
2782
|
-
claimId:
|
|
2987
|
+
claimId: existing.claimId
|
|
2783
2988
|
};
|
|
2784
|
-
}
|
|
2785
|
-
return
|
|
2786
|
-
|
|
2787
|
-
claimId: existing.claimId
|
|
2788
|
-
};
|
|
2789
|
-
});
|
|
2989
|
+
});
|
|
2990
|
+
if (result) return result;
|
|
2991
|
+
}
|
|
2790
2992
|
}
|
|
2791
2993
|
async finalize(token, serializedHook, owner, claimId) {
|
|
2792
2994
|
if (!await runIsActive(this.env, owner.runId)) {
|
|
@@ -2803,10 +3005,7 @@ var HookTokenDO = class extends DurableObject {
|
|
|
2803
3005
|
const indexed = values.get(record);
|
|
2804
3006
|
if (indexed !== void 0 && !sameOwner(ownerFromSerializedHook(indexed), owner)) throw new Error(`Hook token ${token} is owned by another hook`);
|
|
2805
3007
|
const existingClaim = values.get(claim);
|
|
2806
|
-
if (existingClaim && existingClaim.expiresAt <= this.now()) {
|
|
2807
|
-
await txn.delete([claim, existingClaim.deadlineKey]);
|
|
2808
|
-
return { stored: false };
|
|
2809
|
-
}
|
|
3008
|
+
if (existingClaim && existingClaim.expiresAt <= this.now()) throw new Error(`Hook token ${token} reservation expired`);
|
|
2810
3009
|
if (existingClaim !== void 0 && (!sameOwner(existingClaim.owner, owner) || existingClaim.claimId !== claimId)) throw new Error(`Hook token ${token} is reserved by another hook`);
|
|
2811
3010
|
if (indexed === void 0 && existingClaim === void 0) throw new Error(`Hook token ${token} has no active reservation`);
|
|
2812
3011
|
await txn.put(record, serializedHook);
|
|
@@ -2856,21 +3055,51 @@ var HookTokenDO = class extends DurableObject {
|
|
|
2856
3055
|
const current = await storage.getAlarm();
|
|
2857
3056
|
if (current === null || at < current) await storage.setAlarm(at);
|
|
2858
3057
|
}
|
|
3058
|
+
async renewClaim(txn, token, claim, now) {
|
|
3059
|
+
const expiresAt = now + HOOK_CLAIM_LEASE_MS;
|
|
3060
|
+
const deadlineKey = claimDeadlineKey(token, expiresAt);
|
|
3061
|
+
await txn.delete(claim.deadlineKey);
|
|
3062
|
+
await txn.put(claimKey(token), {
|
|
3063
|
+
...claim,
|
|
3064
|
+
expiresAt,
|
|
3065
|
+
deadlineKey
|
|
3066
|
+
});
|
|
3067
|
+
await txn.put(deadlineKey, {
|
|
3068
|
+
token,
|
|
3069
|
+
claimId: claim.claimId,
|
|
3070
|
+
expiresAt
|
|
3071
|
+
});
|
|
3072
|
+
await this.armAtMost(txn, expiresAt);
|
|
3073
|
+
}
|
|
2859
3074
|
async compactClaims() {
|
|
2860
3075
|
const now = this.now();
|
|
3076
|
+
const due = await this.ctx.storage.list({
|
|
3077
|
+
prefix: CLAIM_DEADLINE_PREFIX,
|
|
3078
|
+
end: `${CLAIM_DEADLINE_PREFIX}${pad$1(now + 1)}`,
|
|
3079
|
+
limit: 129
|
|
3080
|
+
});
|
|
3081
|
+
const page = Array.from(due).slice(0, 128);
|
|
3082
|
+
const observed = await this.ctx.storage.get(page.map(([, item]) => claimKey(item.token)));
|
|
3083
|
+
const committed = /* @__PURE__ */ new Map();
|
|
3084
|
+
await Promise.all(page.map(async ([, item]) => {
|
|
3085
|
+
const claim = observed.get(claimKey(item.token));
|
|
3086
|
+
if (!claim || claim.claimId !== item.claimId || claim.expiresAt > now) return;
|
|
3087
|
+
const hook = await resolveExpiredHookClaim(this.env, claim.owner, claim.claimId, item.token);
|
|
3088
|
+
committed.set(claimKey(item.token), hook !== null);
|
|
3089
|
+
}));
|
|
2861
3090
|
await this.ctx.storage.transaction(async (txn) => {
|
|
2862
|
-
const due = await txn.list({
|
|
2863
|
-
prefix: CLAIM_DEADLINE_PREFIX,
|
|
2864
|
-
end: `${CLAIM_DEADLINE_PREFIX}${pad$1(now + 1)}`,
|
|
2865
|
-
limit: 129
|
|
2866
|
-
});
|
|
2867
|
-
const page = Array.from(due).slice(0, 128);
|
|
2868
3091
|
const claims = await txn.get(page.map(([, item]) => claimKey(item.token)));
|
|
2869
|
-
const deletes =
|
|
3092
|
+
const deletes = [];
|
|
3093
|
+
for (const [deadline, item] of page) {
|
|
2870
3094
|
const key = claimKey(item.token);
|
|
2871
3095
|
const claim = claims.get(key);
|
|
2872
|
-
|
|
2873
|
-
|
|
3096
|
+
const snapshot = observed.get(key);
|
|
3097
|
+
if (claim?.claimId !== item.claimId || claim.expiresAt > now) deletes.push(deadline);
|
|
3098
|
+
else if (claim.claimId === snapshot?.claimId && claim.expiresAt === snapshot.expiresAt) {
|
|
3099
|
+
if (committed.get(key)) await this.renewClaim(txn, item.token, claim, now);
|
|
3100
|
+
else deletes.push(deadline, key);
|
|
3101
|
+
}
|
|
3102
|
+
}
|
|
2874
3103
|
for (let offset = 0; offset < deletes.length; offset += 128) await txn.delete(deletes.slice(offset, offset + 128));
|
|
2875
3104
|
if (due.size > 128) {
|
|
2876
3105
|
await txn.setAlarm(now + 1);
|
|
@@ -3049,6 +3278,13 @@ const DELETE_BATCH_KEYS = 128;
|
|
|
3049
3278
|
const DEFAULT_EXPIRE_BYTE_LIMIT = 16777216;
|
|
3050
3279
|
const MAX_EXPIRE_BYTE_LIMIT = DEFAULT_EXPIRE_BYTE_LIMIT;
|
|
3051
3280
|
const MAX_STREAM_INDEX = 2147483647;
|
|
3281
|
+
const SEGMENT_LAYOUT = 2;
|
|
3282
|
+
const SEGMENT_KEY_PREFIX = "seg:";
|
|
3283
|
+
const SEGMENT_SIZE_KEY_PREFIX = "segsize:";
|
|
3284
|
+
/** Payload bytes per segment row, well below the 2 MiB per-value storage limit. */
|
|
3285
|
+
const MAX_SEGMENT_BYTES = 1048576;
|
|
3286
|
+
/** Layout of a stream's first write. Existing streams keep the layout they started with. */
|
|
3287
|
+
const NEW_STREAM_LAYOUT = SEGMENT_LAYOUT;
|
|
3052
3288
|
/** One storage get or put call accepts at most 128 keys. */
|
|
3053
3289
|
const STORAGE_BATCH_KEYS = 128;
|
|
3054
3290
|
function boundedLimit(value, fallback, maximum) {
|
|
@@ -3069,7 +3305,7 @@ function validateMeta(meta) {
|
|
|
3069
3305
|
"closed",
|
|
3070
3306
|
"errored",
|
|
3071
3307
|
"expired"
|
|
3072
|
-
].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");
|
|
3308
|
+
].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");
|
|
3073
3309
|
const error = candidate.error;
|
|
3074
3310
|
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");
|
|
3075
3311
|
return candidate;
|
|
@@ -3081,6 +3317,80 @@ function chunkKey(index) {
|
|
|
3081
3317
|
function chunkSizeKey(index) {
|
|
3082
3318
|
return `${CHUNK_SIZE_KEY_PREFIX}${index.toString().padStart(12, "0")}`;
|
|
3083
3319
|
}
|
|
3320
|
+
function segmentKey(start) {
|
|
3321
|
+
return `${SEGMENT_KEY_PREFIX}${start.toString().padStart(12, "0")}`;
|
|
3322
|
+
}
|
|
3323
|
+
function segmentSizeKey(start) {
|
|
3324
|
+
return `${SEGMENT_SIZE_KEY_PREFIX}${start.toString().padStart(12, "0")}`;
|
|
3325
|
+
}
|
|
3326
|
+
function startFromSegmentKey(key, prefix) {
|
|
3327
|
+
const suffix = key.slice(prefix.length);
|
|
3328
|
+
const start = Number(suffix);
|
|
3329
|
+
if (!key.startsWith(prefix) || !/^\d{12}$/.test(suffix) || !Number.isSafeInteger(start) || start > MAX_STREAM_INDEX) throw new Error(`Invalid persisted stream segment key "${key}"`);
|
|
3330
|
+
return start;
|
|
3331
|
+
}
|
|
3332
|
+
function validateSegmentSize(value, start) {
|
|
3333
|
+
const size = value;
|
|
3334
|
+
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}`);
|
|
3335
|
+
return size;
|
|
3336
|
+
}
|
|
3337
|
+
/** Pack chunks as `[u32 count][u32 length]*count` followed by the payloads. */
|
|
3338
|
+
function encodeSegment(chunks) {
|
|
3339
|
+
const payloadBytes = chunks.reduce((total, chunk) => total + chunk.byteLength, 0);
|
|
3340
|
+
const header = 4 + 4 * chunks.length;
|
|
3341
|
+
const encoded = new Uint8Array(header + payloadBytes);
|
|
3342
|
+
const view = new DataView(encoded.buffer);
|
|
3343
|
+
view.setUint32(0, chunks.length);
|
|
3344
|
+
let offset = header;
|
|
3345
|
+
for (let index = 0; index < chunks.length; index++) {
|
|
3346
|
+
view.setUint32(4 + 4 * index, chunks[index].byteLength);
|
|
3347
|
+
encoded.set(chunks[index], offset);
|
|
3348
|
+
offset += chunks[index].byteLength;
|
|
3349
|
+
}
|
|
3350
|
+
return encoded;
|
|
3351
|
+
}
|
|
3352
|
+
function decodeSegment(value, start, size) {
|
|
3353
|
+
if (!(value instanceof Uint8Array) || value.byteLength < 4) throw new Error(`Invalid persisted stream segment at index ${start}`);
|
|
3354
|
+
const view = new DataView(value.buffer, value.byteOffset, value.byteLength);
|
|
3355
|
+
const count = view.getUint32(0);
|
|
3356
|
+
const header = 4 + 4 * count;
|
|
3357
|
+
if (count !== size.count || header + size.bytes !== value.byteLength) throw new Error(`Invalid persisted stream segment at index ${start}`);
|
|
3358
|
+
const chunks = [];
|
|
3359
|
+
let offset = header;
|
|
3360
|
+
for (let index = 0; index < count; index++) {
|
|
3361
|
+
const length = view.getUint32(4 + 4 * index);
|
|
3362
|
+
if (offset + length > value.byteLength) throw new Error(`Invalid persisted stream segment at index ${start}`);
|
|
3363
|
+
chunks.push(value.subarray(offset, offset + length));
|
|
3364
|
+
offset += length;
|
|
3365
|
+
}
|
|
3366
|
+
if (offset !== value.byteLength) throw new Error(`Invalid persisted stream segment at index ${start}`);
|
|
3367
|
+
return chunks;
|
|
3368
|
+
}
|
|
3369
|
+
/** Group one write's chunks into segments of at most MAX_SEGMENT_BYTES payload bytes. */
|
|
3370
|
+
function packSegments(startIndex, chunks) {
|
|
3371
|
+
const segments = [];
|
|
3372
|
+
let current = [];
|
|
3373
|
+
let bytes = 0;
|
|
3374
|
+
let start = startIndex;
|
|
3375
|
+
for (const chunk of chunks) {
|
|
3376
|
+
if (current.length > 0 && bytes + chunk.byteLength > MAX_SEGMENT_BYTES) {
|
|
3377
|
+
segments.push({
|
|
3378
|
+
start,
|
|
3379
|
+
chunks: current
|
|
3380
|
+
});
|
|
3381
|
+
start += current.length;
|
|
3382
|
+
current = [];
|
|
3383
|
+
bytes = 0;
|
|
3384
|
+
}
|
|
3385
|
+
current.push(chunk);
|
|
3386
|
+
bytes += chunk.byteLength;
|
|
3387
|
+
}
|
|
3388
|
+
if (current.length > 0) segments.push({
|
|
3389
|
+
start,
|
|
3390
|
+
chunks: current
|
|
3391
|
+
});
|
|
3392
|
+
return segments;
|
|
3393
|
+
}
|
|
3084
3394
|
function indexFromChunkSizeKey(key) {
|
|
3085
3395
|
const suffix = key.slice(11);
|
|
3086
3396
|
const index = Number(suffix);
|
|
@@ -3097,6 +3407,9 @@ function validateChunkSize(size, index) {
|
|
|
3097
3407
|
if (typeof size !== "number" || !Number.isSafeInteger(size) || size < 0 || size > 1048576) throw new Error(`Invalid persisted stream chunk size at index ${index}`);
|
|
3098
3408
|
return size;
|
|
3099
3409
|
}
|
|
3410
|
+
async function deleteInBatches(txn, keys) {
|
|
3411
|
+
for (let offset = 0; offset < keys.length; offset += DELETE_BATCH_KEYS) await txn.delete(keys.slice(offset, offset + DELETE_BATCH_KEYS));
|
|
3412
|
+
}
|
|
3100
3413
|
function compactChunk(chunk) {
|
|
3101
3414
|
return chunk.byteOffset === 0 && chunk.byteLength === chunk.buffer.byteLength ? chunk : new Uint8Array(chunk);
|
|
3102
3415
|
}
|
|
@@ -3193,29 +3506,8 @@ var StreamDO = class extends DurableObject {
|
|
|
3193
3506
|
const meta = await this.getMeta();
|
|
3194
3507
|
this.assertOwner(meta, request.runId);
|
|
3195
3508
|
const available = meta.state === "expired" ? 0 : Math.max(0, Math.min(request.maxChunks, meta.count - request.startIndex));
|
|
3196
|
-
|
|
3197
|
-
if (available > 0)
|
|
3198
|
-
const indexes = Array.from({ length: available }, (_, offset) => request.startIndex + offset);
|
|
3199
|
-
const sizeKeys = indexes.map(chunkSizeKey);
|
|
3200
|
-
const storedSizes = await this.getMany(sizeKeys);
|
|
3201
|
-
const selectedIndexes = [];
|
|
3202
|
-
const selectedSizes = [];
|
|
3203
|
-
let bytes = 0;
|
|
3204
|
-
for (const index of indexes) {
|
|
3205
|
-
const size = validateChunkSize(storedSizes.get(chunkSizeKey(index)), index);
|
|
3206
|
-
if (bytes + size > request.maxBytes) break;
|
|
3207
|
-
selectedIndexes.push(index);
|
|
3208
|
-
selectedSizes.push(size);
|
|
3209
|
-
bytes += size;
|
|
3210
|
-
}
|
|
3211
|
-
const payloadKeys = selectedIndexes.map(chunkKey);
|
|
3212
|
-
const storedChunks = await this.getMany(payloadKeys);
|
|
3213
|
-
for (let offset = 0; offset < payloadKeys.length; offset++) {
|
|
3214
|
-
const chunk = storedChunks.get(payloadKeys[offset]);
|
|
3215
|
-
if (!(chunk instanceof Uint8Array) || chunk.byteLength !== selectedSizes[offset]) throw new Error(`Invalid persisted stream chunk at index ${selectedIndexes[offset]}`);
|
|
3216
|
-
chunks.push(chunk);
|
|
3217
|
-
}
|
|
3218
|
-
}
|
|
3509
|
+
let chunks = [];
|
|
3510
|
+
if (available > 0) chunks = meta.layout === SEGMENT_LAYOUT ? await this.readSegmentRows(request.startIndex, available, request.maxBytes) : await this.readChunkRows(request.startIndex, available, request.maxBytes);
|
|
3219
3511
|
return {
|
|
3220
3512
|
startIndex: request.startIndex,
|
|
3221
3513
|
tailIndex: meta.count - 1,
|
|
@@ -3225,10 +3517,100 @@ var StreamDO = class extends DurableObject {
|
|
|
3225
3517
|
error: meta.error
|
|
3226
3518
|
};
|
|
3227
3519
|
}
|
|
3520
|
+
async readChunkRows(startIndex, available, maxBytes) {
|
|
3521
|
+
const indexes = Array.from({ length: available }, (_, offset) => startIndex + offset);
|
|
3522
|
+
const sizeKeys = indexes.map(chunkSizeKey);
|
|
3523
|
+
const storedSizes = await this.getMany(sizeKeys);
|
|
3524
|
+
const selectedIndexes = [];
|
|
3525
|
+
const selectedSizes = [];
|
|
3526
|
+
let bytes = 0;
|
|
3527
|
+
for (const index of indexes) {
|
|
3528
|
+
const size = validateChunkSize(storedSizes.get(chunkSizeKey(index)), index);
|
|
3529
|
+
if (bytes + size > maxBytes) break;
|
|
3530
|
+
selectedIndexes.push(index);
|
|
3531
|
+
selectedSizes.push(size);
|
|
3532
|
+
bytes += size;
|
|
3533
|
+
}
|
|
3534
|
+
const payloadKeys = selectedIndexes.map(chunkKey);
|
|
3535
|
+
const storedChunks = await this.getMany(payloadKeys);
|
|
3536
|
+
const chunks = [];
|
|
3537
|
+
for (let offset = 0; offset < payloadKeys.length; offset++) {
|
|
3538
|
+
const chunk = storedChunks.get(payloadKeys[offset]);
|
|
3539
|
+
if (!(chunk instanceof Uint8Array) || chunk.byteLength !== selectedSizes[offset]) throw new Error(`Invalid persisted stream chunk at index ${selectedIndexes[offset]}`);
|
|
3540
|
+
chunks.push(chunk);
|
|
3541
|
+
}
|
|
3542
|
+
return chunks;
|
|
3543
|
+
}
|
|
3544
|
+
/**
|
|
3545
|
+
* Plan from the small segment size rows, then load only the segments that
|
|
3546
|
+
* cover the requested range. A read that starts on a segment boundary (the
|
|
3547
|
+
* common sequential case) needs one list and one multi-get.
|
|
3548
|
+
*/
|
|
3549
|
+
async readSegmentRows(startIndex, available, maxBytes) {
|
|
3550
|
+
const endIndex = startIndex + available;
|
|
3551
|
+
const listed = await this.ctx.storage.list({
|
|
3552
|
+
prefix: SEGMENT_SIZE_KEY_PREFIX,
|
|
3553
|
+
start: segmentSizeKey(startIndex),
|
|
3554
|
+
end: segmentSizeKey(endIndex),
|
|
3555
|
+
limit: available
|
|
3556
|
+
});
|
|
3557
|
+
const planned = [];
|
|
3558
|
+
for (const [key, value] of listed) {
|
|
3559
|
+
const start = startFromSegmentKey(key, SEGMENT_SIZE_KEY_PREFIX);
|
|
3560
|
+
planned.push({
|
|
3561
|
+
start,
|
|
3562
|
+
size: validateSegmentSize(value, start)
|
|
3563
|
+
});
|
|
3564
|
+
}
|
|
3565
|
+
if (planned[0]?.start !== startIndex) {
|
|
3566
|
+
const entry = (await this.ctx.storage.list({
|
|
3567
|
+
prefix: SEGMENT_SIZE_KEY_PREFIX,
|
|
3568
|
+
end: segmentSizeKey(startIndex + 1),
|
|
3569
|
+
reverse: true,
|
|
3570
|
+
limit: 1
|
|
3571
|
+
})).entries().next().value;
|
|
3572
|
+
if (!entry) throw new Error(`Missing persisted stream segment at index ${startIndex}`);
|
|
3573
|
+
const start = startFromSegmentKey(entry[0], SEGMENT_SIZE_KEY_PREFIX);
|
|
3574
|
+
planned.unshift({
|
|
3575
|
+
start,
|
|
3576
|
+
size: validateSegmentSize(entry[1], start)
|
|
3577
|
+
});
|
|
3578
|
+
}
|
|
3579
|
+
let covered = startIndex;
|
|
3580
|
+
for (let index = 0; index < planned.length; index++) {
|
|
3581
|
+
const { start, size } = planned[index];
|
|
3582
|
+
if (start > covered) throw new Error(`Missing persisted stream segment at index ${covered}`);
|
|
3583
|
+
if (index > 0 && start !== covered) throw new Error(`Overlapping persisted stream segment at index ${start}`);
|
|
3584
|
+
if (start + size.count <= covered) throw new Error(`Missing persisted stream segment at index ${covered}`);
|
|
3585
|
+
covered = start + size.count;
|
|
3586
|
+
}
|
|
3587
|
+
if (covered < endIndex) throw new Error(`Missing persisted stream segment at index ${covered}`);
|
|
3588
|
+
const selected = [];
|
|
3589
|
+
let plannedBytes = 0;
|
|
3590
|
+
for (const segment of planned) {
|
|
3591
|
+
if (selected.length > 0 && plannedBytes >= maxBytes) break;
|
|
3592
|
+
selected.push(segment);
|
|
3593
|
+
plannedBytes += segment.size.bytes;
|
|
3594
|
+
}
|
|
3595
|
+
const values = await this.getMany(selected.map(({ start }) => segmentKey(start)));
|
|
3596
|
+
const chunks = [];
|
|
3597
|
+
let bytes = 0;
|
|
3598
|
+
let expected = startIndex;
|
|
3599
|
+
for (const { start, size } of selected) {
|
|
3600
|
+
if (start > expected) throw new Error(`Missing persisted stream segment at index ${expected}`);
|
|
3601
|
+
const segment = decodeSegment(values.get(segmentKey(start)), start, size);
|
|
3602
|
+
for (let offset = expected - start; offset < segment.length; offset++) {
|
|
3603
|
+
if (expected >= endIndex || bytes + segment[offset].byteLength > maxBytes) return chunks;
|
|
3604
|
+
chunks.push(segment[offset]);
|
|
3605
|
+
bytes += segment[offset].byteLength;
|
|
3606
|
+
expected++;
|
|
3607
|
+
}
|
|
3608
|
+
}
|
|
3609
|
+
return chunks;
|
|
3610
|
+
}
|
|
3228
3611
|
/** Append one bounded binary batch with contiguous indexes. */
|
|
3229
3612
|
async writeChunks(runId, chunks) {
|
|
3230
3613
|
validateStreamWriteChunks(chunks);
|
|
3231
|
-
const storedChunks = chunks.map(compactChunk);
|
|
3232
3614
|
return await this.runMutation(async () => {
|
|
3233
3615
|
const committed = await this.ctx.storage.transaction(async (txn) => {
|
|
3234
3616
|
const stored = await txn.get(META_KEY);
|
|
@@ -3236,16 +3618,28 @@ var StreamDO = class extends DurableObject {
|
|
|
3236
3618
|
this.assertWritable(meta, runId);
|
|
3237
3619
|
const startIndex = meta.count;
|
|
3238
3620
|
if (startIndex + chunks.length > MAX_STREAM_INDEX) throw new Error("Stream offset limit exceeded");
|
|
3621
|
+
const layout = stored === void 0 ? NEW_STREAM_LAYOUT : meta.layout;
|
|
3239
3622
|
const nextMeta = {
|
|
3240
3623
|
count: startIndex + chunks.length,
|
|
3241
3624
|
state: "open",
|
|
3242
|
-
ownerRunId: runId
|
|
3625
|
+
ownerRunId: runId,
|
|
3626
|
+
...layout === void 0 ? {} : { layout }
|
|
3243
3627
|
};
|
|
3244
3628
|
const entries = [[META_KEY, nextMeta]];
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
entries.push([
|
|
3248
|
-
|
|
3629
|
+
if (layout === SEGMENT_LAYOUT) for (const segment of packSegments(startIndex, chunks)) {
|
|
3630
|
+
entries.push([segmentKey(segment.start), encodeSegment(segment.chunks)]);
|
|
3631
|
+
entries.push([segmentSizeKey(segment.start), {
|
|
3632
|
+
count: segment.chunks.length,
|
|
3633
|
+
bytes: segment.chunks.reduce((total, chunk) => total + chunk.byteLength, 0)
|
|
3634
|
+
}]);
|
|
3635
|
+
}
|
|
3636
|
+
else {
|
|
3637
|
+
const storedChunks = chunks.map(compactChunk);
|
|
3638
|
+
for (let offset = 0; offset < storedChunks.length; offset++) {
|
|
3639
|
+
const index = startIndex + offset;
|
|
3640
|
+
entries.push([chunkKey(index), storedChunks[offset]]);
|
|
3641
|
+
entries.push([chunkSizeKey(index), storedChunks[offset].byteLength]);
|
|
3642
|
+
}
|
|
3249
3643
|
}
|
|
3250
3644
|
for (let offset = 0; offset < entries.length; offset += STORAGE_BATCH_KEYS) await txn.put(Object.fromEntries(entries.slice(offset, offset + STORAGE_BATCH_KEYS)));
|
|
3251
3645
|
return {
|
|
@@ -3381,6 +3775,102 @@ var StreamDO = class extends DurableObject {
|
|
|
3381
3775
|
};
|
|
3382
3776
|
});
|
|
3383
3777
|
}
|
|
3778
|
+
/** Delete one page of per-chunk rows, listing size rows to avoid loading payloads. */
|
|
3779
|
+
async expireChunkPage(txn, limit, byteLimit) {
|
|
3780
|
+
const candidates = await txn.list({
|
|
3781
|
+
prefix: CHUNK_SIZE_KEY_PREFIX,
|
|
3782
|
+
limit: limit + 1
|
|
3783
|
+
});
|
|
3784
|
+
const page = [];
|
|
3785
|
+
let bytes = 0;
|
|
3786
|
+
for (const [key, storedSize] of Array.from(candidates).slice(0, limit)) {
|
|
3787
|
+
const index = indexFromChunkSizeKey(key);
|
|
3788
|
+
const size = validateChunkSize(storedSize, index);
|
|
3789
|
+
if (page.length > 0 && bytes + size > byteLimit) break;
|
|
3790
|
+
page.push({
|
|
3791
|
+
index,
|
|
3792
|
+
size
|
|
3793
|
+
});
|
|
3794
|
+
bytes += size;
|
|
3795
|
+
}
|
|
3796
|
+
let payloadFallback = false;
|
|
3797
|
+
if (candidates.size === 0) {
|
|
3798
|
+
const first = (await txn.list({
|
|
3799
|
+
prefix: CHUNK_KEY_PREFIX,
|
|
3800
|
+
limit: 1
|
|
3801
|
+
})).entries().next().value;
|
|
3802
|
+
if (first) {
|
|
3803
|
+
const [key, payload] = first;
|
|
3804
|
+
page.push({
|
|
3805
|
+
index: indexFromChunkKey(key),
|
|
3806
|
+
size: payload instanceof Uint8Array ? payload.byteLength : 0
|
|
3807
|
+
});
|
|
3808
|
+
bytes = page[0].size;
|
|
3809
|
+
payloadFallback = true;
|
|
3810
|
+
}
|
|
3811
|
+
}
|
|
3812
|
+
await deleteInBatches(txn, page.flatMap(({ index }) => [chunkKey(index), chunkSizeKey(index)]));
|
|
3813
|
+
let done = candidates.size === page.length;
|
|
3814
|
+
if (payloadFallback) done = (await txn.list({
|
|
3815
|
+
prefix: CHUNK_KEY_PREFIX,
|
|
3816
|
+
limit: 1
|
|
3817
|
+
})).size === 0;
|
|
3818
|
+
else if (done && candidates.size > 0) done = (await txn.list({
|
|
3819
|
+
prefix: CHUNK_KEY_PREFIX,
|
|
3820
|
+
limit: 1
|
|
3821
|
+
})).size === 0;
|
|
3822
|
+
return {
|
|
3823
|
+
chunks: page.length,
|
|
3824
|
+
bytes,
|
|
3825
|
+
done
|
|
3826
|
+
};
|
|
3827
|
+
}
|
|
3828
|
+
/**
|
|
3829
|
+
* Delete one page of segment rows, listing segment size rows to avoid
|
|
3830
|
+
* loading payloads. One segment always makes progress, even when it holds
|
|
3831
|
+
* more chunks or bytes than the page allows.
|
|
3832
|
+
*/
|
|
3833
|
+
async expireSegmentPage(txn, limit, byteLimit) {
|
|
3834
|
+
const candidates = await txn.list({
|
|
3835
|
+
prefix: SEGMENT_SIZE_KEY_PREFIX,
|
|
3836
|
+
limit: limit + 1
|
|
3837
|
+
});
|
|
3838
|
+
const page = [];
|
|
3839
|
+
let chunks = 0;
|
|
3840
|
+
let bytes = 0;
|
|
3841
|
+
for (const [key, value] of candidates) {
|
|
3842
|
+
const start = startFromSegmentKey(key, SEGMENT_SIZE_KEY_PREFIX);
|
|
3843
|
+
const size = validateSegmentSize(value, start);
|
|
3844
|
+
if (page.length > 0 && (chunks + size.count > limit || bytes + size.bytes > byteLimit)) break;
|
|
3845
|
+
page.push(start);
|
|
3846
|
+
chunks += size.count;
|
|
3847
|
+
bytes += size.bytes;
|
|
3848
|
+
}
|
|
3849
|
+
let orphan = false;
|
|
3850
|
+
if (candidates.size === 0) {
|
|
3851
|
+
const first = (await txn.list({
|
|
3852
|
+
prefix: SEGMENT_KEY_PREFIX,
|
|
3853
|
+
limit: 1
|
|
3854
|
+
})).entries().next().value;
|
|
3855
|
+
if (first) {
|
|
3856
|
+
page.push(startFromSegmentKey(first[0], SEGMENT_KEY_PREFIX));
|
|
3857
|
+
bytes = first[1] instanceof Uint8Array ? first[1].byteLength : 0;
|
|
3858
|
+
chunks = 1;
|
|
3859
|
+
orphan = true;
|
|
3860
|
+
}
|
|
3861
|
+
}
|
|
3862
|
+
await deleteInBatches(txn, page.flatMap((start) => [segmentKey(start), segmentSizeKey(start)]));
|
|
3863
|
+
let done = !orphan && candidates.size === page.length;
|
|
3864
|
+
if (orphan || done) done = (await txn.list({
|
|
3865
|
+
prefix: SEGMENT_KEY_PREFIX,
|
|
3866
|
+
limit: 1
|
|
3867
|
+
})).size === 0;
|
|
3868
|
+
return {
|
|
3869
|
+
chunks,
|
|
3870
|
+
bytes,
|
|
3871
|
+
done
|
|
3872
|
+
};
|
|
3873
|
+
}
|
|
3384
3874
|
/**
|
|
3385
3875
|
* Fence the stream and delete one bounded page. Listing size keys avoids
|
|
3386
3876
|
* loading payloads, and the transaction makes a crash retry resume safely.
|
|
@@ -3394,50 +3884,8 @@ var StreamDO = class extends DurableObject {
|
|
|
3394
3884
|
const meta = stored === void 0 ? emptyMeta() : validateMeta(stored);
|
|
3395
3885
|
this.assertOwner(meta, runId);
|
|
3396
3886
|
const firstFence = meta.state !== "expired";
|
|
3397
|
-
const
|
|
3398
|
-
|
|
3399
|
-
limit: limit + 1
|
|
3400
|
-
});
|
|
3401
|
-
const page = [];
|
|
3402
|
-
let bytes = 0;
|
|
3403
|
-
for (const [key, storedSize] of Array.from(candidates).slice(0, limit)) {
|
|
3404
|
-
const index = indexFromChunkSizeKey(key);
|
|
3405
|
-
const size = validateChunkSize(storedSize, index);
|
|
3406
|
-
if (page.length > 0 && bytes + size > byteLimit) break;
|
|
3407
|
-
page.push({
|
|
3408
|
-
index,
|
|
3409
|
-
size
|
|
3410
|
-
});
|
|
3411
|
-
bytes += size;
|
|
3412
|
-
}
|
|
3413
|
-
let payloadFallback = false;
|
|
3414
|
-
if (candidates.size === 0) {
|
|
3415
|
-
const first = (await txn.list({
|
|
3416
|
-
prefix: CHUNK_KEY_PREFIX,
|
|
3417
|
-
limit: 1
|
|
3418
|
-
})).entries().next().value;
|
|
3419
|
-
if (first) {
|
|
3420
|
-
const [key, payload] = first;
|
|
3421
|
-
page.push({
|
|
3422
|
-
index: indexFromChunkKey(key),
|
|
3423
|
-
size: payload instanceof Uint8Array ? payload.byteLength : 0
|
|
3424
|
-
});
|
|
3425
|
-
bytes = page[0].size;
|
|
3426
|
-
payloadFallback = true;
|
|
3427
|
-
}
|
|
3428
|
-
}
|
|
3429
|
-
const keys = page.flatMap(({ index }) => [chunkKey(index), chunkSizeKey(index)]);
|
|
3430
|
-
for (let offset = 0; offset < keys.length; offset += DELETE_BATCH_KEYS) await txn.delete(keys.slice(offset, offset + DELETE_BATCH_KEYS));
|
|
3431
|
-
let done = candidates.size === page.length;
|
|
3432
|
-
if (payloadFallback) done = (await txn.list({
|
|
3433
|
-
prefix: CHUNK_KEY_PREFIX,
|
|
3434
|
-
limit: 1
|
|
3435
|
-
})).size === 0;
|
|
3436
|
-
else if (done && candidates.size > 0) done = (await txn.list({
|
|
3437
|
-
prefix: CHUNK_KEY_PREFIX,
|
|
3438
|
-
limit: 1
|
|
3439
|
-
})).size === 0;
|
|
3440
|
-
if (done && page.length === 0 && meta.state === "expired" && meta.payloadDeleted) return {
|
|
3887
|
+
const { chunks, bytes, done } = meta.layout === SEGMENT_LAYOUT ? await this.expireSegmentPage(txn, limit, byteLimit) : await this.expireChunkPage(txn, limit, byteLimit);
|
|
3888
|
+
if (done && chunks === 0 && meta.state === "expired" && meta.payloadDeleted) return {
|
|
3441
3889
|
meta,
|
|
3442
3890
|
result: {
|
|
3443
3891
|
deleted: false,
|
|
@@ -3461,7 +3909,7 @@ var StreamDO = class extends DurableObject {
|
|
|
3461
3909
|
meta: nextMeta,
|
|
3462
3910
|
result: {
|
|
3463
3911
|
deleted: firstFence,
|
|
3464
|
-
chunks
|
|
3912
|
+
chunks,
|
|
3465
3913
|
bytes,
|
|
3466
3914
|
done
|
|
3467
3915
|
}
|
|
@@ -3491,6 +3939,14 @@ const CLEANUP_RETRY_MAX_MS = 36e5;
|
|
|
3491
3939
|
const MAX_DATE_MS = 864e13;
|
|
3492
3940
|
const QUEUE_ORPHAN_KEY = "queue-orphan";
|
|
3493
3941
|
const QUEUE_RESERVATION_KEY = "queue-reservation";
|
|
3942
|
+
/** Run-scoped Queue idempotency state, one key per claim name, in the run's own cell. */
|
|
3943
|
+
const RUN_QUEUE_CLAIM_PREFIX = "queue-claim:";
|
|
3944
|
+
/**
|
|
3945
|
+
* Present when every idempotency claim of this run lives in the run's own
|
|
3946
|
+
* cell. Written only when the run is created, so a run keeps one claim scope
|
|
3947
|
+
* for its whole life and claims made before the marker existed stay valid.
|
|
3948
|
+
*/
|
|
3949
|
+
const RUN_QUEUE_CLAIM_SCOPE_KEY = "queue-claim-scope";
|
|
3494
3950
|
function validateCleanupRequest(request, allowDisabled) {
|
|
3495
3951
|
if (!isRecord$1(request)) throw new Error("world-celld retention request must be an object");
|
|
3496
3952
|
if (!(allowDisabled ? isNonNegativeSafeInteger(request.retentionMs) : isPositiveSafeInteger(request.retentionMs))) throw new Error(`world-celld retentionMs must be a ${allowDisabled ? "non-negative" : "positive"} safe integer`);
|
|
@@ -3544,6 +4000,12 @@ async function putStorageEntries(storage, entries) {
|
|
|
3544
4000
|
const all = Array.from(entries);
|
|
3545
4001
|
for (let offset = 0; offset < all.length; offset += STORAGE_BATCH_SIZE) await storage.put(Object.fromEntries(all.slice(offset, offset + STORAGE_BATCH_SIZE)));
|
|
3546
4002
|
}
|
|
4003
|
+
function runQueueClaimKey(claimName) {
|
|
4004
|
+
return `${RUN_QUEUE_CLAIM_PREFIX}${claimName}`;
|
|
4005
|
+
}
|
|
4006
|
+
function validRunQueueClaimName(claimName) {
|
|
4007
|
+
return typeof claimName === "string" && claimName.startsWith("claim:");
|
|
4008
|
+
}
|
|
3547
4009
|
/**
|
|
3548
4010
|
* Durable Object for managing a single workflow run's state.
|
|
3549
4011
|
*
|
|
@@ -3624,6 +4086,7 @@ var WorkflowRunDO = class extends DurableObject {
|
|
|
3624
4086
|
});
|
|
3625
4087
|
if (!outcome.ok) return outcome;
|
|
3626
4088
|
if (eventSequence !== storedSequence) await txn.put(EVENT_SEQUENCE_KEY, eventSequence);
|
|
4089
|
+
if (outcome.runCreated) await txn.put(RUN_QUEUE_CLAIM_SCOPE_KEY, "run");
|
|
3627
4090
|
await putStorageEntries(txn, (outcome.hookToIndex ? [{
|
|
3628
4091
|
hookId: outcome.hookToIndex.hookId,
|
|
3629
4092
|
token: outcome.hookToIndex.token
|
|
@@ -3677,6 +4140,28 @@ var WorkflowRunDO = class extends DurableObject {
|
|
|
3677
4140
|
async getRun() {
|
|
3678
4141
|
return this.readKey("run");
|
|
3679
4142
|
}
|
|
4143
|
+
/** Fence both supported event-claim forms before an expired shard claim is released. */
|
|
4144
|
+
async resolveExpiredHookClaim(request) {
|
|
4145
|
+
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");
|
|
4146
|
+
return await this.ctx.storage.transaction(async (txn) => {
|
|
4147
|
+
const { tombstone } = await this.retentionState(txn);
|
|
4148
|
+
if (tombstone) return null;
|
|
4149
|
+
const run = await txn.get("run");
|
|
4150
|
+
if (run && isTerminalWorkflowRunStatus(run.status)) return null;
|
|
4151
|
+
const hook = await txn.get(`${HOOK_KEY_PREFIX}${request.hookId}`);
|
|
4152
|
+
if (hook && (request.token === void 0 || hook.token === request.token)) return hook;
|
|
4153
|
+
const cancellation = {
|
|
4154
|
+
hookId: request.hookId,
|
|
4155
|
+
token: request.token,
|
|
4156
|
+
canceledAt: this.now()
|
|
4157
|
+
};
|
|
4158
|
+
await txn.put({
|
|
4159
|
+
[hookClaimCancellationKey(`${request.claimId}:${request.claimId}`)]: cancellation,
|
|
4160
|
+
[hookClaimCancellationKey(`-:${request.claimId}`)]: cancellation
|
|
4161
|
+
});
|
|
4162
|
+
return null;
|
|
4163
|
+
});
|
|
4164
|
+
}
|
|
3680
4165
|
/** Single authoritative lifecycle answer used after derivative compaction. */
|
|
3681
4166
|
async getLifecycleStatus() {
|
|
3682
4167
|
return await this.ctx.storage.transaction(async (txn) => {
|
|
@@ -4290,6 +4775,189 @@ var WorkflowRunDO = class extends DurableObject {
|
|
|
4290
4775
|
await txn.setAlarm(this.now() + delay);
|
|
4291
4776
|
});
|
|
4292
4777
|
}
|
|
4778
|
+
/**
|
|
4779
|
+
* Read-only admission for a run-bearing Queue message whose body travels
|
|
4780
|
+
* inline in the broker envelope. It answers what payload registration does
|
|
4781
|
+
* for offloaded bodies, without writing anything.
|
|
4782
|
+
*/
|
|
4783
|
+
async getQueueAdmission() {
|
|
4784
|
+
const { tombstone } = await this.retentionState(this.ctx.storage);
|
|
4785
|
+
return tombstone ? {
|
|
4786
|
+
ok: false,
|
|
4787
|
+
message: `Workflow run "${tombstone.runId}" expired at ${tombstone.expiredAt.toISOString()}`
|
|
4788
|
+
} : { ok: true };
|
|
4789
|
+
}
|
|
4790
|
+
/**
|
|
4791
|
+
* Enqueue-side admission and idempotency reservation in one transaction,
|
|
4792
|
+
* for a run-scoped claim. Replaces a separate claim cell's reservation plus
|
|
4793
|
+
* the run expiry check.
|
|
4794
|
+
*/
|
|
4795
|
+
async reserveRunQueueMessage(params) {
|
|
4796
|
+
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");
|
|
4797
|
+
return await this.ctx.storage.transaction(async (txn) => {
|
|
4798
|
+
const key = runQueueClaimKey(params.claimName);
|
|
4799
|
+
const values = await txn.get([
|
|
4800
|
+
TOMBSTONE_KEY,
|
|
4801
|
+
CLEANUP_RECORD_KEY,
|
|
4802
|
+
RUN_QUEUE_CLAIM_SCOPE_KEY,
|
|
4803
|
+
key
|
|
4804
|
+
]);
|
|
4805
|
+
const tombstone = this.tombstoneFrom(values);
|
|
4806
|
+
if (tombstone) return {
|
|
4807
|
+
ok: false,
|
|
4808
|
+
message: `Workflow run "${tombstone.runId}" expired at ${tombstone.expiredAt.toISOString()}`
|
|
4809
|
+
};
|
|
4810
|
+
if (values.get("queue-claim-scope") !== "run") return {
|
|
4811
|
+
ok: true,
|
|
4812
|
+
scope: "cell"
|
|
4813
|
+
};
|
|
4814
|
+
const state = values.get(key) ?? {};
|
|
4815
|
+
const existing = state.reservation;
|
|
4816
|
+
if (existing && existing.expiresAt > this.now() && (existing.messageId !== params.messageId || params.publicationPending)) return {
|
|
4817
|
+
ok: true,
|
|
4818
|
+
scope: "run",
|
|
4819
|
+
admitted: false,
|
|
4820
|
+
messageId: existing.messageId,
|
|
4821
|
+
...existing.publicationPending ? { publicationPending: true } : {}
|
|
4822
|
+
};
|
|
4823
|
+
await txn.put(key, {
|
|
4824
|
+
...state,
|
|
4825
|
+
reservation: {
|
|
4826
|
+
messageId: params.messageId,
|
|
4827
|
+
expiresAt: Math.max(existing?.expiresAt ?? 0, params.expiresAt),
|
|
4828
|
+
...params.publicationPending ? { publicationPending: true } : {}
|
|
4829
|
+
}
|
|
4830
|
+
});
|
|
4831
|
+
return {
|
|
4832
|
+
ok: true,
|
|
4833
|
+
scope: "run",
|
|
4834
|
+
admitted: true,
|
|
4835
|
+
messageId: params.messageId
|
|
4836
|
+
};
|
|
4837
|
+
});
|
|
4838
|
+
}
|
|
4839
|
+
async confirmRunQueueMessagePublication(params) {
|
|
4840
|
+
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");
|
|
4841
|
+
await this.ctx.storage.transaction(async (txn) => {
|
|
4842
|
+
const key = runQueueClaimKey(params.claimName);
|
|
4843
|
+
const state = await txn.get(key);
|
|
4844
|
+
if (state?.reservation?.messageId !== params.messageId) return;
|
|
4845
|
+
await txn.put(key, {
|
|
4846
|
+
...state,
|
|
4847
|
+
reservation: {
|
|
4848
|
+
messageId: params.messageId,
|
|
4849
|
+
expiresAt: Math.max(state.reservation.expiresAt, params.expiresAt)
|
|
4850
|
+
}
|
|
4851
|
+
});
|
|
4852
|
+
});
|
|
4853
|
+
}
|
|
4854
|
+
/** Delivery-side run expiry check and in-flight claim in one transaction. */
|
|
4855
|
+
async claimRunQueueMessage(params) {
|
|
4856
|
+
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");
|
|
4857
|
+
return await this.ctx.storage.transaction(async (txn) => {
|
|
4858
|
+
const key = runQueueClaimKey(params.claimName);
|
|
4859
|
+
const values = await txn.get([
|
|
4860
|
+
TOMBSTONE_KEY,
|
|
4861
|
+
CLEANUP_RECORD_KEY,
|
|
4862
|
+
key
|
|
4863
|
+
]);
|
|
4864
|
+
if (this.tombstoneFrom(values)) return { expired: true };
|
|
4865
|
+
let state = values.get(key) ?? {};
|
|
4866
|
+
const now = this.now();
|
|
4867
|
+
if (state.reservation?.publicationPending && state.reservation.messageId === params.messageId) {
|
|
4868
|
+
state = {
|
|
4869
|
+
...state,
|
|
4870
|
+
reservation: {
|
|
4871
|
+
messageId: params.messageId,
|
|
4872
|
+
expiresAt: Math.max(state.reservation.expiresAt, now + QUEUE_RESERVATION_GRACE_MS)
|
|
4873
|
+
}
|
|
4874
|
+
};
|
|
4875
|
+
await txn.put(key, state);
|
|
4876
|
+
}
|
|
4877
|
+
const existing = state.claim;
|
|
4878
|
+
if (existing && existing.expiresAt > now) {
|
|
4879
|
+
if (existing.messageId !== params.messageId) return {
|
|
4880
|
+
expired: false,
|
|
4881
|
+
claimed: false
|
|
4882
|
+
};
|
|
4883
|
+
if (existing.retryAt === void 0 || existing.retryAt > now) return {
|
|
4884
|
+
expired: false,
|
|
4885
|
+
claimed: false,
|
|
4886
|
+
retryAt: existing.retryAt ?? existing.expiresAt
|
|
4887
|
+
};
|
|
4888
|
+
}
|
|
4889
|
+
await txn.put(key, {
|
|
4890
|
+
...state,
|
|
4891
|
+
claim: {
|
|
4892
|
+
messageId: params.messageId,
|
|
4893
|
+
expiresAt: now + params.staleMs
|
|
4894
|
+
}
|
|
4895
|
+
});
|
|
4896
|
+
return {
|
|
4897
|
+
expired: false,
|
|
4898
|
+
claimed: true
|
|
4899
|
+
};
|
|
4900
|
+
});
|
|
4901
|
+
}
|
|
4902
|
+
async holdRunQueueMessage(params) {
|
|
4903
|
+
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");
|
|
4904
|
+
return await this.ctx.storage.transaction(async (txn) => {
|
|
4905
|
+
const key = runQueueClaimKey(params.claimName);
|
|
4906
|
+
const state = await txn.get(key) ?? {};
|
|
4907
|
+
const existing = state.claim;
|
|
4908
|
+
if (!existing || existing.messageId !== params.messageId) return { held: false };
|
|
4909
|
+
const next = {
|
|
4910
|
+
...state,
|
|
4911
|
+
claim: {
|
|
4912
|
+
...existing,
|
|
4913
|
+
retryAt: params.retryAt,
|
|
4914
|
+
expiresAt: Math.max(existing.expiresAt, params.expiresAt)
|
|
4915
|
+
}
|
|
4916
|
+
};
|
|
4917
|
+
if (state.reservation?.messageId === params.messageId && params.reservationExpiresAt !== void 0 && params.reservationExpiresAt > state.reservation.expiresAt) next.reservation = {
|
|
4918
|
+
...state.reservation,
|
|
4919
|
+
expiresAt: params.reservationExpiresAt
|
|
4920
|
+
};
|
|
4921
|
+
await txn.put(key, next);
|
|
4922
|
+
return { held: true };
|
|
4923
|
+
});
|
|
4924
|
+
}
|
|
4925
|
+
async releaseRunQueueMessage(params) {
|
|
4926
|
+
if (!validRunQueueClaimName(params?.claimName) || typeof params.messageId !== "string") throw new TypeError("world-celld run queue claim release is invalid");
|
|
4927
|
+
await this.ctx.storage.transaction(async (txn) => {
|
|
4928
|
+
const key = runQueueClaimKey(params.claimName);
|
|
4929
|
+
const state = await txn.get(key);
|
|
4930
|
+
if (state?.claim?.messageId !== params.messageId) return;
|
|
4931
|
+
const { claim: _released, ...rest } = state;
|
|
4932
|
+
if (rest.reservation) await txn.put(key, rest);
|
|
4933
|
+
else await txn.delete(key);
|
|
4934
|
+
});
|
|
4935
|
+
}
|
|
4936
|
+
async completeRunQueueMessage(params) {
|
|
4937
|
+
if (!validRunQueueClaimName(params?.claimName) || typeof params.messageId !== "string") throw new TypeError("world-celld run queue completion is invalid");
|
|
4938
|
+
await this.ctx.storage.transaction(async (txn) => {
|
|
4939
|
+
const key = runQueueClaimKey(params.claimName);
|
|
4940
|
+
const state = await txn.get(key);
|
|
4941
|
+
if (!state) return;
|
|
4942
|
+
const next = {};
|
|
4943
|
+
if (state.reservation && state.reservation.messageId !== params.messageId) next.reservation = state.reservation;
|
|
4944
|
+
if (state.claim && state.claim.messageId !== params.messageId) next.claim = state.claim;
|
|
4945
|
+
if (next.reservation || next.claim) await txn.put(key, next);
|
|
4946
|
+
else await txn.delete(key);
|
|
4947
|
+
});
|
|
4948
|
+
}
|
|
4949
|
+
/** Failed enqueue cleanup must preserve delivery already accepted by the broker. */
|
|
4950
|
+
async abandonRunQueueMessageReservation(params) {
|
|
4951
|
+
if (!validRunQueueClaimName(params?.claimName) || typeof params.messageId !== "string" || params.messageId.length === 0) throw new TypeError("world-celld run queue reservation abandonment is invalid");
|
|
4952
|
+
await this.ctx.storage.transaction(async (txn) => {
|
|
4953
|
+
const key = runQueueClaimKey(params.claimName);
|
|
4954
|
+
const state = await txn.get(key);
|
|
4955
|
+
if (state?.reservation?.messageId !== params.messageId) return;
|
|
4956
|
+
const { reservation: _abandoned, ...rest } = state;
|
|
4957
|
+
if (rest.claim) await txn.put(key, rest);
|
|
4958
|
+
else await txn.delete(key);
|
|
4959
|
+
});
|
|
4960
|
+
}
|
|
4293
4961
|
async registerQueuePayload(registration) {
|
|
4294
4962
|
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");
|
|
4295
4963
|
return await this.ctx.storage.transaction(async (txn) => {
|
|
@@ -4346,22 +5014,24 @@ var WorkflowRunDO = class extends DurableObject {
|
|
|
4346
5014
|
});
|
|
4347
5015
|
}
|
|
4348
5016
|
async reserveQueueMessage(params) {
|
|
4349
|
-
if (typeof params?.messageId !== "string" || params.messageId.length === 0 || !isNonNegativeSafeInteger(params.expiresAt)) throw new TypeError("world-celld queue message reservation is invalid");
|
|
5017
|
+
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");
|
|
4350
5018
|
return await this.ctx.storage.transaction(async (txn) => {
|
|
4351
5019
|
const values = await txn.get([QUEUE_RESERVATION_KEY, "claim"]);
|
|
4352
5020
|
const existing = values.get(QUEUE_RESERVATION_KEY);
|
|
4353
5021
|
const claim = values.get("claim");
|
|
4354
5022
|
const now = this.now();
|
|
4355
|
-
if (existing && existing.expiresAt > now && existing.messageId !== params.messageId) {
|
|
5023
|
+
if (existing && existing.expiresAt > now && (existing.messageId !== params.messageId || params.publicationPending)) {
|
|
4356
5024
|
await txn.setAlarm(Math.min(existing.expiresAt, claim?.expiresAt ?? existing.expiresAt));
|
|
4357
5025
|
return {
|
|
4358
5026
|
admitted: false,
|
|
4359
|
-
messageId: existing.messageId
|
|
5027
|
+
messageId: existing.messageId,
|
|
5028
|
+
...existing.publicationPending ? { publicationPending: true } : {}
|
|
4360
5029
|
};
|
|
4361
5030
|
}
|
|
4362
5031
|
const reservation = {
|
|
4363
5032
|
messageId: params.messageId,
|
|
4364
|
-
expiresAt: Math.max(existing?.expiresAt ?? 0, params.expiresAt)
|
|
5033
|
+
expiresAt: Math.max(existing?.expiresAt ?? 0, params.expiresAt),
|
|
5034
|
+
...params.publicationPending ? { publicationPending: true } : {}
|
|
4365
5035
|
};
|
|
4366
5036
|
await txn.put(QUEUE_RESERVATION_KEY, reservation);
|
|
4367
5037
|
await txn.setAlarm(Math.min(reservation.expiresAt, claim?.expiresAt ?? reservation.expiresAt));
|
|
@@ -4371,6 +5041,20 @@ var WorkflowRunDO = class extends DurableObject {
|
|
|
4371
5041
|
};
|
|
4372
5042
|
});
|
|
4373
5043
|
}
|
|
5044
|
+
async confirmQueueMessagePublication(params) {
|
|
5045
|
+
if (typeof params?.messageId !== "string" || params.messageId.length === 0 || !isNonNegativeSafeInteger(params.expiresAt)) throw new TypeError("world-celld queue publication confirmation is invalid");
|
|
5046
|
+
await this.ctx.storage.transaction(async (txn) => {
|
|
5047
|
+
const values = await txn.get([QUEUE_RESERVATION_KEY, "claim"]);
|
|
5048
|
+
const existing = values.get(QUEUE_RESERVATION_KEY);
|
|
5049
|
+
if (existing?.messageId !== params.messageId) return;
|
|
5050
|
+
const reservation = {
|
|
5051
|
+
messageId: params.messageId,
|
|
5052
|
+
expiresAt: Math.max(existing.expiresAt, params.expiresAt)
|
|
5053
|
+
};
|
|
5054
|
+
await txn.put(QUEUE_RESERVATION_KEY, reservation);
|
|
5055
|
+
await txn.setAlarm(Math.min(reservation.expiresAt, values.get("claim")?.expiresAt ?? reservation.expiresAt));
|
|
5056
|
+
});
|
|
5057
|
+
}
|
|
4374
5058
|
async completeQueueMessage(messageId) {
|
|
4375
5059
|
if (typeof messageId !== "string" || messageId.length === 0) throw new TypeError("world-celld queue messageId must be a non-empty string");
|
|
4376
5060
|
await this.ctx.storage.transaction(async (txn) => {
|
|
@@ -4390,13 +5074,32 @@ var WorkflowRunDO = class extends DurableObject {
|
|
|
4390
5074
|
else await txn.deleteAlarm();
|
|
4391
5075
|
});
|
|
4392
5076
|
}
|
|
5077
|
+
/** Abandon the enqueue reservation without releasing an active delivery claim. */
|
|
5078
|
+
async abandonQueueMessageReservation(messageId) {
|
|
5079
|
+
if (typeof messageId !== "string" || messageId.length === 0) throw new TypeError("world-celld queue reservation messageId must be a non-empty string");
|
|
5080
|
+
await this.ctx.storage.transaction(async (txn) => {
|
|
5081
|
+
const values = await txn.get([QUEUE_RESERVATION_KEY, "claim"]);
|
|
5082
|
+
if (values.get(QUEUE_RESERVATION_KEY)?.messageId !== messageId) return;
|
|
5083
|
+
await txn.delete(QUEUE_RESERVATION_KEY);
|
|
5084
|
+
const claim = values.get("claim");
|
|
5085
|
+
if (claim) await txn.setAlarm(claim.expiresAt);
|
|
5086
|
+
else await txn.deleteAlarm();
|
|
5087
|
+
});
|
|
5088
|
+
}
|
|
4393
5089
|
async claimInflight(params) {
|
|
4394
5090
|
if (typeof params?.messageId !== "string" || params.messageId.length === 0 || !isPositiveSafeInteger(params.staleMs)) throw new TypeError("world-celld queue claim is invalid");
|
|
4395
5091
|
return await this.ctx.storage.transaction(async (txn) => {
|
|
4396
5092
|
const values = await txn.get(["claim", QUEUE_RESERVATION_KEY]);
|
|
4397
5093
|
const existing = values.get("claim");
|
|
4398
|
-
|
|
5094
|
+
let reservation = values.get(QUEUE_RESERVATION_KEY);
|
|
4399
5095
|
const now = this.now();
|
|
5096
|
+
if (reservation?.publicationPending && reservation.messageId === params.messageId) {
|
|
5097
|
+
reservation = {
|
|
5098
|
+
messageId: params.messageId,
|
|
5099
|
+
expiresAt: Math.max(reservation.expiresAt, now + QUEUE_RESERVATION_GRACE_MS)
|
|
5100
|
+
};
|
|
5101
|
+
await txn.put(QUEUE_RESERVATION_KEY, reservation);
|
|
5102
|
+
}
|
|
4400
5103
|
if (existing && existing.expiresAt > now) {
|
|
4401
5104
|
if (existing.messageId !== params.messageId) return { claimed: false };
|
|
4402
5105
|
if (existing.retryAt === void 0 || existing.retryAt > now) return {
|