@ewhauser/world-celld 0.8.1 → 0.9.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/testing.js CHANGED
@@ -1,4 +1,4 @@
1
- import { $ as validateStreamReadRequest, B as MAX_STREAM_READ_BYTES, C as HOOK_CLAIM_LEASE_MS, I as MAX_STREAM_BATCH_BYTES, J as encodeStreamReadResult, K as decodeStreamWriteBatch, L as MAX_STREAM_CHUNK_BYTES, M as isPositiveSafeInteger, N as isRecord$1, O as MAX_RUN_INDEX_PUBLICATION_LIFETIME_MS, Q as normalizeStreamError, U as STREAM_BATCH_CONTENT_TYPE, W as STREAM_CHUNK_LIMIT_HEADER, X as encodeStreamWriteResult, _ as rpcStringify, a as QUEUE_PUBLICATION_LEASE_MS, b as createWorkflowIndex, d as queuePayloadObjectKey, et as validateStreamWriteChunks, f as queuePayloadRegistryKey, g as rpcParse, h as compact, i as QUEUE_PAYLOAD_REGISTRY_PREFIX, j as isNonNegativeSafeInteger, l as queueClaimName, m as validateNativeQueueSendOptions, n as listRunsPage, o as QUEUE_RESERVATION_GRACE_MS, p as validateNativeQueueEnvelope, r as NATIVE_QUEUE_MAX_MESSAGE_BYTES, s as isInlineRunBody, t as MAX_RUN_LIST_LIMIT, u as queueOrphanName, v as parse, w as LIFECYCLE_COMPACTION_RETRY_MS, x as runIndexMetadata, z as MAX_STREAM_LONG_POLL_MS } from "./run-list-DGsdv6Jf.js";
1
+ import { B as MAX_STREAM_BATCH_BYTES, C as parseDeploymentUrls, D as LIFECYCLE_COMPACTION_RETRY_MS, E as HOOK_CLAIM_LEASE_MS, F as isNonNegativeSafeInteger, I as isPositiveSafeInteger, J as STREAM_CHUNK_LIMIT_HEADER, L as isRecord$1, M as QUEUE_CLAIM_STALE_MS, Q as encodeStreamReadResult, R as queueDelayDeadline, S as configuredDeploymentUrl, U as MAX_STREAM_LONG_POLL_MS, V as MAX_STREAM_CHUNK_BYTES, W as MAX_STREAM_READ_BYTES, X as decodeStreamWriteBatch, _ as rpcStringify, a as QUEUE_PUBLICATION_LEASE_MS, b as createWorkflowIndex, c as nativeQueueDelaySeconds, d as queuePayloadObjectKey, et as encodeStreamWriteResult, f as queuePayloadRegistryKey, g as rpcParse, h as compact, i as QUEUE_PAYLOAD_REGISTRY_PREFIX, it as validateStreamWriteChunks, j as MAX_RUN_INDEX_PUBLICATION_LIFETIME_MS, l as queueClaimName, m as validateNativeQueueSendOptions, n as listRunsPage, nt as normalizeStreamError, o as QUEUE_RESERVATION_GRACE_MS, p as validateNativeQueueEnvelope, q as STREAM_BATCH_CONTENT_TYPE, r as NATIVE_QUEUE_MAX_MESSAGE_BYTES, rt as validateStreamReadRequest, s as isInlineRunBody, t as MAX_RUN_LIST_LIMIT, u as queueOrphanName, v as parse, x as runIndexMetadata, y as stringify } from "./run-list-CHRaDRvU.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) {
@@ -2276,6 +2287,70 @@ async function resolveExpiredHookClaim(env, owner, claimId, token) {
2276
2287
  token
2277
2288
  });
2278
2289
  }
2290
+ /** Read-only owner check used by index repair; unlike the resolver it never fences. */
2291
+ async function getCommittedHook(env, owner, token) {
2292
+ const namespace = env.WORKFLOW_DB;
2293
+ if (!namespace) throw new Error("world-celld hook index missing WORKFLOW_DB binding");
2294
+ return await namespace.get(namespace.idFromName(owner.runId)).getCommittedHook({
2295
+ hookId: owner.hookId,
2296
+ token
2297
+ });
2298
+ }
2299
+ function warnRepair(action, owner) {
2300
+ console.warn(`world-celld ${action} hook index record for run ${owner.runId} hook ${owner.hookId}`);
2301
+ }
2302
+ function warnRepublished(owner) {
2303
+ warnRepair("republished", owner);
2304
+ }
2305
+ /**
2306
+ * Publish the record for a claim whose hook committed in its run but whose
2307
+ * creating client stopped before finalizing. The write applies only while the
2308
+ * observed claim is unchanged, so a concurrent release or finalize wins.
2309
+ * Returns the record now stored for the key, if any.
2310
+ */
2311
+ async function publishCommittedClaim(storage, env, keys, token) {
2312
+ const observed = await storage.get(keys.claim);
2313
+ if (!observed) return null;
2314
+ const hook = await getCommittedHook(env, observed.owner, token);
2315
+ if (!hook) return null;
2316
+ const serialized = stringify(hook);
2317
+ const published = await storage.transaction(async (txn) => {
2318
+ const values = await txn.get([keys.record, keys.claim]);
2319
+ const indexed = values.get(keys.record);
2320
+ if (indexed !== void 0) return {
2321
+ raw: indexed,
2322
+ written: false
2323
+ };
2324
+ const claim = values.get(keys.claim);
2325
+ if (claim?.claimId !== observed.claimId) return {
2326
+ raw: null,
2327
+ written: false
2328
+ };
2329
+ await txn.put(keys.record, serialized);
2330
+ await txn.delete([keys.claim, claim.deadlineKey]);
2331
+ return {
2332
+ raw: serialized,
2333
+ written: true
2334
+ };
2335
+ });
2336
+ if (!published.written) return published.raw;
2337
+ warnRepublished(observed.owner);
2338
+ return await confirmRepublished(storage, env, keys.record, observed.owner, token) ? serialized : null;
2339
+ }
2340
+ /**
2341
+ * Re-ask the owner after a repair write. A hook disposed between the owner's
2342
+ * answer and the write would otherwise keep a record whose index release was
2343
+ * already spent, so withdraw it.
2344
+ */
2345
+ async function confirmRepublished(storage, env, record, owner, token) {
2346
+ if (await getCommittedHook(env, owner, token)) return true;
2347
+ await storage.transaction(async (txn) => {
2348
+ const indexed = await txn.get(record);
2349
+ if (indexed !== void 0 && sameOwner(ownerFromSerializedHook(indexed), owner)) await txn.delete(record);
2350
+ });
2351
+ warnRepair("withdrew", owner);
2352
+ return false;
2353
+ }
2279
2354
  //#endregion
2280
2355
  //#region src/worker/durable-objects/HookIdDO.ts
2281
2356
  const MAX_RELEASE_BATCH$1 = 64;
@@ -2298,7 +2373,10 @@ var HookIdDO = class extends DurableObject {
2298
2373
  return typeof this.env.clock === "function" ? this.env.clock() : Date.now();
2299
2374
  }
2300
2375
  async get(hookId) {
2301
- const raw = await this.ctx.storage.get(recordKey$1(hookId));
2376
+ const raw = await this.ctx.storage.get(recordKey$1(hookId)) ?? await publishCommittedClaim(this.ctx.storage, this.env, {
2377
+ record: recordKey$1(hookId),
2378
+ claim: claimKey$1(hookId)
2379
+ });
2302
2380
  if (!raw) return null;
2303
2381
  const owner = ownerFromSerializedHook(raw);
2304
2382
  return await runIsActive(this.env, owner.runId) ? raw : null;
@@ -2463,8 +2541,9 @@ var HookIdDO = class extends DurableObject {
2463
2541
  const claim = observed.get(claimKey$1(item.hookId));
2464
2542
  if (!claim || claim.claimId !== item.claimId || claim.expiresAt > now) return;
2465
2543
  const hook = await resolveExpiredHookClaim(this.env, claim.owner, claim.claimId);
2466
- committed.set(claimKey$1(item.hookId), hook !== null);
2544
+ committed.set(claimKey$1(item.hookId), hook);
2467
2545
  }));
2546
+ const published = [];
2468
2547
  await this.ctx.storage.transaction(async (txn) => {
2469
2548
  const claims = await txn.get(page.map(([, item]) => claimKey$1(item.hookId)));
2470
2549
  const deletes = [];
@@ -2474,8 +2553,15 @@ var HookIdDO = class extends DurableObject {
2474
2553
  const snapshot = observed.get(key);
2475
2554
  if (claim?.claimId !== item.claimId || claim.expiresAt > now) deletes.push(deadline);
2476
2555
  else if (claim.claimId === snapshot?.claimId && claim.expiresAt === snapshot.expiresAt) {
2477
- if (committed.get(key)) await this.renewClaim(txn, item.hookId, claim, now);
2478
- else deletes.push(deadline, key);
2556
+ const hook = committed.get(key);
2557
+ if (hook) {
2558
+ await txn.put(recordKey$1(item.hookId), stringify(hook));
2559
+ published.push({
2560
+ hookId: item.hookId,
2561
+ owner: claim.owner
2562
+ });
2563
+ }
2564
+ deletes.push(deadline, key);
2479
2565
  }
2480
2566
  }
2481
2567
  for (let offset = 0; offset < deletes.length; offset += 128) await txn.delete(deletes.slice(offset, offset + 128));
@@ -2490,6 +2576,10 @@ var HookIdDO = class extends DurableObject {
2490
2576
  if (nextAt === void 0) await txn.deleteAlarm();
2491
2577
  else await txn.setAlarm(Math.max(now + 1, nextAt));
2492
2578
  });
2579
+ for (const { hookId, owner } of published) {
2580
+ warnRepublished(owner);
2581
+ await confirmRepublished(this.ctx.storage, this.env, recordKey$1(hookId), owner);
2582
+ }
2493
2583
  }
2494
2584
  };
2495
2585
  //#endregion
@@ -2514,7 +2604,10 @@ var HookTokenDO = class extends DurableObject {
2514
2604
  return typeof this.env.clock === "function" ? this.env.clock() : Date.now();
2515
2605
  }
2516
2606
  async get(token) {
2517
- const raw = await this.ctx.storage.get(recordKey(token));
2607
+ const raw = await this.ctx.storage.get(recordKey(token)) ?? await publishCommittedClaim(this.ctx.storage, this.env, {
2608
+ record: recordKey(token),
2609
+ claim: claimKey(token)
2610
+ }, token);
2518
2611
  if (!raw) return null;
2519
2612
  const owner = ownerFromSerializedHook(raw);
2520
2613
  return await runIsActive(this.env, owner.runId) ? raw : null;
@@ -2678,8 +2771,9 @@ var HookTokenDO = class extends DurableObject {
2678
2771
  const claim = observed.get(claimKey(item.token));
2679
2772
  if (!claim || claim.claimId !== item.claimId || claim.expiresAt > now) return;
2680
2773
  const hook = await resolveExpiredHookClaim(this.env, claim.owner, claim.claimId, item.token);
2681
- committed.set(claimKey(item.token), hook !== null);
2774
+ committed.set(claimKey(item.token), hook);
2682
2775
  }));
2776
+ const published = [];
2683
2777
  await this.ctx.storage.transaction(async (txn) => {
2684
2778
  const claims = await txn.get(page.map(([, item]) => claimKey(item.token)));
2685
2779
  const deletes = [];
@@ -2689,8 +2783,15 @@ var HookTokenDO = class extends DurableObject {
2689
2783
  const snapshot = observed.get(key);
2690
2784
  if (claim?.claimId !== item.claimId || claim.expiresAt > now) deletes.push(deadline);
2691
2785
  else if (claim.claimId === snapshot?.claimId && claim.expiresAt === snapshot.expiresAt) {
2692
- if (committed.get(key)) await this.renewClaim(txn, item.token, claim, now);
2693
- else deletes.push(deadline, key);
2786
+ const hook = committed.get(key);
2787
+ if (hook) {
2788
+ await txn.put(recordKey(item.token), stringify(hook));
2789
+ published.push({
2790
+ token: item.token,
2791
+ owner: claim.owner
2792
+ });
2793
+ }
2794
+ deletes.push(deadline, key);
2694
2795
  }
2695
2796
  }
2696
2797
  for (let offset = 0; offset < deletes.length; offset += 128) await txn.delete(deletes.slice(offset, offset + 128));
@@ -2705,6 +2806,10 @@ var HookTokenDO = class extends DurableObject {
2705
2806
  if (nextAt === void 0) await txn.deleteAlarm();
2706
2807
  else await txn.setAlarm(Math.max(now + 1, nextAt));
2707
2808
  });
2809
+ for (const { token, owner } of published) {
2810
+ warnRepublished(owner);
2811
+ await confirmRepublished(this.ctx.storage, this.env, recordKey(token), owner, token);
2812
+ }
2708
2813
  }
2709
2814
  };
2710
2815
  //#endregion
@@ -3810,6 +3915,23 @@ var WorkflowRunDO = class extends DurableObject {
3810
3915
  return null;
3811
3916
  });
3812
3917
  }
3918
+ /**
3919
+ * Read-only twin of resolveExpiredHookClaim for index repair: the committed
3920
+ * hook of a live run, or null. It fences nothing, so a shard may ask while
3921
+ * the hook's creation is still in flight.
3922
+ */
3923
+ async getCommittedHook(request) {
3924
+ if (typeof request?.hookId !== "string" || request.hookId.length === 0 || request.token !== void 0 && typeof request.token !== "string") throw new TypeError("world-celld committed hook lookup is invalid");
3925
+ return await this.ctx.storage.transaction(async (txn) => {
3926
+ const { tombstone } = await this.retentionState(txn);
3927
+ if (tombstone) return null;
3928
+ const run = await txn.get("run");
3929
+ if (!run || isTerminalWorkflowRunStatus(run.status)) return null;
3930
+ const hook = await txn.get(`${HOOK_KEY_PREFIX}${request.hookId}`);
3931
+ if (!hook || request.token !== void 0 && hook.token !== request.token) return null;
3932
+ return hook;
3933
+ });
3934
+ }
3813
3935
  /** Single authoritative lifecycle answer used after derivative compaction. */
3814
3936
  async getLifecycleStatus() {
3815
3937
  return await this.ctx.storage.transaction(async (txn) => {
@@ -4801,6 +4923,157 @@ var WorkflowRunDO = class extends DurableObject {
4801
4923
  }
4802
4924
  };
4803
4925
  //#endregion
4926
+ //#region src/worker/queue-delivery.ts
4927
+ /** Internal delivery contract. No Request/Response objects cross the RPC boundary. */
4928
+ const MAX_QUEUE_DELIVERY_RESPONSE_BYTES = 65536;
4929
+ const QUEUE_ORPHAN_GRACE_MS = 432e6;
4930
+ const PERMANENT_QUEUE_STATUSES = /* @__PURE__ */ new Set([
4931
+ 404,
4932
+ 409,
4933
+ 410,
4934
+ 422
4935
+ ]);
4936
+ async function readResponseText(response) {
4937
+ if (!response.body) return "";
4938
+ const reader = response.body.getReader();
4939
+ const decoder = new TextDecoder();
4940
+ let total = 0;
4941
+ let text = "";
4942
+ try {
4943
+ for (;;) {
4944
+ const { done, value } = await reader.read();
4945
+ if (done) break;
4946
+ total += value.byteLength;
4947
+ if (total > MAX_QUEUE_DELIVERY_RESPONSE_BYTES) {
4948
+ await reader.cancel("queue delivery response is too large").catch(() => void 0);
4949
+ throw new Error("workflow queue handler response exceeds configured limit");
4950
+ }
4951
+ text += decoder.decode(value, { stream: true });
4952
+ }
4953
+ return text + decoder.decode();
4954
+ } finally {
4955
+ reader.releaseLock();
4956
+ }
4957
+ }
4958
+ /** Validate at the service boundary; callers cannot bypass auth via RPC. */
4959
+ async function deliverQueueMessage(env, secret, input, attempt) {
4960
+ if (!env.WORLD_SECRET) throw new Error("WORLD_SECRET is not configured");
4961
+ if (typeof secret !== "string" || !secret || !await timingSafeEqual(secret, env.WORLD_SECRET)) throw new Error("Unauthorized Queue delivery");
4962
+ const envelope = validateNativeQueueEnvelope(input);
4963
+ if (!Number.isSafeInteger(attempt) || attempt < 1) throw new TypeError("queue delivery attempt must be a positive safe integer");
4964
+ if (!env.WORKFLOW_DB) throw new Error("missing binding: WORKFLOW_DB");
4965
+ const namespace = env.WORKFLOW_DB;
4966
+ const payloadStore = env.WORKFLOW_QUEUE_PAYLOADS;
4967
+ const runStub = (name) => namespace.get(namespace.idFromName(name));
4968
+ const claim = deliveryQueueClaim(envelope, runStub);
4969
+ let claimed = false;
4970
+ async function releaseClaim() {
4971
+ if (claimed) await claim.release(envelope.messageId);
4972
+ }
4973
+ async function unregisterPayload() {
4974
+ if (envelope.runId) {
4975
+ await runStub(envelope.runId).unregisterQueuePayload(envelope.messageId);
4976
+ await runStub(queueOrphanName(envelope.messageId)).cancelQueuePayloadOrphan(envelope.messageId);
4977
+ }
4978
+ }
4979
+ try {
4980
+ let admission;
4981
+ if (envelope.runId && !envelope.payloadKey && envelope.claimScope !== "run") {
4982
+ admission = runStub(envelope.runId).getQueueAdmission();
4983
+ admission.catch(() => void 0);
4984
+ }
4985
+ if (claim) {
4986
+ const result = await claim.claim(envelope.messageId, QUEUE_CLAIM_STALE_MS);
4987
+ if (result.expired) return { kind: "complete" };
4988
+ if (!result.claimed) {
4989
+ const now = Date.now();
4990
+ if (result.retryAt !== void 0 && result.retryAt > now) return {
4991
+ kind: "suspend",
4992
+ timeoutSeconds: Math.max(1, Math.ceil((result.retryAt - now) / 1e3))
4993
+ };
4994
+ return { kind: "complete" };
4995
+ }
4996
+ claimed = true;
4997
+ }
4998
+ if (admission && !(await admission).ok) {
4999
+ await claim?.complete(envelope.messageId);
5000
+ return { kind: "complete" };
5001
+ }
5002
+ let body = envelope.body;
5003
+ if (envelope.payloadKey) {
5004
+ if (!payloadStore) throw new Error("missing binding: WORKFLOW_QUEUE_PAYLOADS");
5005
+ body = await payloadStore.read(envelope.payloadKey) ?? void 0;
5006
+ if (body === void 0) {
5007
+ await unregisterPayload();
5008
+ await claim?.complete(envelope.messageId);
5009
+ return { kind: "complete" };
5010
+ }
5011
+ }
5012
+ const deploymentUrls = parseDeploymentUrls(env.WORKFLOW_DEPLOYMENT_URLS);
5013
+ let targetBaseUrl = envelope.targetBaseUrl;
5014
+ if (deploymentUrls && envelope.runId) {
5015
+ const stored = await runStub(envelope.runId).getRun();
5016
+ if (!stored.ok) throw new Error("world-celld: cannot route an expired run");
5017
+ const initialId = parse(body).runInput?.deploymentId;
5018
+ const pinnedId = stored.value?.deploymentId ?? initialId;
5019
+ if (typeof pinnedId !== "string" || pinnedId.length === 0) throw new Error(`world-celld: no pinned deployment for run ${envelope.runId}`);
5020
+ if (stored.value && initialId !== void 0 && initialId !== pinnedId) throw new Error(`world-celld: queue run deployment hints disagree for ${envelope.runId}`);
5021
+ targetBaseUrl = configuredDeploymentUrl(deploymentUrls, pinnedId);
5022
+ }
5023
+ const headers = {
5024
+ "content-type": "application/json",
5025
+ "x-vqs-queue-name": envelope.queueName,
5026
+ "x-vqs-message-id": envelope.messageId,
5027
+ "x-vqs-message-attempt": String(attempt)
5028
+ };
5029
+ if (env.WORKFLOW_CALLBACK_SECRET) headers["x-workflow-callback-secret"] = env.WORKFLOW_CALLBACK_SECRET;
5030
+ const callback = await fetch(`${targetBaseUrl.replace(/\/$/, "")}/.well-known/workflow/v1/flow`, {
5031
+ method: "POST",
5032
+ headers,
5033
+ body,
5034
+ signal: AbortSignal.timeout(3e5)
5035
+ });
5036
+ if (callback.ok || !deploymentUrls && PERMANENT_QUEUE_STATUSES.has(callback.status) || !envelope.runId && PERMANENT_QUEUE_STATUSES.has(callback.status)) {
5037
+ await callback.body?.cancel().catch(() => void 0);
5038
+ if (envelope.payloadKey) {
5039
+ await payloadStore.delete(envelope.payloadKey);
5040
+ await unregisterPayload();
5041
+ }
5042
+ await claim?.complete(envelope.messageId);
5043
+ return { kind: "complete" };
5044
+ }
5045
+ if (callback.status === 503) {
5046
+ const responseBody = await readResponseText(callback);
5047
+ const timeoutSeconds = JSON.parse(responseBody).timeoutSeconds;
5048
+ const notBefore = queueDelayDeadline(Date.now(), timeoutSeconds, 1, QUEUE_CLAIM_STALE_MS);
5049
+ if (notBefore === null) throw new Error("workflow queue suspension deadline is invalid");
5050
+ if (envelope.payloadKey && envelope.runId) await runStub(queueOrphanName(envelope.messageId)).scheduleQueuePayloadOrphan({
5051
+ messageId: envelope.messageId,
5052
+ runId: envelope.runId,
5053
+ key: envelope.payloadKey,
5054
+ expiresAt: notBefore + QUEUE_ORPHAN_GRACE_MS
5055
+ });
5056
+ if (claim) await claim.hold({
5057
+ messageId: envelope.messageId,
5058
+ retryAt: notBefore,
5059
+ expiresAt: notBefore + QUEUE_CLAIM_STALE_MS,
5060
+ reservationExpiresAt: notBefore + QUEUE_ORPHAN_GRACE_MS
5061
+ });
5062
+ return {
5063
+ kind: "suspend",
5064
+ timeoutSeconds
5065
+ };
5066
+ }
5067
+ await callback.body?.cancel().catch(() => void 0);
5068
+ await releaseClaim();
5069
+ claimed = false;
5070
+ return { kind: "retry" };
5071
+ } catch (error) {
5072
+ await releaseClaim();
5073
+ throw error;
5074
+ }
5075
+ }
5076
+ //#endregion
4804
5077
  //#region src/testing/http-harness.ts
4805
5078
  /**
4806
5079
  * Boots the real worker router over real cell classes (on FakeFleet state)
@@ -4809,6 +5082,7 @@ var WorkflowRunDO = class extends DurableObject {
4809
5082
  async function startHarness(options = {}) {
4810
5083
  const cellEnv = { ...options.cellEnv };
4811
5084
  const queueMessages = [];
5085
+ const queuePublications = [];
4812
5086
  const queuePayloads = /* @__PURE__ */ new Map();
4813
5087
  const queuePayloadBucket = {
4814
5088
  async put(key, value) {
@@ -4822,8 +5096,12 @@ async function startHarness(options = {}) {
4822
5096
  for (const key of Array.isArray(keys) ? keys : [keys]) queuePayloads.delete(key);
4823
5097
  }
4824
5098
  };
4825
- const nativeQueue = { async send(body) {
5099
+ const nativeQueue = { async send(body, sendOptions) {
4826
5100
  queueMessages.push(body);
5101
+ queuePublications.push({
5102
+ body,
5103
+ delaySeconds: sendOptions?.delaySeconds ?? 0
5104
+ });
4827
5105
  } };
4828
5106
  const fleet = new FakeFleet({
4829
5107
  runs: WorkflowRunDO,
@@ -4842,7 +5120,7 @@ async function startHarness(options = {}) {
4842
5120
  WORKFLOW_HOOK_IDS: fleet.namespace("hook-ids"),
4843
5121
  WORKFLOW_QUEUE_PAYLOADS: queuePayloadBucket
4844
5122
  });
4845
- const router = createRouter({
5123
+ const env = {
4846
5124
  WORKFLOW_DB: fleet.namespace("runs"),
4847
5125
  WORKFLOW_STREAMS: fleet.namespace("streams"),
4848
5126
  WORKFLOW_RUN_CATALOG: fleet.namespace("run-catalog"),
@@ -4851,7 +5129,8 @@ async function startHarness(options = {}) {
4851
5129
  WORKFLOW_QUEUE: nativeQueue,
4852
5130
  WORKFLOW_QUEUE_PAYLOADS: createQueuePayloadStore(queuePayloadBucket),
4853
5131
  WORLD_SECRET: options.secret
4854
- });
5132
+ };
5133
+ const router = createRouter(env);
4855
5134
  const server = http.createServer(async (req, res) => {
4856
5135
  const requestAbort = new AbortController();
4857
5136
  const abortRequest = () => {
@@ -4894,7 +5173,9 @@ async function startHarness(options = {}) {
4894
5173
  url: `http://127.0.0.1:${port}`,
4895
5174
  fleet,
4896
5175
  queueMessages,
5176
+ queuePublications,
4897
5177
  queuePayloads,
5178
+ deliverQueueMessage: (secret, envelope, attempt) => deliverQueueMessage(env, secret, envelope, attempt),
4898
5179
  close: () => new Promise((resolve, reject) => {
4899
5180
  server.close((err) => err ? reject(err) : resolve());
4900
5181
  server.closeAllConnections();
@@ -4902,11 +5183,148 @@ async function startHarness(options = {}) {
4902
5183
  };
4903
5184
  }
4904
5185
  //#endregion
5186
+ //#region src/worker/queue-consumer.ts
5187
+ function backoffSeconds(attempt) {
5188
+ return Math.min(60, 2 ** Math.min(attempt, 30));
5189
+ }
5190
+ async function republish(env, envelope, now = Date.now()) {
5191
+ await env.WORKFLOW_QUEUE.send(JSON.stringify(envelope), {
5192
+ contentType: "text",
5193
+ delaySeconds: nativeQueueDelaySeconds(envelope.notBefore, now)
5194
+ });
5195
+ }
5196
+ async function consume(message, env) {
5197
+ let envelope;
5198
+ try {
5199
+ if (typeof message.body !== "string") throw new TypeError("native Queue message body must be text");
5200
+ envelope = validateNativeQueueEnvelope(JSON.parse(message.body));
5201
+ } catch (error) {
5202
+ console.error("world-celld native Queue rejected malformed broker message", error);
5203
+ message.retry({ delaySeconds: backoffSeconds(message.attempts) });
5204
+ return;
5205
+ }
5206
+ const now = Date.now();
5207
+ if (envelope.notBefore !== void 0 && envelope.notBefore > now) {
5208
+ try {
5209
+ await republish(env, envelope, now);
5210
+ message.ack();
5211
+ } catch (error) {
5212
+ console.error("world-celld native Queue failed to continue a long delay", error);
5213
+ message.retry({ delaySeconds: backoffSeconds(message.attempts) });
5214
+ }
5215
+ return;
5216
+ }
5217
+ const attempt = (envelope.deliveryFailures ?? 0) + message.attempts;
5218
+ if (!env.WORLD_SECRET) {
5219
+ console.error("world-celld Queue consumer missing WORLD_SECRET");
5220
+ message.retry({ delaySeconds: backoffSeconds(attempt) });
5221
+ return;
5222
+ }
5223
+ let response;
5224
+ try {
5225
+ response = await env.WORLD_SERVICE.deliver(env.WORLD_SECRET, envelope, attempt);
5226
+ } catch (error) {
5227
+ console.error("world-celld Queue consumer could not reach the world service", error);
5228
+ message.retry({ delaySeconds: backoffSeconds(attempt) });
5229
+ return;
5230
+ }
5231
+ if (response?.kind === "complete") {
5232
+ message.ack();
5233
+ return;
5234
+ }
5235
+ if (response?.kind === "suspend") try {
5236
+ const notBefore = queueDelayDeadline(Date.now(), response.timeoutSeconds, 1, QUEUE_CLAIM_STALE_MS);
5237
+ const suspensionCount = (envelope.suspensionCount ?? 0) + 1;
5238
+ if (notBefore !== null && suspensionCount <= 256) {
5239
+ await republish(env, {
5240
+ ...envelope,
5241
+ notBefore,
5242
+ deliveryFailures: attempt - 1,
5243
+ suspensionCount
5244
+ });
5245
+ message.ack();
5246
+ return;
5247
+ }
5248
+ } catch (error) {
5249
+ console.error("world-celld Queue consumer received an invalid suspension response", error);
5250
+ }
5251
+ message.retry({ delaySeconds: backoffSeconds(attempt) });
5252
+ }
5253
+ var queue_consumer_default = { async queue(batch, env) {
5254
+ await Promise.all(batch.messages.map((message) => consume(message, env)));
5255
+ } };
5256
+ //#endregion
4905
5257
  //#region src/testing/index.ts
4906
5258
  /** Alarm-pump interval for the dev fleet (real time). */
4907
5259
  const PUMP_INTERVAL_MS = 25;
4908
5260
  async function startDevFleet(options = {}) {
4909
5261
  const harness = await startHarness(options);
5262
+ const pending = [];
5263
+ const deliveries = /* @__PURE__ */ new Set();
5264
+ let publicationCursor = 0;
5265
+ let stopped = false;
5266
+ const queueEnv = {
5267
+ WORKFLOW_QUEUE: { async send(body, sendOptions) {
5268
+ harness.queueMessages.push(body);
5269
+ harness.queuePublications.push({
5270
+ body,
5271
+ delaySeconds: sendOptions?.delaySeconds ?? 0
5272
+ });
5273
+ } },
5274
+ WORLD_SERVICE: { deliver: (secret, envelope, attempt) => harness.deliverQueueMessage(secret, envelope, attempt) },
5275
+ WORLD_SECRET: options.secret
5276
+ };
5277
+ function dispatch(message) {
5278
+ let settled = false;
5279
+ const delivery = queue_consumer_default.queue({ messages: [{
5280
+ body: message.body,
5281
+ attempts: message.attempts,
5282
+ ack() {
5283
+ settled = true;
5284
+ },
5285
+ retry(retryOptions) {
5286
+ settled = true;
5287
+ if (!stopped) pending.push({
5288
+ body: message.body,
5289
+ attempts: message.attempts + 1,
5290
+ dueAt: Date.now() + (retryOptions?.delaySeconds ?? 0) * 1e3
5291
+ });
5292
+ }
5293
+ }] }, queueEnv).then(() => {
5294
+ if (!settled) {
5295
+ console.error("[world-celld dev fleet] Queue delivery was not settled");
5296
+ if (!stopped) pending.push({
5297
+ body: message.body,
5298
+ attempts: message.attempts + 1,
5299
+ dueAt: Date.now() + 1e3
5300
+ });
5301
+ }
5302
+ }).catch((error) => {
5303
+ console.error("[world-celld dev fleet] Queue consumer failed", error);
5304
+ if (!settled && !stopped) pending.push({
5305
+ body: message.body,
5306
+ attempts: message.attempts + 1,
5307
+ dueAt: Date.now() + 1e3
5308
+ });
5309
+ });
5310
+ deliveries.add(delivery);
5311
+ delivery.finally(() => deliveries.delete(delivery));
5312
+ }
5313
+ const queuePump = setInterval(() => {
5314
+ if (stopped) return;
5315
+ while (publicationCursor < harness.queuePublications.length) {
5316
+ const publication = harness.queuePublications[publicationCursor++];
5317
+ pending.push({
5318
+ body: publication.body,
5319
+ attempts: 1,
5320
+ dueAt: Date.now() + publication.delaySeconds * 1e3
5321
+ });
5322
+ }
5323
+ const now = Date.now();
5324
+ for (let index = 0; index < pending.length;) if (pending[index].dueAt <= now) dispatch(pending.splice(index, 1)[0]);
5325
+ else index++;
5326
+ }, PUMP_INTERVAL_MS);
5327
+ queuePump.unref?.();
4910
5328
  let pumping = false;
4911
5329
  const pump = setInterval(async () => {
4912
5330
  if (pumping) return;
@@ -4924,7 +5342,10 @@ async function startDevFleet(options = {}) {
4924
5342
  return {
4925
5343
  ...harness,
4926
5344
  async stop() {
5345
+ stopped = true;
5346
+ clearInterval(queuePump);
4927
5347
  clearInterval(pump);
5348
+ await Promise.allSettled(deliveries);
4928
5349
  await harness.close();
4929
5350
  }
4930
5351
  };