@lostgradient/weft 0.24.1 → 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 +1 -1
- package/dist/core/engine/bulk-operations-purge.d.ts +1 -1
- package/dist/core/engine/bulk-operations-purge.js +3 -2
- package/dist/core/engine/bulk-operations-retry.d.ts +16 -1
- package/dist/core/engine/bulk-operations-retry.js +19 -6
- package/dist/core/engine/bulk-operations.d.ts +3 -2
- package/dist/core/engine/bulk-operations.js +2 -2
- package/dist/core/engine/callback-creators-bundles.js +1 -1
- package/dist/core/engine/callback-creators-schedule.js +2 -2
- package/dist/core/engine/child-workflow.d.ts +13 -1
- package/dist/core/engine/child-workflow.js +22 -4
- package/dist/core/engine/generation-codec.d.ts +22 -0
- package/dist/core/engine/generation-codec.js +2 -0
- package/dist/core/engine/index.js +1 -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/resume-body.d.ts +44 -0
- package/dist/core/engine/lifecycle/resume-body.js +211 -0
- package/dist/core/engine/lifecycle/resume.d.ts +46 -3
- package/dist/core/engine/lifecycle/resume.js +21 -210
- package/dist/core/engine/lifecycle/standalone-claim-acquire.d.ts +68 -2
- package/dist/core/engine/lifecycle/standalone-claim-acquire.js +14 -4
- package/dist/core/engine/lifecycle/start-commit.d.ts +2 -0
- package/dist/core/engine/lifecycle/start-commit.js +16 -8
- 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-precondition-attribution.d.ts +10 -4
- 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-terminal-conflict-purge.d.ts +90 -11
- package/dist/core/engine/lifecycle/start-terminal-conflict-purge.js +24 -5
- package/dist/core/engine/lifecycle/start.d.ts +40 -3
- package/dist/core/engine/lifecycle/start.js +19 -26
- package/dist/core/engine/lifecycle/transition.d.ts +9 -0
- package/dist/core/engine/lifecycle/transition.js +20 -25
- package/dist/core/engine/lifecycle.d.ts +2 -1
- package/dist/core/engine/lifecycle.js +2 -1
- package/dist/core/engine/schedule-overlap.js +2 -1
- package/dist/core/engine/schedule-run.d.ts +10 -0
- package/dist/core/engine/schedule-run.js +1 -1
- package/dist/core/engine/schedules.d.ts +11 -2
- package/dist/core/engine/termination/complete.js +1 -1
- package/dist/core/engine/termination/suspend.js +3 -0
- package/dist/core/engine/validation/schedule.d.ts +25 -1
- package/dist/core/engine/validation/schedule.js +4 -10
- package/dist/core/engine/validation.js +10 -17
- package/dist/core/engine/workflow-claim-reclaim-target.js +6 -6
- 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/start-workflow-validation.d.ts +15 -0
- package/dist/core/start-workflow-validation.js +11 -1
- package/dist/core/workflow-identifiers.d.ts +37 -0
- package/dist/core/workflow-identifiers.js +19 -1
- package/dist/http.js +2 -2
- package/dist/indexeddb.js +1 -1
- package/dist/server/fleet-event-feed.js +6 -6
- package/dist/server/rest-binding.d.ts +11 -0
- package/dist/server/runtime/task-dispatch-envelope.d.ts +32 -5
- package/dist/server/runtime/task-dispatch-envelope.js +23 -1
- package/dist/server/runtime/task-dispatch.d.ts +3 -1
- package/dist/server/runtime/task-dispatch.js +9 -8
- package/dist/server/task-ledger-codec.d.ts +19 -0
- package/dist/server/task-ledger-codec.js +3 -0
- package/dist/server/task-ledger.d.ts +1 -1
- package/dist/server/task-ledger.js +1 -0
- package/dist/storage/bun-sql.js +132 -99
- 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 -65
- package/dist/storage/interface.d.ts +24 -232
- package/dist/storage/interface.js +1 -1
- package/dist/storage/key-prefixes.d.ts +1 -1
- package/dist/storage/key-prefixes.js +1 -0
- package/dist/storage/lease-keys.d.ts +48 -0
- package/dist/storage/lease-keys.js +8 -0
- 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 +132 -99
- 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/package.json +1 -1
|
@@ -1,4 +1,47 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* `engine.resume(id)` / `recoverAll()`'s standalone-replay-from-storage entry
|
|
3
|
+
* point. The actual work — load tracking/checkpoint/registration state,
|
|
4
|
+
* prepare the resume, and relaunch inline or in a worker — lives in
|
|
5
|
+
* `resume-body.ts`'s `performResumeAfterClaimAcquired` (split out to stay
|
|
6
|
+
* under this repository's 500-line implementation-file ceiling once the
|
|
7
|
+
* WFT-134 claim-release wrapping below was added). This file's own job is
|
|
8
|
+
* just: load and validate state, acquire a claim if this run doesn't already
|
|
9
|
+
* hold one, and release a claim it freshly acquired if the resume turns out
|
|
10
|
+
* not to succeed after all — UNLESS `options.deferClaimReleaseOnRejection`
|
|
11
|
+
* asks this call to leave that claim installed for its caller instead (see
|
|
12
|
+
* {@link ResumeFromStorageOptions}).
|
|
13
|
+
*
|
|
14
|
+
* @module core/engine/lifecycle/resume
|
|
15
|
+
*/
|
|
16
|
+
import type { WorkflowHandle } from '../handles.ts';
|
|
2
17
|
import type { EngineInternals } from '../internals.ts';
|
|
3
|
-
import {
|
|
4
|
-
|
|
18
|
+
import type { LifecycleCallbacks, RecoverAllOptions } from './shared.ts';
|
|
19
|
+
/**
|
|
20
|
+
* An out-parameter this call populates with the epoch of a claim it freshly
|
|
21
|
+
* acquired, so a caller using `deferClaimReleaseOnRejection` can release that
|
|
22
|
+
* EXACT generation itself once it is done using it (WFT-134 review round 2,
|
|
23
|
+
* issue D). Left at `{ epoch: null }` when no fresh acquisition happened —
|
|
24
|
+
* folding disabled, no registry, the cached claim matched, or acquisition
|
|
25
|
+
* itself threw `WorkflowClaimUnavailableError` before returning.
|
|
26
|
+
*/
|
|
27
|
+
export type FreshResumeClaimTracker = {
|
|
28
|
+
epoch: number | null;
|
|
29
|
+
};
|
|
30
|
+
/** Options for {@link resumeWorkflowFromStorage}. */
|
|
31
|
+
export type ResumeFromStorageOptions = {
|
|
32
|
+
/**
|
|
33
|
+
* `true` for `recoverAll()`'s per-entry recovery loop (WFT-134 issue D):
|
|
34
|
+
* its own `recoverEntryOrIsolateFailure` isolates several errors into a
|
|
35
|
+
* fenced `failWorkflowForXxx()` commit that needs a freshly-acquired claim
|
|
36
|
+
* to still be installed, so this call must NOT release on rejection —
|
|
37
|
+
* instead it records the acquired epoch in `freshClaimTracker` and lets the
|
|
38
|
+
* caller release once its own isolated-failure handling is done. Default
|
|
39
|
+
* (`false`/omitted) is correct for an explicit `engine.resume()` caller,
|
|
40
|
+
* which has nothing further to do with the claim and must release
|
|
41
|
+
* immediately on any rejection.
|
|
42
|
+
*/
|
|
43
|
+
deferClaimReleaseOnRejection?: boolean;
|
|
44
|
+
/** Populated with the freshly-acquired epoch, if any — see {@link FreshResumeClaimTracker}. */
|
|
45
|
+
freshClaimTracker?: FreshResumeClaimTracker;
|
|
46
|
+
};
|
|
47
|
+
export declare function resumeWorkflowFromStorage(internals: EngineInternals, workflowId: string, dispatchResumedEvent: boolean, callbacks: LifecycleCallbacks, onRecoveredWorkflow?: RecoverAllOptions['onRecoveredWorkflow'], options?: ResumeFromStorageOptions): Promise<WorkflowHandle>;
|
|
@@ -1,220 +1,31 @@
|
|
|
1
1
|
import { KEYS } from "../../../storage/interface.js";
|
|
2
|
-
import { deserializeCheckpoint, serializeCheckpoint } from "../../checkpoint.js";
|
|
3
|
-
import { encode } from "../../codec.js";
|
|
4
|
-
import { Context, setContextWorkflowInterceptor } from "../../context.js";
|
|
5
|
-
import { EventLog } from "../../event-log.js";
|
|
6
|
-
import { WorkflowResumedEvent } from "../../events.js";
|
|
7
|
-
import { buildTimerBatchOperations } from "../../scheduler.js";
|
|
8
|
-
import { createCancelHandlerRegistration, resetCancelHandlers } from "../cancel-handlers.js";
|
|
9
|
-
import { rememberCommittedCheckpointBytes } from "../checkpoint-commit-snapshots.js";
|
|
10
|
-
import { rehydrateChildCancellationHandlers } from "../child-workflow-cancellation.js";
|
|
11
|
-
import { resolveExecutableRegistrationOrRenamedNotFound } from "../dynamic-source-execution.js";
|
|
12
|
-
import { commitFencedEngineWrite } from "../fenced-write.js";
|
|
13
|
-
import { getWorkflowExecutionStartedAt } from "../handles.js";
|
|
14
|
-
import { loadWorkflowState } from "../storage-io.js";
|
|
15
|
-
import { getComposedWorkflowInterceptor } from "../strategy-helpers.js";
|
|
16
2
|
import { decodeWorkflowState } from "../validation.js";
|
|
17
|
-
import {
|
|
18
|
-
import { prepareResumeState } from "./persist.js";
|
|
3
|
+
import { performResumeAfterClaimAcquired } from "./resume-body.js";
|
|
19
4
|
import {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
} from "./
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
async function
|
|
32
|
-
return reprovideRecoveredServices(internals, state, callbacks.failWorkflowForUnavailableServices, callbacks.handleCleanupError, callbacks.dispatchEvent, resolverInfo);
|
|
33
|
-
}
|
|
34
|
-
async function runRecoveredWorkflowHookOrFail(internals, state, handle, resolverInfo, onRecoveredWorkflow, callbacks) {
|
|
35
|
-
const info = {
|
|
36
|
-
...resolverInfo,
|
|
37
|
-
handle,
|
|
38
|
-
launchOptions: resolverInfo.launchOptions ?? { id: state.id },
|
|
39
|
-
services: internals.workflowServices.get(state.id)
|
|
40
|
-
};
|
|
41
|
-
try {
|
|
42
|
-
await onRecoveredWorkflow(info);
|
|
43
|
-
return !1;
|
|
44
|
-
} catch (error) {
|
|
45
|
-
const reason = error instanceof Error ? error.message : String(error), hookError = Error(`Recovery hook failed for workflow "${state.id}": ${reason}`);
|
|
46
|
-
try {
|
|
47
|
-
await callbacks.failWorkflowForRecoveryHook(state.id, hookError);
|
|
48
|
-
} catch (commitError) {
|
|
49
|
-
callbacks.handleCleanupError("onRecoveredWorkflow", commitError, state.id);
|
|
50
|
-
}
|
|
51
|
-
return !0;
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
async function prepareRecoveredWorkflowOrFail(internals, state, callbacks, onRecoveredWorkflow) {
|
|
55
|
-
const resolverInfo = onRecoveredWorkflow === void 0 ? void 0 : await workflowServicesResolverInfoFromState(internals, state), handle = callbacks.getHandle(state.id);
|
|
56
|
-
if (await prepareRecoveredServicesOrFail(internals, state, callbacks, resolverInfo))
|
|
57
|
-
return { handle, shouldStop: !0 };
|
|
58
|
-
if (onRecoveredWorkflow === void 0 || resolverInfo === void 0)
|
|
59
|
-
return { handle, shouldStop: !1 };
|
|
60
|
-
const shouldStop = await runRecoveredWorkflowHookOrFail(internals, state, handle, resolverInfo, onRecoveredWorkflow, callbacks);
|
|
61
|
-
return { handle, shouldStop };
|
|
62
|
-
}
|
|
63
|
-
function assertResumeNotTerminating(internals, workflowId) {
|
|
64
|
-
if (internals.terminalizingWorkflows.has(workflowId))
|
|
65
|
-
throw Error(`Cannot resume workflow "${workflowId}": termination is in progress`);
|
|
66
|
-
}
|
|
67
|
-
function commitSerializedResumeState(internals, args) {
|
|
68
|
-
const {
|
|
69
|
-
workflowId,
|
|
70
|
-
resumeCheckpoint,
|
|
71
|
-
serializedCheckpoint,
|
|
72
|
-
registeredVersionTuple,
|
|
73
|
-
restoredHead,
|
|
74
|
-
callbacks
|
|
75
|
-
} = args;
|
|
76
|
-
internals.checkpoints.set(workflowId, resumeCheckpoint);
|
|
77
|
-
rememberCommittedCheckpointBytes(internals, workflowId, serializedCheckpoint);
|
|
78
|
-
internals.workflowVersionTuples.set(workflowId, registeredVersionTuple);
|
|
79
|
-
internals.eventLogHeads.set(workflowId, restoredHead);
|
|
80
|
-
setWorkflowStartHeaders(internals, workflowId, args.workflowStartHeaders, callbacks);
|
|
81
|
-
internals.parkedInlineWorkflows.delete(workflowId);
|
|
82
|
-
}
|
|
83
|
-
async function relaunchInlineWorkflowAfterResume(internals, latestState, args) {
|
|
84
|
-
const { workflowId, resumeCheckpoint, registration, resolvedRevision, callbacks } = args, accumulatedResults = new Map(resumeCheckpoint.accumulatedResults), workflowAbort = new AbortController;
|
|
85
|
-
internals.workflowTypeByWorkflowId.set(workflowId, {
|
|
86
|
-
type: latestState.type,
|
|
87
|
-
revision: resolvedRevision
|
|
88
|
-
});
|
|
89
|
-
resetCancelHandlers(internals, workflowId);
|
|
90
|
-
await rehydrateChildCancellationHandlers(internals, workflowId, callbacks);
|
|
91
|
-
const context = new Context({
|
|
92
|
-
workflowId,
|
|
93
|
-
...latestState.workflowExecutionToken !== void 0 && {
|
|
94
|
-
workflowExecutionToken: latestState.workflowExecutionToken
|
|
95
|
-
},
|
|
96
|
-
workflowType: latestState.type,
|
|
97
|
-
startedAt: getWorkflowExecutionStartedAt(latestState),
|
|
98
|
-
abortController: workflowAbort,
|
|
99
|
-
getNow: internals.options.getNow,
|
|
100
|
-
resolveWorkflowType: callbacks.resolveWorkflowTypeTarget,
|
|
101
|
-
executionStateOwnerId: latestState.executionStateOwnerId ?? workflowId,
|
|
102
|
-
accumulatedResults,
|
|
103
|
-
locals: resumeCheckpoint.locals,
|
|
104
|
-
searchAttributes: resumeCheckpoint.searchAttributes,
|
|
105
|
-
registerCancelHandler: createCancelHandlerRegistration(internals, workflowId),
|
|
106
|
-
...registration.searchAttributes && {
|
|
107
|
-
searchAttributeSchema: registration.searchAttributes
|
|
108
|
-
},
|
|
109
|
-
sleepReferenceTime: resumeCheckpoint.createdAt,
|
|
110
|
-
...latestState.executionDeadline !== void 0 && {
|
|
111
|
-
deadline: latestState.executionDeadline
|
|
112
|
-
},
|
|
113
|
-
services: internals.workflowServices.get(workflowId),
|
|
114
|
-
...internals.options.onLog != null && { logSink: internals.options.onLog }
|
|
115
|
-
});
|
|
116
|
-
setContextWorkflowInterceptor(context, getComposedWorkflowInterceptor(internals));
|
|
117
|
-
if (internals.options.development)
|
|
118
|
-
context.explain(!0);
|
|
119
|
-
const generator = registration.handler(context, latestState.input), inlineStrategy = internals.inlineStrategy;
|
|
120
|
-
inlineStrategy.adoptWorkflow(workflowId, generator, context, workflowAbort);
|
|
121
|
-
inlineStrategy.continueWorkflow(workflowId, void 0);
|
|
122
|
-
}
|
|
123
|
-
async function relaunchWorkerWorkflowAfterResume(internals, latestState, args) {
|
|
124
|
-
const { workflowId, resumeCheckpoint, workflowStartHeaders, resolvedRevision, callbacks } = args;
|
|
125
|
-
internals.workflowTypeByWorkflowId.set(workflowId, {
|
|
126
|
-
type: latestState.type,
|
|
127
|
-
revision: resolvedRevision
|
|
128
|
-
});
|
|
129
|
-
resetCancelHandlers(internals, workflowId);
|
|
130
|
-
await rehydrateChildCancellationHandlers(internals, workflowId, callbacks);
|
|
131
|
-
const serialized = serializeCheckpoint(resumeCheckpoint);
|
|
132
|
-
internals.strategy.startWorkflow({
|
|
133
|
-
workflowId,
|
|
134
|
-
...latestState.workflowExecutionToken !== void 0 && {
|
|
135
|
-
workflowExecutionToken: latestState.workflowExecutionToken
|
|
136
|
-
},
|
|
137
|
-
workflowType: latestState.type,
|
|
138
|
-
input: latestState.input,
|
|
139
|
-
checkpoint: serialized,
|
|
140
|
-
nestingDepth: internals.workflowNestingDepths.get(workflowId) ?? 0,
|
|
141
|
-
executionStateOwnerId: latestState.executionStateOwnerId ?? workflowId,
|
|
142
|
-
...latestState.executionDeadline !== void 0 && {
|
|
143
|
-
deadline: latestState.executionDeadline
|
|
144
|
-
},
|
|
145
|
-
...workflowStartHeaders !== void 0 && workflowStartHeaders.size > 0 && {
|
|
146
|
-
headers: [...workflowStartHeaders]
|
|
147
|
-
}
|
|
148
|
-
});
|
|
149
|
-
}
|
|
150
|
-
async function performSerializedResume(internals, args) {
|
|
151
|
-
const { workflowId } = args;
|
|
152
|
-
assertResumeNotTerminating(internals, workflowId);
|
|
153
|
-
const latestState = await loadWorkflowState(internals, workflowId);
|
|
154
|
-
assertResumeNotTerminating(internals, workflowId);
|
|
155
|
-
if (!latestState)
|
|
156
|
-
throw Error(`Workflow "${workflowId}" not found in storage`);
|
|
157
|
-
if (latestState.status !== "running" && latestState.status !== "suspended")
|
|
158
|
-
throw Error(`Cannot resume workflow "${workflowId}": status is "${latestState.status}", expected "running" or "suspended"`);
|
|
159
|
-
assertSameGeneration(workflowId, latestState, args.expectedGeneration);
|
|
160
|
-
await reactivateSuspendedWorkflowState(internals, latestState);
|
|
161
|
-
commitSerializedResumeState(internals, args);
|
|
162
|
-
if (internals.inlineStrategy) {
|
|
163
|
-
await relaunchInlineWorkflowAfterResume(internals, latestState, args);
|
|
164
|
-
return;
|
|
165
|
-
}
|
|
166
|
-
await relaunchWorkerWorkflowAfterResume(internals, latestState, args);
|
|
167
|
-
}
|
|
168
|
-
async function reactivateSuspendedWorkflowState(internals, state) {
|
|
169
|
-
if (state.status !== "suspended")
|
|
170
|
-
return;
|
|
171
|
-
const previousState = { ...state };
|
|
172
|
-
state.status = "running";
|
|
173
|
-
state.updatedAt = internals.options.getNow();
|
|
174
|
-
await commitFencedEngineWrite(internals, state.id, [
|
|
175
|
-
{ type: "put", key: KEYS.workflow(state.id), value: encode(state) },
|
|
176
|
-
...buildWorkflowVisibilityIndexTransition(state.id, previousState, state).batchOps,
|
|
177
|
-
...state.executionDeadline !== void 0 ? buildTimerBatchOperations({
|
|
178
|
-
id: `deadline:${state.id}`,
|
|
179
|
-
workflowId: state.id,
|
|
180
|
-
fireAt: state.executionDeadline,
|
|
181
|
-
kind: "execution-deadline"
|
|
182
|
-
}) : []
|
|
183
|
-
], [], () => Error(`Resume of workflow "${state.id}" lost its CAS race.`));
|
|
184
|
-
}
|
|
185
|
-
export async function resumeWorkflowFromStorage(internals, workflowId, dispatchResumedEvent, callbacks, onRecoveredWorkflow) {
|
|
5
|
+
acquireStandaloneClaimBeforeResume,
|
|
6
|
+
releaseFreshlyAcquiredResumeClaim
|
|
7
|
+
} from "./standalone-claim-acquire.js";
|
|
8
|
+
function recordFreshClaimEpoch(acquireResult, tracker) {
|
|
9
|
+
if (acquireResult.freshlyAcquired && tracker !== void 0)
|
|
10
|
+
tracker.epoch = acquireResult.epoch;
|
|
11
|
+
}
|
|
12
|
+
async function releaseFreshClaimOnRejectionUnlessDeferred(internals, workflowId, acquireResult, deferRelease) {
|
|
13
|
+
if (acquireResult.freshlyAcquired && deferRelease !== !0)
|
|
14
|
+
await releaseFreshlyAcquiredResumeClaim(internals, workflowId, acquireResult.epoch);
|
|
15
|
+
}
|
|
16
|
+
export async function resumeWorkflowFromStorage(internals, workflowId, dispatchResumedEvent, callbacks, onRecoveredWorkflow, options) {
|
|
186
17
|
const stateBytes = await internals.storage.get(KEYS.workflow(workflowId));
|
|
187
18
|
if (!stateBytes)
|
|
188
19
|
throw Error(`Workflow "${workflowId}" not found in storage`);
|
|
189
20
|
const state = decodeWorkflowState(stateBytes);
|
|
190
21
|
if (state.status !== "running" && state.status !== "suspended")
|
|
191
22
|
throw Error(`Cannot resume workflow "${workflowId}": status is "${state.status}", expected "running" or "suspended"`);
|
|
192
|
-
await acquireStandaloneClaimBeforeResume(internals, workflowId);
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
const workflowStartHeaders = await loadWorkflowStartHeaders(internals, workflowId, callbacks), { handle, shouldStop } = await prepareRecoveredWorkflowOrFail(internals, state, callbacks, onRecoveredWorkflow);
|
|
201
|
-
if (shouldStop)
|
|
202
|
-
return handle;
|
|
203
|
-
await callbacks.runSerializedWorkflowStateWrite(workflowId, () => performSerializedResume(internals, {
|
|
204
|
-
workflowId,
|
|
205
|
-
resumeCheckpoint,
|
|
206
|
-
serializedCheckpoint: preparedResumeState.serializedCheckpoint,
|
|
207
|
-
registeredVersionTuple,
|
|
208
|
-
restoredHead,
|
|
209
|
-
workflowStartHeaders,
|
|
210
|
-
registration,
|
|
211
|
-
resolvedRevision,
|
|
212
|
-
expectedGeneration: deriveResumeGeneration(state),
|
|
213
|
-
callbacks
|
|
214
|
-
}));
|
|
215
|
-
if (dispatchResumedEvent)
|
|
216
|
-
callbacks.dispatchEvent(new WorkflowResumedEvent(workflowId, resumeCheckpoint.step));
|
|
217
|
-
if (internals.inlineStrategy)
|
|
218
|
-
callbacks.swallowPromiseRejection(callbacks.processPendingUpdatesAfterInlineAdvance(workflowId));
|
|
219
|
-
return handle;
|
|
23
|
+
const acquireResult = await acquireStandaloneClaimBeforeResume(internals, workflowId);
|
|
24
|
+
recordFreshClaimEpoch(acquireResult, options?.freshClaimTracker);
|
|
25
|
+
try {
|
|
26
|
+
return await performResumeAfterClaimAcquired(internals, workflowId, state, dispatchResumedEvent, callbacks, onRecoveredWorkflow);
|
|
27
|
+
} catch (error) {
|
|
28
|
+
await releaseFreshClaimOnRejectionUnlessDeferred(internals, workflowId, acquireResult, options?.deferClaimReleaseOnRejection);
|
|
29
|
+
throw error;
|
|
30
|
+
}
|
|
220
31
|
}
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*
|
|
11
11
|
* Deliberately covers `running` and `suspended` the same way. The ADR
|
|
12
12
|
* describes `suspended` as folding `acquire` into the status flip itself
|
|
13
|
-
* (`reactivateSuspendedWorkflowState`'s commit in `resume.ts`), but the
|
|
13
|
+
* (`reactivateSuspendedWorkflowState`'s commit in `resume-body.ts`), but the
|
|
14
14
|
* EARLIER fenced writes above need a held claim regardless of final status,
|
|
15
15
|
* so one early standalone acquire is the pragmatic shape for both — a
|
|
16
16
|
* documented deviation from that row, not an oversight.
|
|
@@ -27,10 +27,76 @@
|
|
|
27
27
|
* re-check (it can win the CAS against a still-valid claim), so only the
|
|
28
28
|
* durable holder is re-read (no write) and compared.
|
|
29
29
|
*
|
|
30
|
+
* **`'holder-absent'` falls through to a fresh `acquire()` instead of hard-
|
|
31
|
+
* failing (WFT-134).** `suspendWorkflow`'s external terminal rotation
|
|
32
|
+
* durably deletes `wf-owner-holder:<id>` as part of its suspend commit, which
|
|
33
|
+
* can outpace this engine's own local cache correction
|
|
34
|
+
* (`WorkflowClaimRegistry.forgetLocalClaim`). A same-engine `resume()`
|
|
35
|
+
* landing in that gap must not be treated as a real conflict — it is
|
|
36
|
+
* exactly the "no other holder to lose to" case a fresh `acquire()` handles
|
|
37
|
+
* safely. `'holder-undecodable'` and `'generation-mismatch'` remain hard
|
|
38
|
+
* failures: both mean a holder record IS present and either corrupt or
|
|
39
|
+
* naming a different generation, a real conflict this function must not
|
|
40
|
+
* paper over.
|
|
41
|
+
*
|
|
30
42
|
* @module core/engine/lifecycle/standalone-claim-acquire
|
|
31
43
|
*/
|
|
32
44
|
import type { EngineInternals } from '../internals.ts';
|
|
33
|
-
|
|
45
|
+
/**
|
|
46
|
+
* Result of {@link acquireStandaloneClaimBeforeResume}. `freshlyAcquired:
|
|
47
|
+
* true` means this call itself performed a fresh `registry.acquire()`
|
|
48
|
+
* (whether because no claim was cached yet, or because the `'holder-absent'`
|
|
49
|
+
* fallback below ran), and carries the exact `epoch` that acquire minted —
|
|
50
|
+
* `freshlyAcquired: false` means no acquisition happened at all (folding
|
|
51
|
+
* disabled, no registry, or the cached claim matched). Callers use this to
|
|
52
|
+
* know whether, and under which exact generation, THEY are now responsible
|
|
53
|
+
* for releasing a claim this call just installed if the resume they were
|
|
54
|
+
* preparing turns out not to be resumable after all — see `resume.ts`'s
|
|
55
|
+
* `resumeWorkflowFromStorage` (WFT-134).
|
|
56
|
+
*/
|
|
57
|
+
export type StandaloneClaimAcquireResult = {
|
|
58
|
+
freshlyAcquired: true;
|
|
59
|
+
epoch: number;
|
|
60
|
+
} | {
|
|
61
|
+
freshlyAcquired: false;
|
|
62
|
+
};
|
|
63
|
+
export declare function acquireStandaloneClaimBeforeResume(internals: EngineInternals, workflowId: string): Promise<StandaloneClaimAcquireResult>;
|
|
64
|
+
/**
|
|
65
|
+
* Best-effort durable release of a claim {@link acquireStandaloneClaimBeforeResume}
|
|
66
|
+
* freshly acquired for THIS resume attempt, called when a later, status-aware
|
|
67
|
+
* check (`performSerializedResume`'s status/generation re-validation in
|
|
68
|
+
* `resume-body.ts`) rejects the resume after all (WFT-134). Without this, a claim
|
|
69
|
+
* acquired for a workflow that turned out to be non-resumable — already
|
|
70
|
+
* terminal, or replaced by an `onTerminalConflict: 'start-new'` run — stays
|
|
71
|
+
* installed: the renewal task keeps refreshing it indefinitely, and a
|
|
72
|
+
* legitimate `start-new` (or the replacement run's own fold-acquire) loses
|
|
73
|
+
* its CAS against a claim nothing is actually driving.
|
|
74
|
+
*
|
|
75
|
+
* `acquiredEpoch` MUST be the exact epoch {@link acquireStandaloneClaimBeforeResume}
|
|
76
|
+
* returned to this same caller — passed straight through to
|
|
77
|
+
* `WorkflowClaimRegistry.release()`'s own `expectedEpoch` guard (WFT-134
|
|
78
|
+
* review round 2). Between this resume's acquire and its rejection, a
|
|
79
|
+
* DIFFERENT `start-new` or resume can legitimately replace the registry's
|
|
80
|
+
* entry for `workflowId`; releasing "whatever is current" instead of this
|
|
81
|
+
* exact generation would drop that replacement's live claim and strand it
|
|
82
|
+
* without local epoch bytes. `release()` no-ops (`'not-held'`) when the
|
|
83
|
+
* registry has moved past `acquiredEpoch`, so this call is always safe to
|
|
84
|
+
* make regardless of what has happened to the entry since.
|
|
85
|
+
*
|
|
86
|
+
* A full durable `release()`, not a local-only `forgetLocalClaim()`: the
|
|
87
|
+
* acquire this undoes durably wrote BOTH `wf-owner-epoch:<id>` and
|
|
88
|
+
* `wf-owner-holder:<id>`, and it is specifically the durable holder record
|
|
89
|
+
* that would otherwise block a legitimate replacement's own acquire — merely
|
|
90
|
+
* clearing this engine's local cache would leave that durable block in place.
|
|
91
|
+
* Swallows a lost-race/thrown release the same way every other best-effort
|
|
92
|
+
* claim release in this codebase does (see `complete.ts`'s
|
|
93
|
+
* `releaseWorkflowClaimAfterTerminalSettlement`): a failed release here just
|
|
94
|
+
* leaves the claim for TTL/grace expiry, never worse than not attempting it.
|
|
95
|
+
* `release()` itself already forgets its LOCAL entry on a thrown durable
|
|
96
|
+
* error (identity/epoch-guarded), so this call needs no matching cleanup of
|
|
97
|
+
* its own to stop the renewal task from renewing a claim nothing drives.
|
|
98
|
+
*/
|
|
99
|
+
export declare function releaseFreshlyAcquiredResumeClaim(internals: EngineInternals, workflowId: string, acquiredEpoch: number): Promise<void>;
|
|
34
100
|
/**
|
|
35
101
|
* Standalone-acquire `workflowId`'s claim (see {@link acquireStandaloneClaimBeforeResume},
|
|
36
102
|
* a no-op once already held) and hydrate its terminal-cleanup tracking from
|
|
@@ -3,10 +3,10 @@ import { WorkflowClaimUnavailableError } from "../lease-errors.js";
|
|
|
3
3
|
import { wakeOwnershipCheck } from "../wake-ownership-check.js";
|
|
4
4
|
export async function acquireStandaloneClaimBeforeResume(internals, workflowId) {
|
|
5
5
|
if (internals.options.ownershipMode !== "workflow-lease")
|
|
6
|
-
return;
|
|
6
|
+
return { freshlyAcquired: !1 };
|
|
7
7
|
const registry = internals.workflowClaimRegistry;
|
|
8
8
|
if (registry === null)
|
|
9
|
-
return;
|
|
9
|
+
return { freshlyAcquired: !1 };
|
|
10
10
|
const cachedEpoch = registry.currentEpoch(workflowId);
|
|
11
11
|
if (cachedEpoch !== null) {
|
|
12
12
|
const check = await wakeOwnershipCheck({
|
|
@@ -16,13 +16,23 @@ export async function acquireStandaloneClaimBeforeResume(internals, workflowId)
|
|
|
16
16
|
expectedEngineId: registry.engineId,
|
|
17
17
|
expectedEpoch: cachedEpoch
|
|
18
18
|
});
|
|
19
|
-
if (check.status === "
|
|
19
|
+
if (check.status === "match")
|
|
20
|
+
return { freshlyAcquired: !1 };
|
|
21
|
+
if (check.reason !== "holder-absent")
|
|
20
22
|
throw new WorkflowClaimUnavailableError(workflowId, check.observedEngineId);
|
|
21
|
-
return;
|
|
22
23
|
}
|
|
23
24
|
const result = await registry.acquire(workflowId);
|
|
24
25
|
if (result.status === "lost-race")
|
|
25
26
|
throw new WorkflowClaimUnavailableError(workflowId, result.heldBy);
|
|
27
|
+
return { freshlyAcquired: !0, epoch: result.epoch };
|
|
28
|
+
}
|
|
29
|
+
export async function releaseFreshlyAcquiredResumeClaim(internals, workflowId, acquiredEpoch) {
|
|
30
|
+
const registry = internals.workflowClaimRegistry;
|
|
31
|
+
if (registry === null)
|
|
32
|
+
return;
|
|
33
|
+
try {
|
|
34
|
+
await registry.release(workflowId, acquiredEpoch);
|
|
35
|
+
} catch {}
|
|
26
36
|
}
|
|
27
37
|
export async function ensureDelayedStartClaimAndCleanupBeforeFailure(internals, workflowId) {
|
|
28
38
|
await acquireStandaloneClaimBeforeResume(internals, workflowId);
|
|
@@ -59,6 +59,8 @@ export type StartBatchContext = {
|
|
|
59
59
|
* path.
|
|
60
60
|
*/
|
|
61
61
|
duplicateIdCondition: ConditionalBatchCondition | undefined;
|
|
62
|
+
/** ADDITIONAL WFT-153 precondition on observed `wf-gen:<id>` bytes; see `StartDuplicateIdDecision.duplicateIdGenerationCondition` in `start-terminal-conflict-purge.ts`. Undefined exactly when `duplicateIdCondition` is. */
|
|
63
|
+
duplicateIdGenerationCondition: ConditionalBatchCondition | undefined;
|
|
62
64
|
};
|
|
63
65
|
/**
|
|
64
66
|
* Assemble the start batch — folding in the id-dependent idempotency mapping and
|
|
@@ -49,9 +49,9 @@ async function persistStartBatch(internals, workflowId, startOperations, conditi
|
|
|
49
49
|
requireStorageCapability(internals.storage, "conditionalBatch", "start preconditions");
|
|
50
50
|
return await commitFencedEngineWriteAllowingPreconditionFailure(internals, fenceWorkflowId, startOperations, conditions.map((entry) => entry.condition)) ? "committed" : "precondition-lost";
|
|
51
51
|
}
|
|
52
|
-
async function hasStartConditionConflict(internals, conditions,
|
|
52
|
+
async function hasStartConditionConflict(internals, conditions, sources) {
|
|
53
53
|
for (const entry of conditions) {
|
|
54
|
-
if (entry.source
|
|
54
|
+
if (!sources.includes(entry.source))
|
|
55
55
|
continue;
|
|
56
56
|
const currentValue = await internals.storage.get(entry.condition.key);
|
|
57
57
|
if (!storageValuesEqual(currentValue, entry.condition.expectedValue))
|
|
@@ -75,8 +75,13 @@ function tagStartPreconditions(conditions) {
|
|
|
75
75
|
condition
|
|
76
76
|
}));
|
|
77
77
|
}
|
|
78
|
-
function
|
|
79
|
-
|
|
78
|
+
function tagDuplicateIdConditions(condition, generationCondition) {
|
|
79
|
+
const tagged = [];
|
|
80
|
+
if (condition !== void 0)
|
|
81
|
+
tagged.push({ source: "duplicate-id", condition });
|
|
82
|
+
if (generationCondition !== void 0)
|
|
83
|
+
tagged.push({ source: "duplicate-id-generation", condition: generationCondition });
|
|
84
|
+
return tagged;
|
|
80
85
|
}
|
|
81
86
|
function tagWorkflowConcurrencyConditions(conditions) {
|
|
82
87
|
return conditions.map((condition) => ({
|
|
@@ -97,21 +102,24 @@ export async function buildAndCommitStartBatch(context, buildIdempotentStartOper
|
|
|
97
102
|
lastWorkflowConcurrencyStateKey = workflowConcurrency?.stateKey ?? lastWorkflowConcurrencyStateKey;
|
|
98
103
|
const catalogEntryPrecondition = needsCatalogEntryStartPrecondition(internals, state) ? await buildCatalogEntryStartPrecondition(internals, state) : void 0, startOperations = buildStartBatchOperations(internals, workflowId, state, checkpoint, registration, options, state.executionDeadline, context.delayedStartTimer, context.persistedWorkflowStartHeaders, mergeAdditionalStartOperations(context.additionalStartOperations, mergeAdditionalStartOperations(idempotent?.operations, workflowConcurrency?.operations)), context.callbacks, context.purgeDeleteOperations), conditions = [
|
|
99
104
|
...tagStartPreconditions(idempotent?.conditions),
|
|
100
|
-
...
|
|
105
|
+
...tagDuplicateIdConditions(context.duplicateIdCondition, context.duplicateIdGenerationCondition),
|
|
101
106
|
...tagWorkflowConcurrencyConditions(workflowConcurrency?.conditions ?? []),
|
|
102
107
|
...catalogEntryPrecondition === void 0 ? [] : [catalogEntryPrecondition]
|
|
103
108
|
], isDelayedStart = context.delayedStartTimer !== void 0, claimFold = isDelayedStart ? void 0 : await prepareWorkflowClaimFold(internals, workflowId), outcome = await persistStartBatch(internals, workflowId, startOperations, conditions, claimFold, isDelayedStart);
|
|
104
109
|
if (outcome === "committed")
|
|
105
110
|
return;
|
|
106
|
-
if (outcome !== "claim-lost" && await hasStartConditionConflict(internals, conditions,
|
|
111
|
+
if (outcome !== "claim-lost" && await hasStartConditionConflict(internals, conditions, [
|
|
112
|
+
"duplicate-id",
|
|
113
|
+
"duplicate-id-generation"
|
|
114
|
+
]))
|
|
107
115
|
throw new WorkflowAlreadyExistsError(workflowId);
|
|
108
|
-
if (await hasStartConditionConflict(internals, conditions, "start-precondition"))
|
|
116
|
+
if (await hasStartConditionConflict(internals, conditions, ["start-precondition"]))
|
|
109
117
|
throw new StartIdempotencyRaceLostError;
|
|
110
118
|
if (await hasCatalogEntryConflict(internals, conditions))
|
|
111
119
|
throw new WorkflowRevisionUnavailableError(state.type, state.revision, "not-installed");
|
|
112
120
|
if (outcome === "claim-lost")
|
|
113
121
|
return throwWorkflowClaimUnavailable(internals, workflowId);
|
|
114
|
-
await attributeLostStartPreconditionOrRetry(workflowId, context.duplicateIdCondition !== void 0, workflowConcurrency !== void 0, () => hasStartConditionConflict(internals, conditions, "workflow-concurrency"));
|
|
122
|
+
await attributeLostStartPreconditionOrRetry(workflowId, context.duplicateIdCondition !== void 0, workflowConcurrency !== void 0, () => hasStartConditionConflict(internals, conditions, ["workflow-concurrency"]));
|
|
115
123
|
}
|
|
116
124
|
throw new AtomicStateConflictError(lastWorkflowConcurrencyStateKey ?? "workflow concurrency admission", WORKFLOW_CONCURRENCY_ADMISSION_MAX_ATTEMPTS);
|
|
117
125
|
}
|
|
@@ -41,11 +41,17 @@ export declare function resolveExistingRunOrThrowPurged(internals: EngineInterna
|
|
|
41
41
|
* resolves it instead of racing it to a terminal-conflict). Only when the record is
|
|
42
42
|
* absent do we wait for the reservation to clear and read once more to discriminate:
|
|
43
43
|
*
|
|
44
|
-
* - **record present** — the winner committed: signal it (or conflict if terminal)
|
|
45
|
-
* and return the handle.
|
|
44
|
+
* - **record present** — the winner committed: signal it (or conflict if terminal).
|
|
46
45
|
* - **record absent after the reservation clears** — the winner aborted before
|
|
47
46
|
* committing (storage failure, oversized payload, throwing start interceptor): no
|
|
48
47
|
* run exists, so return `undefined` and let the caller retry its own create.
|
|
48
|
+
*
|
|
49
|
+
* Either terminal-conflict branch is checked against {@link wasSignalAcceptedByWinner}
|
|
50
|
+
* before throwing: a fast workflow can reach a terminal status between this
|
|
51
|
+
* loser's reads, entirely independent of how much slower or faster the winner's
|
|
52
|
+
* OWN commit path happens to be — so a same-`signalId` convergent caller (the
|
|
53
|
+
* documented "concurrent absent-target callers" contract) must not depend on
|
|
54
|
+
* catching the winner mid-flight to avoid a spurious conflict.
|
|
49
55
|
*/
|
|
50
56
|
export declare function resolveCallerIdWinnerOrRetry(internals: EngineInternals, winnerId: string, signalSpec: StartOrSignalSignal, signalId: string, callbacks: StartOrSignalCallbacks, allowTerminalRestart?: boolean): Promise<WorkflowHandle | undefined>;
|
|
51
57
|
/**
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { sleep } from "../../../runtime/portable.js";
|
|
2
|
-
import { KEYS } from "../../../storage/interface.js";
|
|
2
|
+
import { KEYS, storageHas } from "../../../storage/interface.js";
|
|
3
3
|
import { decode } from "../../codec.js";
|
|
4
4
|
import { IdempotencyKeyPurgedError, StartOrSignalConflictError } from "../errors.js";
|
|
5
5
|
import { loadWorkflowState } from "../storage-io.js";
|
|
@@ -23,6 +23,14 @@ async function awaitReservationCleared(internals, workflowId) {
|
|
|
23
23
|
await sleep(RESERVATION_CLEAR_RETRY_DELAY_MS);
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
|
+
async function wasSignalAcceptedByWinner(internals, workflowId, signalName, signalId) {
|
|
27
|
+
return storageHas(internals.storage, KEYS.signalAcceptedResponse(workflowId, signalName, signalId));
|
|
28
|
+
}
|
|
29
|
+
async function resolveTerminalConflictOrConvergence(internals, winnerId, status, signalSpec, signalId, callbacks) {
|
|
30
|
+
if (await wasSignalAcceptedByWinner(internals, winnerId, signalSpec.name, signalId))
|
|
31
|
+
return callbacks.getHandle(winnerId);
|
|
32
|
+
throw new StartOrSignalConflictError(winnerId, status);
|
|
33
|
+
}
|
|
26
34
|
export async function resolveCallerIdWinnerOrRetry(internals, winnerId, signalSpec, signalId, callbacks, allowTerminalRestart = !1) {
|
|
27
35
|
const state = await loadWorkflowState(internals, winnerId);
|
|
28
36
|
if (state !== null) {
|
|
@@ -31,7 +39,7 @@ export async function resolveCallerIdWinnerOrRetry(internals, winnerId, signalSp
|
|
|
31
39
|
return callbacks.getHandle(winnerId);
|
|
32
40
|
}
|
|
33
41
|
if (!allowTerminalRestart)
|
|
34
|
-
|
|
42
|
+
return resolveTerminalConflictOrConvergence(internals, winnerId, state.status, signalSpec, signalId, callbacks);
|
|
35
43
|
}
|
|
36
44
|
await awaitReservationCleared(internals, winnerId);
|
|
37
45
|
const stateAfterReservation = await loadWorkflowState(internals, winnerId);
|
|
@@ -40,7 +48,7 @@ export async function resolveCallerIdWinnerOrRetry(internals, winnerId, signalSp
|
|
|
40
48
|
if (isTerminalWorkflowStatus(stateAfterReservation.status)) {
|
|
41
49
|
if (state !== null && isSameTerminalRun(state, stateAfterReservation))
|
|
42
50
|
return;
|
|
43
|
-
|
|
51
|
+
return resolveTerminalConflictOrConvergence(internals, winnerId, stateAfterReservation.status, signalSpec, signalId, callbacks);
|
|
44
52
|
}
|
|
45
53
|
await callbacks.signalExistingWorkflow(winnerId, signalSpec.name, signalSpec.payload, signalId);
|
|
46
54
|
return callbacks.getHandle(winnerId);
|
|
@@ -37,10 +37,16 @@
|
|
|
37
37
|
* expectation — the id is free right now, and a retry re-conditions on that same
|
|
38
38
|
* still-matching value.
|
|
39
39
|
*
|
|
40
|
-
* The
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
* this
|
|
40
|
+
* The pre-compare-and-swap purge ABA — a winner purged between the read and the
|
|
41
|
+
* commit, making an absent record look never-used to a value-comparing condition
|
|
42
|
+
* alone — no longer reaches this function's elimination logic (WFT-153, closing
|
|
43
|
+
* the residual this comment used to describe as untracked). The caller now checks
|
|
44
|
+
* a SECOND, positive-evidence condition first (`'duplicate-id-generation'` in
|
|
45
|
+
* `buildAndCommitStartBatch`) on a durable per-id generation counter that a purge
|
|
46
|
+
* bumps but never resets, so that case throws `WorkflowAlreadyExistsError` before
|
|
47
|
+
* reaching here. This function still does not claim exclusive attribution for
|
|
48
|
+
* every conceivable cause — only that the two positively-detectable duplicate-id
|
|
49
|
+
* causes are ruled out ahead of it.
|
|
44
50
|
*
|
|
45
51
|
* With no concurrency evidence at all, fail closed: something missed, nothing
|
|
46
52
|
* retryable explains it, and a spurious `WorkflowAlreadyExistsError` is public,
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { StartOptions } from '../../types.ts';
|
|
2
|
+
import type { EngineInternals } from '../internals.ts';
|
|
3
|
+
import { type LifecycleCallbacks } from './shared.ts';
|
|
4
|
+
/**
|
|
5
|
+
* Resolve a start's effective delayed-start timestamp from
|
|
6
|
+
* `options.startAt`/`options.startAfter`, or `undefined` for an immediate
|
|
7
|
+
* start. Split out of `start.ts` to keep that file under the repository's
|
|
8
|
+
* line-count ceiling; this is a pure resolution step with no dependency on
|
|
9
|
+
* `startWorkflow`'s own reservation/commit state.
|
|
10
|
+
*/
|
|
11
|
+
export declare function resolveScheduledStartAt(internals: EngineInternals, options: StartOptions | undefined, submissionTime: number, callbacks: LifecycleCallbacks): number | undefined;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { normalizeStorageTimestamp } from "../../scheduler.js";
|
|
2
|
+
import {
|
|
3
|
+
assertExclusiveStartWorkflowOptions,
|
|
4
|
+
coerceStartWorkflowTimestamp,
|
|
5
|
+
StartWorkflowValidationError
|
|
6
|
+
} from "../../start-workflow-validation.js";
|
|
7
|
+
import { parseStartOptionDuration } from "./start-state.js";
|
|
8
|
+
export function resolveScheduledStartAt(internals, options, submissionTime, callbacks) {
|
|
9
|
+
assertExclusiveStartWorkflowOptions(options?.startAt, options?.startAfter);
|
|
10
|
+
if (options?.startAt !== void 0)
|
|
11
|
+
return coerceStartWorkflowTimestamp(options.startAt, "options.startAt");
|
|
12
|
+
if (options?.startAfter !== void 0) {
|
|
13
|
+
const startAfterMilliseconds = parseStartOptionDuration(internals, options.startAfter, "options.startAfter", callbacks);
|
|
14
|
+
try {
|
|
15
|
+
return normalizeStorageTimestamp(submissionTime + startAfterMilliseconds, "options.startAfter");
|
|
16
|
+
} catch {
|
|
17
|
+
throw new StartWorkflowValidationError("options.startAfter must resolve to a finite, non-negative start time");
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return;
|
|
21
|
+
}
|