@ewhauser/world-celld 0.2.0 → 0.3.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/testing.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { a as cleanupTombstone, c as globalRunIndexKey, d as workflowRunIndexKey, f as compact, h as parse, i as TOMBSTONE_KEY, l as hookMarkerKey, m as rpcStringify, n as HOOK_MARKER_PREFIX, o as correlationIndexKey, p as rpcParse, r as INDEX_MARKER_PREFIX, s as expiredRead, t as CLEANUP_RECORD_KEY, u as indexMarkerKey } from "./retention-bLY1vcOu.js";
2
- import { EventSchema, HookSchema, SPEC_VERSION_CURRENT, StepSchema, WorkflowRunSchema, isLegacySpecVersion, isTerminalStepStatus, isTerminalWorkflowRunStatus, requiresNewerWorld } from "@workflow/world";
2
+ import { 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
5
5
  function applyListOptions(keys, options) {
@@ -1224,6 +1224,7 @@ const RUN_KEY = "run";
1224
1224
  const EVENT_KEY_PREFIX = "event:";
1225
1225
  const STEP_KEY_PREFIX = "step:";
1226
1226
  const HOOK_KEY_PREFIX = "hook:";
1227
+ const WAIT_KEY_PREFIX = "wait:";
1227
1228
  const STEP_CREATED_KEY_PREFIX = "stepcreated:";
1228
1229
  const HOOK_CREATED_KEY_PREFIX = "hookcreated:";
1229
1230
  /**
@@ -1233,6 +1234,10 @@ const HOOK_CREATED_KEY_PREFIX = "hookcreated:";
1233
1234
  * rejected as a duplicate instead of resurrecting the hook.
1234
1235
  */
1235
1236
  const HOOK_EVENT_MARKER_PREFIX = "hookevent:";
1237
+ /** Workflow-authored attr_set correlation claims. */
1238
+ const ATTRIBUTE_EVENT_MARKER_PREFIX = "attrevent:";
1239
+ /** Durable lazy-hook resume claims, scoped by the containing run DO. */
1240
+ const HOOK_RESUME_CLAIM_PREFIX = "hookresume:";
1236
1241
  function failure(code, message, extra) {
1237
1242
  return {
1238
1243
  ok: false,
@@ -1335,9 +1340,15 @@ async function releaseAllHooks(store) {
1335
1340
  }
1336
1341
  return released;
1337
1342
  }
1343
+ /** Delete all persisted waits once their owning run becomes terminal. */
1344
+ async function releaseAllWaits(store) {
1345
+ const waits = await store.list({ prefix: WAIT_KEY_PREFIX });
1346
+ for (const key of waits.keys()) await store.delete(key);
1347
+ }
1338
1348
  async function applyEvent(store, ctx) {
1339
1349
  const { runId, data, now } = ctx;
1340
1350
  const specVersion = data.specVersion ?? SPEC_VERSION_CURRENT;
1351
+ if (specVersion !== SPEC_VERSION_CURRENT) return failure("RUN_NOT_SUPPORTED", `Run "${runId}" uses unsupported specVersion ${specVersion}; this world requires ${SPEC_VERSION_CURRENT}`, { runSpecVersion: specVersion });
1341
1352
  /** Build and validate the stored event BEFORE anything is persisted. */
1342
1353
  const buildEvent = (record) => {
1343
1354
  const withMeta = {
@@ -1345,9 +1356,16 @@ async function applyEvent(store, ctx) {
1345
1356
  runId,
1346
1357
  eventId: ctx.nextEventId(),
1347
1358
  createdAt: now,
1359
+ occurredAt: ctx.params?.occurredAt,
1360
+ resumeId: ctx.params?.resumeId,
1348
1361
  specVersion
1349
1362
  };
1363
+ if (record.eventType !== "hook_received") delete withMeta.resumeId;
1350
1364
  if (record.eventType === "run_started") delete withMeta.eventData;
1365
+ if (record.eventType === "step_started" && withMeta.eventData !== null && typeof withMeta.eventData === "object") {
1366
+ const { input: _input, ...eventData } = withMeta.eventData;
1367
+ withMeta.eventData = eventData;
1368
+ }
1351
1369
  return EventSchema.parse(compact(withMeta));
1352
1370
  };
1353
1371
  const putEvent = async (event) => {
@@ -1358,14 +1376,21 @@ async function applyEvent(store, ctx) {
1358
1376
  if (!skipRunValidation) currentRun = await store.get(RUN_KEY);
1359
1377
  let bootstrapped;
1360
1378
  if (data.eventType === "run_started" && !currentRun && data.eventData) {
1361
- const { deploymentId, workflowName, input, executionContext } = data.eventData;
1379
+ const { deploymentId, workflowName, input, executionContext, attributes, allowReservedAttributes, encryptionPublicKey } = data.eventData;
1362
1380
  if (deploymentId && workflowName && input !== void 0) {
1381
+ const attributeChanges = Object.entries(attributes ?? {}).map(([key, value]) => ({
1382
+ key,
1383
+ value
1384
+ }));
1385
+ validateAttributeChanges(attributeChanges, { allowReservedAttributes });
1363
1386
  const createdRun = WorkflowRunSchema.parse(compact({
1364
1387
  runId,
1365
1388
  deploymentId,
1366
1389
  workflowName,
1367
1390
  specVersion,
1368
1391
  executionContext,
1392
+ attributes,
1393
+ encryptionPublicKey,
1369
1394
  input,
1370
1395
  status: "pending",
1371
1396
  output: void 0,
@@ -1381,7 +1406,10 @@ async function applyEvent(store, ctx) {
1381
1406
  deploymentId,
1382
1407
  workflowName,
1383
1408
  input,
1384
- executionContext
1409
+ executionContext,
1410
+ attributes,
1411
+ allowReservedAttributes,
1412
+ encryptionPublicKey
1385
1413
  }
1386
1414
  });
1387
1415
  await store.put(RUN_KEY, createdRun);
@@ -1393,10 +1421,7 @@ async function applyEvent(store, ctx) {
1393
1421
  };
1394
1422
  }
1395
1423
  }
1396
- if (currentRun) {
1397
- if (requiresNewerWorld(currentRun.specVersion)) return failure("RUN_NOT_SUPPORTED", `Run "${runId}" requires a newer world (run specVersion ${currentRun.specVersion}, world ${SPEC_VERSION_CURRENT})`, { runSpecVersion: currentRun.specVersion });
1398
- if (isLegacySpecVersion(currentRun.specVersion)) return failure("LEGACY_RUN_NOT_SUPPORTED", `Run "${runId}" was created with a legacy spec version (${currentRun.specVersion ?? "undefined"}) not supported by this world`);
1399
- }
1424
+ if (currentRun && currentRun.specVersion !== SPEC_VERSION_CURRENT) return failure("RUN_NOT_SUPPORTED", `Run "${runId}" uses unsupported specVersion ${currentRun.specVersion}; this world requires ${SPEC_VERSION_CURRENT}`, { runSpecVersion: currentRun.specVersion });
1400
1425
  if (currentRun && isTerminalWorkflowRunStatus(currentRun.status)) {
1401
1426
  if (data.eventType === "run_cancelled" && currentRun.status === "cancelled") {
1402
1427
  const event = buildEvent({ ...data });
@@ -1410,14 +1435,32 @@ async function applyEvent(store, ctx) {
1410
1435
  }
1411
1436
  if (data.eventType === "run_started") return failure("RUN_EXPIRED", `Workflow run "${runId}" is already in terminal state "${currentRun.status}"`);
1412
1437
  if (data.eventType === "run_completed" || data.eventType === "run_failed" || data.eventType === "run_cancelled") return failure("ENTITY_CONFLICT", `Cannot transition run from terminal state "${currentRun.status}"`);
1413
- if (data.eventType === "step_created" || data.eventType === "hook_created" || data.eventType === "wait_created") return failure("ENTITY_CONFLICT", `Cannot create new entities on run in terminal state "${currentRun.status}"`);
1438
+ if (isChildEntityCreationEvent(data)) return failure("ENTITY_CONFLICT", `Cannot create new entities on run in terminal state "${currentRun.status}"`);
1439
+ if (data.eventType === "attr_set") return failure("ENTITY_CONFLICT", `Cannot set attributes on run in terminal state "${currentRun.status}"`);
1414
1440
  }
1415
1441
  let validatedStep;
1416
1442
  if (data.eventType === "step_started" || data.eventType === "step_completed" || data.eventType === "step_failed" || data.eventType === "step_retrying") {
1443
+ const lazyStepStart = data.eventType === "step_started" && isChildEntityCreationEvent(data);
1417
1444
  validatedStep = await store.get(`${STEP_KEY_PREFIX}${data.correlationId}`);
1418
- if (!validatedStep) return failure("STEP_NOT_FOUND", `Step "${data.correlationId}" not found`);
1419
- if (isTerminalStepStatus(validatedStep.status)) return failure("ENTITY_CONFLICT", `Cannot modify step in terminal state "${validatedStep.status}"`);
1420
- if (currentRun && isTerminalWorkflowRunStatus(currentRun.status) && validatedStep.status !== "running") return failure("RUN_EXPIRED", `Cannot modify non-running step on run in terminal state "${currentRun.status}"`);
1445
+ if (!validatedStep) {
1446
+ if (!lazyStepStart) return failure("STEP_NOT_FOUND", `Step "${data.correlationId}" not found`);
1447
+ } else if (lazyStepStart) return failure("ENTITY_CONFLICT", `Step "${data.correlationId}" already exists`);
1448
+ if (validatedStep && isTerminalStepStatus(validatedStep.status)) return failure("ENTITY_CONFLICT", `Cannot modify step in terminal state "${validatedStep.status}"`);
1449
+ if (currentRun && isTerminalWorkflowRunStatus(currentRun.status) && validatedStep && validatedStep.status !== "running") return failure("RUN_EXPIRED", `Cannot modify non-running step on run in terminal state "${currentRun.status}"`);
1450
+ }
1451
+ if (data.eventType === "hook_received" && ctx.params?.resumeId) {
1452
+ const claim = await store.get(`${HOOK_RESUME_CLAIM_PREFIX}${ctx.params.resumeId}`);
1453
+ if (claim) {
1454
+ if (claim.hookId !== data.correlationId) return failure("ENTITY_CONFLICT", `hook_received resumeId "${ctx.params.resumeId}" already recorded for a different hook`);
1455
+ if (claim.payloadDigest && ctx.params.resumePayloadDigest && claim.payloadDigest !== ctx.params.resumePayloadDigest) return failure("ENTITY_CONFLICT", `hook_received resumeId "${ctx.params.resumeId}" already recorded with a different payload`);
1456
+ const committed = await store.get(`${EVENT_KEY_PREFIX}${claim.eventId}`);
1457
+ if (committed?.eventType === "hook_received" && committed.correlationId === claim.hookId && committed.resumeId === ctx.params.resumeId) return {
1458
+ ok: true,
1459
+ event: committed,
1460
+ releasedHooks: []
1461
+ };
1462
+ if (committed) return failure("ENTITY_CONFLICT", `hook_received resumeId "${ctx.params.resumeId}" points to a different event`);
1463
+ }
1421
1464
  }
1422
1465
  if ((data.eventType === "hook_disposed" || data.eventType === "hook_received") && data.correlationId) {
1423
1466
  if (!await store.get(`hook:${data.correlationId}`)) return failure("HOOK_NOT_FOUND", `Hook "${data.correlationId}" not found`);
@@ -1431,12 +1474,19 @@ async function applyEvent(store, ctx) {
1431
1474
  run: existing,
1432
1475
  releasedHooks: []
1433
1476
  };
1477
+ const attributeChanges = Object.entries(data.eventData.attributes ?? {}).map(([key, value]) => ({
1478
+ key,
1479
+ value
1480
+ }));
1481
+ validateAttributeChanges(attributeChanges, { allowReservedAttributes: data.eventData.allowReservedAttributes });
1434
1482
  const run = WorkflowRunSchema.parse(compact({
1435
1483
  runId,
1436
1484
  deploymentId: data.eventData.deploymentId,
1437
1485
  workflowName: data.eventData.workflowName,
1438
1486
  specVersion,
1439
1487
  executionContext: data.eventData.executionContext,
1488
+ attributes: data.eventData.attributes,
1489
+ encryptionPublicKey: data.eventData.encryptionPublicKey,
1440
1490
  input: data.eventData.input,
1441
1491
  status: "pending",
1442
1492
  output: void 0,
@@ -1462,11 +1512,25 @@ async function applyEvent(store, ctx) {
1462
1512
  }
1463
1513
  case "run_started": {
1464
1514
  if (!currentRun) return failure("RUN_NOT_FOUND", `Workflow run "${runId}" not found`);
1465
- if (currentRun.status === "running") return {
1466
- ok: true,
1467
- run: currentRun,
1468
- releasedHooks: []
1469
- };
1515
+ if (currentRun.status === "running") {
1516
+ const result = {
1517
+ ok: true,
1518
+ run: currentRun,
1519
+ releasedHooks: [],
1520
+ maxEvents: getMaxEventsPerRun()
1521
+ };
1522
+ if (ctx.params?.skipPreload) return result;
1523
+ const allEvents = await listByPrefix(store, EVENT_KEY_PREFIX, {
1524
+ limit: getMaxEventsPerRun(),
1525
+ sortOrder: "asc"
1526
+ }, (event) => event.eventId);
1527
+ return {
1528
+ ...result,
1529
+ events: allEvents.data,
1530
+ cursor: allEvents.cursor,
1531
+ hasMore: allEvents.hasMore
1532
+ };
1533
+ }
1470
1534
  const run = WorkflowRunSchema.parse(compact({
1471
1535
  ...currentRun,
1472
1536
  status: "running",
@@ -1479,17 +1543,24 @@ async function applyEvent(store, ctx) {
1479
1543
  const event = buildEvent({ ...data });
1480
1544
  await store.put(RUN_KEY, run);
1481
1545
  await putEvent(event);
1482
- const allEvents = await listByPrefix(store, EVENT_KEY_PREFIX, {
1483
- limit: 1e3,
1484
- sortOrder: "asc"
1485
- }, (e) => e.eventId);
1486
- return {
1546
+ const result = {
1487
1547
  ok: true,
1488
1548
  event,
1489
1549
  run,
1490
1550
  releasedHooks: [],
1491
1551
  runCreated: bootstrapped,
1492
- events: allEvents.data
1552
+ maxEvents: getMaxEventsPerRun()
1553
+ };
1554
+ if (ctx.params?.skipPreload) return result;
1555
+ const allEvents = await listByPrefix(store, EVENT_KEY_PREFIX, {
1556
+ limit: getMaxEventsPerRun(),
1557
+ sortOrder: "asc"
1558
+ }, (e) => e.eventId);
1559
+ return {
1560
+ ...result,
1561
+ events: allEvents.data,
1562
+ cursor: allEvents.cursor,
1563
+ hasMore: allEvents.hasMore
1493
1564
  };
1494
1565
  }
1495
1566
  case "run_completed": {
@@ -1505,11 +1576,13 @@ async function applyEvent(store, ctx) {
1505
1576
  const event = buildEvent({ ...data });
1506
1577
  await store.put(RUN_KEY, run);
1507
1578
  await putEvent(event);
1579
+ const releasedHooks = await releaseAllHooks(store);
1580
+ await releaseAllWaits(store);
1508
1581
  return {
1509
1582
  ok: true,
1510
1583
  event,
1511
1584
  run,
1512
- releasedHooks: await releaseAllHooks(store)
1585
+ releasedHooks
1513
1586
  };
1514
1587
  }
1515
1588
  case "run_failed": {
@@ -1525,11 +1598,13 @@ async function applyEvent(store, ctx) {
1525
1598
  const event = buildEvent({ ...data });
1526
1599
  await store.put(RUN_KEY, run);
1527
1600
  await putEvent(event);
1601
+ const releasedHooks = await releaseAllHooks(store);
1602
+ await releaseAllWaits(store);
1528
1603
  return {
1529
1604
  ok: true,
1530
1605
  event,
1531
1606
  run,
1532
- releasedHooks: await releaseAllHooks(store)
1607
+ releasedHooks
1533
1608
  };
1534
1609
  }
1535
1610
  case "run_cancelled": {
@@ -1545,11 +1620,37 @@ async function applyEvent(store, ctx) {
1545
1620
  const event = buildEvent({ ...data });
1546
1621
  await store.put(RUN_KEY, run);
1547
1622
  await putEvent(event);
1623
+ const releasedHooks = await releaseAllHooks(store);
1624
+ await releaseAllWaits(store);
1625
+ return {
1626
+ ok: true,
1627
+ event,
1628
+ run,
1629
+ releasedHooks
1630
+ };
1631
+ }
1632
+ case "attr_set": {
1633
+ if (!currentRun) return failure("RUN_NOT_FOUND", `Workflow run "${runId}" not found`);
1634
+ validateAttributeChanges(data.eventData.changes, {
1635
+ existingKeys: Object.keys(currentRun.attributes),
1636
+ allowReservedAttributes: data.eventData.allowReservedAttributes
1637
+ });
1638
+ const markerKey = data.correlationId && data.eventData.writer.type === "workflow" ? `${ATTRIBUTE_EVENT_MARKER_PREFIX}${data.correlationId}` : void 0;
1639
+ if (markerKey && await store.get(markerKey) !== void 0) return failure("ENTITY_CONFLICT", `Attribute event "${data.correlationId}" already exists`);
1640
+ const run = WorkflowRunSchema.parse(compact({
1641
+ ...currentRun,
1642
+ attributes: applyAttributeChanges(currentRun.attributes, data.eventData.changes),
1643
+ updatedAt: now
1644
+ }));
1645
+ const event = buildEvent({ ...data });
1646
+ await store.put(RUN_KEY, run);
1647
+ if (markerKey) await store.put(markerKey, event.eventId);
1648
+ await putEvent(event);
1548
1649
  return {
1549
1650
  ok: true,
1550
1651
  event,
1551
1652
  run,
1552
- releasedHooks: await releaseAllHooks(store)
1653
+ releasedHooks: []
1553
1654
  };
1554
1655
  }
1555
1656
  case "step_created": {
@@ -1583,7 +1684,34 @@ async function applyEvent(store, ctx) {
1583
1684
  };
1584
1685
  }
1585
1686
  case "step_started": {
1586
- const step = validatedStep;
1687
+ let step = validatedStep;
1688
+ let stepCreated = false;
1689
+ if (!step && data.eventData?.input !== void 0 && data.eventData.stepName) {
1690
+ step = StepSchema.parse(compact({
1691
+ runId,
1692
+ stepId: data.correlationId,
1693
+ stepName: data.eventData.stepName,
1694
+ status: "pending",
1695
+ input: data.eventData.input,
1696
+ attempt: 0,
1697
+ createdAt: now,
1698
+ updatedAt: now,
1699
+ specVersion
1700
+ }));
1701
+ const createdEvent = buildEvent({
1702
+ eventType: "step_created",
1703
+ correlationId: data.correlationId,
1704
+ eventData: {
1705
+ stepName: data.eventData.stepName,
1706
+ workflowName: data.eventData.workflowName,
1707
+ input: data.eventData.input
1708
+ }
1709
+ });
1710
+ await store.put(`${STEP_KEY_PREFIX}${data.correlationId}`, step);
1711
+ await store.put(creationIndexKey(STEP_CREATED_KEY_PREFIX, step.createdAt, step.stepId), step.stepId);
1712
+ await putEvent(createdEvent);
1713
+ stepCreated = true;
1714
+ }
1587
1715
  if (!step) return failure("STEP_NOT_FOUND", `Step "${data.correlationId}" not found`);
1588
1716
  if (step.retryAfter && step.retryAfter.getTime() > now.getTime()) return failure("TOO_EARLY", `Cannot start step "${data.correlationId}": retryAfter timestamp has not been reached yet`, { retryAfterSeconds: Math.ceil((step.retryAfter.getTime() - now.getTime()) / 1e3) });
1589
1717
  const updated = StepSchema.parse(compact({
@@ -1601,7 +1729,8 @@ async function applyEvent(store, ctx) {
1601
1729
  ok: true,
1602
1730
  event,
1603
1731
  step: updated,
1604
- releasedHooks: []
1732
+ releasedHooks: [],
1733
+ ...stepCreated ? { stepCreated: true } : {}
1605
1734
  };
1606
1735
  }
1607
1736
  case "step_completed": {
@@ -1630,7 +1759,7 @@ async function applyEvent(store, ctx) {
1630
1759
  const updated = StepSchema.parse(compact({
1631
1760
  ...step,
1632
1761
  status: "failed",
1633
- error: toStructuredError(data.eventData.error, { stack: data.eventData.stack }),
1762
+ error: toStructuredError(data.eventData.error),
1634
1763
  completedAt: now,
1635
1764
  updatedAt: now
1636
1765
  }));
@@ -1650,7 +1779,7 @@ async function applyEvent(store, ctx) {
1650
1779
  const updated = StepSchema.parse(compact({
1651
1780
  ...step,
1652
1781
  status: "pending",
1653
- error: toStructuredError(data.eventData.error, { stack: data.eventData.stack }),
1782
+ error: toStructuredError(data.eventData.error),
1654
1783
  retryAfter: data.eventData.retryAfter ? new Date(data.eventData.retryAfter) : void 0,
1655
1784
  updatedAt: now
1656
1785
  }));
@@ -1706,7 +1835,9 @@ async function applyEvent(store, ctx) {
1706
1835
  environment: "",
1707
1836
  createdAt: now,
1708
1837
  specVersion,
1709
- isWebhook: data.eventData.isWebhook ?? false
1838
+ isWebhook: data.eventData.isWebhook ?? false,
1839
+ isSystem: data.eventData.isSystem,
1840
+ tokenRetentionUntil: data.eventData.tokenRetentionUntil
1710
1841
  }));
1711
1842
  const event = buildEvent({ ...data });
1712
1843
  await store.put(hookKey, hook);
@@ -1738,9 +1869,58 @@ async function applyEvent(store, ctx) {
1738
1869
  }]
1739
1870
  };
1740
1871
  }
1872
+ case "wait_created": {
1873
+ const waitKey = `${WAIT_KEY_PREFIX}${data.correlationId}`;
1874
+ if (await store.get(waitKey) !== void 0) return failure("ENTITY_CONFLICT", `Wait "${data.correlationId}" already exists`);
1875
+ const wait = WaitSchema.parse({
1876
+ waitId: `${runId}-${data.correlationId}`,
1877
+ runId,
1878
+ status: "waiting",
1879
+ resumeAt: data.eventData.resumeAt,
1880
+ createdAt: now,
1881
+ updatedAt: now,
1882
+ specVersion
1883
+ });
1884
+ const event = buildEvent({ ...data });
1885
+ await store.put(waitKey, wait);
1886
+ await putEvent(event);
1887
+ return {
1888
+ ok: true,
1889
+ event,
1890
+ wait,
1891
+ releasedHooks: []
1892
+ };
1893
+ }
1894
+ case "wait_completed": {
1895
+ const waitKey = `${WAIT_KEY_PREFIX}${data.correlationId}`;
1896
+ const existing = await store.get(waitKey);
1897
+ if (!existing) return failure("WAIT_NOT_FOUND", `Wait "${data.correlationId}" not found`);
1898
+ if (existing.status === "completed") return failure("ENTITY_CONFLICT", `Wait "${data.correlationId}" already completed`);
1899
+ const event = buildEvent({ ...data });
1900
+ const wait = WaitSchema.parse({
1901
+ ...existing,
1902
+ status: "completed",
1903
+ resumeAt: data.eventData?.resumeAt ?? existing.resumeAt,
1904
+ completedAt: now,
1905
+ updatedAt: now
1906
+ });
1907
+ await store.put(waitKey, wait);
1908
+ await putEvent(event);
1909
+ return {
1910
+ ok: true,
1911
+ event,
1912
+ wait,
1913
+ releasedHooks: []
1914
+ };
1915
+ }
1741
1916
  default: {
1742
1917
  const event = buildEvent({ ...data });
1743
1918
  await putEvent(event);
1919
+ if (data.eventType === "hook_received" && ctx.params?.resumeId) await store.put(`${HOOK_RESUME_CLAIM_PREFIX}${ctx.params.resumeId}`, {
1920
+ hookId: data.correlationId,
1921
+ eventId: event.eventId,
1922
+ ...ctx.params.resumePayloadDigest ? { payloadDigest: ctx.params.resumePayloadDigest } : {}
1923
+ });
1744
1924
  return {
1745
1925
  ok: true,
1746
1926
  event,
@@ -1749,71 +1929,54 @@ async function applyEvent(store, ctx) {
1749
1929
  }
1750
1930
  }
1751
1931
  }
1932
+ /**
1933
+ * Complete Workflow 5's event-page response after an atomic apply. A celld
1934
+ * run is a single serialized writer, so the committed sequence is dense; a
1935
+ * stale eventCount therefore needs only the slots between the requested
1936
+ * position and the event this call committed.
1937
+ */
1938
+ async function finalizeEventPage(store, outcome, params) {
1939
+ if (!outcome.ok) return outcome;
1940
+ const result = outcome.run && outcome.maxEvents === void 0 ? {
1941
+ ...outcome,
1942
+ maxEvents: getMaxEventsPerRun()
1943
+ } : outcome;
1944
+ if (!result.event) return result;
1945
+ if (typeof params?.sinceCursor === "string") {
1946
+ const page = await listByPrefix(store, EVENT_KEY_PREFIX, {
1947
+ limit: 100,
1948
+ cursor: params.sinceCursor,
1949
+ sortOrder: "asc"
1950
+ }, (event) => event.eventId);
1951
+ return {
1952
+ ...result,
1953
+ events: page.data,
1954
+ cursor: page.cursor,
1955
+ hasMore: page.hasMore
1956
+ };
1957
+ }
1958
+ if (params?.eventCount === void 0) return result;
1959
+ const committedSlot = eventIdToSlot(result.event.eventId);
1960
+ if (committedSlot === null || committedSlot <= params.eventCount + 1) return result;
1961
+ const skippedCount = committedSlot - params.eventCount - 1;
1962
+ const events = (await listByPrefix(store, EVENT_KEY_PREFIX, {
1963
+ limit: skippedCount,
1964
+ cursor: params.eventCount > 0 ? slotToEventId(params.eventCount) : void 0,
1965
+ sortOrder: "asc"
1966
+ }, (event) => event.eventId)).data.filter((event) => event.eventId < result.event.eventId);
1967
+ return {
1968
+ ...result,
1969
+ events,
1970
+ cursor: null,
1971
+ hasMore: events.length < skippedCount
1972
+ };
1973
+ }
1752
1974
  //#endregion
1753
1975
  //#region src/worker/durable-objects/WorkflowRunDO.ts
1754
1976
  const PAYLOAD_DELETE_BATCH = 256;
1755
1977
  const STREAM_DELETE_CONCURRENCY = 16;
1756
1978
  const CLEANUP_RETRY_MAX_MS = 36e5;
1757
1979
  /**
1758
- * Current schema version for DO storage.
1759
- * Increment when making breaking changes to the storage layout.
1760
- * Migrations run lazily on first request after deploy.
1761
- *
1762
- * v2: one storage key per event ('event:<eventId>'), step ('step:<stepId>')
1763
- * and hook ('hook:<hookId>') instead of single aggregate values. This package
1764
- * is pre-production, so the migration simply drops the legacy aggregates.
1765
- */
1766
- const CURRENT_SCHEMA_VERSION = 3;
1767
- /**
1768
- * Idempotent migration functions keyed by target version. The schema version
1769
- * is written only after the migration completes, so eviction safely retries
1770
- * an interrupted migration.
1771
- */
1772
- const MIGRATIONS = {
1773
- 2: async (storage) => {
1774
- await storage.transaction(async (txn) => {
1775
- await txn.delete("events");
1776
- await txn.delete("steps");
1777
- await txn.delete("hooks");
1778
- });
1779
- },
1780
- 3: async (storage) => {
1781
- await backfillCreationIndex(storage, STEP_KEY_PREFIX, STEP_CREATED_KEY_PREFIX, (step) => step.stepId);
1782
- await backfillCreationIndex(storage, HOOK_KEY_PREFIX, HOOK_CREATED_KEY_PREFIX, (hook) => hook.hookId);
1783
- }
1784
- };
1785
- async function backfillCreationIndex(storage, entityPrefix, indexPrefix, getId) {
1786
- let startAfter;
1787
- while (true) {
1788
- const entities = await storage.list({
1789
- prefix: entityPrefix,
1790
- startAfter,
1791
- limit: 100
1792
- });
1793
- if (entities.size === 0) return;
1794
- for (const [key, entity] of entities) {
1795
- const id = getId(entity);
1796
- await storage.put(`${indexPrefix}${entity.createdAt.toISOString()}:${id}`, id);
1797
- startAfter = key;
1798
- }
1799
- if (entities.size < 100) return;
1800
- }
1801
- }
1802
- /**
1803
- * Ensure the DO storage schema is up to date.
1804
- * Runs any pending migrations sequentially inside transactions,
1805
- * persisting the version after each successful migration.
1806
- * Safe to call on every request -- it's a no-op when already current.
1807
- */
1808
- async function ensureSchemaUpToDate(storage) {
1809
- const stored = await storage.get("schema_version") ?? 1;
1810
- for (let v = stored + 1; v <= CURRENT_SCHEMA_VERSION; v++) {
1811
- const migration = MIGRATIONS[v];
1812
- if (migration) await migration(storage);
1813
- await storage.put("schema_version", v);
1814
- }
1815
- }
1816
- /**
1817
1980
  * Adapt DO storage (or a transaction) to the {@link EventStore} interface
1818
1981
  * shared with the test mocks. No casts: the methods line up structurally,
1819
1982
  * this just pins the subset we rely on.
@@ -1837,15 +2000,10 @@ function storeFrom(storage) {
1837
2000
  * queue-message dedup claims (see claimInflight/releaseInflight).
1838
2001
  */
1839
2002
  var WorkflowRunDO = class extends DurableObject {
1840
- schemaReady = null;
1841
2003
  now() {
1842
2004
  const clock = this.env?.clock;
1843
2005
  return typeof clock === "function" ? clock() : Date.now();
1844
2006
  }
1845
- ensureSchema() {
1846
- if (!this.schemaReady) this.schemaReady = ensureSchemaUpToDate(this.ctx.storage);
1847
- return this.schemaReady;
1848
- }
1849
2007
  async expiredTombstone(storage, now = this.now()) {
1850
2008
  const tombstone = await storage.get(TOMBSTONE_KEY);
1851
2009
  if (tombstone) return tombstone;
@@ -1854,7 +2012,6 @@ var WorkflowRunDO = class extends DurableObject {
1854
2012
  return cleanupTombstone(cleanup, cleanup.tombstonedAt ?? cleanup.dueAt);
1855
2013
  }
1856
2014
  async read(reader) {
1857
- await this.ensureSchema();
1858
2015
  const tombstone = await this.expiredTombstone(this.ctx.storage);
1859
2016
  if (tombstone) return expiredRead(tombstone);
1860
2017
  return {
@@ -1864,7 +2021,6 @@ var WorkflowRunDO = class extends DurableObject {
1864
2021
  }
1865
2022
  /** Apply an event and capture all retention metadata in the same transaction. */
1866
2023
  async applyEvent(request) {
1867
- await this.ensureSchema();
1868
2024
  return await this.ctx.storage.transaction(async (txn) => {
1869
2025
  const now = new Date(this.now());
1870
2026
  const tombstone = await this.expiredTombstone(txn, now.getTime());
@@ -1876,7 +2032,7 @@ var WorkflowRunDO = class extends DurableObject {
1876
2032
  let eventSequence = await txn.get("event_sequence") ?? 0;
1877
2033
  const outcome = await applyEvent(storeFrom(txn), {
1878
2034
  ...request,
1879
- nextEventId: () => `wevt_z${String(++eventSequence).padStart(20, "0")}`,
2035
+ nextEventId: () => slotToEventId(++eventSequence),
1880
2036
  now
1881
2037
  });
1882
2038
  if (!outcome.ok) return outcome;
@@ -1921,7 +2077,7 @@ var WorkflowRunDO = class extends DurableObject {
1921
2077
  await txn.setAlarm(dueAt);
1922
2078
  outcome.run = run;
1923
2079
  }
1924
- return outcome;
2080
+ return finalizeEventPage(storeFrom(txn), outcome, request.params);
1925
2081
  });
1926
2082
  }
1927
2083
  async getRun() {
@@ -1955,12 +2111,10 @@ var WorkflowRunDO = class extends DurableObject {
1955
2111
  }));
1956
2112
  }
1957
2113
  async getCleanupStatus() {
1958
- await this.ensureSchema();
1959
2114
  return await this.ctx.storage.get("retention:cleanup") ?? null;
1960
2115
  }
1961
2116
  /** Explicitly schedule an existing terminal run, used for opt-in backfills. */
1962
2117
  async scheduleCleanup(request) {
1963
- await this.ensureSchema();
1964
2118
  if (request.retentionMs <= 0) return this.getCleanupStatus();
1965
2119
  return await this.ctx.storage.transaction(async (txn) => {
1966
2120
  const existing = await txn.get(CLEANUP_RECORD_KEY);
@@ -1996,7 +2150,6 @@ var WorkflowRunDO = class extends DurableObject {
1996
2150
  });
1997
2151
  }
1998
2152
  async cleanupNow(request) {
1999
- await this.ensureSchema();
2000
2153
  if (!await this.ctx.storage.get("retention:cleanup")) await this.scheduleCleanup({
2001
2154
  ...request,
2002
2155
  retentionMs: Math.max(1, request.retentionMs)
@@ -2024,7 +2177,6 @@ var WorkflowRunDO = class extends DurableObject {
2024
2177
  return this.getCleanupStatus();
2025
2178
  }
2026
2179
  async rearmCleanup() {
2027
- await this.ensureSchema();
2028
2180
  const cleanup = await this.ctx.storage.get(CLEANUP_RECORD_KEY);
2029
2181
  if (!cleanup) return null;
2030
2182
  if (cleanup.phase === "tombstoned") await this.ctx.storage.deleteAlarm();
@@ -2032,7 +2184,6 @@ var WorkflowRunDO = class extends DurableObject {
2032
2184
  return cleanup;
2033
2185
  }
2034
2186
  async alarm() {
2035
- await this.ensureSchema();
2036
2187
  try {
2037
2188
  await this.executeCleanup();
2038
2189
  } catch (error) {
@@ -2135,7 +2286,7 @@ var WorkflowRunDO = class extends DurableObject {
2135
2286
  const cleanup = await txn.get(CLEANUP_RECORD_KEY);
2136
2287
  if (!cleanup || cleanup.phase !== "payload") return true;
2137
2288
  const entries = await txn.list({ limit: 259 });
2138
- const keys = Array.from(entries.keys()).filter((key) => key !== "retention:cleanup" && key !== "retention:tombstone" && key !== "schema_version").slice(0, PAYLOAD_DELETE_BATCH);
2289
+ const keys = Array.from(entries.keys()).filter((key) => key !== "retention:cleanup" && key !== "retention:tombstone").slice(0, PAYLOAD_DELETE_BATCH);
2139
2290
  const now = new Date(this.now());
2140
2291
  await txn.put(TOMBSTONE_KEY, cleanupTombstone(cleanup, now));
2141
2292
  for (const key of keys) await txn.delete(key);
@@ -2175,7 +2326,6 @@ var WorkflowRunDO = class extends DurableObject {
2175
2326
  });
2176
2327
  }
2177
2328
  async claimInflight(params) {
2178
- await this.ensureSchema();
2179
2329
  return await this.ctx.storage.transaction(async (txn) => {
2180
2330
  const existing = await txn.get("claim");
2181
2331
  const now = this.now();
@@ -2188,7 +2338,6 @@ var WorkflowRunDO = class extends DurableObject {
2188
2338
  });
2189
2339
  }
2190
2340
  async releaseInflight() {
2191
- await this.ensureSchema();
2192
2341
  await this.ctx.storage.delete("claim");
2193
2342
  }
2194
2343
  };