@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/worker.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { EventSchema, HookSchema, SPEC_VERSION_CURRENT, StepSchema, WorkflowRunSchema,
|
|
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
|
|
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)
|
|
1261
|
-
|
|
1262
|
-
|
|
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")
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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:
|
|
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
|
-
|
|
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
|
|
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
|
|
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: () =>
|
|
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"
|
|
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
|
};
|