@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/CHANGELOG.md +10 -0
- package/README.md +1 -1
- package/dist/index.d.ts +15 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +122 -105
- package/dist/index.js.map +1 -1
- package/dist/testing.js +255 -106
- package/dist/testing.js.map +1 -1
- package/dist/worker.d.ts +14 -6
- package/dist/worker.d.ts.map +1 -1
- package/dist/worker.js +255 -106
- package/dist/worker.js.map +1 -1
- package/package.json +4 -4
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,
|
|
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
|
|
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)
|
|
1419
|
-
|
|
1420
|
-
|
|
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")
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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:
|
|
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
|
-
|
|
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
|
|
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
|
|
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: () =>
|
|
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"
|
|
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
|
};
|