@ewhauser/world-celld 0.4.0 → 0.5.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 +39 -0
- package/NOTICE +1 -1
- package/README.md +131 -92
- package/celld-queue-worker/README.md +26 -0
- package/celld-queue-worker/worker.ts +3 -0
- package/celld-queue-worker/wrangler.jsonc +26 -0
- package/celld-worker/README.md +33 -23
- package/celld-worker/worker.ts +1 -1
- package/celld-worker/wrangler.jsonc +12 -8
- package/dist/index.d.ts +34 -71
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +290 -132
- package/dist/index.js.map +1 -1
- package/dist/queue-consumer.d.ts +33 -0
- package/dist/queue-consumer.d.ts.map +1 -0
- package/dist/queue-consumer.js +75 -0
- package/dist/queue-consumer.js.map +1 -0
- package/dist/queue-delivery-DzNOtCYA.d.ts +49 -0
- package/dist/queue-delivery-DzNOtCYA.d.ts.map +1 -0
- package/dist/{util-ya3V_FTA.js → queue-protocol-CmJNMH_5.js} +138 -15
- package/dist/queue-protocol-CmJNMH_5.js.map +1 -0
- package/dist/queue-protocol-Ds44uuND.js +104 -0
- package/dist/queue-protocol-Ds44uuND.js.map +1 -0
- package/dist/testing.d.ts +13 -1
- package/dist/testing.d.ts.map +1 -1
- package/dist/testing.js +2715 -2619
- package/dist/testing.js.map +1 -1
- package/dist/worker.d.ts +109 -182
- package/dist/worker.d.ts.map +1 -1
- package/dist/worker.js +3327 -3098
- package/dist/worker.js.map +1 -1
- package/docs/celld-v0.5-upgrade.md +130 -0
- package/package.json +12 -3
- package/dist/util-ya3V_FTA.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,15 +1,10 @@
|
|
|
1
|
-
import { C as
|
|
2
|
-
import { EventSchema, HookSchema, MessageId, SPEC_VERSION_CURRENT, StepSchema, WorkflowRunSchema, isTerminalWorkflowRunStatus, parseQueueName } from "@workflow/world";
|
|
1
|
+
import { A as MAX_STREAM_BATCH_BYTES, B as decodeStreamWriteResult, C as boundedIntegerOption, F as MAX_STREAM_READ_CHUNKS, H as encodeStreamWriteBatch, I as MAX_STREAM_WRITE_CHUNKS, L as STREAM_BATCH_CONTENT_TYPE, M as MAX_STREAM_ERROR_BYTES, N as MAX_STREAM_LONG_POLL_MS, O as queueDelayDeadline, P as MAX_STREAM_READ_BYTES, R as decodeStreamReadResult, W as normalizeStreamError, b as MAX_QUEUE_DELIVERY_TIMEOUT_MS, d as rpcParse, f as rpcStringify, h as createWorkflowIndex, j as MAX_STREAM_CHUNK_BYTES, k as strictIntegerSetting, m as stringify, p as parse, r as nativeQueueDelaySeconds, u as compact, w as checkedQueueTimestampAdd, x as MAX_QUEUE_DERIVED_DEADLINE_HEADROOM_MS, y as MAX_FLEET_RPC_TIMEOUT_MS } from "./queue-protocol-CmJNMH_5.js";
|
|
2
|
+
import { CreateEventSchema, EventSchema, HookSchema, MessageId, QueuePayloadSchema, SPEC_VERSION_CURRENT, StepSchema, WaitSchema, WorkflowRunSchema, isTerminalWorkflowRunStatus, parseQueueName } from "@workflow/world";
|
|
3
3
|
import { EntityConflictError, HookNotFoundError, RunExpiredError, RunNotSupportedError, TooEarlyError, WorkflowRunNotFoundError, WorkflowWorldError } from "@workflow/errors";
|
|
4
|
-
import { setTimeout as setTimeout$1 } from "node:timers/promises";
|
|
5
4
|
import { monotonicFactory } from "ulid";
|
|
6
5
|
//#region src/config.ts
|
|
7
6
|
function resolveConfig(config) {
|
|
8
|
-
const
|
|
9
|
-
if (!Number.isSafeInteger(queueShards) || queueShards < 1) throw new Error("world-celld: queueShards must be a positive safe integer");
|
|
10
|
-
const retentionRaw = config?.runRetentionMs ?? process.env.CELLD_RUN_RETENTION_MS ?? 0;
|
|
11
|
-
const runRetentionMs = typeof retentionRaw === "number" ? retentionRaw : Number.parseInt(retentionRaw, 10);
|
|
12
|
-
if (!Number.isSafeInteger(runRetentionMs) || runRetentionMs < 0) throw new Error("world-celld: runRetentionMs must be a non-negative safe integer");
|
|
7
|
+
const runRetentionMs = config?.runRetentionMs !== void 0 ? boundedIntegerOption("world-celld: runRetentionMs", config.runRetentionMs, 0, 0) : strictIntegerSetting("world-celld: runRetentionMs", process.env.CELLD_RUN_RETENTION_MS, 0, 0);
|
|
13
8
|
const rpcTimeoutMs = config?.rpcTimeoutMs ?? 3e4;
|
|
14
9
|
if (!Number.isSafeInteger(rpcTimeoutMs) || rpcTimeoutMs < 1 || rpcTimeoutMs > 3e5) throw new Error(`world-celld: rpcTimeoutMs must be between 1 and ${MAX_FLEET_RPC_TIMEOUT_MS}`);
|
|
15
10
|
const streamLongPollMs = config?.streamLongPollMs ?? Math.min(2e4, Math.max(1, rpcTimeoutMs - 1e3));
|
|
@@ -23,7 +18,6 @@ function resolveConfig(config) {
|
|
|
23
18
|
env: config?.env,
|
|
24
19
|
deploymentId: config?.deploymentId ?? process.env.CELLD_DEPLOYMENT_ID ?? "celld-default",
|
|
25
20
|
baseUrl: config?.baseUrl,
|
|
26
|
-
queueShards,
|
|
27
21
|
runRetentionMs,
|
|
28
22
|
streamLongPollMs,
|
|
29
23
|
streamFlushIntervalMs,
|
|
@@ -61,10 +55,15 @@ const RETRYABLE_STATUSES$1 = /* @__PURE__ */ new Set([
|
|
|
61
55
|
503,
|
|
62
56
|
504
|
|
63
57
|
]);
|
|
64
|
-
const DEFAULT_TIMEOUT_MS
|
|
58
|
+
const DEFAULT_TIMEOUT_MS = 3e4;
|
|
65
59
|
function delayMs$1(ms) {
|
|
66
60
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
67
61
|
}
|
|
62
|
+
function resolveFleetTimeoutMs(transport) {
|
|
63
|
+
const timeoutMs = transport.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
64
|
+
if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 3e5) throw new Error(`world-celld: fleet RPC timeout must be between 1 and ${MAX_FLEET_RPC_TIMEOUT_MS}`);
|
|
65
|
+
return timeoutMs;
|
|
66
|
+
}
|
|
68
67
|
/**
|
|
69
68
|
* Invoke one DO method through the worker router.
|
|
70
69
|
*
|
|
@@ -81,8 +80,7 @@ async function callFleetRoute(transport, path, args, opts) {
|
|
|
81
80
|
const doFetch = transport.fetchImpl ?? fetch;
|
|
82
81
|
const url = `${transport.fleetUrl.replace(/\/$/, "")}${path}`;
|
|
83
82
|
const attempts = opts?.idempotent ? 3 : 1;
|
|
84
|
-
const timeoutMs = transport
|
|
85
|
-
if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 3e5) throw new Error(`world-celld: fleet RPC timeout must be between 1 and ${MAX_FLEET_RPC_TIMEOUT_MS}`);
|
|
83
|
+
const timeoutMs = resolveFleetTimeoutMs(transport);
|
|
86
84
|
let lastError;
|
|
87
85
|
for (let attempt = 1; attempt <= attempts; attempt++) {
|
|
88
86
|
if (attempt > 1) await delayMs$1(100 * attempt + Math.random() * 200);
|
|
@@ -131,15 +129,27 @@ const RETRYABLE_STATUSES = /* @__PURE__ */ new Set([
|
|
|
131
129
|
504
|
|
132
130
|
]);
|
|
133
131
|
const READ_ATTEMPTS = 3;
|
|
134
|
-
const DEFAULT_TIMEOUT_MS = 3e4;
|
|
135
132
|
const MAX_WRITE_RESPONSE_BYTES = 64;
|
|
136
133
|
const MAX_READ_RESPONSE_BYTES = MAX_STREAM_READ_BYTES + 65536;
|
|
137
134
|
const MAX_ERROR_RESPONSE_BYTES = 65536;
|
|
138
|
-
function delayMs(ms) {
|
|
139
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
135
|
+
function delayMs(ms, signal) {
|
|
136
|
+
if (!signal) return new Promise((resolve) => setTimeout(resolve, ms));
|
|
137
|
+
if (signal.aborted) return Promise.reject(aborted(signal));
|
|
138
|
+
return new Promise((resolve, reject) => {
|
|
139
|
+
const timer = setTimeout(() => {
|
|
140
|
+
signal.removeEventListener("abort", onAbort);
|
|
141
|
+
resolve();
|
|
142
|
+
}, ms);
|
|
143
|
+
const onAbort = () => {
|
|
144
|
+
clearTimeout(timer);
|
|
145
|
+
signal.removeEventListener("abort", onAbort);
|
|
146
|
+
reject(aborted(signal));
|
|
147
|
+
};
|
|
148
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
149
|
+
});
|
|
140
150
|
}
|
|
141
151
|
function requestSignal(timeoutMs, signal) {
|
|
142
|
-
const timeout = AbortSignal.timeout(
|
|
152
|
+
const timeout = AbortSignal.timeout(timeoutMs);
|
|
143
153
|
return signal ? AbortSignal.any([signal, timeout]) : timeout;
|
|
144
154
|
}
|
|
145
155
|
function aborted(signal) {
|
|
@@ -190,6 +200,7 @@ function streamUrl(transport, name) {
|
|
|
190
200
|
return `${transport.fleetUrl.replace(/\/$/, "")}/v1/streams/${encodeURIComponent(name)}/chunks`;
|
|
191
201
|
}
|
|
192
202
|
async function writeStreamChunks(transport, name, runId, chunks) {
|
|
203
|
+
const timeoutMs = resolveFleetTimeoutMs(transport);
|
|
193
204
|
const body = encodeStreamWriteBatch(chunks);
|
|
194
205
|
if (body.byteLength > 8389632) throw new Error("world-celld: encoded stream batch exceeds configured limit");
|
|
195
206
|
const url = new URL(streamUrl(transport, name));
|
|
@@ -204,7 +215,7 @@ async function writeStreamChunks(transport, name, runId, chunks) {
|
|
|
204
215
|
authorization: `Bearer ${transport.secret}`
|
|
205
216
|
},
|
|
206
217
|
body,
|
|
207
|
-
signal: requestSignal(
|
|
218
|
+
signal: requestSignal(timeoutMs)
|
|
208
219
|
});
|
|
209
220
|
} catch (error) {
|
|
210
221
|
throw new FleetTransportError(`world-celld: fleet unreachable at ${url.href}`, error);
|
|
@@ -218,6 +229,7 @@ async function writeStreamChunks(transport, name, runId, chunks) {
|
|
|
218
229
|
}
|
|
219
230
|
}
|
|
220
231
|
async function readStreamChunks(transport, name, request, signal) {
|
|
232
|
+
const timeoutMs = resolveFleetTimeoutMs(transport);
|
|
221
233
|
const url = new URL(streamUrl(transport, name));
|
|
222
234
|
url.searchParams.set("runId", request.runId);
|
|
223
235
|
url.searchParams.set("startIndex", String(request.startIndex));
|
|
@@ -228,27 +240,38 @@ async function readStreamChunks(transport, name, request, signal) {
|
|
|
228
240
|
let lastError;
|
|
229
241
|
for (let attempt = 1; attempt <= READ_ATTEMPTS; attempt++) {
|
|
230
242
|
if (signal?.aborted) throw aborted(signal);
|
|
231
|
-
if (attempt > 1) await delayMs(100 * attempt + Math.random() * 200);
|
|
243
|
+
if (attempt > 1) await delayMs(100 * attempt + Math.random() * 200, signal);
|
|
232
244
|
let response;
|
|
233
245
|
try {
|
|
234
246
|
response = await doFetch(url, {
|
|
235
247
|
method: "GET",
|
|
236
248
|
headers: { authorization: `Bearer ${transport.secret}` },
|
|
237
|
-
signal: requestSignal(
|
|
249
|
+
signal: requestSignal(timeoutMs, signal)
|
|
238
250
|
});
|
|
239
251
|
} catch (error) {
|
|
240
252
|
if (signal?.aborted) throw aborted(signal);
|
|
241
253
|
lastError = new FleetTransportError(`world-celld: fleet unreachable at ${url.href}`, error);
|
|
242
254
|
continue;
|
|
243
255
|
}
|
|
256
|
+
if (signal?.aborted) {
|
|
257
|
+
await response.body?.cancel(aborted(signal)).catch(() => void 0);
|
|
258
|
+
throw aborted(signal);
|
|
259
|
+
}
|
|
244
260
|
if (response.ok) try {
|
|
245
261
|
return decodeStreamReadResult(await readBoundedResponse(response, MAX_READ_RESPONSE_BYTES));
|
|
246
262
|
} catch (error) {
|
|
247
|
-
|
|
263
|
+
if (signal?.aborted) throw aborted(signal);
|
|
264
|
+
lastError = error instanceof FleetTransportError ? error : new FleetTransportError(`world-celld: malformed stream response from ${url.href}`, error);
|
|
248
265
|
if (attempt < READ_ATTEMPTS) continue;
|
|
249
266
|
throw lastError;
|
|
250
267
|
}
|
|
251
|
-
|
|
268
|
+
let error;
|
|
269
|
+
try {
|
|
270
|
+
error = await errorFromResponse(response);
|
|
271
|
+
} catch (cause) {
|
|
272
|
+
if (signal?.aborted) throw aborted(signal);
|
|
273
|
+
error = cause instanceof FleetTransportError ? cause : new FleetTransportError(`world-celld: malformed stream error response from ${url.href}`, cause);
|
|
274
|
+
}
|
|
252
275
|
if (RETRYABLE_STATUSES.has(response.status) && attempt < READ_ATTEMPTS) {
|
|
253
276
|
lastError = error;
|
|
254
277
|
continue;
|
|
@@ -279,7 +302,8 @@ const RUNS = {
|
|
|
279
302
|
"applyEvent",
|
|
280
303
|
"scheduleCleanup",
|
|
281
304
|
"cleanupNow",
|
|
282
|
-
"rearmCleanup"
|
|
305
|
+
"rearmCleanup",
|
|
306
|
+
"resolveHookTokenClaim"
|
|
283
307
|
])
|
|
284
308
|
};
|
|
285
309
|
const STREAMS = {
|
|
@@ -315,26 +339,6 @@ function makeStreamNamespace(transport) {
|
|
|
315
339
|
}
|
|
316
340
|
};
|
|
317
341
|
}
|
|
318
|
-
const QUEUE = {
|
|
319
|
-
methods: [
|
|
320
|
-
"enqueue",
|
|
321
|
-
"stats",
|
|
322
|
-
"listDeadLetters",
|
|
323
|
-
"redriveDeadLetter",
|
|
324
|
-
"purgeDeadLetters",
|
|
325
|
-
"rearmAlarm",
|
|
326
|
-
"expireRun",
|
|
327
|
-
"acknowledgeExpireRun"
|
|
328
|
-
],
|
|
329
|
-
mutating: /* @__PURE__ */ new Set([
|
|
330
|
-
"enqueue",
|
|
331
|
-
"redriveDeadLetter",
|
|
332
|
-
"purgeDeadLetters",
|
|
333
|
-
"rearmAlarm",
|
|
334
|
-
"expireRun",
|
|
335
|
-
"acknowledgeExpireRun"
|
|
336
|
-
])
|
|
337
|
-
};
|
|
338
342
|
function makeStub(transport, binding, name, spec) {
|
|
339
343
|
const stub = {};
|
|
340
344
|
for (const method of spec.methods) stub[method] = (...args) => callDO(transport, binding, name, method, args, { idempotent: !spec.mutating.has(method) });
|
|
@@ -414,7 +418,7 @@ function createRemoteEnv(transport) {
|
|
|
414
418
|
], { idempotent: true }),
|
|
415
419
|
releaseHookIndexes: (request) => callFleetRoute(transport, "/v1/index/hooks/release", [request], { idempotent: true })
|
|
416
420
|
},
|
|
417
|
-
WORKFLOW_QUEUE:
|
|
421
|
+
WORKFLOW_QUEUE: { send: (envelope, options) => callFleetRoute(transport, "/v1/queue/send", [envelope, options], { idempotent: false }) }
|
|
418
422
|
};
|
|
419
423
|
}
|
|
420
424
|
//#endregion
|
|
@@ -454,18 +458,30 @@ const debug = createDebugLogger("celld-world");
|
|
|
454
458
|
* Vendored from vinnymac/worlds packages/world-cloudflare/src/queue.ts
|
|
455
459
|
* (Apache-2.0, see NOTICE), modified for celld:
|
|
456
460
|
*
|
|
457
|
-
* -
|
|
458
|
-
*
|
|
459
|
-
*
|
|
460
|
-
*
|
|
461
|
-
*
|
|
462
|
-
*
|
|
463
|
-
* cell (at enqueue and across the delivery state machine).
|
|
461
|
+
* - Production publishes a small pointer through celld v0.5.0 native Queues.
|
|
462
|
+
* The deployed Queue consumer forwards deliveries to the same HTTP handler
|
|
463
|
+
* used by the in-process test pump.
|
|
464
|
+
* - Run-bearing message bodies live in the fleet object store. This preserves
|
|
465
|
+
* the World's larger payload contract and lets run retention delete payload
|
|
466
|
+
* bytes independently of the broker's fixed retention window.
|
|
464
467
|
* - `createQueueHandler` has ONE dialect — the x-vqs wire format the test
|
|
465
468
|
* pump has always used — extended so permanent errors surface as their
|
|
466
|
-
* HTTP statuses.
|
|
469
|
+
* HTTP statuses. Native Queue deliveries and the in-process test pump hit the
|
|
467
470
|
* exact same handler path.
|
|
468
471
|
*/
|
|
472
|
+
const MAX_TIMER_DELAY_MS = 2147483647;
|
|
473
|
+
const MAX_TEST_PUMP_BACKOFF_MS = 6e4;
|
|
474
|
+
function delay(milliseconds) {
|
|
475
|
+
return new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
476
|
+
}
|
|
477
|
+
async function delayFor(milliseconds) {
|
|
478
|
+
let remaining = milliseconds;
|
|
479
|
+
while (remaining > MAX_TIMER_DELAY_MS) {
|
|
480
|
+
await delay(MAX_TIMER_DELAY_MS);
|
|
481
|
+
remaining -= MAX_TIMER_DELAY_MS;
|
|
482
|
+
}
|
|
483
|
+
await delay(remaining);
|
|
484
|
+
}
|
|
469
485
|
/**
|
|
470
486
|
* HTTP status codes that indicate permanent (non-retryable) failures.
|
|
471
487
|
* Deliveries with these statuses are dropped immediately instead of retried.
|
|
@@ -491,7 +507,7 @@ function computeBackoff(attempt) {
|
|
|
491
507
|
}
|
|
492
508
|
const QUEUE_PATHNAME = "flow";
|
|
493
509
|
function isTestMode() {
|
|
494
|
-
if (process.env.CELLD_QUEUE_MODE === "
|
|
510
|
+
if (process.env.CELLD_QUEUE_MODE === "native") return false;
|
|
495
511
|
return process.env.VITEST === "true" || process.env.NODE_ENV === "test";
|
|
496
512
|
}
|
|
497
513
|
function resolveBaseUrl(config) {
|
|
@@ -500,19 +516,6 @@ function resolveBaseUrl(config) {
|
|
|
500
516
|
return `http://localhost:${process.env.PORT ?? "3000"}`;
|
|
501
517
|
}
|
|
502
518
|
/**
|
|
503
|
-
* FNV-1a hash for shard selection. All messages sharing an idempotencyKey
|
|
504
|
-
* land on the same cell so dedup stays strictly consistent.
|
|
505
|
-
*/
|
|
506
|
-
function shardFor(key, shards) {
|
|
507
|
-
if (shards <= 1) return 0;
|
|
508
|
-
let hash = 2166136261;
|
|
509
|
-
for (let i = 0; i < key.length; i++) {
|
|
510
|
-
hash ^= key.charCodeAt(i);
|
|
511
|
-
hash = Math.imul(hash, 16777619);
|
|
512
|
-
}
|
|
513
|
-
return (hash >>> 0) % shards;
|
|
514
|
-
}
|
|
515
|
-
/**
|
|
516
519
|
* In-process test pump. Holds an in-memory FIFO and HTTP-dispatches envelopes
|
|
517
520
|
* to the user's server at `${baseUrl}/.well-known/workflow/v1/flow`.
|
|
518
521
|
*
|
|
@@ -522,12 +525,12 @@ function shardFor(key, shards) {
|
|
|
522
525
|
*
|
|
523
526
|
* Modified vs upstream: permanent-error statuses (404/409/410/422) from the
|
|
524
527
|
* handler drop the message immediately instead of burning retry budget —
|
|
525
|
-
* matching
|
|
528
|
+
* matching the native Queue consumer's production semantics.
|
|
526
529
|
*/
|
|
527
530
|
function createTestPump(config) {
|
|
528
|
-
const httpTimeoutMs = config.httpTimeoutMs
|
|
529
|
-
const maxAttempts = config.maxAttempts
|
|
530
|
-
const baseBackoffMs = config.backoffDelayMs
|
|
531
|
+
const httpTimeoutMs = boundedIntegerOption("world-celld test pump httpTimeoutMs", config.httpTimeoutMs, 3e5, 1, MAX_QUEUE_DELIVERY_TIMEOUT_MS);
|
|
532
|
+
const maxAttempts = boundedIntegerOption("world-celld test pump maxAttempts", config.maxAttempts, 5, 1);
|
|
533
|
+
const baseBackoffMs = boundedIntegerOption("world-celld test pump backoffDelayMs", config.backoffDelayMs, 1e3, 0, MAX_TEST_PUMP_BACKOFF_MS);
|
|
531
534
|
const queues = { flow: [] };
|
|
532
535
|
const wakers = { flow: [] };
|
|
533
536
|
/** Inflight messageIds by idempotencyKey (world-local queue.js semantics). */
|
|
@@ -576,8 +579,10 @@ function createTestPump(config) {
|
|
|
576
579
|
parsed = null;
|
|
577
580
|
}
|
|
578
581
|
const timeoutSeconds = parsed?.timeoutSeconds;
|
|
579
|
-
|
|
580
|
-
|
|
582
|
+
const now = Date.now();
|
|
583
|
+
const redeliveryAt = queueDelayDeadline(now, timeoutSeconds, 1, MAX_QUEUE_DERIVED_DEADLINE_HEADROOM_MS);
|
|
584
|
+
if (redeliveryAt !== null) {
|
|
585
|
+
delayFor(redeliveryAt - now).then(() => enqueue(pathname, envelope));
|
|
581
586
|
return;
|
|
582
587
|
}
|
|
583
588
|
}
|
|
@@ -591,8 +596,13 @@ function createTestPump(config) {
|
|
|
591
596
|
...envelope,
|
|
592
597
|
attempt: envelope.attempt + 1
|
|
593
598
|
};
|
|
594
|
-
const backoff = baseBackoffMs * 2 ** (next.attempt - 1);
|
|
595
|
-
|
|
599
|
+
const backoff = Math.min(MAX_TEST_PUMP_BACKOFF_MS, baseBackoffMs * 2 ** Math.min(next.attempt - 1, 30));
|
|
600
|
+
if (checkedQueueTimestampAdd(Date.now(), backoff, 900001) === null) {
|
|
601
|
+
release(envelope);
|
|
602
|
+
console.error(`[world-celld test pump] dropping ${envelope.messageId}: retry deadline exceeds the queue timestamp horizon`);
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
delayFor(backoff).then(() => enqueue(pathname, next));
|
|
596
606
|
} else {
|
|
597
607
|
release(envelope);
|
|
598
608
|
console.error(`[world-celld test pump] dropping ${envelope.messageId} after ${envelope.attempt} attempts: HTTP ${response.status}: ${text}`);
|
|
@@ -617,8 +627,9 @@ function createTestPump(config) {
|
|
|
617
627
|
return idempotencyKey ? inflightMessages.get(idempotencyKey) : void 0;
|
|
618
628
|
},
|
|
619
629
|
push(pathname, envelope, delaySeconds) {
|
|
630
|
+
if (queueDelayDeadline(Date.now(), delaySeconds ?? 0, 0, 900001) === null) throw new Error("world-celld queue delaySeconds lacks required delivery headroom");
|
|
620
631
|
if (envelope.idempotencyKey) inflightMessages.set(envelope.idempotencyKey, MessageId.parse(envelope.messageId));
|
|
621
|
-
if (delaySeconds && delaySeconds > 0)
|
|
632
|
+
if (delaySeconds && delaySeconds > 0) delayFor(delaySeconds * 1e3).then(() => enqueue(pathname, envelope));
|
|
622
633
|
else enqueue(pathname, envelope);
|
|
623
634
|
},
|
|
624
635
|
async start() {
|
|
@@ -634,17 +645,16 @@ function createTestPump(config) {
|
|
|
634
645
|
}
|
|
635
646
|
function createQueue(config) {
|
|
636
647
|
const { env, deploymentId } = config;
|
|
637
|
-
|
|
648
|
+
if (!env?.WORKFLOW_QUEUE) throw new Error("world-celld queue missing WORKFLOW_QUEUE binding");
|
|
638
649
|
const generateMessageId = monotonicFactory();
|
|
639
650
|
const testPump = createTestPump(config);
|
|
640
|
-
const getQueueStub = (shard) => {
|
|
641
|
-
const id = env.WORKFLOW_QUEUE.idFromName(`q:${shard}`);
|
|
642
|
-
return env.WORKFLOW_QUEUE.get(id);
|
|
643
|
-
};
|
|
644
651
|
return {
|
|
645
652
|
async queue(queueName, message, opts) {
|
|
646
653
|
parseQueueName(queueName);
|
|
647
|
-
const
|
|
654
|
+
const parsedMessage = QueuePayloadSchema.safeParse(message);
|
|
655
|
+
if (!parsedMessage.success) throw new WorkflowWorldError("world-celld queue payload is invalid", { status: 422 });
|
|
656
|
+
if (queueDelayDeadline(Date.now(), opts?.delaySeconds ?? 0, 0, 900001) === null) throw new Error("world-celld queue delaySeconds lacks required delivery headroom");
|
|
657
|
+
const runId = "runId" in parsedMessage.data && typeof parsedMessage.data.runId === "string" ? parsedMessage.data.runId : void 0;
|
|
648
658
|
if (isTestMode()) {
|
|
649
659
|
const existing = testPump.inflight(opts?.idempotencyKey);
|
|
650
660
|
if (existing) return { messageId: existing };
|
|
@@ -653,50 +663,63 @@ function createQueue(config) {
|
|
|
653
663
|
messageId,
|
|
654
664
|
queueName,
|
|
655
665
|
attempt: 1,
|
|
656
|
-
message,
|
|
666
|
+
message: parsedMessage.data,
|
|
657
667
|
idempotencyKey: opts?.idempotencyKey
|
|
658
668
|
}, opts?.delaySeconds);
|
|
659
669
|
return { messageId };
|
|
660
670
|
}
|
|
661
671
|
const messageId = MessageId.parse(`msg_${generateMessageId()}`);
|
|
662
|
-
const
|
|
663
|
-
const
|
|
672
|
+
const body = stringify(parsedMessage.data);
|
|
673
|
+
const notBefore = opts?.delaySeconds && opts.delaySeconds > 0 ? queueDelayDeadline(Date.now(), opts.delaySeconds, 0, 900001) ?? void 0 : void 0;
|
|
674
|
+
const envelope = {
|
|
675
|
+
version: 1,
|
|
664
676
|
messageId,
|
|
665
677
|
queueName,
|
|
666
|
-
|
|
667
|
-
body: stringify(message),
|
|
678
|
+
targetBaseUrl: resolveBaseUrl(config),
|
|
668
679
|
runId,
|
|
669
680
|
idempotencyKey: opts?.idempotencyKey,
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
});
|
|
676
|
-
if (!outcome.ok) {
|
|
677
|
-
if (outcome.code === "RUN_EXPIRED") throw new RunExpiredError(outcome.message);
|
|
678
|
-
throw new WorkflowWorldError(outcome.message, { status: 409 });
|
|
679
|
-
}
|
|
680
|
-
return { messageId: MessageId.parse(outcome.messageId) };
|
|
681
|
+
body,
|
|
682
|
+
notBefore
|
|
683
|
+
};
|
|
684
|
+
const published = await env.WORKFLOW_QUEUE.send(envelope, { delaySeconds: nativeQueueDelaySeconds(notBefore, Date.now()) });
|
|
685
|
+
return { messageId: MessageId.parse(published.messageId) };
|
|
681
686
|
},
|
|
682
687
|
createQueueHandler(queueNamePrefix, handler) {
|
|
683
688
|
return async (req) => {
|
|
689
|
+
if (req.method !== "POST") return Response.json({ error: "Expected POST" }, { status: 405 });
|
|
690
|
+
if (req.headers.get("content-type")?.split(";", 1)[0].trim().toLowerCase() !== "application/json") return Response.json({ error: "Expected application/json" }, { status: 415 });
|
|
684
691
|
const reqQueueName = req.headers.get("x-vqs-queue-name");
|
|
685
692
|
const reqMessageId = req.headers.get("x-vqs-message-id");
|
|
686
693
|
const attemptStr = req.headers.get("x-vqs-message-attempt");
|
|
687
694
|
if (!reqQueueName || !reqMessageId || !attemptStr || !req.body) return Response.json({ error: "Missing required headers or body" }, { status: 400 });
|
|
688
695
|
if (!reqQueueName.startsWith(queueNamePrefix)) return Response.json({ error: "Unhandled queue" }, { status: 400 });
|
|
689
|
-
|
|
696
|
+
let attempt;
|
|
690
697
|
try {
|
|
691
|
-
|
|
698
|
+
attempt = strictIntegerSetting("x-vqs-message-attempt", attemptStr, 1, 1);
|
|
699
|
+
} catch {
|
|
700
|
+
return Response.json({ error: "Invalid x-vqs-message-attempt header" }, { status: 400 });
|
|
701
|
+
}
|
|
702
|
+
try {
|
|
703
|
+
let body;
|
|
704
|
+
try {
|
|
705
|
+
body = parse(await req.text());
|
|
706
|
+
} catch {
|
|
707
|
+
return Response.json({ error: "Malformed tagged JSON body" }, { status: 422 });
|
|
708
|
+
}
|
|
709
|
+
const parsedBody = QueuePayloadSchema.safeParse(body);
|
|
710
|
+
if (!parsedBody.success) return Response.json({ error: "Invalid queue payload" }, { status: 422 });
|
|
711
|
+
const result = await handler(parsedBody.data, {
|
|
692
712
|
attempt,
|
|
693
713
|
queueName: reqQueueName,
|
|
694
714
|
messageId: reqMessageId
|
|
695
715
|
});
|
|
696
|
-
if (result && typeof result.timeoutSeconds === "number")
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
716
|
+
if (result && typeof result.timeoutSeconds === "number") {
|
|
717
|
+
if (queueDelayDeadline(Date.now(), result.timeoutSeconds, 1, 900001) === null) throw new Error("Queue handler timeoutSeconds is out of range");
|
|
718
|
+
return Response.json({ timeoutSeconds: result.timeoutSeconds }, {
|
|
719
|
+
status: 503,
|
|
720
|
+
headers: { "Retry-After": String(result.timeoutSeconds) }
|
|
721
|
+
});
|
|
722
|
+
}
|
|
700
723
|
return new Response(null, { status: 204 });
|
|
701
724
|
} catch (error) {
|
|
702
725
|
if (isPermanentError(error)) {
|
|
@@ -740,6 +763,114 @@ function hookOwner(hook) {
|
|
|
740
763
|
hookId: hook.hookId
|
|
741
764
|
};
|
|
742
765
|
}
|
|
766
|
+
const APPLY_EVENT_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
767
|
+
"RUN_NOT_FOUND",
|
|
768
|
+
"STEP_NOT_FOUND",
|
|
769
|
+
"HOOK_NOT_FOUND",
|
|
770
|
+
"WAIT_NOT_FOUND",
|
|
771
|
+
"ENTITY_CONFLICT",
|
|
772
|
+
"HOOK_CLAIM_CANCELLED",
|
|
773
|
+
"RUN_EXPIRED",
|
|
774
|
+
"TOO_EARLY",
|
|
775
|
+
"RUN_NOT_SUPPORTED"
|
|
776
|
+
]);
|
|
777
|
+
function malformedApplyEventOutcome(reason) {
|
|
778
|
+
throw new FleetTransportError(`world-celld: malformed applyEvent outcome: ${reason}`);
|
|
779
|
+
}
|
|
780
|
+
function isRecord(value) {
|
|
781
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
782
|
+
}
|
|
783
|
+
function validateOptionalNumber(outcome, key, integer) {
|
|
784
|
+
const value = outcome[key];
|
|
785
|
+
if (value !== void 0 && (typeof value !== "number" || !Number.isFinite(value) || integer && !Number.isSafeInteger(value))) malformedApplyEventOutcome(`${key} must be ${integer ? "a safe integer" : "a finite number"}`);
|
|
786
|
+
}
|
|
787
|
+
function validateOptionalRetryAfterMs(outcome) {
|
|
788
|
+
const value = outcome.retryAfterMs;
|
|
789
|
+
if (value !== void 0 && (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0)) malformedApplyEventOutcome("retryAfterMs must be a non-negative safe integer");
|
|
790
|
+
}
|
|
791
|
+
function parseSuccessEntity(outcome, key, schema) {
|
|
792
|
+
const value = outcome[key];
|
|
793
|
+
if (value === void 0) return void 0;
|
|
794
|
+
if (!isRecord(value)) malformedApplyEventOutcome(`${key} is invalid`);
|
|
795
|
+
const result = schema.safeParse(compact(value));
|
|
796
|
+
if (!result.success) malformedApplyEventOutcome(`${key} is invalid`);
|
|
797
|
+
return result.data;
|
|
798
|
+
}
|
|
799
|
+
/**
|
|
800
|
+
* Validate the untrusted value decoded from the remote applyEvent RPC.
|
|
801
|
+
* Unknown failure codes are intentionally valid and classified separately so
|
|
802
|
+
* an older client can reconstruct their wire metadata without treating them
|
|
803
|
+
* as success. All fields consumed by the success path are checked before any
|
|
804
|
+
* derivative index operation can run.
|
|
805
|
+
*/
|
|
806
|
+
function parseApplyEventOutcome(value) {
|
|
807
|
+
if (!isRecord(value)) malformedApplyEventOutcome("response must be an object");
|
|
808
|
+
if (value.ok === false) {
|
|
809
|
+
if (typeof value.code !== "string" || value.code.length === 0) malformedApplyEventOutcome("failure code must be a non-empty string");
|
|
810
|
+
if (typeof value.message !== "string") malformedApplyEventOutcome("failure message must be a string");
|
|
811
|
+
validateOptionalNumber(value, "status", true);
|
|
812
|
+
validateOptionalNumber(value, "retryAfter", false);
|
|
813
|
+
validateOptionalRetryAfterMs(value);
|
|
814
|
+
validateOptionalNumber(value, "retryAfterSeconds", false);
|
|
815
|
+
validateOptionalNumber(value, "runSpecVersion", true);
|
|
816
|
+
validateOptionalNumber(value, "worldSpecVersion", true);
|
|
817
|
+
const outcome = value;
|
|
818
|
+
return APPLY_EVENT_ERROR_CODES.has(outcome.code) ? {
|
|
819
|
+
kind: "known-failure",
|
|
820
|
+
outcome
|
|
821
|
+
} : {
|
|
822
|
+
kind: "unknown-failure",
|
|
823
|
+
outcome
|
|
824
|
+
};
|
|
825
|
+
}
|
|
826
|
+
if (value.ok !== true) malformedApplyEventOutcome("ok must be true or false");
|
|
827
|
+
if (!Array.isArray(value.releasedHooks)) malformedApplyEventOutcome("releasedHooks must be an array");
|
|
828
|
+
for (const releasedHook of value.releasedHooks) if (!isRecord(releasedHook) || typeof releasedHook.hookId !== "string" || releasedHook.hookId.length === 0 || typeof releasedHook.token !== "string" || releasedHook.token.length === 0) malformedApplyEventOutcome("releasedHooks entries must contain hookId and token strings");
|
|
829
|
+
const event = parseSuccessEntity(value, "event", EventSchema);
|
|
830
|
+
const run = parseSuccessEntity(value, "run", WorkflowRunSchema);
|
|
831
|
+
const step = parseSuccessEntity(value, "step", StepSchema);
|
|
832
|
+
const hook = parseSuccessEntity(value, "hook", HookSchema);
|
|
833
|
+
const wait = parseSuccessEntity(value, "wait", WaitSchema);
|
|
834
|
+
const hookToIndex = parseSuccessEntity(value, "hookToIndex", HookSchema);
|
|
835
|
+
if (value.stepCreated !== void 0 && value.stepCreated !== true) malformedApplyEventOutcome("stepCreated must be true when present");
|
|
836
|
+
if (value.runCreated !== void 0) {
|
|
837
|
+
if (!isRecord(value.runCreated) || typeof value.runCreated.workflowName !== "string" || !(value.runCreated.createdAt instanceof Date) || Number.isNaN(value.runCreated.createdAt.getTime())) malformedApplyEventOutcome("runCreated is invalid");
|
|
838
|
+
}
|
|
839
|
+
let events;
|
|
840
|
+
if (value.events !== void 0) {
|
|
841
|
+
if (!Array.isArray(value.events)) malformedApplyEventOutcome("events must be an array");
|
|
842
|
+
events = value.events.map((candidate) => {
|
|
843
|
+
if (!isRecord(candidate)) malformedApplyEventOutcome("events contains an invalid event");
|
|
844
|
+
const result = EventSchema.safeParse(compact(candidate));
|
|
845
|
+
if (!result.success) malformedApplyEventOutcome("events contains an invalid event");
|
|
846
|
+
return result.data;
|
|
847
|
+
});
|
|
848
|
+
if (value.cursor !== null && typeof value.cursor !== "string") malformedApplyEventOutcome("cursor is required when events are present");
|
|
849
|
+
if (typeof value.hasMore !== "boolean") malformedApplyEventOutcome("hasMore is required when events are present");
|
|
850
|
+
}
|
|
851
|
+
if (value.cursor !== void 0 && value.cursor !== null && typeof value.cursor !== "string") malformedApplyEventOutcome("cursor must be a string or null");
|
|
852
|
+
if (value.hasMore !== void 0 && typeof value.hasMore !== "boolean") malformedApplyEventOutcome("hasMore must be a boolean");
|
|
853
|
+
if (value.maxEvents !== void 0 && (typeof value.maxEvents !== "number" || !Number.isSafeInteger(value.maxEvents) || value.maxEvents < 0)) malformedApplyEventOutcome("maxEvents must be a non-negative safe integer");
|
|
854
|
+
if (run !== void 0 && value.indexPublicationExpiresAt === void 0) malformedApplyEventOutcome("indexPublicationExpiresAt is required when run is present");
|
|
855
|
+
if (value.indexPublicationExpiresAt !== void 0 && (typeof value.indexPublicationExpiresAt !== "number" || !Number.isFinite(value.indexPublicationExpiresAt))) malformedApplyEventOutcome("indexPublicationExpiresAt must be a finite number");
|
|
856
|
+
return {
|
|
857
|
+
kind: "success",
|
|
858
|
+
outcome: {
|
|
859
|
+
...value,
|
|
860
|
+
event,
|
|
861
|
+
run,
|
|
862
|
+
step,
|
|
863
|
+
hook,
|
|
864
|
+
wait,
|
|
865
|
+
hookToIndex,
|
|
866
|
+
events,
|
|
867
|
+
releasedHooks: value.releasedHooks.map((releasedHook) => ({
|
|
868
|
+
hookId: releasedHook.hookId,
|
|
869
|
+
token: releasedHook.token
|
|
870
|
+
}))
|
|
871
|
+
}
|
|
872
|
+
};
|
|
873
|
+
}
|
|
743
874
|
/**
|
|
744
875
|
* Filter data based on ResolveData parameter.
|
|
745
876
|
* When resolveData is 'none', strips specified keys to reduce data transfer.
|
|
@@ -772,16 +903,44 @@ function filterHookData(hook, resolveData) {
|
|
|
772
903
|
* outcome objects instead of throwing.
|
|
773
904
|
*/
|
|
774
905
|
function throwOutcomeError(outcome, runId, data) {
|
|
906
|
+
const withCode = (error) => {
|
|
907
|
+
Object.assign(error, { code: outcome.code });
|
|
908
|
+
return error;
|
|
909
|
+
};
|
|
775
910
|
switch (outcome.code) {
|
|
776
|
-
case "RUN_NOT_FOUND": throw new WorkflowRunNotFoundError(runId);
|
|
777
|
-
case "STEP_NOT_FOUND": throw new WorkflowWorldError(outcome.message, {
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
case "
|
|
782
|
-
case "
|
|
783
|
-
|
|
784
|
-
|
|
911
|
+
case "RUN_NOT_FOUND": throw withCode(new WorkflowRunNotFoundError(runId));
|
|
912
|
+
case "STEP_NOT_FOUND": throw new WorkflowWorldError(outcome.message, {
|
|
913
|
+
status: 404,
|
|
914
|
+
code: outcome.code
|
|
915
|
+
});
|
|
916
|
+
case "HOOK_NOT_FOUND": throw withCode(new HookNotFoundError(data.correlationId ?? runId));
|
|
917
|
+
case "WAIT_NOT_FOUND": throw new WorkflowWorldError(outcome.message, {
|
|
918
|
+
status: 404,
|
|
919
|
+
code: outcome.code
|
|
920
|
+
});
|
|
921
|
+
case "ENTITY_CONFLICT": throw withCode(new EntityConflictError(outcome.message));
|
|
922
|
+
case "HOOK_CLAIM_CANCELLED": throw new WorkflowWorldError(outcome.message, {
|
|
923
|
+
status: 503,
|
|
924
|
+
code: outcome.code
|
|
925
|
+
});
|
|
926
|
+
case "RUN_EXPIRED": throw withCode(new RunExpiredError(outcome.message));
|
|
927
|
+
case "TOO_EARLY": throw withCode(new TooEarlyError(outcome.message, { retryAfter: outcome.retryAfterSeconds }));
|
|
928
|
+
case "RUN_NOT_SUPPORTED": throw withCode(new RunNotSupportedError(outcome.runSpecVersion ?? 0, SPEC_VERSION_CURRENT));
|
|
929
|
+
default: {
|
|
930
|
+
const error = new WorkflowWorldError(outcome.message, {
|
|
931
|
+
status: outcome.status,
|
|
932
|
+
code: outcome.code,
|
|
933
|
+
retryAfter: outcome.retryAfter
|
|
934
|
+
});
|
|
935
|
+
for (const key of [
|
|
936
|
+
"retryAfterMs",
|
|
937
|
+
"retryAfterSeconds",
|
|
938
|
+
"runSpecVersion",
|
|
939
|
+
"worldSpecVersion",
|
|
940
|
+
"details"
|
|
941
|
+
]) if (Object.hasOwn(outcome, key)) Object.assign(error, { [key]: outcome[key] });
|
|
942
|
+
throw error;
|
|
943
|
+
}
|
|
785
944
|
}
|
|
786
945
|
}
|
|
787
946
|
const parseRun = (run) => WorkflowRunSchema.parse(compact(run));
|
|
@@ -807,10 +966,7 @@ function indexStatusExcludes(indexed, requested) {
|
|
|
807
966
|
}
|
|
808
967
|
function createStorage(config) {
|
|
809
968
|
const { env } = config;
|
|
810
|
-
const cleanup = {
|
|
811
|
-
retentionMs: config.runRetentionMs ?? 0,
|
|
812
|
-
queueShards: config.queueShards ?? 1
|
|
813
|
-
};
|
|
969
|
+
const cleanup = { retentionMs: config.runRetentionMs ?? 0 };
|
|
814
970
|
const ulid = monotonicFactory();
|
|
815
971
|
const getRunDO = (runId) => {
|
|
816
972
|
const id = env.WORKFLOW_DB.idFromName(runId);
|
|
@@ -883,6 +1039,8 @@ function createStorage(config) {
|
|
|
883
1039
|
},
|
|
884
1040
|
events: {
|
|
885
1041
|
async create(runId, data, params) {
|
|
1042
|
+
data = CreateEventSchema.parse(compact(data));
|
|
1043
|
+
if (runId !== null && typeof runId !== "string") throw new WorkflowWorldError("runId must be a string or null", { status: 400 });
|
|
886
1044
|
let effectiveRunId;
|
|
887
1045
|
if (data.eventType === "run_created" && (!runId || runId === "")) effectiveRunId = `wrun_${ulid()}`;
|
|
888
1046
|
else if (!runId) throw new WorkflowWorldError("runId is required for non-run_created events", { status: 400 });
|
|
@@ -905,16 +1063,16 @@ function createStorage(config) {
|
|
|
905
1063
|
};
|
|
906
1064
|
} else tokenHolder = admission.holder;
|
|
907
1065
|
}
|
|
908
|
-
let
|
|
1066
|
+
let parsedOutcome;
|
|
909
1067
|
try {
|
|
910
|
-
|
|
1068
|
+
parsedOutcome = parseApplyEventOutcome(await stub.applyEvent({
|
|
911
1069
|
runId: effectiveRunId,
|
|
912
1070
|
data,
|
|
913
1071
|
params,
|
|
914
1072
|
tokenHolder,
|
|
915
1073
|
hookClaimId: hookAdmission?.reservation.claimId,
|
|
916
1074
|
cleanup
|
|
917
|
-
});
|
|
1075
|
+
}));
|
|
918
1076
|
} catch (error) {
|
|
919
1077
|
if (hookAdmission) {
|
|
920
1078
|
let resolution;
|
|
@@ -931,10 +1089,11 @@ function createStorage(config) {
|
|
|
931
1089
|
}
|
|
932
1090
|
throw error;
|
|
933
1091
|
}
|
|
934
|
-
if (
|
|
1092
|
+
if (parsedOutcome.kind !== "success") {
|
|
935
1093
|
if (hookAdmission) await env.WORKFLOW_INDEX.releaseHookReservation(hookAdmission.token, hookAdmission.owner, hookAdmission.reservation);
|
|
936
|
-
throwOutcomeError(outcome, effectiveRunId, data);
|
|
1094
|
+
throwOutcomeError(parsedOutcome.outcome, effectiveRunId, data);
|
|
937
1095
|
}
|
|
1096
|
+
const outcome = parsedOutcome.outcome;
|
|
938
1097
|
if (outcome.run) {
|
|
939
1098
|
const meta = JSON.stringify({
|
|
940
1099
|
runId: effectiveRunId,
|
|
@@ -1262,6 +1421,12 @@ function createStreamer(config) {
|
|
|
1262
1421
|
* DevKit. Modeled on vinnymac/worlds packages/world-cloudflare/src/index.ts
|
|
1263
1422
|
* (Apache-2.0, see NOTICE).
|
|
1264
1423
|
*/
|
|
1424
|
+
const REQUIRED_ENV_BINDINGS = [
|
|
1425
|
+
"WORKFLOW_DB",
|
|
1426
|
+
"WORKFLOW_INDEX",
|
|
1427
|
+
"WORKFLOW_QUEUE",
|
|
1428
|
+
"WORKFLOW_STREAMS"
|
|
1429
|
+
];
|
|
1265
1430
|
function createCelldWorld(config) {
|
|
1266
1431
|
const resolved = resolveConfig(config);
|
|
1267
1432
|
let env = resolved.env;
|
|
@@ -1280,20 +1445,19 @@ function createCelldWorld(config) {
|
|
|
1280
1445
|
}
|
|
1281
1446
|
}
|
|
1282
1447
|
if (!env) throw new Error("celld environment not configured. Provide config.fleetUrl + config.secret (or CELLD_FLEET_URL / CELLD_WORLD_SECRET), or pass config.env with WORKFLOW_DB, WORKFLOW_INDEX, WORKFLOW_QUEUE, WORKFLOW_STREAMS");
|
|
1448
|
+
for (const binding of REQUIRED_ENV_BINDINGS) if (!env[binding]) throw new Error(`world-celld: config.env is missing required binding ${binding}`);
|
|
1283
1449
|
const storage = createStorage({
|
|
1284
1450
|
env: {
|
|
1285
1451
|
WORKFLOW_DB: env.WORKFLOW_DB,
|
|
1286
1452
|
WORKFLOW_INDEX: env.WORKFLOW_INDEX
|
|
1287
1453
|
},
|
|
1288
1454
|
deploymentId: resolved.deploymentId,
|
|
1289
|
-
runRetentionMs: resolved.runRetentionMs
|
|
1290
|
-
queueShards: resolved.queueShards
|
|
1455
|
+
runRetentionMs: resolved.runRetentionMs
|
|
1291
1456
|
});
|
|
1292
1457
|
const queue = createQueue({
|
|
1293
1458
|
env: { WORKFLOW_QUEUE: env.WORKFLOW_QUEUE },
|
|
1294
1459
|
deploymentId: resolved.deploymentId,
|
|
1295
|
-
baseUrl: resolved.baseUrl
|
|
1296
|
-
queueShards: resolved.queueShards
|
|
1460
|
+
baseUrl: resolved.baseUrl
|
|
1297
1461
|
});
|
|
1298
1462
|
const streamer = createStreamer({
|
|
1299
1463
|
env: { WORKFLOW_STREAMS: env.WORKFLOW_STREAMS },
|
|
@@ -1305,15 +1469,9 @@ function createCelldWorld(config) {
|
|
|
1305
1469
|
getStatus: (runId) => runStub(runId).getCleanupStatus(),
|
|
1306
1470
|
schedule: (runId) => {
|
|
1307
1471
|
if (resolved.runRetentionMs === 0) throw new Error("world-celld: runRetentionMs must be greater than zero to schedule cleanup");
|
|
1308
|
-
return runStub(runId).scheduleCleanup({
|
|
1309
|
-
retentionMs: resolved.runRetentionMs,
|
|
1310
|
-
queueShards: resolved.queueShards
|
|
1311
|
-
});
|
|
1472
|
+
return runStub(runId).scheduleCleanup({ retentionMs: resolved.runRetentionMs });
|
|
1312
1473
|
},
|
|
1313
|
-
cleanupNow: (runId) => runStub(runId).cleanupNow({
|
|
1314
|
-
retentionMs: Math.max(1, resolved.runRetentionMs),
|
|
1315
|
-
queueShards: resolved.queueShards
|
|
1316
|
-
}),
|
|
1474
|
+
cleanupNow: (runId) => runStub(runId).cleanupNow({ retentionMs: Math.max(1, resolved.runRetentionMs) }),
|
|
1317
1475
|
rearm: (runId) => runStub(runId).rearmCleanup()
|
|
1318
1476
|
};
|
|
1319
1477
|
return {
|