@ewhauser/world-celld 0.1.0 → 0.2.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/dist/index.js CHANGED
@@ -1,10 +1,13 @@
1
- import { a as stringify, i as parse, n as rpcParse, r as rpcStringify, t as compact } from "./util-BvWlkqgV.js";
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
2
  import { EventSchema, HookSchema, MessageId, SPEC_VERSION_CURRENT, StepSchema, WorkflowRunSchema, 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";
6
6
  //#region src/config.ts
7
7
  function resolveConfig(config) {
8
+ const retentionRaw = config?.runRetentionMs ?? process.env.CELLD_RUN_RETENTION_MS ?? 0;
9
+ const runRetentionMs = typeof retentionRaw === "number" ? retentionRaw : Number.parseInt(retentionRaw, 10);
10
+ if (!Number.isSafeInteger(runRetentionMs) || runRetentionMs < 0) throw new Error("world-celld: runRetentionMs must be a non-negative safe integer");
8
11
  return {
9
12
  fleetUrl: config?.fleetUrl ?? process.env.CELLD_FLEET_URL,
10
13
  secret: config?.secret ?? process.env.CELLD_WORLD_SECRET,
@@ -12,6 +15,7 @@ function resolveConfig(config) {
12
15
  deploymentId: config?.deploymentId ?? process.env.CELLD_DEPLOYMENT_ID ?? "celld-default",
13
16
  baseUrl: config?.baseUrl,
14
17
  queueShards: config?.queueShards ?? 1,
18
+ runRetentionMs,
15
19
  readPollMs: config?.readPollMs ?? 250,
16
20
  rpcTimeoutMs: config?.rpcTimeoutMs ?? 3e4
17
21
  };
@@ -113,9 +117,18 @@ const RUNS = {
113
117
  "getEvent",
114
118
  "listEvents",
115
119
  "listSteps",
116
- "listHooks"
120
+ "listHooks",
121
+ "getCleanupStatus",
122
+ "scheduleCleanup",
123
+ "cleanupNow",
124
+ "rearmCleanup"
117
125
  ],
118
- mutating: /* @__PURE__ */ new Set(["applyEvent"])
126
+ mutating: /* @__PURE__ */ new Set([
127
+ "applyEvent",
128
+ "scheduleCleanup",
129
+ "cleanupNow",
130
+ "rearmCleanup"
131
+ ])
119
132
  };
120
133
  const STREAMS = {
121
134
  methods: [
@@ -124,12 +137,18 @@ const STREAMS = {
124
137
  "getChunks",
125
138
  "getInfo",
126
139
  "registerStream",
127
- "listStreams"
140
+ "listStreams",
141
+ "expireRegistry",
142
+ "finalizeRegistry",
143
+ "expireStream"
128
144
  ],
129
145
  mutating: /* @__PURE__ */ new Set([
130
146
  "writeChunk",
131
147
  "closeStream",
132
- "registerStream"
148
+ "registerStream",
149
+ "expireRegistry",
150
+ "finalizeRegistry",
151
+ "expireStream"
133
152
  ])
134
153
  };
135
154
  const QUEUE = {
@@ -139,13 +158,15 @@ const QUEUE = {
139
158
  "listDeadLetters",
140
159
  "redriveDeadLetter",
141
160
  "purgeDeadLetters",
142
- "rearmAlarm"
161
+ "rearmAlarm",
162
+ "expireRun"
143
163
  ],
144
164
  mutating: /* @__PURE__ */ new Set([
145
165
  "enqueue",
146
166
  "redriveDeadLetter",
147
167
  "purgeDeadLetters",
148
- "rearmAlarm"
168
+ "rearmAlarm",
169
+ "expireRun"
149
170
  ])
150
171
  };
151
172
  function makeStub(transport, binding, name, spec) {
@@ -171,6 +192,12 @@ function makeIndexNamespace(transport) {
171
192
  get: (key) => call("get", [key], true),
172
193
  put: (key, value) => call("put", [key, value], false),
173
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),
174
201
  list: (options) => call("list", [options], true),
175
202
  reserveHookToken: (token, owner) => call("reserveHookToken", [token, owner], false),
176
203
  finalizeHookIndexes: (token, hookId, serializedHook, owner) => call("finalizeHookIndexes", [
@@ -430,6 +457,7 @@ function createQueue(config) {
430
457
  return {
431
458
  async queue(queueName, message, opts) {
432
459
  const { kind } = parseQueueName(queueName);
460
+ const runId = "runId" in message ? message.runId : "workflowRunId" in message ? message.workflowRunId : void 0;
433
461
  if (isTestMode()) {
434
462
  const existing = testPump.inflight(opts?.idempotencyKey);
435
463
  if (existing) return { messageId: existing };
@@ -450,6 +478,7 @@ function createQueue(config) {
450
478
  queueName,
451
479
  pathname: QUEUE_PATHNAMES[kind],
452
480
  body: stringify(message),
481
+ runId,
453
482
  idempotencyKey: opts?.idempotencyKey,
454
483
  delaySeconds: opts?.delaySeconds,
455
484
  config: {
@@ -457,7 +486,10 @@ function createQueue(config) {
457
486
  queueShards
458
487
  }
459
488
  });
460
- if (!outcome.ok) throw new WorkflowWorldError(outcome.message, { status: 409 });
489
+ if (!outcome.ok) {
490
+ if (outcome.code === "RUN_EXPIRED") throw new RunExpiredError(outcome.message);
491
+ throw new WorkflowWorldError(outcome.message, { status: 409 });
492
+ }
461
493
  return { messageId: MessageId.parse(outcome.messageId) };
462
494
  },
463
495
  createQueueHandler(queueNamePrefix, handler) {
@@ -515,15 +547,6 @@ function createQueue(config) {
515
547
  }
516
548
  //#endregion
517
549
  //#region src/storage.ts
518
- function sortableTimestamp(date) {
519
- return date.getTime().toString().padStart(13, "0");
520
- }
521
- function workflowRunIndexKey(run) {
522
- return `run:${run.workflowName}:${sortableTimestamp(run.createdAt)}:${run.runId}`;
523
- }
524
- function globalRunIndexKey(run) {
525
- return `runall:${sortableTimestamp(run.createdAt)}:${run.runId}`;
526
- }
527
550
  function hookOwner(hook) {
528
551
  return {
529
552
  runId: hook.runId,
@@ -577,15 +600,23 @@ const parseRun = (run) => WorkflowRunSchema.parse(compact(run));
577
600
  const parseStep = (step) => StepSchema.parse(compact(step));
578
601
  const parseHook = (hook) => HookSchema.parse(compact(hook));
579
602
  const parseEvent = (event) => EventSchema.parse(compact(event));
603
+ function unwrapRead(outcome) {
604
+ if (!outcome.ok) throw new RunExpiredError(outcome.message);
605
+ return outcome.value;
606
+ }
580
607
  function createStorage(config) {
581
608
  const { env } = config;
609
+ const cleanup = {
610
+ retentionMs: config.runRetentionMs ?? 0,
611
+ queueShards: config.queueShards ?? 1
612
+ };
582
613
  const ulid = monotonicFactory();
583
614
  const getRunDO = (runId) => {
584
615
  const id = env.WORKFLOW_DB.idFromName(runId);
585
616
  return env.WORKFLOW_DB.get(id);
586
617
  };
587
618
  const runsGet = async (runId, params) => {
588
- const run = await getRunDO(runId).getRun();
619
+ const run = unwrapRead(await getRunDO(runId).getRun());
589
620
  if (!run) throw new WorkflowRunNotFoundError(runId);
590
621
  return filterData(parseRun(run), params?.resolveData, ["input", "output"]);
591
622
  };
@@ -624,7 +655,7 @@ function createStorage(config) {
624
655
  if (matches.length > limit) break;
625
656
  }
626
657
  } catch (error) {
627
- if (!WorkflowRunNotFoundError.is(error)) throw error;
658
+ if (!WorkflowRunNotFoundError.is(error) && !RunExpiredError.is(error)) throw error;
628
659
  }
629
660
  }
630
661
  exhausted = kvList.list_complete;
@@ -661,7 +692,8 @@ function createStorage(config) {
661
692
  outcome = await stub.applyEvent({
662
693
  runId: effectiveRunId,
663
694
  data,
664
- tokenHolder
695
+ tokenHolder,
696
+ cleanup
665
697
  });
666
698
  } catch (error) {
667
699
  if (hookReservation && data.eventType === "hook_created") await env.WORKFLOW_INDEX.releaseHookToken(data.eventData.token, hookReservation);
@@ -677,8 +709,8 @@ function createStorage(config) {
677
709
  createdAt: outcome.run.createdAt.toISOString(),
678
710
  status: outcome.run.status
679
711
  });
680
- await env.WORKFLOW_INDEX.put(workflowRunIndexKey(outcome.run), meta);
681
- await env.WORKFLOW_INDEX.put(globalRunIndexKey(outcome.run), meta);
712
+ await env.WORKFLOW_INDEX.putOwned(effectiveRunId, workflowRunIndexKey(outcome.run), meta);
713
+ await env.WORKFLOW_INDEX.putOwned(effectiveRunId, globalRunIndexKey(outcome.run), meta);
682
714
  }
683
715
  if (outcome.hookToIndex) {
684
716
  const serialized = stringify(outcome.hookToIndex);
@@ -689,8 +721,8 @@ function createStorage(config) {
689
721
  hookId: released.hookId
690
722
  });
691
723
  if (outcome.event?.correlationId) {
692
- const correlationKey = `correlation:${encodeURIComponent(outcome.event.correlationId)}:${sortableTimestamp(outcome.event.createdAt)}:${outcome.event.eventId}:${effectiveRunId}`;
693
- await env.WORKFLOW_INDEX.put(correlationKey, JSON.stringify({
724
+ const correlationKey = correlationIndexKey(outcome.event.correlationId, outcome.event.createdAt, outcome.event.eventId, effectiveRunId);
725
+ await env.WORKFLOW_INDEX.putOwned(effectiveRunId, correlationKey, JSON.stringify({
694
726
  runId: effectiveRunId,
695
727
  eventId: outcome.event.eventId
696
728
  }));
@@ -704,18 +736,18 @@ function createStorage(config) {
704
736
  };
705
737
  },
706
738
  async get(runId, eventId, _params) {
707
- const event = await getRunDO(runId).getEvent(eventId);
739
+ const event = unwrapRead(await getRunDO(runId).getEvent(eventId));
708
740
  if (!event) throw new WorkflowWorldError(`Event not found: ${eventId}`, { status: 404 });
709
741
  return parseEvent(event);
710
742
  },
711
743
  async list(params) {
712
744
  const { runId } = params;
713
745
  const limit = params?.pagination?.limit ?? 100;
714
- const result = await getRunDO(runId).listEvents({
746
+ const result = unwrapRead(await getRunDO(runId).listEvents({
715
747
  limit,
716
748
  cursor: params?.pagination?.cursor || void 0,
717
749
  sortOrder: params.pagination?.sortOrder || "asc"
718
- });
750
+ }));
719
751
  return {
720
752
  data: result.data.map(parseEvent),
721
753
  cursor: result.cursor,
@@ -736,7 +768,8 @@ function createStorage(config) {
736
768
  const raw = await env.WORKFLOW_INDEX.get(name);
737
769
  if (!raw) return null;
738
770
  const { runId, eventId } = JSON.parse(raw);
739
- return getRunDO(runId).getEvent(eventId);
771
+ const outcome = await getRunDO(runId).getEvent(eventId);
772
+ return outcome.ok ? outcome.value : null;
740
773
  }))).filter((event) => event !== null).map(parseEvent),
741
774
  cursor: listed.list_complete ? null : listed.cursor ?? null,
742
775
  hasMore: !listed.list_complete
@@ -746,18 +779,18 @@ function createStorage(config) {
746
779
  steps: {
747
780
  async get(runId, stepId, params) {
748
781
  if (!runId) throw new WorkflowWorldError("runId is required for Cloudflare step lookup", { status: 400 });
749
- const step = await getRunDO(runId).getStep(stepId);
782
+ const step = unwrapRead(await getRunDO(runId).getStep(stepId));
750
783
  if (!step) throw new WorkflowWorldError(`Step not found: ${stepId}`, { status: 404 });
751
784
  return filterData(parseStep(step), params?.resolveData, ["input", "output"]);
752
785
  },
753
786
  async list(params) {
754
787
  const { runId } = params;
755
788
  const limit = params?.pagination?.limit ?? 20;
756
- const result = await getRunDO(runId).listSteps({
789
+ const result = unwrapRead(await getRunDO(runId).listSteps({
757
790
  limit,
758
791
  cursor: params?.pagination?.cursor || void 0,
759
792
  sortOrder: params?.pagination?.sortOrder ?? "asc"
760
- });
793
+ }));
761
794
  return {
762
795
  data: result.data.map((s) => filterData(parseStep(s), params?.resolveData, ["input", "output"])),
763
796
  cursor: result.cursor,
@@ -780,11 +813,11 @@ function createStorage(config) {
780
813
  if (!params.runId) throw new WorkflowWorldError("runId is required for listing hooks", { status: 400 });
781
814
  const runId = params.runId;
782
815
  const limit = params?.pagination?.limit ?? 100;
783
- const result = await getRunDO(runId).listHooks({
816
+ const result = unwrapRead(await getRunDO(runId).listHooks({
784
817
  limit,
785
818
  cursor: params?.pagination?.cursor || void 0,
786
819
  sortOrder: params?.pagination?.sortOrder ?? "asc"
787
- });
820
+ }));
788
821
  return {
789
822
  data: result.data.map((h) => filterHookData(parseHook(h), params?.resolveData ?? "all")),
790
823
  cursor: result.cursor,
@@ -822,17 +855,19 @@ function createStreamer(config) {
822
855
  async function registerStreamForRun(name, runId) {
823
856
  const cacheKey = `${runId}\0${name}`;
824
857
  if (registeredStreams.has(cacheKey)) return;
825
- await getRunRegistryDO(runId).registerStream(name);
858
+ await getRunRegistryDO(runId).registerStream(runId, name);
826
859
  registeredStreams.add(cacheKey);
827
860
  }
828
861
  return {
829
862
  async writeToStream(name, runId, chunk) {
830
- await registerStreamForRun(name, await runId);
831
- await getStreamDO(name).writeChunk(toBytes(chunk));
863
+ const resolvedRunId = await runId;
864
+ await registerStreamForRun(name, resolvedRunId);
865
+ await getStreamDO(name).writeChunk(resolvedRunId, toBytes(chunk));
832
866
  },
833
867
  async closeStream(name, runId) {
834
- await registerStreamForRun(name, await runId);
835
- await getStreamDO(name).closeStream();
868
+ const resolvedRunId = await runId;
869
+ await registerStreamForRun(name, resolvedRunId);
870
+ await getStreamDO(name).closeStream(resolvedRunId);
836
871
  },
837
872
  async readFromStream(name, startIndex = 0) {
838
873
  const stub = getStreamDO(name);
@@ -924,7 +959,9 @@ function createCelldWorld(config) {
924
959
  WORKFLOW_DB: env.WORKFLOW_DB,
925
960
  WORKFLOW_INDEX: env.WORKFLOW_INDEX
926
961
  },
927
- deploymentId: resolved.deploymentId
962
+ deploymentId: resolved.deploymentId,
963
+ runRetentionMs: resolved.runRetentionMs,
964
+ queueShards: resolved.queueShards
928
965
  });
929
966
  const queue = createQueue({
930
967
  env: { WORKFLOW_QUEUE: env.WORKFLOW_QUEUE },
@@ -936,10 +973,27 @@ function createCelldWorld(config) {
936
973
  env: { WORKFLOW_STREAMS: env.WORKFLOW_STREAMS },
937
974
  readPollMs: resolved.readPollMs
938
975
  });
976
+ const runStub = (runId) => env.WORKFLOW_DB.get(env.WORKFLOW_DB.idFromName(runId));
977
+ const retention = {
978
+ getStatus: (runId) => runStub(runId).getCleanupStatus(),
979
+ schedule: (runId) => {
980
+ if (resolved.runRetentionMs === 0) throw new Error("world-celld: runRetentionMs must be greater than zero to schedule cleanup");
981
+ return runStub(runId).scheduleCleanup({
982
+ retentionMs: resolved.runRetentionMs,
983
+ queueShards: resolved.queueShards
984
+ });
985
+ },
986
+ cleanupNow: (runId) => runStub(runId).cleanupNow({
987
+ retentionMs: Math.max(1, resolved.runRetentionMs),
988
+ queueShards: resolved.queueShards
989
+ }),
990
+ rearm: (runId) => runStub(runId).rearmCleanup()
991
+ };
939
992
  return {
940
993
  ...storage,
941
994
  ...queue,
942
995
  ...streamer,
996
+ retention,
943
997
  specVersion: SPEC_VERSION_CURRENT
944
998
  };
945
999
  }