@ewhauser/world-celld 0.7.0 → 0.8.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/dist/index.d.ts +10 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +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-BdeNUbRN.d.ts} +29 -3
- package/dist/queue-delivery-BdeNUbRN.d.ts.map +1 -0
- package/dist/{queue-protocol-CtH7UUCO.js → queue-protocol-DKYt8qZJ.js} +25 -2
- package/dist/queue-protocol-DKYt8qZJ.js.map +1 -0
- package/dist/{run-list-B__jKuE7.js → run-list-C987PkJ2.js} +25 -2
- package/dist/{run-list-B__jKuE7.js.map → run-list-C987PkJ2.js.map} +1 -1
- package/dist/testing.d.ts +2 -0
- package/dist/testing.d.ts.map +1 -1
- package/dist/testing.js +557 -92
- package/dist/testing.js.map +1 -1
- package/dist/worker.d.ts +66 -1
- package/dist/worker.d.ts.map +1 -1
- package/dist/worker.js +581 -103
- package/dist/worker.js.map +1 -1
- package/package.json +1 -1
- package/dist/queue-delivery-DzNOtCYA.d.ts.map +0 -1
- package/dist/queue-protocol-CtH7UUCO.js.map +0 -1
package/dist/testing.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { A as
|
|
1
|
+
import { A as isPositiveSafeInteger, E as MAX_RUN_INDEX_PUBLICATION_LIFETIME_MS, F as MAX_STREAM_CHUNK_BYTES, H as STREAM_CHUNK_LIMIT_HEADER, J as encodeStreamWriteResult, K as encodeStreamReadResult, L as MAX_STREAM_LONG_POLL_MS, P as MAX_STREAM_BATCH_BYTES, Q as validateStreamWriteChunks, R as MAX_STREAM_READ_BYTES, S as LIFECYCLE_COMPACTION_RETRY_MS, V as STREAM_BATCH_CONTENT_TYPE, W as decodeStreamWriteBatch, X as normalizeStreamError, Z as validateStreamReadRequest, a as isInlineRunBody, c as queueOrphanName, d as validateNativeQueueEnvelope, f as validateNativeQueueSendOptions, g as parse, h as rpcStringify, i as QUEUE_PAYLOAD_REGISTRY_PREFIX, j as isRecord$1, k as isNonNegativeSafeInteger, l as queuePayloadObjectKey, m as rpcParse, n as listRunsPage, p as compact, r as NATIVE_QUEUE_MAX_MESSAGE_BYTES, s as queueClaimName, t as MAX_RUN_LIST_LIMIT, u as queuePayloadRegistryKey, v as createWorkflowIndex, x as HOOK_CLAIM_LEASE_MS, y as runIndexMetadata } from "./run-list-C987PkJ2.js";
|
|
2
2
|
import { CreateEventSchema, EventSchema, HookSchema, SPEC_VERSION_CURRENT, StepSchema, WaitSchema, WorkflowRunSchema, applyAttributeChanges, eventIdToSlot, getMaxEventsPerRun, isChildEntityCreationEvent, isTerminalStepStatus, isTerminalWorkflowRunStatus, slotToEventId, validateAttributeChanges } from "@workflow/world";
|
|
3
3
|
import http from "node:http";
|
|
4
4
|
//#region src/testing/fake-cell.ts
|
|
@@ -373,6 +373,10 @@ var FakeFleet = class {
|
|
|
373
373
|
}
|
|
374
374
|
}, this.cellEnv);
|
|
375
375
|
}
|
|
376
|
+
/** Whether a cell has been activated, without activating it. */
|
|
377
|
+
hasCell(bindingKey, name) {
|
|
378
|
+
return this.cells.has(`${bindingKey}\0${name}`);
|
|
379
|
+
}
|
|
376
380
|
cell(bindingKey, name) {
|
|
377
381
|
const key = `${bindingKey}\0${name}`;
|
|
378
382
|
let slot = this.cells.get(key);
|
|
@@ -1350,6 +1354,46 @@ async function authenticate(request, secret) {
|
|
|
1350
1354
|
return { ok: true };
|
|
1351
1355
|
}
|
|
1352
1356
|
//#endregion
|
|
1357
|
+
//#region src/worker/queue-claims.ts
|
|
1358
|
+
function cellQueueClaim(cell) {
|
|
1359
|
+
return {
|
|
1360
|
+
claim: (messageId, staleMs) => cell.claimInflight({
|
|
1361
|
+
messageId,
|
|
1362
|
+
staleMs
|
|
1363
|
+
}),
|
|
1364
|
+
hold: (params) => cell.holdInflight(params),
|
|
1365
|
+
release: (messageId) => cell.releaseInflight(messageId),
|
|
1366
|
+
complete: (messageId) => cell.completeQueueMessage(messageId)
|
|
1367
|
+
};
|
|
1368
|
+
}
|
|
1369
|
+
function runQueueClaim(run, claimName) {
|
|
1370
|
+
return {
|
|
1371
|
+
claim: async (messageId, staleMs) => {
|
|
1372
|
+
const result = await run.claimRunQueueMessage({
|
|
1373
|
+
claimName,
|
|
1374
|
+
messageId,
|
|
1375
|
+
staleMs
|
|
1376
|
+
});
|
|
1377
|
+
return result.expired ? {
|
|
1378
|
+
expired: true,
|
|
1379
|
+
claimed: false
|
|
1380
|
+
} : result;
|
|
1381
|
+
},
|
|
1382
|
+
hold: (params) => run.holdRunQueueMessage({
|
|
1383
|
+
claimName,
|
|
1384
|
+
...params
|
|
1385
|
+
}),
|
|
1386
|
+
release: (messageId) => run.releaseRunQueueMessage({
|
|
1387
|
+
claimName,
|
|
1388
|
+
messageId
|
|
1389
|
+
}),
|
|
1390
|
+
complete: (messageId) => run.completeRunQueueMessage({
|
|
1391
|
+
claimName,
|
|
1392
|
+
messageId
|
|
1393
|
+
})
|
|
1394
|
+
};
|
|
1395
|
+
}
|
|
1396
|
+
//#endregion
|
|
1353
1397
|
//#region src/worker/index-validation.ts
|
|
1354
1398
|
function invalid(message) {
|
|
1355
1399
|
const error = /* @__PURE__ */ new Error(`invalid index RPC arguments: ${message}`);
|
|
@@ -1598,6 +1642,18 @@ const INDEX_OPERATIONS = /* @__PURE__ */ new Set([
|
|
|
1598
1642
|
*/
|
|
1599
1643
|
const WORLD_NAME = "world-celld";
|
|
1600
1644
|
const WORLD_VERSION = "0.1.0";
|
|
1645
|
+
/**
|
|
1646
|
+
* Per-isolate record of runs whose Queue idempotency claims stay in claim
|
|
1647
|
+
* cells (created before run-scoped claims) or live in the run cell. A run's
|
|
1648
|
+
* scope never changes, so the cache only saves the run-cell question.
|
|
1649
|
+
*/
|
|
1650
|
+
const runClaimScopes = /* @__PURE__ */ new Map();
|
|
1651
|
+
const MAX_REMEMBERED_RUN_CLAIM_SCOPES = 4096;
|
|
1652
|
+
function rememberRunClaimScope(runId, scope) {
|
|
1653
|
+
if (runClaimScopes.has(runId)) return;
|
|
1654
|
+
if (runClaimScopes.size >= MAX_REMEMBERED_RUN_CLAIM_SCOPES) runClaimScopes.delete(runClaimScopes.keys().next().value);
|
|
1655
|
+
runClaimScopes.set(runId, scope);
|
|
1656
|
+
}
|
|
1601
1657
|
function workflowIndex(env) {
|
|
1602
1658
|
return createWorkflowIndex({
|
|
1603
1659
|
runCatalog: env.WORKFLOW_RUN_CATALOG,
|
|
@@ -1835,31 +1891,71 @@ function createRouter(env) {
|
|
|
1835
1891
|
try {
|
|
1836
1892
|
const namespace = env.WORKFLOW_DB;
|
|
1837
1893
|
const payloadStore = env.WORKFLOW_QUEUE_PAYLOADS;
|
|
1838
|
-
const
|
|
1839
|
-
const
|
|
1894
|
+
const inlineRunBody = envelope.runId !== void 0 && isInlineRunBody(envelope.body);
|
|
1895
|
+
const payloadKey = envelope.runId && !inlineRunBody ? queuePayloadObjectKey(envelope.runId, envelope.messageId) : void 0;
|
|
1896
|
+
let brokerEnvelope = payloadKey ? {
|
|
1840
1897
|
...envelope,
|
|
1841
1898
|
payloadKey,
|
|
1842
1899
|
body: void 0
|
|
1843
1900
|
} : envelope;
|
|
1844
|
-
const
|
|
1845
|
-
|
|
1901
|
+
const sized = JSON.stringify({
|
|
1902
|
+
...brokerEnvelope,
|
|
1903
|
+
claimScope: "run"
|
|
1904
|
+
});
|
|
1905
|
+
if (new TextEncoder().encode(sized).byteLength > 128e3) return errorResponse(413, "PayloadTooLarge", `native queue envelope exceeds ${NATIVE_QUEUE_MAX_MESSAGE_BYTES} bytes`);
|
|
1846
1906
|
const now = Date.now();
|
|
1847
1907
|
const reservationExpiresAt = Math.max(now, envelope.notBefore ?? now) + QUEUE_ORPHAN_GRACE_MS;
|
|
1908
|
+
const run = envelope.runId ? namespace.get(namespace.idFromName(envelope.runId)) : void 0;
|
|
1848
1909
|
let claim;
|
|
1910
|
+
let admission;
|
|
1849
1911
|
if (envelope.idempotencyKey) {
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1912
|
+
const claimName = queueClaimName(envelope.queueName, envelope.idempotencyKey);
|
|
1913
|
+
let scope = run && envelope.runId ? runClaimScopes.get(envelope.runId) : "cell";
|
|
1914
|
+
let reservation;
|
|
1915
|
+
if (run && envelope.runId && scope !== "cell") {
|
|
1916
|
+
const reserved = await run.reserveRunQueueMessage({
|
|
1917
|
+
claimName,
|
|
1918
|
+
messageId: envelope.messageId,
|
|
1919
|
+
expiresAt: reservationExpiresAt
|
|
1920
|
+
});
|
|
1921
|
+
if (!reserved.ok) return errorResponse(410, "RunExpiredError", reserved.message);
|
|
1922
|
+
scope = reserved.scope;
|
|
1923
|
+
rememberRunClaimScope(envelope.runId, reserved.scope);
|
|
1924
|
+
if (reserved.scope === "run") {
|
|
1925
|
+
reservation = reserved;
|
|
1926
|
+
claim = runQueueClaim(run, claimName);
|
|
1927
|
+
brokerEnvelope = {
|
|
1928
|
+
...brokerEnvelope,
|
|
1929
|
+
claimScope: "run"
|
|
1930
|
+
};
|
|
1931
|
+
}
|
|
1932
|
+
} else if (inlineRunBody && run) {
|
|
1933
|
+
admission = run.getQueueAdmission();
|
|
1934
|
+
admission.catch(() => void 0);
|
|
1935
|
+
}
|
|
1936
|
+
if (!reservation) {
|
|
1937
|
+
const cell = namespace.get(namespace.idFromName(claimName));
|
|
1938
|
+
reservation = await cell.reserveQueueMessage({
|
|
1939
|
+
messageId: envelope.messageId,
|
|
1940
|
+
expiresAt: reservationExpiresAt
|
|
1941
|
+
});
|
|
1942
|
+
claim = cellQueueClaim(cell);
|
|
1943
|
+
}
|
|
1855
1944
|
if (!reservation.admitted) return new Response(rpcStringify({ messageId: reservation.messageId }), {
|
|
1856
1945
|
status: 200,
|
|
1857
1946
|
headers: { "content-type": "application/json" }
|
|
1858
1947
|
});
|
|
1948
|
+
} else if (inlineRunBody && run) admission = run.getQueueAdmission();
|
|
1949
|
+
const encoded = JSON.stringify(brokerEnvelope);
|
|
1950
|
+
if (admission) {
|
|
1951
|
+
const admitted = await admission;
|
|
1952
|
+
if (!admitted.ok) {
|
|
1953
|
+
await claim?.complete(envelope.messageId);
|
|
1954
|
+
return errorResponse(410, "RunExpiredError", admitted.message);
|
|
1955
|
+
}
|
|
1859
1956
|
}
|
|
1860
|
-
if (envelope.runId && payloadKey) {
|
|
1957
|
+
if (run && envelope.runId && payloadKey) {
|
|
1861
1958
|
if (!payloadStore) return errorResponse(500, "WorldMisconfigured", "missing binding: WORKFLOW_QUEUE_PAYLOADS");
|
|
1862
|
-
const run = namespace.get(namespace.idFromName(envelope.runId));
|
|
1863
1959
|
const orphanExpiresAt = reservationExpiresAt;
|
|
1864
1960
|
const registered = await run.registerQueuePayload({
|
|
1865
1961
|
messageId: envelope.messageId,
|
|
@@ -1867,7 +1963,7 @@ function createRouter(env) {
|
|
|
1867
1963
|
orphanExpiresAt
|
|
1868
1964
|
});
|
|
1869
1965
|
if (!registered.ok) {
|
|
1870
|
-
await claim?.
|
|
1966
|
+
await claim?.complete(envelope.messageId);
|
|
1871
1967
|
return errorResponse(410, "RunExpiredError", registered.message);
|
|
1872
1968
|
}
|
|
1873
1969
|
const orphan = namespace.get(namespace.idFromName(queueOrphanName(envelope.messageId)));
|
|
@@ -1880,7 +1976,7 @@ function createRouter(env) {
|
|
|
1880
1976
|
});
|
|
1881
1977
|
} catch (error) {
|
|
1882
1978
|
await run.unregisterQueuePayload(envelope.messageId);
|
|
1883
|
-
await claim?.
|
|
1979
|
+
await claim?.complete(envelope.messageId);
|
|
1884
1980
|
throw error;
|
|
1885
1981
|
}
|
|
1886
1982
|
try {
|
|
@@ -1889,21 +1985,21 @@ function createRouter(env) {
|
|
|
1889
1985
|
messageId: envelope.messageId
|
|
1890
1986
|
});
|
|
1891
1987
|
} catch (error) {
|
|
1892
|
-
await claim?.
|
|
1988
|
+
await claim?.complete(envelope.messageId);
|
|
1893
1989
|
throw error;
|
|
1894
1990
|
}
|
|
1895
1991
|
let finalized;
|
|
1896
1992
|
try {
|
|
1897
1993
|
finalized = await run.finalizeQueuePayload(envelope.messageId);
|
|
1898
1994
|
} catch (error) {
|
|
1899
|
-
await claim?.
|
|
1995
|
+
await claim?.complete(envelope.messageId);
|
|
1900
1996
|
throw error;
|
|
1901
1997
|
}
|
|
1902
1998
|
if (!finalized.ok) {
|
|
1903
1999
|
await payloadStore.delete(payloadKey);
|
|
1904
2000
|
await run.unregisterQueuePayload(envelope.messageId);
|
|
1905
2001
|
await orphan.cancelQueuePayloadOrphan(envelope.messageId);
|
|
1906
|
-
await claim?.
|
|
2002
|
+
await claim?.complete(envelope.messageId);
|
|
1907
2003
|
return errorResponse(410, "RunExpiredError", finalized.message);
|
|
1908
2004
|
}
|
|
1909
2005
|
}
|
|
@@ -2655,6 +2751,13 @@ const DELETE_BATCH_KEYS = 128;
|
|
|
2655
2751
|
const DEFAULT_EXPIRE_BYTE_LIMIT = 16777216;
|
|
2656
2752
|
const MAX_EXPIRE_BYTE_LIMIT = DEFAULT_EXPIRE_BYTE_LIMIT;
|
|
2657
2753
|
const MAX_STREAM_INDEX = 2147483647;
|
|
2754
|
+
const SEGMENT_LAYOUT = 2;
|
|
2755
|
+
const SEGMENT_KEY_PREFIX = "seg:";
|
|
2756
|
+
const SEGMENT_SIZE_KEY_PREFIX = "segsize:";
|
|
2757
|
+
/** Payload bytes per segment row, well below the 2 MiB per-value storage limit. */
|
|
2758
|
+
const MAX_SEGMENT_BYTES = 1048576;
|
|
2759
|
+
/** Layout of a stream's first write. Existing streams keep the layout they started with. */
|
|
2760
|
+
const NEW_STREAM_LAYOUT = SEGMENT_LAYOUT;
|
|
2658
2761
|
/** One storage get or put call accepts at most 128 keys. */
|
|
2659
2762
|
const STORAGE_BATCH_KEYS = 128;
|
|
2660
2763
|
function boundedLimit(value, fallback, maximum) {
|
|
@@ -2675,7 +2778,7 @@ function validateMeta(meta) {
|
|
|
2675
2778
|
"closed",
|
|
2676
2779
|
"errored",
|
|
2677
2780
|
"expired"
|
|
2678
|
-
].includes(candidate.state) || candidate.ownerRunId !== void 0 && typeof candidate.ownerRunId !== "string" || candidate.payloadDeleted !== void 0 && typeof candidate.payloadDeleted !== "boolean") throw new Error("Invalid persisted stream metadata");
|
|
2781
|
+
].includes(candidate.state) || candidate.ownerRunId !== void 0 && typeof candidate.ownerRunId !== "string" || candidate.payloadDeleted !== void 0 && typeof candidate.payloadDeleted !== "boolean" || candidate.layout !== void 0 && candidate.layout !== SEGMENT_LAYOUT) throw new Error("Invalid persisted stream metadata");
|
|
2679
2782
|
const error = candidate.error;
|
|
2680
2783
|
if (error !== void 0 && (!error || typeof error !== "object" || typeof error.name !== "string" || typeof error.message !== "string") || candidate.state === "errored" !== (error !== void 0)) throw new Error("Invalid persisted stream error metadata");
|
|
2681
2784
|
return candidate;
|
|
@@ -2687,6 +2790,80 @@ function chunkKey(index) {
|
|
|
2687
2790
|
function chunkSizeKey(index) {
|
|
2688
2791
|
return `${CHUNK_SIZE_KEY_PREFIX}${index.toString().padStart(12, "0")}`;
|
|
2689
2792
|
}
|
|
2793
|
+
function segmentKey(start) {
|
|
2794
|
+
return `${SEGMENT_KEY_PREFIX}${start.toString().padStart(12, "0")}`;
|
|
2795
|
+
}
|
|
2796
|
+
function segmentSizeKey(start) {
|
|
2797
|
+
return `${SEGMENT_SIZE_KEY_PREFIX}${start.toString().padStart(12, "0")}`;
|
|
2798
|
+
}
|
|
2799
|
+
function startFromSegmentKey(key, prefix) {
|
|
2800
|
+
const suffix = key.slice(prefix.length);
|
|
2801
|
+
const start = Number(suffix);
|
|
2802
|
+
if (!key.startsWith(prefix) || !/^\d{12}$/.test(suffix) || !Number.isSafeInteger(start) || start > MAX_STREAM_INDEX) throw new Error(`Invalid persisted stream segment key "${key}"`);
|
|
2803
|
+
return start;
|
|
2804
|
+
}
|
|
2805
|
+
function validateSegmentSize(value, start) {
|
|
2806
|
+
const size = value;
|
|
2807
|
+
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}`);
|
|
2808
|
+
return size;
|
|
2809
|
+
}
|
|
2810
|
+
/** Pack chunks as `[u32 count][u32 length]*count` followed by the payloads. */
|
|
2811
|
+
function encodeSegment(chunks) {
|
|
2812
|
+
const payloadBytes = chunks.reduce((total, chunk) => total + chunk.byteLength, 0);
|
|
2813
|
+
const header = 4 + 4 * chunks.length;
|
|
2814
|
+
const encoded = new Uint8Array(header + payloadBytes);
|
|
2815
|
+
const view = new DataView(encoded.buffer);
|
|
2816
|
+
view.setUint32(0, chunks.length);
|
|
2817
|
+
let offset = header;
|
|
2818
|
+
for (let index = 0; index < chunks.length; index++) {
|
|
2819
|
+
view.setUint32(4 + 4 * index, chunks[index].byteLength);
|
|
2820
|
+
encoded.set(chunks[index], offset);
|
|
2821
|
+
offset += chunks[index].byteLength;
|
|
2822
|
+
}
|
|
2823
|
+
return encoded;
|
|
2824
|
+
}
|
|
2825
|
+
function decodeSegment(value, start, size) {
|
|
2826
|
+
if (!(value instanceof Uint8Array) || value.byteLength < 4) throw new Error(`Invalid persisted stream segment at index ${start}`);
|
|
2827
|
+
const view = new DataView(value.buffer, value.byteOffset, value.byteLength);
|
|
2828
|
+
const count = view.getUint32(0);
|
|
2829
|
+
const header = 4 + 4 * count;
|
|
2830
|
+
if (count !== size.count || header + size.bytes !== value.byteLength) throw new Error(`Invalid persisted stream segment at index ${start}`);
|
|
2831
|
+
const chunks = [];
|
|
2832
|
+
let offset = header;
|
|
2833
|
+
for (let index = 0; index < count; index++) {
|
|
2834
|
+
const length = view.getUint32(4 + 4 * index);
|
|
2835
|
+
if (offset + length > value.byteLength) throw new Error(`Invalid persisted stream segment at index ${start}`);
|
|
2836
|
+
chunks.push(value.subarray(offset, offset + length));
|
|
2837
|
+
offset += length;
|
|
2838
|
+
}
|
|
2839
|
+
if (offset !== value.byteLength) throw new Error(`Invalid persisted stream segment at index ${start}`);
|
|
2840
|
+
return chunks;
|
|
2841
|
+
}
|
|
2842
|
+
/** Group one write's chunks into segments of at most MAX_SEGMENT_BYTES payload bytes. */
|
|
2843
|
+
function packSegments(startIndex, chunks) {
|
|
2844
|
+
const segments = [];
|
|
2845
|
+
let current = [];
|
|
2846
|
+
let bytes = 0;
|
|
2847
|
+
let start = startIndex;
|
|
2848
|
+
for (const chunk of chunks) {
|
|
2849
|
+
if (current.length > 0 && bytes + chunk.byteLength > MAX_SEGMENT_BYTES) {
|
|
2850
|
+
segments.push({
|
|
2851
|
+
start,
|
|
2852
|
+
chunks: current
|
|
2853
|
+
});
|
|
2854
|
+
start += current.length;
|
|
2855
|
+
current = [];
|
|
2856
|
+
bytes = 0;
|
|
2857
|
+
}
|
|
2858
|
+
current.push(chunk);
|
|
2859
|
+
bytes += chunk.byteLength;
|
|
2860
|
+
}
|
|
2861
|
+
if (current.length > 0) segments.push({
|
|
2862
|
+
start,
|
|
2863
|
+
chunks: current
|
|
2864
|
+
});
|
|
2865
|
+
return segments;
|
|
2866
|
+
}
|
|
2690
2867
|
function indexFromChunkSizeKey(key) {
|
|
2691
2868
|
const suffix = key.slice(11);
|
|
2692
2869
|
const index = Number(suffix);
|
|
@@ -2703,6 +2880,9 @@ function validateChunkSize(size, index) {
|
|
|
2703
2880
|
if (typeof size !== "number" || !Number.isSafeInteger(size) || size < 0 || size > 1048576) throw new Error(`Invalid persisted stream chunk size at index ${index}`);
|
|
2704
2881
|
return size;
|
|
2705
2882
|
}
|
|
2883
|
+
async function deleteInBatches(txn, keys) {
|
|
2884
|
+
for (let offset = 0; offset < keys.length; offset += DELETE_BATCH_KEYS) await txn.delete(keys.slice(offset, offset + DELETE_BATCH_KEYS));
|
|
2885
|
+
}
|
|
2706
2886
|
function compactChunk(chunk) {
|
|
2707
2887
|
return chunk.byteOffset === 0 && chunk.byteLength === chunk.buffer.byteLength ? chunk : new Uint8Array(chunk);
|
|
2708
2888
|
}
|
|
@@ -2799,29 +2979,8 @@ var StreamDO = class extends DurableObject {
|
|
|
2799
2979
|
const meta = await this.getMeta();
|
|
2800
2980
|
this.assertOwner(meta, request.runId);
|
|
2801
2981
|
const available = meta.state === "expired" ? 0 : Math.max(0, Math.min(request.maxChunks, meta.count - request.startIndex));
|
|
2802
|
-
|
|
2803
|
-
if (available > 0)
|
|
2804
|
-
const indexes = Array.from({ length: available }, (_, offset) => request.startIndex + offset);
|
|
2805
|
-
const sizeKeys = indexes.map(chunkSizeKey);
|
|
2806
|
-
const storedSizes = await this.getMany(sizeKeys);
|
|
2807
|
-
const selectedIndexes = [];
|
|
2808
|
-
const selectedSizes = [];
|
|
2809
|
-
let bytes = 0;
|
|
2810
|
-
for (const index of indexes) {
|
|
2811
|
-
const size = validateChunkSize(storedSizes.get(chunkSizeKey(index)), index);
|
|
2812
|
-
if (bytes + size > request.maxBytes) break;
|
|
2813
|
-
selectedIndexes.push(index);
|
|
2814
|
-
selectedSizes.push(size);
|
|
2815
|
-
bytes += size;
|
|
2816
|
-
}
|
|
2817
|
-
const payloadKeys = selectedIndexes.map(chunkKey);
|
|
2818
|
-
const storedChunks = await this.getMany(payloadKeys);
|
|
2819
|
-
for (let offset = 0; offset < payloadKeys.length; offset++) {
|
|
2820
|
-
const chunk = storedChunks.get(payloadKeys[offset]);
|
|
2821
|
-
if (!(chunk instanceof Uint8Array) || chunk.byteLength !== selectedSizes[offset]) throw new Error(`Invalid persisted stream chunk at index ${selectedIndexes[offset]}`);
|
|
2822
|
-
chunks.push(chunk);
|
|
2823
|
-
}
|
|
2824
|
-
}
|
|
2982
|
+
let chunks = [];
|
|
2983
|
+
if (available > 0) chunks = meta.layout === SEGMENT_LAYOUT ? await this.readSegmentRows(request.startIndex, available, request.maxBytes) : await this.readChunkRows(request.startIndex, available, request.maxBytes);
|
|
2825
2984
|
return {
|
|
2826
2985
|
startIndex: request.startIndex,
|
|
2827
2986
|
tailIndex: meta.count - 1,
|
|
@@ -2831,10 +2990,91 @@ var StreamDO = class extends DurableObject {
|
|
|
2831
2990
|
error: meta.error
|
|
2832
2991
|
};
|
|
2833
2992
|
}
|
|
2993
|
+
async readChunkRows(startIndex, available, maxBytes) {
|
|
2994
|
+
const indexes = Array.from({ length: available }, (_, offset) => startIndex + offset);
|
|
2995
|
+
const sizeKeys = indexes.map(chunkSizeKey);
|
|
2996
|
+
const storedSizes = await this.getMany(sizeKeys);
|
|
2997
|
+
const selectedIndexes = [];
|
|
2998
|
+
const selectedSizes = [];
|
|
2999
|
+
let bytes = 0;
|
|
3000
|
+
for (const index of indexes) {
|
|
3001
|
+
const size = validateChunkSize(storedSizes.get(chunkSizeKey(index)), index);
|
|
3002
|
+
if (bytes + size > maxBytes) break;
|
|
3003
|
+
selectedIndexes.push(index);
|
|
3004
|
+
selectedSizes.push(size);
|
|
3005
|
+
bytes += size;
|
|
3006
|
+
}
|
|
3007
|
+
const payloadKeys = selectedIndexes.map(chunkKey);
|
|
3008
|
+
const storedChunks = await this.getMany(payloadKeys);
|
|
3009
|
+
const chunks = [];
|
|
3010
|
+
for (let offset = 0; offset < payloadKeys.length; offset++) {
|
|
3011
|
+
const chunk = storedChunks.get(payloadKeys[offset]);
|
|
3012
|
+
if (!(chunk instanceof Uint8Array) || chunk.byteLength !== selectedSizes[offset]) throw new Error(`Invalid persisted stream chunk at index ${selectedIndexes[offset]}`);
|
|
3013
|
+
chunks.push(chunk);
|
|
3014
|
+
}
|
|
3015
|
+
return chunks;
|
|
3016
|
+
}
|
|
3017
|
+
/**
|
|
3018
|
+
* Plan from the small segment size rows, then load only the segments that
|
|
3019
|
+
* cover the requested range. A read that starts on a segment boundary (the
|
|
3020
|
+
* common sequential case) needs one list and one multi-get.
|
|
3021
|
+
*/
|
|
3022
|
+
async readSegmentRows(startIndex, available, maxBytes) {
|
|
3023
|
+
const endIndex = startIndex + available;
|
|
3024
|
+
const listed = await this.ctx.storage.list({
|
|
3025
|
+
prefix: SEGMENT_SIZE_KEY_PREFIX,
|
|
3026
|
+
start: segmentSizeKey(startIndex),
|
|
3027
|
+
end: segmentSizeKey(endIndex),
|
|
3028
|
+
limit: available
|
|
3029
|
+
});
|
|
3030
|
+
const planned = [];
|
|
3031
|
+
for (const [key, value] of listed) {
|
|
3032
|
+
const start = startFromSegmentKey(key, SEGMENT_SIZE_KEY_PREFIX);
|
|
3033
|
+
planned.push({
|
|
3034
|
+
start,
|
|
3035
|
+
size: validateSegmentSize(value, start)
|
|
3036
|
+
});
|
|
3037
|
+
}
|
|
3038
|
+
if (planned[0]?.start !== startIndex) {
|
|
3039
|
+
const entry = (await this.ctx.storage.list({
|
|
3040
|
+
prefix: SEGMENT_SIZE_KEY_PREFIX,
|
|
3041
|
+
end: segmentSizeKey(startIndex + 1),
|
|
3042
|
+
reverse: true,
|
|
3043
|
+
limit: 1
|
|
3044
|
+
})).entries().next().value;
|
|
3045
|
+
if (!entry) throw new Error(`Missing persisted stream segment at index ${startIndex}`);
|
|
3046
|
+
const start = startFromSegmentKey(entry[0], SEGMENT_SIZE_KEY_PREFIX);
|
|
3047
|
+
planned.unshift({
|
|
3048
|
+
start,
|
|
3049
|
+
size: validateSegmentSize(entry[1], start)
|
|
3050
|
+
});
|
|
3051
|
+
}
|
|
3052
|
+
const selected = [];
|
|
3053
|
+
let plannedBytes = 0;
|
|
3054
|
+
for (const segment of planned) {
|
|
3055
|
+
if (selected.length > 0 && plannedBytes >= maxBytes) break;
|
|
3056
|
+
selected.push(segment);
|
|
3057
|
+
plannedBytes += segment.size.bytes;
|
|
3058
|
+
}
|
|
3059
|
+
const values = await this.getMany(selected.map(({ start }) => segmentKey(start)));
|
|
3060
|
+
const chunks = [];
|
|
3061
|
+
let bytes = 0;
|
|
3062
|
+
let expected = startIndex;
|
|
3063
|
+
for (const { start, size } of selected) {
|
|
3064
|
+
if (start > expected) throw new Error(`Missing persisted stream segment at index ${expected}`);
|
|
3065
|
+
const segment = decodeSegment(values.get(segmentKey(start)), start, size);
|
|
3066
|
+
for (let offset = expected - start; offset < segment.length; offset++) {
|
|
3067
|
+
if (expected >= endIndex || bytes + segment[offset].byteLength > maxBytes) return chunks;
|
|
3068
|
+
chunks.push(segment[offset]);
|
|
3069
|
+
bytes += segment[offset].byteLength;
|
|
3070
|
+
expected++;
|
|
3071
|
+
}
|
|
3072
|
+
}
|
|
3073
|
+
return chunks;
|
|
3074
|
+
}
|
|
2834
3075
|
/** Append one bounded binary batch with contiguous indexes. */
|
|
2835
3076
|
async writeChunks(runId, chunks) {
|
|
2836
3077
|
validateStreamWriteChunks(chunks);
|
|
2837
|
-
const storedChunks = chunks.map(compactChunk);
|
|
2838
3078
|
return await this.runMutation(async () => {
|
|
2839
3079
|
const committed = await this.ctx.storage.transaction(async (txn) => {
|
|
2840
3080
|
const stored = await txn.get(META_KEY);
|
|
@@ -2842,16 +3082,28 @@ var StreamDO = class extends DurableObject {
|
|
|
2842
3082
|
this.assertWritable(meta, runId);
|
|
2843
3083
|
const startIndex = meta.count;
|
|
2844
3084
|
if (startIndex + chunks.length > MAX_STREAM_INDEX) throw new Error("Stream offset limit exceeded");
|
|
3085
|
+
const layout = stored === void 0 ? NEW_STREAM_LAYOUT : meta.layout;
|
|
2845
3086
|
const nextMeta = {
|
|
2846
3087
|
count: startIndex + chunks.length,
|
|
2847
3088
|
state: "open",
|
|
2848
|
-
ownerRunId: runId
|
|
3089
|
+
ownerRunId: runId,
|
|
3090
|
+
...layout === void 0 ? {} : { layout }
|
|
2849
3091
|
};
|
|
2850
3092
|
const entries = [[META_KEY, nextMeta]];
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
entries.push([
|
|
2854
|
-
|
|
3093
|
+
if (layout === SEGMENT_LAYOUT) for (const segment of packSegments(startIndex, chunks)) {
|
|
3094
|
+
entries.push([segmentKey(segment.start), encodeSegment(segment.chunks)]);
|
|
3095
|
+
entries.push([segmentSizeKey(segment.start), {
|
|
3096
|
+
count: segment.chunks.length,
|
|
3097
|
+
bytes: segment.chunks.reduce((total, chunk) => total + chunk.byteLength, 0)
|
|
3098
|
+
}]);
|
|
3099
|
+
}
|
|
3100
|
+
else {
|
|
3101
|
+
const storedChunks = chunks.map(compactChunk);
|
|
3102
|
+
for (let offset = 0; offset < storedChunks.length; offset++) {
|
|
3103
|
+
const index = startIndex + offset;
|
|
3104
|
+
entries.push([chunkKey(index), storedChunks[offset]]);
|
|
3105
|
+
entries.push([chunkSizeKey(index), storedChunks[offset].byteLength]);
|
|
3106
|
+
}
|
|
2855
3107
|
}
|
|
2856
3108
|
for (let offset = 0; offset < entries.length; offset += STORAGE_BATCH_KEYS) await txn.put(Object.fromEntries(entries.slice(offset, offset + STORAGE_BATCH_KEYS)));
|
|
2857
3109
|
return {
|
|
@@ -2987,6 +3239,102 @@ var StreamDO = class extends DurableObject {
|
|
|
2987
3239
|
};
|
|
2988
3240
|
});
|
|
2989
3241
|
}
|
|
3242
|
+
/** Delete one page of per-chunk rows, listing size rows to avoid loading payloads. */
|
|
3243
|
+
async expireChunkPage(txn, limit, byteLimit) {
|
|
3244
|
+
const candidates = await txn.list({
|
|
3245
|
+
prefix: CHUNK_SIZE_KEY_PREFIX,
|
|
3246
|
+
limit: limit + 1
|
|
3247
|
+
});
|
|
3248
|
+
const page = [];
|
|
3249
|
+
let bytes = 0;
|
|
3250
|
+
for (const [key, storedSize] of Array.from(candidates).slice(0, limit)) {
|
|
3251
|
+
const index = indexFromChunkSizeKey(key);
|
|
3252
|
+
const size = validateChunkSize(storedSize, index);
|
|
3253
|
+
if (page.length > 0 && bytes + size > byteLimit) break;
|
|
3254
|
+
page.push({
|
|
3255
|
+
index,
|
|
3256
|
+
size
|
|
3257
|
+
});
|
|
3258
|
+
bytes += size;
|
|
3259
|
+
}
|
|
3260
|
+
let payloadFallback = false;
|
|
3261
|
+
if (candidates.size === 0) {
|
|
3262
|
+
const first = (await txn.list({
|
|
3263
|
+
prefix: CHUNK_KEY_PREFIX,
|
|
3264
|
+
limit: 1
|
|
3265
|
+
})).entries().next().value;
|
|
3266
|
+
if (first) {
|
|
3267
|
+
const [key, payload] = first;
|
|
3268
|
+
page.push({
|
|
3269
|
+
index: indexFromChunkKey(key),
|
|
3270
|
+
size: payload instanceof Uint8Array ? payload.byteLength : 0
|
|
3271
|
+
});
|
|
3272
|
+
bytes = page[0].size;
|
|
3273
|
+
payloadFallback = true;
|
|
3274
|
+
}
|
|
3275
|
+
}
|
|
3276
|
+
await deleteInBatches(txn, page.flatMap(({ index }) => [chunkKey(index), chunkSizeKey(index)]));
|
|
3277
|
+
let done = candidates.size === page.length;
|
|
3278
|
+
if (payloadFallback) done = (await txn.list({
|
|
3279
|
+
prefix: CHUNK_KEY_PREFIX,
|
|
3280
|
+
limit: 1
|
|
3281
|
+
})).size === 0;
|
|
3282
|
+
else if (done && candidates.size > 0) done = (await txn.list({
|
|
3283
|
+
prefix: CHUNK_KEY_PREFIX,
|
|
3284
|
+
limit: 1
|
|
3285
|
+
})).size === 0;
|
|
3286
|
+
return {
|
|
3287
|
+
chunks: page.length,
|
|
3288
|
+
bytes,
|
|
3289
|
+
done
|
|
3290
|
+
};
|
|
3291
|
+
}
|
|
3292
|
+
/**
|
|
3293
|
+
* Delete one page of segment rows, listing segment size rows to avoid
|
|
3294
|
+
* loading payloads. One segment always makes progress, even when it holds
|
|
3295
|
+
* more chunks or bytes than the page allows.
|
|
3296
|
+
*/
|
|
3297
|
+
async expireSegmentPage(txn, limit, byteLimit) {
|
|
3298
|
+
const candidates = await txn.list({
|
|
3299
|
+
prefix: SEGMENT_SIZE_KEY_PREFIX,
|
|
3300
|
+
limit: limit + 1
|
|
3301
|
+
});
|
|
3302
|
+
const page = [];
|
|
3303
|
+
let chunks = 0;
|
|
3304
|
+
let bytes = 0;
|
|
3305
|
+
for (const [key, value] of candidates) {
|
|
3306
|
+
const start = startFromSegmentKey(key, SEGMENT_SIZE_KEY_PREFIX);
|
|
3307
|
+
const size = validateSegmentSize(value, start);
|
|
3308
|
+
if (page.length > 0 && (chunks + size.count > limit || bytes + size.bytes > byteLimit)) break;
|
|
3309
|
+
page.push(start);
|
|
3310
|
+
chunks += size.count;
|
|
3311
|
+
bytes += size.bytes;
|
|
3312
|
+
}
|
|
3313
|
+
let orphan = false;
|
|
3314
|
+
if (candidates.size === 0) {
|
|
3315
|
+
const first = (await txn.list({
|
|
3316
|
+
prefix: SEGMENT_KEY_PREFIX,
|
|
3317
|
+
limit: 1
|
|
3318
|
+
})).entries().next().value;
|
|
3319
|
+
if (first) {
|
|
3320
|
+
page.push(startFromSegmentKey(first[0], SEGMENT_KEY_PREFIX));
|
|
3321
|
+
bytes = first[1] instanceof Uint8Array ? first[1].byteLength : 0;
|
|
3322
|
+
chunks = 1;
|
|
3323
|
+
orphan = true;
|
|
3324
|
+
}
|
|
3325
|
+
}
|
|
3326
|
+
await deleteInBatches(txn, page.flatMap((start) => [segmentKey(start), segmentSizeKey(start)]));
|
|
3327
|
+
let done = !orphan && candidates.size === page.length;
|
|
3328
|
+
if (orphan || done) done = (await txn.list({
|
|
3329
|
+
prefix: SEGMENT_KEY_PREFIX,
|
|
3330
|
+
limit: 1
|
|
3331
|
+
})).size === 0;
|
|
3332
|
+
return {
|
|
3333
|
+
chunks,
|
|
3334
|
+
bytes,
|
|
3335
|
+
done
|
|
3336
|
+
};
|
|
3337
|
+
}
|
|
2990
3338
|
/**
|
|
2991
3339
|
* Fence the stream and delete one bounded page. Listing size keys avoids
|
|
2992
3340
|
* loading payloads, and the transaction makes a crash retry resume safely.
|
|
@@ -3000,50 +3348,8 @@ var StreamDO = class extends DurableObject {
|
|
|
3000
3348
|
const meta = stored === void 0 ? emptyMeta() : validateMeta(stored);
|
|
3001
3349
|
this.assertOwner(meta, runId);
|
|
3002
3350
|
const firstFence = meta.state !== "expired";
|
|
3003
|
-
const
|
|
3004
|
-
|
|
3005
|
-
limit: limit + 1
|
|
3006
|
-
});
|
|
3007
|
-
const page = [];
|
|
3008
|
-
let bytes = 0;
|
|
3009
|
-
for (const [key, storedSize] of Array.from(candidates).slice(0, limit)) {
|
|
3010
|
-
const index = indexFromChunkSizeKey(key);
|
|
3011
|
-
const size = validateChunkSize(storedSize, index);
|
|
3012
|
-
if (page.length > 0 && bytes + size > byteLimit) break;
|
|
3013
|
-
page.push({
|
|
3014
|
-
index,
|
|
3015
|
-
size
|
|
3016
|
-
});
|
|
3017
|
-
bytes += size;
|
|
3018
|
-
}
|
|
3019
|
-
let payloadFallback = false;
|
|
3020
|
-
if (candidates.size === 0) {
|
|
3021
|
-
const first = (await txn.list({
|
|
3022
|
-
prefix: CHUNK_KEY_PREFIX,
|
|
3023
|
-
limit: 1
|
|
3024
|
-
})).entries().next().value;
|
|
3025
|
-
if (first) {
|
|
3026
|
-
const [key, payload] = first;
|
|
3027
|
-
page.push({
|
|
3028
|
-
index: indexFromChunkKey(key),
|
|
3029
|
-
size: payload instanceof Uint8Array ? payload.byteLength : 0
|
|
3030
|
-
});
|
|
3031
|
-
bytes = page[0].size;
|
|
3032
|
-
payloadFallback = true;
|
|
3033
|
-
}
|
|
3034
|
-
}
|
|
3035
|
-
const keys = page.flatMap(({ index }) => [chunkKey(index), chunkSizeKey(index)]);
|
|
3036
|
-
for (let offset = 0; offset < keys.length; offset += DELETE_BATCH_KEYS) await txn.delete(keys.slice(offset, offset + DELETE_BATCH_KEYS));
|
|
3037
|
-
let done = candidates.size === page.length;
|
|
3038
|
-
if (payloadFallback) done = (await txn.list({
|
|
3039
|
-
prefix: CHUNK_KEY_PREFIX,
|
|
3040
|
-
limit: 1
|
|
3041
|
-
})).size === 0;
|
|
3042
|
-
else if (done && candidates.size > 0) done = (await txn.list({
|
|
3043
|
-
prefix: CHUNK_KEY_PREFIX,
|
|
3044
|
-
limit: 1
|
|
3045
|
-
})).size === 0;
|
|
3046
|
-
if (done && page.length === 0 && meta.state === "expired" && meta.payloadDeleted) return {
|
|
3351
|
+
const { chunks, bytes, done } = meta.layout === SEGMENT_LAYOUT ? await this.expireSegmentPage(txn, limit, byteLimit) : await this.expireChunkPage(txn, limit, byteLimit);
|
|
3352
|
+
if (done && chunks === 0 && meta.state === "expired" && meta.payloadDeleted) return {
|
|
3047
3353
|
meta,
|
|
3048
3354
|
result: {
|
|
3049
3355
|
deleted: false,
|
|
@@ -3067,7 +3373,7 @@ var StreamDO = class extends DurableObject {
|
|
|
3067
3373
|
meta: nextMeta,
|
|
3068
3374
|
result: {
|
|
3069
3375
|
deleted: firstFence,
|
|
3070
|
-
chunks
|
|
3376
|
+
chunks,
|
|
3071
3377
|
bytes,
|
|
3072
3378
|
done
|
|
3073
3379
|
}
|
|
@@ -3152,6 +3458,14 @@ const CLEANUP_RETRY_MAX_MS = 36e5;
|
|
|
3152
3458
|
const MAX_DATE_MS = 864e13;
|
|
3153
3459
|
const QUEUE_ORPHAN_KEY = "queue-orphan";
|
|
3154
3460
|
const QUEUE_RESERVATION_KEY = "queue-reservation";
|
|
3461
|
+
/** Run-scoped Queue idempotency state, one key per claim name, in the run's own cell. */
|
|
3462
|
+
const RUN_QUEUE_CLAIM_PREFIX = "queue-claim:";
|
|
3463
|
+
/**
|
|
3464
|
+
* Present when every idempotency claim of this run lives in the run's own
|
|
3465
|
+
* cell. Written only when the run is created, so a run keeps one claim scope
|
|
3466
|
+
* for its whole life and claims made before the marker existed stay valid.
|
|
3467
|
+
*/
|
|
3468
|
+
const RUN_QUEUE_CLAIM_SCOPE_KEY = "queue-claim-scope";
|
|
3155
3469
|
function validateCleanupRequest(request, allowDisabled) {
|
|
3156
3470
|
if (!isRecord$1(request)) throw new Error("world-celld retention request must be an object");
|
|
3157
3471
|
if (!(allowDisabled ? isNonNegativeSafeInteger(request.retentionMs) : isPositiveSafeInteger(request.retentionMs))) throw new Error(`world-celld retentionMs must be a ${allowDisabled ? "non-negative" : "positive"} safe integer`);
|
|
@@ -3205,6 +3519,12 @@ async function putStorageEntries(storage, entries) {
|
|
|
3205
3519
|
const all = Array.from(entries);
|
|
3206
3520
|
for (let offset = 0; offset < all.length; offset += STORAGE_BATCH_SIZE) await storage.put(Object.fromEntries(all.slice(offset, offset + STORAGE_BATCH_SIZE)));
|
|
3207
3521
|
}
|
|
3522
|
+
function runQueueClaimKey(claimName) {
|
|
3523
|
+
return `${RUN_QUEUE_CLAIM_PREFIX}${claimName}`;
|
|
3524
|
+
}
|
|
3525
|
+
function validRunQueueClaimName(claimName) {
|
|
3526
|
+
return typeof claimName === "string" && claimName.startsWith("claim:");
|
|
3527
|
+
}
|
|
3208
3528
|
/**
|
|
3209
3529
|
* Durable Object for managing a single workflow run's state.
|
|
3210
3530
|
*
|
|
@@ -3285,6 +3605,7 @@ var WorkflowRunDO = class extends DurableObject {
|
|
|
3285
3605
|
});
|
|
3286
3606
|
if (!outcome.ok) return outcome;
|
|
3287
3607
|
if (eventSequence !== storedSequence) await txn.put(EVENT_SEQUENCE_KEY, eventSequence);
|
|
3608
|
+
if (outcome.runCreated) await txn.put(RUN_QUEUE_CLAIM_SCOPE_KEY, "run");
|
|
3288
3609
|
await putStorageEntries(txn, (outcome.hookToIndex ? [{
|
|
3289
3610
|
hookId: outcome.hookToIndex.hookId,
|
|
3290
3611
|
token: outcome.hookToIndex.token
|
|
@@ -3951,6 +4272,150 @@ var WorkflowRunDO = class extends DurableObject {
|
|
|
3951
4272
|
await txn.setAlarm(this.now() + delay);
|
|
3952
4273
|
});
|
|
3953
4274
|
}
|
|
4275
|
+
/**
|
|
4276
|
+
* Read-only admission for a run-bearing Queue message whose body travels
|
|
4277
|
+
* inline in the broker envelope. It answers what payload registration does
|
|
4278
|
+
* for offloaded bodies, without writing anything.
|
|
4279
|
+
*/
|
|
4280
|
+
async getQueueAdmission() {
|
|
4281
|
+
const { tombstone } = await this.retentionState(this.ctx.storage);
|
|
4282
|
+
return tombstone ? {
|
|
4283
|
+
ok: false,
|
|
4284
|
+
message: `Workflow run "${tombstone.runId}" expired at ${tombstone.expiredAt.toISOString()}`
|
|
4285
|
+
} : { ok: true };
|
|
4286
|
+
}
|
|
4287
|
+
/**
|
|
4288
|
+
* Enqueue-side admission and idempotency reservation in one transaction,
|
|
4289
|
+
* for a run-scoped claim. Replaces a separate claim cell's reservation plus
|
|
4290
|
+
* the run expiry check.
|
|
4291
|
+
*/
|
|
4292
|
+
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");
|
|
4294
|
+
return await this.ctx.storage.transaction(async (txn) => {
|
|
4295
|
+
const key = runQueueClaimKey(params.claimName);
|
|
4296
|
+
const values = await txn.get([
|
|
4297
|
+
TOMBSTONE_KEY,
|
|
4298
|
+
CLEANUP_RECORD_KEY,
|
|
4299
|
+
RUN_QUEUE_CLAIM_SCOPE_KEY,
|
|
4300
|
+
key
|
|
4301
|
+
]);
|
|
4302
|
+
const tombstone = this.tombstoneFrom(values);
|
|
4303
|
+
if (tombstone) return {
|
|
4304
|
+
ok: false,
|
|
4305
|
+
message: `Workflow run "${tombstone.runId}" expired at ${tombstone.expiredAt.toISOString()}`
|
|
4306
|
+
};
|
|
4307
|
+
if (values.get("queue-claim-scope") !== "run") return {
|
|
4308
|
+
ok: true,
|
|
4309
|
+
scope: "cell"
|
|
4310
|
+
};
|
|
4311
|
+
const state = values.get(key) ?? {};
|
|
4312
|
+
const existing = state.reservation;
|
|
4313
|
+
if (existing && existing.expiresAt > this.now() && existing.messageId !== params.messageId) return {
|
|
4314
|
+
ok: true,
|
|
4315
|
+
scope: "run",
|
|
4316
|
+
admitted: false,
|
|
4317
|
+
messageId: existing.messageId
|
|
4318
|
+
};
|
|
4319
|
+
await txn.put(key, {
|
|
4320
|
+
...state,
|
|
4321
|
+
reservation: {
|
|
4322
|
+
messageId: params.messageId,
|
|
4323
|
+
expiresAt: Math.max(existing?.expiresAt ?? 0, params.expiresAt)
|
|
4324
|
+
}
|
|
4325
|
+
});
|
|
4326
|
+
return {
|
|
4327
|
+
ok: true,
|
|
4328
|
+
scope: "run",
|
|
4329
|
+
admitted: true,
|
|
4330
|
+
messageId: params.messageId
|
|
4331
|
+
};
|
|
4332
|
+
});
|
|
4333
|
+
}
|
|
4334
|
+
/** Delivery-side run expiry check and in-flight claim in one transaction. */
|
|
4335
|
+
async claimRunQueueMessage(params) {
|
|
4336
|
+
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");
|
|
4337
|
+
return await this.ctx.storage.transaction(async (txn) => {
|
|
4338
|
+
const key = runQueueClaimKey(params.claimName);
|
|
4339
|
+
const values = await txn.get([
|
|
4340
|
+
TOMBSTONE_KEY,
|
|
4341
|
+
CLEANUP_RECORD_KEY,
|
|
4342
|
+
key
|
|
4343
|
+
]);
|
|
4344
|
+
if (this.tombstoneFrom(values)) return { expired: true };
|
|
4345
|
+
const state = values.get(key) ?? {};
|
|
4346
|
+
const existing = state.claim;
|
|
4347
|
+
const now = this.now();
|
|
4348
|
+
if (existing && existing.expiresAt > now) {
|
|
4349
|
+
if (existing.messageId !== params.messageId) return {
|
|
4350
|
+
expired: false,
|
|
4351
|
+
claimed: false
|
|
4352
|
+
};
|
|
4353
|
+
if (existing.retryAt === void 0 || existing.retryAt > now) return {
|
|
4354
|
+
expired: false,
|
|
4355
|
+
claimed: false,
|
|
4356
|
+
retryAt: existing.retryAt ?? existing.expiresAt
|
|
4357
|
+
};
|
|
4358
|
+
}
|
|
4359
|
+
await txn.put(key, {
|
|
4360
|
+
...state,
|
|
4361
|
+
claim: {
|
|
4362
|
+
messageId: params.messageId,
|
|
4363
|
+
expiresAt: now + params.staleMs
|
|
4364
|
+
}
|
|
4365
|
+
});
|
|
4366
|
+
return {
|
|
4367
|
+
expired: false,
|
|
4368
|
+
claimed: true
|
|
4369
|
+
};
|
|
4370
|
+
});
|
|
4371
|
+
}
|
|
4372
|
+
async holdRunQueueMessage(params) {
|
|
4373
|
+
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");
|
|
4374
|
+
return await this.ctx.storage.transaction(async (txn) => {
|
|
4375
|
+
const key = runQueueClaimKey(params.claimName);
|
|
4376
|
+
const state = await txn.get(key) ?? {};
|
|
4377
|
+
const existing = state.claim;
|
|
4378
|
+
if (!existing || existing.messageId !== params.messageId) return { held: false };
|
|
4379
|
+
const next = {
|
|
4380
|
+
...state,
|
|
4381
|
+
claim: {
|
|
4382
|
+
...existing,
|
|
4383
|
+
retryAt: params.retryAt,
|
|
4384
|
+
expiresAt: Math.max(existing.expiresAt, params.expiresAt)
|
|
4385
|
+
}
|
|
4386
|
+
};
|
|
4387
|
+
if (state.reservation?.messageId === params.messageId && params.reservationExpiresAt !== void 0 && params.reservationExpiresAt > state.reservation.expiresAt) next.reservation = {
|
|
4388
|
+
...state.reservation,
|
|
4389
|
+
expiresAt: params.reservationExpiresAt
|
|
4390
|
+
};
|
|
4391
|
+
await txn.put(key, next);
|
|
4392
|
+
return { held: true };
|
|
4393
|
+
});
|
|
4394
|
+
}
|
|
4395
|
+
async releaseRunQueueMessage(params) {
|
|
4396
|
+
if (!validRunQueueClaimName(params?.claimName) || typeof params.messageId !== "string") throw new TypeError("world-celld run queue claim release is invalid");
|
|
4397
|
+
await this.ctx.storage.transaction(async (txn) => {
|
|
4398
|
+
const key = runQueueClaimKey(params.claimName);
|
|
4399
|
+
const state = await txn.get(key);
|
|
4400
|
+
if (state?.claim?.messageId !== params.messageId) return;
|
|
4401
|
+
const { claim: _released, ...rest } = state;
|
|
4402
|
+
if (rest.reservation) await txn.put(key, rest);
|
|
4403
|
+
else await txn.delete(key);
|
|
4404
|
+
});
|
|
4405
|
+
}
|
|
4406
|
+
async completeRunQueueMessage(params) {
|
|
4407
|
+
if (!validRunQueueClaimName(params?.claimName) || typeof params.messageId !== "string") throw new TypeError("world-celld run queue completion is invalid");
|
|
4408
|
+
await this.ctx.storage.transaction(async (txn) => {
|
|
4409
|
+
const key = runQueueClaimKey(params.claimName);
|
|
4410
|
+
const state = await txn.get(key);
|
|
4411
|
+
if (!state) return;
|
|
4412
|
+
const next = {};
|
|
4413
|
+
if (state.reservation && state.reservation.messageId !== params.messageId) next.reservation = state.reservation;
|
|
4414
|
+
if (state.claim && state.claim.messageId !== params.messageId) next.claim = state.claim;
|
|
4415
|
+
if (next.reservation || next.claim) await txn.put(key, next);
|
|
4416
|
+
else await txn.delete(key);
|
|
4417
|
+
});
|
|
4418
|
+
}
|
|
3954
4419
|
async registerQueuePayload(registration) {
|
|
3955
4420
|
if (!registration || typeof registration.messageId !== "string" || registration.messageId.length === 0 || typeof registration.key !== "string" || registration.key.length === 0 || !isNonNegativeSafeInteger(registration.orphanExpiresAt)) throw new TypeError("world-celld queue payload registration is invalid");
|
|
3956
4421
|
return await this.ctx.storage.transaction(async (txn) => {
|