@ewhauser/world-celld 0.2.0 → 0.3.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/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
- import { c as globalRunIndexKey, d as workflowRunIndexKey, f as compact, g as stringify, h as parse, m as rpcStringify, o as correlationIndexKey, p as rpcParse } from "./retention-bLY1vcOu.js";
2
- import { EventSchema, HookSchema, MessageId, SPEC_VERSION_CURRENT, StepSchema, WorkflowRunSchema, parseQueueName } from "@workflow/world";
1
+ import { S as normalizeStreamError, a as stringify, b as encodeStreamWriteBatch, c as MAX_STREAM_BATCH_BYTES, d as MAX_STREAM_LONG_POLL_MS, f as MAX_STREAM_READ_BYTES, g as decodeStreamReadResult, h as STREAM_BATCH_CONTENT_TYPE, i as parse, l as MAX_STREAM_CHUNK_BYTES, m as MAX_STREAM_WRITE_CHUNKS, n as rpcParse, o as createWorkflowIndex, p as MAX_STREAM_READ_CHUNKS, r as rpcStringify, t as compact, u as MAX_STREAM_ERROR_BYTES, v as decodeStreamWriteResult } from "./util-DmO0XkFw.js";
2
+ import { EventSchema, HookSchema, MessageId, SPEC_VERSION_CURRENT, StepSchema, WorkflowRunSchema, isTerminalWorkflowRunStatus, parseQueueName } from "@workflow/world";
3
3
  import { EntityConflictError, HookNotFoundError, RunExpiredError, RunNotSupportedError, TooEarlyError, WorkflowRunNotFoundError, WorkflowWorldError } from "@workflow/errors";
4
4
  import { setTimeout as setTimeout$1 } from "node:timers/promises";
5
5
  import { monotonicFactory } from "ulid";
@@ -8,6 +8,13 @@ function resolveConfig(config) {
8
8
  const retentionRaw = config?.runRetentionMs ?? process.env.CELLD_RUN_RETENTION_MS ?? 0;
9
9
  const runRetentionMs = typeof retentionRaw === "number" ? retentionRaw : Number.parseInt(retentionRaw, 10);
10
10
  if (!Number.isSafeInteger(runRetentionMs) || runRetentionMs < 0) throw new Error("world-celld: runRetentionMs must be a non-negative safe integer");
11
+ const rpcTimeoutMs = config?.rpcTimeoutMs ?? 3e4;
12
+ if (!Number.isSafeInteger(rpcTimeoutMs) || rpcTimeoutMs < 1) throw new Error("world-celld: rpcTimeoutMs must be a positive safe integer");
13
+ const streamLongPollMs = config?.streamLongPollMs ?? Math.min(2e4, Math.max(1, rpcTimeoutMs - 1e3));
14
+ if (!Number.isSafeInteger(streamLongPollMs) || streamLongPollMs < 1 || streamLongPollMs > 2e4) throw new Error(`world-celld: streamLongPollMs must be between 1 and ${MAX_STREAM_LONG_POLL_MS}`);
15
+ if (streamLongPollMs >= rpcTimeoutMs) throw new Error("world-celld: streamLongPollMs must be less than rpcTimeoutMs");
16
+ const streamFlushIntervalMs = config?.streamFlushIntervalMs ?? 0;
17
+ if (!Number.isSafeInteger(streamFlushIntervalMs) || streamFlushIntervalMs < 0) throw new Error("world-celld: streamFlushIntervalMs must be a non-negative safe integer");
11
18
  return {
12
19
  fleetUrl: config?.fleetUrl ?? process.env.CELLD_FLEET_URL,
13
20
  secret: config?.secret ?? process.env.CELLD_WORLD_SECRET,
@@ -16,8 +23,9 @@ function resolveConfig(config) {
16
23
  baseUrl: config?.baseUrl,
17
24
  queueShards: config?.queueShards ?? 1,
18
25
  runRetentionMs,
19
- readPollMs: config?.readPollMs ?? 250,
20
- rpcTimeoutMs: config?.rpcTimeoutMs ?? 3e4
26
+ streamLongPollMs,
27
+ streamFlushIntervalMs,
28
+ rpcTimeoutMs
21
29
  };
22
30
  }
23
31
  //#endregion
@@ -46,13 +54,13 @@ var FleetTransportError = class extends Error {
46
54
  //#endregion
47
55
  //#region src/remote/rpc-client.ts
48
56
  /** Statuses that mean "the fleet couldn't take the request" (safe to retry reads). */
49
- const RETRYABLE_STATUSES = /* @__PURE__ */ new Set([
57
+ const RETRYABLE_STATUSES$1 = /* @__PURE__ */ new Set([
50
58
  502,
51
59
  503,
52
60
  504
53
61
  ]);
54
- const DEFAULT_TIMEOUT_MS = 3e4;
55
- function delayMs(ms) {
62
+ const DEFAULT_TIMEOUT_MS$1 = 3e4;
63
+ function delayMs$1(ms) {
56
64
  return new Promise((resolve) => setTimeout(resolve, ms));
57
65
  }
58
66
  /**
@@ -64,12 +72,16 @@ function delayMs(ms) {
64
72
  * via queue redelivery. Read-only calls retry on transport failures.
65
73
  */
66
74
  async function callDO(transport, binding, name, method, args, opts) {
75
+ return await callFleetRoute(transport, `/v1/rpc/${binding}/${encodeURIComponent(name)}/${method}`, args, opts);
76
+ }
77
+ /** Invoke a fixed authenticated worker route using the generic RPC codec. */
78
+ async function callFleetRoute(transport, path, args, opts) {
67
79
  const doFetch = transport.fetchImpl ?? fetch;
68
- const url = `${transport.fleetUrl.replace(/\/$/, "")}/v1/rpc/${binding}/${encodeURIComponent(name)}/${method}`;
80
+ const url = `${transport.fleetUrl.replace(/\/$/, "")}${path}`;
69
81
  const attempts = opts?.idempotent ? 3 : 1;
70
82
  let lastError;
71
83
  for (let attempt = 1; attempt <= attempts; attempt++) {
72
- if (attempt > 1) await delayMs(100 * attempt + Math.random() * 200);
84
+ if (attempt > 1) await delayMs$1(100 * attempt + Math.random() * 200);
73
85
  let response;
74
86
  try {
75
87
  response = await doFetch(url, {
@@ -79,7 +91,7 @@ async function callDO(transport, binding, name, method, args, opts) {
79
91
  authorization: `Bearer ${transport.secret}`
80
92
  },
81
93
  body: rpcStringify(args),
82
- signal: AbortSignal.timeout(Math.max(1, transport.timeoutMs ?? DEFAULT_TIMEOUT_MS))
94
+ signal: AbortSignal.timeout(Math.max(1, transport.timeoutMs ?? DEFAULT_TIMEOUT_MS$1))
83
95
  });
84
96
  } catch (error) {
85
97
  lastError = new FleetTransportError(`world-celld: fleet unreachable at ${url}`, error);
@@ -99,7 +111,7 @@ async function callDO(transport, binding, name, method, args, opts) {
99
111
  } catch {
100
112
  wire = { message: text || `HTTP ${response.status}` };
101
113
  }
102
- if (RETRYABLE_STATUSES.has(response.status) && attempt < attempts) {
114
+ if (RETRYABLE_STATUSES$1.has(response.status) && attempt < attempts) {
103
115
  lastError = reconstructError(wire, response.status);
104
116
  continue;
105
117
  }
@@ -108,6 +120,140 @@ async function callDO(transport, binding, name, method, args, opts) {
108
120
  throw lastError instanceof Error ? lastError : new FleetTransportError("world-celld: rpc failed", lastError);
109
121
  }
110
122
  //#endregion
123
+ //#region src/remote/stream-client.ts
124
+ const RETRYABLE_STATUSES = /* @__PURE__ */ new Set([
125
+ 502,
126
+ 503,
127
+ 504
128
+ ]);
129
+ const READ_ATTEMPTS = 3;
130
+ const DEFAULT_TIMEOUT_MS = 3e4;
131
+ const MAX_WRITE_RESPONSE_BYTES = 64;
132
+ const MAX_READ_RESPONSE_BYTES = MAX_STREAM_READ_BYTES + 65536;
133
+ const MAX_ERROR_RESPONSE_BYTES = 65536;
134
+ function delayMs(ms) {
135
+ return new Promise((resolve) => setTimeout(resolve, ms));
136
+ }
137
+ function requestSignal(timeoutMs, signal) {
138
+ const timeout = AbortSignal.timeout(Math.max(1, timeoutMs));
139
+ return signal ? AbortSignal.any([signal, timeout]) : timeout;
140
+ }
141
+ function aborted(signal) {
142
+ return signal.reason ?? new DOMException("The operation was aborted", "AbortError");
143
+ }
144
+ async function readBoundedResponse(response, maxBytes) {
145
+ if (Number(response.headers.get("content-length") ?? "0") > maxBytes) {
146
+ await response.body?.cancel("stream response exceeds configured limit").catch(() => void 0);
147
+ throw new FleetTransportError(`world-celld: stream response exceeds ${maxBytes} bytes`);
148
+ }
149
+ if (!response.body) return /* @__PURE__ */ new Uint8Array();
150
+ const reader = response.body.getReader();
151
+ const chunks = [];
152
+ let total = 0;
153
+ try {
154
+ for (;;) {
155
+ const { done, value } = await reader.read();
156
+ if (done) break;
157
+ total += value.byteLength;
158
+ if (total > maxBytes) {
159
+ await reader.cancel("stream response exceeds configured limit").catch(() => void 0);
160
+ throw new FleetTransportError(`world-celld: stream response exceeds ${maxBytes} bytes`);
161
+ }
162
+ chunks.push(value);
163
+ }
164
+ } finally {
165
+ reader.releaseLock();
166
+ }
167
+ const body = new Uint8Array(total);
168
+ let offset = 0;
169
+ for (const chunk of chunks) {
170
+ body.set(chunk, offset);
171
+ offset += chunk.byteLength;
172
+ }
173
+ return body;
174
+ }
175
+ async function errorFromResponse(response) {
176
+ const text = new TextDecoder().decode(await readBoundedResponse(response, MAX_ERROR_RESPONSE_BYTES));
177
+ let wire;
178
+ try {
179
+ wire = JSON.parse(text).error;
180
+ } catch {
181
+ wire = { message: text || `HTTP ${response.status}` };
182
+ }
183
+ return reconstructError(wire, response.status);
184
+ }
185
+ function streamUrl(transport, name) {
186
+ return `${transport.fleetUrl.replace(/\/$/, "")}/v1/streams/${encodeURIComponent(name)}/chunks`;
187
+ }
188
+ async function writeStreamChunks(transport, name, runId, chunks) {
189
+ const body = encodeStreamWriteBatch(chunks);
190
+ if (body.byteLength > 8389632) throw new Error("world-celld: encoded stream batch exceeds configured limit");
191
+ const url = new URL(streamUrl(transport, name));
192
+ url.searchParams.set("runId", runId);
193
+ const doFetch = transport.fetchImpl ?? fetch;
194
+ let response;
195
+ try {
196
+ response = await doFetch(url, {
197
+ method: "POST",
198
+ headers: {
199
+ "content-type": STREAM_BATCH_CONTENT_TYPE,
200
+ authorization: `Bearer ${transport.secret}`
201
+ },
202
+ body,
203
+ signal: requestSignal(transport.timeoutMs ?? DEFAULT_TIMEOUT_MS)
204
+ });
205
+ } catch (error) {
206
+ throw new FleetTransportError(`world-celld: fleet unreachable at ${url.href}`, error);
207
+ }
208
+ if (!response.ok) throw await errorFromResponse(response);
209
+ try {
210
+ return decodeStreamWriteResult(await readBoundedResponse(response, MAX_WRITE_RESPONSE_BYTES));
211
+ } catch (error) {
212
+ if (error instanceof FleetTransportError) throw error;
213
+ throw new FleetTransportError(`world-celld: malformed stream response from ${url.href}`, error);
214
+ }
215
+ }
216
+ async function readStreamChunks(transport, name, request, signal) {
217
+ const url = new URL(streamUrl(transport, name));
218
+ url.searchParams.set("runId", request.runId);
219
+ url.searchParams.set("startIndex", String(request.startIndex));
220
+ url.searchParams.set("maxChunks", String(request.maxChunks));
221
+ url.searchParams.set("maxBytes", String(request.maxBytes));
222
+ url.searchParams.set("waitMs", String(request.waitMs));
223
+ const doFetch = transport.fetchImpl ?? fetch;
224
+ let lastError;
225
+ for (let attempt = 1; attempt <= READ_ATTEMPTS; attempt++) {
226
+ if (signal?.aborted) throw aborted(signal);
227
+ if (attempt > 1) await delayMs(100 * attempt + Math.random() * 200);
228
+ let response;
229
+ try {
230
+ response = await doFetch(url, {
231
+ method: "GET",
232
+ headers: { authorization: `Bearer ${transport.secret}` },
233
+ signal: requestSignal(transport.timeoutMs ?? DEFAULT_TIMEOUT_MS, signal)
234
+ });
235
+ } catch (error) {
236
+ if (signal?.aborted) throw aborted(signal);
237
+ lastError = new FleetTransportError(`world-celld: fleet unreachable at ${url.href}`, error);
238
+ continue;
239
+ }
240
+ if (response.ok) try {
241
+ return decodeStreamReadResult(await readBoundedResponse(response, MAX_READ_RESPONSE_BYTES));
242
+ } catch (error) {
243
+ lastError = new FleetTransportError(`world-celld: malformed stream response from ${url.href}`, error);
244
+ if (attempt < READ_ATTEMPTS) continue;
245
+ throw lastError;
246
+ }
247
+ const error = await errorFromResponse(response);
248
+ if (RETRYABLE_STATUSES.has(response.status) && attempt < READ_ATTEMPTS) {
249
+ lastError = error;
250
+ continue;
251
+ }
252
+ throw error;
253
+ }
254
+ throw lastError instanceof Error ? lastError : new FleetTransportError("world-celld: stream read failed", lastError);
255
+ }
256
+ //#endregion
111
257
  //#region src/remote/namespaces.ts
112
258
  const RUNS = {
113
259
  methods: [
@@ -121,7 +267,8 @@ const RUNS = {
121
267
  "getCleanupStatus",
122
268
  "scheduleCleanup",
123
269
  "cleanupNow",
124
- "rearmCleanup"
270
+ "rearmCleanup",
271
+ "resolveHookTokenClaim"
125
272
  ],
126
273
  mutating: /* @__PURE__ */ new Set([
127
274
  "applyEvent",
@@ -132,10 +279,8 @@ const RUNS = {
132
279
  };
133
280
  const STREAMS = {
134
281
  methods: [
135
- "writeChunk",
136
282
  "closeStream",
137
- "getChunks",
138
- "getInfo",
283
+ "failStream",
139
284
  "registerStream",
140
285
  "listStreams",
141
286
  "expireRegistry",
@@ -143,14 +288,28 @@ const STREAMS = {
143
288
  "expireStream"
144
289
  ],
145
290
  mutating: /* @__PURE__ */ new Set([
146
- "writeChunk",
147
291
  "closeStream",
292
+ "failStream",
148
293
  "registerStream",
149
294
  "expireRegistry",
150
295
  "finalizeRegistry",
151
296
  "expireStream"
152
297
  ])
153
298
  };
299
+ function makeStreamNamespace(transport) {
300
+ return {
301
+ idFromName(name) {
302
+ return { toString: () => name };
303
+ },
304
+ get(id) {
305
+ const name = id.toString();
306
+ const control = makeStub(transport, "streams", name, STREAMS);
307
+ control.writeChunks = (runId, chunks) => writeStreamChunks(transport, name, runId, chunks);
308
+ control.readChunks = (request, signal) => readStreamChunks(transport, name, request, signal);
309
+ return control;
310
+ }
311
+ };
312
+ }
154
313
  const QUEUE = {
155
314
  methods: [
156
315
  "enqueue",
@@ -184,41 +343,76 @@ function makeNamespace(transport, binding, spec) {
184
343
  }
185
344
  };
186
345
  }
187
- /** The IndexDO is a single well-known cell. */
188
- const INDEX_CELL_NAME = "index";
189
- function makeIndexNamespace(transport) {
190
- const call = (method, args, idempotent) => callDO(transport, "index", INDEX_CELL_NAME, method, args, { idempotent });
191
- return {
192
- get: (key) => call("get", [key], true),
193
- put: (key, value) => call("put", [key, value], false),
194
- delete: (key) => call("delete", [key], false),
195
- putOwned: (runId, key, value) => call("putOwned", [
196
- runId,
197
- key,
198
- value
199
- ], false),
200
- expireRun: (request) => call("expireRun", [request], false),
201
- list: (options) => call("list", [options], true),
202
- reserveHookToken: (token, owner) => call("reserveHookToken", [token, owner], false),
203
- finalizeHookIndexes: (token, hookId, serializedHook, owner) => call("finalizeHookIndexes", [
204
- token,
205
- hookId,
206
- serializedHook,
207
- owner
208
- ], false),
209
- releaseHookToken: (token, owner) => call("releaseHookToken", [token, owner], false),
210
- deleteHookIndexes: (token, hookId, owner) => call("deleteHookIndexes", [
211
- token,
212
- hookId,
213
- owner
214
- ], false)
215
- };
216
- }
346
+ const RUN_CATALOG = {
347
+ methods: [
348
+ "upsertRun",
349
+ "list",
350
+ "expireRun"
351
+ ],
352
+ mutating: /* @__PURE__ */ new Set()
353
+ };
354
+ const RUN_FENCES = {
355
+ methods: [
356
+ "getStatus",
357
+ "fenceTerminal",
358
+ "fenceExpired"
359
+ ],
360
+ mutating: /* @__PURE__ */ new Set()
361
+ };
362
+ const HOOK_TOKENS = {
363
+ methods: [
364
+ "get",
365
+ "reserve",
366
+ "finalize",
367
+ "releaseClaim",
368
+ "releaseBatch"
369
+ ],
370
+ mutating: /* @__PURE__ */ new Set()
371
+ };
372
+ const HOOK_IDS = {
373
+ methods: [
374
+ "get",
375
+ "reserve",
376
+ "publish",
377
+ "releaseClaim",
378
+ "releaseBatch"
379
+ ],
380
+ mutating: /* @__PURE__ */ new Set()
381
+ };
217
382
  function createRemoteEnv(transport) {
383
+ const runCatalog = makeNamespace(transport, "run-catalog", RUN_CATALOG);
384
+ const runFences = makeNamespace(transport, "run-fences", RUN_FENCES);
385
+ const hookTokens = makeNamespace(transport, "hook-tokens", HOOK_TOKENS);
386
+ const hookIds = makeNamespace(transport, "hook-ids", HOOK_IDS);
387
+ const workflowIndex = createWorkflowIndex({
388
+ runCatalog,
389
+ runFences,
390
+ hookTokens,
391
+ hookIds
392
+ });
218
393
  return {
219
394
  WORKFLOW_DB: makeNamespace(transport, "runs", RUNS),
220
- WORKFLOW_STREAMS: makeNamespace(transport, "streams", STREAMS),
221
- WORKFLOW_INDEX: makeIndexNamespace(transport),
395
+ WORKFLOW_STREAMS: makeStreamNamespace(transport),
396
+ WORKFLOW_INDEX: {
397
+ ...workflowIndex,
398
+ commitRun: (run, serializedMetadata) => callFleetRoute(transport, "/v1/index/runs/commit", [run, serializedMetadata], { idempotent: true }),
399
+ listRuns: (options) => callFleetRoute(transport, "/v1/index/runs/list", [options], { idempotent: true }),
400
+ expireRun: (request) => callFleetRoute(transport, "/v1/index/runs/expire", [request], { idempotent: true }),
401
+ reserveHook: (token, owner) => callFleetRoute(transport, "/v1/index/hooks/reserve", [token, owner], { idempotent: true }),
402
+ finalizeHookIndexes: (token, hookId, serializedHook, owner, reservation) => callFleetRoute(transport, "/v1/index/hooks/finalize", [
403
+ token,
404
+ hookId,
405
+ serializedHook,
406
+ owner,
407
+ reservation
408
+ ], { idempotent: true }),
409
+ releaseHookReservation: (token, owner, reservation) => callFleetRoute(transport, "/v1/index/hooks/release-reservation", [
410
+ token,
411
+ owner,
412
+ reservation
413
+ ], { idempotent: true }),
414
+ releaseHookIndexes: (request) => callFleetRoute(transport, "/v1/index/hooks/release", [request], { idempotent: true })
415
+ },
222
416
  WORKFLOW_QUEUE: makeNamespace(transport, "queue", QUEUE)
223
417
  };
224
418
  }
@@ -294,10 +488,7 @@ function isPermanentError(err) {
294
488
  function computeBackoff(attempt) {
295
489
  return Math.min(60, 2 ** attempt);
296
490
  }
297
- const QUEUE_PATHNAMES = {
298
- workflow: "flow",
299
- step: "step"
300
- };
491
+ const QUEUE_PATHNAME = "flow";
301
492
  function isTestMode() {
302
493
  if (process.env.CELLD_QUEUE_MODE === "cells") return false;
303
494
  return process.env.VITEST === "true" || process.env.NODE_ENV === "test";
@@ -321,9 +512,8 @@ function shardFor(key, shards) {
321
512
  return (hash >>> 0) % shards;
322
513
  }
323
514
  /**
324
- * In-process test pump. Holds two in-memory FIFOs and HTTP-dispatches
325
- * envelopes to the user's server at
326
- * `${baseUrl}/.well-known/workflow/v1/{flow|step}`.
515
+ * In-process test pump. Holds an in-memory FIFO and HTTP-dispatches envelopes
516
+ * to the user's server at `${baseUrl}/.well-known/workflow/v1/flow`.
327
517
  *
328
518
  * Mirrors world-local's idempotency semantics: messages are deduplicated on
329
519
  * `idempotencyKey` while a message with the same key is in flight, and the
@@ -337,14 +527,8 @@ function createTestPump(config) {
337
527
  const httpTimeoutMs = config.httpTimeoutMs ?? 3e5;
338
528
  const maxAttempts = config.maxAttempts ?? 5;
339
529
  const baseBackoffMs = config.backoffDelayMs ?? 1e3;
340
- const queues = {
341
- flow: [],
342
- step: []
343
- };
344
- const wakers = {
345
- flow: [],
346
- step: []
347
- };
530
+ const queues = { flow: [] };
531
+ const wakers = { flow: [] };
348
532
  /** Inflight messageIds by idempotencyKey (world-local queue.js semantics). */
349
533
  const inflightMessages = /* @__PURE__ */ new Map();
350
534
  let running = false;
@@ -376,6 +560,9 @@ function createTestPump(config) {
376
560
  signal: AbortSignal.timeout(httpTimeoutMs)
377
561
  });
378
562
  if (response.ok) {
563
+ try {
564
+ await response.body?.cancel();
565
+ } catch {}
379
566
  release(envelope);
380
567
  return;
381
568
  }
@@ -437,7 +624,6 @@ function createTestPump(config) {
437
624
  if (running) return;
438
625
  running = true;
439
626
  loop("flow");
440
- loop("step");
441
627
  },
442
628
  stop() {
443
629
  running = false;
@@ -456,13 +642,13 @@ function createQueue(config) {
456
642
  };
457
643
  return {
458
644
  async queue(queueName, message, opts) {
459
- const { kind } = parseQueueName(queueName);
460
- const runId = "runId" in message ? message.runId : "workflowRunId" in message ? message.workflowRunId : void 0;
645
+ parseQueueName(queueName);
646
+ const runId = "runId" in message && typeof message.runId === "string" ? message.runId : void 0;
461
647
  if (isTestMode()) {
462
648
  const existing = testPump.inflight(opts?.idempotencyKey);
463
649
  if (existing) return { messageId: existing };
464
650
  const messageId = MessageId.parse(`msg_${generateMessageId()}`);
465
- testPump.push(QUEUE_PATHNAMES[kind], {
651
+ testPump.push(QUEUE_PATHNAME, {
466
652
  messageId,
467
653
  queueName,
468
654
  attempt: 1,
@@ -476,7 +662,7 @@ function createQueue(config) {
476
662
  const outcome = await getQueueStub(shard).enqueue({
477
663
  messageId,
478
664
  queueName,
479
- pathname: QUEUE_PATHNAMES[kind],
665
+ pathname: QUEUE_PATHNAME,
480
666
  body: stringify(message),
481
667
  runId,
482
668
  idempotencyKey: opts?.idempotencyKey,
@@ -510,7 +696,7 @@ function createQueue(config) {
510
696
  status: 503,
511
697
  headers: { "Retry-After": String(result.timeoutSeconds) }
512
698
  });
513
- return Response.json({ ok: true });
699
+ return new Response(null, { status: 204 });
514
700
  } catch (error) {
515
701
  if (isPermanentError(error)) {
516
702
  debug("[world-celld queue handler] permanent error:", {
@@ -589,11 +775,12 @@ function throwOutcomeError(outcome, runId, data) {
589
775
  case "RUN_NOT_FOUND": throw new WorkflowRunNotFoundError(runId);
590
776
  case "STEP_NOT_FOUND": throw new WorkflowWorldError(outcome.message, { status: 404 });
591
777
  case "HOOK_NOT_FOUND": throw new HookNotFoundError(data.correlationId ?? runId);
778
+ case "WAIT_NOT_FOUND": throw new WorkflowWorldError(outcome.message, { status: 404 });
592
779
  case "ENTITY_CONFLICT": throw new EntityConflictError(outcome.message);
780
+ case "HOOK_CLAIM_CANCELLED": throw new WorkflowWorldError(outcome.message, { status: 503 });
593
781
  case "RUN_EXPIRED": throw new RunExpiredError(outcome.message);
594
782
  case "TOO_EARLY": throw new TooEarlyError(outcome.message, { retryAfter: outcome.retryAfterSeconds });
595
783
  case "RUN_NOT_SUPPORTED": throw new RunNotSupportedError(outcome.runSpecVersion ?? 0, SPEC_VERSION_CURRENT);
596
- case "LEGACY_RUN_NOT_SUPPORTED": throw new WorkflowWorldError(outcome.message, { status: 422 });
597
784
  }
598
785
  }
599
786
  const parseRun = (run) => WorkflowRunSchema.parse(compact(run));
@@ -604,6 +791,19 @@ function unwrapRead(outcome) {
604
791
  if (!outcome.ok) throw new RunExpiredError(outcome.message);
605
792
  return outcome.value;
606
793
  }
794
+ /** Bound cross-run fanout so large list pages cannot create an RPC burst. */
795
+ const RUN_LIST_CONCURRENCY = 8;
796
+ /**
797
+ * Run statuses only move pending -> running -> terminal, and terminal statuses
798
+ * are immutable. An older index value may therefore safely exclude a filter
799
+ * only when it is already terminal, or when the caller asks for pending and
800
+ * the index has advanced beyond pending. Earlier non-terminal metadata cannot
801
+ * exclude a later status because a post-commit index write may need replay.
802
+ */
803
+ function indexStatusExcludes(indexed, requested) {
804
+ if (requested === void 0 || indexed === requested) return false;
805
+ return isTerminalWorkflowRunStatus(indexed) || requested === "pending";
806
+ }
607
807
  function createStorage(config) {
608
808
  const { env } = config;
609
809
  const cleanup = {
@@ -631,7 +831,7 @@ function createStorage(config) {
631
831
  let scanCursor = params?.pagination?.cursor;
632
832
  let exhausted = false;
633
833
  while (matches.length <= limit && !exhausted) {
634
- const kvList = await env.WORKFLOW_INDEX.list({
834
+ const kvList = await env.WORKFLOW_INDEX.listRuns({
635
835
  prefix,
636
836
  limit: Math.min(1e3, Math.max(50, limit)),
637
837
  cursor: scanCursor,
@@ -641,22 +841,32 @@ function createStorage(config) {
641
841
  exhausted = true;
642
842
  break;
643
843
  }
844
+ const candidates = [];
644
845
  for (const key of kvList.keys) {
645
- const meta = await env.WORKFLOW_INDEX.get(key.name);
646
- if (!meta) continue;
647
- const { runId } = JSON.parse(meta);
648
- try {
649
- const run = await runsGet(runId, { resolveData: params?.resolveData });
650
- if (!params?.status || run.status === params.status) {
651
- matches.push({
652
- key: key.name,
846
+ const metadata = JSON.parse(key.value);
847
+ if (indexStatusExcludes(metadata.status, params?.status)) continue;
848
+ candidates.push({
849
+ key: key.name,
850
+ metadata
851
+ });
852
+ }
853
+ for (let offset = 0; offset < candidates.length && matches.length <= limit;) {
854
+ const needed = limit + 1 - matches.length;
855
+ const batch = candidates.slice(offset, offset + Math.min(RUN_LIST_CONCURRENCY, needed));
856
+ const resolved = await Promise.all(batch.map(async ({ key, metadata }) => {
857
+ try {
858
+ const run = await runsGet(metadata.runId, { resolveData: params?.resolveData });
859
+ return !params?.status || run.status === params.status ? {
860
+ key,
653
861
  run
654
- });
655
- if (matches.length > limit) break;
862
+ } : null;
863
+ } catch (error) {
864
+ if (!WorkflowRunNotFoundError.is(error) && !RunExpiredError.is(error)) throw error;
865
+ return null;
656
866
  }
657
- } catch (error) {
658
- if (!WorkflowRunNotFoundError.is(error) && !RunExpiredError.is(error)) throw error;
659
- }
867
+ }));
868
+ for (const match of resolved) if (match) matches.push(match);
869
+ offset += batch.length;
660
870
  }
661
871
  exhausted = kvList.list_complete;
662
872
  scanCursor = kvList.cursor ?? kvList.keys.at(-1)?.name;
@@ -671,36 +881,57 @@ function createStorage(config) {
671
881
  }
672
882
  },
673
883
  events: {
674
- async create(runId, data, _params) {
884
+ async create(runId, data, params) {
675
885
  let effectiveRunId;
676
886
  if (data.eventType === "run_created" && (!runId || runId === "")) effectiveRunId = `wrun_${ulid()}`;
677
887
  else if (!runId) throw new WorkflowWorldError("runId is required for non-run_created events", { status: 400 });
678
888
  else effectiveRunId = runId;
679
889
  const stub = getRunDO(effectiveRunId);
680
890
  let tokenHolder;
681
- let hookReservation;
891
+ let hookAdmission;
682
892
  if (data.eventType === "hook_created") {
683
- hookReservation = {
893
+ const owner = {
684
894
  runId: effectiveRunId,
685
895
  hookId: data.correlationId
686
896
  };
687
- const reservation = await env.WORKFLOW_INDEX.reserveHookToken(data.eventData.token, hookReservation);
688
- tokenHolder = reservation.claimed ? null : reservation.holder;
897
+ const admission = await env.WORKFLOW_INDEX.reserveHook(data.eventData.token, owner);
898
+ if (admission.admitted) {
899
+ tokenHolder = null;
900
+ hookAdmission = {
901
+ owner,
902
+ reservation: admission.reservation,
903
+ token: data.eventData.token
904
+ };
905
+ } else tokenHolder = admission.holder;
689
906
  }
690
907
  let outcome;
691
908
  try {
692
909
  outcome = await stub.applyEvent({
693
910
  runId: effectiveRunId,
694
911
  data,
912
+ params,
695
913
  tokenHolder,
914
+ hookClaimId: hookAdmission?.reservation.claimId,
696
915
  cleanup
697
916
  });
698
917
  } catch (error) {
699
- if (hookReservation && data.eventType === "hook_created") await env.WORKFLOW_INDEX.releaseHookToken(data.eventData.token, hookReservation);
918
+ if (hookAdmission) {
919
+ let resolution;
920
+ try {
921
+ resolution = await stub.resolveHookTokenClaim({
922
+ hookId: hookAdmission.owner.hookId,
923
+ token: hookAdmission.token,
924
+ claimId: hookAdmission.reservation.claimId
925
+ });
926
+ } catch {
927
+ throw error;
928
+ }
929
+ if (!resolution.committed) await env.WORKFLOW_INDEX.releaseHookReservation(hookAdmission.token, hookAdmission.owner, hookAdmission.reservation);
930
+ }
700
931
  throw error;
701
932
  }
702
933
  if (!outcome.ok) {
703
- if (hookReservation && data.eventType === "hook_created") await env.WORKFLOW_INDEX.releaseHookToken(data.eventData.token, hookReservation);
934
+ if (hookAdmission) await env.WORKFLOW_INDEX.releaseHookReservation(hookAdmission.token, hookAdmission.owner, hookAdmission.reservation);
704
935
  throwOutcomeError(outcome, effectiveRunId, data);
705
936
  }
706
937
  if (outcome.run) {
@@ -709,30 +940,32 @@ function createStorage(config) {
709
940
  createdAt: outcome.run.createdAt.toISOString(),
710
941
  status: outcome.run.status
711
942
  });
712
- await env.WORKFLOW_INDEX.putOwned(effectiveRunId, workflowRunIndexKey(outcome.run), meta);
713
- await env.WORKFLOW_INDEX.putOwned(effectiveRunId, globalRunIndexKey(outcome.run), meta);
943
+ await env.WORKFLOW_INDEX.commitRun(outcome.run, meta);
714
944
  }
715
945
  if (outcome.hookToIndex) {
716
946
  const serialized = stringify(outcome.hookToIndex);
717
- await env.WORKFLOW_INDEX.finalizeHookIndexes(outcome.hookToIndex.token, outcome.hookToIndex.hookId, serialized, hookOwner(outcome.hookToIndex));
718
- } else if (hookReservation && data.eventType === "hook_created") await env.WORKFLOW_INDEX.releaseHookToken(data.eventData.token, hookReservation);
719
- for (const released of outcome.releasedHooks) await env.WORKFLOW_INDEX.deleteHookIndexes(released.token, released.hookId, {
947
+ await env.WORKFLOW_INDEX.finalizeHookIndexes(outcome.hookToIndex.token, outcome.hookToIndex.hookId, serialized, hookOwner(outcome.hookToIndex), hookAdmission?.reservation);
948
+ } else if (hookAdmission) await env.WORKFLOW_INDEX.releaseHookReservation(hookAdmission.token, hookAdmission.owner, hookAdmission.reservation);
949
+ const terminal = Boolean(outcome.run && isTerminalWorkflowRunStatus(outcome.run.status));
950
+ if (outcome.releasedHooks.length > 0) await env.WORKFLOW_INDEX.releaseHookIndexes({
720
951
  runId: effectiveRunId,
721
- hookId: released.hookId
952
+ hooks: outcome.releasedHooks,
953
+ terminal
722
954
  });
723
- if (outcome.event?.correlationId) {
724
- const correlationKey = correlationIndexKey(outcome.event.correlationId, outcome.event.createdAt, outcome.event.eventId, effectiveRunId);
725
- await env.WORKFLOW_INDEX.putOwned(effectiveRunId, correlationKey, JSON.stringify({
726
- runId: effectiveRunId,
727
- eventId: outcome.event.eventId
728
- }));
729
- }
955
+ const eventPage = outcome.events === void 0 ? {} : {
956
+ events: outcome.events,
957
+ cursor: outcome.cursor ?? null,
958
+ hasMore: outcome.hasMore ?? false
959
+ };
730
960
  return {
731
961
  event: outcome.event,
732
962
  run: outcome.run,
733
963
  step: outcome.step,
734
964
  hook: outcome.hook,
735
- events: outcome.events
965
+ wait: outcome.wait,
966
+ stepCreated: outcome.stepCreated,
967
+ maxEvents: outcome.maxEvents,
968
+ ...eventPage
736
969
  };
737
970
  },
738
971
  async get(runId, eventId, _params) {
@@ -756,23 +989,30 @@ function createStorage(config) {
756
989
  },
757
990
  async listByCorrelationId(params) {
758
991
  const limit = params.pagination?.limit ?? 100;
759
- const prefix = `correlation:${encodeURIComponent(params.correlationId)}:`;
760
- const listed = await env.WORKFLOW_INDEX.list({
761
- prefix,
762
- limit,
763
- cursor: params.pagination?.cursor,
764
- reverse: params.pagination?.sortOrder === "desc"
765
- });
992
+ const stub = getRunDO(params.runId);
993
+ const matches = [];
994
+ let scanCursor = params.pagination?.cursor;
995
+ let exhausted = false;
996
+ while (matches.length <= limit && !exhausted) {
997
+ const page = unwrapRead(await stub.listEvents({
998
+ limit: Math.min(1e3, Math.max(50, limit + 1)),
999
+ cursor: scanCursor,
1000
+ sortOrder: params.pagination?.sortOrder ?? "asc"
1001
+ }));
1002
+ for (const event of page.data) if (event.correlationId === params.correlationId) {
1003
+ matches.push(event);
1004
+ if (matches.length > limit) break;
1005
+ }
1006
+ exhausted = !page.hasMore;
1007
+ scanCursor = page.cursor ?? page.data.at(-1)?.eventId;
1008
+ if (page.data.length === 0) exhausted = true;
1009
+ }
1010
+ const hasMore = matches.length > limit;
1011
+ const data = matches.slice(0, limit);
766
1012
  return {
767
- data: (await Promise.all(listed.keys.map(async ({ name }) => {
768
- const raw = await env.WORKFLOW_INDEX.get(name);
769
- if (!raw) return null;
770
- const { runId, eventId } = JSON.parse(raw);
771
- const outcome = await getRunDO(runId).getEvent(eventId);
772
- return outcome.ok ? outcome.value : null;
773
- }))).filter((event) => event !== null).map(parseEvent),
774
- cursor: listed.list_complete ? null : listed.cursor ?? null,
775
- hasMore: !listed.list_complete
1013
+ data: data.map(parseEvent),
1014
+ cursor: hasMore ? data.at(-1)?.eventId ?? null : null,
1015
+ hasMore
776
1016
  };
777
1017
  }
778
1018
  },
@@ -800,12 +1040,12 @@ function createStorage(config) {
800
1040
  },
801
1041
  hooks: {
802
1042
  async get(hookId, params) {
803
- const raw = await env.WORKFLOW_INDEX.get(`hookid:${hookId}`);
1043
+ const raw = await env.WORKFLOW_INDEX.getHookById(hookId);
804
1044
  if (!raw) throw new HookNotFoundError(hookId);
805
1045
  return filterHookData(parseHook(parse(raw)), params?.resolveData ?? "all");
806
1046
  },
807
1047
  async getByToken(token, params) {
808
- const raw = await env.WORKFLOW_INDEX.get(`hook:${token}`);
1048
+ const raw = await env.WORKFLOW_INDEX.getHookByToken(token);
809
1049
  if (!raw) throw new HookNotFoundError(token);
810
1050
  return filterHookData(parseHook(parse(raw)), params?.resolveData ?? "all");
811
1051
  },
@@ -829,19 +1069,27 @@ function createStorage(config) {
829
1069
  }
830
1070
  //#endregion
831
1071
  //#region src/streamer.ts
832
- /** Default poll interval while waiting for new chunks on a live stream. */
833
- const READ_POLL_MS = 100;
834
- /** Chunks fetched per DO round-trip while reading. */
835
- const READ_BATCH_SIZE = 32;
836
- /** Default / maximum page sizes for getStreamChunks. */
1072
+ const DEFAULT_LONG_POLL_MS = MAX_STREAM_LONG_POLL_MS;
837
1073
  const DEFAULT_CHUNK_PAGE_SIZE = 32;
838
1074
  const MAX_CHUNK_PAGE_SIZE = 32;
1075
+ const REGISTERED_STREAM_CACHE_SIZE = 4096;
839
1076
  function toBytes(chunk) {
840
1077
  return typeof chunk === "string" ? new TextEncoder().encode(chunk) : chunk;
841
1078
  }
1079
+ function throwTerminal(result) {
1080
+ if (result.state === "expired") throw new Error("Stream has expired");
1081
+ if (result.state === "errored") {
1082
+ const error = new Error(result.error?.message ?? "Stream failed");
1083
+ error.name = result.error?.name ?? "Error";
1084
+ throw error;
1085
+ }
1086
+ }
842
1087
  function createStreamer(config) {
843
1088
  const { env } = config;
844
- const readPollMs = config.readPollMs ?? READ_POLL_MS;
1089
+ const streamLongPollMs = config.streamLongPollMs ?? DEFAULT_LONG_POLL_MS;
1090
+ if (!Number.isSafeInteger(streamLongPollMs) || streamLongPollMs < 1 || streamLongPollMs > 2e4) throw new Error(`streamLongPollMs must be between 1 and ${MAX_STREAM_LONG_POLL_MS}`);
1091
+ const streamFlushIntervalMs = config.streamFlushIntervalMs ?? 0;
1092
+ if (!Number.isSafeInteger(streamFlushIntervalMs) || streamFlushIntervalMs < 0) throw new Error("streamFlushIntervalMs must be a non-negative safe integer");
845
1093
  const getStreamDO = (streamName) => {
846
1094
  const id = env.WORKFLOW_STREAMS.idFromName(`stream:${streamName}`);
847
1095
  return env.WORKFLOW_STREAMS.get(id);
@@ -850,83 +1098,161 @@ function createStreamer(config) {
850
1098
  const id = env.WORKFLOW_STREAMS.idFromName(`run-streams:${runId}`);
851
1099
  return env.WORKFLOW_STREAMS.get(id);
852
1100
  };
853
- /** Per-isolate cache so each (runId, stream) pair registers only once. */
1101
+ /** Bounded per-isolate cache; eviction only repeats idempotent registration. */
854
1102
  const registeredStreams = /* @__PURE__ */ new Set();
855
1103
  async function registerStreamForRun(name, runId) {
856
1104
  const cacheKey = `${runId}\0${name}`;
857
1105
  if (registeredStreams.has(cacheKey)) return;
858
1106
  await getRunRegistryDO(runId).registerStream(runId, name);
1107
+ if (registeredStreams.size >= REGISTERED_STREAM_CACHE_SIZE) {
1108
+ const oldest = registeredStreams.values().next().value;
1109
+ if (oldest !== void 0) registeredStreams.delete(oldest);
1110
+ }
859
1111
  registeredStreams.add(cacheKey);
860
1112
  }
861
- return {
862
- async writeToStream(name, runId, chunk) {
863
- const resolvedRunId = await runId;
864
- await registerStreamForRun(name, resolvedRunId);
865
- await getStreamDO(name).writeChunk(resolvedRunId, toBytes(chunk));
866
- },
867
- async closeStream(name, runId) {
868
- const resolvedRunId = await runId;
869
- await registerStreamForRun(name, resolvedRunId);
870
- await getStreamDO(name).closeStream(resolvedRunId);
871
- },
872
- async readFromStream(name, startIndex = 0) {
873
- const stub = getStreamDO(name);
874
- let nextIndex;
875
- if (startIndex < 0) {
876
- const info = await stub.getInfo();
877
- nextIndex = Math.max(0, info.tailIndex + 1 + startIndex);
878
- } else nextIndex = startIndex;
879
- let cancelled = false;
880
- return new ReadableStream({
881
- async pull(controller) {
882
- while (true) {
883
- if (cancelled) return;
884
- const { chunks, done } = await stub.getChunks({
1113
+ const writeChunksToStream = async (name, runId, input) => {
1114
+ if (input.length === 0) return;
1115
+ const resolvedRunId = await runId;
1116
+ const chunks = input.map(toBytes);
1117
+ for (const chunk of chunks) if (chunk.byteLength > 1048576) throw new Error(`Stream chunk exceeds ${MAX_STREAM_CHUNK_BYTES} bytes`);
1118
+ await registerStreamForRun(name, resolvedRunId);
1119
+ let batch = [];
1120
+ let batchBytes = 0;
1121
+ for (const chunk of chunks) {
1122
+ if (batch.length > 0 && (batch.length === 32 || batchBytes + chunk.byteLength > 8388608)) {
1123
+ await getStreamDO(name).writeChunks(resolvedRunId, batch);
1124
+ batch = [];
1125
+ batchBytes = 0;
1126
+ }
1127
+ batch.push(chunk);
1128
+ batchBytes += chunk.byteLength;
1129
+ }
1130
+ if (batch.length > 0) await getStreamDO(name).writeChunks(resolvedRunId, batch);
1131
+ };
1132
+ const writeToStream = (name, runId, chunk) => writeChunksToStream(name, runId, [chunk]);
1133
+ const closeStream = async (name, runId) => {
1134
+ const resolvedRunId = await runId;
1135
+ await registerStreamForRun(name, resolvedRunId);
1136
+ await getStreamDO(name).closeStream(resolvedRunId);
1137
+ };
1138
+ const errorStream = async (name, runId, error) => {
1139
+ const resolvedRunId = await runId;
1140
+ await registerStreamForRun(name, resolvedRunId);
1141
+ await getStreamDO(name).failStream(resolvedRunId, normalizeStreamError(error));
1142
+ };
1143
+ const readFromStream = async (name, runId, startIndex = 0) => {
1144
+ const stub = getStreamDO(name);
1145
+ let nextIndex;
1146
+ if (startIndex < 0) {
1147
+ const info = await stub.readChunks({
1148
+ runId,
1149
+ startIndex: 0,
1150
+ maxChunks: 0,
1151
+ maxBytes: MAX_STREAM_READ_BYTES,
1152
+ waitMs: 0
1153
+ });
1154
+ throwTerminal(info);
1155
+ nextIndex = Math.max(0, info.tailIndex + 1 + startIndex);
1156
+ } else nextIndex = startIndex;
1157
+ let cancelled = false;
1158
+ const abortController = new AbortController();
1159
+ return new ReadableStream({
1160
+ async pull(controller) {
1161
+ for (;;) {
1162
+ if (cancelled) return;
1163
+ let result;
1164
+ try {
1165
+ result = await stub.readChunks({
1166
+ runId,
885
1167
  startIndex: nextIndex,
886
- limit: READ_BATCH_SIZE
887
- });
888
- if (chunks.length > 0) {
889
- for (const chunk of chunks) controller.enqueue(chunk);
890
- nextIndex += chunks.length;
891
- return;
892
- }
893
- if (done) {
894
- controller.close();
895
- return;
896
- }
897
- await new Promise((resolve) => setTimeout(resolve, readPollMs));
1168
+ maxChunks: 32,
1169
+ maxBytes: MAX_STREAM_READ_BYTES,
1170
+ waitMs: streamLongPollMs
1171
+ }, abortController.signal);
1172
+ } catch (error) {
1173
+ if (cancelled || abortController.signal.aborted) return;
1174
+ throw error;
1175
+ }
1176
+ if (result.chunks.length > 0) {
1177
+ for (const chunk of result.chunks) controller.enqueue(chunk);
1178
+ nextIndex += result.chunks.length;
1179
+ return;
1180
+ }
1181
+ throwTerminal(result);
1182
+ if (result.state === "closed") {
1183
+ controller.close();
1184
+ return;
898
1185
  }
899
- },
900
- cancel() {
901
- cancelled = true;
902
1186
  }
903
- });
904
- },
905
- async listStreamsByRunId(runId) {
906
- return getRunRegistryDO(runId).listStreams();
907
- },
908
- async getStreamChunks(name, _runId, options) {
909
- const limit = Math.min(options?.limit ?? DEFAULT_CHUNK_PAGE_SIZE, MAX_CHUNK_PAGE_SIZE);
910
- const startIndex = options?.cursor ? Number.parseInt(options.cursor, 10) : 0;
911
- if (Number.isNaN(startIndex) || startIndex < 0) throw new Error(`Invalid stream cursor: ${options?.cursor}`);
912
- const result = await getStreamDO(name).getChunks({
913
- startIndex,
914
- limit: limit + 1
915
- });
916
- const hasMore = result.chunks.length > limit;
917
- return {
918
- data: result.chunks.slice(0, limit).map((chunk, offset) => ({
919
- index: startIndex + offset,
920
- data: chunk
921
- })),
922
- cursor: hasMore ? String(startIndex + limit) : null,
923
- hasMore,
924
- done: result.done
925
- };
1187
+ },
1188
+ cancel() {
1189
+ cancelled = true;
1190
+ abortController.abort(new DOMException("Stream reader cancelled", "AbortError"));
1191
+ }
1192
+ });
1193
+ };
1194
+ const listStreamsByRunId = (runId) => getRunRegistryDO(runId).listStreams();
1195
+ const getStreamChunks = async (name, runId, options) => {
1196
+ const requestedLimit = options?.limit ?? DEFAULT_CHUNK_PAGE_SIZE;
1197
+ if (!Number.isSafeInteger(requestedLimit) || requestedLimit < 1) throw new Error("Stream chunk limit must be a positive safe integer");
1198
+ const limit = Math.min(requestedLimit, MAX_CHUNK_PAGE_SIZE);
1199
+ const startIndex = options?.cursor ? Number.parseInt(options.cursor, 10) : 0;
1200
+ if (Number.isNaN(startIndex) || startIndex < 0) throw new Error(`Invalid stream cursor: ${options?.cursor}`);
1201
+ const result = await getStreamDO(name).readChunks({
1202
+ runId,
1203
+ startIndex,
1204
+ maxChunks: limit,
1205
+ maxBytes: MAX_STREAM_READ_BYTES,
1206
+ waitMs: 0
1207
+ });
1208
+ throwTerminal(result);
1209
+ const data = result.chunks.map((chunk, offset) => ({
1210
+ index: startIndex + offset,
1211
+ data: chunk
1212
+ }));
1213
+ const nextIndex = startIndex + data.length;
1214
+ const hasMore = nextIndex <= result.tailIndex;
1215
+ return {
1216
+ data,
1217
+ cursor: hasMore ? String(nextIndex) : null,
1218
+ hasMore,
1219
+ done: result.state === "closed"
1220
+ };
1221
+ };
1222
+ const getStreamInfo = async (name, runId) => {
1223
+ const result = await getStreamDO(name).readChunks({
1224
+ runId,
1225
+ startIndex: 0,
1226
+ maxChunks: 0,
1227
+ maxBytes: MAX_STREAM_READ_BYTES,
1228
+ waitMs: 0
1229
+ });
1230
+ throwTerminal(result);
1231
+ return {
1232
+ tailIndex: result.tailIndex,
1233
+ done: result.state === "closed"
1234
+ };
1235
+ };
1236
+ return {
1237
+ streamFlushIntervalMs,
1238
+ streams: {
1239
+ write: (runId, name, chunk) => writeToStream(name, runId, chunk),
1240
+ writeMulti: (runId, name, chunks) => writeChunksToStream(name, runId, chunks),
1241
+ close: (runId, name) => closeStream(name, runId),
1242
+ fail: (runId, name, error) => errorStream(name, runId, error),
1243
+ get: (runId, name, startIndex) => readFromStream(name, runId, startIndex),
1244
+ list: listStreamsByRunId,
1245
+ getChunks: (runId, name, options) => getStreamChunks(name, runId, options),
1246
+ getInfo: (runId, name) => getStreamInfo(name, runId)
926
1247
  },
927
- async getStreamInfo(name, _runId) {
928
- return getStreamDO(name).getInfo();
929
- }
1248
+ writeToStream,
1249
+ writeChunksToStream,
1250
+ closeStream,
1251
+ errorStream,
1252
+ readFromStream,
1253
+ listStreamsByRunId,
1254
+ getStreamChunks,
1255
+ getStreamInfo
930
1256
  };
931
1257
  }
932
1258
  //#endregion
@@ -971,7 +1297,8 @@ function createCelldWorld(config) {
971
1297
  });
972
1298
  const streamer = createStreamer({
973
1299
  env: { WORKFLOW_STREAMS: env.WORKFLOW_STREAMS },
974
- readPollMs: resolved.readPollMs
1300
+ streamLongPollMs: resolved.streamLongPollMs,
1301
+ streamFlushIntervalMs: resolved.streamFlushIntervalMs
975
1302
  });
976
1303
  const runStub = (runId) => env.WORKFLOW_DB.get(env.WORKFLOW_DB.idFromName(runId));
977
1304
  const retention = {
@@ -998,6 +1325,6 @@ function createCelldWorld(config) {
998
1325
  };
999
1326
  }
1000
1327
  //#endregion
1001
- export { createCelldWorld, createCelldWorld as createWorld };
1328
+ export { MAX_STREAM_BATCH_BYTES, MAX_STREAM_CHUNK_BYTES, MAX_STREAM_ERROR_BYTES, MAX_STREAM_LONG_POLL_MS, MAX_STREAM_READ_BYTES, MAX_STREAM_READ_CHUNKS, MAX_STREAM_WRITE_CHUNKS, createCelldWorld, createCelldWorld as createWorld };
1002
1329
 
1003
1330
  //# sourceMappingURL=index.js.map