@ewhauser/world-celld 0.8.1 → 0.9.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 +8 -0
- package/README.md +12 -5
- package/dist/index.d.ts +1 -40
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/queue-protocol-8iiZ8o_1.d.ts +42 -0
- package/dist/queue-protocol-8iiZ8o_1.d.ts.map +1 -0
- package/dist/{run-list-DGsdv6Jf.js → run-list-C4B4SBYW.js} +3 -2
- package/dist/{run-list-DGsdv6Jf.js.map → run-list-C4B4SBYW.js.map} +1 -1
- package/dist/testing.d.ts +17 -0
- package/dist/testing.d.ts.map +1 -1
- package/dist/testing.js +303 -4
- package/dist/testing.js.map +1 -1
- package/package.json +4 -4
package/dist/testing.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $ as
|
|
1
|
+
import { $ as normalizeStreamError, B as MAX_STREAM_LONG_POLL_MS, C as HOOK_CLAIM_LEASE_MS, F as queueDelayDeadline, G as STREAM_CHUNK_LIMIT_HEADER, L as MAX_STREAM_BATCH_BYTES, M as isNonNegativeSafeInteger, N as isPositiveSafeInteger, O as MAX_RUN_INDEX_PUBLICATION_LIFETIME_MS, P as isRecord$1, R as MAX_STREAM_CHUNK_BYTES, V as MAX_STREAM_READ_BYTES, W as STREAM_BATCH_CONTENT_TYPE, Y as encodeStreamReadResult, Z as encodeStreamWriteResult, _ as rpcStringify, a as QUEUE_PUBLICATION_LEASE_MS, b as createWorkflowIndex, c as nativeQueueDelaySeconds, d as queuePayloadObjectKey, et as validateStreamReadRequest, f as queuePayloadRegistryKey, g as rpcParse, h as compact, i as QUEUE_PAYLOAD_REGISTRY_PREFIX, k as QUEUE_CLAIM_STALE_MS, l as queueClaimName, m as validateNativeQueueSendOptions, n as listRunsPage, o as QUEUE_RESERVATION_GRACE_MS, p as validateNativeQueueEnvelope, q as decodeStreamWriteBatch, r as NATIVE_QUEUE_MAX_MESSAGE_BYTES, s as isInlineRunBody, t as MAX_RUN_LIST_LIMIT, tt as validateStreamWriteChunks, u as queueOrphanName, v as parse, w as LIFECYCLE_COMPACTION_RETRY_MS, x as runIndexMetadata } from "./run-list-C4B4SBYW.js";
|
|
2
2
|
import { CreateEventSchema, EventSchema, HookSchema, SPEC_VERSION_CURRENT, StepSchema, WaitSchema, WorkflowRunSchema, applyAttributeChanges, eventIdToSlot, getMaxEventsPerRun, isChildEntityCreationEvent, isTerminalStepStatus, isTerminalWorkflowRunStatus, slotToEventId, validateAttributeChanges } from "@workflow/world";
|
|
3
3
|
import http from "node:http";
|
|
4
4
|
//#region src/testing/fake-cell.ts
|
|
@@ -1361,6 +1361,11 @@ async function authenticate(request, secret) {
|
|
|
1361
1361
|
}
|
|
1362
1362
|
//#endregion
|
|
1363
1363
|
//#region src/worker/queue-claims.ts
|
|
1364
|
+
/**
|
|
1365
|
+
* Idempotency claims for Queue messages. A claim lives either in its own
|
|
1366
|
+
* claim cell (named by queue and idempotency key) or, for run-bearing
|
|
1367
|
+
* messages whose envelope says so, as an entry in the run's own cell.
|
|
1368
|
+
*/
|
|
1364
1369
|
function cellQueueClaim(cell) {
|
|
1365
1370
|
return {
|
|
1366
1371
|
claim: (messageId, staleMs) => cell.claimInflight({
|
|
@@ -1413,6 +1418,12 @@ function runQueueClaim(run, claimName) {
|
|
|
1413
1418
|
})
|
|
1414
1419
|
};
|
|
1415
1420
|
}
|
|
1421
|
+
/** The claim a delivered envelope was reserved under, if it has an idempotency key. */
|
|
1422
|
+
function deliveryQueueClaim(envelope, runStub) {
|
|
1423
|
+
if (!envelope.idempotencyKey) return void 0;
|
|
1424
|
+
const claimName = queueClaimName(envelope.queueName, envelope.idempotencyKey);
|
|
1425
|
+
return envelope.claimScope === "run" && envelope.runId ? runQueueClaim(runStub(envelope.runId), claimName) : cellQueueClaim(runStub(claimName));
|
|
1426
|
+
}
|
|
1416
1427
|
//#endregion
|
|
1417
1428
|
//#region src/worker/index-validation.ts
|
|
1418
1429
|
function invalid(message) {
|
|
@@ -4801,6 +4812,146 @@ var WorkflowRunDO = class extends DurableObject {
|
|
|
4801
4812
|
}
|
|
4802
4813
|
};
|
|
4803
4814
|
//#endregion
|
|
4815
|
+
//#region src/worker/queue-delivery.ts
|
|
4816
|
+
/** Internal delivery contract. No Request/Response objects cross the RPC boundary. */
|
|
4817
|
+
const MAX_QUEUE_DELIVERY_RESPONSE_BYTES = 65536;
|
|
4818
|
+
const QUEUE_ORPHAN_GRACE_MS = 432e6;
|
|
4819
|
+
const PERMANENT_QUEUE_STATUSES = /* @__PURE__ */ new Set([
|
|
4820
|
+
404,
|
|
4821
|
+
409,
|
|
4822
|
+
410,
|
|
4823
|
+
422
|
|
4824
|
+
]);
|
|
4825
|
+
async function readResponseText(response) {
|
|
4826
|
+
if (!response.body) return "";
|
|
4827
|
+
const reader = response.body.getReader();
|
|
4828
|
+
const decoder = new TextDecoder();
|
|
4829
|
+
let total = 0;
|
|
4830
|
+
let text = "";
|
|
4831
|
+
try {
|
|
4832
|
+
for (;;) {
|
|
4833
|
+
const { done, value } = await reader.read();
|
|
4834
|
+
if (done) break;
|
|
4835
|
+
total += value.byteLength;
|
|
4836
|
+
if (total > MAX_QUEUE_DELIVERY_RESPONSE_BYTES) {
|
|
4837
|
+
await reader.cancel("queue delivery response is too large").catch(() => void 0);
|
|
4838
|
+
throw new Error("workflow queue handler response exceeds configured limit");
|
|
4839
|
+
}
|
|
4840
|
+
text += decoder.decode(value, { stream: true });
|
|
4841
|
+
}
|
|
4842
|
+
return text + decoder.decode();
|
|
4843
|
+
} finally {
|
|
4844
|
+
reader.releaseLock();
|
|
4845
|
+
}
|
|
4846
|
+
}
|
|
4847
|
+
/** Validate at the service boundary; callers cannot bypass auth via RPC. */
|
|
4848
|
+
async function deliverQueueMessage(env, secret, input, attempt) {
|
|
4849
|
+
if (!env.WORLD_SECRET) throw new Error("WORLD_SECRET is not configured");
|
|
4850
|
+
if (typeof secret !== "string" || !secret || !await timingSafeEqual(secret, env.WORLD_SECRET)) throw new Error("Unauthorized Queue delivery");
|
|
4851
|
+
const envelope = validateNativeQueueEnvelope(input);
|
|
4852
|
+
if (!Number.isSafeInteger(attempt) || attempt < 1) throw new TypeError("queue delivery attempt must be a positive safe integer");
|
|
4853
|
+
if (!env.WORKFLOW_DB) throw new Error("missing binding: WORKFLOW_DB");
|
|
4854
|
+
const namespace = env.WORKFLOW_DB;
|
|
4855
|
+
const payloadStore = env.WORKFLOW_QUEUE_PAYLOADS;
|
|
4856
|
+
const runStub = (name) => namespace.get(namespace.idFromName(name));
|
|
4857
|
+
const claim = deliveryQueueClaim(envelope, runStub);
|
|
4858
|
+
let claimed = false;
|
|
4859
|
+
async function releaseClaim() {
|
|
4860
|
+
if (claimed) await claim.release(envelope.messageId);
|
|
4861
|
+
}
|
|
4862
|
+
async function unregisterPayload() {
|
|
4863
|
+
if (envelope.runId) {
|
|
4864
|
+
await runStub(envelope.runId).unregisterQueuePayload(envelope.messageId);
|
|
4865
|
+
await runStub(queueOrphanName(envelope.messageId)).cancelQueuePayloadOrphan(envelope.messageId);
|
|
4866
|
+
}
|
|
4867
|
+
}
|
|
4868
|
+
try {
|
|
4869
|
+
let admission;
|
|
4870
|
+
if (envelope.runId && !envelope.payloadKey && envelope.claimScope !== "run") {
|
|
4871
|
+
admission = runStub(envelope.runId).getQueueAdmission();
|
|
4872
|
+
admission.catch(() => void 0);
|
|
4873
|
+
}
|
|
4874
|
+
if (claim) {
|
|
4875
|
+
const result = await claim.claim(envelope.messageId, QUEUE_CLAIM_STALE_MS);
|
|
4876
|
+
if (result.expired) return { kind: "complete" };
|
|
4877
|
+
if (!result.claimed) {
|
|
4878
|
+
const now = Date.now();
|
|
4879
|
+
if (result.retryAt !== void 0 && result.retryAt > now) return {
|
|
4880
|
+
kind: "suspend",
|
|
4881
|
+
timeoutSeconds: Math.max(1, Math.ceil((result.retryAt - now) / 1e3))
|
|
4882
|
+
};
|
|
4883
|
+
return { kind: "complete" };
|
|
4884
|
+
}
|
|
4885
|
+
claimed = true;
|
|
4886
|
+
}
|
|
4887
|
+
if (admission && !(await admission).ok) {
|
|
4888
|
+
await claim?.complete(envelope.messageId);
|
|
4889
|
+
return { kind: "complete" };
|
|
4890
|
+
}
|
|
4891
|
+
let body = envelope.body;
|
|
4892
|
+
if (envelope.payloadKey) {
|
|
4893
|
+
if (!payloadStore) throw new Error("missing binding: WORKFLOW_QUEUE_PAYLOADS");
|
|
4894
|
+
body = await payloadStore.read(envelope.payloadKey) ?? void 0;
|
|
4895
|
+
if (body === void 0) {
|
|
4896
|
+
await unregisterPayload();
|
|
4897
|
+
await claim?.complete(envelope.messageId);
|
|
4898
|
+
return { kind: "complete" };
|
|
4899
|
+
}
|
|
4900
|
+
}
|
|
4901
|
+
const headers = {
|
|
4902
|
+
"content-type": "application/json",
|
|
4903
|
+
"x-vqs-queue-name": envelope.queueName,
|
|
4904
|
+
"x-vqs-message-id": envelope.messageId,
|
|
4905
|
+
"x-vqs-message-attempt": String(attempt)
|
|
4906
|
+
};
|
|
4907
|
+
if (env.WORKFLOW_CALLBACK_SECRET) headers["x-workflow-callback-secret"] = env.WORKFLOW_CALLBACK_SECRET;
|
|
4908
|
+
const callback = await fetch(`${envelope.targetBaseUrl.replace(/\/$/, "")}/.well-known/workflow/v1/flow`, {
|
|
4909
|
+
method: "POST",
|
|
4910
|
+
headers,
|
|
4911
|
+
body,
|
|
4912
|
+
signal: AbortSignal.timeout(3e5)
|
|
4913
|
+
});
|
|
4914
|
+
if (callback.ok || PERMANENT_QUEUE_STATUSES.has(callback.status)) {
|
|
4915
|
+
await callback.body?.cancel().catch(() => void 0);
|
|
4916
|
+
if (envelope.payloadKey) {
|
|
4917
|
+
await payloadStore.delete(envelope.payloadKey);
|
|
4918
|
+
await unregisterPayload();
|
|
4919
|
+
}
|
|
4920
|
+
await claim?.complete(envelope.messageId);
|
|
4921
|
+
return { kind: "complete" };
|
|
4922
|
+
}
|
|
4923
|
+
if (callback.status === 503) {
|
|
4924
|
+
const responseBody = await readResponseText(callback);
|
|
4925
|
+
const timeoutSeconds = JSON.parse(responseBody).timeoutSeconds;
|
|
4926
|
+
const notBefore = queueDelayDeadline(Date.now(), timeoutSeconds, 1, QUEUE_CLAIM_STALE_MS);
|
|
4927
|
+
if (notBefore === null) throw new Error("workflow queue suspension deadline is invalid");
|
|
4928
|
+
if (envelope.payloadKey && envelope.runId) await runStub(queueOrphanName(envelope.messageId)).scheduleQueuePayloadOrphan({
|
|
4929
|
+
messageId: envelope.messageId,
|
|
4930
|
+
runId: envelope.runId,
|
|
4931
|
+
key: envelope.payloadKey,
|
|
4932
|
+
expiresAt: notBefore + QUEUE_ORPHAN_GRACE_MS
|
|
4933
|
+
});
|
|
4934
|
+
if (claim) await claim.hold({
|
|
4935
|
+
messageId: envelope.messageId,
|
|
4936
|
+
retryAt: notBefore,
|
|
4937
|
+
expiresAt: notBefore + QUEUE_CLAIM_STALE_MS,
|
|
4938
|
+
reservationExpiresAt: notBefore + QUEUE_ORPHAN_GRACE_MS
|
|
4939
|
+
});
|
|
4940
|
+
return {
|
|
4941
|
+
kind: "suspend",
|
|
4942
|
+
timeoutSeconds
|
|
4943
|
+
};
|
|
4944
|
+
}
|
|
4945
|
+
await callback.body?.cancel().catch(() => void 0);
|
|
4946
|
+
await releaseClaim();
|
|
4947
|
+
claimed = false;
|
|
4948
|
+
return { kind: "retry" };
|
|
4949
|
+
} catch (error) {
|
|
4950
|
+
await releaseClaim();
|
|
4951
|
+
throw error;
|
|
4952
|
+
}
|
|
4953
|
+
}
|
|
4954
|
+
//#endregion
|
|
4804
4955
|
//#region src/testing/http-harness.ts
|
|
4805
4956
|
/**
|
|
4806
4957
|
* Boots the real worker router over real cell classes (on FakeFleet state)
|
|
@@ -4809,6 +4960,7 @@ var WorkflowRunDO = class extends DurableObject {
|
|
|
4809
4960
|
async function startHarness(options = {}) {
|
|
4810
4961
|
const cellEnv = { ...options.cellEnv };
|
|
4811
4962
|
const queueMessages = [];
|
|
4963
|
+
const queuePublications = [];
|
|
4812
4964
|
const queuePayloads = /* @__PURE__ */ new Map();
|
|
4813
4965
|
const queuePayloadBucket = {
|
|
4814
4966
|
async put(key, value) {
|
|
@@ -4822,8 +4974,12 @@ async function startHarness(options = {}) {
|
|
|
4822
4974
|
for (const key of Array.isArray(keys) ? keys : [keys]) queuePayloads.delete(key);
|
|
4823
4975
|
}
|
|
4824
4976
|
};
|
|
4825
|
-
const nativeQueue = { async send(body) {
|
|
4977
|
+
const nativeQueue = { async send(body, sendOptions) {
|
|
4826
4978
|
queueMessages.push(body);
|
|
4979
|
+
queuePublications.push({
|
|
4980
|
+
body,
|
|
4981
|
+
delaySeconds: sendOptions?.delaySeconds ?? 0
|
|
4982
|
+
});
|
|
4827
4983
|
} };
|
|
4828
4984
|
const fleet = new FakeFleet({
|
|
4829
4985
|
runs: WorkflowRunDO,
|
|
@@ -4842,7 +4998,7 @@ async function startHarness(options = {}) {
|
|
|
4842
4998
|
WORKFLOW_HOOK_IDS: fleet.namespace("hook-ids"),
|
|
4843
4999
|
WORKFLOW_QUEUE_PAYLOADS: queuePayloadBucket
|
|
4844
5000
|
});
|
|
4845
|
-
const
|
|
5001
|
+
const env = {
|
|
4846
5002
|
WORKFLOW_DB: fleet.namespace("runs"),
|
|
4847
5003
|
WORKFLOW_STREAMS: fleet.namespace("streams"),
|
|
4848
5004
|
WORKFLOW_RUN_CATALOG: fleet.namespace("run-catalog"),
|
|
@@ -4851,7 +5007,8 @@ async function startHarness(options = {}) {
|
|
|
4851
5007
|
WORKFLOW_QUEUE: nativeQueue,
|
|
4852
5008
|
WORKFLOW_QUEUE_PAYLOADS: createQueuePayloadStore(queuePayloadBucket),
|
|
4853
5009
|
WORLD_SECRET: options.secret
|
|
4854
|
-
}
|
|
5010
|
+
};
|
|
5011
|
+
const router = createRouter(env);
|
|
4855
5012
|
const server = http.createServer(async (req, res) => {
|
|
4856
5013
|
const requestAbort = new AbortController();
|
|
4857
5014
|
const abortRequest = () => {
|
|
@@ -4894,7 +5051,9 @@ async function startHarness(options = {}) {
|
|
|
4894
5051
|
url: `http://127.0.0.1:${port}`,
|
|
4895
5052
|
fleet,
|
|
4896
5053
|
queueMessages,
|
|
5054
|
+
queuePublications,
|
|
4897
5055
|
queuePayloads,
|
|
5056
|
+
deliverQueueMessage: (secret, envelope, attempt) => deliverQueueMessage(env, secret, envelope, attempt),
|
|
4898
5057
|
close: () => new Promise((resolve, reject) => {
|
|
4899
5058
|
server.close((err) => err ? reject(err) : resolve());
|
|
4900
5059
|
server.closeAllConnections();
|
|
@@ -4902,11 +5061,148 @@ async function startHarness(options = {}) {
|
|
|
4902
5061
|
};
|
|
4903
5062
|
}
|
|
4904
5063
|
//#endregion
|
|
5064
|
+
//#region src/worker/queue-consumer.ts
|
|
5065
|
+
function backoffSeconds(attempt) {
|
|
5066
|
+
return Math.min(60, 2 ** Math.min(attempt, 30));
|
|
5067
|
+
}
|
|
5068
|
+
async function republish(env, envelope, now = Date.now()) {
|
|
5069
|
+
await env.WORKFLOW_QUEUE.send(JSON.stringify(envelope), {
|
|
5070
|
+
contentType: "text",
|
|
5071
|
+
delaySeconds: nativeQueueDelaySeconds(envelope.notBefore, now)
|
|
5072
|
+
});
|
|
5073
|
+
}
|
|
5074
|
+
async function consume(message, env) {
|
|
5075
|
+
let envelope;
|
|
5076
|
+
try {
|
|
5077
|
+
if (typeof message.body !== "string") throw new TypeError("native Queue message body must be text");
|
|
5078
|
+
envelope = validateNativeQueueEnvelope(JSON.parse(message.body));
|
|
5079
|
+
} catch (error) {
|
|
5080
|
+
console.error("world-celld native Queue rejected malformed broker message", error);
|
|
5081
|
+
message.retry({ delaySeconds: backoffSeconds(message.attempts) });
|
|
5082
|
+
return;
|
|
5083
|
+
}
|
|
5084
|
+
const now = Date.now();
|
|
5085
|
+
if (envelope.notBefore !== void 0 && envelope.notBefore > now) {
|
|
5086
|
+
try {
|
|
5087
|
+
await republish(env, envelope, now);
|
|
5088
|
+
message.ack();
|
|
5089
|
+
} catch (error) {
|
|
5090
|
+
console.error("world-celld native Queue failed to continue a long delay", error);
|
|
5091
|
+
message.retry({ delaySeconds: backoffSeconds(message.attempts) });
|
|
5092
|
+
}
|
|
5093
|
+
return;
|
|
5094
|
+
}
|
|
5095
|
+
const attempt = (envelope.deliveryFailures ?? 0) + message.attempts;
|
|
5096
|
+
if (!env.WORLD_SECRET) {
|
|
5097
|
+
console.error("world-celld Queue consumer missing WORLD_SECRET");
|
|
5098
|
+
message.retry({ delaySeconds: backoffSeconds(attempt) });
|
|
5099
|
+
return;
|
|
5100
|
+
}
|
|
5101
|
+
let response;
|
|
5102
|
+
try {
|
|
5103
|
+
response = await env.WORLD_SERVICE.deliver(env.WORLD_SECRET, envelope, attempt);
|
|
5104
|
+
} catch (error) {
|
|
5105
|
+
console.error("world-celld Queue consumer could not reach the world service", error);
|
|
5106
|
+
message.retry({ delaySeconds: backoffSeconds(attempt) });
|
|
5107
|
+
return;
|
|
5108
|
+
}
|
|
5109
|
+
if (response?.kind === "complete") {
|
|
5110
|
+
message.ack();
|
|
5111
|
+
return;
|
|
5112
|
+
}
|
|
5113
|
+
if (response?.kind === "suspend") try {
|
|
5114
|
+
const notBefore = queueDelayDeadline(Date.now(), response.timeoutSeconds, 1, QUEUE_CLAIM_STALE_MS);
|
|
5115
|
+
const suspensionCount = (envelope.suspensionCount ?? 0) + 1;
|
|
5116
|
+
if (notBefore !== null && suspensionCount <= 256) {
|
|
5117
|
+
await republish(env, {
|
|
5118
|
+
...envelope,
|
|
5119
|
+
notBefore,
|
|
5120
|
+
deliveryFailures: attempt - 1,
|
|
5121
|
+
suspensionCount
|
|
5122
|
+
});
|
|
5123
|
+
message.ack();
|
|
5124
|
+
return;
|
|
5125
|
+
}
|
|
5126
|
+
} catch (error) {
|
|
5127
|
+
console.error("world-celld Queue consumer received an invalid suspension response", error);
|
|
5128
|
+
}
|
|
5129
|
+
message.retry({ delaySeconds: backoffSeconds(attempt) });
|
|
5130
|
+
}
|
|
5131
|
+
var queue_consumer_default = { async queue(batch, env) {
|
|
5132
|
+
await Promise.all(batch.messages.map((message) => consume(message, env)));
|
|
5133
|
+
} };
|
|
5134
|
+
//#endregion
|
|
4905
5135
|
//#region src/testing/index.ts
|
|
4906
5136
|
/** Alarm-pump interval for the dev fleet (real time). */
|
|
4907
5137
|
const PUMP_INTERVAL_MS = 25;
|
|
4908
5138
|
async function startDevFleet(options = {}) {
|
|
4909
5139
|
const harness = await startHarness(options);
|
|
5140
|
+
const pending = [];
|
|
5141
|
+
const deliveries = /* @__PURE__ */ new Set();
|
|
5142
|
+
let publicationCursor = 0;
|
|
5143
|
+
let stopped = false;
|
|
5144
|
+
const queueEnv = {
|
|
5145
|
+
WORKFLOW_QUEUE: { async send(body, sendOptions) {
|
|
5146
|
+
harness.queueMessages.push(body);
|
|
5147
|
+
harness.queuePublications.push({
|
|
5148
|
+
body,
|
|
5149
|
+
delaySeconds: sendOptions?.delaySeconds ?? 0
|
|
5150
|
+
});
|
|
5151
|
+
} },
|
|
5152
|
+
WORLD_SERVICE: { deliver: (secret, envelope, attempt) => harness.deliverQueueMessage(secret, envelope, attempt) },
|
|
5153
|
+
WORLD_SECRET: options.secret
|
|
5154
|
+
};
|
|
5155
|
+
function dispatch(message) {
|
|
5156
|
+
let settled = false;
|
|
5157
|
+
const delivery = queue_consumer_default.queue({ messages: [{
|
|
5158
|
+
body: message.body,
|
|
5159
|
+
attempts: message.attempts,
|
|
5160
|
+
ack() {
|
|
5161
|
+
settled = true;
|
|
5162
|
+
},
|
|
5163
|
+
retry(retryOptions) {
|
|
5164
|
+
settled = true;
|
|
5165
|
+
if (!stopped) pending.push({
|
|
5166
|
+
body: message.body,
|
|
5167
|
+
attempts: message.attempts + 1,
|
|
5168
|
+
dueAt: Date.now() + (retryOptions?.delaySeconds ?? 0) * 1e3
|
|
5169
|
+
});
|
|
5170
|
+
}
|
|
5171
|
+
}] }, queueEnv).then(() => {
|
|
5172
|
+
if (!settled) {
|
|
5173
|
+
console.error("[world-celld dev fleet] Queue delivery was not settled");
|
|
5174
|
+
if (!stopped) pending.push({
|
|
5175
|
+
body: message.body,
|
|
5176
|
+
attempts: message.attempts + 1,
|
|
5177
|
+
dueAt: Date.now() + 1e3
|
|
5178
|
+
});
|
|
5179
|
+
}
|
|
5180
|
+
}).catch((error) => {
|
|
5181
|
+
console.error("[world-celld dev fleet] Queue consumer failed", error);
|
|
5182
|
+
if (!settled && !stopped) pending.push({
|
|
5183
|
+
body: message.body,
|
|
5184
|
+
attempts: message.attempts + 1,
|
|
5185
|
+
dueAt: Date.now() + 1e3
|
|
5186
|
+
});
|
|
5187
|
+
});
|
|
5188
|
+
deliveries.add(delivery);
|
|
5189
|
+
delivery.finally(() => deliveries.delete(delivery));
|
|
5190
|
+
}
|
|
5191
|
+
const queuePump = setInterval(() => {
|
|
5192
|
+
if (stopped) return;
|
|
5193
|
+
while (publicationCursor < harness.queuePublications.length) {
|
|
5194
|
+
const publication = harness.queuePublications[publicationCursor++];
|
|
5195
|
+
pending.push({
|
|
5196
|
+
body: publication.body,
|
|
5197
|
+
attempts: 1,
|
|
5198
|
+
dueAt: Date.now() + publication.delaySeconds * 1e3
|
|
5199
|
+
});
|
|
5200
|
+
}
|
|
5201
|
+
const now = Date.now();
|
|
5202
|
+
for (let index = 0; index < pending.length;) if (pending[index].dueAt <= now) dispatch(pending.splice(index, 1)[0]);
|
|
5203
|
+
else index++;
|
|
5204
|
+
}, PUMP_INTERVAL_MS);
|
|
5205
|
+
queuePump.unref?.();
|
|
4910
5206
|
let pumping = false;
|
|
4911
5207
|
const pump = setInterval(async () => {
|
|
4912
5208
|
if (pumping) return;
|
|
@@ -4924,7 +5220,10 @@ async function startDevFleet(options = {}) {
|
|
|
4924
5220
|
return {
|
|
4925
5221
|
...harness,
|
|
4926
5222
|
async stop() {
|
|
5223
|
+
stopped = true;
|
|
5224
|
+
clearInterval(queuePump);
|
|
4927
5225
|
clearInterval(pump);
|
|
5226
|
+
await Promise.allSettled(deliveries);
|
|
4928
5227
|
await harness.close();
|
|
4929
5228
|
}
|
|
4930
5229
|
};
|