@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
|
@@ -38,17 +38,41 @@ export type StartDuplicateIdDecision = {
|
|
|
38
38
|
* capability at `Engine.create()`. A store that cannot honour this condition
|
|
39
39
|
* cannot host an engine that could start a workflow in the first place.
|
|
40
40
|
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
* the indefinite hang this fence exists to remove.
|
|
41
|
+
* RESOLVED (PR #959 review; closed by WFT-153). This condition alone compares
|
|
42
|
+
* only a VALUE, so on its own it cannot distinguish "this id was never used"
|
|
43
|
+
* from "a run existed here and was purged" — a racing winner that commits,
|
|
44
|
+
* completes, and is purged (or swept by retention) before this batch commits
|
|
45
|
+
* would make `wf:<id>` absent again, matching `expectedValue: null`. That gap
|
|
46
|
+
* is now closed by {@link duplicateIdGenerationCondition} below, an ADDITIONAL
|
|
47
|
+
* condition on a durable per-id generation counter a purge bumps but never
|
|
48
|
+
* resets — see its own doc for the mechanism.
|
|
50
49
|
*/
|
|
51
50
|
duplicateIdCondition: ConditionalBatchCondition;
|
|
51
|
+
/**
|
|
52
|
+
* ADDITIONAL compare-and-swap precondition on the observed `wf-gen:<id>`
|
|
53
|
+
* bytes, closing the residual ABA hole {@link duplicateIdCondition} cannot
|
|
54
|
+
* detect (WFT-153, following WFT-152). `duplicateIdCondition` compares a
|
|
55
|
+
* VALUE, so it cannot tell "this id was never used" from "a run existed
|
|
56
|
+
* here and was purged" — if a racing winner completes and is purged before
|
|
57
|
+
* this batch commits, `wf:<id>` looks absent again and `duplicateIdCondition`
|
|
58
|
+
* alone would match. `wf-gen:<id>` is bumped in the SAME atomic batch that
|
|
59
|
+
* deletes `wf:<id>` on purge (`workflow-generation-fence.ts`) and is never
|
|
60
|
+
* otherwise touched, so a purge landing in the read-to-commit gap changes
|
|
61
|
+
* this key even though `wf:<id>` reads the same — the stale loser's
|
|
62
|
+
* condition on the pre-purge generation bytes fails where the value-only
|
|
63
|
+
* comparison could not detect it. See `storage/generation-keys.ts`.
|
|
64
|
+
*/
|
|
65
|
+
duplicateIdGenerationCondition: ConditionalBatchCondition;
|
|
66
|
+
/**
|
|
67
|
+
* The exact `wf-gen:<id>` bytes {@link duplicateIdGenerationCondition} was
|
|
68
|
+
* built from, threaded through to {@link prepareTerminalRunPurge} so a
|
|
69
|
+
* `'start-new'` restart's own displacing purge bumps the generation from
|
|
70
|
+
* this SAME observed value rather than a second, independent read. This is
|
|
71
|
+
* what makes the restart's own CAS trivially self-consistent — it can never
|
|
72
|
+
* fence itself out on its own legitimate restart, because the value it
|
|
73
|
+
* bumps from is exactly the value its own precondition checks.
|
|
74
|
+
*/
|
|
75
|
+
observedGenerationBytes: Uint8Array | null;
|
|
52
76
|
};
|
|
53
77
|
/**
|
|
54
78
|
* The decision for a start whose id was GENERATED rather than caller-supplied. A
|
|
@@ -59,6 +83,8 @@ export type StartDuplicateIdDecision = {
|
|
|
59
83
|
export declare const GENERATED_ID_START_DECISION: {
|
|
60
84
|
readonly terminalRunToPurge: null;
|
|
61
85
|
readonly duplicateIdCondition: undefined;
|
|
86
|
+
readonly duplicateIdGenerationCondition: undefined;
|
|
87
|
+
readonly observedGenerationBytes: null;
|
|
62
88
|
};
|
|
63
89
|
/**
|
|
64
90
|
* Decide what a caller-supplied workflow id that already has a persisted record
|
|
@@ -86,9 +112,53 @@ export declare const GENERATED_ID_START_DECISION: {
|
|
|
86
112
|
* batch. The returned {@link StartDuplicateIdDecision} therefore carries a
|
|
87
113
|
* `duplicateIdCondition` holding the exact bytes seen here, so that batch can be
|
|
88
114
|
* conditioned on them — turning that window into a lost compare-and-swap rather
|
|
89
|
-
* than a blind overwrite (WFT-152).
|
|
115
|
+
* than a blind overwrite (WFT-152). It also reads `wf-gen:<id>` in the SAME pass
|
|
116
|
+
* and carries `duplicateIdGenerationCondition`/`observedGenerationBytes` (WFT-153),
|
|
117
|
+
* closing the residual ABA window a value-only condition cannot detect — see that
|
|
118
|
+
* field's own doc.
|
|
90
119
|
*/
|
|
91
120
|
export declare function resolveTerminalConflictForRestart(internals: EngineInternals, workflowId: string, options: StartWorkflowOptions | undefined): Promise<StartDuplicateIdDecision>;
|
|
121
|
+
/**
|
|
122
|
+
* The `'reattach-only'`/`'bulk-retry-only'` fence for `startWorkflow`'s
|
|
123
|
+
* `skipAdmissionIdCheck` parameter (WFT-95 TOCTOU fix). Call immediately
|
|
124
|
+
* after {@link resolveTerminalConflictForRestart} resolves.
|
|
125
|
+
*
|
|
126
|
+
* Both call sites reach `startWorkflow` after a separate, non-atomic read
|
|
127
|
+
* confirmed an already-persisted record they mean to replay or replace — but
|
|
128
|
+
* that confirmation and `resolveTerminalConflictForRestart`'s own atomic read
|
|
129
|
+
* are not the same read:
|
|
130
|
+
*
|
|
131
|
+
* - `dispatchChildWorkflowStart()`'s crash-reattach retry confirms a matching
|
|
132
|
+
* persisted child via `loadWorkflowState()`, then leaves
|
|
133
|
+
* `options.onTerminalConflict` unset — so `resolveTerminalConflictForRestart`
|
|
134
|
+
* either finds the record still there (throws {@link WorkflowAlreadyExistsError},
|
|
135
|
+
* this function is never reached: the expected, unraced reattach) or gone
|
|
136
|
+
* (`terminalRunToPurge: null`, the ordinary fresh-create branch).
|
|
137
|
+
* - `retryFailedWorkflow()`'s checkpoint-absent fallback (`bulk-operations-retry.ts`)
|
|
138
|
+
* confirms a `failed` record via its own `loadWorkflowState()`, then always
|
|
139
|
+
* sets `options.onTerminalConflict: 'start-new'` — so
|
|
140
|
+
* `resolveTerminalConflictForRestart` either finds the SAME terminal record
|
|
141
|
+
* still there (returns its bytes as `terminalRunToPurge`, the expected,
|
|
142
|
+
* unraced purge-and-replace) or finds it gone (`terminalRunToPurge: null`,
|
|
143
|
+
* the same ordinary fresh-create branch).
|
|
144
|
+
*
|
|
145
|
+
* Under `ownership: 'workflow-lease'`, another engine can purge the matched
|
|
146
|
+
* record in the window between either call site's own confirmation read and
|
|
147
|
+
* `resolveTerminalConflictForRestart`'s. Being called with a `'reattach-only'`
|
|
148
|
+
* or `'bulk-retry-only'` `skipAdmissionIdCheck` and a `null` `terminalRunToPurge`
|
|
149
|
+
* therefore means the race happened: there is nothing left to reattach to or
|
|
150
|
+
* replace, so this re-runs strict admission rather than let a bypassed
|
|
151
|
+
* `.`/`..` id fall through into a genuinely fresh create. `coerceStartWorkflowId`
|
|
152
|
+
* throws the same `StartWorkflowValidationError` strict admission would have
|
|
153
|
+
* thrown on the caller's very first (non-retry) attempt — a clean,
|
|
154
|
+
* deterministic rejection instead of a silently created reserved-id run.
|
|
155
|
+
*
|
|
156
|
+
* A no-op for every other `skipAdmissionIdCheck` value: `true` (schedule
|
|
157
|
+
* drain) applies unconditionally and never calls this, and `undefined`
|
|
158
|
+
* (every public start surface) already went through strict admission in
|
|
159
|
+
* `prepareStartWorkflow`.
|
|
160
|
+
*/
|
|
161
|
+
export declare function enforceReplayOnlyIdFence(skipAdmissionIdCheck: boolean | 'reattach-only' | 'bulk-retry-only' | undefined, workflowId: string, terminalRunToPurge: WorkflowState | null): void;
|
|
92
162
|
/**
|
|
93
163
|
* Prepare a prior terminal run for displacement by a `'start-new'` restart WITHOUT
|
|
94
164
|
* committing the destructive delete. Returns the storage delete operations (for the
|
|
@@ -98,5 +168,14 @@ export declare function resolveTerminalConflictForRestart(internals: EngineInter
|
|
|
98
168
|
* entries. `clearPurgedWorkflowInMemoryState` runs `cleanupWaiters` to settle the
|
|
99
169
|
* old run's pending signal/update/sleep waiters; it only needs
|
|
100
170
|
* `swallowPromiseRejection`, which `LifecycleCallbacks` already exposes.
|
|
171
|
+
*
|
|
172
|
+
* Also appends the `wf-gen:<id>` bump PUT operation (WFT-153), built from
|
|
173
|
+
* `observedGenerationBytes` — the SAME bytes `resolveTerminalConflictForRestart`
|
|
174
|
+
* already read for `duplicateIdGenerationCondition` — rather than a second,
|
|
175
|
+
* independent read. Reusing that one observed value for both the outer CAS
|
|
176
|
+
* condition and the bump amount is what makes this restart's own commit
|
|
177
|
+
* trivially self-consistent: it can never fence itself out on its own
|
|
178
|
+
* legitimate restart, because the value it bumps from is exactly the value its
|
|
179
|
+
* own precondition checks (see `duplicateIdGenerationCondition`'s doc).
|
|
101
180
|
*/
|
|
102
|
-
export declare function prepareTerminalRunPurge(internals: EngineInternals, state: WorkflowState, callbacks: LifecycleCallbacks): Promise<BatchOperation[]>;
|
|
181
|
+
export declare function prepareTerminalRunPurge(internals: EngineInternals, state: WorkflowState, callbacks: LifecycleCallbacks, observedGenerationBytes: Uint8Array | null): Promise<BatchOperation[]>;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { KEYS, storageHas } from "../../../storage/interface.js";
|
|
2
|
+
import { coerceStartWorkflowId } from "../../start-workflow-validation.js";
|
|
2
3
|
import {
|
|
3
4
|
clearPurgedWorkflowInMemoryState,
|
|
4
5
|
collectWorkflowPurgeDeleteOperations
|
|
@@ -6,16 +7,27 @@ import {
|
|
|
6
7
|
import { WorkflowAlreadyExistsError, WorkflowTeardownPendingError } from "../errors.js";
|
|
7
8
|
import { cleanupWaiters } from "../termination/cleanup.js";
|
|
8
9
|
import { decodeWorkflowState, isTerminalWorkflowStatus } from "../validation.js";
|
|
10
|
+
import { buildWorkflowGenerationBumpOperation } from "../workflow-generation-fence.js";
|
|
9
11
|
export const GENERATED_ID_START_DECISION = {
|
|
10
12
|
terminalRunToPurge: null,
|
|
11
|
-
duplicateIdCondition: void 0
|
|
13
|
+
duplicateIdCondition: void 0,
|
|
14
|
+
duplicateIdGenerationCondition: void 0,
|
|
15
|
+
observedGenerationBytes: null
|
|
12
16
|
};
|
|
13
17
|
export async function resolveTerminalConflictForRestart(internals, workflowId, options) {
|
|
14
|
-
const key = KEYS.workflow(workflowId), existingBytes = await
|
|
18
|
+
const key = KEYS.workflow(workflowId), generationKey = KEYS.workflowGeneration(workflowId), [existingBytes, observedGenerationBytes] = await Promise.all([
|
|
19
|
+
internals.storage.get(key),
|
|
20
|
+
internals.storage.get(generationKey)
|
|
21
|
+
]), duplicateIdGenerationCondition = {
|
|
22
|
+
key: generationKey,
|
|
23
|
+
expectedValue: observedGenerationBytes
|
|
24
|
+
};
|
|
15
25
|
if (existingBytes === null)
|
|
16
26
|
return {
|
|
17
27
|
terminalRunToPurge: null,
|
|
18
|
-
duplicateIdCondition: { key, expectedValue: null }
|
|
28
|
+
duplicateIdCondition: { key, expectedValue: null },
|
|
29
|
+
duplicateIdGenerationCondition,
|
|
30
|
+
observedGenerationBytes
|
|
19
31
|
};
|
|
20
32
|
if (options?.onTerminalConflict !== "start-new")
|
|
21
33
|
throw new WorkflowAlreadyExistsError(workflowId);
|
|
@@ -26,13 +38,20 @@ export async function resolveTerminalConflictForRestart(internals, workflowId, o
|
|
|
26
38
|
throw new WorkflowTeardownPendingError(workflowId);
|
|
27
39
|
return {
|
|
28
40
|
terminalRunToPurge: existingState,
|
|
29
|
-
duplicateIdCondition: { key, expectedValue: existingBytes }
|
|
41
|
+
duplicateIdCondition: { key, expectedValue: existingBytes },
|
|
42
|
+
duplicateIdGenerationCondition,
|
|
43
|
+
observedGenerationBytes
|
|
30
44
|
};
|
|
31
45
|
}
|
|
32
|
-
export
|
|
46
|
+
export function enforceReplayOnlyIdFence(skipAdmissionIdCheck, workflowId, terminalRunToPurge) {
|
|
47
|
+
if ((skipAdmissionIdCheck === "reattach-only" || skipAdmissionIdCheck === "bulk-retry-only") && terminalRunToPurge === null)
|
|
48
|
+
coerceStartWorkflowId(workflowId, "options.id");
|
|
49
|
+
}
|
|
50
|
+
export async function prepareTerminalRunPurge(internals, state, callbacks, observedGenerationBytes) {
|
|
33
51
|
const cleanupWaitersForStart = (id) => cleanupWaiters(internals, id, {
|
|
34
52
|
swallowPromiseRejection: callbacks.swallowPromiseRejection
|
|
35
53
|
}), deleteOperations = await collectWorkflowPurgeDeleteOperations(internals, state);
|
|
54
|
+
deleteOperations.push(buildWorkflowGenerationBumpOperation(state.id, observedGenerationBytes));
|
|
36
55
|
clearPurgedWorkflowInMemoryState(internals, state.id, cleanupWaitersForStart);
|
|
37
56
|
return deleteOperations;
|
|
38
57
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { BatchOperation } from '../../../storage/interface.ts';
|
|
2
|
-
import type {
|
|
2
|
+
import type { StartWorkflowOptions } from '../../types.ts';
|
|
3
3
|
import { type WorkflowHandle } from '../handles.ts';
|
|
4
4
|
import type { EngineInternals } from '../internals.ts';
|
|
5
5
|
import { type LifecycleCallbacks } from './shared.ts';
|
|
@@ -19,5 +19,42 @@ export declare function startWorkflow(internals: EngineInternals, type: string,
|
|
|
19
19
|
* `undefined` (the default, every non-schedule start) is byte-for-byte
|
|
20
20
|
* the pre-WFT-20 admission path.
|
|
21
21
|
*/
|
|
22
|
-
revisionOverride?: string
|
|
23
|
-
|
|
22
|
+
revisionOverride?: string,
|
|
23
|
+
/**
|
|
24
|
+
* Internal-only, never part of the public `StartOptions` type (WFT-95).
|
|
25
|
+
* When truthy, `options.id` skips strict fresh-admission validation
|
|
26
|
+
* (`assertValidWorkflowId`'s `.`/`..` rejection) and is instead validated
|
|
27
|
+
* with the decode-compatible `assertDecodableWorkflowId`. This exists
|
|
28
|
+
* ONLY to replay an id that was already durably accepted before strict
|
|
29
|
+
* admission existed — never to let a genuinely fresh caller admit `.`/`..`.
|
|
30
|
+
*
|
|
31
|
+
* Set from exactly three internal call sites, all replaying an
|
|
32
|
+
* already-persisted id rather than admitting a new one:
|
|
33
|
+
* - `drainQueuedScheduleRun()` (via `ScheduledRunStartOptions.skipAdmissionIdCheck`,
|
|
34
|
+
* threaded through `startScheduledRun()`), which restarts a schedule's
|
|
35
|
+
* persisted `queuedRuns[].workflowId` — safe unconditionally (`true`),
|
|
36
|
+
* since a queued run created after this fix was already validated as
|
|
37
|
+
* non-`.`/`..` at schedule-admission time, so relaxing the check here
|
|
38
|
+
* is a no-op for it and only matters for a historical pre-WFT-95 queued run.
|
|
39
|
+
* - `retryFailedWorkflow()`'s checkpoint-absent fallback (`bulk-operations-retry.ts`),
|
|
40
|
+
* which rebuilds an already-persisted, already-validated-at-the-time
|
|
41
|
+
* `workflowId` from its stored input via `onTerminalConflict: 'start-new'`.
|
|
42
|
+
* Passes the literal `'bulk-retry-only'` rather than `true` — see
|
|
43
|
+
* {@link enforceReplayOnlyIdFence}'s doc comment for the TOCTOU race that
|
|
44
|
+
* value fences: the retry's own confirmation read and
|
|
45
|
+
* `resolveTerminalConflictForRestart`'s atomic read are not the same
|
|
46
|
+
* read, so a concurrent purge under `ownership: 'workflow-lease'` can
|
|
47
|
+
* leave nothing to purge-and-replace by the time this runs.
|
|
48
|
+
* - `dispatchChildWorkflowStart()`'s crash-reattach retry, which passes
|
|
49
|
+
* the literal `'reattach-only'` rather than `true` — see
|
|
50
|
+
* {@link enforceReplayOnlyIdFence}'s doc comment for the same TOCTOU
|
|
51
|
+
* race (this one only *speculatively* matched an existing record
|
|
52
|
+
* before this call, unlike the schedule-drain site above).
|
|
53
|
+
*
|
|
54
|
+
* Every other caller (REST, JSON-RPC, direct `engine.start()`,
|
|
55
|
+
* `engine.startOrSignal()`, a fresh `ctx.startChild()`) omits this
|
|
56
|
+
* parameter and gets the strict check, because it is not part of
|
|
57
|
+
* `StartOptions`/`StartWorkflowOptions` and therefore cannot be set from
|
|
58
|
+
* any public surface.
|
|
59
|
+
*/
|
|
60
|
+
skipAdmissionIdCheck?: boolean | 'reattach-only' | 'bulk-retry-only'): Promise<WorkflowHandle>;
|
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
import { assertPayloadWithinLimit } from "../../payload-size.js";
|
|
2
|
-
import { normalizeStorageTimestamp } from "../../scheduler.js";
|
|
3
2
|
import {
|
|
4
|
-
StartWorkflowValidationError,
|
|
5
|
-
assertExclusiveStartWorkflowOptions,
|
|
6
3
|
assertValidOnTerminalConflict,
|
|
7
|
-
|
|
8
|
-
|
|
4
|
+
coerceReplayWorkflowId,
|
|
5
|
+
coerceStartWorkflowId
|
|
9
6
|
} from "../../start-workflow-validation.js";
|
|
10
7
|
import {
|
|
11
8
|
releaseInFlightStart,
|
|
@@ -33,6 +30,7 @@ import {
|
|
|
33
30
|
resolveCachedStartRevision,
|
|
34
31
|
resolveStartRevisionUncached
|
|
35
32
|
} from "./start-revision-resolution.js";
|
|
33
|
+
import { resolveScheduledStartAt } from "./start-schedule-timing.js";
|
|
36
34
|
import {
|
|
37
35
|
applyRestartLineage,
|
|
38
36
|
createInitialCheckpoint,
|
|
@@ -40,6 +38,7 @@ import {
|
|
|
40
38
|
parseStartOptionDuration
|
|
41
39
|
} from "./start-state.js";
|
|
42
40
|
import {
|
|
41
|
+
enforceReplayOnlyIdFence,
|
|
43
42
|
GENERATED_ID_START_DECISION,
|
|
44
43
|
prepareTerminalRunPurge,
|
|
45
44
|
resolveTerminalConflictForRestart
|
|
@@ -47,8 +46,8 @@ import {
|
|
|
47
46
|
export async function start(internals, type, input, options, callbacks) {
|
|
48
47
|
return startWorkflow(internals, type, input, options, void 0, callbacks);
|
|
49
48
|
}
|
|
50
|
-
function prepareStartWorkflow(internals, options, callbacks) {
|
|
51
|
-
const callerProvidedId = options?.id !== void 0, workflowId = options?.id !== void 0 ? coerceStartWorkflowId(options.id, "options.id") : crypto.randomUUID(), parentHeaders = internals.pendingParentHeaders;
|
|
49
|
+
function prepareStartWorkflow(internals, options, callbacks, skipAdmissionIdCheck) {
|
|
50
|
+
const callerProvidedId = options?.id !== void 0, workflowId = options?.id !== void 0 ? skipAdmissionIdCheck ? coerceReplayWorkflowId(options.id, "options.id") : coerceStartWorkflowId(options.id, "options.id") : crypto.randomUUID(), parentHeaders = internals.pendingParentHeaders;
|
|
52
51
|
internals.pendingParentHeaders = void 0;
|
|
53
52
|
const pendingExecutionStateOwnerId = internals.pendingExecutionStateOwnerId, executionStateOwnerId = pendingExecutionStateOwnerId === null ? void 0 : pendingExecutionStateOwnerId ?? workflowId;
|
|
54
53
|
internals.pendingExecutionStateOwnerId = void 0;
|
|
@@ -92,10 +91,10 @@ function resolveAndReserveStartRegistration(internals, type, revisionOverride, c
|
|
|
92
91
|
resolvedRevision: revisionOverride
|
|
93
92
|
}));
|
|
94
93
|
}
|
|
95
|
-
export async function startWorkflow(internals, type, input, options, additionalStartOperations, callbacks, buildIdempotentStartOperations, revisionOverride) {
|
|
94
|
+
export async function startWorkflow(internals, type, input, options, additionalStartOperations, callbacks, buildIdempotentStartOperations, revisionOverride, skipAdmissionIdCheck) {
|
|
96
95
|
assertServicesSupportedForMode(internals, options);
|
|
97
96
|
assertValidOnTerminalConflict(options);
|
|
98
|
-
const preparation = prepareStartWorkflow(internals, options, callbacks), {
|
|
97
|
+
const preparation = prepareStartWorkflow(internals, options, callbacks, Boolean(skipAdmissionIdCheck)), {
|
|
99
98
|
workflowId,
|
|
100
99
|
callerProvidedId,
|
|
101
100
|
parentHeaders,
|
|
@@ -117,9 +116,16 @@ export async function startWorkflow(internals, type, input, options, additionalS
|
|
|
117
116
|
resolvedRevision
|
|
118
117
|
} = await resolveAndReserveStartRegistration(internals, type, revisionOverride, callbacks);
|
|
119
118
|
inFlightRevision = reservedRevision;
|
|
120
|
-
const workflowConcurrency = registration.concurrency, revision = resolveCachedStartRevision(internals, type, resolvedRevision) ?? await resolveStartRevisionUncached(internals, type), {
|
|
119
|
+
const workflowConcurrency = registration.concurrency, revision = resolveCachedStartRevision(internals, type, resolvedRevision) ?? await resolveStartRevisionUncached(internals, type), {
|
|
120
|
+
terminalRunToPurge,
|
|
121
|
+
duplicateIdCondition,
|
|
122
|
+
duplicateIdGenerationCondition,
|
|
123
|
+
observedGenerationBytes
|
|
124
|
+
} = callerProvidedId ? await resolveTerminalConflictForRestart(internals, workflowId, options) : GENERATED_ID_START_DECISION;
|
|
125
|
+
enforceReplayOnlyIdFence(skipAdmissionIdCheck, workflowId, terminalRunToPurge);
|
|
126
|
+
const versionTuple = createWorkflowVersionTuple(internals, registration, callbacks), state = createInitialWorkflowState(internals, workflowId, type, input, versionTuple, revision, options, preparation.normalizedTags, executionStateOwnerId, parentWorkflowId, parentWorkflowExecutionToken, delayedStartTimer, callbacks);
|
|
121
127
|
applyRestartLineage(state, terminalRunToPurge);
|
|
122
|
-
const checkpoint = createInitialCheckpoint(internals, workflowId, versionTuple.workflowVersion, options, callbacks), workflowStartHeaders = runWorkflowStartInterceptor(internals, workflowId, type, input, parentHeaders, callbacks), persistedWorkflowStartHeaders = selectPersistedWorkflowStartHeaders(workflowStartHeaders), purgeDeleteOperations = terminalRunToPurge !== null ? await prepareTerminalRunPurge(internals, terminalRunToPurge, callbacks) : void 0;
|
|
128
|
+
const checkpoint = createInitialCheckpoint(internals, workflowId, versionTuple.workflowVersion, options, callbacks), workflowStartHeaders = runWorkflowStartInterceptor(internals, workflowId, type, input, parentHeaders, callbacks), persistedWorkflowStartHeaders = selectPersistedWorkflowStartHeaders(workflowStartHeaders), purgeDeleteOperations = terminalRunToPurge !== null ? await prepareTerminalRunPurge(internals, terminalRunToPurge, callbacks, observedGenerationBytes) : void 0;
|
|
123
129
|
internals.checkpoints.set(workflowId, checkpoint);
|
|
124
130
|
setWorkflowStartHeaders(internals, workflowId, workflowStartHeaders, callbacks);
|
|
125
131
|
internals.workflowVersionTuples.set(workflowId, versionTuple);
|
|
@@ -136,7 +142,8 @@ export async function startWorkflow(internals, type, input, options, additionalS
|
|
|
136
142
|
buildWorkflowConcurrencyStartOperations: workflowConcurrency === void 0 ? void 0 : () => buildWorkflowConcurrencyStartOperations(internals, type, workflowId, input, workflowConcurrency),
|
|
137
143
|
callbacks,
|
|
138
144
|
purgeDeleteOperations,
|
|
139
|
-
duplicateIdCondition
|
|
145
|
+
duplicateIdCondition,
|
|
146
|
+
duplicateIdGenerationCondition
|
|
140
147
|
}, buildIdempotentStartOperations);
|
|
141
148
|
if (options?.services !== void 0) {
|
|
142
149
|
internals.workflowServices.set(workflowId, options.services);
|
|
@@ -163,17 +170,3 @@ export async function startWorkflow(internals, type, input, options, additionalS
|
|
|
163
170
|
rollbackTransientStartState(internals, workflowId);
|
|
164
171
|
}
|
|
165
172
|
}
|
|
166
|
-
export function resolveScheduledStartAt(internals, options, submissionTime, callbacks) {
|
|
167
|
-
assertExclusiveStartWorkflowOptions(options?.startAt, options?.startAfter);
|
|
168
|
-
if (options?.startAt !== void 0)
|
|
169
|
-
return coerceStartWorkflowTimestamp(options.startAt, "options.startAt");
|
|
170
|
-
if (options?.startAfter !== void 0) {
|
|
171
|
-
const startAfterMilliseconds = parseStartOptionDuration(internals, options.startAfter, "options.startAfter", callbacks);
|
|
172
|
-
try {
|
|
173
|
-
return normalizeStorageTimestamp(submissionTime + startAfterMilliseconds, "options.startAfter");
|
|
174
|
-
} catch {
|
|
175
|
-
throw new StartWorkflowValidationError("options.startAfter must resolve to a finite, non-negative start time");
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
return;
|
|
179
|
-
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ForkOptions } from '../../types.ts';
|
|
2
2
|
import type { WorkflowHandle } from '../handles.ts';
|
|
3
3
|
import type { EngineInternals } from '../internals.ts';
|
|
4
|
+
import { type FreshResumeClaimTracker } from './resume.ts';
|
|
4
5
|
import { type LifecycleCallbacks, type RecoverAllOptions } from './shared.ts';
|
|
5
6
|
export declare function recoverAll(internals: EngineInternals, callbacks: LifecycleCallbacks, options?: RecoverAllOptions): Promise<WorkflowHandle[]>;
|
|
6
7
|
/** Options for {@link resume}. */
|
|
@@ -16,6 +17,14 @@ export type ResumeOptions = {
|
|
|
16
17
|
* `engine.resume()` leaves it `false` — nothing was deposed there.
|
|
17
18
|
*/
|
|
18
19
|
readonly forceReplayFromStorage?: boolean;
|
|
20
|
+
/**
|
|
21
|
+
* Forwarded to `resumeWorkflowFromStorage()`'s matching options — see
|
|
22
|
+
* `ResumeFromStorageOptions` (WFT-134 issue D). Set by
|
|
23
|
+
* `recoverEntryOrIsolateFailure` only; unset for the local-ownership fast
|
|
24
|
+
* path above, which never reaches that function.
|
|
25
|
+
*/
|
|
26
|
+
readonly deferClaimReleaseOnRejection?: boolean;
|
|
27
|
+
readonly freshClaimTracker?: FreshResumeClaimTracker;
|
|
19
28
|
};
|
|
20
29
|
export declare function resume(internals: EngineInternals, workflowId: string, callbacks: LifecycleCallbacks, onRecoveredWorkflow?: RecoverAllOptions['onRecoveredWorkflow'], options?: ResumeOptions): Promise<WorkflowHandle>;
|
|
21
30
|
export declare function fork(internals: EngineInternals, sourceWorkflowId: string, options: ForkOptions | undefined, callbacks: LifecycleCallbacks): Promise<WorkflowHandle>;
|
|
@@ -1,17 +1,12 @@
|
|
|
1
1
|
import { KEYS } from "../../../storage/interface.js";
|
|
2
2
|
import { deserializeCheckpoint, serializeCheckpoint } from "../../checkpoint.js";
|
|
3
|
-
import { RegExpExtensionDecodeError } from "../../codec/extension-codec.js";
|
|
4
3
|
import { EMPTY_EVENT_HEAD } from "../../event-log.js";
|
|
5
4
|
import { WorkflowRecoverySkippedEvent } from "../../events.js";
|
|
6
|
-
import { VersionMismatchError } from "../../versioning.js";
|
|
7
5
|
import { forgetCommittedCheckpointBytes } from "../checkpoint-commit-snapshots.js";
|
|
8
6
|
import { hydrateCheckpointReplayState } from "../checkpoint-replay.js";
|
|
9
|
-
import { DynamicWorkflowSourceUnavailableError } from "../dynamic-source-errors.js";
|
|
10
7
|
import { resolveExecutableRegistrationOrRenamedNotFound } from "../dynamic-source-execution.js";
|
|
11
8
|
import { WorkflowTypeNotRegisteredForRecoveryError } from "../errors.js";
|
|
12
9
|
import { commitFencedEngineWrite } from "../fenced-write.js";
|
|
13
|
-
import { WorkflowClaimUnavailableError } from "../lease-errors.js";
|
|
14
|
-
import { WorkflowRevisionUnavailableError } from "../revision-errors.js";
|
|
15
10
|
import { normalizeForkStep, selectPersistedWorkflowStartHeaders } from "../state-utilities.js";
|
|
16
11
|
import { loadWorkflowState } from "../storage-io.js";
|
|
17
12
|
import { decodeWorkflowState } from "../validation.js";
|
|
@@ -23,6 +18,7 @@ import {
|
|
|
23
18
|
createForkedWorkflowState
|
|
24
19
|
} from "./fork-helpers.js";
|
|
25
20
|
import { derivePreparedExecutionState } from "./persist.js";
|
|
21
|
+
import { isolateRecoveryFailure } from "./recovery-isolation.js";
|
|
26
22
|
import {
|
|
27
23
|
buildRecoveryRevisionGroups,
|
|
28
24
|
classifyRevisionGroups,
|
|
@@ -34,6 +30,7 @@ import {
|
|
|
34
30
|
loadWorkflowStartHeaders,
|
|
35
31
|
setWorkflowStartHeaders
|
|
36
32
|
} from "./shared.js";
|
|
33
|
+
import { releaseFreshlyAcquiredResumeClaim } from "./standalone-claim-acquire.js";
|
|
37
34
|
function isWorkflowSideRecordKey(key) {
|
|
38
35
|
return key.includes(":ckpt") || key.includes(":offload") || key.includes(":archive") || key.includes(":timeline:");
|
|
39
36
|
}
|
|
@@ -67,28 +64,19 @@ async function preflightRecoverAll(internals, callbacks) {
|
|
|
67
64
|
return result;
|
|
68
65
|
}
|
|
69
66
|
async function recoverEntryOrIsolateFailure(internals, workflowId, callbacks, options) {
|
|
67
|
+
const freshClaimTracker = { epoch: null };
|
|
70
68
|
try {
|
|
71
|
-
return await resume(internals, workflowId, callbacks, options?.onRecoveredWorkflow
|
|
69
|
+
return await resume(internals, workflowId, callbacks, options?.onRecoveredWorkflow, {
|
|
70
|
+
deferClaimReleaseOnRejection: !0,
|
|
71
|
+
freshClaimTracker
|
|
72
|
+
});
|
|
72
73
|
} catch (error) {
|
|
73
|
-
|
|
74
|
-
await
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
await callbacks.failWorkflowForVersionMismatch(workflowId, error);
|
|
79
|
-
return null;
|
|
80
|
-
}
|
|
81
|
-
if (error instanceof WorkflowClaimUnavailableError)
|
|
82
|
-
return null;
|
|
83
|
-
if (error instanceof WorkflowRevisionUnavailableError) {
|
|
84
|
-
await callbacks.failWorkflowForRevisionUnavailable(workflowId, error);
|
|
85
|
-
return null;
|
|
74
|
+
try {
|
|
75
|
+
return await isolateRecoveryFailure(workflowId, callbacks, options, error);
|
|
76
|
+
} finally {
|
|
77
|
+
if (freshClaimTracker.epoch !== null)
|
|
78
|
+
await releaseFreshlyAcquiredResumeClaim(internals, workflowId, freshClaimTracker.epoch);
|
|
86
79
|
}
|
|
87
|
-
if (error instanceof DynamicWorkflowSourceUnavailableError) {
|
|
88
|
-
await callbacks.failWorkflowForUnavailableDynamicSource(workflowId, error);
|
|
89
|
-
return null;
|
|
90
|
-
}
|
|
91
|
-
throw error;
|
|
92
80
|
}
|
|
93
81
|
}
|
|
94
82
|
export async function recoverAll(internals, callbacks, options) {
|
|
@@ -125,7 +113,14 @@ export async function resume(internals, workflowId, callbacks, onRecoveredWorkfl
|
|
|
125
113
|
return callbacks.getHandle(workflowId);
|
|
126
114
|
}
|
|
127
115
|
}
|
|
128
|
-
return resumeWorkflowFromStorage(internals, workflowId, !0, callbacks, onRecoveredWorkflow
|
|
116
|
+
return resumeWorkflowFromStorage(internals, workflowId, !0, callbacks, onRecoveredWorkflow, {
|
|
117
|
+
...options?.deferClaimReleaseOnRejection !== void 0 && {
|
|
118
|
+
deferClaimReleaseOnRejection: options.deferClaimReleaseOnRejection
|
|
119
|
+
},
|
|
120
|
+
...options?.freshClaimTracker !== void 0 && {
|
|
121
|
+
freshClaimTracker: options.freshClaimTracker
|
|
122
|
+
}
|
|
123
|
+
});
|
|
129
124
|
}
|
|
130
125
|
export async function fork(internals, sourceWorkflowId, options, callbacks) {
|
|
131
126
|
const sourceState = await loadWorkflowState(internals, sourceWorkflowId);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { EMPTY_STORAGE_VALUE, createWorkflowHandle, loadTerminalCleanupTrackedState, loadWorkflowStartHeaders, normalizeStartWorkflowTags, processPendingUpdatesAfterReplay, setWorkflowStartHeaders, type LifecycleCallbacks, type RecoverAllOptions, type RecoveredWorkflowInfo, } from './lifecycle/shared.ts';
|
|
2
2
|
export { createWorkflowVersionTuple, derivePreparedExecutionState, prepareResumeState, throwVersionMismatch, workflowStateWithVersionTuple, workflowVersionTupleFromState, } from './lifecycle/persist.ts';
|
|
3
|
-
export { resolveScheduledStartAt
|
|
3
|
+
export { resolveScheduledStartAt } from './lifecycle/start-schedule-timing.ts';
|
|
4
|
+
export { start, startWorkflow } from './lifecycle/start.ts';
|
|
4
5
|
export { applyRestartLineage, createInitialCheckpoint, createInitialWorkflowState, parseStartOptionDuration, } from './lifecycle/start-state.ts';
|
|
5
6
|
export { startOrSignal, startWithIdempotency, type StartOrSignalCallbacks, type StartOrSignalResult, } from './lifecycle/start-or-signal.ts';
|
|
6
7
|
export { buildInitialSearchAttributeOperations, buildStartBatchOperations, validateSearchAttributes, } from './lifecycle/start-batch.ts';
|
|
@@ -15,7 +15,8 @@ export {
|
|
|
15
15
|
workflowStateWithVersionTuple,
|
|
16
16
|
workflowVersionTupleFromState
|
|
17
17
|
} from "./lifecycle/persist.js";
|
|
18
|
-
export { resolveScheduledStartAt
|
|
18
|
+
export { resolveScheduledStartAt } from "./lifecycle/start-schedule-timing.js";
|
|
19
|
+
export { start, startWorkflow } from "./lifecycle/start.js";
|
|
19
20
|
export {
|
|
20
21
|
applyRestartLineage,
|
|
21
22
|
createInitialCheckpoint,
|
|
@@ -44,7 +44,8 @@ export async function drainQueuedScheduleRun(state, callbacks, completedWorkflow
|
|
|
44
44
|
workflowId: queuedRun.workflowId,
|
|
45
45
|
...queuedRun.occurrence !== void 0 && { occurrence: queuedRun.occurrence },
|
|
46
46
|
scheduleStateAfterStart: stateAfterStart,
|
|
47
|
-
...completedWorkflowId !== void 0 && { completedWorkflowId }
|
|
47
|
+
...completedWorkflowId !== void 0 && { completedWorkflowId },
|
|
48
|
+
skipAdmissionIdCheck: !0
|
|
48
49
|
});
|
|
49
50
|
return stateAfterStart;
|
|
50
51
|
}
|
|
@@ -9,6 +9,16 @@ export type ScheduledRunStartOptions = {
|
|
|
9
9
|
scheduleStateAfterStart?: ScheduleState;
|
|
10
10
|
/** Prior terminal run whose transient schedule metadata is now settled. */
|
|
11
11
|
completedWorkflowId?: string;
|
|
12
|
+
/**
|
|
13
|
+
* Internal only (WFT-95). Set by `drainQueuedScheduleRun()` when `workflowId`
|
|
14
|
+
* is a persisted `queuedRuns[].workflowId` being replayed, not a freshly
|
|
15
|
+
* minted one — safe unconditionally, since a post-fix queued run was already
|
|
16
|
+
* validated as non-`.`/`..` at schedule-admission time (relaxing the check
|
|
17
|
+
* here is then a no-op), and it is what lets a legacy pre-WFT-95 queued run
|
|
18
|
+
* whose id is `.`/`..` keep draining instead of pausing the schedule.
|
|
19
|
+
* Threaded straight through to `startWorkflow`'s `skipAdmissionIdCheck`.
|
|
20
|
+
*/
|
|
21
|
+
skipAdmissionIdCheck?: boolean;
|
|
12
22
|
};
|
|
13
23
|
/**
|
|
14
24
|
* Launch one scheduled occurrence's workflow run and emit `schedule:fired`.
|
|
@@ -50,7 +50,7 @@ export async function startScheduledRun(internals, state, callbacks, options = {
|
|
|
50
50
|
internals.workflowServices.set(workflowId, resolution.services);
|
|
51
51
|
}
|
|
52
52
|
internals.workflowsNeedingTerminalCleanup.add(workflowId);
|
|
53
|
-
await callbacks.startWorkflow(state.workflowType, state.input, { id: workflowId }, scheduleRunOperations, buildPinnedRevisionOverride(state));
|
|
53
|
+
await callbacks.startWorkflow(state.workflowType, state.input, { id: workflowId }, scheduleRunOperations, buildPinnedRevisionOverride(state), options.skipAdmissionIdCheck);
|
|
54
54
|
internals.engine.dispatchEvent(new ScheduleFiredEvent(state.id, workflowId, internals.options.getNow(), occurrence));
|
|
55
55
|
if (resolution !== null && resolution.status === "unavailable") {
|
|
56
56
|
const unavailableError = unavailableServicesError(workflowId, resolution.reason);
|
|
@@ -10,10 +10,19 @@ export type RefreshedScheduleState = {
|
|
|
10
10
|
currentWorkflowState: WorkflowState | null;
|
|
11
11
|
};
|
|
12
12
|
export type ScheduleCallbacks = {
|
|
13
|
-
/**
|
|
13
|
+
/**
|
|
14
|
+
* `revisionOverride` (WFT-20) is the exact revision a `revisionPolicy: 'pinned'`
|
|
15
|
+
* occurrence must resolve against; `undefined` for an `'active-at-fire'` schedule.
|
|
16
|
+
*
|
|
17
|
+
* `skipAdmissionIdCheck` (WFT-95, internal only) is threaded straight through to
|
|
18
|
+
* `startWorkflow`'s parameter of the same name — see its doc comment. Set only when
|
|
19
|
+
* replaying a schedule's already-persisted `queuedRuns[].workflowId` via
|
|
20
|
+
* `drainQueuedScheduleRun()`; every other schedule-run start (a fresh cadence tick,
|
|
21
|
+
* a `cancel-running` replacement) omits it and gets strict id admission.
|
|
22
|
+
*/
|
|
14
23
|
startWorkflow: (type: string, input: unknown, options: {
|
|
15
24
|
id: string;
|
|
16
|
-
}, additionalStartOperations?: BatchOperation[], revisionOverride?: string) => Promise<void>;
|
|
25
|
+
}, additionalStartOperations?: BatchOperation[], revisionOverride?: string, skipAdmissionIdCheck?: boolean) => Promise<void>;
|
|
17
26
|
loadWorkflowState: (workflowId: string) => Promise<WorkflowState | null | undefined>;
|
|
18
27
|
cancelWorkflow: (workflowId: string) => Promise<void>;
|
|
19
28
|
getWorkflowResult: (workflowId: string) => Promise<unknown>;
|
|
@@ -177,7 +177,7 @@ async function releaseWorkflowClaimAfterTerminalSettlement(internals, workflowId
|
|
|
177
177
|
if (registry.currentEpoch(workflowId) !== capturedEpoch)
|
|
178
178
|
return;
|
|
179
179
|
try {
|
|
180
|
-
await registry.release(workflowId);
|
|
180
|
+
await registry.release(workflowId, capturedEpoch);
|
|
181
181
|
} catch {}
|
|
182
182
|
}
|
|
183
183
|
export async function completeWorkflow(internals, workflowId, result, callbacks) {
|
|
@@ -6,6 +6,7 @@ import { dropQueuedInlineWorkflowStart } from "../inline-launch-queue.js";
|
|
|
6
6
|
import { buildWorkflowVisibilityIndexTransition } from "../workflow-indexes.js";
|
|
7
7
|
import { evictSuspendedWorkflowWaiters } from "./cleanup.js";
|
|
8
8
|
export async function suspendWorkflow(internals, workflowId, callbacks) {
|
|
9
|
+
const capturedClaimEpochForForget = internals.workflowClaimRegistry?.currentEpoch(workflowId) ?? null;
|
|
9
10
|
if (!await callbacks.runSerializedWorkflowStateWrite(workflowId, async () => {
|
|
10
11
|
const state = await callbacks.loadWorkflowState(workflowId);
|
|
11
12
|
if (!state || state.status !== "running")
|
|
@@ -26,6 +27,8 @@ export async function suspendWorkflow(internals, workflowId, callbacks) {
|
|
|
26
27
|
return !0;
|
|
27
28
|
}))
|
|
28
29
|
return;
|
|
30
|
+
if (internals.workflowClaimRegistry !== null && capturedClaimEpochForForget !== null && internals.workflowClaimRegistry.currentEpoch(workflowId) === capturedClaimEpochForForget)
|
|
31
|
+
internals.workflowClaimRegistry.forgetLocalClaim(workflowId);
|
|
29
32
|
const event = new WorkflowSuspendedEvent(workflowId);
|
|
30
33
|
callbacks.dispatchEvent(event);
|
|
31
34
|
callbacks.forwardEventToHandle(workflowId, event);
|
|
@@ -6,8 +6,32 @@ export declare function isValidScheduleTimestamp(value: unknown): value is numbe
|
|
|
6
6
|
export declare function isValidScheduleStatus(value: unknown): value is ScheduleStatus;
|
|
7
7
|
export declare function isValidScheduleOverlapPolicy(value: unknown): value is ScheduleOverlapPolicy;
|
|
8
8
|
export declare function isValidScheduleRevisionPolicy(value: unknown): value is ScheduleRevisionPolicy;
|
|
9
|
+
/**
|
|
10
|
+
* Whether `value` is a schedule-related workflow id that decoded persisted
|
|
11
|
+
* data may still carry. Deliberately reuses the pre-WFT-95 decode predicate
|
|
12
|
+
* (not the fresh-admission `.`/`..` rejection), because every caller of this
|
|
13
|
+
* function — `decodeScheduleIdentityFields`, `decodeScheduleRunMetadata`,
|
|
14
|
+
* and the persisted `currentWorkflowId`/`queuedRuns[].workflowId` fields in
|
|
15
|
+
* this module — reads already-persisted data. A schedule created before
|
|
16
|
+
* WFT-95 with `id: '.'` or `'..'` must remain decodable and keep firing
|
|
17
|
+
* after upgrade.
|
|
18
|
+
*/
|
|
9
19
|
export declare function isValidScheduleIdentifier(value: unknown): value is string;
|
|
10
|
-
|
|
20
|
+
/**
|
|
21
|
+
* Coerce a caller-supplied `scheduleId` used to look up or control an
|
|
22
|
+
* already-persisted schedule — `getSchedule`, `pauseSchedule`,
|
|
23
|
+
* `resumeSchedule`, `cancelSchedule`, and `updateSchedule` all call this,
|
|
24
|
+
* never schedule creation (that admission path is
|
|
25
|
+
* `normalizeScheduleOptions`'s direct `coerceStartWorkflowId(options.id, …)`
|
|
26
|
+
* call, which keeps the strict `.`/`..` rejection). Deliberately uses the
|
|
27
|
+
* decode-compatible `assertDecodableWorkflowId`, not the admission-only
|
|
28
|
+
* `.`/`..` rejection (WFT-95 review): a schedule created before that
|
|
29
|
+
* rejection landed may already carry `id: '.'` or `'..'`, and callers must
|
|
30
|
+
* still be able to inspect, pause, resume, cancel, or update it instead of
|
|
31
|
+
* having every control operation reject a schedule they can't otherwise
|
|
32
|
+
* reach.
|
|
33
|
+
*/
|
|
34
|
+
export declare function coerceScheduleId(scheduleId: unknown, fieldName: string): string;
|
|
11
35
|
/**
|
|
12
36
|
* A recurrence specification normalized into the discriminated cadence the
|
|
13
37
|
* engine persists. Interval periods are resolved to whole milliseconds.
|
|
@@ -2,7 +2,7 @@ import { decode } from "../../codec.js";
|
|
|
2
2
|
import { isRecord } from "../../debug-output.js";
|
|
3
3
|
import { parseCronExpression } from "../../schedule.js";
|
|
4
4
|
import { parseDuration } from "../../scheduler.js";
|
|
5
|
-
import {
|
|
5
|
+
import { assertDecodableWorkflowId, isDecodableWorkflowId } from "../../workflow-identifiers.js";
|
|
6
6
|
import { decodeScheduleCadence } from "./schedule-cadence.js";
|
|
7
7
|
import { SCHEDULE_OVERLAP_POLICIES, SCHEDULE_REVISION_POLICIES } from "./schedule-options.js";
|
|
8
8
|
import { decodeScheduleRevisionPolicyFields } from "./schedule-revision.js";
|
|
@@ -28,17 +28,11 @@ export function isValidScheduleRevisionPolicy(value) {
|
|
|
28
28
|
return typeof value === "string" && SCHEDULE_REVISION_POLICIES.has(value);
|
|
29
29
|
}
|
|
30
30
|
export function isValidScheduleIdentifier(value) {
|
|
31
|
-
|
|
32
|
-
return !1;
|
|
33
|
-
try {
|
|
34
|
-
coerceStartWorkflowId(value, "schedule id");
|
|
35
|
-
return !0;
|
|
36
|
-
} catch {
|
|
37
|
-
return !1;
|
|
38
|
-
}
|
|
31
|
+
return typeof value === "string" && isDecodableWorkflowId(value);
|
|
39
32
|
}
|
|
40
33
|
export function coerceScheduleId(scheduleId, fieldName) {
|
|
41
|
-
|
|
34
|
+
assertDecodableWorkflowId(scheduleId, fieldName);
|
|
35
|
+
return scheduleId;
|
|
42
36
|
}
|
|
43
37
|
function normalizeIntervalEvery(every) {
|
|
44
38
|
if (typeof every !== "string" && typeof every !== "number")
|