@lostgradient/weft 0.20.0 → 0.21.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/README.md +2 -2
- package/dist/core/engine/activity-reconciliation.d.ts +2 -2
- package/dist/core/engine/activity-reconciliation.js +12 -8
- package/dist/core/engine/async-activity-completion.js +12 -4
- package/dist/core/engine/async-activity-records.d.ts +11 -1
- package/dist/core/engine/async-activity-records.js +4 -3
- package/dist/core/engine/attributes-tags.d.ts +11 -1
- package/dist/core/engine/attributes-tags.js +4 -3
- package/dist/core/engine/bulk-operations-purge.js +5 -2
- package/dist/core/engine/bulk-operations.js +17 -5
- package/dist/core/engine/callback-creators-bundles.js +1 -1
- package/dist/core/engine/callback-creators-core.js +4 -2
- package/dist/core/engine/checkpoint-io.js +1 -1
- package/dist/core/engine/child-workflow.js +2 -1
- package/dist/core/engine/completed-review-storage.js +1 -1
- package/dist/core/engine/condition-waiters.d.ts +45 -3
- package/dist/core/engine/condition-waiters.js +12 -3
- package/dist/core/engine/disposal.js +6 -0
- package/dist/core/engine/engine-internal-types.d.ts +12 -4
- package/dist/core/engine/fenced-write.d.ts +46 -15
- package/dist/core/engine/fenced-write.js +39 -6
- package/dist/core/engine/finalizer-state.d.ts +3 -1
- package/dist/core/engine/handle-result.d.ts +49 -1
- package/dist/core/engine/handle-result.js +95 -20
- package/dist/core/engine/index.d.ts +13 -5
- package/dist/core/engine/index.js +155 -5
- package/dist/core/engine/inline-launch-queue.js +9 -0
- package/dist/core/engine/internals.d.ts +73 -0
- package/dist/core/engine/invoke-update-handler.d.ts +19 -0
- package/dist/core/engine/invoke-update-handler.js +7 -0
- package/dist/core/engine/lease-deposition.d.ts +132 -0
- package/dist/core/engine/lease-deposition.js +31 -0
- package/dist/core/engine/lease-errors.d.ts +80 -9
- package/dist/core/engine/lease-errors.js +28 -2
- package/dist/core/engine/lifecycle/resume.js +3 -1
- package/dist/core/engine/lifecycle/standalone-claim-acquire.d.ts +33 -0
- package/dist/core/engine/lifecycle/standalone-claim-acquire.js +25 -0
- package/dist/core/engine/lifecycle/start-commit.js +20 -6
- package/dist/core/engine/lifecycle/transition.d.ts +15 -1
- package/dist/core/engine/lifecycle/transition.js +6 -3
- package/dist/core/engine/operations-activity.js +1 -1
- package/dist/core/engine/operations-time.js +21 -7
- package/dist/core/engine/owner-side-signal-poll.d.ts +132 -0
- package/dist/core/engine/owner-side-signal-poll.js +22 -0
- package/dist/core/engine/owner-side-update-poll.d.ts +116 -0
- package/dist/core/engine/owner-side-update-poll.js +22 -0
- package/dist/core/engine/ownership-bootstrap.d.ts +280 -0
- package/dist/core/engine/ownership-bootstrap.js +89 -0
- package/dist/core/engine/ownership-mode-marker.d.ts +82 -0
- package/dist/core/engine/ownership-mode-marker.js +52 -0
- package/dist/core/engine/ownership-options.d.ts +22 -6
- package/dist/core/engine/ownership-options.js +42 -12
- package/dist/core/engine/pending-updates.js +35 -24
- package/dist/core/engine/queries.d.ts +70 -0
- package/dist/core/engine/queries.js +58 -4
- package/dist/core/engine/schedules.js +1 -1
- package/dist/core/engine/signals.d.ts +11 -1
- package/dist/core/engine/signals.js +11 -7
- package/dist/core/engine/sleep-timer-acknowledgements.d.ts +68 -0
- package/dist/core/engine/sleep-timer-acknowledgements.js +19 -0
- package/dist/core/engine/storage-io.d.ts +58 -10
- package/dist/core/engine/storage-io.js +27 -9
- package/dist/core/engine/termination/cleanup.d.ts +5 -4
- package/dist/core/engine/termination/complete.js +23 -4
- package/dist/core/engine/termination/finalizer-claim.js +4 -4
- package/dist/core/engine/termination/state-commit-callbacks.d.ts +15 -0
- package/dist/core/engine/termination/state-commit-callbacks.js +0 -0
- package/dist/core/engine/termination/suspend.js +1 -1
- package/dist/core/engine/update-validation.d.ts +28 -0
- package/dist/core/engine/update-validation.js +37 -0
- package/dist/core/engine/updates.d.ts +17 -16
- package/dist/core/engine/updates.js +24 -47
- package/dist/core/engine/wake-ownership-check.d.ts +86 -0
- package/dist/core/engine/wake-ownership-check.js +19 -0
- package/dist/core/engine/wake-ownership-guard.d.ts +46 -0
- package/dist/core/engine/wake-ownership-guard.js +23 -0
- package/dist/core/engine/workflow-claim-codec.d.ts +64 -0
- package/dist/core/engine/workflow-claim-codec.js +44 -0
- package/dist/core/engine/workflow-claim-cooldown.d.ts +60 -0
- package/dist/core/engine/workflow-claim-cooldown.js +17 -0
- package/dist/core/engine/workflow-claim-fold.d.ts +82 -0
- package/dist/core/engine/workflow-claim-fold.js +53 -0
- package/dist/core/engine/workflow-claim-metrics.d.ts +131 -0
- package/dist/core/engine/workflow-claim-metrics.js +62 -0
- package/dist/core/engine/workflow-claim-reclaim-scan.d.ts +96 -0
- package/dist/core/engine/workflow-claim-reclaim-scan.js +66 -0
- package/dist/core/engine/workflow-claim-reclaim-target.d.ts +107 -0
- package/dist/core/engine/workflow-claim-reclaim-target.js +155 -0
- package/dist/core/engine/workflow-claim-registry.d.ts +227 -0
- package/dist/core/engine/workflow-claim-registry.js +198 -0
- package/dist/core/engine/workflow-claim-renewal-interval.d.ts +50 -0
- package/dist/core/engine/workflow-claim-renewal-interval.js +132 -0
- package/dist/core/engine/workflow-claim-renewal-subpasses.d.ts +265 -0
- package/dist/core/engine/workflow-claim-renewal-subpasses.js +104 -0
- package/dist/core/engine/workflow-claim-renewal-task.d.ts +159 -0
- package/dist/core/engine/workflow-claim-renewal-task.js +65 -0
- package/dist/core/engine/workflow-claim-transitions.d.ts +186 -0
- package/dist/core/engine/workflow-claim-transitions.js +120 -0
- package/dist/core/inline-execution-strategy.d.ts +20 -0
- package/dist/core/inline-execution-strategy.js +15 -4
- package/dist/core/scheduler/scheduler-class.js +3 -3
- package/dist/core/scheduler/timer-sources.d.ts +13 -0
- package/dist/core/scheduler/timer-sources.js +1 -1
- package/dist/core/types/options.d.ts +17 -1
- package/dist/core/weft-error.d.ts +1 -1
- package/dist/core/weft-error.js +2 -1
- package/dist/http.js +2 -2
- package/dist/index.d.ts +2 -1
- package/dist/index.js +8 -0
- package/dist/indexeddb.js +1 -1
- package/dist/json-schema.js +3 -3
- package/dist/observability/index.js +2 -2
- package/dist/storage/auto.js +1 -1
- package/dist/storage/bun-sql.js +113 -0
- package/dist/storage/compressed-storage.js +1 -1
- package/dist/storage/index.d.ts +38 -35
- package/dist/storage/interface.d.ts +87 -125
- package/dist/storage/interface.js +1 -1
- package/dist/storage/key-encoding.d.ts +51 -0
- package/dist/storage/key-encoding.js +13 -0
- package/dist/storage/key-prefixes.d.ts +1 -1
- package/dist/storage/key-prefixes.js +3 -0
- package/dist/storage/lmdb.js +1 -1
- package/dist/storage/memory.js +1 -1
- package/dist/storage/neon.js +2 -2
- package/dist/storage/node-sqlite.js +113 -0
- package/dist/storage/ownership-keys.d.ts +55 -0
- package/dist/storage/ownership-keys.js +6 -0
- package/dist/storage/postgres.js +2 -2
- package/dist/storage/resolve.js +1 -1
- package/dist/storage/scoped-storage.js +1 -1
- package/dist/storage/testing.js +1 -1
- package/dist/storage/turso.js +2 -2
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/web-extension.js +1 -1
- package/dist/worker/protocol.js +1 -1
- package/package.json +1 -1
|
@@ -11,6 +11,7 @@ import { validateSignalId } from "../signal-id.js";
|
|
|
11
11
|
import { commitAnonymousSignalOperations } from "./anonymous-signal-sequence.js";
|
|
12
12
|
import { stageAtomicWorkflowCommitSideEffects } from "./checkpoint-side-effects.js";
|
|
13
13
|
import { isTerminalWorkflowStatus } from "./validation.js";
|
|
14
|
+
import { confirmWakeOwnership } from "./wake-ownership-guard.js";
|
|
14
15
|
const EMPTY_STORAGE_VALUE = new Uint8Array(0), SIGNAL_ACCEPTED_RESPONSE = { ok: !0 }, SIGNAL_KEY_COMPONENT_COUNT = 5;
|
|
15
16
|
export async function signal(internals, workflowId, name, payload, callbacks, options = {}) {
|
|
16
17
|
const deliverSignal = async (targetWorkflowId, signalName, signalPayload) => {
|
|
@@ -44,11 +45,12 @@ export async function signal(internals, workflowId, name, payload, callbacks, op
|
|
|
44
45
|
export function releaseSignalWaiter(internals, workflowId, waiterKey, expectedResolve) {
|
|
45
46
|
const currentWaiter = internals.signalWaiters.get(waiterKey);
|
|
46
47
|
if (!currentWaiter)
|
|
47
|
-
return;
|
|
48
|
+
return !1;
|
|
48
49
|
if (expectedResolve && currentWaiter !== expectedResolve)
|
|
49
|
-
return;
|
|
50
|
+
return !1;
|
|
50
51
|
internals.signalWaiters.delete(waiterKey);
|
|
51
52
|
untrackWaiterKey(internals.signalWaitersByWorkflow, workflowId, waiterKey);
|
|
53
|
+
return !0;
|
|
52
54
|
}
|
|
53
55
|
export async function bufferSignalPayloads(internals, workflowId, deliveries, callbacks, defaultOptions = {}) {
|
|
54
56
|
if (deliveries.length === 0)
|
|
@@ -69,11 +71,11 @@ export async function bufferSignalPayloads(internals, workflowId, deliveries, ca
|
|
|
69
71
|
if (!await storageConditionalBatch(internals.storage, [{ key: acceptedResponseKey, expectedValue: null }], [...operations, { type: "put", key: acceptedResponseKey, value: acceptedResponse }]))
|
|
70
72
|
return;
|
|
71
73
|
markTerminalCleanupTracked(internals, workflowId);
|
|
72
|
-
deliverBufferedSignals(internals, workflowId, deliveries, callbacks);
|
|
74
|
+
await deliverBufferedSignals(internals, workflowId, deliveries, callbacks);
|
|
73
75
|
return;
|
|
74
76
|
}
|
|
75
77
|
await commitAnonymousSignalOperations(internals, workflowId, deliveries, (operations) => appendTerminalCleanupOperation(internals, workflowId, operations), () => markTerminalCleanupTracked(internals, workflowId));
|
|
76
|
-
deliverBufferedSignals(internals, workflowId, deliveries, callbacks);
|
|
78
|
+
await deliverBufferedSignals(internals, workflowId, deliveries, callbacks);
|
|
77
79
|
}
|
|
78
80
|
function createExplicitSignalOperations(internals, workflowId, deliveries, signalId) {
|
|
79
81
|
return deliveries.map(({ signalName, payload }) => ({
|
|
@@ -109,7 +111,7 @@ function appendTerminalCleanupOperation(internals, workflowId, operations) {
|
|
|
109
111
|
function markTerminalCleanupTracked(internals, workflowId) {
|
|
110
112
|
internals.workflowsNeedingTerminalCleanup.add(workflowId);
|
|
111
113
|
}
|
|
112
|
-
function deliverBufferedSignals(internals, workflowId, deliveries, callbacks) {
|
|
114
|
+
async function deliverBufferedSignals(internals, workflowId, deliveries, callbacks) {
|
|
113
115
|
let shouldResumeParkedWorkflow = !1;
|
|
114
116
|
for (const { signalName, payload, options } of deliveries) {
|
|
115
117
|
if (options?.emitPublicEvent !== !1) {
|
|
@@ -118,8 +120,10 @@ function deliverBufferedSignals(internals, workflowId, deliveries, callbacks) {
|
|
|
118
120
|
}
|
|
119
121
|
const waiterKey = `${workflowId}:${signalName}`, waiter = internals.signalWaiters.get(waiterKey);
|
|
120
122
|
if (waiter) {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
+
if (internals.workflowClaimRegistry !== null && await confirmWakeOwnership(internals, workflowId, "signal") === "discard")
|
|
124
|
+
continue;
|
|
125
|
+
if (releaseSignalWaiter(internals, workflowId, waiterKey, waiter))
|
|
126
|
+
waiter();
|
|
123
127
|
continue;
|
|
124
128
|
}
|
|
125
129
|
if (internals.parkedInlineWorkflows.has(workflowId))
|
|
@@ -5,7 +5,75 @@ export type SleepTimerAcknowledgement = {
|
|
|
5
5
|
cancel: () => void;
|
|
6
6
|
promise: Promise<void>;
|
|
7
7
|
};
|
|
8
|
+
/**
|
|
9
|
+
* Durable sleep timers fire globally — under `ownership: 'workflow-lease'`
|
|
10
|
+
* every engine sharing the store observes the same expired timer, not only
|
|
11
|
+
* the workflow's owner (see ADR 0002's entry-point classification: the
|
|
12
|
+
* scheduler dispatch shell is claim-acquiring only for claim-ACQUIRING
|
|
13
|
+
* branches; this one is claim-REQUIRING and checks for itself). Running
|
|
14
|
+
* `confirmWakeOwnership` FIRST — before `shouldIgnoreUnclaimedSleepTimer`
|
|
15
|
+
* even loads workflow state — matters: on a non-owning engine that state
|
|
16
|
+
* legitimately reads `'running'` (the true owner is actively driving it),
|
|
17
|
+
* which would otherwise hit `shouldIgnoreUnclaimedSleepTimer`'s "fired
|
|
18
|
+
* before ready" throw meant for a same-engine registration race, not a
|
|
19
|
+
* cross-engine ownership miss.
|
|
20
|
+
*
|
|
21
|
+
* A discard is NOT a silent no-op: `handleTimerFired`'s caller — the
|
|
22
|
+
* `Scheduler` — treats a callback that returns without throwing as
|
|
23
|
+
* "processed" and durably deletes the fired timer key
|
|
24
|
+
* (`commitTimerCleanup`, engine-scoped and unfenced on any single workflow's
|
|
25
|
+
* claim, since one tick's cleanup batch can span fired timers from many
|
|
26
|
+
* workflows — see `src/core/engine/index.ts`'s `Scheduler` wiring). If a
|
|
27
|
+
* discarding non-owner let that deletion proceed, it would delete the true
|
|
28
|
+
* owner's only durable record of this fire before the owner ever observes
|
|
29
|
+
* it — see `retainDiscardedDurableTimer` below.
|
|
30
|
+
*/
|
|
8
31
|
export declare function handleSleepTimerWithAcknowledgement(internals: EngineInternals, entry: TimerEntry, loadWorkflowState: (workflowId: string) => Promise<WorkflowState | null>): Promise<void>;
|
|
32
|
+
/** What to do with a durable timer key once a claim-requiring wake has discarded it. */
|
|
33
|
+
export type DiscardedTimerDisposition = 'retain' | 'collect';
|
|
34
|
+
/**
|
|
35
|
+
* Decide what a discarded claim-requiring timer fire (ADR 0002's `sleep` and
|
|
36
|
+
* `wait-condition` wake kinds — see `operations-time.ts`'s
|
|
37
|
+
* `resolveConditionTimer` for the second caller) should do with its durable
|
|
38
|
+
* timer key. Shared by both wake kinds because they share the exact same
|
|
39
|
+
* hazard: `commitTimerCleanup` batches deletes across many workflows in one
|
|
40
|
+
* engine-scoped, unfenced write, so a non-owner that discards must not let
|
|
41
|
+
* the Scheduler treat the fire as "processed" while some other engine still
|
|
42
|
+
* needs the same durable record to perform the real wake.
|
|
43
|
+
*
|
|
44
|
+
* "Not locally owned" is not by itself proof the timer is stale, though —
|
|
45
|
+
* `resolveSleepTimer`'s own comment documents that a durable sleep timer
|
|
46
|
+
* OUTLIVES terminal cleanup (cleanup only drops the in-memory resolver, not
|
|
47
|
+
* the durable key). Blindly retaining on every discard would turn an
|
|
48
|
+
* orphaned post-terminal timer into an immortal one: every engine sharing
|
|
49
|
+
* the store would rediscover it, discard it, and retain it again on every
|
|
50
|
+
* Scheduler tick forever. So this reads the workflow's CURRENT persisted
|
|
51
|
+
* status to disambiguate:
|
|
52
|
+
* - `null`, or a terminal status ({@link isTerminalWorkflowStatus}):
|
|
53
|
+
* `'collect'` — no engine holds or will ever again acquire a claim for
|
|
54
|
+
* this workflow, so nothing will ever consume this fire; let the
|
|
55
|
+
* Scheduler's normal cleanup remove the orphaned key, matching
|
|
56
|
+
* pre-ADR-0002 behavior for an unclaimed timer.
|
|
57
|
+
* - `'suspended'`: `'collect'` — `engine.suspend()`'s durable re-arm
|
|
58
|
+
* establishes its own fresh timer on resume; the pre-suspend fire being
|
|
59
|
+
* discarded here is not the one that wakes the resumed run.
|
|
60
|
+
* - any other status (`'running'`, `'pending'`): `'retain'` — some engine
|
|
61
|
+
* still holds, or will still acquire, a live claim for this workflow;
|
|
62
|
+
* leave the durable key so that engine's own copy of this same fire
|
|
63
|
+
* performs the real wake and deletes it for real, bounded by that
|
|
64
|
+
* engine's own next Scheduler poll.
|
|
65
|
+
*/
|
|
66
|
+
export declare function resolveDiscardedTimerDisposition(workflowId: string, loadWorkflowState: (workflowId: string) => Promise<WorkflowState | null>): Promise<DiscardedTimerDisposition>;
|
|
67
|
+
/**
|
|
68
|
+
* Apply {@link resolveDiscardedTimerDisposition} to a discarded claim-requiring
|
|
69
|
+
* timer fire: throws when the durable key must be retained, so the
|
|
70
|
+
* `Scheduler`'s `#processSelectedTimer` catch block treats this fire as
|
|
71
|
+
* `'retry'` — leaving the timer key in storage instead of collecting it —
|
|
72
|
+
* and resolves normally when the workflow is gone, terminal, or suspended,
|
|
73
|
+
* so a genuinely orphaned timer is still collected exactly as it was before
|
|
74
|
+
* this ownership check existed.
|
|
75
|
+
*/
|
|
76
|
+
export declare function retainDiscardedDurableTimer(timerId: string, workflowId: string, loadWorkflowState: (workflowId: string) => Promise<WorkflowState | null>): Promise<void>;
|
|
9
77
|
export declare function createSleepTimerAcknowledgement(internals: EngineInternals, workflowId: string, operationId: string, fireAt: number): SleepTimerAcknowledgement;
|
|
10
78
|
export declare function recordDurableInlineOperation(internals: EngineInternals, workflowId: string, operation: ContextOperationRequest): void;
|
|
11
79
|
export declare function settleSleepTimerAcknowledgements(internals: EngineInternals, workflowId: string, disposition: 'suspended' | 'terminal'): void;
|
|
@@ -1,4 +1,10 @@
|
|
|
1
|
+
import { isTerminalWorkflowStatus } from "./validation.js";
|
|
2
|
+
import { confirmWakeOwnership } from "./wake-ownership-guard.js";
|
|
1
3
|
export async function handleSleepTimerWithAcknowledgement(internals, entry, loadWorkflowState) {
|
|
4
|
+
if (await confirmWakeOwnership(internals, entry.workflowId, "sleep") === "discard") {
|
|
5
|
+
await retainDiscardedDurableTimer(entry.id, entry.workflowId, loadWorkflowState);
|
|
6
|
+
return;
|
|
7
|
+
}
|
|
2
8
|
const operationId = entry.id.replace("sleep:", "");
|
|
3
9
|
if (await shouldIgnoreUnclaimedSleepTimer(internals, entry, operationId, loadWorkflowState))
|
|
4
10
|
return;
|
|
@@ -9,6 +15,19 @@ export async function handleSleepTimerWithAcknowledgement(internals, entry, load
|
|
|
9
15
|
}
|
|
10
16
|
await acknowledgement?.promise;
|
|
11
17
|
}
|
|
18
|
+
export async function resolveDiscardedTimerDisposition(workflowId, loadWorkflowState) {
|
|
19
|
+
const state = await loadWorkflowState(workflowId);
|
|
20
|
+
if (state === null)
|
|
21
|
+
return "collect";
|
|
22
|
+
if (state.status === "suspended" || isTerminalWorkflowStatus(state.status))
|
|
23
|
+
return "collect";
|
|
24
|
+
return "retain";
|
|
25
|
+
}
|
|
26
|
+
export async function retainDiscardedDurableTimer(timerId, workflowId, loadWorkflowState) {
|
|
27
|
+
if (await resolveDiscardedTimerDisposition(workflowId, loadWorkflowState) !== "retain")
|
|
28
|
+
return;
|
|
29
|
+
throw Error(`Durable timer "${timerId}" for workflow "${workflowId}" was discarded by a non-owning engine under ownership: 'workflow-lease'; retaining it in storage for the true owner instead of letting the scheduler delete it.`);
|
|
30
|
+
}
|
|
12
31
|
async function shouldIgnoreUnclaimedSleepTimer(internals, entry, operationId, loadWorkflowState) {
|
|
13
32
|
const resolverKey = `${entry.workflowId}:${operationId}`;
|
|
14
33
|
if (internals.sleepResolvers.has(resolverKey) || internals.inlineStrategy === null)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type BatchOperation } from '../../storage/interface.ts';
|
|
2
2
|
import type { ScheduleState, WorkflowState } from '../types.ts';
|
|
3
3
|
import type { EngineInternals } from './internals.ts';
|
|
4
|
+
import { type WorkflowClaimTransitionFragment } from './workflow-claim-transitions.ts';
|
|
4
5
|
/** Run a workflow-state write after any earlier write for that workflow has settled. */
|
|
5
6
|
export declare function runSerializedWorkflowStateWrite<TResult>(internals: EngineInternals, workflowId: string, writeOperation: () => Promise<TResult>): Promise<TResult>;
|
|
6
7
|
/**
|
|
@@ -15,23 +16,70 @@ export declare function runSerializedWorkflowStateWrite<TResult>(internals: Engi
|
|
|
15
16
|
export declare function runSerializedScheduleStateOperation<TResult>(internals: EngineInternals, scheduleId: string, operation: () => Promise<TResult>): Promise<TResult>;
|
|
16
17
|
/** Load and decode persisted workflow state by workflow ID. */
|
|
17
18
|
export declare function loadWorkflowState(internals: EngineInternals, workflowId: string): Promise<WorkflowState | null>;
|
|
19
|
+
/**
|
|
20
|
+
* Derive a terminal result (or throw the persisted terminal error) from an
|
|
21
|
+
* ALREADY-LOADED `WorkflowState` — pure, no storage read. Callers that hold a
|
|
22
|
+
* state snapshot they have already validated as terminal (e.g.
|
|
23
|
+
* `bootstrapWorkflowResultResolver`) must use this instead of
|
|
24
|
+
* {@link loadWorkflowResult}: a second independent `loadWorkflowState` read
|
|
25
|
+
* can observe a DIFFERENT run than the one the caller validated, if
|
|
26
|
+
* `onTerminalConflict: 'start-new'` replaces the workflow between the two
|
|
27
|
+
* reads — attributing a replacement run's result (or a spurious "still
|
|
28
|
+
* running") to a waiter that was made terminal by the original run.
|
|
29
|
+
*/
|
|
30
|
+
export declare function deriveWorkflowResultFromState(state: WorkflowState): unknown;
|
|
18
31
|
/** Load a terminal workflow result or throw the persisted terminal error. */
|
|
19
32
|
export declare function loadWorkflowResult(internals: EngineInternals, workflowId: string): Promise<unknown>;
|
|
20
33
|
type WorkflowStateCommitOptions = {
|
|
21
34
|
includePendingAtomicSideEffects?: boolean;
|
|
22
35
|
};
|
|
23
36
|
/**
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
37
|
+
* Build the `wf-owner-epoch:<id>` / `wf-owner-holder:<id>` ROTATION fragment
|
|
38
|
+
* for an EXTERNAL terminal transition — cancel, timeout, suspend, purge (ADR
|
|
39
|
+
* 0002 § "External terminal transitions must rotate the epoch"). Any engine
|
|
40
|
+
* may commit these against a workflow it does not own; deleting the holder
|
|
41
|
+
* alone is not sufficient to make that safe, so the epoch is rotated in the
|
|
42
|
+
* SAME atomic batch that writes the terminal/suspended state, deposing a
|
|
43
|
+
* still-running owner — its next write carries the now-stale epoch and loses
|
|
44
|
+
* its CAS. Meant to be folded (via `[...fragment.operations]` /
|
|
45
|
+
* `[...fragment.conditions]`) into the caller's own operations/conditions,
|
|
46
|
+
* never committed standalone.
|
|
47
|
+
*
|
|
48
|
+
* Under `ownership: 'none'` or `'lease'` this returns an EMPTY fragment with
|
|
49
|
+
* NO storage read: the `wf-owner-*` keyspace is not in play under those
|
|
50
|
+
* modes, and this function must not touch it — that is what keeps external
|
|
51
|
+
* terminal commits byte-for-byte unchanged there.
|
|
52
|
+
*/
|
|
53
|
+
export declare function buildExternalTerminalRotationFragment(internals: EngineInternals, workflowId: string): Promise<WorkflowClaimTransitionFragment>;
|
|
54
|
+
/**
|
|
55
|
+
* Commit a SELF-transition workflow-state advance (complete, fail): this
|
|
56
|
+
* engine is finishing its OWN workflow, so the write is workflow-scoped and
|
|
57
|
+
* fenced on THIS engine's claim epoch under `ownership: 'workflow-lease'`
|
|
58
|
+
* (via {@link commitFencedEngineWrite}'s `workflowId` parameter), or the
|
|
59
|
+
* global lease epoch under `ownership: 'lease'`. A deposed engine's write
|
|
60
|
+
* loses its CAS instead of corrupting the successor's state; the deposition
|
|
61
|
+
* is detected and the engine (or, under `workflow-lease`, just this one
|
|
62
|
+
* workflow) halts. Under `ownership: 'none'` this is byte-for-byte the
|
|
63
|
+
* pre-ADR commit shape. Never rotates `wf-owner-epoch:<id>` — that is the
|
|
64
|
+
* external-transition shape below. Operator/external mutations
|
|
65
|
+
* (search-attribute and tag edits) do NOT use this helper; they batch
|
|
66
|
+
* directly and are intentionally never fenced.
|
|
67
|
+
*/
|
|
68
|
+
export declare function commitSelfWorkflowStateOperations(internals: EngineInternals, state: WorkflowState, operations: BatchOperation[], options?: WorkflowStateCommitOptions): Promise<void>;
|
|
69
|
+
/**
|
|
70
|
+
* Commit an EXTERNAL terminal workflow-state transition — cancel, timeout, or
|
|
71
|
+
* suspend (ADR 0002 § "External terminal transitions must rotate the
|
|
72
|
+
* epoch"). ANY engine may commit these against a workflow it does not own, so
|
|
73
|
+
* — unlike {@link commitSelfWorkflowStateOperations} — this is never fenced
|
|
74
|
+
* on this engine's own workflow claim (`workflowId: null` is passed to
|
|
75
|
+
* {@link commitFencedEngineWrite}). Instead {@link
|
|
76
|
+
* buildExternalTerminalRotationFragment} folds a claim ROTATION into the same
|
|
77
|
+
* atomic batch under `ownership: 'workflow-lease'`, deposing a still-running
|
|
78
|
+
* owner. Under `ownership: 'lease'` the write still carries the global lease
|
|
79
|
+
* epoch condition (via `commitFencedEngineWrite`'s `workflowId: null` path);
|
|
80
|
+
* under `'none'` it is byte-for-byte unchanged.
|
|
33
81
|
*/
|
|
34
|
-
export declare function
|
|
82
|
+
export declare function commitExternalTerminalWorkflowStateOperations(internals: EngineInternals, state: WorkflowState, operations: BatchOperation[], options?: WorkflowStateCommitOptions): Promise<void>;
|
|
35
83
|
/** Load and decode persisted schedule state by schedule ID. */
|
|
36
84
|
export declare function loadScheduleState(internals: EngineInternals, scheduleId: string): Promise<ScheduleState | null>;
|
|
37
85
|
/** Load a schedule state. */
|
|
@@ -14,6 +14,9 @@ import { resolveEffectiveScheduleFireAt } from "./schedule-jitter.js";
|
|
|
14
14
|
import { createScheduleTimerId, decodeWorkflowStartHeaders } from "./state-utilities.js";
|
|
15
15
|
import { decodeWorkflowState } from "./validation.js";
|
|
16
16
|
import { decodeScheduleState } from "./validation/schedule.js";
|
|
17
|
+
import {
|
|
18
|
+
buildWorkflowClaimExternalTerminalRotationTransition
|
|
19
|
+
} from "./workflow-claim-transitions.js";
|
|
17
20
|
export async function runSerializedWorkflowStateWrite(internals, workflowId, writeOperation) {
|
|
18
21
|
const execution = (internals.workflowStateWriteChains.get(workflowId) ?? Promise.resolve()).catch(() => {
|
|
19
22
|
return;
|
|
@@ -51,10 +54,7 @@ export async function loadWorkflowState(internals, workflowId) {
|
|
|
51
54
|
return null;
|
|
52
55
|
return decodeWorkflowState(bytes);
|
|
53
56
|
}
|
|
54
|
-
export
|
|
55
|
-
const state = await loadWorkflowState(internals, workflowId);
|
|
56
|
-
if (!state)
|
|
57
|
-
throw Error(`Workflow "${workflowId}" not found`);
|
|
57
|
+
export function deriveWorkflowResultFromState(state) {
|
|
58
58
|
if (state.status === "completed")
|
|
59
59
|
return state.result;
|
|
60
60
|
if (state.status === "failed") {
|
|
@@ -67,9 +67,15 @@ export async function loadWorkflowResult(internals, workflowId) {
|
|
|
67
67
|
throw Error("Workflow cancelled");
|
|
68
68
|
if (state.status === "timed-out") {
|
|
69
69
|
const elapsed = state.executionDeadline ? state.executionDeadline - getWorkflowExecutionStartedAt(state) : 0;
|
|
70
|
-
throw new WorkflowTimeoutError(
|
|
70
|
+
throw new WorkflowTimeoutError(state.id, "execution", elapsed, state.terminationReason);
|
|
71
71
|
}
|
|
72
|
-
throw Error(`Workflow "${
|
|
72
|
+
throw Error(`Workflow "${state.id}" is still ${state.status}`);
|
|
73
|
+
}
|
|
74
|
+
export async function loadWorkflowResult(internals, workflowId) {
|
|
75
|
+
const state = await loadWorkflowState(internals, workflowId);
|
|
76
|
+
if (!state)
|
|
77
|
+
throw Error(`Workflow "${workflowId}" not found`);
|
|
78
|
+
return deriveWorkflowResultFromState(state);
|
|
73
79
|
}
|
|
74
80
|
function buildWorkflowStateCommit(internals, workflowId, operations, options) {
|
|
75
81
|
const pendingSideEffects = options.includePendingAtomicSideEffects ? takePendingAtomicWorkflowCommitSideEffects(internals, workflowId) : void 0, operationsWithSideEffects = pendingSideEffects === void 0 ? operations : [...operations, ...pendingSideEffects.operations], conditions = internals.storage.capabilities().conditionalBatch ? pendingSideEffects?.conditions ?? [] : [];
|
|
@@ -79,9 +85,21 @@ function buildWorkflowStateCommit(internals, workflowId, operations, options) {
|
|
|
79
85
|
hasPendingSideEffects: pendingSideEffects !== void 0
|
|
80
86
|
};
|
|
81
87
|
}
|
|
82
|
-
export async function
|
|
88
|
+
export async function buildExternalTerminalRotationFragment(internals, workflowId) {
|
|
89
|
+
if (internals.options.ownershipMode !== "workflow-lease")
|
|
90
|
+
return { conditions: [], operations: [] };
|
|
91
|
+
const observedEpochBytes = await internals.storage.get(KEYS.workflowOwnerEpoch(workflowId));
|
|
92
|
+
return buildWorkflowClaimExternalTerminalRotationTransition({ workflowId, observedEpochBytes });
|
|
93
|
+
}
|
|
94
|
+
export async function commitSelfWorkflowStateOperations(internals, state, operations, options = {}) {
|
|
83
95
|
const commit = buildWorkflowStateCommit(internals, state.id, operations, options);
|
|
84
|
-
await commitFencedEngineWrite(internals, commit.operations, commit.conditions, () => Error(`Workflow state commit for workflow "${state.id}" lost its atomic side-effect precondition.`));
|
|
96
|
+
await commitFencedEngineWrite(internals, state.id, commit.operations, commit.conditions, () => Error(`Workflow state commit for workflow "${state.id}" lost its atomic side-effect precondition.`));
|
|
97
|
+
if (commit.hasPendingSideEffects)
|
|
98
|
+
clearPendingAtomicWorkflowCommitSideEffects(internals, state.id);
|
|
99
|
+
}
|
|
100
|
+
export async function commitExternalTerminalWorkflowStateOperations(internals, state, operations, options = {}) {
|
|
101
|
+
const commit = buildWorkflowStateCommit(internals, state.id, operations, options), rotation = await buildExternalTerminalRotationFragment(internals, state.id);
|
|
102
|
+
await commitFencedEngineWrite(internals, null, [...commit.operations, ...rotation.operations], [...commit.conditions, ...rotation.conditions], () => Error(`External terminal commit for workflow "${state.id}" lost a commit precondition (staged side effects, or the ownership epoch rotation under \`workflow-lease\`).`));
|
|
85
103
|
if (commit.hasPendingSideEffects)
|
|
86
104
|
clearPendingAtomicWorkflowCommitSideEffects(internals, state.id);
|
|
87
105
|
}
|
|
@@ -132,7 +150,7 @@ export async function writeScheduleState(internals, state, options) {
|
|
|
132
150
|
], includeTimer = options?.includeTimer ?? state.status === "active";
|
|
133
151
|
operations.push(...buildScheduleTimerReplacementOperations(state, includeTimer, options?.replaceTimerFrom));
|
|
134
152
|
operations.push(...options?.additionalOperations ?? []);
|
|
135
|
-
await commitFencedEngineWrite(internals, operations, [], () => Error(`Schedule state commit for schedule "${state.id}" lost its precondition.`));
|
|
153
|
+
await commitFencedEngineWrite(internals, null, operations, [], () => Error(`Schedule state commit for schedule "${state.id}" lost its precondition.`));
|
|
136
154
|
}
|
|
137
155
|
export async function loadWorkflowStartHeaders(internals, workflowId) {
|
|
138
156
|
const bytes = await internals.storage.get(KEYS.workflowHeaders(workflowId));
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type { BatchOperation } from '../../../storage/interface.ts';
|
|
2
1
|
import type { WorkflowState, WorkflowStatus } from '../../types.ts';
|
|
3
2
|
import type { EngineInternals } from '../internals.ts';
|
|
3
|
+
import type { WorkflowStateCommitCallback } from './state-commit-callbacks.ts';
|
|
4
4
|
export type TerminationCallbacks = {
|
|
5
5
|
dispatchEvent: (event: Event) => void;
|
|
6
6
|
forwardEventToHandle: (workflowId: string, event: Event) => void;
|
|
@@ -13,9 +13,10 @@ export type TerminationCallbacks = {
|
|
|
13
13
|
handleScheduledWorkflowTerminal: (workflowId: string) => Promise<void>;
|
|
14
14
|
loadWorkflowState: (workflowId: string) => Promise<WorkflowState | null>;
|
|
15
15
|
runSerializedWorkflowStateWrite: <Result>(workflowId: string, writeOperation: () => Promise<Result>) => Promise<Result>;
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
/** SELF-transition (complete): fences on this engine's own claim. See ADR 0002. */
|
|
17
|
+
commitSelfWorkflowStateOperations: WorkflowStateCommitCallback;
|
|
18
|
+
/** EXTERNAL terminal transition (suspend): rotates the claim epoch. See ADR 0002. */
|
|
19
|
+
commitExternalTerminalWorkflowStateOperations: WorkflowStateCommitCallback;
|
|
19
20
|
cleanupReviews: (workflowId: string) => Promise<void>;
|
|
20
21
|
};
|
|
21
22
|
export declare const TERMINAL_WORKFLOW_STATUSES: ReadonlySet<WorkflowStatus>;
|
|
@@ -64,7 +64,7 @@ export async function terminateWorkflow(internals, workflowId, status, callbacks
|
|
|
64
64
|
status,
|
|
65
65
|
...reason !== void 0 ? { terminationReason: reason } : {},
|
|
66
66
|
...terminalCleanupToken !== void 0 ? { terminalCleanupToken } : {}
|
|
67
|
-
}, {
|
|
67
|
+
}, "external-terminal", {
|
|
68
68
|
allowedStatuses: FORCIBLY_TERMINABLE_STATUSES,
|
|
69
69
|
buildAdditionalOperations: (_previousState, updatedAt) => {
|
|
70
70
|
finalizePendingTimelineEntry(internals, workflowId, status, terminationMessage, updatedAt);
|
|
@@ -78,6 +78,7 @@ export async function terminateWorkflow(internals, workflowId, status, callbacks
|
|
|
78
78
|
});
|
|
79
79
|
if (!terminationResult)
|
|
80
80
|
return;
|
|
81
|
+
const claimEpoch = captureCurrentClaimEpoch(internals, workflowId);
|
|
81
82
|
await runCancellationHandlersForStatus(internals, workflowId, status, callbacks);
|
|
82
83
|
await releaseWorkflowConcurrencySlot(internals, workflowId);
|
|
83
84
|
const { previousState, updatedAt } = terminationResult, elapsed = updatedAt - getWorkflowExecutionStartedAt(previousState);
|
|
@@ -87,6 +88,7 @@ export async function terminateWorkflow(internals, workflowId, status, callbacks
|
|
|
87
88
|
if (previousState.status === "pending")
|
|
88
89
|
callbacks.swallowPromiseRejection(internals.scheduler.cancel(`delayed-start:${workflowId}`, workflowId));
|
|
89
90
|
const resolver = internals.resultResolvers.get(workflowId), terminalError = buildTerminalError(workflowId, status, elapsed, reason);
|
|
91
|
+
await releaseWorkflowClaimAfterTerminalSettlement(internals, workflowId, claimEpoch);
|
|
90
92
|
try {
|
|
91
93
|
await cleanupTerminalWorkflowSynchronously(internals, workflowId, !0, callbacks);
|
|
92
94
|
const event = buildTerminalEvent(workflowId, status, elapsed, reason);
|
|
@@ -165,6 +167,19 @@ function notifyCompletionWaiters(internals, workflowId, result, duration, callba
|
|
|
165
167
|
internals.resultResolvers.delete(workflowId);
|
|
166
168
|
}
|
|
167
169
|
}
|
|
170
|
+
function captureCurrentClaimEpoch(internals, workflowId) {
|
|
171
|
+
return internals.workflowClaimRegistry?.currentEpoch(workflowId) ?? null;
|
|
172
|
+
}
|
|
173
|
+
async function releaseWorkflowClaimAfterTerminalSettlement(internals, workflowId, capturedEpoch) {
|
|
174
|
+
const registry = internals.workflowClaimRegistry;
|
|
175
|
+
if (registry === null || capturedEpoch === null)
|
|
176
|
+
return;
|
|
177
|
+
if (registry.currentEpoch(workflowId) !== capturedEpoch)
|
|
178
|
+
return;
|
|
179
|
+
try {
|
|
180
|
+
await registry.release(workflowId);
|
|
181
|
+
} catch {}
|
|
182
|
+
}
|
|
168
183
|
export async function completeWorkflow(internals, workflowId, result, callbacks) {
|
|
169
184
|
const completionMetadata = await callbacks.runSerializedWorkflowStateWrite(workflowId, async () => {
|
|
170
185
|
const state = await callbacks.loadWorkflowState(workflowId);
|
|
@@ -180,14 +195,16 @@ export async function completeWorkflow(internals, workflowId, result, callbacks)
|
|
|
180
195
|
appendSearchAttributeOperations(completionOperations, workflowId, currentAttributes);
|
|
181
196
|
if (terminalCleanupToken !== void 0)
|
|
182
197
|
completionOperations.push(...buildTerminalCleanupTimerOperations(internals, workflowId, !1, now, terminalCleanupToken));
|
|
183
|
-
await callbacks.
|
|
198
|
+
await callbacks.commitSelfWorkflowStateOperations(state, completionOperations, {
|
|
184
199
|
includePendingAtomicSideEffects: !0
|
|
185
200
|
});
|
|
186
|
-
|
|
201
|
+
const claimEpoch = captureCurrentClaimEpoch(internals, workflowId);
|
|
202
|
+
return { duration, claimEpoch };
|
|
187
203
|
});
|
|
188
204
|
if (!completionMetadata)
|
|
189
205
|
return;
|
|
190
206
|
await releaseWorkflowConcurrencySlot(internals, workflowId);
|
|
207
|
+
await releaseWorkflowClaimAfterTerminalSettlement(internals, workflowId, completionMetadata.claimEpoch);
|
|
191
208
|
notifyCompletionWaiters(internals, workflowId, result, completionMetadata.duration, callbacks);
|
|
192
209
|
}
|
|
193
210
|
export async function failWorkflow(internals, workflowId, error, callbacks, failureCategory = "system") {
|
|
@@ -202,7 +219,7 @@ export async function failWorkflow(internals, workflowId, error, callbacks, fail
|
|
|
202
219
|
stateUpdate.errorStack = error.stack;
|
|
203
220
|
if (terminalCleanupToken !== void 0)
|
|
204
221
|
stateUpdate.terminalCleanupToken = terminalCleanupToken;
|
|
205
|
-
if (!await updateWorkflowState(internals, workflowId, stateUpdate, {
|
|
222
|
+
if (!await updateWorkflowState(internals, workflowId, stateUpdate, "self", {
|
|
206
223
|
allowedStatuses: FORCIBLY_TERMINABLE_STATUSES,
|
|
207
224
|
buildAdditionalOperations: (_previousState, updatedAt) => {
|
|
208
225
|
finalizePendingTimelineEntry(internals, workflowId, "failed", error.message, updatedAt);
|
|
@@ -214,11 +231,13 @@ export async function failWorkflow(internals, workflowId, error, callbacks, fail
|
|
|
214
231
|
}
|
|
215
232
|
}))
|
|
216
233
|
return;
|
|
234
|
+
const claimEpoch = captureCurrentClaimEpoch(internals, workflowId);
|
|
217
235
|
await releaseWorkflowConcurrencySlot(internals, workflowId);
|
|
218
236
|
await cleanupAttributeIndex(internals, workflowId, attributes);
|
|
219
237
|
callbacks.swallowPromiseRejection(internals.scheduler.cancel(`deadline:${workflowId}`, workflowId));
|
|
220
238
|
await writeRetainedTerminalSearchAttributes(internals, workflowId, retainedAttributes);
|
|
221
239
|
const resolver = internals.resultResolvers.get(workflowId);
|
|
240
|
+
await releaseWorkflowClaimAfterTerminalSettlement(internals, workflowId, claimEpoch);
|
|
222
241
|
try {
|
|
223
242
|
await cleanupTerminalWorkflowSynchronously(internals, workflowId, !1, callbacks);
|
|
224
243
|
const event = new WorkflowFailedEvent(workflowId, error);
|
|
@@ -45,22 +45,22 @@ export function encodeRunningClaim(attempts, token, claimedAt) {
|
|
|
45
45
|
export async function rearmTeardownTimer(internals, workflowId, token, delayMs) {
|
|
46
46
|
const fireAt = internals.options.getNow() + delayMs;
|
|
47
47
|
try {
|
|
48
|
-
await commitFencedEngineWrite(internals, teardownTimerOperations(token, workflowId, fireAt), [], () => Error("teardown self-heal re-arm lost the lease fence"));
|
|
48
|
+
await commitFencedEngineWrite(internals, workflowId, teardownTimerOperations(token, workflowId, fireAt), [], () => Error("teardown self-heal re-arm lost the lease fence"));
|
|
49
49
|
} catch {}
|
|
50
50
|
}
|
|
51
51
|
export async function clearTeardownMarker(internals, workflowId, expectedBytes) {
|
|
52
52
|
try {
|
|
53
|
-
return await commitFencedEngineWriteAllowingPreconditionFailure(internals, [{ type: "delete", key: KEYS.teardownOwed(workflowId) }], [{ key: KEYS.teardownOwed(workflowId), expectedValue: expectedBytes }]);
|
|
53
|
+
return await commitFencedEngineWriteAllowingPreconditionFailure(internals, workflowId, [{ type: "delete", key: KEYS.teardownOwed(workflowId) }], [{ key: KEYS.teardownOwed(workflowId), expectedValue: expectedBytes }]);
|
|
54
54
|
} catch {
|
|
55
55
|
return !1;
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
58
|
export async function claimTeardownMarker(internals, workflowId, expectedBytes, attempts, token) {
|
|
59
59
|
const runningBytes = encodeRunningClaim(attempts, token, internals.options.getNow());
|
|
60
|
-
return await commitFencedEngineWriteAllowingPreconditionFailure(internals, [{ type: "put", key: KEYS.teardownOwed(workflowId), value: runningBytes }], [{ key: KEYS.teardownOwed(workflowId), expectedValue: expectedBytes }]) ? runningBytes : null;
|
|
60
|
+
return await commitFencedEngineWriteAllowingPreconditionFailure(internals, workflowId, [{ type: "put", key: KEYS.teardownOwed(workflowId), value: runningBytes }], [{ key: KEYS.teardownOwed(workflowId), expectedValue: expectedBytes }]) ? runningBytes : null;
|
|
61
61
|
}
|
|
62
62
|
export async function settleOnRunningClaim(internals, workflowId, runningBytes, operations) {
|
|
63
|
-
return commitFencedEngineWriteAllowingPreconditionFailure(internals, operations, [
|
|
63
|
+
return commitFencedEngineWriteAllowingPreconditionFailure(internals, workflowId, operations, [
|
|
64
64
|
{ key: KEYS.teardownOwed(workflowId), expectedValue: runningBytes }
|
|
65
65
|
]);
|
|
66
66
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { BatchOperation } from '../../../storage/interface.ts';
|
|
2
|
+
import type { WorkflowState } from '../../types.ts';
|
|
3
|
+
/**
|
|
4
|
+
* Shared function shape for the two workflow-state commit callbacks
|
|
5
|
+
* {@link TerminationCallbacks} exposes, split by ADR 0002's self/
|
|
6
|
+
* external-terminal transition classification (see
|
|
7
|
+
* `documentation/contributing/architecture-decisions/0002-multiengine-per-workflow-ownership.md`).
|
|
8
|
+
* A self-transition (complete, fail) fences on this engine's own claim; an
|
|
9
|
+
* external terminal transition (cancel, timeout, suspend) rotates the claim
|
|
10
|
+
* epoch instead, so any engine may commit it. Kept as one shared type so the
|
|
11
|
+
* two `TerminationCallbacks` fields cannot drift in shape.
|
|
12
|
+
*/
|
|
13
|
+
export type WorkflowStateCommitCallback = (state: WorkflowState, operations: BatchOperation[], options?: {
|
|
14
|
+
includePendingAtomicSideEffects?: boolean;
|
|
15
|
+
}) => Promise<void>;
|
|
File without changes
|
|
@@ -18,7 +18,7 @@ export async function suspendWorkflow(internals, workflowId, callbacks) {
|
|
|
18
18
|
internals.parkedInlineWorkflows.delete(workflowId);
|
|
19
19
|
evictSuspendedWorkflowWaiters(internals, workflowId, callbacks);
|
|
20
20
|
const updatedAt = internals.options.getNow(), updatedState = { ...state, status: "suspended", updatedAt };
|
|
21
|
-
await callbacks.
|
|
21
|
+
await callbacks.commitExternalTerminalWorkflowStateOperations(state, [
|
|
22
22
|
{ type: "put", key: KEYS.workflow(workflowId), value: encode(updatedState) },
|
|
23
23
|
...buildWorkflowVisibilityIndexTransition(workflowId, state, updatedState).batchOps,
|
|
24
24
|
...buildDeadlineTimerDeleteOperations(workflowId, state.executionDeadline)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pre-acceptance validation for workflow updates: running a registered
|
|
3
|
+
* `updateValidators` entry and normalizing a Standard Schema v1 failure result
|
|
4
|
+
* into the issue list `UpdateValidationError` carries.
|
|
5
|
+
*
|
|
6
|
+
* Lives apart from `updates.ts` because it is shared — `pending-updates.ts`
|
|
7
|
+
* normalizes validator results through the same helper — and because it is
|
|
8
|
+
* pure payload validation with no coupling to the in-memory waiter maps or the
|
|
9
|
+
* durable coordinated-update protocol that make up the rest of update delivery.
|
|
10
|
+
*
|
|
11
|
+
* @module core/engine/update-validation
|
|
12
|
+
*/
|
|
13
|
+
import type { EngineInternals } from './internals.ts';
|
|
14
|
+
/**
|
|
15
|
+
* Run the pre-acceptance validator for an update, if one is registered.
|
|
16
|
+
* Throws `UpdateValidationError` if the validator rejects (by throwing or by
|
|
17
|
+
* returning a Standard Schema `{ issues: [...] }` failure result).
|
|
18
|
+
*/
|
|
19
|
+
export declare function runUpdateValidator(internals: EngineInternals, workflowId: string, name: string, payload: unknown): Promise<void>;
|
|
20
|
+
/**
|
|
21
|
+
* Extract issues from a Standard Schema v1 failure result, or null if absent.
|
|
22
|
+
* No string-`message` entries yields `[]`; callers reject only on a non-empty
|
|
23
|
+
* array, so `null` and `[]` both mean acceptance. Preserves `path` (RFC 6901).
|
|
24
|
+
*/
|
|
25
|
+
export declare function extractStandardSchemaIssues(result: unknown): Array<{
|
|
26
|
+
message: string;
|
|
27
|
+
path?: string;
|
|
28
|
+
}> | null;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { UpdateValidationError } from "../updates.js";
|
|
2
|
+
export async function runUpdateValidator(internals, workflowId, name, payload) {
|
|
3
|
+
const validator = internals.inlineStrategy?.getContext(workflowId)?.updateValidators.get(name);
|
|
4
|
+
if (validator === void 0)
|
|
5
|
+
return;
|
|
6
|
+
let result;
|
|
7
|
+
try {
|
|
8
|
+
result = await validator(payload);
|
|
9
|
+
} catch (error) {
|
|
10
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
11
|
+
throw new UpdateValidationError(name, [{ message }]);
|
|
12
|
+
}
|
|
13
|
+
const issues = extractStandardSchemaIssues(result);
|
|
14
|
+
if (issues !== null && issues.length > 0)
|
|
15
|
+
throw new UpdateValidationError(name, issues);
|
|
16
|
+
}
|
|
17
|
+
export function extractStandardSchemaIssues(result) {
|
|
18
|
+
if (result === null || typeof result !== "object" || !("issues" in result))
|
|
19
|
+
return null;
|
|
20
|
+
const { issues } = result;
|
|
21
|
+
if (!Array.isArray(issues))
|
|
22
|
+
return null;
|
|
23
|
+
return issues.flatMap((issue) => {
|
|
24
|
+
if (issue === null || typeof issue !== "object")
|
|
25
|
+
return [];
|
|
26
|
+
const obj = issue;
|
|
27
|
+
if (typeof obj.message !== "string")
|
|
28
|
+
return [];
|
|
29
|
+
const entry = { message: obj.message };
|
|
30
|
+
if (Array.isArray(obj.path) && obj.path.length > 0)
|
|
31
|
+
entry.path = obj.path.reduce((p, seg) => {
|
|
32
|
+
const k = seg !== null && typeof seg === "object" && "key" in seg ? String(seg.key) : String(seg);
|
|
33
|
+
return p + "/" + k.replace(/~/g, "~0").replace(/\//g, "~1");
|
|
34
|
+
}, "");
|
|
35
|
+
return [entry];
|
|
36
|
+
});
|
|
37
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ContextOperationRequest } from '../context.ts';
|
|
2
2
|
import type { CoordinatedUpdateResult } from '../types.ts';
|
|
3
|
-
import {
|
|
3
|
+
import type { UpdateRequest, UpdateResponse } from '../updates.ts';
|
|
4
4
|
import type { EngineInternals } from './internals.ts';
|
|
5
5
|
export type UpdateCallbacks = {
|
|
6
6
|
dispatchEvent: (event: Event) => boolean;
|
|
@@ -22,6 +22,22 @@ export type UpdateCallbacks = {
|
|
|
22
22
|
export declare function update(internals: EngineInternals, workflowId: string, name: string, payload: unknown, options: {
|
|
23
23
|
timeout?: number;
|
|
24
24
|
} | undefined, callbacks: UpdateCallbacks): Promise<unknown>;
|
|
25
|
+
/**
|
|
26
|
+
* `handled: false` used to conflate two reasons: no live local context at all
|
|
27
|
+
* (`'not-owned-locally'`, possible under `ownership: 'workflow-lease'` when
|
|
28
|
+
* another engine holds the claim) versus a live context with no handler
|
|
29
|
+
* registered for `name` (`'no-handler'`). See the `update()` call site for
|
|
30
|
+
* why neither is routed differently — the reason is exposed so a caller that
|
|
31
|
+
* DOES care (tests, future routing) can tell them apart.
|
|
32
|
+
*/
|
|
33
|
+
export type InlineUpdateAttemptResult = {
|
|
34
|
+
handled: true;
|
|
35
|
+
value: unknown;
|
|
36
|
+
} | {
|
|
37
|
+
handled: false;
|
|
38
|
+
reason: 'not-owned-locally' | 'no-handler';
|
|
39
|
+
};
|
|
40
|
+
export declare function tryInlineUpdateHandler(internals: EngineInternals, workflowId: string, name: string, payload: unknown, callbacks: UpdateCallbacks): Promise<InlineUpdateAttemptResult>;
|
|
25
41
|
/** Retrieve the result of a coordinated update by its ID. */
|
|
26
42
|
export declare function getUpdateResult(internals: EngineInternals, updateId: string): Promise<UpdateResponse | null>;
|
|
27
43
|
/**
|
|
@@ -39,18 +55,3 @@ export declare function dispatchPendingUpdateReceived(_internals: EngineInternal
|
|
|
39
55
|
export declare function createCoordinatedUpdateResponder(_internals: EngineInternals, workflowId: string, updateName: string, updateRequest: UpdateRequest, callbacks: Pick<UpdateCallbacks, 'persistCoordinatedUpdateResponse'>): (value: unknown) => void;
|
|
40
56
|
export declare function deliverCoordinatedUpdateToWaiterIfAvailable(internals: EngineInternals, workflowId: string, updateRequest: UpdateRequest, dispatchReceivedEvent: boolean | undefined, callbacks: UpdateCallbacks): Promise<boolean>;
|
|
41
57
|
export declare function findPendingUpdateByName(internals: EngineInternals, workflowId: string, name: string): Promise<UpdateRequest | undefined>;
|
|
42
|
-
/**
|
|
43
|
-
* Invoke an update handler, checking that it does not return a generator.
|
|
44
|
-
* Centralises the runtime generator guard for both the inline-handler path
|
|
45
|
-
* in `update()` and the pending-drain path on resume.
|
|
46
|
-
*/
|
|
47
|
-
export declare function invokeUpdateHandler(_internals: EngineInternals, name: string, handler: (payload: unknown) => unknown, payload: unknown): Promise<unknown>;
|
|
48
|
-
/**
|
|
49
|
-
* Extract issues from a Standard Schema v1 failure result, or null if absent.
|
|
50
|
-
* No string-`message` entries yields `[]`; callers reject only on a non-empty
|
|
51
|
-
* array, so `null` and `[]` both mean acceptance. Preserves `path` (RFC 6901).
|
|
52
|
-
*/
|
|
53
|
-
export declare function extractStandardSchemaIssues(result: unknown): Array<{
|
|
54
|
-
message: string;
|
|
55
|
-
path?: string;
|
|
56
|
-
}> | null;
|