@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/worker.js CHANGED
@@ -1,4 +1,4 @@
1
- import { EventSchema, HookSchema, SPEC_VERSION_CURRENT, StepSchema, WorkflowRunSchema, isLegacySpecVersion, isTerminalStepStatus, isTerminalWorkflowRunStatus, requiresNewerWorld } from "@workflow/world";
1
+ import { EventSchema, HookSchema, SPEC_VERSION_CURRENT, StepSchema, WaitSchema, WorkflowRunSchema, applyAttributeChanges, eventIdToSlot, getMaxEventsPerRun, isChildEntityCreationEvent, isTerminalStepStatus, isTerminalWorkflowRunStatus, slotToEventId, validateAttributeChanges } from "@workflow/world";
2
2
  import { DurableObject } from "cloudflare:workers";
3
3
  //#region src/vendor/shared/serialization.ts
4
4
  /** Standard Date fields used across all worlds */
@@ -1066,6 +1066,7 @@ const RUN_KEY = "run";
1066
1066
  const EVENT_KEY_PREFIX = "event:";
1067
1067
  const STEP_KEY_PREFIX = "step:";
1068
1068
  const HOOK_KEY_PREFIX = "hook:";
1069
+ const WAIT_KEY_PREFIX = "wait:";
1069
1070
  const STEP_CREATED_KEY_PREFIX = "stepcreated:";
1070
1071
  const HOOK_CREATED_KEY_PREFIX = "hookcreated:";
1071
1072
  /**
@@ -1075,6 +1076,10 @@ const HOOK_CREATED_KEY_PREFIX = "hookcreated:";
1075
1076
  * rejected as a duplicate instead of resurrecting the hook.
1076
1077
  */
1077
1078
  const HOOK_EVENT_MARKER_PREFIX = "hookevent:";
1079
+ /** Workflow-authored attr_set correlation claims. */
1080
+ const ATTRIBUTE_EVENT_MARKER_PREFIX = "attrevent:";
1081
+ /** Durable lazy-hook resume claims, scoped by the containing run DO. */
1082
+ const HOOK_RESUME_CLAIM_PREFIX = "hookresume:";
1078
1083
  function failure(code, message, extra) {
1079
1084
  return {
1080
1085
  ok: false,
@@ -1177,9 +1182,15 @@ async function releaseAllHooks(store) {
1177
1182
  }
1178
1183
  return released;
1179
1184
  }
1185
+ /** Delete all persisted waits once their owning run becomes terminal. */
1186
+ async function releaseAllWaits(store) {
1187
+ const waits = await store.list({ prefix: WAIT_KEY_PREFIX });
1188
+ for (const key of waits.keys()) await store.delete(key);
1189
+ }
1180
1190
  async function applyEvent(store, ctx) {
1181
1191
  const { runId, data, now } = ctx;
1182
1192
  const specVersion = data.specVersion ?? SPEC_VERSION_CURRENT;
1193
+ if (specVersion !== SPEC_VERSION_CURRENT) return failure("RUN_NOT_SUPPORTED", `Run "${runId}" uses unsupported specVersion ${specVersion}; this world requires ${SPEC_VERSION_CURRENT}`, { runSpecVersion: specVersion });
1183
1194
  /** Build and validate the stored event BEFORE anything is persisted. */
1184
1195
  const buildEvent = (record) => {
1185
1196
  const withMeta = {
@@ -1187,9 +1198,16 @@ async function applyEvent(store, ctx) {
1187
1198
  runId,
1188
1199
  eventId: ctx.nextEventId(),
1189
1200
  createdAt: now,
1201
+ occurredAt: ctx.params?.occurredAt,
1202
+ resumeId: ctx.params?.resumeId,
1190
1203
  specVersion
1191
1204
  };
1205
+ if (record.eventType !== "hook_received") delete withMeta.resumeId;
1192
1206
  if (record.eventType === "run_started") delete withMeta.eventData;
1207
+ if (record.eventType === "step_started" && withMeta.eventData !== null && typeof withMeta.eventData === "object") {
1208
+ const { input: _input, ...eventData } = withMeta.eventData;
1209
+ withMeta.eventData = eventData;
1210
+ }
1193
1211
  return EventSchema.parse(compact(withMeta));
1194
1212
  };
1195
1213
  const putEvent = async (event) => {
@@ -1200,14 +1218,21 @@ async function applyEvent(store, ctx) {
1200
1218
  if (!skipRunValidation) currentRun = await store.get(RUN_KEY);
1201
1219
  let bootstrapped;
1202
1220
  if (data.eventType === "run_started" && !currentRun && data.eventData) {
1203
- const { deploymentId, workflowName, input, executionContext } = data.eventData;
1221
+ const { deploymentId, workflowName, input, executionContext, attributes, allowReservedAttributes, encryptionPublicKey } = data.eventData;
1204
1222
  if (deploymentId && workflowName && input !== void 0) {
1223
+ const attributeChanges = Object.entries(attributes ?? {}).map(([key, value]) => ({
1224
+ key,
1225
+ value
1226
+ }));
1227
+ validateAttributeChanges(attributeChanges, { allowReservedAttributes });
1205
1228
  const createdRun = WorkflowRunSchema.parse(compact({
1206
1229
  runId,
1207
1230
  deploymentId,
1208
1231
  workflowName,
1209
1232
  specVersion,
1210
1233
  executionContext,
1234
+ attributes,
1235
+ encryptionPublicKey,
1211
1236
  input,
1212
1237
  status: "pending",
1213
1238
  output: void 0,
@@ -1223,7 +1248,10 @@ async function applyEvent(store, ctx) {
1223
1248
  deploymentId,
1224
1249
  workflowName,
1225
1250
  input,
1226
- executionContext
1251
+ executionContext,
1252
+ attributes,
1253
+ allowReservedAttributes,
1254
+ encryptionPublicKey
1227
1255
  }
1228
1256
  });
1229
1257
  await store.put(RUN_KEY, createdRun);
@@ -1235,10 +1263,7 @@ async function applyEvent(store, ctx) {
1235
1263
  };
1236
1264
  }
1237
1265
  }
1238
- if (currentRun) {
1239
- 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 });
1240
- 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`);
1241
- }
1266
+ 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 });
1242
1267
  if (currentRun && isTerminalWorkflowRunStatus(currentRun.status)) {
1243
1268
  if (data.eventType === "run_cancelled" && currentRun.status === "cancelled") {
1244
1269
  const event = buildEvent({ ...data });
@@ -1252,14 +1277,32 @@ async function applyEvent(store, ctx) {
1252
1277
  }
1253
1278
  if (data.eventType === "run_started") return failure("RUN_EXPIRED", `Workflow run "${runId}" is already in terminal state "${currentRun.status}"`);
1254
1279
  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}"`);
1255
- 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}"`);
1280
+ if (isChildEntityCreationEvent(data)) return failure("ENTITY_CONFLICT", `Cannot create new entities on run in terminal state "${currentRun.status}"`);
1281
+ if (data.eventType === "attr_set") return failure("ENTITY_CONFLICT", `Cannot set attributes on run in terminal state "${currentRun.status}"`);
1256
1282
  }
1257
1283
  let validatedStep;
1258
1284
  if (data.eventType === "step_started" || data.eventType === "step_completed" || data.eventType === "step_failed" || data.eventType === "step_retrying") {
1285
+ const lazyStepStart = data.eventType === "step_started" && isChildEntityCreationEvent(data);
1259
1286
  validatedStep = await store.get(`${STEP_KEY_PREFIX}${data.correlationId}`);
1260
- if (!validatedStep) return failure("STEP_NOT_FOUND", `Step "${data.correlationId}" not found`);
1261
- if (isTerminalStepStatus(validatedStep.status)) return failure("ENTITY_CONFLICT", `Cannot modify step in terminal state "${validatedStep.status}"`);
1262
- if (currentRun && isTerminalWorkflowRunStatus(currentRun.status) && validatedStep.status !== "running") return failure("RUN_EXPIRED", `Cannot modify non-running step on run in terminal state "${currentRun.status}"`);
1287
+ if (!validatedStep) {
1288
+ if (!lazyStepStart) return failure("STEP_NOT_FOUND", `Step "${data.correlationId}" not found`);
1289
+ } else if (lazyStepStart) return failure("ENTITY_CONFLICT", `Step "${data.correlationId}" already exists`);
1290
+ if (validatedStep && isTerminalStepStatus(validatedStep.status)) return failure("ENTITY_CONFLICT", `Cannot modify step in terminal state "${validatedStep.status}"`);
1291
+ 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}"`);
1292
+ }
1293
+ if (data.eventType === "hook_received" && ctx.params?.resumeId) {
1294
+ const claim = await store.get(`${HOOK_RESUME_CLAIM_PREFIX}${ctx.params.resumeId}`);
1295
+ if (claim) {
1296
+ if (claim.hookId !== data.correlationId) return failure("ENTITY_CONFLICT", `hook_received resumeId "${ctx.params.resumeId}" already recorded for a different hook`);
1297
+ 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`);
1298
+ const committed = await store.get(`${EVENT_KEY_PREFIX}${claim.eventId}`);
1299
+ if (committed?.eventType === "hook_received" && committed.correlationId === claim.hookId && committed.resumeId === ctx.params.resumeId) return {
1300
+ ok: true,
1301
+ event: committed,
1302
+ releasedHooks: []
1303
+ };
1304
+ if (committed) return failure("ENTITY_CONFLICT", `hook_received resumeId "${ctx.params.resumeId}" points to a different event`);
1305
+ }
1263
1306
  }
1264
1307
  if ((data.eventType === "hook_disposed" || data.eventType === "hook_received") && data.correlationId) {
1265
1308
  if (!await store.get(`hook:${data.correlationId}`)) return failure("HOOK_NOT_FOUND", `Hook "${data.correlationId}" not found`);
@@ -1273,12 +1316,19 @@ async function applyEvent(store, ctx) {
1273
1316
  run: existing,
1274
1317
  releasedHooks: []
1275
1318
  };
1319
+ const attributeChanges = Object.entries(data.eventData.attributes ?? {}).map(([key, value]) => ({
1320
+ key,
1321
+ value
1322
+ }));
1323
+ validateAttributeChanges(attributeChanges, { allowReservedAttributes: data.eventData.allowReservedAttributes });
1276
1324
  const run = WorkflowRunSchema.parse(compact({
1277
1325
  runId,
1278
1326
  deploymentId: data.eventData.deploymentId,
1279
1327
  workflowName: data.eventData.workflowName,
1280
1328
  specVersion,
1281
1329
  executionContext: data.eventData.executionContext,
1330
+ attributes: data.eventData.attributes,
1331
+ encryptionPublicKey: data.eventData.encryptionPublicKey,
1282
1332
  input: data.eventData.input,
1283
1333
  status: "pending",
1284
1334
  output: void 0,
@@ -1304,11 +1354,25 @@ async function applyEvent(store, ctx) {
1304
1354
  }
1305
1355
  case "run_started": {
1306
1356
  if (!currentRun) return failure("RUN_NOT_FOUND", `Workflow run "${runId}" not found`);
1307
- if (currentRun.status === "running") return {
1308
- ok: true,
1309
- run: currentRun,
1310
- releasedHooks: []
1311
- };
1357
+ if (currentRun.status === "running") {
1358
+ const result = {
1359
+ ok: true,
1360
+ run: currentRun,
1361
+ releasedHooks: [],
1362
+ maxEvents: getMaxEventsPerRun()
1363
+ };
1364
+ if (ctx.params?.skipPreload) return result;
1365
+ const allEvents = await listByPrefix(store, EVENT_KEY_PREFIX, {
1366
+ limit: getMaxEventsPerRun(),
1367
+ sortOrder: "asc"
1368
+ }, (event) => event.eventId);
1369
+ return {
1370
+ ...result,
1371
+ events: allEvents.data,
1372
+ cursor: allEvents.cursor,
1373
+ hasMore: allEvents.hasMore
1374
+ };
1375
+ }
1312
1376
  const run = WorkflowRunSchema.parse(compact({
1313
1377
  ...currentRun,
1314
1378
  status: "running",
@@ -1321,17 +1385,24 @@ async function applyEvent(store, ctx) {
1321
1385
  const event = buildEvent({ ...data });
1322
1386
  await store.put(RUN_KEY, run);
1323
1387
  await putEvent(event);
1324
- const allEvents = await listByPrefix(store, EVENT_KEY_PREFIX, {
1325
- limit: 1e3,
1326
- sortOrder: "asc"
1327
- }, (e) => e.eventId);
1328
- return {
1388
+ const result = {
1329
1389
  ok: true,
1330
1390
  event,
1331
1391
  run,
1332
1392
  releasedHooks: [],
1333
1393
  runCreated: bootstrapped,
1334
- events: allEvents.data
1394
+ maxEvents: getMaxEventsPerRun()
1395
+ };
1396
+ if (ctx.params?.skipPreload) return result;
1397
+ const allEvents = await listByPrefix(store, EVENT_KEY_PREFIX, {
1398
+ limit: getMaxEventsPerRun(),
1399
+ sortOrder: "asc"
1400
+ }, (e) => e.eventId);
1401
+ return {
1402
+ ...result,
1403
+ events: allEvents.data,
1404
+ cursor: allEvents.cursor,
1405
+ hasMore: allEvents.hasMore
1335
1406
  };
1336
1407
  }
1337
1408
  case "run_completed": {
@@ -1347,11 +1418,13 @@ async function applyEvent(store, ctx) {
1347
1418
  const event = buildEvent({ ...data });
1348
1419
  await store.put(RUN_KEY, run);
1349
1420
  await putEvent(event);
1421
+ const releasedHooks = await releaseAllHooks(store);
1422
+ await releaseAllWaits(store);
1350
1423
  return {
1351
1424
  ok: true,
1352
1425
  event,
1353
1426
  run,
1354
- releasedHooks: await releaseAllHooks(store)
1427
+ releasedHooks
1355
1428
  };
1356
1429
  }
1357
1430
  case "run_failed": {
@@ -1367,11 +1440,13 @@ async function applyEvent(store, ctx) {
1367
1440
  const event = buildEvent({ ...data });
1368
1441
  await store.put(RUN_KEY, run);
1369
1442
  await putEvent(event);
1443
+ const releasedHooks = await releaseAllHooks(store);
1444
+ await releaseAllWaits(store);
1370
1445
  return {
1371
1446
  ok: true,
1372
1447
  event,
1373
1448
  run,
1374
- releasedHooks: await releaseAllHooks(store)
1449
+ releasedHooks
1375
1450
  };
1376
1451
  }
1377
1452
  case "run_cancelled": {
@@ -1387,11 +1462,37 @@ async function applyEvent(store, ctx) {
1387
1462
  const event = buildEvent({ ...data });
1388
1463
  await store.put(RUN_KEY, run);
1389
1464
  await putEvent(event);
1465
+ const releasedHooks = await releaseAllHooks(store);
1466
+ await releaseAllWaits(store);
1467
+ return {
1468
+ ok: true,
1469
+ event,
1470
+ run,
1471
+ releasedHooks
1472
+ };
1473
+ }
1474
+ case "attr_set": {
1475
+ if (!currentRun) return failure("RUN_NOT_FOUND", `Workflow run "${runId}" not found`);
1476
+ validateAttributeChanges(data.eventData.changes, {
1477
+ existingKeys: Object.keys(currentRun.attributes),
1478
+ allowReservedAttributes: data.eventData.allowReservedAttributes
1479
+ });
1480
+ const markerKey = data.correlationId && data.eventData.writer.type === "workflow" ? `${ATTRIBUTE_EVENT_MARKER_PREFIX}${data.correlationId}` : void 0;
1481
+ if (markerKey && await store.get(markerKey) !== void 0) return failure("ENTITY_CONFLICT", `Attribute event "${data.correlationId}" already exists`);
1482
+ const run = WorkflowRunSchema.parse(compact({
1483
+ ...currentRun,
1484
+ attributes: applyAttributeChanges(currentRun.attributes, data.eventData.changes),
1485
+ updatedAt: now
1486
+ }));
1487
+ const event = buildEvent({ ...data });
1488
+ await store.put(RUN_KEY, run);
1489
+ if (markerKey) await store.put(markerKey, event.eventId);
1490
+ await putEvent(event);
1390
1491
  return {
1391
1492
  ok: true,
1392
1493
  event,
1393
1494
  run,
1394
- releasedHooks: await releaseAllHooks(store)
1495
+ releasedHooks: []
1395
1496
  };
1396
1497
  }
1397
1498
  case "step_created": {
@@ -1425,7 +1526,34 @@ async function applyEvent(store, ctx) {
1425
1526
  };
1426
1527
  }
1427
1528
  case "step_started": {
1428
- const step = validatedStep;
1529
+ let step = validatedStep;
1530
+ let stepCreated = false;
1531
+ if (!step && data.eventData?.input !== void 0 && data.eventData.stepName) {
1532
+ step = StepSchema.parse(compact({
1533
+ runId,
1534
+ stepId: data.correlationId,
1535
+ stepName: data.eventData.stepName,
1536
+ status: "pending",
1537
+ input: data.eventData.input,
1538
+ attempt: 0,
1539
+ createdAt: now,
1540
+ updatedAt: now,
1541
+ specVersion
1542
+ }));
1543
+ const createdEvent = buildEvent({
1544
+ eventType: "step_created",
1545
+ correlationId: data.correlationId,
1546
+ eventData: {
1547
+ stepName: data.eventData.stepName,
1548
+ workflowName: data.eventData.workflowName,
1549
+ input: data.eventData.input
1550
+ }
1551
+ });
1552
+ await store.put(`${STEP_KEY_PREFIX}${data.correlationId}`, step);
1553
+ await store.put(creationIndexKey(STEP_CREATED_KEY_PREFIX, step.createdAt, step.stepId), step.stepId);
1554
+ await putEvent(createdEvent);
1555
+ stepCreated = true;
1556
+ }
1429
1557
  if (!step) return failure("STEP_NOT_FOUND", `Step "${data.correlationId}" not found`);
1430
1558
  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) });
1431
1559
  const updated = StepSchema.parse(compact({
@@ -1443,7 +1571,8 @@ async function applyEvent(store, ctx) {
1443
1571
  ok: true,
1444
1572
  event,
1445
1573
  step: updated,
1446
- releasedHooks: []
1574
+ releasedHooks: [],
1575
+ ...stepCreated ? { stepCreated: true } : {}
1447
1576
  };
1448
1577
  }
1449
1578
  case "step_completed": {
@@ -1472,7 +1601,7 @@ async function applyEvent(store, ctx) {
1472
1601
  const updated = StepSchema.parse(compact({
1473
1602
  ...step,
1474
1603
  status: "failed",
1475
- error: toStructuredError(data.eventData.error, { stack: data.eventData.stack }),
1604
+ error: toStructuredError(data.eventData.error),
1476
1605
  completedAt: now,
1477
1606
  updatedAt: now
1478
1607
  }));
@@ -1492,7 +1621,7 @@ async function applyEvent(store, ctx) {
1492
1621
  const updated = StepSchema.parse(compact({
1493
1622
  ...step,
1494
1623
  status: "pending",
1495
- error: toStructuredError(data.eventData.error, { stack: data.eventData.stack }),
1624
+ error: toStructuredError(data.eventData.error),
1496
1625
  retryAfter: data.eventData.retryAfter ? new Date(data.eventData.retryAfter) : void 0,
1497
1626
  updatedAt: now
1498
1627
  }));
@@ -1548,7 +1677,9 @@ async function applyEvent(store, ctx) {
1548
1677
  environment: "",
1549
1678
  createdAt: now,
1550
1679
  specVersion,
1551
- isWebhook: data.eventData.isWebhook ?? false
1680
+ isWebhook: data.eventData.isWebhook ?? false,
1681
+ isSystem: data.eventData.isSystem,
1682
+ tokenRetentionUntil: data.eventData.tokenRetentionUntil
1552
1683
  }));
1553
1684
  const event = buildEvent({ ...data });
1554
1685
  await store.put(hookKey, hook);
@@ -1580,9 +1711,58 @@ async function applyEvent(store, ctx) {
1580
1711
  }]
1581
1712
  };
1582
1713
  }
1714
+ case "wait_created": {
1715
+ const waitKey = `${WAIT_KEY_PREFIX}${data.correlationId}`;
1716
+ if (await store.get(waitKey) !== void 0) return failure("ENTITY_CONFLICT", `Wait "${data.correlationId}" already exists`);
1717
+ const wait = WaitSchema.parse({
1718
+ waitId: `${runId}-${data.correlationId}`,
1719
+ runId,
1720
+ status: "waiting",
1721
+ resumeAt: data.eventData.resumeAt,
1722
+ createdAt: now,
1723
+ updatedAt: now,
1724
+ specVersion
1725
+ });
1726
+ const event = buildEvent({ ...data });
1727
+ await store.put(waitKey, wait);
1728
+ await putEvent(event);
1729
+ return {
1730
+ ok: true,
1731
+ event,
1732
+ wait,
1733
+ releasedHooks: []
1734
+ };
1735
+ }
1736
+ case "wait_completed": {
1737
+ const waitKey = `${WAIT_KEY_PREFIX}${data.correlationId}`;
1738
+ const existing = await store.get(waitKey);
1739
+ if (!existing) return failure("WAIT_NOT_FOUND", `Wait "${data.correlationId}" not found`);
1740
+ if (existing.status === "completed") return failure("ENTITY_CONFLICT", `Wait "${data.correlationId}" already completed`);
1741
+ const event = buildEvent({ ...data });
1742
+ const wait = WaitSchema.parse({
1743
+ ...existing,
1744
+ status: "completed",
1745
+ resumeAt: data.eventData?.resumeAt ?? existing.resumeAt,
1746
+ completedAt: now,
1747
+ updatedAt: now
1748
+ });
1749
+ await store.put(waitKey, wait);
1750
+ await putEvent(event);
1751
+ return {
1752
+ ok: true,
1753
+ event,
1754
+ wait,
1755
+ releasedHooks: []
1756
+ };
1757
+ }
1583
1758
  default: {
1584
1759
  const event = buildEvent({ ...data });
1585
1760
  await putEvent(event);
1761
+ if (data.eventType === "hook_received" && ctx.params?.resumeId) await store.put(`${HOOK_RESUME_CLAIM_PREFIX}${ctx.params.resumeId}`, {
1762
+ hookId: data.correlationId,
1763
+ eventId: event.eventId,
1764
+ ...ctx.params.resumePayloadDigest ? { payloadDigest: ctx.params.resumePayloadDigest } : {}
1765
+ });
1586
1766
  return {
1587
1767
  ok: true,
1588
1768
  event,
@@ -1591,6 +1771,48 @@ async function applyEvent(store, ctx) {
1591
1771
  }
1592
1772
  }
1593
1773
  }
1774
+ /**
1775
+ * Complete Workflow 5's event-page response after an atomic apply. A celld
1776
+ * run is a single serialized writer, so the committed sequence is dense; a
1777
+ * stale eventCount therefore needs only the slots between the requested
1778
+ * position and the event this call committed.
1779
+ */
1780
+ async function finalizeEventPage(store, outcome, params) {
1781
+ if (!outcome.ok) return outcome;
1782
+ const result = outcome.run && outcome.maxEvents === void 0 ? {
1783
+ ...outcome,
1784
+ maxEvents: getMaxEventsPerRun()
1785
+ } : outcome;
1786
+ if (!result.event) return result;
1787
+ if (typeof params?.sinceCursor === "string") {
1788
+ const page = await listByPrefix(store, EVENT_KEY_PREFIX, {
1789
+ limit: 100,
1790
+ cursor: params.sinceCursor,
1791
+ sortOrder: "asc"
1792
+ }, (event) => event.eventId);
1793
+ return {
1794
+ ...result,
1795
+ events: page.data,
1796
+ cursor: page.cursor,
1797
+ hasMore: page.hasMore
1798
+ };
1799
+ }
1800
+ if (params?.eventCount === void 0) return result;
1801
+ const committedSlot = eventIdToSlot(result.event.eventId);
1802
+ if (committedSlot === null || committedSlot <= params.eventCount + 1) return result;
1803
+ const skippedCount = committedSlot - params.eventCount - 1;
1804
+ const events = (await listByPrefix(store, EVENT_KEY_PREFIX, {
1805
+ limit: skippedCount,
1806
+ cursor: params.eventCount > 0 ? slotToEventId(params.eventCount) : void 0,
1807
+ sortOrder: "asc"
1808
+ }, (event) => event.eventId)).data.filter((event) => event.eventId < result.event.eventId);
1809
+ return {
1810
+ ...result,
1811
+ events,
1812
+ cursor: null,
1813
+ hasMore: events.length < skippedCount
1814
+ };
1815
+ }
1594
1816
  //#endregion
1595
1817
  //#region src/retention.ts
1596
1818
  const CLEANUP_RECORD_KEY = "retention:cleanup";
@@ -1639,65 +1861,6 @@ const PAYLOAD_DELETE_BATCH = 256;
1639
1861
  const STREAM_DELETE_CONCURRENCY = 16;
1640
1862
  const CLEANUP_RETRY_MAX_MS = 36e5;
1641
1863
  /**
1642
- * Current schema version for DO storage.
1643
- * Increment when making breaking changes to the storage layout.
1644
- * Migrations run lazily on first request after deploy.
1645
- *
1646
- * v2: one storage key per event ('event:<eventId>'), step ('step:<stepId>')
1647
- * and hook ('hook:<hookId>') instead of single aggregate values. This package
1648
- * is pre-production, so the migration simply drops the legacy aggregates.
1649
- */
1650
- const CURRENT_SCHEMA_VERSION = 3;
1651
- /**
1652
- * Idempotent migration functions keyed by target version. The schema version
1653
- * is written only after the migration completes, so eviction safely retries
1654
- * an interrupted migration.
1655
- */
1656
- const MIGRATIONS = {
1657
- 2: async (storage) => {
1658
- await storage.transaction(async (txn) => {
1659
- await txn.delete("events");
1660
- await txn.delete("steps");
1661
- await txn.delete("hooks");
1662
- });
1663
- },
1664
- 3: async (storage) => {
1665
- await backfillCreationIndex(storage, STEP_KEY_PREFIX, STEP_CREATED_KEY_PREFIX, (step) => step.stepId);
1666
- await backfillCreationIndex(storage, HOOK_KEY_PREFIX, HOOK_CREATED_KEY_PREFIX, (hook) => hook.hookId);
1667
- }
1668
- };
1669
- async function backfillCreationIndex(storage, entityPrefix, indexPrefix, getId) {
1670
- let startAfter;
1671
- while (true) {
1672
- const entities = await storage.list({
1673
- prefix: entityPrefix,
1674
- startAfter,
1675
- limit: 100
1676
- });
1677
- if (entities.size === 0) return;
1678
- for (const [key, entity] of entities) {
1679
- const id = getId(entity);
1680
- await storage.put(`${indexPrefix}${entity.createdAt.toISOString()}:${id}`, id);
1681
- startAfter = key;
1682
- }
1683
- if (entities.size < 100) return;
1684
- }
1685
- }
1686
- /**
1687
- * Ensure the DO storage schema is up to date.
1688
- * Runs any pending migrations sequentially inside transactions,
1689
- * persisting the version after each successful migration.
1690
- * Safe to call on every request -- it's a no-op when already current.
1691
- */
1692
- async function ensureSchemaUpToDate(storage) {
1693
- const stored = await storage.get("schema_version") ?? 1;
1694
- for (let v = stored + 1; v <= CURRENT_SCHEMA_VERSION; v++) {
1695
- const migration = MIGRATIONS[v];
1696
- if (migration) await migration(storage);
1697
- await storage.put("schema_version", v);
1698
- }
1699
- }
1700
- /**
1701
1864
  * Adapt DO storage (or a transaction) to the {@link EventStore} interface
1702
1865
  * shared with the test mocks. No casts: the methods line up structurally,
1703
1866
  * this just pins the subset we rely on.
@@ -1721,15 +1884,10 @@ function storeFrom(storage) {
1721
1884
  * queue-message dedup claims (see claimInflight/releaseInflight).
1722
1885
  */
1723
1886
  var WorkflowRunDO = class extends DurableObject {
1724
- schemaReady = null;
1725
1887
  now() {
1726
1888
  const clock = this.env?.clock;
1727
1889
  return typeof clock === "function" ? clock() : Date.now();
1728
1890
  }
1729
- ensureSchema() {
1730
- if (!this.schemaReady) this.schemaReady = ensureSchemaUpToDate(this.ctx.storage);
1731
- return this.schemaReady;
1732
- }
1733
1891
  async expiredTombstone(storage, now = this.now()) {
1734
1892
  const tombstone = await storage.get(TOMBSTONE_KEY);
1735
1893
  if (tombstone) return tombstone;
@@ -1738,7 +1896,6 @@ var WorkflowRunDO = class extends DurableObject {
1738
1896
  return cleanupTombstone(cleanup, cleanup.tombstonedAt ?? cleanup.dueAt);
1739
1897
  }
1740
1898
  async read(reader) {
1741
- await this.ensureSchema();
1742
1899
  const tombstone = await this.expiredTombstone(this.ctx.storage);
1743
1900
  if (tombstone) return expiredRead(tombstone);
1744
1901
  return {
@@ -1748,7 +1905,6 @@ var WorkflowRunDO = class extends DurableObject {
1748
1905
  }
1749
1906
  /** Apply an event and capture all retention metadata in the same transaction. */
1750
1907
  async applyEvent(request) {
1751
- await this.ensureSchema();
1752
1908
  return await this.ctx.storage.transaction(async (txn) => {
1753
1909
  const now = new Date(this.now());
1754
1910
  const tombstone = await this.expiredTombstone(txn, now.getTime());
@@ -1760,7 +1916,7 @@ var WorkflowRunDO = class extends DurableObject {
1760
1916
  let eventSequence = await txn.get("event_sequence") ?? 0;
1761
1917
  const outcome = await applyEvent(storeFrom(txn), {
1762
1918
  ...request,
1763
- nextEventId: () => `wevt_z${String(++eventSequence).padStart(20, "0")}`,
1919
+ nextEventId: () => slotToEventId(++eventSequence),
1764
1920
  now
1765
1921
  });
1766
1922
  if (!outcome.ok) return outcome;
@@ -1805,7 +1961,7 @@ var WorkflowRunDO = class extends DurableObject {
1805
1961
  await txn.setAlarm(dueAt);
1806
1962
  outcome.run = run;
1807
1963
  }
1808
- return outcome;
1964
+ return finalizeEventPage(storeFrom(txn), outcome, request.params);
1809
1965
  });
1810
1966
  }
1811
1967
  async getRun() {
@@ -1839,12 +1995,10 @@ var WorkflowRunDO = class extends DurableObject {
1839
1995
  }));
1840
1996
  }
1841
1997
  async getCleanupStatus() {
1842
- await this.ensureSchema();
1843
1998
  return await this.ctx.storage.get("retention:cleanup") ?? null;
1844
1999
  }
1845
2000
  /** Explicitly schedule an existing terminal run, used for opt-in backfills. */
1846
2001
  async scheduleCleanup(request) {
1847
- await this.ensureSchema();
1848
2002
  if (request.retentionMs <= 0) return this.getCleanupStatus();
1849
2003
  return await this.ctx.storage.transaction(async (txn) => {
1850
2004
  const existing = await txn.get(CLEANUP_RECORD_KEY);
@@ -1880,7 +2034,6 @@ var WorkflowRunDO = class extends DurableObject {
1880
2034
  });
1881
2035
  }
1882
2036
  async cleanupNow(request) {
1883
- await this.ensureSchema();
1884
2037
  if (!await this.ctx.storage.get("retention:cleanup")) await this.scheduleCleanup({
1885
2038
  ...request,
1886
2039
  retentionMs: Math.max(1, request.retentionMs)
@@ -1908,7 +2061,6 @@ var WorkflowRunDO = class extends DurableObject {
1908
2061
  return this.getCleanupStatus();
1909
2062
  }
1910
2063
  async rearmCleanup() {
1911
- await this.ensureSchema();
1912
2064
  const cleanup = await this.ctx.storage.get(CLEANUP_RECORD_KEY);
1913
2065
  if (!cleanup) return null;
1914
2066
  if (cleanup.phase === "tombstoned") await this.ctx.storage.deleteAlarm();
@@ -1916,7 +2068,6 @@ var WorkflowRunDO = class extends DurableObject {
1916
2068
  return cleanup;
1917
2069
  }
1918
2070
  async alarm() {
1919
- await this.ensureSchema();
1920
2071
  try {
1921
2072
  await this.executeCleanup();
1922
2073
  } catch (error) {
@@ -2019,7 +2170,7 @@ var WorkflowRunDO = class extends DurableObject {
2019
2170
  const cleanup = await txn.get(CLEANUP_RECORD_KEY);
2020
2171
  if (!cleanup || cleanup.phase !== "payload") return true;
2021
2172
  const entries = await txn.list({ limit: 259 });
2022
- const keys = Array.from(entries.keys()).filter((key) => key !== "retention:cleanup" && key !== "retention:tombstone" && key !== "schema_version").slice(0, PAYLOAD_DELETE_BATCH);
2173
+ const keys = Array.from(entries.keys()).filter((key) => key !== "retention:cleanup" && key !== "retention:tombstone").slice(0, PAYLOAD_DELETE_BATCH);
2023
2174
  const now = new Date(this.now());
2024
2175
  await txn.put(TOMBSTONE_KEY, cleanupTombstone(cleanup, now));
2025
2176
  for (const key of keys) await txn.delete(key);
@@ -2059,7 +2210,6 @@ var WorkflowRunDO = class extends DurableObject {
2059
2210
  });
2060
2211
  }
2061
2212
  async claimInflight(params) {
2062
- await this.ensureSchema();
2063
2213
  return await this.ctx.storage.transaction(async (txn) => {
2064
2214
  const existing = await txn.get("claim");
2065
2215
  const now = this.now();
@@ -2072,7 +2222,6 @@ var WorkflowRunDO = class extends DurableObject {
2072
2222
  });
2073
2223
  }
2074
2224
  async releaseInflight() {
2075
- await this.ensureSchema();
2076
2225
  await this.ctx.storage.delete("claim");
2077
2226
  }
2078
2227
  };