@ewhauser/world-celld 0.3.2 → 0.4.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 +9 -0
- package/README.md +60 -20
- package/celld-worker/README.md +22 -0
- package/celld-worker/wrangler.jsonc +12 -0
- package/dist/index.d.ts +14 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/testing.js +133 -36
- package/dist/testing.js.map +1 -1
- package/dist/util-ya3V_FTA.js.map +1 -1
- package/dist/worker.d.ts +46 -5
- package/dist/worker.d.ts.map +1 -1
- package/dist/worker.js +259 -84
- package/dist/worker.js.map +1 -1
- package/package.json +1 -1
package/dist/testing.js
CHANGED
|
@@ -1744,8 +1744,11 @@ var RunCatalogDO = class extends DurableObject {
|
|
|
1744
1744
|
prefix: options.prefix ?? "",
|
|
1745
1745
|
...options.reverse ? {
|
|
1746
1746
|
reverse: true,
|
|
1747
|
-
end: options.cursor
|
|
1748
|
-
} : {
|
|
1747
|
+
end: options.cursor ?? options.end
|
|
1748
|
+
} : {
|
|
1749
|
+
startAfter: options.cursor,
|
|
1750
|
+
end: options.end
|
|
1751
|
+
},
|
|
1749
1752
|
limit: limit + 1
|
|
1750
1753
|
});
|
|
1751
1754
|
const page = Array.from(entries).slice(0, limit);
|
|
@@ -1759,6 +1762,15 @@ var RunCatalogDO = class extends DurableObject {
|
|
|
1759
1762
|
cursor: listComplete ? void 0 : page.at(-1)?.[0]
|
|
1760
1763
|
};
|
|
1761
1764
|
}
|
|
1765
|
+
/** Compare-and-delete one rejected retention discovery candidate. */
|
|
1766
|
+
async deleteStaleGlobalRun(runId, key, expectedValue) {
|
|
1767
|
+
if (!key.startsWith("runall:") || !key.endsWith(`:${runId}`)) throw new Error("Stale run catalog deletion requires the exact global run key");
|
|
1768
|
+
if (JSON.parse(expectedValue).runId !== runId) throw new Error("Stale run catalog deletion requires matching run metadata");
|
|
1769
|
+
return await this.ctx.storage.transaction(async (txn) => {
|
|
1770
|
+
if (await txn.get(key) !== expectedValue) return { deleted: false };
|
|
1771
|
+
return { deleted: await txn.delete(key) };
|
|
1772
|
+
});
|
|
1773
|
+
}
|
|
1762
1774
|
async expireRun(runId, keys, expiredAt) {
|
|
1763
1775
|
if (keys.length !== 2) throw new Error("Run catalog expiry requires exactly two keys");
|
|
1764
1776
|
return await this.ctx.storage.transaction(async (txn) => {
|
|
@@ -3043,6 +3055,7 @@ function cleanupTombstone(record, now) {
|
|
|
3043
3055
|
return {
|
|
3044
3056
|
version: 1,
|
|
3045
3057
|
runId: record.runId,
|
|
3058
|
+
status: record.status ?? record.terminalStatus,
|
|
3046
3059
|
terminalStatus: record.terminalStatus,
|
|
3047
3060
|
completedAt: record.completedAt,
|
|
3048
3061
|
expiredAt: record.dueAt,
|
|
@@ -3074,6 +3087,28 @@ const STREAM_BYTES_PER_CLEANUP_PAGE = 16777216;
|
|
|
3074
3087
|
const QUEUE_REFERENCES_PER_CLEANUP_PAGE = 64;
|
|
3075
3088
|
const CLEANUP_RETRY_MAX_MS = 36e5;
|
|
3076
3089
|
const CLEANUP_PROGRESS_KEY = "retention:progress";
|
|
3090
|
+
const RUN_QUEUE_SHARDS_KEY = "retention:queue-shards";
|
|
3091
|
+
function cleanupRecord(run, dueAt, queueShards, reason) {
|
|
3092
|
+
const terminal = isTerminalWorkflowRunStatus(run.status) ? run : void 0;
|
|
3093
|
+
return {
|
|
3094
|
+
version: 1,
|
|
3095
|
+
runId: run.runId,
|
|
3096
|
+
workflowName: run.workflowName,
|
|
3097
|
+
createdAt: run.createdAt,
|
|
3098
|
+
status: run.status,
|
|
3099
|
+
completedAt: terminal?.completedAt,
|
|
3100
|
+
terminalStatus: terminal?.status,
|
|
3101
|
+
reason,
|
|
3102
|
+
dueAt,
|
|
3103
|
+
queueShards,
|
|
3104
|
+
phase: "retained",
|
|
3105
|
+
generation: 0,
|
|
3106
|
+
attempts: 0,
|
|
3107
|
+
deletedPayloadKeys: 0,
|
|
3108
|
+
deletedStreams: 0,
|
|
3109
|
+
deletedQueueMessages: 0
|
|
3110
|
+
};
|
|
3111
|
+
}
|
|
3077
3112
|
/**
|
|
3078
3113
|
* Adapt DO storage (or a transaction) to the {@link EventStore} interface
|
|
3079
3114
|
* shared with the test mocks. No casts: the methods line up structurally,
|
|
@@ -3169,6 +3204,7 @@ var WorkflowRunDO = class extends DurableObject {
|
|
|
3169
3204
|
now
|
|
3170
3205
|
});
|
|
3171
3206
|
if (!outcome.ok) return outcome;
|
|
3207
|
+
if (outcome.runCreated && request.cleanup) await txn.put(RUN_QUEUE_SHARDS_KEY, request.cleanup.queueShards);
|
|
3172
3208
|
await txn.put("event_sequence", eventSequence);
|
|
3173
3209
|
await putStorageEntries(txn, (outcome.hookToIndex ? [{
|
|
3174
3210
|
hookId: outcome.hookToIndex.hookId,
|
|
@@ -3182,22 +3218,7 @@ var WorkflowRunDO = class extends DurableObject {
|
|
|
3182
3218
|
...terminalRun,
|
|
3183
3219
|
expiredAt: dueAt
|
|
3184
3220
|
});
|
|
3185
|
-
const cleanup =
|
|
3186
|
-
version: 1,
|
|
3187
|
-
runId: run.runId,
|
|
3188
|
-
workflowName: run.workflowName,
|
|
3189
|
-
createdAt: run.createdAt,
|
|
3190
|
-
completedAt: run.completedAt,
|
|
3191
|
-
terminalStatus: run.status,
|
|
3192
|
-
dueAt,
|
|
3193
|
-
queueShards: request.cleanup?.queueShards ?? 1,
|
|
3194
|
-
phase: "retained",
|
|
3195
|
-
generation: 0,
|
|
3196
|
-
attempts: 0,
|
|
3197
|
-
deletedPayloadKeys: 0,
|
|
3198
|
-
deletedStreams: 0,
|
|
3199
|
-
deletedQueueMessages: 0
|
|
3200
|
-
};
|
|
3221
|
+
const cleanup = cleanupRecord(run, dueAt, request.cleanup?.queueShards ?? 1, "terminal-retention");
|
|
3201
3222
|
await txn.put("run", run);
|
|
3202
3223
|
await txn.put(CLEANUP_RECORD_KEY, cleanup);
|
|
3203
3224
|
await txn.setAlarm(dueAt);
|
|
@@ -3307,28 +3328,95 @@ var WorkflowRunDO = class extends DurableObject {
|
|
|
3307
3328
|
...terminalRun,
|
|
3308
3329
|
expiredAt: dueAt
|
|
3309
3330
|
});
|
|
3310
|
-
const cleanup =
|
|
3311
|
-
version: 1,
|
|
3312
|
-
runId: run.runId,
|
|
3313
|
-
workflowName: run.workflowName,
|
|
3314
|
-
createdAt: run.createdAt,
|
|
3315
|
-
completedAt: run.completedAt,
|
|
3316
|
-
terminalStatus: run.status,
|
|
3317
|
-
dueAt,
|
|
3318
|
-
queueShards: request.queueShards,
|
|
3319
|
-
phase: "retained",
|
|
3320
|
-
generation: 0,
|
|
3321
|
-
attempts: 0,
|
|
3322
|
-
deletedPayloadKeys: 0,
|
|
3323
|
-
deletedStreams: 0,
|
|
3324
|
-
deletedQueueMessages: 0
|
|
3325
|
-
};
|
|
3331
|
+
const cleanup = cleanupRecord(run, dueAt, request.queueShards, "manual");
|
|
3326
3332
|
await txn.put("run", run);
|
|
3327
3333
|
await txn.put(CLEANUP_RECORD_KEY, cleanup);
|
|
3328
3334
|
await txn.setAlarm(values.get("terminal:cleanup") || dueAt.getTime() <= this.now() ? this.now() + 1 : dueAt);
|
|
3329
3335
|
return cleanup;
|
|
3330
3336
|
});
|
|
3331
3337
|
}
|
|
3338
|
+
/**
|
|
3339
|
+
* Apply the fleet-wide maximum-age policy to one authoritative run.
|
|
3340
|
+
*
|
|
3341
|
+
* The catalog is only a discovery index: eligibility is rechecked from the
|
|
3342
|
+
* RunDO's own createdAt. Once eligible, the cleanup deadline immediately
|
|
3343
|
+
* becomes an authoritative write/read fence, including for active runs.
|
|
3344
|
+
*/
|
|
3345
|
+
async enforceRetention(request) {
|
|
3346
|
+
if (!Number.isSafeInteger(request.retentionMs) || request.retentionMs <= 0) throw new Error("world-celld retentionMs must be a positive safe integer");
|
|
3347
|
+
if (!Number.isSafeInteger(request.queueShards) || request.queueShards <= 0) throw new Error("world-celld retention queueShards must be a positive safe integer");
|
|
3348
|
+
if (!Number.isSafeInteger(request.scheduledTime) || request.scheduledTime < 0) throw new Error("world-celld retention scheduledTime must be a non-negative safe integer");
|
|
3349
|
+
const result = await this.ctx.storage.transaction(async (txn) => {
|
|
3350
|
+
const values = await txn.get([
|
|
3351
|
+
CLEANUP_RECORD_KEY,
|
|
3352
|
+
TOMBSTONE_KEY,
|
|
3353
|
+
RUN_QUEUE_SHARDS_KEY,
|
|
3354
|
+
"run"
|
|
3355
|
+
]);
|
|
3356
|
+
const tombstone = values.get(TOMBSTONE_KEY);
|
|
3357
|
+
if (tombstone) return {
|
|
3358
|
+
state: "expired",
|
|
3359
|
+
cleanup: cleanupFromTombstone(tombstone)
|
|
3360
|
+
};
|
|
3361
|
+
const run = values.get("run");
|
|
3362
|
+
if (!run) return {
|
|
3363
|
+
state: "missing",
|
|
3364
|
+
cleanup: null
|
|
3365
|
+
};
|
|
3366
|
+
const dueAtMs = run.createdAt.getTime() + request.retentionMs;
|
|
3367
|
+
if (!Number.isSafeInteger(dueAtMs) || dueAtMs > 864e13) throw new Error(`world-celld retention deadline is out of range for ${run.runId}`);
|
|
3368
|
+
if (dueAtMs > request.scheduledTime) return {
|
|
3369
|
+
state: "not-due",
|
|
3370
|
+
cleanup: null
|
|
3371
|
+
};
|
|
3372
|
+
const dueAt = new Date(dueAtMs);
|
|
3373
|
+
const existing = values.get(CLEANUP_RECORD_KEY);
|
|
3374
|
+
const storedQueueShards = values.get(RUN_QUEUE_SHARDS_KEY);
|
|
3375
|
+
const queueShards = Number.isSafeInteger(storedQueueShards) && storedQueueShards > 0 ? storedQueueShards : request.queueShards;
|
|
3376
|
+
let cleanup = existing;
|
|
3377
|
+
if (!cleanup) {
|
|
3378
|
+
cleanup = cleanupRecord(run, dueAt, queueShards, "maximum-age");
|
|
3379
|
+
await txn.put("run", WorkflowRunSchema.parse({
|
|
3380
|
+
...run,
|
|
3381
|
+
expiredAt: dueAt
|
|
3382
|
+
}));
|
|
3383
|
+
await txn.put(CLEANUP_RECORD_KEY, cleanup);
|
|
3384
|
+
} else if (cleanup.phase === "retained" && dueAtMs < cleanup.dueAt.getTime()) {
|
|
3385
|
+
const terminal = isTerminalWorkflowRunStatus(run.status) ? run : void 0;
|
|
3386
|
+
cleanup = {
|
|
3387
|
+
...cleanup,
|
|
3388
|
+
status: run.status,
|
|
3389
|
+
completedAt: terminal?.completedAt,
|
|
3390
|
+
terminalStatus: terminal?.status,
|
|
3391
|
+
reason: "maximum-age",
|
|
3392
|
+
dueAt,
|
|
3393
|
+
generation: cleanup.generation + 1
|
|
3394
|
+
};
|
|
3395
|
+
await txn.put("run", WorkflowRunSchema.parse({
|
|
3396
|
+
...run,
|
|
3397
|
+
expiredAt: dueAt
|
|
3398
|
+
}));
|
|
3399
|
+
await txn.put(CLEANUP_RECORD_KEY, cleanup);
|
|
3400
|
+
}
|
|
3401
|
+
await txn.setAlarm(this.now() + 1);
|
|
3402
|
+
return {
|
|
3403
|
+
state: cleanup.dueAt.getTime() <= this.now() ? "expired" : "scheduled",
|
|
3404
|
+
cleanup
|
|
3405
|
+
};
|
|
3406
|
+
});
|
|
3407
|
+
if (result.state === "scheduled" || result.state === "expired") {
|
|
3408
|
+
for (let page = 0; page < 2; page++) {
|
|
3409
|
+
const cleanup = await this.ctx.storage.get(CLEANUP_RECORD_KEY);
|
|
3410
|
+
if (cleanup?.phase !== "retained" && cleanup?.phase !== "index") break;
|
|
3411
|
+
await this.executeScheduledCleanupPage();
|
|
3412
|
+
}
|
|
3413
|
+
return {
|
|
3414
|
+
...result,
|
|
3415
|
+
cleanup: await this.getCleanupStatus() ?? result.cleanup
|
|
3416
|
+
};
|
|
3417
|
+
}
|
|
3418
|
+
return result;
|
|
3419
|
+
}
|
|
3332
3420
|
async cleanupNow(request) {
|
|
3333
3421
|
if (!await this.ctx.storage.get("retention:cleanup")) await this.scheduleCleanup({
|
|
3334
3422
|
...request,
|
|
@@ -3392,7 +3480,17 @@ var WorkflowRunDO = class extends DurableObject {
|
|
|
3392
3480
|
});
|
|
3393
3481
|
}
|
|
3394
3482
|
async executeScheduledCleanupPage() {
|
|
3395
|
-
const
|
|
3483
|
+
const values = await this.ctx.storage.get([CLEANUP_RECORD_KEY, TERMINAL_CLEANUP_KEY]);
|
|
3484
|
+
const cleanup = values.get(CLEANUP_RECORD_KEY);
|
|
3485
|
+
const terminalCleanup = values.get(TERMINAL_CLEANUP_KEY);
|
|
3486
|
+
if (cleanup && cleanup.phase !== "tombstoned" && cleanup.dueAt.getTime() <= this.now()) {
|
|
3487
|
+
try {
|
|
3488
|
+
await this.executeCleanup(cleanup);
|
|
3489
|
+
} catch (error) {
|
|
3490
|
+
await this.recordCleanupFailure(error, cleanup);
|
|
3491
|
+
}
|
|
3492
|
+
return;
|
|
3493
|
+
}
|
|
3396
3494
|
if (terminalCleanup) {
|
|
3397
3495
|
try {
|
|
3398
3496
|
await this.executeTerminalCleanup(terminalCleanup);
|
|
@@ -3401,7 +3499,6 @@ var WorkflowRunDO = class extends DurableObject {
|
|
|
3401
3499
|
}
|
|
3402
3500
|
return;
|
|
3403
3501
|
}
|
|
3404
|
-
const cleanup = await this.ctx.storage.get(CLEANUP_RECORD_KEY);
|
|
3405
3502
|
if (!cleanup || cleanup.phase === "tombstoned") {
|
|
3406
3503
|
await this.ctx.storage.deleteAlarm();
|
|
3407
3504
|
return;
|