@lostgradient/weft 0.24.0 → 0.24.2
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/README.md +2 -2
- package/dist/cli/conformance.js +5 -2
- package/dist/cli/generated/operation-catalog.snapshot.json +95 -1
- package/dist/cli/generated/operation-client.generated.d.ts +28 -2
- package/dist/cli/generated/operation-client.generated.js +2 -0
- package/dist/cli/parse-schedule-arguments.js +15 -3
- package/dist/cli/schedule.js +2 -1
- package/dist/cli/types.d.ts +2 -1
- package/dist/client/http-client-requests.js +6 -9
- package/dist/client/start-body.js +1 -0
- package/dist/core/catalog/index.d.ts +2 -2
- package/dist/core/catalog/index.js +9 -2
- package/dist/core/catalog/reference-counts.d.ts +29 -14
- package/dist/core/catalog/removal.d.ts +63 -10
- package/dist/core/catalog/removal.js +18 -2
- package/dist/core/catalog/storage-io.d.ts +23 -0
- package/dist/core/catalog/storage-io.js +1 -1
- package/dist/core/engine/activity-resolution.js +17 -17
- package/dist/core/engine/bulk-operations-purge.d.ts +1 -1
- package/dist/core/engine/bulk-operations-purge.js +17 -22
- package/dist/core/engine/bulk-operations-retry.d.ts +24 -0
- package/dist/core/engine/bulk-operations-retry.js +190 -0
- package/dist/core/engine/bulk-operations-shared.d.ts +18 -0
- package/dist/core/engine/bulk-operations-shared.js +14 -0
- package/dist/core/engine/bulk-operations.d.ts +3 -2
- package/dist/core/engine/bulk-operations.js +10 -169
- package/dist/core/engine/callback-creators-bundles.js +5 -3
- package/dist/core/engine/callback-creators-core.js +9 -1
- package/dist/core/engine/callback-creators-schedule.js +2 -2
- package/dist/core/engine/catalog-readiness.js +2 -0
- package/dist/core/engine/catalog-removal.d.ts +107 -3
- package/dist/core/engine/catalog-removal.js +66 -13
- package/dist/core/engine/catalog-tombstone-recovery.d.ts +56 -0
- package/dist/core/engine/catalog-tombstone-recovery.js +39 -0
- package/dist/core/engine/child-workflow.d.ts +13 -1
- package/dist/core/engine/child-workflow.js +22 -4
- package/dist/core/engine/constraints.js +5 -1
- package/dist/core/engine/construction.d.ts +1 -1
- package/dist/core/engine/decode-revision.d.ts +32 -0
- package/dist/core/engine/decode-revision.js +9 -0
- package/dist/core/engine/disposal.d.ts +6 -2
- package/dist/core/engine/disposal.js +24 -10
- package/dist/core/engine/dynamic-source-errors.d.ts +81 -0
- package/dist/core/engine/dynamic-source-errors.js +25 -0
- package/dist/core/engine/dynamic-source-execution.d.ts +173 -0
- package/dist/core/engine/dynamic-source-execution.js +108 -0
- package/dist/core/engine/engine-internal-types.d.ts +16 -0
- package/dist/core/engine/engine-workflows-namespace.d.ts +10 -0
- package/dist/core/engine/engine-workflows-namespace.js +3 -1
- package/dist/core/engine/generation-codec.d.ts +22 -0
- package/dist/core/engine/generation-codec.js +2 -0
- package/dist/core/engine/index.d.ts +18 -10
- package/dist/core/engine/index.js +22 -11
- package/dist/core/engine/inline-launch-queue.js +1 -1
- package/dist/core/engine/internals.d.ts +26 -30
- package/dist/core/engine/lifecycle/checkpoint-launch.d.ts +29 -0
- package/dist/core/engine/lifecycle/checkpoint-launch.js +75 -0
- package/dist/core/engine/lifecycle/delayed-start-registration.d.ts +30 -0
- package/dist/core/engine/lifecycle/delayed-start-registration.js +11 -0
- package/dist/core/engine/lifecycle/fork-helpers.d.ts +33 -1
- package/dist/core/engine/lifecycle/fork-helpers.js +3 -1
- package/dist/core/engine/lifecycle/recovery-isolation.d.ts +20 -0
- package/dist/core/engine/lifecycle/recovery-isolation.js +26 -0
- package/dist/core/engine/lifecycle/recovery-revision-groups.d.ts +75 -0
- package/dist/core/engine/lifecycle/recovery-revision-groups.js +64 -0
- package/dist/core/engine/lifecycle/resume-body.d.ts +44 -0
- package/dist/core/engine/lifecycle/resume-body.js +211 -0
- package/dist/core/engine/lifecycle/resume-generation-guard.d.ts +58 -0
- package/dist/core/engine/lifecycle/resume-generation-guard.js +11 -0
- package/dist/core/engine/lifecycle/resume.d.ts +46 -3
- package/dist/core/engine/lifecycle/resume.js +21 -201
- package/dist/core/engine/lifecycle/shared.d.ts +43 -0
- package/dist/core/engine/lifecycle/standalone-claim-acquire.d.ts +80 -2
- package/dist/core/engine/lifecycle/standalone-claim-acquire.js +20 -4
- package/dist/core/engine/lifecycle/start-commit-errors.d.ts +12 -0
- package/dist/core/engine/lifecycle/start-commit-errors.js +6 -0
- package/dist/core/engine/lifecycle/start-commit.d.ts +24 -7
- package/dist/core/engine/lifecycle/start-commit.js +57 -15
- package/dist/core/engine/lifecycle/start-exec.d.ts +18 -2
- package/dist/core/engine/lifecycle/start-exec.js +7 -5
- package/dist/core/engine/lifecycle/start-or-signal-create.js +1 -3
- package/dist/core/engine/lifecycle/start-or-signal-resolution.d.ts +8 -2
- package/dist/core/engine/lifecycle/start-or-signal-resolution.js +11 -3
- package/dist/core/engine/lifecycle/start-or-signal.js +1 -1
- package/dist/core/engine/lifecycle/start-precondition-attribution.d.ts +55 -0
- package/dist/core/engine/lifecycle/start-precondition-attribution.js +13 -0
- package/dist/core/engine/lifecycle/start-revision-resolution.d.ts +34 -0
- package/dist/core/engine/lifecycle/start-revision-resolution.js +11 -0
- package/dist/core/engine/lifecycle/start-schedule-timing.d.ts +11 -0
- package/dist/core/engine/lifecycle/start-schedule-timing.js +21 -0
- package/dist/core/engine/lifecycle/start-state.d.ts +16 -0
- package/dist/core/engine/lifecycle/start-state.js +60 -0
- package/dist/core/engine/lifecycle/start-terminal-conflict-purge.d.ts +146 -4
- package/dist/core/engine/lifecycle/start-terminal-conflict-purge.js +33 -4
- package/dist/core/engine/lifecycle/start.d.ts +54 -7
- package/dist/core/engine/lifecycle/start.js +53 -90
- package/dist/core/engine/lifecycle/transition.d.ts +12 -4
- package/dist/core/engine/lifecycle/transition.js +40 -95
- package/dist/core/engine/lifecycle.d.ts +5 -2
- package/dist/core/engine/lifecycle.js +7 -6
- package/dist/core/engine/listing.js +15 -1
- package/dist/core/engine/memo-durable-activity.js +4 -1
- package/dist/core/engine/nonterminal-revision-count.d.ts +25 -0
- package/dist/core/engine/nonterminal-revision-count.js +16 -0
- package/dist/core/engine/operations-time.d.ts +5 -23
- package/dist/core/engine/operations-time.js +13 -11
- package/dist/core/engine/ownership-bootstrap.d.ts +1 -1
- package/dist/core/engine/pinned-schedule-revision-count.d.ts +26 -0
- package/dist/core/engine/pinned-schedule-revision-count.js +19 -0
- package/dist/core/engine/pinned-schedule-revision.d.ts +76 -0
- package/dist/core/engine/pinned-schedule-revision.js +53 -0
- package/dist/core/engine/registration.d.ts +30 -0
- package/dist/core/engine/registration.js +17 -2
- package/dist/core/engine/retention.js +9 -2
- package/dist/core/engine/revision-errors.d.ts +71 -0
- package/dist/core/engine/revision-errors.js +13 -0
- package/dist/core/engine/schedule-overlap.js +2 -1
- package/dist/core/engine/schedule-revision-fire.d.ts +17 -0
- package/dist/core/engine/schedule-revision-fire.js +3 -0
- package/dist/core/engine/schedule-run.d.ts +10 -0
- package/dist/core/engine/schedule-run.js +2 -1
- package/dist/core/engine/schedules.d.ts +11 -1
- package/dist/core/engine/schedules.js +28 -9
- package/dist/core/engine/source-diagnostics.d.ts +76 -0
- package/dist/core/engine/source-diagnostics.js +105 -0
- package/dist/core/engine/source-registration.js +4 -2
- package/dist/core/engine/source-resolution.d.ts +25 -9
- package/dist/core/engine/source-resolution.js +49 -14
- package/dist/core/engine/source-runtime-state.d.ts +92 -0
- package/dist/core/engine/source-runtime-state.js +36 -0
- package/dist/core/engine/storage-io.d.ts +19 -2
- package/dist/core/engine/storage-io.js +21 -2
- package/dist/core/engine/termination/complete.js +1 -1
- package/dist/core/engine/termination/finalizer-registration.d.ts +18 -0
- package/dist/core/engine/termination/finalizer-registration.js +21 -0
- package/dist/core/engine/termination/finalizer.d.ts +16 -19
- package/dist/core/engine/termination/finalizer.js +3 -3
- package/dist/core/engine/termination/suspend.js +3 -0
- package/dist/core/engine/time-operation-callbacks.d.ts +32 -0
- package/dist/core/engine/time-operation-callbacks.js +0 -0
- package/dist/core/engine/validation/schedule-cadence.d.ts +16 -0
- package/dist/core/engine/validation/schedule-cadence.js +20 -0
- package/dist/core/engine/validation/schedule-options.d.ts +4 -1
- package/dist/core/engine/validation/schedule-options.js +14 -0
- package/dist/core/engine/validation/schedule-revision.d.ts +24 -0
- package/dist/core/engine/validation/schedule-revision.js +21 -0
- package/dist/core/engine/validation/schedule-warnings.d.ts +11 -0
- package/dist/core/engine/validation/schedule-warnings.js +5 -0
- package/dist/core/engine/validation/schedule.d.ts +29 -4
- package/dist/core/engine/validation/schedule.js +19 -36
- package/dist/core/engine/validation.js +13 -17
- package/dist/core/engine/workflow-claim-reclaim-target.d.ts +1 -1
- package/dist/core/engine/workflow-claim-reclaim-target.js +22 -8
- package/dist/core/engine/workflow-claim-registry.d.ts +79 -75
- package/dist/core/engine/workflow-claim-registry.js +15 -2
- package/dist/core/engine/workflow-generation-fence.d.ts +74 -0
- package/dist/core/engine/workflow-generation-fence.js +19 -0
- package/dist/core/engine/workflow-retention-deadline.d.ts +16 -0
- package/dist/core/engine/workflow-retention-deadline.js +33 -0
- package/dist/core/events/event-map.d.ts +5 -0
- package/dist/core/events/index.d.ts +1 -0
- package/dist/core/events/index.js +1 -0
- package/dist/core/events/workflow-source-events.d.ts +102 -0
- package/dist/core/events/workflow-source-events.js +57 -0
- package/dist/core/execution-strategy.d.ts +7 -0
- package/dist/core/inline-execution-strategy.context-options.d.ts +1 -1
- package/dist/core/inline-execution-strategy.js +1 -1
- package/dist/core/start-workflow-validation.d.ts +15 -0
- package/dist/core/start-workflow-validation.js +11 -1
- package/dist/core/types/checkpoint.d.ts +14 -0
- package/dist/core/types/schedules.d.ts +52 -1
- package/dist/core/types/state.d.ts +16 -0
- package/dist/core/weft-error.d.ts +1 -1
- package/dist/core/weft-error.js +4 -1
- package/dist/core/worker-execution-strategy.d.ts +1 -0
- package/dist/core/worker-execution-strategy.js +13 -1
- package/dist/core/worker-fault-handling.d.ts +10 -0
- package/dist/core/worker-fault-handling.js +1 -0
- package/dist/core/worker-inbound-message.d.ts +1 -0
- package/dist/core/worker-inbound-message.js +1 -0
- package/dist/core/worker-protocol-guard.js +4 -0
- package/dist/core/worker-protocol.d.ts +7 -0
- package/dist/core/worker-protocol.js +2 -1
- package/dist/core/worker-turn-watchdog.d.ts +8 -1
- package/dist/core/worker-turn-watchdog.js +3 -2
- package/dist/core/workflow-identifiers.d.ts +37 -0
- package/dist/core/workflow-identifiers.js +19 -1
- package/dist/diagnostics/format.js +5 -0
- package/dist/diagnostics/types.d.ts +24 -0
- package/dist/diagnostics/version-check.js +8 -2
- package/dist/http.js +2 -2
- package/dist/index.d.ts +3 -3
- package/dist/index.js +7 -0
- package/dist/indexeddb.js +1 -1
- package/dist/json-schema.js +3 -3
- package/dist/server/fault-to-json-rpc.js +2 -1
- package/dist/server/fleet-event-feed.js +6 -6
- package/dist/server/index.d.ts +11 -0
- package/dist/server/operation-fault.d.ts +2 -0
- package/dist/server/operation-fault.js +2 -1
- package/dist/server/operations/create-schedule.d.ts +2 -0
- package/dist/server/operations/create-schedule.js +10 -6
- package/dist/server/operations/get-catalog-diagnostics.d.ts +28 -0
- package/dist/server/operations/get-catalog-diagnostics.js +10 -2
- package/dist/server/operations/get-task-detail-schema.d.ts +8 -0
- package/dist/server/operations/get-task-detail-schema.js +1 -0
- package/dist/server/operations/get-task-detail.d.ts +8 -0
- package/dist/server/operations/get-task-detail.js +1 -0
- package/dist/server/operations/get-task-diagnostics.d.ts +4 -4
- package/dist/server/operations/preload-workflow-revision.d.ts +28 -0
- package/dist/server/operations/preload-workflow-revision.js +60 -0
- package/dist/server/operations/schedule-faults.d.ts +1 -0
- package/dist/server/operations/schedule-faults.js +32 -4
- package/dist/server/operations/schedule-rest-body.d.ts +1 -0
- package/dist/server/operations/schedule-rest-body.js +2 -1
- package/dist/server/operations/static-registrations.js +6 -0
- package/dist/server/operations/update-schedule.d.ts +2 -0
- package/dist/server/operations/update-schedule.js +4 -2
- package/dist/server/operations/workflow-catalog-operation-helpers.js +27 -0
- package/dist/server/rest-binding.d.ts +11 -0
- package/dist/server/runtime/task-dispatch-envelope.d.ts +41 -0
- package/dist/server/runtime/task-dispatch-envelope.js +54 -0
- package/dist/server/runtime/task-dispatch-revision.d.ts +29 -0
- package/dist/server/runtime/task-dispatch-revision.js +13 -0
- package/dist/server/runtime/task-dispatch.d.ts +3 -1
- package/dist/server/runtime/task-dispatch.js +20 -44
- package/dist/server/runtime/task-ledger-recovery.js +1 -1
- package/dist/server/runtime/task-ledger-runtime.d.ts +14 -2
- package/dist/server/runtime/task-ledger-runtime.js +5 -3
- package/dist/server/runtime/task-polling.js +9 -3
- package/dist/server/runtime/task-reconciliation.js +2 -1
- package/dist/server/runtime/websocket-worker.js +9 -0
- package/dist/server/task-ledger-codec.d.ts +28 -0
- package/dist/server/task-ledger-codec.js +7 -1
- package/dist/server/task-ledger-transition-helpers.js +1 -0
- package/dist/server/task-ledger-types.d.ts +9 -0
- package/dist/server/task-ledger.d.ts +1 -1
- package/dist/server/task-ledger.js +3 -1
- package/dist/server/task-queue-types.d.ts +2 -0
- package/dist/storage/bun-sql.js +135 -100
- package/dist/storage/catalog-keys.d.ts +19 -0
- package/dist/storage/catalog-keys.js +3 -1
- package/dist/storage/compressed-storage.js +1 -1
- package/dist/storage/generation-keys.d.ts +52 -0
- package/dist/storage/generation-keys.js +4 -0
- package/dist/storage/index.d.ts +76 -63
- package/dist/storage/interface.d.ts +24 -230
- package/dist/storage/interface.js +1 -1
- package/dist/storage/key-prefixes.d.ts +1 -1
- package/dist/storage/key-prefixes.js +2 -0
- package/dist/storage/lease-keys.d.ts +48 -0
- package/dist/storage/lease-keys.js +8 -0
- package/dist/storage/lmdb.d.ts +30 -2
- package/dist/storage/lmdb.js +1 -1
- package/dist/storage/memory.js +1 -1
- package/dist/storage/neon.js +3 -3
- package/dist/storage/node-sqlite.js +135 -100
- package/dist/storage/postgres.js +3 -3
- package/dist/storage/resolve.js +1 -1
- package/dist/storage/scoped-storage.js +1 -1
- package/dist/storage/signal-keys.d.ts +23 -0
- package/dist/storage/signal-keys.js +9 -0
- package/dist/storage/testing.js +1 -1
- package/dist/storage/turso.js +2 -2
- package/dist/storage/workflow-lifecycle-keys.d.ts +128 -0
- package/dist/storage/workflow-lifecycle-keys.js +46 -0
- package/dist/storage/workflow-record-keys.d.ts +98 -0
- package/dist/storage/workflow-record-keys.js +47 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/web-extension.js +1 -1
- package/dist/worker/protocol-messages.d.ts +17 -0
- package/dist/worker/protocol-schemas.d.ts +32 -0
- package/dist/worker/protocol-schemas.js +7 -3
- package/dist/worker/protocol-task-result.js +35 -3
- package/dist/worker/protocol.js +1 -1
- package/dist/worker/registry/types.d.ts +2 -0
- package/dist/worker/registry.d.ts +1 -1
- package/dist/worker/registry.js +3 -1
- package/dist/workers/workflow-runner.d.ts +9 -0
- package/dist/workers/workflow-runner.js +9 -2
- package/dist/workers/workflow-worker-entry.js +9 -4
- package/package.json +2 -2
|
@@ -17,7 +17,21 @@ async function isWorkflowTypeRegistered(storage, workflowId, isTypeRegistered) {
|
|
|
17
17
|
if (bytes === null)
|
|
18
18
|
return !1;
|
|
19
19
|
try {
|
|
20
|
-
|
|
20
|
+
const state = decodeWorkflowState(bytes);
|
|
21
|
+
return isTypeRegistered(state.type, state.revision);
|
|
22
|
+
} catch {
|
|
23
|
+
return !1;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
async function isWorkflowStillRunningAndEligible(storage, workflowId, isTypeRegistered) {
|
|
27
|
+
const bytes = await storage.get(KEYS.workflow(workflowId));
|
|
28
|
+
if (bytes === null)
|
|
29
|
+
return !1;
|
|
30
|
+
try {
|
|
31
|
+
const state = decodeWorkflowState(bytes);
|
|
32
|
+
if (state.status !== "running")
|
|
33
|
+
return !1;
|
|
34
|
+
return isTypeRegistered === void 0 || isTypeRegistered(state.type, state.revision);
|
|
21
35
|
} catch {
|
|
22
36
|
return !1;
|
|
23
37
|
}
|
|
@@ -39,14 +53,14 @@ export function createWorkflowClaimReclaimTarget(registry, storage, metrics, onR
|
|
|
39
53
|
throw error;
|
|
40
54
|
}
|
|
41
55
|
}
|
|
42
|
-
async function releaseClaimAcquiredWhileDisposing(workflowId) {
|
|
43
|
-
await registry.release(workflowId);
|
|
56
|
+
async function releaseClaimAcquiredWhileDisposing(workflowId, acquiredEpoch) {
|
|
57
|
+
await registry.release(workflowId, acquiredEpoch);
|
|
44
58
|
}
|
|
45
59
|
async function confirmStillRunningOrReleaseFreshClaim(workflowId, acquiredEpoch) {
|
|
46
|
-
if (await
|
|
60
|
+
if (await isWorkflowStillRunningAndEligible(storage, workflowId, isTypeRegistered))
|
|
47
61
|
return !0;
|
|
48
62
|
if (registry.currentEpoch(workflowId) === acquiredEpoch)
|
|
49
|
-
await registry.release(workflowId);
|
|
63
|
+
await registry.release(workflowId, acquiredEpoch);
|
|
50
64
|
return !1;
|
|
51
65
|
}
|
|
52
66
|
async function redriveAlreadyHeldClaim(workflowId, expectedEpoch) {
|
|
@@ -55,7 +69,7 @@ export function createWorkflowClaimReclaimTarget(registry, storage, metrics, onR
|
|
|
55
69
|
if (!await isWorkflowStillRunning(storage, workflowId)) {
|
|
56
70
|
pendingRedriveWorkflowIds.delete(workflowId);
|
|
57
71
|
if (registry.currentEpoch(workflowId) === expectedEpoch)
|
|
58
|
-
await registry.release(workflowId);
|
|
72
|
+
await registry.release(workflowId, expectedEpoch);
|
|
59
73
|
return { status: "not-eligible" };
|
|
60
74
|
}
|
|
61
75
|
if (disposing)
|
|
@@ -76,7 +90,7 @@ export function createWorkflowClaimReclaimTarget(registry, storage, metrics, onR
|
|
|
76
90
|
}
|
|
77
91
|
metrics.recordClaimAttempt("acquired");
|
|
78
92
|
if (disposing) {
|
|
79
|
-
await releaseClaimAcquiredWhileDisposing(workflowId);
|
|
93
|
+
await releaseClaimAcquiredWhileDisposing(workflowId, acquireResult.epoch);
|
|
80
94
|
return { status: "not-eligible" };
|
|
81
95
|
}
|
|
82
96
|
if (!await confirmStillRunningOrReleaseFreshClaim(workflowId, acquireResult.epoch))
|
|
@@ -86,7 +100,7 @@ export function createWorkflowClaimReclaimTarget(registry, storage, metrics, onR
|
|
|
86
100
|
async function handleTakeoverAcquired(workflowId, acquiredEpoch) {
|
|
87
101
|
metrics.recordClaimAttempt("takeover");
|
|
88
102
|
if (disposing) {
|
|
89
|
-
await releaseClaimAcquiredWhileDisposing(workflowId);
|
|
103
|
+
await releaseClaimAcquiredWhileDisposing(workflowId, acquiredEpoch);
|
|
90
104
|
return { status: "not-eligible" };
|
|
91
105
|
}
|
|
92
106
|
if (!await confirmStillRunningOrReleaseFreshClaim(workflowId, acquiredEpoch))
|
|
@@ -8,39 +8,43 @@
|
|
|
8
8
|
* fragments; this module is the thin, stateful layer around them that reads
|
|
9
9
|
* storage, executes `storageConditionalBatch`, and tracks — for every claim
|
|
10
10
|
* THIS engine currently holds — the exact epoch and holder bytes it last
|
|
11
|
-
* wrote
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* "never round-trip encode(decode(raw))" discipline.
|
|
11
|
+
* wrote, extracted from the fragment they were just written by rather than
|
|
12
|
+
* re-encoded, mirroring `lease-manager.ts`'s "never round-trip
|
|
13
|
+
* encode(decode(raw))" discipline.
|
|
15
14
|
*
|
|
16
15
|
* **Scope.** The unit itself: acquire, renew, release, takeover, and
|
|
17
16
|
* release-all — plus, additively, {@link WorkflowClaimRegistry.prepareAcquireFragment}
|
|
18
17
|
* and {@link WorkflowClaimRegistry.recordFoldedAcquire}, the two-step seam a
|
|
19
|
-
* caller uses to fold `acquire` into ITS OWN atomic enabling write
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
* decision to the caller — `takeover` also gates on a per-workflow-id
|
|
30
|
-
* anti-thrash cooldown ({@link WorkflowClaimTakeoverCooldown}). Still out of
|
|
31
|
-
* scope: `weft_workflow_claim_*` metrics, `wakeOwnershipCheck`, and external
|
|
32
|
-
* terminal-transition rotation (any engine may commit those unconditioned).
|
|
18
|
+
* caller uses to fold `acquire` into ITS OWN atomic enabling write instead of
|
|
19
|
+
* committing the fragment through this registry's own `acquire()`. Driving
|
|
20
|
+
* `renew`/the reclaim scan, and turning a lost `acquire`/`takeover` into
|
|
21
|
+
* `WorkflowClaimUnavailableError` for explicit single-workflow callers, stay
|
|
22
|
+
* each call site's responsibility — this registry never throws that error,
|
|
23
|
+
* and per the ADR background scanning never does either, so every method
|
|
24
|
+
* returns a discriminated result instead. `takeover` also gates on a
|
|
25
|
+
* per-workflow-id anti-thrash cooldown ({@link WorkflowClaimTakeoverCooldown}).
|
|
26
|
+
* Still out of scope: `weft_workflow_claim_*` metrics, `wakeOwnershipCheck`,
|
|
27
|
+
* and external terminal-transition rotation.
|
|
33
28
|
*
|
|
34
29
|
* **Renewal-vs-release serialization.** A per-workflow in-flight-renewal
|
|
35
30
|
* promise (mirroring `lease-manager.ts`'s single `inFlightRenewal`) lets
|
|
36
31
|
* `release()` await a renewal already in progress before reading the cached
|
|
37
|
-
* bytes it conditions on; otherwise both race the same holder bytes
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
32
|
+
* bytes it conditions on; otherwise both race the same holder bytes. A
|
|
33
|
+
* `releasing` set stops a NEW renewal from starting once release has begun,
|
|
34
|
+
* so a `renew()` arriving mid-release fails fast as `'not-held'`. A renewal
|
|
35
|
+
* that THROWS (transient, not a CAS-false result) is not a lost claim: it
|
|
36
|
+
* propagates leaving cached bytes untouched, and `release()`'s wait swallows
|
|
37
|
+
* it — a storage hiccup must not fail a terminal or shutdown release.
|
|
38
|
+
*
|
|
39
|
+
* **Epoch-guarded release (WFT-134).** `release(workflowId, expectedEpoch?)`
|
|
40
|
+
* takes an OPTIONAL epoch a caller itself acquired; without it, `release`
|
|
41
|
+
* drops whatever entry is CURRENTLY tracked — correct only when nothing else
|
|
42
|
+
* could have replaced the claim meanwhile. A caller undoing ITS OWN
|
|
43
|
+
* acquisition must pass that epoch: a `recordFoldedAcquire` replacement can
|
|
44
|
+
* land even during this call's own in-flight-renewal await, and
|
|
45
|
+
* `expectedEpoch` is re-checked against the freshly-read entry AFTER that
|
|
46
|
+
* await, so a stale pre-await snapshot never authorizes releasing a newer,
|
|
47
|
+
* live claim.
|
|
44
48
|
*
|
|
45
49
|
* @module core/engine/workflow-claim-registry
|
|
46
50
|
*/
|
|
@@ -56,10 +60,7 @@ export type WorkflowClaimRegistryOptions = {
|
|
|
56
60
|
getNow: () => number;
|
|
57
61
|
/** `workflowClaimTtl` (ms), resolved by `ownership-options.ts`. */
|
|
58
62
|
claimTtlMs: number;
|
|
59
|
-
/**
|
|
60
|
-
* `workflowClaimRenewInterval` (ms), resolved by `ownership-options.ts` —
|
|
61
|
-
* feeds `isWorkflowClaimExpired`'s grace term.
|
|
62
|
-
*/
|
|
63
|
+
/** `workflowClaimRenewInterval` (ms) — feeds `isWorkflowClaimExpired`'s grace term. */
|
|
63
64
|
claimRenewIntervalMs: number;
|
|
64
65
|
/** Operator-warning seam; defaults to `process.emitWarning` via {@link emitWorkflowClaimLostWarning}. */
|
|
65
66
|
warn?: EmitWorkflowLeaseWarning;
|
|
@@ -98,8 +99,8 @@ export type WorkflowClaimReleaseResult = {
|
|
|
98
99
|
};
|
|
99
100
|
/**
|
|
100
101
|
* A prepared, not-yet-committed `acquire` — the output of
|
|
101
|
-
* {@link WorkflowClaimRegistry.prepareAcquireFragment},
|
|
102
|
-
*
|
|
102
|
+
* {@link WorkflowClaimRegistry.prepareAcquireFragment}, merged into a
|
|
103
|
+
* caller's own atomic write and handed to
|
|
103
104
|
* {@link WorkflowClaimRegistry.recordFoldedAcquire} once that write commits.
|
|
104
105
|
*/
|
|
105
106
|
export type WorkflowClaimAcquirePreparation = {
|
|
@@ -130,8 +131,8 @@ export type WorkflowClaimTakeoverResult = {
|
|
|
130
131
|
};
|
|
131
132
|
/**
|
|
132
133
|
* Owns this engine's per-workflow ownership claims: reads storage, executes
|
|
133
|
-
* the pure transition fragments, and tracks the exact bytes
|
|
134
|
-
* every claim
|
|
134
|
+
* the pure transition fragments, and tracks the exact bytes last written for
|
|
135
|
+
* every held claim. See the module doc for scope.
|
|
135
136
|
*/
|
|
136
137
|
export declare class WorkflowClaimRegistry {
|
|
137
138
|
#private;
|
|
@@ -142,25 +143,20 @@ export declare class WorkflowClaimRegistry {
|
|
|
142
143
|
get engineId(): string;
|
|
143
144
|
/**
|
|
144
145
|
* Every workflow id this engine currently tracks a live claim for — active
|
|
145
|
-
* or parked. A defensive-copy snapshot
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
* internal map while iterating a snapshot they already took.
|
|
146
|
+
* or parked. A defensive-copy snapshot: callers (the claim-renewal task, an
|
|
147
|
+
* active-claims metrics gauge) must not observe mutations to this
|
|
148
|
+
* registry's internal map while iterating a snapshot they already took.
|
|
149
149
|
*/
|
|
150
150
|
listHeldWorkflowIds(): readonly string[];
|
|
151
151
|
/**
|
|
152
152
|
* Defensive copy of the epoch bytes this engine last wrote for
|
|
153
|
-
* `workflowId`, for fencing durable writes — `null` if untracked.
|
|
154
|
-
* a caller mutating the returned buffer cannot corrupt this registry's
|
|
155
|
-
* cached fencing token.
|
|
153
|
+
* `workflowId`, for fencing durable writes — `null` if untracked.
|
|
156
154
|
*/
|
|
157
155
|
currentEpochBytes(workflowId: string): Uint8Array | null;
|
|
158
156
|
/**
|
|
159
|
-
* `acquire`: always reads both keys fresh
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
* non-null; otherwise (a competitor raced in between the read and the
|
|
163
|
-
* write) re-reads to report the true current holder.
|
|
157
|
+
* `acquire`: always reads both keys fresh, then builds and attempts the
|
|
158
|
+
* fragment from what it read. On a lost CAS, resolves `heldBy` from the
|
|
159
|
+
* already-read holder bytes when non-null; otherwise re-reads.
|
|
164
160
|
*/
|
|
165
161
|
acquire(workflowId: string): Promise<WorkflowClaimAcquireResult>;
|
|
166
162
|
/**
|
|
@@ -168,60 +164,68 @@ export declare class WorkflowClaimRegistry {
|
|
|
168
164
|
* `acquire` fragment WITHOUT committing it or updating this registry's
|
|
169
165
|
* tracking — for a caller that folds the fragment into ITS OWN atomic
|
|
170
166
|
* enabling write instead of letting {@link acquire} commit it alone. The
|
|
171
|
-
* caller merges
|
|
172
|
-
*
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
-
* re-reads fresh bytes, so a stale epoch from an earlier attempt never
|
|
176
|
-
* dooms a later one.
|
|
167
|
+
* caller merges the fragment into its own operation list, commits ONE
|
|
168
|
+
* atomic `storageConditionalBatch`, and — ONLY on success — calls
|
|
169
|
+
* {@link recordFoldedAcquire} with this SAME preparation. Safe to call
|
|
170
|
+
* again on every retry: it always re-reads fresh bytes.
|
|
177
171
|
*/
|
|
178
172
|
prepareAcquireFragment(workflowId: string): Promise<WorkflowClaimAcquirePreparation>;
|
|
179
173
|
/**
|
|
180
174
|
* Install the tracking entry for a claim acquired via a FOLDED enabling
|
|
181
175
|
* write (see {@link prepareAcquireFragment}) — call ONLY after the
|
|
182
|
-
* caller's own atomic commit
|
|
183
|
-
*
|
|
184
|
-
*
|
|
185
|
-
* encode(decode(raw))" discipline every other grant path in this class
|
|
186
|
-
* uses.
|
|
176
|
+
* caller's own atomic commit of `preparation.fragment` has succeeded.
|
|
177
|
+
* Extracts the exact written bytes, the same "never round-trip
|
|
178
|
+
* encode(decode(raw))" discipline every other grant path here uses.
|
|
187
179
|
*/
|
|
188
180
|
recordFoldedAcquire(workflowId: string, preparation: WorkflowClaimAcquirePreparation): void;
|
|
189
181
|
/**
|
|
190
182
|
* `renew`: conditions on the exact holder bytes this engine last wrote.
|
|
191
183
|
* Concurrent calls for the same id share the one in-flight promise. A
|
|
192
184
|
* CAS-false result marks the claim lost locally and emits
|
|
193
|
-
* `WeftWorkflowClaimLostWarning
|
|
194
|
-
* touches any other tracked claim.
|
|
185
|
+
* `WeftWorkflowClaimLostWarning`.
|
|
195
186
|
*/
|
|
196
187
|
renew(workflowId: string): Promise<WorkflowClaimRenewResult>;
|
|
197
188
|
/**
|
|
198
189
|
* `release`: stops new renewals for `workflowId` and awaits any renewal
|
|
199
|
-
* already in flight (
|
|
200
|
-
*
|
|
201
|
-
*
|
|
202
|
-
*
|
|
203
|
-
*
|
|
204
|
-
*
|
|
205
|
-
*
|
|
190
|
+
* already in flight (best-effort — swallows a thrown rejection) before
|
|
191
|
+
* building the expected bytes, so the two never race the same holder
|
|
192
|
+
* bytes. Deletes only the holder key, never the epoch key, so a
|
|
193
|
+
* successor's next `acquire` reads the true prior epoch.
|
|
194
|
+
*
|
|
195
|
+
* `expectedEpoch`, when given, makes this a no-op (`'not-held'`) against
|
|
196
|
+
* any entry but the exact generation the caller means to undo — see the
|
|
197
|
+
* module doc's "Epoch-guarded release". A thrown storage error during the
|
|
198
|
+
* CAS still forgets the LOCAL entry (identity-guarded like the
|
|
199
|
+
* success/failure paths below) before rethrowing, so a caller giving up
|
|
200
|
+
* after a failed release does not leave the renewal task renewing forever
|
|
201
|
+
* (WFT-134).
|
|
206
202
|
*/
|
|
207
|
-
release(workflowId: string): Promise<WorkflowClaimReleaseResult>;
|
|
203
|
+
release(workflowId: string, expectedEpoch?: number): Promise<WorkflowClaimReleaseResult>;
|
|
208
204
|
/**
|
|
209
205
|
* `takeover`: reads the holder and epoch keys fresh, and only attempts the
|
|
210
206
|
* CAS once the holder is not live — either its grace-adjusted `expiresAt`
|
|
211
207
|
* has passed ({@link isWorkflowClaimExpired}), or the holder bytes are
|
|
212
208
|
* foreign/undecodable garbage no valid engine could have written (mirrors
|
|
213
|
-
* `lease-manager.ts`'s "garbage is not a live owner" treatment
|
|
214
|
-
*
|
|
215
|
-
*
|
|
216
|
-
*
|
|
217
|
-
* treated defensively as nothing safe to fence a takeover against.
|
|
209
|
+
* `lease-manager.ts`'s "garbage is not a live owner" treatment). A holder
|
|
210
|
+
* with no epoch key violates the write invariant — the two are always
|
|
211
|
+
* written together, and the epoch key is never deleted — so it is treated
|
|
212
|
+
* defensively as nothing safe to fence a takeover against.
|
|
218
213
|
*/
|
|
219
214
|
takeover(workflowId: string): Promise<WorkflowClaimTakeoverResult>;
|
|
220
215
|
/**
|
|
221
|
-
*
|
|
222
|
-
*
|
|
223
|
-
*
|
|
224
|
-
*
|
|
216
|
+
* Forget this engine's LOCAL tracking entry for `workflowId` — no durable
|
|
217
|
+
* write, just `this.#claims.delete`. Caller: `suspendWorkflow` (WFT-134),
|
|
218
|
+
* whose commit already durably deletes `wf-owner-holder:<id>`, leaving only
|
|
219
|
+
* this stale cache entry — else {@link currentEpoch} stays non-null for a
|
|
220
|
+
* gone holder, routing a same-engine `resume()` onto the stale-cache fast
|
|
221
|
+
* path in `acquireStandaloneClaimBeforeResume`. No-op on an untracked id;
|
|
222
|
+
* check {@link currentEpoch} first to avoid clobbering a newer generation.
|
|
223
|
+
*/
|
|
224
|
+
forgetLocalClaim(workflowId: string): void;
|
|
225
|
+
/**
|
|
226
|
+
* Best-effort release of every claim this engine tracks, for graceful
|
|
227
|
+
* shutdown. A failed release (thrown or lost-race) is swallowed per
|
|
228
|
+
* workflow — the reclaim scan later collects any stranded claim.
|
|
225
229
|
*/
|
|
226
230
|
releaseAll(): Promise<void>;
|
|
227
231
|
}
|
|
@@ -125,7 +125,7 @@ export class WorkflowClaimRegistry {
|
|
|
125
125
|
this.#claims.set(workflowId, { ...entry, holderBytes });
|
|
126
126
|
return { status: "renewed", workflowId };
|
|
127
127
|
}
|
|
128
|
-
async release(workflowId) {
|
|
128
|
+
async release(workflowId, expectedEpoch) {
|
|
129
129
|
this.#releasing.add(workflowId);
|
|
130
130
|
try {
|
|
131
131
|
const inFlight = this.#inFlightRenewals.get(workflowId);
|
|
@@ -136,11 +136,21 @@ export class WorkflowClaimRegistry {
|
|
|
136
136
|
const entry = this.#claims.get(workflowId);
|
|
137
137
|
if (entry === void 0)
|
|
138
138
|
return { status: "not-held", workflowId };
|
|
139
|
+
if (expectedEpoch !== void 0 && entry.epoch !== expectedEpoch)
|
|
140
|
+
return { status: "not-held", workflowId };
|
|
139
141
|
const fragment = buildWorkflowClaimReleaseTransition({
|
|
140
142
|
workflowId,
|
|
141
143
|
currentEpochBytes: entry.epochBytes,
|
|
142
144
|
currentHolderBytes: entry.holderBytes
|
|
143
|
-
})
|
|
145
|
+
});
|
|
146
|
+
let committed;
|
|
147
|
+
try {
|
|
148
|
+
committed = await storageConditionalBatch(this.#claimStorage, fragment.conditions, fragment.operations);
|
|
149
|
+
} catch (error) {
|
|
150
|
+
if (this.#claims.get(workflowId) === entry)
|
|
151
|
+
this.#claims.delete(workflowId);
|
|
152
|
+
throw error;
|
|
153
|
+
}
|
|
144
154
|
if (this.#claims.get(workflowId) === entry)
|
|
145
155
|
this.#claims.delete(workflowId);
|
|
146
156
|
return { status: committed ? "released" : "lost-race", workflowId };
|
|
@@ -187,6 +197,9 @@ export class WorkflowClaimRegistry {
|
|
|
187
197
|
this.#takeoverCooldown.clear(workflowId);
|
|
188
198
|
return { status: "acquired", workflowId, epoch };
|
|
189
199
|
}
|
|
200
|
+
forgetLocalClaim(workflowId) {
|
|
201
|
+
this.#claims.delete(workflowId);
|
|
202
|
+
}
|
|
190
203
|
async releaseAll() {
|
|
191
204
|
const workflowIds = [...this.#claims.keys()];
|
|
192
205
|
await Promise.all(workflowIds.map(async (workflowId) => {
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bump helpers for the durable per-workflow-id generation counter
|
|
3
|
+
* (`wf-gen:<id>`, WFT-153). See `storage/generation-keys.ts` for the
|
|
4
|
+
* keyspace's full rationale.
|
|
5
|
+
*
|
|
6
|
+
* Mirrors `workflow-claim-transitions.ts`'s epoch-rotation fragment shape
|
|
7
|
+
* (`nextEpochFromObservedBytes` / `buildWorkflowClaimExternalTerminalRotationTransition`)
|
|
8
|
+
* deliberately: both are "mint `(observed ?? 0) + 1`, never a literal"
|
|
9
|
+
* permanently-retained counters bumped in the same atomic batch as the
|
|
10
|
+
* transition that motivates the bump.
|
|
11
|
+
*
|
|
12
|
+
* @module core/engine/workflow-generation-fence
|
|
13
|
+
*/
|
|
14
|
+
import { type BatchOperation } from '../../storage/interface.ts';
|
|
15
|
+
import type { EngineInternals } from './internals.ts';
|
|
16
|
+
import type { WorkflowClaimTransitionFragment } from './workflow-claim-transitions.ts';
|
|
17
|
+
/** Mint the next generation from bytes just read: `(decode(bytes) ?? 0) + 1`, never a literal. */
|
|
18
|
+
export declare function nextGenerationFromObservedBytes(observedGenerationBytes: Uint8Array | null): number;
|
|
19
|
+
/**
|
|
20
|
+
* Build just the `wf-gen:<id>` bump PUT operation from an ALREADY-OBSERVED
|
|
21
|
+
* value — no storage read of its own. Used by the `onTerminalConflict:
|
|
22
|
+
* 'start-new'` restart path (`start-terminal-conflict-purge.ts`), which folds
|
|
23
|
+
* this operation into its create batch and relies on that batch's own outer
|
|
24
|
+
* `duplicateIdGenerationCondition` — built from the SAME observed bytes this
|
|
25
|
+
* function bumps from — as the CAS fence, rather than a second, independent
|
|
26
|
+
* condition here. Reusing one observed value for both the fence and the bump
|
|
27
|
+
* amount is what makes the restart's own CAS trivially self-consistent: it
|
|
28
|
+
* can never fence itself out on its own legitimate restart, because the
|
|
29
|
+
* value it bumps from is exactly the value its own precondition checks.
|
|
30
|
+
*/
|
|
31
|
+
export declare function buildWorkflowGenerationBumpOperation(workflowId: string, observedGenerationBytes: Uint8Array | null): BatchOperation;
|
|
32
|
+
/**
|
|
33
|
+
* Read the current `wf-gen:<id>` value, build its bump PUT operation and (CAS
|
|
34
|
+
* condition permitting) fold BOTH into `base` — the caller's own fragment, in
|
|
35
|
+
* `purgeWorkflow` the `wf-owner-epoch` rotation fragment
|
|
36
|
+
* `buildExternalTerminalRotationFragment` already built — for a STANDALONE
|
|
37
|
+
* purge/retention commit that, unlike the `'start-new'` restart path above,
|
|
38
|
+
* has no pre-existing duplicate-id-style condition of its own to piggyback
|
|
39
|
+
* on. Folding happens HERE, not at the call site, so `purgeWorkflow` stays a
|
|
40
|
+
* flat read-then-commit without its own merge step.
|
|
41
|
+
*
|
|
42
|
+
* RESOLVED (chatgpt-codex-connector review, WFT-153): this bump PUT was
|
|
43
|
+
* previously unconditioned, on the theory that a lost update between two
|
|
44
|
+
* concurrent purges of the same id was harmless — either purge's bump moves
|
|
45
|
+
* the value away from whatever an earlier duplicate-id read observed. That
|
|
46
|
+
* theory misses a THIRD purge landing in between: purge A reads generation
|
|
47
|
+
* N and (slowly) prepares a bump to N+1; purge B — a later purge of the
|
|
48
|
+
* SAME id, after the id was reused and purged again — reads the CURRENT
|
|
49
|
+
* value N+1 and commits a bump to N+2; if A's stale N+1 write then commits
|
|
50
|
+
* UNCONDITIONED, it overwrites B's N+2 with A's own N+1, rolling the
|
|
51
|
+
* "monotonic" counter backward. A cross-engine start that captured the
|
|
52
|
+
* intermediate absent/N+1 pair during the window before B's purge could then
|
|
53
|
+
* pass its `duplicateIdGenerationCondition` re-check after the rollback, even
|
|
54
|
+
* though a run genuinely executed and was purged in between — reopening the
|
|
55
|
+
* exact ABA this key exists to close.
|
|
56
|
+
*
|
|
57
|
+
* The condition folded in now closes that: `expectedValue` is the SAME
|
|
58
|
+
* `observedGenerationBytes` the bump amount is minted from, so a lost race
|
|
59
|
+
* (another purge already changed `wf-gen:<id>` since this read) fails the
|
|
60
|
+
* CAS instead of overwriting a newer generation with a stale one. Gated on
|
|
61
|
+
* `internals.storage.capabilities().conditionalBatch` (the same
|
|
62
|
+
* capability-conditioned pattern `buildWorkflowStateCommit` uses in
|
|
63
|
+
* `storage-io.ts`) rather than required unconditionally: a backend that
|
|
64
|
+
* honestly reports no `conditionalBatch` support keeps the pre-existing
|
|
65
|
+
* unconditioned bump (a residual, capability-limited ABA window, not a new
|
|
66
|
+
* regression) instead of newly requiring a capability purge never required
|
|
67
|
+
* before this fix — purge must keep working, degraded, on such backends.
|
|
68
|
+
*
|
|
69
|
+
* Runs under EVERY ownership mode — unlike the `wf-owner-epoch` rotation
|
|
70
|
+
* fragment, which is a no-op outside `ownership: 'workflow-lease'`, this
|
|
71
|
+
* always reads and bumps: the ABA hole it closes exists under `'none'` and
|
|
72
|
+
* `'lease'` too.
|
|
73
|
+
*/
|
|
74
|
+
export declare function foldWorkflowGenerationBumpForPurge(internals: EngineInternals, workflowId: string, base: WorkflowClaimTransitionFragment): Promise<WorkflowClaimTransitionFragment>;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { KEYS } from "../../storage/interface.js";
|
|
2
|
+
import { decodeGeneration, encodeGeneration } from "./generation-codec.js";
|
|
3
|
+
export function nextGenerationFromObservedBytes(observedGenerationBytes) {
|
|
4
|
+
return ((observedGenerationBytes === null ? null : decodeGeneration(observedGenerationBytes)) ?? 0) + 1;
|
|
5
|
+
}
|
|
6
|
+
export function buildWorkflowGenerationBumpOperation(workflowId, observedGenerationBytes) {
|
|
7
|
+
return {
|
|
8
|
+
type: "put",
|
|
9
|
+
key: KEYS.workflowGeneration(workflowId),
|
|
10
|
+
value: encodeGeneration(nextGenerationFromObservedBytes(observedGenerationBytes))
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
export async function foldWorkflowGenerationBumpForPurge(internals, workflowId, base) {
|
|
14
|
+
const key = KEYS.workflowGeneration(workflowId), observedGenerationBytes = await internals.storage.get(key), operation = buildWorkflowGenerationBumpOperation(workflowId, observedGenerationBytes), condition = internals.storage.capabilities().conditionalBatch ? { key, expectedValue: observedGenerationBytes } : void 0;
|
|
15
|
+
return {
|
|
16
|
+
operations: [...base.operations, operation],
|
|
17
|
+
conditions: base.conditions.concat(condition ?? [])
|
|
18
|
+
};
|
|
19
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { WorkflowState } from '../types.ts';
|
|
2
|
+
import type { EngineInternals } from './internals.ts';
|
|
3
|
+
/**
|
|
4
|
+
* The timestamp at which `state` becomes eligible for retention-driven
|
|
5
|
+
* purge, or `null` when it is non-terminal, has no applicable retention
|
|
6
|
+
* window, or (WFT-19 review round 2) its own pinned revision's retention
|
|
7
|
+
* policy cannot be resolved on this process right now — an unresolvable pin
|
|
8
|
+
* is NOT purge-eligible under the engine default; it is re-examined on a
|
|
9
|
+
* later sweep once the pin becomes resolvable, rather than risking an
|
|
10
|
+
* irreversible early purge against a shorter policy than the run's own.
|
|
11
|
+
* Falls back to the resolved dynamic definition's own retention policy for a
|
|
12
|
+
* `registerSource()`-registered type (see `resolveRetentionPolicyForState()`),
|
|
13
|
+
* then the engine-wide default for any other type — mirroring `retention.ts`'s
|
|
14
|
+
* `resolveWorkflowTypeRetention`.
|
|
15
|
+
*/
|
|
16
|
+
export declare function getWorkflowRetentionDeadline(internals: EngineInternals, state: WorkflowState): Promise<number | null>;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { DynamicWorkflowSourceUnavailableError } from "./dynamic-source-errors.js";
|
|
2
|
+
import {
|
|
3
|
+
getResolvedDynamicRegistration,
|
|
4
|
+
resolveExecutableRegistrationForRevision
|
|
5
|
+
} from "./dynamic-source-execution.js";
|
|
6
|
+
import { WorkflowRevisionUnavailableError } from "./revision-errors.js";
|
|
7
|
+
import { isTerminalWorkflowStatus, resolveRetentionForStatus } from "./validation.js";
|
|
8
|
+
async function resolveRetentionPolicyForState(internals, state) {
|
|
9
|
+
const eagerOrAlreadyResolved = getResolvedDynamicRegistration(internals, state.type, state.revision)?.retention;
|
|
10
|
+
if (eagerOrAlreadyResolved !== void 0)
|
|
11
|
+
return { kind: "resolved", policy: eagerOrAlreadyResolved };
|
|
12
|
+
if (!internals.sources.byName.has(state.type))
|
|
13
|
+
return { kind: "resolved", policy: void 0 };
|
|
14
|
+
try {
|
|
15
|
+
const { entry } = await resolveExecutableRegistrationForRevision(internals.engine, internals, state.type, state.revision);
|
|
16
|
+
return { kind: "resolved", policy: entry.retention };
|
|
17
|
+
} catch (error) {
|
|
18
|
+
if (error instanceof DynamicWorkflowSourceUnavailableError || error instanceof WorkflowRevisionUnavailableError)
|
|
19
|
+
return { kind: "unresolvable" };
|
|
20
|
+
throw error;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export async function getWorkflowRetentionDeadline(internals, state) {
|
|
24
|
+
if (!isTerminalWorkflowStatus(state.status))
|
|
25
|
+
return null;
|
|
26
|
+
const resolution = await resolveRetentionPolicyForState(internals, state);
|
|
27
|
+
if (resolution.kind === "unresolvable")
|
|
28
|
+
return null;
|
|
29
|
+
const policy = resolution.policy ?? internals.options.retention, retentionMs = resolveRetentionForStatus(policy, state.status);
|
|
30
|
+
if (retentionMs === void 0)
|
|
31
|
+
return null;
|
|
32
|
+
return state.updatedAt + retentionMs;
|
|
33
|
+
}
|
|
@@ -7,6 +7,7 @@ import type { SignalDeliveredEvent, SignalReceivedEvent } from './signal-events.
|
|
|
7
7
|
import type { AlertFiredEvent, AlertResolvedEvent, CheckpointSizeWarningEvent, CleanupWarningEvent, ConstraintViolatedEvent, DevelopmentWarningEvent, StorageSizeReportedEvent, WorkerConnectedEvent, WorkerDisconnectedEvent } from './system-events.ts';
|
|
8
8
|
import type { UpdateCompletedEvent, UpdateReceivedEvent } from './update-events.ts';
|
|
9
9
|
import type { WorkflowCancelledEvent, WorkflowCompletedEvent, WorkflowDefinitionRegisteredEvent, WorkflowFailedEvent, WorkflowRecoverySkippedEvent, WorkflowResumedEvent, WorkflowStartedEvent, WorkflowSuspendedEvent, WorkflowTeardownEvent, WorkflowTimedOutEvent } from './workflow-events.ts';
|
|
10
|
+
import type { WorkflowSourceLoadCancelledEvent, WorkflowSourceLoadFailedEvent, WorkflowSourceLoadReadyEvent, WorkflowSourceLoadStartedEvent } from './workflow-source-events.ts';
|
|
10
11
|
/**
|
|
11
12
|
* Record mapping each event-name string the {@link Engine} dispatches to its
|
|
12
13
|
* corresponding typed `Event` subclass.
|
|
@@ -63,6 +64,10 @@ export type WeftEventMap = {
|
|
|
63
64
|
'catalog:activation-rejected': WorkflowRevisionActivationRejectedEvent;
|
|
64
65
|
'catalog:revision-draining': WorkflowRevisionDrainingEvent;
|
|
65
66
|
'catalog:revision-removed': WorkflowRevisionRemovedEvent;
|
|
67
|
+
'workflow-source:load-started': WorkflowSourceLoadStartedEvent;
|
|
68
|
+
'workflow-source:load-ready': WorkflowSourceLoadReadyEvent;
|
|
69
|
+
'workflow-source:load-failed': WorkflowSourceLoadFailedEvent;
|
|
70
|
+
'workflow-source:load-cancelled': WorkflowSourceLoadCancelledEvent;
|
|
66
71
|
};
|
|
67
72
|
/**
|
|
68
73
|
* Typed version of the `EventTarget` interface that constrains
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import type { FailureCategory } from '../types/identity.ts';
|
|
2
|
+
type WorkflowSourceKind = import('../source/index.ts').WorkflowSourceKind;
|
|
3
|
+
/**
|
|
4
|
+
* Fired on the {@link Engine} when a dynamic workflow source's single-flight
|
|
5
|
+
* load for `(workflowType, revision)` starts — the loader is about to be
|
|
6
|
+
* invoked. Never fired for a cache hit (an already-installed revision
|
|
7
|
+
* `resolveWorkflowSource()` returns without invoking the loader).
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```ts
|
|
11
|
+
* import { Engine, WorkflowSourceLoadStartedEvent } from '@lostgradient/weft';
|
|
12
|
+
*
|
|
13
|
+
* const engine = new Engine();
|
|
14
|
+
* engine.addEventListener(WorkflowSourceLoadStartedEvent.type, (event) => {
|
|
15
|
+
* console.log('loading:', event.workflowType, event.revision);
|
|
16
|
+
* });
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
export declare class WorkflowSourceLoadStartedEvent extends Event {
|
|
20
|
+
static readonly type: "workflow-source:load-started";
|
|
21
|
+
readonly workflowType: string;
|
|
22
|
+
readonly revision: string;
|
|
23
|
+
readonly kind: WorkflowSourceKind;
|
|
24
|
+
constructor(workflowType: string, revision: string, kind: WorkflowSourceKind);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Fired on the {@link Engine} when a dynamic workflow source's load for
|
|
28
|
+
* `(workflowType, revision)` completes successfully — the loader ran,
|
|
29
|
+
* validation passed, and the manifest is durably installed.
|
|
30
|
+
* `loadDurationMs` is measured via the engine's own injected clock.
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```ts
|
|
34
|
+
* import { Engine, WorkflowSourceLoadReadyEvent } from '@lostgradient/weft';
|
|
35
|
+
*
|
|
36
|
+
* const engine = new Engine();
|
|
37
|
+
* engine.addEventListener(WorkflowSourceLoadReadyEvent.type, (event) => {
|
|
38
|
+
* console.log('ready:', event.workflowType, event.revision, event.loadDurationMs);
|
|
39
|
+
* });
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
42
|
+
export declare class WorkflowSourceLoadReadyEvent extends Event {
|
|
43
|
+
static readonly type: "workflow-source:load-ready";
|
|
44
|
+
readonly workflowType: string;
|
|
45
|
+
readonly revision: string;
|
|
46
|
+
readonly kind: WorkflowSourceKind;
|
|
47
|
+
readonly loadDurationMs: number;
|
|
48
|
+
constructor(workflowType: string, revision: string, kind: WorkflowSourceKind, loadDurationMs: number);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Fired on the {@link Engine} when a dynamic workflow source's load for
|
|
52
|
+
* `(workflowType, revision)` fails — the loader threw, validation rejected
|
|
53
|
+
* the loaded module, or the durable install failed. `failureCategory` is
|
|
54
|
+
* the closed, low-cardinality {@link FailureCategory} classification, safe
|
|
55
|
+
* to use as a metric label; the underlying error itself is never included.
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* ```ts
|
|
59
|
+
* import { Engine, WorkflowSourceLoadFailedEvent } from '@lostgradient/weft';
|
|
60
|
+
*
|
|
61
|
+
* const engine = new Engine();
|
|
62
|
+
* engine.addEventListener(WorkflowSourceLoadFailedEvent.type, (event) => {
|
|
63
|
+
* console.log('failed:', event.workflowType, event.revision, event.failureCategory);
|
|
64
|
+
* });
|
|
65
|
+
* ```
|
|
66
|
+
*/
|
|
67
|
+
export declare class WorkflowSourceLoadFailedEvent extends Event {
|
|
68
|
+
static readonly type: "workflow-source:load-failed";
|
|
69
|
+
readonly workflowType: string;
|
|
70
|
+
readonly revision: string;
|
|
71
|
+
readonly kind: WorkflowSourceKind;
|
|
72
|
+
readonly loadDurationMs: number;
|
|
73
|
+
readonly failureCategory: FailureCategory;
|
|
74
|
+
constructor(workflowType: string, revision: string, kind: WorkflowSourceKind, loadDurationMs: number, failureCategory: FailureCategory);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Fired on the {@link Engine} when the LAST outstanding
|
|
78
|
+
* `resolveWorkflowSource()` waiter for `(workflowType, revision)` releases
|
|
79
|
+
* (its own abort, or engine disposal) while the shared load is still
|
|
80
|
+
* unsettled. The shared load itself is never aborted — a fresh caller
|
|
81
|
+
* starting a new attempt for the same key re-fires
|
|
82
|
+
* {@link WorkflowSourceLoadStartedEvent}, not this event again for the
|
|
83
|
+
* orphaned attempt.
|
|
84
|
+
*
|
|
85
|
+
* @example
|
|
86
|
+
* ```ts
|
|
87
|
+
* import { Engine, WorkflowSourceLoadCancelledEvent } from '@lostgradient/weft';
|
|
88
|
+
*
|
|
89
|
+
* const engine = new Engine();
|
|
90
|
+
* engine.addEventListener(WorkflowSourceLoadCancelledEvent.type, (event) => {
|
|
91
|
+
* console.log('cancelled:', event.workflowType, event.revision);
|
|
92
|
+
* });
|
|
93
|
+
* ```
|
|
94
|
+
*/
|
|
95
|
+
export declare class WorkflowSourceLoadCancelledEvent extends Event {
|
|
96
|
+
static readonly type: "workflow-source:load-cancelled";
|
|
97
|
+
readonly workflowType: string;
|
|
98
|
+
readonly revision: string;
|
|
99
|
+
readonly kind: WorkflowSourceKind;
|
|
100
|
+
constructor(workflowType: string, revision: string, kind: WorkflowSourceKind);
|
|
101
|
+
}
|
|
102
|
+
export {};
|