@ewhauser/world-celld 0.5.0 → 0.7.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 +26 -0
- package/README.md +43 -15
- package/celld-queue-worker/README.md +2 -2
- package/celld-queue-worker/wrangler.jsonc +3 -1
- package/celld-worker/README.md +3 -3
- package/dist/index.d.ts +44 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +113 -99
- package/dist/index.js.map +1 -1
- package/dist/queue-consumer.js +1 -1
- package/dist/{queue-protocol-Ds44uuND.js → queue-protocol-CtH7UUCO.js} +2 -2
- package/dist/{queue-protocol-Ds44uuND.js.map → queue-protocol-CtH7UUCO.js.map} +1 -1
- package/dist/{queue-protocol-CmJNMH_5.js → run-list-B__jKuE7.js} +124 -18
- package/dist/run-list-B__jKuE7.js.map +1 -0
- package/dist/testing.d.ts +2 -2
- package/dist/testing.d.ts.map +1 -1
- package/dist/testing.js +211 -25
- package/dist/testing.js.map +1 -1
- package/dist/worker.d.ts +19 -0
- package/dist/worker.d.ts.map +1 -1
- package/dist/worker.js +313 -33
- package/dist/worker.js.map +1 -1
- package/docs/celld-v0.6-upgrade.md +121 -0
- package/package.json +7 -5
- package/dist/queue-protocol-CmJNMH_5.js.map +0 -1
- package/docs/celld-v0.5-upgrade.md +0 -130
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { CreateEventSchema, EventSchema, HookSchema, MessageId, QueuePayloadSchema, SPEC_VERSION_CURRENT, StepSchema, WaitSchema, WorkflowRunSchema,
|
|
1
|
+
import { B as STREAM_BATCH_CONTENT_TYPE, C as MAX_QUEUE_DELIVERY_TIMEOUT_MS, D as checkedQueueTimestampAdd, E as boundedIntegerOption, F as MAX_STREAM_ERROR_BYTES, H as decodeStreamReadResult, I as MAX_STREAM_LONG_POLL_MS, J as isChunkLimitRejection, K as encodeStreamWriteBatch, L as MAX_STREAM_READ_BYTES, M as strictIntegerSetting, N as MAX_STREAM_BATCH_BYTES, P as MAX_STREAM_CHUNK_BYTES, R as MAX_STREAM_READ_CHUNKS, S as MAX_FLEET_RPC_TIMEOUT_MS, V as STREAM_CHUNK_LIMIT_HEADER, W as decodeStreamWriteResult, Y as normalizeStreamError, _ as createWorkflowIndex, a as nativeQueueDelaySeconds, f as compact, g as stringify, h as parse, j as queueDelayDeadline, m as rpcStringify, n as listRunsPage, p as rpcParse, v as runIndexMetadata, w as MAX_QUEUE_DERIVED_DEADLINE_HEADROOM_MS, z as MAX_STREAM_WRITE_CHUNKS } from "./run-list-B__jKuE7.js";
|
|
2
|
+
import { CreateEventSchema, EventSchema, HookSchema, MessageId, QueuePayloadSchema, SPEC_VERSION_CURRENT, StepSchema, WaitSchema, WorkflowRunSchema, parseQueueName } from "@workflow/world";
|
|
3
3
|
import { EntityConflictError, HookNotFoundError, RunExpiredError, RunNotSupportedError, TooEarlyError, WorkflowRunNotFoundError, WorkflowWorldError } from "@workflow/errors";
|
|
4
4
|
import { monotonicFactory } from "ulid";
|
|
5
5
|
//#region src/config.ts
|
|
@@ -132,6 +132,23 @@ const READ_ATTEMPTS = 3;
|
|
|
132
132
|
const MAX_WRITE_RESPONSE_BYTES = 64;
|
|
133
133
|
const MAX_READ_RESPONSE_BYTES = MAX_STREAM_READ_BYTES + 65536;
|
|
134
134
|
const MAX_ERROR_RESPONSE_BYTES = 65536;
|
|
135
|
+
/**
|
|
136
|
+
* Per-request chunk limit the fleet has advertised, per transport. Absent
|
|
137
|
+
* until a worker advertises one, so a client never sends an older worker more
|
|
138
|
+
* than the baseline it accepts; a response without the header drops back.
|
|
139
|
+
*/
|
|
140
|
+
const advertisedChunkLimits = /* @__PURE__ */ new WeakMap();
|
|
141
|
+
function streamChunkLimit(transport) {
|
|
142
|
+
return advertisedChunkLimits.get(transport) ?? 32;
|
|
143
|
+
}
|
|
144
|
+
function resetStreamChunkLimit(transport) {
|
|
145
|
+
advertisedChunkLimits.delete(transport);
|
|
146
|
+
}
|
|
147
|
+
function recordStreamChunkLimit(transport, response) {
|
|
148
|
+
const advertised = Number(response.headers.get(STREAM_CHUNK_LIMIT_HEADER));
|
|
149
|
+
if (Number.isSafeInteger(advertised) && advertised > 32) advertisedChunkLimits.set(transport, Math.min(advertised, 512));
|
|
150
|
+
else advertisedChunkLimits.delete(transport);
|
|
151
|
+
}
|
|
135
152
|
function delayMs(ms, signal) {
|
|
136
153
|
if (!signal) return new Promise((resolve) => setTimeout(resolve, ms));
|
|
137
154
|
if (signal.aborted) return Promise.reject(aborted(signal));
|
|
@@ -202,7 +219,7 @@ function streamUrl(transport, name) {
|
|
|
202
219
|
async function writeStreamChunks(transport, name, runId, chunks) {
|
|
203
220
|
const timeoutMs = resolveFleetTimeoutMs(transport);
|
|
204
221
|
const body = encodeStreamWriteBatch(chunks);
|
|
205
|
-
if (body.byteLength >
|
|
222
|
+
if (body.byteLength > 8391680) throw new Error("world-celld: encoded stream batch exceeds configured limit");
|
|
206
223
|
const url = new URL(streamUrl(transport, name));
|
|
207
224
|
url.searchParams.set("runId", runId);
|
|
208
225
|
const doFetch = transport.fetchImpl ?? fetch;
|
|
@@ -221,6 +238,7 @@ async function writeStreamChunks(transport, name, runId, chunks) {
|
|
|
221
238
|
throw new FleetTransportError(`world-celld: fleet unreachable at ${url.href}`, error);
|
|
222
239
|
}
|
|
223
240
|
if (!response.ok) throw await errorFromResponse(response);
|
|
241
|
+
recordStreamChunkLimit(transport, response);
|
|
224
242
|
try {
|
|
225
243
|
return decodeStreamWriteResult(await readBoundedResponse(response, MAX_WRITE_RESPONSE_BYTES));
|
|
226
244
|
} catch (error) {
|
|
@@ -257,13 +275,16 @@ async function readStreamChunks(transport, name, request, signal) {
|
|
|
257
275
|
await response.body?.cancel(aborted(signal)).catch(() => void 0);
|
|
258
276
|
throw aborted(signal);
|
|
259
277
|
}
|
|
260
|
-
if (response.ok)
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
278
|
+
if (response.ok) {
|
|
279
|
+
recordStreamChunkLimit(transport, response);
|
|
280
|
+
try {
|
|
281
|
+
return decodeStreamReadResult(await readBoundedResponse(response, MAX_READ_RESPONSE_BYTES));
|
|
282
|
+
} catch (error) {
|
|
283
|
+
if (signal?.aborted) throw aborted(signal);
|
|
284
|
+
lastError = error instanceof FleetTransportError ? error : new FleetTransportError(`world-celld: malformed stream response from ${url.href}`, error);
|
|
285
|
+
if (attempt < READ_ATTEMPTS) continue;
|
|
286
|
+
throw lastError;
|
|
287
|
+
}
|
|
267
288
|
}
|
|
268
289
|
let error;
|
|
269
290
|
try {
|
|
@@ -276,6 +297,12 @@ async function readStreamChunks(transport, name, request, signal) {
|
|
|
276
297
|
lastError = error;
|
|
277
298
|
continue;
|
|
278
299
|
}
|
|
300
|
+
if (request.maxChunks > 32 && isChunkLimitRejection(error) && attempt < READ_ATTEMPTS) {
|
|
301
|
+
resetStreamChunkLimit(transport);
|
|
302
|
+
url.searchParams.set("maxChunks", String(32));
|
|
303
|
+
lastError = error;
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
279
306
|
throw error;
|
|
280
307
|
}
|
|
281
308
|
throw lastError instanceof Error ? lastError : new FleetTransportError("world-celld: stream read failed", lastError);
|
|
@@ -335,6 +362,8 @@ function makeStreamNamespace(transport) {
|
|
|
335
362
|
const control = makeStub(transport, "streams", name, STREAMS);
|
|
336
363
|
control.writeChunks = (runId, chunks) => writeStreamChunks(transport, name, runId, chunks);
|
|
337
364
|
control.readChunks = (request, signal) => readStreamChunks(transport, name, request, signal);
|
|
365
|
+
control.streamChunkLimit = () => streamChunkLimit(transport);
|
|
366
|
+
control.resetStreamChunkLimit = () => resetStreamChunkLimit(transport);
|
|
338
367
|
return control;
|
|
339
368
|
}
|
|
340
369
|
};
|
|
@@ -382,6 +411,20 @@ const HOOK_IDS = {
|
|
|
382
411
|
],
|
|
383
412
|
mutating: /* @__PURE__ */ new Set()
|
|
384
413
|
};
|
|
414
|
+
/** Transports whose worker answered the fleet-side run listing with 404. */
|
|
415
|
+
const legacyRunListing = /* @__PURE__ */ new WeakSet();
|
|
416
|
+
async function listResolvedRuns(transport, request) {
|
|
417
|
+
if (legacyRunListing.has(transport)) return null;
|
|
418
|
+
try {
|
|
419
|
+
return await callFleetRoute(transport, "/v1/index/runs/list-resolved", [request], { idempotent: true });
|
|
420
|
+
} catch (error) {
|
|
421
|
+
if (error.status === 404) {
|
|
422
|
+
legacyRunListing.add(transport);
|
|
423
|
+
return null;
|
|
424
|
+
}
|
|
425
|
+
throw error;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
385
428
|
function createRemoteEnv(transport) {
|
|
386
429
|
const runCatalog = makeNamespace(transport, "run-catalog", RUN_CATALOG);
|
|
387
430
|
const hookTokens = makeNamespace(transport, "hook-tokens", HOOK_TOKENS);
|
|
@@ -402,6 +445,7 @@ function createRemoteEnv(transport) {
|
|
|
402
445
|
publicationExpiresAt
|
|
403
446
|
], { idempotent: true }),
|
|
404
447
|
listRuns: (options) => callFleetRoute(transport, "/v1/index/runs/list", [options], { idempotent: true }),
|
|
448
|
+
listResolvedRuns: (request) => listResolvedRuns(transport, request),
|
|
405
449
|
expireRun: (request) => callFleetRoute(transport, "/v1/index/runs/expire", [request], { idempotent: true }),
|
|
406
450
|
reserveHook: (token, owner) => callFleetRoute(transport, "/v1/index/hooks/reserve", [token, owner], { idempotent: true }),
|
|
407
451
|
finalizeHookIndexes: (token, hookId, serializedHook, owner, reservation) => callFleetRoute(transport, "/v1/index/hooks/finalize", [
|
|
@@ -458,7 +502,7 @@ const debug = createDebugLogger("celld-world");
|
|
|
458
502
|
* Vendored from vinnymac/worlds packages/world-cloudflare/src/queue.ts
|
|
459
503
|
* (Apache-2.0, see NOTICE), modified for celld:
|
|
460
504
|
*
|
|
461
|
-
* - Production publishes a small pointer through celld v0.
|
|
505
|
+
* - Production publishes a small pointer through celld v0.6.0 native Queues.
|
|
462
506
|
* The deployed Queue consumer forwards deliveries to the same HTTP handler
|
|
463
507
|
* used by the in-process test pump.
|
|
464
508
|
* - Run-bearing message bodies live in the fleet object store. This preserves
|
|
@@ -828,6 +872,7 @@ function parseApplyEventOutcome(value) {
|
|
|
828
872
|
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
873
|
const event = parseSuccessEntity(value, "event", EventSchema);
|
|
830
874
|
const run = parseSuccessEntity(value, "run", WorkflowRunSchema);
|
|
875
|
+
const replayRun = parseSuccessEntity(value, "replayRun", WorkflowRunSchema);
|
|
831
876
|
const step = parseSuccessEntity(value, "step", StepSchema);
|
|
832
877
|
const hook = parseSuccessEntity(value, "hook", HookSchema);
|
|
833
878
|
const wait = parseSuccessEntity(value, "wait", WaitSchema);
|
|
@@ -851,6 +896,7 @@ function parseApplyEventOutcome(value) {
|
|
|
851
896
|
if (value.cursor !== void 0 && value.cursor !== null && typeof value.cursor !== "string") malformedApplyEventOutcome("cursor must be a string or null");
|
|
852
897
|
if (value.hasMore !== void 0 && typeof value.hasMore !== "boolean") malformedApplyEventOutcome("hasMore must be a boolean");
|
|
853
898
|
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");
|
|
899
|
+
for (const flag of ["runIndexPublished", "hookIndexesReleased"]) if (value[flag] !== void 0 && value[flag] !== true) malformedApplyEventOutcome(`${flag} must be true when present`);
|
|
854
900
|
if (run !== void 0 && value.indexPublicationExpiresAt === void 0) malformedApplyEventOutcome("indexPublicationExpiresAt is required when run is present");
|
|
855
901
|
if (value.indexPublicationExpiresAt !== void 0 && (typeof value.indexPublicationExpiresAt !== "number" || !Number.isFinite(value.indexPublicationExpiresAt))) malformedApplyEventOutcome("indexPublicationExpiresAt must be a finite number");
|
|
856
902
|
return {
|
|
@@ -859,6 +905,7 @@ function parseApplyEventOutcome(value) {
|
|
|
859
905
|
...value,
|
|
860
906
|
event,
|
|
861
907
|
run,
|
|
908
|
+
replayRun,
|
|
862
909
|
step,
|
|
863
910
|
hook,
|
|
864
911
|
wait,
|
|
@@ -951,19 +998,6 @@ function unwrapRead(outcome) {
|
|
|
951
998
|
if (!outcome.ok) throw new RunExpiredError(outcome.message);
|
|
952
999
|
return outcome.value;
|
|
953
1000
|
}
|
|
954
|
-
/** Bound cross-run fanout so large list pages cannot create an RPC burst. */
|
|
955
|
-
const RUN_LIST_CONCURRENCY = 8;
|
|
956
|
-
/**
|
|
957
|
-
* Run statuses only move pending -> running -> terminal, and terminal statuses
|
|
958
|
-
* are immutable. An older index value may therefore safely exclude a filter
|
|
959
|
-
* only when it is already terminal, or when the caller asks for pending and
|
|
960
|
-
* the index has advanced beyond pending. Earlier non-terminal metadata cannot
|
|
961
|
-
* exclude a later status because a post-commit index write may need replay.
|
|
962
|
-
*/
|
|
963
|
-
function indexStatusExcludes(indexed, requested) {
|
|
964
|
-
if (requested === void 0 || indexed === requested) return false;
|
|
965
|
-
return isTerminalWorkflowRunStatus(indexed) || requested === "pending";
|
|
966
|
-
}
|
|
967
1001
|
function createStorage(config) {
|
|
968
1002
|
const { env } = config;
|
|
969
1003
|
const cleanup = { retentionMs: config.runRetentionMs ?? 0 };
|
|
@@ -981,60 +1015,31 @@ function createStorage(config) {
|
|
|
981
1015
|
runs: {
|
|
982
1016
|
get: runsGet,
|
|
983
1017
|
async list(params) {
|
|
984
|
-
const
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
if (kvList.keys.length === 0) {
|
|
998
|
-
exhausted = true;
|
|
999
|
-
break;
|
|
1000
|
-
}
|
|
1001
|
-
const candidates = [];
|
|
1002
|
-
for (const key of kvList.keys) {
|
|
1003
|
-
const metadata = JSON.parse(key.value);
|
|
1004
|
-
if (indexStatusExcludes(metadata.status, params?.status)) continue;
|
|
1005
|
-
candidates.push({
|
|
1006
|
-
key: key.name,
|
|
1007
|
-
metadata
|
|
1008
|
-
});
|
|
1009
|
-
}
|
|
1010
|
-
for (let offset = 0; offset < candidates.length && matches.length <= limit;) {
|
|
1011
|
-
const needed = limit + 1 - matches.length;
|
|
1012
|
-
const batch = candidates.slice(offset, offset + Math.min(RUN_LIST_CONCURRENCY, needed));
|
|
1013
|
-
const resolved = await Promise.all(batch.map(async ({ key, metadata }) => {
|
|
1014
|
-
try {
|
|
1015
|
-
const run = await runsGet(metadata.runId, { resolveData: params?.resolveData });
|
|
1016
|
-
return !params?.status || run.status === params.status ? {
|
|
1017
|
-
key,
|
|
1018
|
-
run
|
|
1019
|
-
} : null;
|
|
1020
|
-
} catch (error) {
|
|
1021
|
-
if (!WorkflowRunNotFoundError.is(error) && !RunExpiredError.is(error)) throw error;
|
|
1022
|
-
return null;
|
|
1023
|
-
}
|
|
1024
|
-
}));
|
|
1025
|
-
for (const match of resolved) if (match) matches.push(match);
|
|
1026
|
-
offset += batch.length;
|
|
1027
|
-
}
|
|
1028
|
-
exhausted = kvList.list_complete;
|
|
1029
|
-
scanCursor = kvList.cursor ?? kvList.keys.at(-1)?.name;
|
|
1030
|
-
}
|
|
1031
|
-
const hasMore = matches.length > limit;
|
|
1032
|
-
const page = matches.slice(0, limit);
|
|
1033
|
-
return {
|
|
1034
|
-
data: page.map(({ run }) => run),
|
|
1035
|
-
cursor: hasMore ? page.at(-1)?.key ?? null : null,
|
|
1036
|
-
hasMore
|
|
1018
|
+
const request = {
|
|
1019
|
+
workflowName: params?.workflowName,
|
|
1020
|
+
statuses: params?.status === void 0 ? void 0 : Array.isArray(params.status) ? params.status : [params.status],
|
|
1021
|
+
limit: params?.pagination?.limit ?? 20,
|
|
1022
|
+
cursor: params?.pagination?.cursor,
|
|
1023
|
+
sortOrder: params?.pagination?.sortOrder,
|
|
1024
|
+
resolveData: params?.resolveData
|
|
1025
|
+
};
|
|
1026
|
+
const fleetPage = Number.isSafeInteger(request.limit) && request.limit >= 1 && request.limit <= 1e3 ? await env.WORKFLOW_INDEX.listResolvedRuns?.(compact(request)) : void 0;
|
|
1027
|
+
if (fleetPage) return {
|
|
1028
|
+
data: fleetPage.data.map((run) => filterData(parseRun(run), params?.resolveData, ["input", "output"])),
|
|
1029
|
+
cursor: fleetPage.cursor,
|
|
1030
|
+
hasMore: fleetPage.hasMore
|
|
1037
1031
|
};
|
|
1032
|
+
return await listRunsPage({
|
|
1033
|
+
listRuns: (options) => env.WORKFLOW_INDEX.listRuns(options),
|
|
1034
|
+
readRun: async (runId) => {
|
|
1035
|
+
try {
|
|
1036
|
+
return await runsGet(runId);
|
|
1037
|
+
} catch (error) {
|
|
1038
|
+
if (WorkflowRunNotFoundError.is(error) || RunExpiredError.is(error)) return null;
|
|
1039
|
+
throw error;
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
}, request);
|
|
1038
1043
|
}
|
|
1039
1044
|
},
|
|
1040
1045
|
events: {
|
|
@@ -1094,20 +1099,18 @@ function createStorage(config) {
|
|
|
1094
1099
|
throwOutcomeError(parsedOutcome.outcome, effectiveRunId, data);
|
|
1095
1100
|
}
|
|
1096
1101
|
const outcome = parsedOutcome.outcome;
|
|
1097
|
-
if (outcome.run) {
|
|
1098
|
-
const meta = JSON.stringify({
|
|
1099
|
-
runId: effectiveRunId,
|
|
1100
|
-
createdAt: outcome.run.createdAt.toISOString(),
|
|
1101
|
-
status: outcome.run.status
|
|
1102
|
-
});
|
|
1102
|
+
if (outcome.run && !outcome.runIndexPublished) {
|
|
1103
1103
|
if (outcome.indexPublicationExpiresAt === void 0) throw new Error("world-celld: authoritative run mutation omitted its index lease");
|
|
1104
|
-
await env.WORKFLOW_INDEX.commitRun(outcome.run,
|
|
1104
|
+
await env.WORKFLOW_INDEX.commitRun(outcome.run, runIndexMetadata({
|
|
1105
|
+
...outcome.run,
|
|
1106
|
+
runId: effectiveRunId
|
|
1107
|
+
}), outcome.indexPublicationExpiresAt);
|
|
1105
1108
|
}
|
|
1106
1109
|
if (outcome.hookToIndex) {
|
|
1107
1110
|
const serialized = stringify(outcome.hookToIndex);
|
|
1108
1111
|
await env.WORKFLOW_INDEX.finalizeHookIndexes(outcome.hookToIndex.token, outcome.hookToIndex.hookId, serialized, hookOwner(outcome.hookToIndex), hookAdmission?.reservation);
|
|
1109
1112
|
} else if (hookAdmission) await env.WORKFLOW_INDEX.releaseHookReservation(hookAdmission.token, hookAdmission.owner, hookAdmission.reservation);
|
|
1110
|
-
if (outcome.releasedHooks.length > 0) await env.WORKFLOW_INDEX.releaseHookIndexes({
|
|
1113
|
+
if (outcome.releasedHooks.length > 0 && !outcome.hookIndexesReleased) await env.WORKFLOW_INDEX.releaseHookIndexes({
|
|
1111
1114
|
runId: effectiveRunId,
|
|
1112
1115
|
hooks: outcome.releasedHooks
|
|
1113
1116
|
});
|
|
@@ -1118,7 +1121,7 @@ function createStorage(config) {
|
|
|
1118
1121
|
};
|
|
1119
1122
|
return {
|
|
1120
1123
|
event: outcome.event,
|
|
1121
|
-
run: outcome.run,
|
|
1124
|
+
run: outcome.run ?? outcome.replayRun,
|
|
1122
1125
|
step: outcome.step,
|
|
1123
1126
|
hook: outcome.hook,
|
|
1124
1127
|
wait: outcome.wait,
|
|
@@ -1275,18 +1278,28 @@ function createStreamer(config) {
|
|
|
1275
1278
|
const chunks = input.map(toBytes);
|
|
1276
1279
|
for (const chunk of chunks) if (chunk.byteLength > 1048576) throw new Error(`Stream chunk exceeds ${MAX_STREAM_CHUNK_BYTES} bytes`);
|
|
1277
1280
|
await registerStreamForRun(name, resolvedRunId);
|
|
1278
|
-
|
|
1279
|
-
let
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1281
|
+
const stub = getStreamDO(name);
|
|
1282
|
+
let next = 0;
|
|
1283
|
+
while (next < chunks.length) {
|
|
1284
|
+
const chunkLimit = stub.streamChunkLimit?.() ?? 32;
|
|
1285
|
+
let end = next;
|
|
1286
|
+
let batchBytes = 0;
|
|
1287
|
+
while (end < chunks.length && end - next < chunkLimit && (end === next || batchBytes + chunks[end].byteLength <= 8388608)) {
|
|
1288
|
+
batchBytes += chunks[end].byteLength;
|
|
1289
|
+
end++;
|
|
1290
|
+
}
|
|
1291
|
+
const batch = chunks.slice(next, end);
|
|
1292
|
+
try {
|
|
1293
|
+
await stub.writeChunks(resolvedRunId, batch);
|
|
1294
|
+
} catch (error) {
|
|
1295
|
+
if (batch.length > 32 && isChunkLimitRejection(error)) {
|
|
1296
|
+
stub.resetStreamChunkLimit?.();
|
|
1297
|
+
continue;
|
|
1298
|
+
}
|
|
1299
|
+
throw error;
|
|
1285
1300
|
}
|
|
1286
|
-
|
|
1287
|
-
batchBytes += chunk.byteLength;
|
|
1301
|
+
next = end;
|
|
1288
1302
|
}
|
|
1289
|
-
if (batch.length > 0) await getStreamDO(name).writeChunks(resolvedRunId, batch);
|
|
1290
1303
|
};
|
|
1291
1304
|
const writeToStream = (name, runId, chunk) => writeChunksToStream(name, runId, [chunk]);
|
|
1292
1305
|
const closeStream = async (name, runId) => {
|
|
@@ -1324,7 +1337,7 @@ function createStreamer(config) {
|
|
|
1324
1337
|
result = await stub.readChunks({
|
|
1325
1338
|
runId,
|
|
1326
1339
|
startIndex: nextIndex,
|
|
1327
|
-
maxChunks: 32,
|
|
1340
|
+
maxChunks: stub.streamChunkLimit?.() ?? 32,
|
|
1328
1341
|
maxBytes: MAX_STREAM_READ_BYTES,
|
|
1329
1342
|
waitMs: streamLongPollMs
|
|
1330
1343
|
}, abortController.signal);
|
|
@@ -1354,10 +1367,11 @@ function createStreamer(config) {
|
|
|
1354
1367
|
const getStreamChunks = async (name, runId, options) => {
|
|
1355
1368
|
const requestedLimit = options?.limit ?? DEFAULT_CHUNK_PAGE_SIZE;
|
|
1356
1369
|
if (!Number.isSafeInteger(requestedLimit) || requestedLimit < 1) throw new Error("Stream chunk limit must be a positive safe integer");
|
|
1357
|
-
const
|
|
1370
|
+
const stub = getStreamDO(name);
|
|
1371
|
+
const limit = Math.min(requestedLimit, stub.streamChunkLimit?.() ?? MAX_CHUNK_PAGE_SIZE);
|
|
1358
1372
|
const startIndex = options?.cursor ? Number.parseInt(options.cursor, 10) : 0;
|
|
1359
1373
|
if (Number.isNaN(startIndex) || startIndex < 0) throw new Error(`Invalid stream cursor: ${options?.cursor}`);
|
|
1360
|
-
const result = await
|
|
1374
|
+
const result = await stub.readChunks({
|
|
1361
1375
|
runId,
|
|
1362
1376
|
startIndex,
|
|
1363
1377
|
maxChunks: limit,
|