@lostgradient/weft 0.2.1 → 0.3.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 +47 -22
- package/dist/cli/generated/operation-client.generated.d.ts +28 -1
- package/dist/cli/generated/operation-client.generated.js +2 -0
- package/dist/cli-main.js +79 -79
- package/dist/client/handle-delegation.d.ts +4 -0
- package/dist/client/handle-delegation.js +6 -0
- package/dist/client/http-client-requests.d.ts +2 -0
- package/dist/client/http-client-requests.js +3 -0
- package/dist/client/http-client.d.ts +4 -1
- package/dist/client/http-client.js +9 -1
- package/dist/client/interface.d.ts +57 -2
- package/dist/client/local.d.ts +4 -1
- package/dist/client/local.js +7 -0
- package/dist/client/start-body.d.ts +7 -1
- package/dist/client/start-body.js +13 -4
- package/dist/core/codec/extension-codec.js +4 -2
- package/dist/core/codec/index.d.ts +1 -0
- package/dist/core/codec/index.js +1 -0
- package/dist/core/codec/serializer-registry.d.ts +122 -0
- package/dist/core/codec/serializer-registry.js +51 -0
- package/dist/core/context/index.d.ts +9 -0
- package/dist/core/context/internals.d.ts +9 -0
- package/dist/core/context/internals.js +3 -0
- package/dist/core/context/run-operation.d.ts +16 -3
- package/dist/core/context/run-operation.js +16 -7
- package/dist/core/engine/bulk-operations.js +1 -1
- package/dist/core/engine/construction.d.ts +0 -1
- package/dist/core/engine/construction.js +10 -1
- package/dist/core/engine/disposal.js +12 -0
- package/dist/core/engine/engine-create-types.d.ts +0 -14
- package/dist/core/engine/engine-internal-types.d.ts +12 -0
- package/dist/core/engine/engine-leak-warnings.d.ts +6 -0
- package/dist/core/engine/engine-leak-warnings.js +4 -0
- package/dist/core/engine/engine-runtime-helpers.d.ts +17 -0
- package/dist/core/engine/engine-runtime-helpers.js +26 -5
- package/dist/core/engine/errors.d.ts +74 -0
- package/dist/core/engine/errors.js +25 -1
- package/dist/core/engine/handle-result.js +1 -1
- package/dist/core/engine/handles.d.ts +89 -40
- package/dist/core/engine/handles.js +25 -27
- package/dist/core/engine/index.d.ts +96 -4
- package/dist/core/engine/index.js +75 -4
- package/dist/core/engine/inline-launch-queue.d.ts +14 -0
- package/dist/core/engine/inline-launch-queue.js +32 -7
- package/dist/core/engine/internals.d.ts +18 -10
- package/dist/core/engine/lifecycle/fork-helpers.js +1 -7
- package/dist/core/engine/lifecycle/persist.js +5 -20
- package/dist/core/engine/lifecycle/resume.js +25 -4
- package/dist/core/engine/lifecycle/start-commit.d.ts +47 -0
- package/dist/core/engine/lifecycle/start-commit.js +27 -0
- package/dist/core/engine/lifecycle/start-exec.d.ts +30 -2
- package/dist/core/engine/lifecycle/start-exec.js +38 -0
- package/dist/core/engine/lifecycle/start-or-signal-resolution.d.ts +79 -0
- package/dist/core/engine/lifecycle/start-or-signal-resolution.js +60 -0
- package/dist/core/engine/lifecycle/start-or-signal.d.ts +45 -0
- package/dist/core/engine/lifecycle/start-or-signal.js +141 -0
- package/dist/core/engine/lifecycle/start.d.ts +3 -3
- package/dist/core/engine/lifecycle/start.js +31 -37
- package/dist/core/engine/lifecycle.d.ts +3 -2
- package/dist/core/engine/lifecycle.js +9 -2
- package/dist/core/engine/listing.js +1 -1
- package/dist/core/engine/persisted-data-version.d.ts +5 -9
- package/dist/core/engine/persisted-data-version.js +4 -5
- package/dist/core/engine/schedule-handle.d.ts +45 -0
- package/dist/core/engine/schedule-handle.js +26 -0
- package/dist/core/engine/schedules.d.ts +1 -1
- package/dist/core/engine/schedules.js +7 -3
- package/dist/core/engine/second-instance-detector.d.ts +96 -0
- package/dist/core/engine/second-instance-detector.js +108 -0
- package/dist/core/engine/signals.d.ts +22 -0
- package/dist/core/engine/signals.js +15 -0
- package/dist/core/engine/termination/cleanup.d.ts +25 -0
- package/dist/core/engine/termination/cleanup.js +19 -1
- package/dist/core/engine/termination/complete.js +4 -3
- package/dist/core/engine/termination/suspend.d.ts +68 -0
- package/dist/core/engine/termination/suspend.js +41 -0
- package/dist/core/engine/termination.d.ts +4 -2
- package/dist/core/engine/termination.js +2 -0
- package/dist/core/engine/validation.js +25 -1
- package/dist/core/engine/workflow-feed.d.ts +5 -3
- package/dist/core/events/event-map.d.ts +2 -1
- package/dist/core/events/workflow-events.d.ts +23 -0
- package/dist/core/events/workflow-events.js +9 -0
- package/dist/core/list-filter-validation.js +2 -1
- package/dist/core/start-workflow-validation.d.ts +22 -0
- package/dist/core/start-workflow-validation.js +11 -1
- package/dist/core/step-context.d.ts +10 -6
- package/dist/core/step-context.js +7 -15
- package/dist/core/types/activity.d.ts +6 -3
- package/dist/core/types/identity.d.ts +8 -1
- package/dist/core/types/launch-metadata.d.ts +33 -0
- package/dist/core/types/launch-metadata.js +0 -0
- package/dist/core/types/message-handles.d.ts +25 -0
- package/dist/core/types/options.d.ts +48 -54
- package/dist/core/types/reviews.d.ts +2 -1
- package/dist/core/types/services-resolution.d.ts +47 -0
- package/dist/core/types/services-resolution.js +0 -0
- package/dist/core/types/state.d.ts +11 -11
- package/dist/core/types/workflow-builder.d.ts +5 -4
- package/dist/core/types/workflow-function.d.ts +17 -0
- package/dist/core/types/workflow-snapshot.d.ts +29 -0
- package/dist/core/types/workflow-snapshot.js +0 -0
- package/dist/core/types.d.ts +3 -0
- package/dist/core/types.js +3 -0
- package/dist/core/weft-error.d.ts +1 -1
- package/dist/core/weft-error.js +3 -1
- package/dist/diagnostics/doctor.js +6 -3
- package/dist/diagnostics/format.js +2 -2
- package/dist/diagnostics/types.d.ts +1 -0
- package/dist/diagnostics/version-check.js +6 -4
- package/dist/index.d.ts +4 -4
- package/dist/index.js +10 -1
- package/dist/json-schema.js +1 -1
- package/dist/mcp/cli.js +35 -35
- package/dist/mcp/list-filter.js +2 -1
- package/dist/mcp/session.js +1 -0
- package/dist/observability/index.js +2 -2
- package/dist/server/handler.js +30 -30
- package/dist/server/index.js +33 -33
- package/dist/server/interactive-operations.js +1 -0
- package/dist/server/operations/resume-workflow.js +2 -2
- package/dist/server/operations/start-or-signal-workflow.d.ts +39 -0
- package/dist/server/operations/start-or-signal-workflow.js +140 -0
- package/dist/server/operations/start-workflow-options.d.ts +32 -0
- package/dist/server/operations/start-workflow-options.js +63 -0
- package/dist/server/operations/start-workflow.js +7 -69
- package/dist/server/operations/suspend-workflow.d.ts +13 -0
- package/dist/server/operations/suspend-workflow.js +36 -0
- package/dist/server/rest-binding.d.ts +18 -7
- package/dist/server/rest-bindings.js +12 -0
- package/dist/server/runtime/task-dispatch.js +5 -3
- package/dist/server/runtime/task-polling.d.ts +16 -2
- package/dist/server/runtime/task-polling.js +20 -5
- package/dist/server/runtime/websocket-worker.js +8 -0
- package/dist/server/serve-internals.d.ts +8 -0
- package/dist/server/serve-internals.js +4 -2
- package/dist/server/task-state.d.ts +8 -0
- package/dist/service-worker/index.js +28 -28
- package/dist/storage/capabilities.d.ts +10 -2
- package/dist/storage/capabilities.js +2 -2
- package/dist/storage/http.js +2 -2
- package/dist/storage/index.d.ts +6 -1
- package/dist/storage/indexeddb.js +1 -1
- package/dist/storage/interface.d.ts +26 -0
- package/dist/storage/interface.js +1 -1
- package/dist/storage/key-prefixes.d.ts +1 -1
- package/dist/storage/key-prefixes.js +2 -0
- package/dist/storage/lmdb.js +1 -1
- package/dist/storage/memory.js +1 -1
- package/dist/storage/neon-value-mapping.d.ts +47 -0
- package/dist/storage/neon-value-mapping.js +11 -0
- package/dist/storage/neon.d.ts +108 -0
- package/dist/storage/neon.js +10 -0
- package/dist/storage/node-sqlite-loader.d.ts +71 -0
- package/dist/storage/node-sqlite-loader.js +41 -0
- package/dist/storage/node-sqlite.d.ts +1 -19
- package/dist/storage/node-sqlite.js +38 -32
- package/dist/storage/postgres-key-value-queries.d.ts +79 -0
- package/dist/storage/postgres-key-value-queries.js +63 -0
- package/dist/storage/resolve.d.ts +2 -165
- package/dist/storage/resolve.js +1 -1
- package/dist/storage/scoped-storage.js +1 -1
- package/dist/storage/storage-configuration.d.ts +209 -0
- package/dist/storage/storage-configuration.js +0 -0
- package/dist/storage/text-value-store.d.ts +9 -9
- package/dist/storage/turso.js +2 -2
- package/dist/storage/typed-storage.js +1 -1
- package/dist/storage/web-extension.js +1 -1
- package/dist/testing/index.js +33 -33
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worker/index.js +9 -5
- package/dist/worker/long-poll.js +4 -0
- package/dist/worker/protocol-messages.d.ts +20 -0
- package/dist/worker/protocol-schemas.d.ts +32 -0
- package/dist/worker/protocol-schemas.js +8 -4
- package/dist/worker/protocol-task-result.d.ts +28 -0
- package/dist/worker/protocol-task-result.js +76 -0
- package/dist/worker/protocol.d.ts +4 -15
- package/dist/worker/protocol.js +1 -1
- package/dist/worker/registry/fair-share.d.ts +29 -0
- package/dist/worker/registry/fair-share.js +30 -0
- package/dist/worker/registry/routing.d.ts +18 -0
- package/dist/worker/registry/routing.js +14 -0
- package/dist/worker/registry/types.d.ts +7 -0
- package/dist/worker/registry.d.ts +16 -1
- package/dist/worker/registry.js +24 -36
- package/package.json +17 -4
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import { KEYS } from "../../../storage/interface.js";
|
|
2
2
|
import { deserializeCheckpoint, serializeCheckpoint } from "../../checkpoint.js";
|
|
3
|
+
import { encode } from "../../codec.js";
|
|
3
4
|
import { Context, setContextWorkflowInterceptor } from "../../context.js";
|
|
4
5
|
import { EventLog } from "../../event-log.js";
|
|
5
6
|
import { WorkflowResumedEvent } from "../../events.js";
|
|
7
|
+
import { buildTimerBatchOperations } from "../../scheduler.js";
|
|
6
8
|
import { createCancelHandlerRegistration, resetCancelHandlers } from "../cancel-handlers.js";
|
|
7
9
|
import { rememberCommittedCheckpointBytes } from "../checkpoint-commit-snapshots.js";
|
|
8
10
|
import { getWorkflowExecutionStartedAt } from "../handles.js";
|
|
9
11
|
import { loadWorkflowState } from "../storage-io.js";
|
|
10
12
|
import { getComposedWorkflowInterceptor } from "../strategy-helpers.js";
|
|
11
13
|
import { decodeWorkflowState } from "../validation.js";
|
|
14
|
+
import { buildWorkflowVisibilityIndexTransition } from "../workflow-indexes.js";
|
|
12
15
|
import { prepareResumeState } from "./persist.js";
|
|
13
16
|
import { reprovideRecoveredServices } from "./recovered-services.js";
|
|
14
17
|
import {
|
|
@@ -95,8 +98,9 @@ async function performSerializedResume(internals, args) {
|
|
|
95
98
|
assertResumeNotTerminating(internals, workflowId);
|
|
96
99
|
if (!latestState)
|
|
97
100
|
throw Error(`Workflow "${workflowId}" not found in storage`);
|
|
98
|
-
if (latestState.status !== "running")
|
|
99
|
-
throw Error(`Cannot resume workflow "${workflowId}": status is "${latestState.status}", expected "running"`);
|
|
101
|
+
if (latestState.status !== "running" && latestState.status !== "suspended")
|
|
102
|
+
throw Error(`Cannot resume workflow "${workflowId}": status is "${latestState.status}", expected "running" or "suspended"`);
|
|
103
|
+
await reactivateSuspendedWorkflowState(internals, latestState);
|
|
100
104
|
commitSerializedResumeState(internals, args);
|
|
101
105
|
if (internals.inlineStrategy) {
|
|
102
106
|
relaunchInlineWorkflowAfterResume(internals, latestState, args);
|
|
@@ -104,13 +108,30 @@ async function performSerializedResume(internals, args) {
|
|
|
104
108
|
}
|
|
105
109
|
relaunchWorkerWorkflowAfterResume(internals, latestState, args);
|
|
106
110
|
}
|
|
111
|
+
async function reactivateSuspendedWorkflowState(internals, state) {
|
|
112
|
+
if (state.status !== "suspended")
|
|
113
|
+
return;
|
|
114
|
+
const previousState = { ...state };
|
|
115
|
+
state.status = "running";
|
|
116
|
+
state.updatedAt = internals.options.getNow();
|
|
117
|
+
await internals.storage.batch([
|
|
118
|
+
{ type: "put", key: KEYS.workflow(state.id), value: encode(state) },
|
|
119
|
+
...buildWorkflowVisibilityIndexTransition(state.id, previousState, state).batchOps,
|
|
120
|
+
...state.executionDeadline !== void 0 ? buildTimerBatchOperations({
|
|
121
|
+
id: `deadline:${state.id}`,
|
|
122
|
+
workflowId: state.id,
|
|
123
|
+
fireAt: state.executionDeadline,
|
|
124
|
+
kind: "execution-deadline"
|
|
125
|
+
}) : []
|
|
126
|
+
]);
|
|
127
|
+
}
|
|
107
128
|
export async function resumeWorkflowFromStorage(internals, workflowId, dispatchResumedEvent, callbacks) {
|
|
108
129
|
const stateBytes = await internals.storage.get(KEYS.workflow(workflowId));
|
|
109
130
|
if (!stateBytes)
|
|
110
131
|
throw Error(`Workflow "${workflowId}" not found in storage`);
|
|
111
132
|
const state = decodeWorkflowState(stateBytes);
|
|
112
|
-
if (state.status !== "running")
|
|
113
|
-
throw Error(`Cannot resume workflow "${workflowId}": status is "${state.status}", expected "running"`);
|
|
133
|
+
if (state.status !== "running" && state.status !== "suspended")
|
|
134
|
+
throw Error(`Cannot resume workflow "${workflowId}": status is "${state.status}", expected "running" or "suspended"`);
|
|
114
135
|
const checkpointBytes = await internals.storage.get(KEYS.checkpoint(workflowId));
|
|
115
136
|
if (!checkpointBytes)
|
|
116
137
|
throw Error(`Checkpoint not found for workflow "${workflowId}"`);
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { BatchOperation, ConditionalBatchCondition } from '../../../storage/interface.ts';
|
|
2
|
+
import type { Checkpoint, StartOptions, TimerEntry, WorkflowState } from '../../types.ts';
|
|
3
|
+
import type { EngineInternals } from '../internals.ts';
|
|
4
|
+
import { type LifecycleCallbacks, type RegistrationEntry } from './shared.ts';
|
|
5
|
+
/**
|
|
6
|
+
* Builds the id-dependent operations and compare-and-swap preconditions for an
|
|
7
|
+
* idempotent start or `startOrSignal`. Invoked by `startWorkflow` with the real
|
|
8
|
+
* `workflowId` once it has been generated, so the idempotency mapping put (and
|
|
9
|
+
* any create-batch signal) can carry that id. The whole start batch then commits
|
|
10
|
+
* through a single `storageConditionalBatch` gated on the returned conditions; a
|
|
11
|
+
* lost CAS rolls back the start and throws {@link StartIdempotencyRaceLostError}
|
|
12
|
+
* so the caller resolves to the winner.
|
|
13
|
+
*/
|
|
14
|
+
export type BuildIdempotentStartOperations = (workflowId: string) => {
|
|
15
|
+
operations: BatchOperation[];
|
|
16
|
+
conditions: ConditionalBatchCondition[];
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Internal sentinel: the idempotent create batch lost its compare-and-swap to a
|
|
20
|
+
* concurrent caller holding the same idempotency key. Never surfaced to users —
|
|
21
|
+
* `start` / `startOrSignal` catch it and resolve to the winning run's handle.
|
|
22
|
+
*/
|
|
23
|
+
export declare class StartIdempotencyRaceLostError extends Error {
|
|
24
|
+
constructor();
|
|
25
|
+
}
|
|
26
|
+
/** Everything {@link buildAndCommitStartBatch} needs to assemble the start batch. */
|
|
27
|
+
export type StartBatchContext = {
|
|
28
|
+
internals: EngineInternals;
|
|
29
|
+
workflowId: string;
|
|
30
|
+
state: WorkflowState;
|
|
31
|
+
checkpoint: Checkpoint;
|
|
32
|
+
registration: RegistrationEntry;
|
|
33
|
+
options: StartOptions | undefined;
|
|
34
|
+
delayedStartTimer: TimerEntry | undefined;
|
|
35
|
+
persistedWorkflowStartHeaders: Map<string, string> | undefined;
|
|
36
|
+
additionalStartOperations: BatchOperation[] | undefined;
|
|
37
|
+
callbacks: LifecycleCallbacks;
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Assemble the start batch — folding in the id-dependent idempotency mapping and
|
|
41
|
+
* create-batch signal once the real workflow id exists — and commit it, gated on
|
|
42
|
+
* any idempotency preconditions. Throws {@link StartIdempotencyRaceLostError}
|
|
43
|
+
* when a concurrent same-key caller won the compare-and-swap, so the calling
|
|
44
|
+
* `startWorkflow` rolls back its transient state and the wrapper resolves to the
|
|
45
|
+
* winning run.
|
|
46
|
+
*/
|
|
47
|
+
export declare function buildAndCommitStartBatch(context: StartBatchContext, buildIdempotentStartOperations: BuildIdempotentStartOperations | undefined): Promise<void>;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { requireStorageCapability, storageConditionalBatch } from "../../../storage/interface.js";
|
|
2
|
+
import { buildStartBatchOperations } from "./start-batch.js";
|
|
3
|
+
|
|
4
|
+
export class StartIdempotencyRaceLostError extends Error {
|
|
5
|
+
constructor() {
|
|
6
|
+
super("start idempotency compare-and-swap lost to a concurrent caller");
|
|
7
|
+
this.name = "StartIdempotencyRaceLostError";
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
async function persistStartBatch(internals, startOperations, conditions) {
|
|
11
|
+
if (conditions === void 0) {
|
|
12
|
+
await internals.storage.batch(startOperations);
|
|
13
|
+
return !0;
|
|
14
|
+
}
|
|
15
|
+
requireStorageCapability(internals.storage, "conditionalBatch", "start idempotency");
|
|
16
|
+
return storageConditionalBatch(internals.storage, conditions, startOperations);
|
|
17
|
+
}
|
|
18
|
+
function mergeAdditionalStartOperations(additional, idempotent) {
|
|
19
|
+
if (idempotent === void 0 || idempotent.length === 0)
|
|
20
|
+
return additional;
|
|
21
|
+
return [...additional ?? [], ...idempotent];
|
|
22
|
+
}
|
|
23
|
+
export async function buildAndCommitStartBatch(context, buildIdempotentStartOperations) {
|
|
24
|
+
const { internals, workflowId, state, checkpoint, registration, options } = context, idempotent = buildIdempotentStartOperations?.(workflowId), startOperations = buildStartBatchOperations(internals, workflowId, state, checkpoint, registration, options, state.executionDeadline, context.delayedStartTimer, context.persistedWorkflowStartHeaders, mergeAdditionalStartOperations(context.additionalStartOperations, idempotent?.operations), context.callbacks);
|
|
25
|
+
if (!await persistStartBatch(internals, startOperations, idempotent?.conditions))
|
|
26
|
+
throw new StartIdempotencyRaceLostError;
|
|
27
|
+
}
|
|
@@ -1,5 +1,33 @@
|
|
|
1
|
-
import type { Checkpoint } from '../../types.ts';
|
|
1
|
+
import type { Checkpoint, StartOptions, WorkflowState } from '../../types.ts';
|
|
2
2
|
import type { EngineInternals } from '../internals.ts';
|
|
3
|
-
import { type LifecycleCallbacks } from './shared.ts';
|
|
3
|
+
import { type LifecycleCallbacks, type RegistrationEntry } from './shared.ts';
|
|
4
4
|
export declare function runWorkflowStartInterceptor(_internals: EngineInternals, workflowId: string, workflowType: string, input: unknown, parentHeaders: Map<string, string> | undefined, callbacks: LifecycleCallbacks): Map<string, string> | undefined;
|
|
5
5
|
export declare function startWorkflowExecution(internals: EngineInternals, workflowId: string, workflowType: string, input: unknown, checkpoint: Checkpoint, nestingDepth: number, executionDeadline: number | undefined, executionStateOwnerId: string, _callbacks?: LifecycleCallbacks): void;
|
|
6
|
+
export declare function beginWorkflowExecution(internals: EngineInternals, workflowId: string, workflowType: string, input: unknown, checkpoint: Checkpoint, executionDeadline: number | undefined, executionStateOwnerId: string, _registration: RegistrationEntry, callbacks: LifecycleCallbacks, onStarted?: () => void): void;
|
|
7
|
+
/**
|
|
8
|
+
* `defer: false` is an inline-only liveness gate: it awaits the moment the
|
|
9
|
+
* generator is first driven. A worker-mode start queues to the Worker transport
|
|
10
|
+
* (no inline liveness to await), and a delayed start has not begun executing at
|
|
11
|
+
* all — so both reject rather than silently behaving like `defer: true`.
|
|
12
|
+
*/
|
|
13
|
+
export declare function assertDeferSupported(internals: EngineInternals, options: StartOptions | undefined, isDelayedStart: boolean): void;
|
|
14
|
+
type BeginExecutionParams = {
|
|
15
|
+
type: string;
|
|
16
|
+
input: unknown;
|
|
17
|
+
checkpoint: Checkpoint;
|
|
18
|
+
state: WorkflowState;
|
|
19
|
+
registration: RegistrationEntry;
|
|
20
|
+
options: StartOptions | undefined;
|
|
21
|
+
isDelayed: boolean;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Drive the initial execution for a freshly-started workflow and, when
|
|
25
|
+
* `defer: false`, await the run actually beginning before resolving. A delayed
|
|
26
|
+
* start does not begin executing now, so it neither begins execution here nor
|
|
27
|
+
* awaits liveness. The liveness promise is settled by the inline-launch queue
|
|
28
|
+
* once the generator is driven (or by dispose if the queued start is discarded),
|
|
29
|
+
* so a `defer: false` caller can rely on the run being live without a macrotask
|
|
30
|
+
* round-trip the moment `engine.start()` resolves.
|
|
31
|
+
*/
|
|
32
|
+
export declare function beginExecutionAwaitingLiveness(internals: EngineInternals, params: BeginExecutionParams, workflowId: string, callbacks: LifecycleCallbacks): Promise<void>;
|
|
33
|
+
export {};
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { serializeCheckpoint } from "../../checkpoint.js";
|
|
2
|
+
import { WorkflowStartedEvent } from "../../events.js";
|
|
3
|
+
import { StartWorkflowValidationError } from "../../start-workflow-validation.js";
|
|
2
4
|
export function runWorkflowStartInterceptor(_internals, workflowId, workflowType, input, parentHeaders, callbacks) {
|
|
3
5
|
const composedInterceptor = callbacks.getComposedWorkflowInterceptor();
|
|
4
6
|
if (!composedInterceptor)
|
|
@@ -37,3 +39,39 @@ export function startWorkflowExecution(internals, workflowId, workflowType, inpu
|
|
|
37
39
|
}
|
|
38
40
|
});
|
|
39
41
|
}
|
|
42
|
+
export function beginWorkflowExecution(internals, workflowId, workflowType, input, checkpoint, executionDeadline, executionStateOwnerId, _registration, callbacks, onStarted) {
|
|
43
|
+
const nestingDepth = internals.pendingNestingDepth ?? 0;
|
|
44
|
+
internals.pendingNestingDepth = void 0;
|
|
45
|
+
if (internals.inlineStrategy !== null) {
|
|
46
|
+
callbacks.queueInlineWorkflowExecutionStart({
|
|
47
|
+
workflowId,
|
|
48
|
+
workflowType,
|
|
49
|
+
input,
|
|
50
|
+
checkpoint,
|
|
51
|
+
nestingDepth,
|
|
52
|
+
executionDeadline,
|
|
53
|
+
executionStateOwnerId,
|
|
54
|
+
...onStarted !== void 0 && { onStarted }
|
|
55
|
+
});
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
callbacks.dispatchEvent(new WorkflowStartedEvent(workflowId, workflowType, input));
|
|
59
|
+
startWorkflowExecution(internals, workflowId, workflowType, input, checkpoint, nestingDepth, executionDeadline, executionStateOwnerId, callbacks);
|
|
60
|
+
onStarted?.();
|
|
61
|
+
}
|
|
62
|
+
export function assertDeferSupported(internals, options, isDelayedStart) {
|
|
63
|
+
if (options?.defer !== !1)
|
|
64
|
+
return;
|
|
65
|
+
if (internals.inlineStrategy === null)
|
|
66
|
+
throw new StartWorkflowValidationError('options.defer: false is only supported in inline execution mode; a worker-mode start cannot be awaited for inline liveness. Use workflowExecutionMode: "inline" or remove defer.');
|
|
67
|
+
if (isDelayedStart)
|
|
68
|
+
throw new StartWorkflowValidationError("options.defer: false is incompatible with a delayed start (startAt/startAfter): a scheduled run has not begun executing, so there is no liveness to await. Remove defer or the delayed-start option.");
|
|
69
|
+
}
|
|
70
|
+
export async function beginExecutionAwaitingLiveness(internals, params, workflowId, callbacks) {
|
|
71
|
+
if (params.isDelayed)
|
|
72
|
+
return;
|
|
73
|
+
const liveness = params.options?.defer === !1 ? Promise.withResolvers() : void 0;
|
|
74
|
+
beginWorkflowExecution(internals, workflowId, params.type, params.input, params.checkpoint, params.state.executionDeadline, params.state.executionStateOwnerId ?? workflowId, params.registration, callbacks, liveness ? () => liveness.resolve() : void 0);
|
|
75
|
+
if (liveness)
|
|
76
|
+
await liveness.promise;
|
|
77
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { StartOrSignalSignal } from '../../types.ts';
|
|
2
|
+
import { type WorkflowHandle } from '../handles.ts';
|
|
3
|
+
import type { EngineInternals } from '../internals.ts';
|
|
4
|
+
import { type LifecycleCallbacks } from './shared.ts';
|
|
5
|
+
/**
|
|
6
|
+
* Callbacks `startOrSignal` needs beyond the lifecycle set: a way to deliver a
|
|
7
|
+
* signal to an already-running workflow through the full engine signal path
|
|
8
|
+
* (interceptors, events, parked-run wakeups). Supplied by the engine so the
|
|
9
|
+
* "signal an existing non-terminal run" branch reuses `engine.signal` with the
|
|
10
|
+
* same `signalId` the create batch would have used.
|
|
11
|
+
*/
|
|
12
|
+
export type StartOrSignalCallbacks = LifecycleCallbacks & {
|
|
13
|
+
signalExistingWorkflow: (workflowId: string, signalName: string, payload: unknown, signalId: string) => Promise<void>;
|
|
14
|
+
};
|
|
15
|
+
/** The value stored at `KEYS.startIdempotency(key)`: the workflow the key created. */
|
|
16
|
+
export type StartIdempotencyMapping = {
|
|
17
|
+
workflowId: string;
|
|
18
|
+
};
|
|
19
|
+
/** Resolve an existing workflow id for an idempotency key, if one was created. */
|
|
20
|
+
export declare function resolveIdempotencyKeyWorkflowId(internals: EngineInternals, idempotencyKey: string): Promise<string | undefined>;
|
|
21
|
+
/**
|
|
22
|
+
* Resolve a key-mapped workflow id to a handle id, asserting its record still
|
|
23
|
+
* exists. The `start-idem:` mapping is permanent — it survives BOTH terminal
|
|
24
|
+
* cleanup AND purge/retention (those reclaim the workflow record, never the
|
|
25
|
+
* `start-idem:` keyspace) — so a present mapping whose record is gone means the
|
|
26
|
+
* key is spent: a fresh create would fail the still-present mapping CAS and strand
|
|
27
|
+
* the caller. Surface {@link IdempotencyKeyPurgedError} instead of handing back a
|
|
28
|
+
* handle to a vanished run. Shared by the synchronous mapping hit and the
|
|
29
|
+
* post-race winner lookup so both reject a purged key identically.
|
|
30
|
+
*/
|
|
31
|
+
export declare function resolveExistingRunOrThrowPurged(internals: EngineInternals, workflowId: string): Promise<string>;
|
|
32
|
+
/**
|
|
33
|
+
* Resolve a caller-`id` create-race loss without conflating an in-memory
|
|
34
|
+
* reservation with a durable record. A loser collides on the winner's
|
|
35
|
+
* `pendingStarts` reservation (start.ts) BEFORE the winner commits, so the bare
|
|
36
|
+
* collision proves nothing about whether a run will exist.
|
|
37
|
+
*
|
|
38
|
+
* Read the winner's record FIRST: this catches a winner that has already committed
|
|
39
|
+
* but is still non-terminal before it can complete (a fast workflow consumes its
|
|
40
|
+
* create-batch signal and finishes the moment it is driven — reading immediately
|
|
41
|
+
* resolves it instead of racing it to a terminal-conflict). Only when the record is
|
|
42
|
+
* absent do we wait for the reservation to clear and read once more to discriminate:
|
|
43
|
+
*
|
|
44
|
+
* - **record present** — the winner committed: signal it (or conflict if terminal)
|
|
45
|
+
* and return the handle.
|
|
46
|
+
* - **record absent after the reservation clears** — the winner aborted before
|
|
47
|
+
* committing (storage failure, oversized payload, throwing start interceptor): no
|
|
48
|
+
* run exists, so return `undefined` and let the caller retry its own create.
|
|
49
|
+
*/
|
|
50
|
+
export declare function resolveCallerIdWinnerOrRetry(internals: EngineInternals, winnerId: string, signalSpec: StartOrSignalSignal, signalId: string, callbacks: StartOrSignalCallbacks): Promise<WorkflowHandle | undefined>;
|
|
51
|
+
/**
|
|
52
|
+
* For a workflow that already exists: signal it if non-terminal, conflict if
|
|
53
|
+
* terminal. Returns the handle on a successful signal, or `undefined` when the
|
|
54
|
+
* workflow record is not present (so the caller falls through to create).
|
|
55
|
+
*/
|
|
56
|
+
export declare function signalOrConflictExistingWorkflow(internals: EngineInternals, workflowId: string, signalSpec: StartOrSignalSignal, signalId: string, callbacks: StartOrSignalCallbacks): Promise<WorkflowHandle | undefined>;
|
|
57
|
+
/**
|
|
58
|
+
* Signal a KEYED race winner, bounded-retrying when its record is not yet readable.
|
|
59
|
+
* The keyed winner commits its record atomically with the `start-idem:` mapping, so
|
|
60
|
+
* the record is guaranteed to exist — but the loser may read before the commit
|
|
61
|
+
* settles, so a short delay between reads lets it land. (Caller-`id` winners can
|
|
62
|
+
* abort before committing and are handled by `resolveCallerIdWinnerOrRetry`, not
|
|
63
|
+
* here.)
|
|
64
|
+
*
|
|
65
|
+
* After {@link WINNER_RESOLUTION_MAX_ATTEMPTS} reads with no record, the record is
|
|
66
|
+
* absent for a committed-with-mapping winner only because it was purged: re-read
|
|
67
|
+
* the mapping, and if it still resolves to this exact `winnerId` the key is spent —
|
|
68
|
+
* throw {@link IdempotencyKeyPurgedError}. A mapping that now resolves to a
|
|
69
|
+
* DIFFERENT id (or vanished) cannot prove this winner was purged, so it falls
|
|
70
|
+
* through to the invariant throw rather than mislabelling external keyspace
|
|
71
|
+
* mutation as a spent key.
|
|
72
|
+
*/
|
|
73
|
+
export declare function resolveWinnerWithSignal(internals: EngineInternals, winnerId: string, signalSpec: StartOrSignalSignal, signalId: string, callbacks: StartOrSignalCallbacks, idempotencyKey: string): Promise<WorkflowHandle>;
|
|
74
|
+
/**
|
|
75
|
+
* Read the winning workflow id from the idempotency mapping after a lost CAS. The
|
|
76
|
+
* mapping must exist once any caller's create commits; its absence means the
|
|
77
|
+
* `start-idem:` keyspace was mutated externally.
|
|
78
|
+
*/
|
|
79
|
+
export declare function requireWinnerId(internals: EngineInternals, idempotencyKey: string): Promise<string>;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { sleep } from "../../../runtime/portable.js";
|
|
2
|
+
import { KEYS } from "../../../storage/interface.js";
|
|
3
|
+
import { decode } from "../../codec.js";
|
|
4
|
+
import { IdempotencyKeyPurgedError, StartOrSignalConflictError } from "../errors.js";
|
|
5
|
+
import { loadWorkflowState } from "../storage-io.js";
|
|
6
|
+
import { isTerminalWorkflowStatus } from "../validation.js";
|
|
7
|
+
export async function resolveIdempotencyKeyWorkflowId(internals, idempotencyKey) {
|
|
8
|
+
const bytes = await internals.storage.get(KEYS.startIdempotency(idempotencyKey));
|
|
9
|
+
if (bytes === null)
|
|
10
|
+
return;
|
|
11
|
+
return decode(bytes).workflowId;
|
|
12
|
+
}
|
|
13
|
+
export async function resolveExistingRunOrThrowPurged(internals, workflowId) {
|
|
14
|
+
if (await loadWorkflowState(internals, workflowId) === null)
|
|
15
|
+
throw new IdempotencyKeyPurgedError(workflowId);
|
|
16
|
+
return workflowId;
|
|
17
|
+
}
|
|
18
|
+
const RESERVATION_CLEAR_MAX_ATTEMPTS = 5, RESERVATION_CLEAR_RETRY_DELAY_MS = 5;
|
|
19
|
+
async function awaitReservationCleared(internals, workflowId) {
|
|
20
|
+
for (let attempt = 0;attempt < RESERVATION_CLEAR_MAX_ATTEMPTS; attempt += 1) {
|
|
21
|
+
if (!internals.pendingStarts.has(workflowId))
|
|
22
|
+
return;
|
|
23
|
+
await sleep(RESERVATION_CLEAR_RETRY_DELAY_MS);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export async function resolveCallerIdWinnerOrRetry(internals, winnerId, signalSpec, signalId, callbacks) {
|
|
27
|
+
const resolved = await signalOrConflictExistingWorkflow(internals, winnerId, signalSpec, signalId, callbacks);
|
|
28
|
+
if (resolved !== void 0)
|
|
29
|
+
return resolved;
|
|
30
|
+
await awaitReservationCleared(internals, winnerId);
|
|
31
|
+
return signalOrConflictExistingWorkflow(internals, winnerId, signalSpec, signalId, callbacks);
|
|
32
|
+
}
|
|
33
|
+
export async function signalOrConflictExistingWorkflow(internals, workflowId, signalSpec, signalId, callbacks) {
|
|
34
|
+
const state = await loadWorkflowState(internals, workflowId);
|
|
35
|
+
if (state === null)
|
|
36
|
+
return;
|
|
37
|
+
if (isTerminalWorkflowStatus(state.status))
|
|
38
|
+
throw new StartOrSignalConflictError(workflowId, state.status);
|
|
39
|
+
await callbacks.signalExistingWorkflow(workflowId, signalSpec.name, signalSpec.payload, signalId);
|
|
40
|
+
return callbacks.getHandle(workflowId);
|
|
41
|
+
}
|
|
42
|
+
const WINNER_RESOLUTION_MAX_ATTEMPTS = 5, WINNER_RESOLUTION_RETRY_DELAY_MS = 5;
|
|
43
|
+
export async function resolveWinnerWithSignal(internals, winnerId, signalSpec, signalId, callbacks, idempotencyKey) {
|
|
44
|
+
for (let attempt = 0;attempt < WINNER_RESOLUTION_MAX_ATTEMPTS; attempt += 1) {
|
|
45
|
+
const resolved = await signalOrConflictExistingWorkflow(internals, winnerId, signalSpec, signalId, callbacks);
|
|
46
|
+
if (resolved !== void 0)
|
|
47
|
+
return resolved;
|
|
48
|
+
if (attempt < WINNER_RESOLUTION_MAX_ATTEMPTS - 1)
|
|
49
|
+
await sleep(WINNER_RESOLUTION_RETRY_DELAY_MS);
|
|
50
|
+
}
|
|
51
|
+
if (await resolveIdempotencyKeyWorkflowId(internals, idempotencyKey) === winnerId)
|
|
52
|
+
throw new IdempotencyKeyPurgedError(winnerId);
|
|
53
|
+
throw Error(`startOrSignal resolved winning workflow "${winnerId}" but its record never became readable after ${WINNER_RESOLUTION_MAX_ATTEMPTS} attempts.`);
|
|
54
|
+
}
|
|
55
|
+
export async function requireWinnerId(internals, idempotencyKey) {
|
|
56
|
+
const winnerId = await resolveIdempotencyKeyWorkflowId(internals, idempotencyKey);
|
|
57
|
+
if (winnerId === void 0)
|
|
58
|
+
throw Error(`start idempotency mapping for key "${idempotencyKey}" vanished after a lost compare-and-swap; the start-idem: keyspace may have been mutated externally.`);
|
|
59
|
+
return winnerId;
|
|
60
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { StartOptions, StartOrSignalSignal } from '../../types.ts';
|
|
2
|
+
import { type WorkflowHandle } from '../handles.ts';
|
|
3
|
+
import type { EngineInternals } from '../internals.ts';
|
|
4
|
+
import { type LifecycleCallbacks } from './shared.ts';
|
|
5
|
+
import { type StartOrSignalCallbacks } from './start-or-signal-resolution.ts';
|
|
6
|
+
export type { StartOrSignalCallbacks };
|
|
7
|
+
/**
|
|
8
|
+
* Enforce at-most-once start for a given `idempotencyKey`. On the first call,
|
|
9
|
+
* the workflow record and a `startIdempotency(key) → { workflowId }` mapping
|
|
10
|
+
* commit in one compare-and-swap gated on the mapping being absent. Every later
|
|
11
|
+
* call with the same key resolves the mapping and returns a handle to that run —
|
|
12
|
+
* even if it has since reached a terminal state (idempotent start is a pure
|
|
13
|
+
* dedup; it never restarts).
|
|
14
|
+
*
|
|
15
|
+
* Concurrent same-key callers race at the lookup→commit gap; the CAS lets exactly
|
|
16
|
+
* one win, and the loser (its create batch rejected) resolves to the winner's
|
|
17
|
+
* run. Requires the `conditionalBatch` capability and throws if it is absent —
|
|
18
|
+
* single-execution semantics cannot be honored without atomic compare-and-swap.
|
|
19
|
+
*/
|
|
20
|
+
export declare function startWithIdempotency(internals: EngineInternals, type: string, input: unknown, options: StartOptions, callbacks: LifecycleCallbacks): Promise<WorkflowHandle>;
|
|
21
|
+
/**
|
|
22
|
+
* Atomic start-or-signal (signal-with-start). Resolves the target workflow, then:
|
|
23
|
+
*
|
|
24
|
+
* - **Absent** → create the workflow and deliver the signal in ONE conditional
|
|
25
|
+
* batch (workflow record + `sig:`/`sigres:` pair + optional idempotency
|
|
26
|
+
* mapping). The freshly-launched run consumes the signal on its first drive.
|
|
27
|
+
* - **Non-terminal** (running, pending, suspended) → deliver the signal through
|
|
28
|
+
* the standard engine signal path with the same `signalId`, so it dedups
|
|
29
|
+
* against a create-batch signal a concurrent winner may have written.
|
|
30
|
+
* - **Terminal** → throw {@link StartOrSignalConflictError}: a finished run
|
|
31
|
+
* cannot be signalled and is not silently replaced.
|
|
32
|
+
*
|
|
33
|
+
* Convergence requires a SHARED workflow identity. Concurrent callers converge on
|
|
34
|
+
* one workflow and one signal only when they share an `options.idempotencyKey`
|
|
35
|
+
* (the durable mapping picks one creator and the signal id derives from the key)
|
|
36
|
+
* or an explicit `options.id` (the caller-id reservation picks one creator).
|
|
37
|
+
*
|
|
38
|
+
* A bare `signal.signalId` with NEITHER `options.id` nor `options.idempotencyKey`
|
|
39
|
+
* does NOT converge: each absent-target call generates its own workflow id, so
|
|
40
|
+
* concurrent callers create distinct runs and each delivers its own signal. In
|
|
41
|
+
* that mode `startOrSignal` is an atomic start-with-one-initial-signal, not a
|
|
42
|
+
* convergence primitive. Use `idempotencyKey` (id-free convergence) or
|
|
43
|
+
* `id` + `signalId` when concurrent callers must converge.
|
|
44
|
+
*/
|
|
45
|
+
export declare function startOrSignal(internals: EngineInternals, type: string, input: unknown, signalSpec: StartOrSignalSignal, options: StartOptions | undefined, callbacks: StartOrSignalCallbacks): Promise<WorkflowHandle>;
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { KEYS, requireStorageCapability } from "../../../storage/interface.js";
|
|
2
|
+
import { encode } from "../../codec.js";
|
|
3
|
+
import {
|
|
4
|
+
assertValidIdempotencyKey,
|
|
5
|
+
StartWorkflowValidationError
|
|
6
|
+
} from "../../start-workflow-validation.js";
|
|
7
|
+
import { IdempotencyKeyPurgedError, WorkflowAlreadyExistsError } from "../errors.js";
|
|
8
|
+
import { buildCreateBatchSignalOperations } from "../signals.js";
|
|
9
|
+
import {
|
|
10
|
+
StartIdempotencyRaceLostError
|
|
11
|
+
} from "./start-commit.js";
|
|
12
|
+
import {
|
|
13
|
+
requireWinnerId,
|
|
14
|
+
resolveCallerIdWinnerOrRetry,
|
|
15
|
+
resolveExistingRunOrThrowPurged,
|
|
16
|
+
resolveIdempotencyKeyWorkflowId,
|
|
17
|
+
resolveWinnerWithSignal,
|
|
18
|
+
signalOrConflictExistingWorkflow
|
|
19
|
+
} from "./start-or-signal-resolution.js";
|
|
20
|
+
import { startWorkflow } from "./start.js";
|
|
21
|
+
function resolveSignalId(signalSpec, idempotencyKey) {
|
|
22
|
+
if (idempotencyKey !== void 0)
|
|
23
|
+
return KEYS.startIdempotencySignalId(idempotencyKey);
|
|
24
|
+
if (signalSpec.signalId !== void 0)
|
|
25
|
+
return signalSpec.signalId;
|
|
26
|
+
throw new StartWorkflowValidationError("startOrSignal requires either signal.signalId or options.idempotencyKey to identify the signal to deliver. (Concurrent callers converge on one workflow and one signal only with a shared idempotencyKey, or a shared id plus signalId; a bare signalId starts a fresh run per caller.)");
|
|
27
|
+
}
|
|
28
|
+
function idempotentStartOperationsFor(internals, idempotencyKey, signal) {
|
|
29
|
+
return (workflowId) => {
|
|
30
|
+
const operations = [], conditions = [];
|
|
31
|
+
if (idempotencyKey !== void 0) {
|
|
32
|
+
const key = KEYS.startIdempotency(idempotencyKey);
|
|
33
|
+
operations.push({
|
|
34
|
+
type: "put",
|
|
35
|
+
key,
|
|
36
|
+
value: encode({ workflowId })
|
|
37
|
+
});
|
|
38
|
+
conditions.push({ key, expectedValue: null });
|
|
39
|
+
}
|
|
40
|
+
if (signal !== void 0) {
|
|
41
|
+
const built = buildCreateBatchSignalOperations(internals, workflowId, signal.name, signal.payload, signal.signalId);
|
|
42
|
+
operations.push(...built.operations);
|
|
43
|
+
conditions.push(built.condition);
|
|
44
|
+
}
|
|
45
|
+
return { operations, conditions };
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
export async function startWithIdempotency(internals, type, input, options, callbacks) {
|
|
49
|
+
requireStorageCapability(internals.storage, "conditionalBatch", "start idempotency");
|
|
50
|
+
const { idempotencyKey } = options;
|
|
51
|
+
if (idempotencyKey === void 0)
|
|
52
|
+
throw new StartWorkflowValidationError("startWithIdempotency requires options.idempotencyKey");
|
|
53
|
+
assertValidIdempotencyKey(idempotencyKey, "options.idempotencyKey");
|
|
54
|
+
assertIdAndIdempotencyKeyExclusive(options);
|
|
55
|
+
const existingId = await resolveIdempotencyKeyWorkflowId(internals, idempotencyKey);
|
|
56
|
+
if (existingId !== void 0)
|
|
57
|
+
return callbacks.getHandle(await resolveExistingRunOrThrowPurged(internals, existingId));
|
|
58
|
+
try {
|
|
59
|
+
return await startWorkflow(internals, type, input, options, void 0, callbacks, idempotentStartOperationsFor(internals, idempotencyKey, void 0));
|
|
60
|
+
} catch (error) {
|
|
61
|
+
if (!(error instanceof StartIdempotencyRaceLostError))
|
|
62
|
+
throw error;
|
|
63
|
+
}
|
|
64
|
+
return callbacks.getHandle(await resolveExistingRunOrThrowPurged(internals, await requireWinnerId(internals, idempotencyKey)));
|
|
65
|
+
}
|
|
66
|
+
function assertIdAndIdempotencyKeyExclusive(options) {
|
|
67
|
+
if (options.id !== void 0 && options.idempotencyKey !== void 0)
|
|
68
|
+
throw new StartWorkflowValidationError("options.id and options.idempotencyKey are mutually exclusive: idempotency assigns its own workflow id and dedups through the idempotency key. Provide one or the other.");
|
|
69
|
+
}
|
|
70
|
+
function validateStartOrSignalConvergence(signalSpec, options) {
|
|
71
|
+
const idempotencyKey = options?.idempotencyKey;
|
|
72
|
+
if (idempotencyKey === void 0)
|
|
73
|
+
return;
|
|
74
|
+
assertValidIdempotencyKey(idempotencyKey, "options.idempotencyKey");
|
|
75
|
+
assertIdAndIdempotencyKeyExclusive(options ?? {});
|
|
76
|
+
if (signalSpec.signalId !== void 0)
|
|
77
|
+
throw new StartWorkflowValidationError("startOrSignal does not accept both signal.signalId and options.idempotencyKey: the signal id derives from the idempotency key for convergence. Provide exactly one.");
|
|
78
|
+
}
|
|
79
|
+
export async function startOrSignal(internals, type, input, signalSpec, options, callbacks) {
|
|
80
|
+
requireStorageCapability(internals.storage, "conditionalBatch", "startOrSignal");
|
|
81
|
+
const idempotencyKey = options?.idempotencyKey;
|
|
82
|
+
validateStartOrSignalConvergence(signalSpec, options);
|
|
83
|
+
const signalId = resolveSignalId(signalSpec, idempotencyKey), mappedId = idempotencyKey !== void 0 ? await resolveIdempotencyKeyWorkflowId(internals, idempotencyKey) : void 0, existingId = mappedId ?? options?.id;
|
|
84
|
+
if (existingId !== void 0) {
|
|
85
|
+
const resolved = await signalOrConflictExistingWorkflow(internals, existingId, signalSpec, signalId, callbacks);
|
|
86
|
+
if (resolved !== void 0)
|
|
87
|
+
return resolved;
|
|
88
|
+
if (mappedId !== void 0)
|
|
89
|
+
throw new IdempotencyKeyPurgedError(mappedId);
|
|
90
|
+
}
|
|
91
|
+
return createWithSignalOrFallback(internals, type, input, signalSpec, signalId, options, callbacks);
|
|
92
|
+
}
|
|
93
|
+
const CALLER_ID_CREATE_MAX_ATTEMPTS = 5;
|
|
94
|
+
async function createWithSignalOrFallback(internals, type, input, signalSpec, signalId, options, callbacks) {
|
|
95
|
+
const idempotencyKey = options?.idempotencyKey;
|
|
96
|
+
for (let attempt = 0;attempt < CALLER_ID_CREATE_MAX_ATTEMPTS; attempt += 1) {
|
|
97
|
+
const outcome = await resolveCreateRaceOutcome(internals, options, async () => {
|
|
98
|
+
return startWorkflow(internals, type, input, options, void 0, callbacks, idempotentStartOperationsFor(internals, idempotencyKey, {
|
|
99
|
+
name: signalSpec.name,
|
|
100
|
+
payload: signalSpec.payload,
|
|
101
|
+
signalId
|
|
102
|
+
}));
|
|
103
|
+
});
|
|
104
|
+
if (outcome.kind === "created")
|
|
105
|
+
return outcome.handle;
|
|
106
|
+
if (outcome.kind === "lost-keyed")
|
|
107
|
+
return resolveWinnerWithSignal(internals, outcome.id, signalSpec, signalId, callbacks, outcome.idempotencyKey);
|
|
108
|
+
const resolved = outcome.kind === "lost-caller-id" ? await resolveCallerIdWinnerOrRetry(internals, outcome.id, signalSpec, signalId, callbacks) : await plainCreateBufferedSignalOrResolve(internals, type, input, signalSpec, signalId, options, callbacks);
|
|
109
|
+
if (resolved !== void 0)
|
|
110
|
+
return resolved;
|
|
111
|
+
}
|
|
112
|
+
throw Error(`startOrSignal could not create workflow "${options?.id ?? "<generated>"}" after ${CALLER_ID_CREATE_MAX_ATTEMPTS} attempts: each concurrent same-id winner aborted before its durable commit.`);
|
|
113
|
+
}
|
|
114
|
+
async function plainCreateBufferedSignalOrResolve(internals, type, input, signalSpec, signalId, options, callbacks) {
|
|
115
|
+
try {
|
|
116
|
+
return await startWorkflow(internals, type, input, options, void 0, callbacks);
|
|
117
|
+
} catch (error) {
|
|
118
|
+
if (!(error instanceof WorkflowAlreadyExistsError))
|
|
119
|
+
throw error;
|
|
120
|
+
return resolveCallerIdWinnerOrRetry(internals, error.workflowId, signalSpec, signalId, callbacks);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
async function resolveCreateRaceOutcome(internals, options, runCreate) {
|
|
124
|
+
try {
|
|
125
|
+
return { kind: "created", handle: await runCreate() };
|
|
126
|
+
} catch (error) {
|
|
127
|
+
if (error instanceof WorkflowAlreadyExistsError)
|
|
128
|
+
return { kind: "lost-caller-id", id: error.workflowId };
|
|
129
|
+
if (error instanceof StartIdempotencyRaceLostError) {
|
|
130
|
+
const idempotencyKey = options?.idempotencyKey;
|
|
131
|
+
if (idempotencyKey !== void 0)
|
|
132
|
+
return {
|
|
133
|
+
kind: "lost-keyed",
|
|
134
|
+
id: await requireWinnerId(internals, idempotencyKey),
|
|
135
|
+
idempotencyKey
|
|
136
|
+
};
|
|
137
|
+
return { kind: "signal-already-buffered" };
|
|
138
|
+
}
|
|
139
|
+
throw error;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
@@ -3,11 +3,11 @@ import type { Checkpoint, Duration, StartOptions, TimerEntry, WorkflowState } fr
|
|
|
3
3
|
import { type WorkflowVersionTuple } from '../../workflow-version-tuple.ts';
|
|
4
4
|
import { type WorkflowHandle } from '../handles.ts';
|
|
5
5
|
import type { EngineInternals } from '../internals.ts';
|
|
6
|
-
import { type LifecycleCallbacks
|
|
6
|
+
import { type LifecycleCallbacks } from './shared.ts';
|
|
7
|
+
import { type BuildIdempotentStartOperations } from './start-commit.ts';
|
|
7
8
|
export declare function start(internals: EngineInternals, type: string, input: unknown, options: StartOptions | undefined, callbacks: LifecycleCallbacks): Promise<WorkflowHandle>;
|
|
8
|
-
export declare function startWorkflow(internals: EngineInternals, type: string, input: unknown, options: StartOptions | undefined, additionalStartOperations: BatchOperation[] | undefined, callbacks: LifecycleCallbacks): Promise<WorkflowHandle>;
|
|
9
|
+
export declare function startWorkflow(internals: EngineInternals, type: string, input: unknown, options: StartOptions | undefined, additionalStartOperations: BatchOperation[] | undefined, callbacks: LifecycleCallbacks, buildIdempotentStartOperations?: BuildIdempotentStartOperations): Promise<WorkflowHandle>;
|
|
9
10
|
export declare function resolveScheduledStartAt(internals: EngineInternals, options: StartOptions | undefined, submissionTime: number, callbacks: LifecycleCallbacks): number | undefined;
|
|
10
11
|
export declare function parseStartOptionDuration(_internals: EngineInternals, duration: Duration, fieldName: 'options.executionTimeout' | 'options.startAfter', _callbacks: LifecycleCallbacks): number;
|
|
11
|
-
export declare function beginWorkflowExecution(internals: EngineInternals, workflowId: string, workflowType: string, input: unknown, checkpoint: Checkpoint, executionDeadline: number | undefined, executionStateOwnerId: string, _registration: RegistrationEntry, callbacks: LifecycleCallbacks): void;
|
|
12
12
|
export declare function createInitialWorkflowState(internals: EngineInternals, workflowId: string, type: string, input: unknown, versionTuple: WorkflowVersionTuple, options: StartOptions | undefined, tags: string[] | undefined, executionStateOwnerId: string, delayedStartTimer: TimerEntry | undefined, callbacks: LifecycleCallbacks): WorkflowState;
|
|
13
13
|
export declare function createInitialCheckpoint(internals: EngineInternals, workflowId: string, workflowVersion: string, options: StartOptions | undefined, _callbacks: LifecycleCallbacks): Checkpoint;
|