@lostgradient/weft 0.2.0 → 0.2.1
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/dist/cli-main.js +77 -77
- package/dist/core/context/index.d.ts +1 -0
- package/dist/core/context/index.js +3 -0
- package/dist/core/context/internals.d.ts +1 -0
- package/dist/core/context/internals.js +2 -1
- package/dist/core/context/speculative-child.js +2 -0
- package/dist/core/context/types.d.ts +6 -0
- package/dist/core/engine/bulk-operations-purge.js +1 -0
- package/dist/core/engine/callback-creators-bundles.js +2 -1
- package/dist/core/engine/callback-creators-core.js +2 -1
- package/dist/core/engine/construction.d.ts +1 -0
- package/dist/core/engine/construction.js +5 -2
- package/dist/core/engine/engine-internal-types.d.ts +5 -0
- package/dist/core/engine/index.d.ts +26 -0
- package/dist/core/engine/index.js +7 -1
- package/dist/core/engine/internals.d.ts +8 -0
- package/dist/core/engine/lifecycle/recovered-services.d.ts +45 -0
- package/dist/core/engine/lifecycle/recovered-services.js +34 -0
- package/dist/core/engine/lifecycle/resume.js +8 -1
- package/dist/core/engine/lifecycle/shared.d.ts +8 -0
- package/dist/core/engine/lifecycle/start-batch.js +23 -12
- package/dist/core/engine/lifecycle/start.js +11 -0
- package/dist/core/engine/operations-data.d.ts +16 -0
- package/dist/core/engine/operations-data.js +6 -0
- package/dist/core/engine/operations-time.d.ts +3 -2
- package/dist/core/engine/operations-time.js +6 -1
- package/dist/core/engine/termination/cleanup.js +2 -0
- package/dist/core/inline-execution-strategy.d.ts +5 -0
- package/dist/core/inline-execution-strategy.js +2 -1
- package/dist/core/types/options.d.ts +89 -0
- package/dist/core/types/workflow-context.d.ts +25 -0
- package/dist/core/weft-error.d.ts +45 -13
- package/dist/core/weft-error.js +9 -0
- package/dist/index.d.ts +7 -2
- package/dist/index.js +1 -1
- package/dist/json-schema.js +3 -3
- package/dist/mcp/cli.js +17 -17
- package/dist/server/handler.js +12 -12
- package/dist/server/index.js +24 -24
- package/dist/service-worker/index.js +12 -12
- package/dist/storage/http.js +1 -1
- package/dist/storage/index.d.ts +2 -1
- package/dist/storage/indexeddb.js +1 -1
- package/dist/storage/interface.d.ts +14 -0
- 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/lmdb.js +1 -1
- package/dist/storage/memory.js +1 -1
- package/dist/storage/resolve.js +1 -1
- package/dist/storage/scoped-storage.js +1 -1
- package/dist/storage/text-value-store.d.ts +4 -1
- package/dist/storage/turso.js +1 -1
- package/dist/storage/typed-storage.js +1 -1
- package/dist/storage/web-extension.js +1 -1
- package/dist/testing/event-loop.d.ts +36 -2
- package/dist/testing/index.d.ts +31 -1
- package/dist/testing/index.js +17 -17
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -27,6 +27,7 @@ export declare class Context implements WorkflowContext {
|
|
|
27
27
|
readonly signal: AbortSignal;
|
|
28
28
|
constructor(options: ContextOptions);
|
|
29
29
|
get executionTimeRemaining(): number;
|
|
30
|
+
get services(): unknown;
|
|
30
31
|
get stepIndex(): number;
|
|
31
32
|
get nestingDepth(): number;
|
|
32
33
|
get accumulatedResults(): Map<number, unknown>;
|
|
@@ -25,6 +25,7 @@ export interface ContextInternals {
|
|
|
25
25
|
executionStateOwnerId: string;
|
|
26
26
|
resolveWorkflowType: ((target: string | Function) => string) | undefined;
|
|
27
27
|
registerCancelHandler: ((handler: () => Promise<void> | void) => () => void) | undefined;
|
|
28
|
+
services: unknown;
|
|
28
29
|
}
|
|
29
30
|
export declare function initializeInternals(context: Context, options: ContextOptions, initialSessionState: Record<string, unknown> | undefined): void;
|
|
30
31
|
export declare function getInternals(context: Context): ContextInternals;
|
|
@@ -24,7 +24,8 @@ export function initializeInternals(context, options, initialSessionState) {
|
|
|
24
24
|
nestingDepth: options.nestingDepth ?? 0,
|
|
25
25
|
executionStateOwnerId: options.executionStateOwnerId ?? options.workflowId,
|
|
26
26
|
resolveWorkflowType: options.resolveWorkflowType,
|
|
27
|
-
registerCancelHandler: options.registerCancelHandler
|
|
27
|
+
registerCancelHandler: options.registerCancelHandler,
|
|
28
|
+
services: options.services
|
|
28
29
|
};
|
|
29
30
|
INTERNALS.set(context, internals);
|
|
30
31
|
}
|
|
@@ -16,6 +16,8 @@ function assignOptionalContextOptions(options, internals) {
|
|
|
16
16
|
options.sleepReferenceTime = internals.sleepReferenceTime;
|
|
17
17
|
if (internals.resolveWorkflowType !== void 0)
|
|
18
18
|
options.resolveWorkflowType = internals.resolveWorkflowType;
|
|
19
|
+
if (internals.services !== void 0)
|
|
20
|
+
options.services = internals.services;
|
|
19
21
|
}
|
|
20
22
|
function createSpeculativeChildOptions(parent, internals) {
|
|
21
23
|
const options = {
|
|
@@ -178,4 +178,10 @@ export interface ContextOptions {
|
|
|
178
178
|
* it is not persisted or restored after engine restart.
|
|
179
179
|
*/
|
|
180
180
|
registerCancelHandler?: (handler: () => Promise<void> | void) => () => void;
|
|
181
|
+
/**
|
|
182
|
+
* Host-supplied, per-run capabilities exposed as `ctx.services`. Never
|
|
183
|
+
* checkpointed; held only for this run and re-provided on recovery via the
|
|
184
|
+
* engine's `resolveWorkflowServices` resolver.
|
|
185
|
+
*/
|
|
186
|
+
services?: unknown;
|
|
181
187
|
}
|
|
@@ -178,6 +178,7 @@ function buildBaseWorkflowDeleteKeys(state) {
|
|
|
178
178
|
KEYS.checkpoint(state.id),
|
|
179
179
|
KEYS.workflowHeaders(state.id),
|
|
180
180
|
KEYS.terminalCleanupNeeded(state.id),
|
|
181
|
+
KEYS.workflowHasServices(state.id),
|
|
181
182
|
KEYS.attribute(state.id),
|
|
182
183
|
KEYS.terminalWorkflow(state.updatedAt, state.id)
|
|
183
184
|
]);
|
|
@@ -102,7 +102,8 @@ export function createTimeOperationCallbacks(engine) {
|
|
|
102
102
|
parseStartOptionDuration: (value, fieldName) => parseStartOptionDuration(getInternals(engine), value, fieldName, createLifecycleCallbacks(engine)),
|
|
103
103
|
runDeferredTerminalCleanup: (workflowId, timerId) => runDeferredTerminalCleanup(getInternals(engine), workflowId, timerId, createTerminationCallbacks(engine)),
|
|
104
104
|
handleScheduleTimer: (entry) => handleScheduleTimerForEngine(engine, entry),
|
|
105
|
-
timeout: (workflowId) => engine.timeout(workflowId)
|
|
105
|
+
timeout: (workflowId) => engine.timeout(workflowId),
|
|
106
|
+
handleCleanupError: (source, error, workflowId) => createTerminationCallbacks(engine).handleCleanupError(source, error, workflowId)
|
|
106
107
|
};
|
|
107
108
|
}
|
|
108
109
|
export function createUpdateCallbacks(engine) {
|
|
@@ -74,7 +74,8 @@ export function createLifecycleCallbacks(engine) {
|
|
|
74
74
|
hasLocalCheckpointOwnership: (workflowId, workflowStatus) => hasLocalCheckpointOwnership(getInternals(engine), workflowId, workflowStatus),
|
|
75
75
|
handleCleanupError: (source, error, workflowId) => createTerminationCallbacks(engine).handleCleanupError(source, error, workflowId),
|
|
76
76
|
swallowPromiseRejection: (promise) => swallowPromiseRejection(promise),
|
|
77
|
-
enforceHistoryCircuitBreaker: (workflowId) => terminateWorkflow(getInternals(engine), workflowId, "timed-out", createTerminationCallbacks(engine), HISTORY_CIRCUIT_BREAKER_REASON)
|
|
77
|
+
enforceHistoryCircuitBreaker: (workflowId) => terminateWorkflow(getInternals(engine), workflowId, "timed-out", createTerminationCallbacks(engine), HISTORY_CIRCUIT_BREAKER_REASON),
|
|
78
|
+
failWorkflowForUnavailableServices: (workflowId, error) => failWorkflow(getInternals(engine), workflowId, error, createTerminationCallbacks(engine), "system")
|
|
78
79
|
};
|
|
79
80
|
}
|
|
80
81
|
export function createTerminationCallbacksWith(engine, handleScheduledWorkflowTerminal) {
|
|
@@ -54,6 +54,7 @@ export declare function createExecutionStrategyBundle(parameters: {
|
|
|
54
54
|
getComposedWorkflowInterceptor?: () => ComposedWorkflowInterceptor | null;
|
|
55
55
|
resolveWorkflowType: (target: string | Function) => string;
|
|
56
56
|
registerCancelHandler?: (workflowId: string, handler: () => Promise<void> | void) => () => void;
|
|
57
|
+
getWorkflowServices?: (workflowId: string) => unknown;
|
|
57
58
|
}): ExecutionStrategyBundle;
|
|
58
59
|
export declare function createActivityWorkerDispatcher(activityExecution: EngineConstructorOptions['activityExecution']): ActivityWorkerDispatcher | null;
|
|
59
60
|
export declare function createAlertManagerForEngine(engine: EventTarget, alerts: EngineOptions['alerts'] | undefined, getNow: () => number): AlertManager | null;
|
|
@@ -107,6 +107,7 @@ export function resolveEngineOptions(storage, options, getNow) {
|
|
|
107
107
|
return {
|
|
108
108
|
storage,
|
|
109
109
|
getNow,
|
|
110
|
+
resolveWorkflowServices: options?.resolveWorkflowServices ?? null,
|
|
110
111
|
...resolveBooleanDefaults(options),
|
|
111
112
|
...resolveNumericDefaults(options),
|
|
112
113
|
...resolveRetentionFields(options),
|
|
@@ -169,7 +170,8 @@ export function createExecutionStrategyBundle(parameters) {
|
|
|
169
170
|
getRegistration,
|
|
170
171
|
getComposedWorkflowInterceptor,
|
|
171
172
|
resolveWorkflowType,
|
|
172
|
-
registerCancelHandler
|
|
173
|
+
registerCancelHandler,
|
|
174
|
+
getWorkflowServices
|
|
173
175
|
} = parameters, workerExecutionConfiguration = normalizeWorkerExecutionConfiguration(options);
|
|
174
176
|
if (workerExecutionConfiguration.mode === "worker") {
|
|
175
177
|
const pool = new WorkerPool({
|
|
@@ -195,7 +197,8 @@ export function createExecutionStrategyBundle(parameters) {
|
|
|
195
197
|
maxNestingDepth,
|
|
196
198
|
development,
|
|
197
199
|
resolveWorkflowType,
|
|
198
|
-
...registerCancelHandler !== void 0 && { registerCancelHandler }
|
|
200
|
+
...registerCancelHandler !== void 0 && { registerCancelHandler },
|
|
201
|
+
...getWorkflowServices !== void 0 && { getWorkflowServices }
|
|
199
202
|
});
|
|
200
203
|
return { strategy: inlineStrategy, inlineStrategy };
|
|
201
204
|
}
|
|
@@ -30,6 +30,11 @@ export interface ResolvedOptions {
|
|
|
30
30
|
archiveAdapter: ArchiveAdapter | null;
|
|
31
31
|
payloadSizePolicy: NormalizedPayloadSizePolicy;
|
|
32
32
|
getNow: () => number;
|
|
33
|
+
/**
|
|
34
|
+
* Re-provides the non-serialized per-run `services` value on recovery; `null`
|
|
35
|
+
* when the engine was created without `resolveWorkflowServices`.
|
|
36
|
+
*/
|
|
37
|
+
resolveWorkflowServices: EngineOptions['resolveWorkflowServices'] | null;
|
|
33
38
|
}
|
|
34
39
|
export interface WorkflowResultWaiter {
|
|
35
40
|
promise: Promise<unknown>;
|
|
@@ -230,6 +230,32 @@ export declare class Engine<TWorkflows extends object = DefaultWorkflowRegistry,
|
|
|
230
230
|
getStreamChunks(workflowId: string, key: string, options?: {
|
|
231
231
|
after?: number;
|
|
232
232
|
}): Promise<StoredStreamChunk[]>;
|
|
233
|
+
/**
|
|
234
|
+
* Read a value a workflow offloaded with `ctx.offload(key, ...)` back out of
|
|
235
|
+
* storage by `workflowId` + `key`.
|
|
236
|
+
*
|
|
237
|
+
* This is the external, post-completion reader for offloaded artifacts — the
|
|
238
|
+
* missing sibling of {@link getStreamChunks} and {@link getEvents}. Offloaded
|
|
239
|
+
* values survive normal completion (`completeWorkflow`/`failWorkflow` preserve
|
|
240
|
+
* them) so a consumer can read a finished workflow's offloaded output after
|
|
241
|
+
* `handle.result()` resolves. They are swept only when a workflow is
|
|
242
|
+
* terminated, cancelled, or times out.
|
|
243
|
+
*
|
|
244
|
+
* @returns The decoded offload value, or `null` when no value is stored under
|
|
245
|
+
* that key (key was never written, workflow ID unknown, or artifact swept).
|
|
246
|
+
*
|
|
247
|
+
* @example
|
|
248
|
+
* ```ts
|
|
249
|
+
* import { Engine } from '@lostgradient/weft';
|
|
250
|
+
*
|
|
251
|
+
* async function readReport(engine: Engine, workflowId: string): Promise<unknown> {
|
|
252
|
+
* // `null` when the workflow offloaded nothing under this key, or after a
|
|
253
|
+
* // terminated workflow swept its output artifacts.
|
|
254
|
+
* return engine.getOffload(workflowId, 'report');
|
|
255
|
+
* }
|
|
256
|
+
* ```
|
|
257
|
+
*/
|
|
258
|
+
getOffload(workflowId: string, key: string): Promise<unknown>;
|
|
233
259
|
fork(sourceWorkflowId: string, options?: ForkOptions): Promise<WorkflowHandle>;
|
|
234
260
|
resume(workflowId: string): Promise<WorkflowHandle>;
|
|
235
261
|
/**
|
|
@@ -90,6 +90,7 @@ import {
|
|
|
90
90
|
removeTags as removeWorkflowTags,
|
|
91
91
|
setAttributes as setWorkflowAttributes
|
|
92
92
|
} from "./listing.js";
|
|
93
|
+
import { getOffloadFromInternals } from "./operations-data.js";
|
|
93
94
|
import { getStreamChunksFromInternals } from "./operations-stream.js";
|
|
94
95
|
import {
|
|
95
96
|
handleTimerFired as handleTimerFiredFromInternals
|
|
@@ -213,7 +214,8 @@ export class Engine extends EventTarget {
|
|
|
213
214
|
getRegistration: getInternals(this).registrations.get.bind(getInternals(this).registrations),
|
|
214
215
|
getComposedWorkflowInterceptor: () => getComposedWorkflowInterceptor(getInternals(this)),
|
|
215
216
|
resolveWorkflowType: this.#resolveWorkflowTypeTarget.bind(this),
|
|
216
|
-
registerCancelHandler: (workflowId, handler) => registerCancelHandler(getInternals(this), workflowId, handler)
|
|
217
|
+
registerCancelHandler: (workflowId, handler) => registerCancelHandler(getInternals(this), workflowId, handler),
|
|
218
|
+
getWorkflowServices: (workflowId) => getInternals(this).workflowServices.get(workflowId)
|
|
217
219
|
});
|
|
218
220
|
getInternals(this).storage = storage;
|
|
219
221
|
getInternals(this).abortController = new AbortController;
|
|
@@ -261,6 +263,7 @@ export class Engine extends EventTarget {
|
|
|
261
263
|
if (queuedInlineWorkflowStartChannel !== null)
|
|
262
264
|
queuedInlineWorkflowStartChannel.port1.onmessage = createQueuedInlineWorkflowStartHandler(weakEngine, queuedInlineWorkflowStartChannel);
|
|
263
265
|
getInternals(this).heartbeatDetails = new Map;
|
|
266
|
+
getInternals(this).workflowServices = new Map;
|
|
264
267
|
getInternals(this).pendingAsyncActivities = new Map;
|
|
265
268
|
getInternals(this).pendingStarts = new Set;
|
|
266
269
|
getInternals(this).pendingScheduleCreations = new Set;
|
|
@@ -496,6 +499,9 @@ export class Engine extends EventTarget {
|
|
|
496
499
|
async getStreamChunks(workflowId, key, options) {
|
|
497
500
|
return getStreamChunksFromInternals(getInternals(this), workflowId, key, options);
|
|
498
501
|
}
|
|
502
|
+
async getOffload(workflowId, key) {
|
|
503
|
+
return getOffloadFromInternals(getInternals(this), workflowId, key);
|
|
504
|
+
}
|
|
499
505
|
async fork(sourceWorkflowId, options) {
|
|
500
506
|
return forkFromLifecycle(getInternals(this), sourceWorkflowId, options, this.#createLifecycleCallbacks());
|
|
501
507
|
}
|
|
@@ -108,6 +108,14 @@ export interface EngineInternals {
|
|
|
108
108
|
workflowHeaders: Map<string, Map<string, string>>;
|
|
109
109
|
workflowStateWriteChains: Map<string, Promise<void>>;
|
|
110
110
|
heartbeatDetails: Map<string, unknown>;
|
|
111
|
+
/**
|
|
112
|
+
* Per-run, non-serialized `services` value exposed to the workflow body as
|
|
113
|
+
* `ctx.services`. Set at `engine.start({ services })` and re-provided on
|
|
114
|
+
* recovery by `resolveWorkflowServices`. Never checkpointed — held only in
|
|
115
|
+
* engine memory, keyed by workflowId, and cleared on terminal cleanup (the
|
|
116
|
+
* same lifecycle as {@link heartbeatDetails}).
|
|
117
|
+
*/
|
|
118
|
+
workflowServices: Map<string, unknown>;
|
|
111
119
|
/**
|
|
112
120
|
* Activities that called `ctx.completeAsync()` and are awaiting out-of-band
|
|
113
121
|
* completion via `engine.completeAsyncActivity` / `failAsyncActivity`, keyed
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { WorkflowState } from '../../types.ts';
|
|
2
|
+
import type { EngineInternals } from '../internals.ts';
|
|
3
|
+
/**
|
|
4
|
+
* Re-provide a recovered inline workflow's non-serialized `services` before its
|
|
5
|
+
* generator is driven forward, and decide whether execution should proceed.
|
|
6
|
+
*
|
|
7
|
+
* Both recovery entry points use this: `resumeWorkflowFromStorage` (for a run
|
|
8
|
+
* left `running`) and the delayed-start timer handler (for a `startAfter`/
|
|
9
|
+
* `startAt` run that crashed `pending` before its timer fired). On a fresh
|
|
10
|
+
* process the in-memory `workflowServices` map is empty, so without this a
|
|
11
|
+
* recovered run that originally had services would silently execute with
|
|
12
|
+
* `ctx.services === undefined`.
|
|
13
|
+
*
|
|
14
|
+
* The resolver is only consulted for runs launched WITH services, detected by
|
|
15
|
+
* the durable `KEYS.workflowHasServices` marker. A run that never had services
|
|
16
|
+
* has no marker and proceeds without touching the resolver — so a fail-closed
|
|
17
|
+
* resolver does not fail healthy no-services runs.
|
|
18
|
+
*
|
|
19
|
+
* Returns `false` to proceed (services available, none expected, or no resolver
|
|
20
|
+
* is configured). Returns `true` to STOP — the run was terminally
|
|
21
|
+
* failed (services unavailable), or the terminal commit faulted and the run was
|
|
22
|
+
* left for a later boot to retry. Either way the generator must not advance:
|
|
23
|
+
* driving it without services would crash the body and that throw would escape
|
|
24
|
+
* into the recovery loop, aborting sibling runs.
|
|
25
|
+
*
|
|
26
|
+
* Worker mode skips this (services are inline-only, rejected at `engine.start()`).
|
|
27
|
+
* A resolver throw is treated as `unavailable` with the error as the reason,
|
|
28
|
+
* for the same sibling-isolation reason.
|
|
29
|
+
*
|
|
30
|
+
* @param failRun - Terminally fails just this run with `reason`. Supplied by the
|
|
31
|
+
* caller because the two entry points reach the termination machinery through
|
|
32
|
+
* different callback bundles. It receives the canonical terminal error built
|
|
33
|
+
* by {@link unavailableServicesError}, so both recovery paths fail the run
|
|
34
|
+
* with an identical message and (via `failWorkflow`'s default) the `system`
|
|
35
|
+
* failure category.
|
|
36
|
+
* @param onCommitError - Records a fail-warn when `failRun` itself throws, so the
|
|
37
|
+
* swallowed terminal-commit fault is still observable.
|
|
38
|
+
*/
|
|
39
|
+
export declare function reprovideRecoveredServices(internals: EngineInternals, state: WorkflowState, failRun: (workflowId: string, error: Error) => Promise<void>, onCommitError: (source: string, error: unknown, workflowId: string) => void): Promise<boolean>;
|
|
40
|
+
/**
|
|
41
|
+
* The canonical terminal error for a recovered run whose services could not be
|
|
42
|
+
* re-provided. Shared by both recovery paths so they fail with an identical
|
|
43
|
+
* message (the failure category is `system`, the default for `failWorkflow`).
|
|
44
|
+
*/
|
|
45
|
+
export declare function unavailableServicesError(workflowId: string, reason: string): Error;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { KEYS } from "../../../storage/interface.js";
|
|
2
|
+
export async function reprovideRecoveredServices(internals, state, failRun, onCommitError) {
|
|
3
|
+
const resolver = internals.options.resolveWorkflowServices;
|
|
4
|
+
if (internals.inlineStrategy === null || !resolver)
|
|
5
|
+
return !1;
|
|
6
|
+
if (internals.workflowServices.has(state.id))
|
|
7
|
+
return !1;
|
|
8
|
+
if (await internals.storage.get(KEYS.workflowHasServices(state.id)) === null)
|
|
9
|
+
return !1;
|
|
10
|
+
let reason;
|
|
11
|
+
try {
|
|
12
|
+
const resolution = await resolver({
|
|
13
|
+
workflowId: state.id,
|
|
14
|
+
workflowType: state.type,
|
|
15
|
+
input: state.input
|
|
16
|
+
});
|
|
17
|
+
if (resolution.status === "available") {
|
|
18
|
+
internals.workflowServices.set(state.id, resolution.services);
|
|
19
|
+
return !1;
|
|
20
|
+
}
|
|
21
|
+
reason = resolution.reason;
|
|
22
|
+
} catch (error) {
|
|
23
|
+
reason = error instanceof Error ? error.message : String(error);
|
|
24
|
+
}
|
|
25
|
+
try {
|
|
26
|
+
await failRun(state.id, unavailableServicesError(state.id, reason));
|
|
27
|
+
} catch (error) {
|
|
28
|
+
onCommitError("reprovideRecoveredServices", error, state.id);
|
|
29
|
+
}
|
|
30
|
+
return !0;
|
|
31
|
+
}
|
|
32
|
+
export function unavailableServicesError(workflowId, reason) {
|
|
33
|
+
return Error(`Recovered workflow "${workflowId}" services unavailable: ${reason}`);
|
|
34
|
+
}
|
|
@@ -10,12 +10,16 @@ import { loadWorkflowState } from "../storage-io.js";
|
|
|
10
10
|
import { getComposedWorkflowInterceptor } from "../strategy-helpers.js";
|
|
11
11
|
import { decodeWorkflowState } from "../validation.js";
|
|
12
12
|
import { prepareResumeState } from "./persist.js";
|
|
13
|
+
import { reprovideRecoveredServices } from "./recovered-services.js";
|
|
13
14
|
import {
|
|
14
15
|
enforceHistoryPolicyBeforeReplay,
|
|
15
16
|
loadTerminalCleanupTrackedState,
|
|
16
17
|
loadWorkflowStartHeaders,
|
|
17
18
|
setWorkflowStartHeaders
|
|
18
19
|
} from "./shared.js";
|
|
20
|
+
async function prepareRecoveredServicesOrFail(internals, state, callbacks) {
|
|
21
|
+
return reprovideRecoveredServices(internals, state, callbacks.failWorkflowForUnavailableServices, callbacks.handleCleanupError);
|
|
22
|
+
}
|
|
19
23
|
function assertResumeNotTerminating(internals, workflowId) {
|
|
20
24
|
if (internals.terminalizingWorkflows.has(workflowId))
|
|
21
25
|
throw Error(`Cannot resume workflow "${workflowId}": termination is in progress`);
|
|
@@ -57,7 +61,8 @@ function relaunchInlineWorkflowAfterResume(internals, latestState, args) {
|
|
|
57
61
|
sleepReferenceTime: resumeCheckpoint.createdAt,
|
|
58
62
|
...latestState.executionDeadline !== void 0 && {
|
|
59
63
|
deadline: latestState.executionDeadline
|
|
60
|
-
}
|
|
64
|
+
},
|
|
65
|
+
services: internals.workflowServices.get(workflowId)
|
|
61
66
|
});
|
|
62
67
|
setContextWorkflowInterceptor(context, getComposedWorkflowInterceptor(internals));
|
|
63
68
|
if (internals.options.development)
|
|
@@ -117,6 +122,8 @@ export async function resumeWorkflowFromStorage(internals, workflowId, dispatchR
|
|
|
117
122
|
return callbacks.getHandle(workflowId);
|
|
118
123
|
const workflowStartHeaders = await loadWorkflowStartHeaders(internals, workflowId, callbacks);
|
|
119
124
|
await loadTerminalCleanupTrackedState(internals, workflowId, callbacks);
|
|
125
|
+
if (await prepareRecoveredServicesOrFail(internals, state, callbacks))
|
|
126
|
+
return callbacks.getHandle(workflowId);
|
|
120
127
|
const handle = callbacks.getHandle(workflowId);
|
|
121
128
|
await callbacks.runSerializedWorkflowStateWrite(workflowId, () => performSerializedResume(internals, {
|
|
122
129
|
workflowId,
|
|
@@ -48,6 +48,14 @@ export type LifecycleCallbacks = {
|
|
|
48
48
|
* history is terminated without being replayed.
|
|
49
49
|
*/
|
|
50
50
|
enforceHistoryCircuitBreaker: (workflowId: string) => Promise<void>;
|
|
51
|
+
/**
|
|
52
|
+
* Force a recovered workflow to a terminal `failed` state because its
|
|
53
|
+
* non-serialized `services` could not be re-provided (the engine's
|
|
54
|
+
* `resolveWorkflowServices` reported `unavailable`). Fails just this run with
|
|
55
|
+
* a `system` failure category; the engine and other recovered runs continue.
|
|
56
|
+
* `error` is the canonical {@link unavailableServicesError}.
|
|
57
|
+
*/
|
|
58
|
+
failWorkflowForUnavailableServices: (workflowId: string, error: Error) => Promise<void>;
|
|
51
59
|
};
|
|
52
60
|
/**
|
|
53
61
|
* Pre-replay history circuit breaker. Called at every restore-from-checkpoint
|
|
@@ -19,18 +19,7 @@ export function buildStartBatchOperations(_internals, workflowId, state, checkpo
|
|
|
19
19
|
...visibilityIndexOperations,
|
|
20
20
|
...buildWorkflowTagIndexOperations(workflowId, void 0, state.tags),
|
|
21
21
|
...buildInitialSearchAttributeOperations(_internals, workflowId, registration, options?.searchAttributes, callbacks),
|
|
22
|
-
...
|
|
23
|
-
{
|
|
24
|
-
type: "put",
|
|
25
|
-
key: KEYS.workflowHeaders(workflowId),
|
|
26
|
-
value: encodeWorkflowStartHeaders(workflowStartHeaders)
|
|
27
|
-
},
|
|
28
|
-
{
|
|
29
|
-
type: "put",
|
|
30
|
-
key: KEYS.terminalCleanupNeeded(workflowId),
|
|
31
|
-
value: EMPTY_STORAGE_VALUE
|
|
32
|
-
}
|
|
33
|
-
] : [],
|
|
22
|
+
...buildPerRunScratchOperations(workflowId, options, workflowStartHeaders),
|
|
34
23
|
...additionalOperations ?? []
|
|
35
24
|
];
|
|
36
25
|
if (executionDeadline !== void 0)
|
|
@@ -44,6 +33,28 @@ export function buildStartBatchOperations(_internals, workflowId, state, checkpo
|
|
|
44
33
|
operations.push(...buildTimerBatchOperations(delayedStartTimer));
|
|
45
34
|
return operations;
|
|
46
35
|
}
|
|
36
|
+
function buildPerRunScratchOperations(workflowId, options, workflowStartHeaders) {
|
|
37
|
+
const hasHeaders = workflowStartHeaders !== void 0 && workflowStartHeaders.size > 0, hasServices = options?.services !== void 0, operations = [];
|
|
38
|
+
if (hasHeaders)
|
|
39
|
+
operations.push({
|
|
40
|
+
type: "put",
|
|
41
|
+
key: KEYS.workflowHeaders(workflowId),
|
|
42
|
+
value: encodeWorkflowStartHeaders(workflowStartHeaders)
|
|
43
|
+
});
|
|
44
|
+
if (hasServices)
|
|
45
|
+
operations.push({
|
|
46
|
+
type: "put",
|
|
47
|
+
key: KEYS.workflowHasServices(workflowId),
|
|
48
|
+
value: EMPTY_STORAGE_VALUE
|
|
49
|
+
});
|
|
50
|
+
if (hasHeaders || hasServices)
|
|
51
|
+
operations.push({
|
|
52
|
+
type: "put",
|
|
53
|
+
key: KEYS.terminalCleanupNeeded(workflowId),
|
|
54
|
+
value: EMPTY_STORAGE_VALUE
|
|
55
|
+
});
|
|
56
|
+
return operations;
|
|
57
|
+
}
|
|
47
58
|
export function buildInitialSearchAttributeOperations(_internals, workflowId, registration, searchAttributes, callbacks) {
|
|
48
59
|
if (!searchAttributes || Object.keys(searchAttributes).length === 0)
|
|
49
60
|
return [];
|
|
@@ -51,11 +51,18 @@ function rollbackTransientStartState(internals, workflowId) {
|
|
|
51
51
|
internals.checkpoints.delete(workflowId);
|
|
52
52
|
internals.workflowHeaders.delete(workflowId);
|
|
53
53
|
internals.workflowVersionTuples.delete(workflowId);
|
|
54
|
+
internals.workflowServices.delete(workflowId);
|
|
55
|
+
internals.workflowsNeedingTerminalCleanup.delete(workflowId);
|
|
56
|
+
}
|
|
57
|
+
function assertServicesSupportedForMode(internals, options) {
|
|
58
|
+
if (options?.services !== void 0 && internals.inlineStrategy === null)
|
|
59
|
+
throw Error('options.services is only supported in inline execution mode; it cannot be serialized to a Worker. Remove services or use workflowExecutionMode: "inline".');
|
|
54
60
|
}
|
|
55
61
|
export async function startWorkflow(internals, type, input, options, additionalStartOperations, callbacks) {
|
|
56
62
|
const registration = internals.registrations.get(type);
|
|
57
63
|
if (!registration)
|
|
58
64
|
throw new WorkflowNotRegisteredError(type);
|
|
65
|
+
assertServicesSupportedForMode(internals, options);
|
|
59
66
|
const preparation = prepareStartWorkflow(internals, options, callbacks), { workflowId, callerProvidedId, parentHeaders, executionStateOwnerId, delayedStartTimer } = preparation;
|
|
60
67
|
if (internals.pendingStarts.has(workflowId))
|
|
61
68
|
throw new WorkflowAlreadyExistsError(workflowId);
|
|
@@ -73,6 +80,10 @@ export async function startWorkflow(internals, type, input, options, additionalS
|
|
|
73
80
|
internals.workflowVersionTuples.set(workflowId, versionTuple);
|
|
74
81
|
const startOperations = buildStartBatchOperations(internals, workflowId, state, checkpoint, registration, options, state.executionDeadline, delayedStartTimer, persistedWorkflowStartHeaders, additionalStartOperations, callbacks);
|
|
75
82
|
await persistStartBatch(internals, startOperations);
|
|
83
|
+
if (options?.services !== void 0) {
|
|
84
|
+
internals.workflowServices.set(workflowId, options.services);
|
|
85
|
+
internals.workflowsNeedingTerminalCleanup.add(workflowId);
|
|
86
|
+
}
|
|
76
87
|
const handle = createWorkflowHandle(internals, workflowId, callbacks);
|
|
77
88
|
if (!delayedStartTimer)
|
|
78
89
|
beginWorkflowExecution(internals, workflowId, type, input, checkpoint, state.executionDeadline, state.executionStateOwnerId ?? workflowId, registration, callbacks);
|
|
@@ -19,5 +19,21 @@ export type DataOperationCallbacks = {
|
|
|
19
19
|
export declare function processMemoOperation(_internals: EngineInternals, workflowId: string, operation: MemoOperation, callbacks: DataOperationCallbacks): Promise<void>;
|
|
20
20
|
export declare function processOffloadOperation(internals: EngineInternals, workflowId: string, operation: OffloadOperation, callbacks: DataOperationCallbacks): Promise<void>;
|
|
21
21
|
export declare function processLoadOperation(internals: EngineInternals, workflowId: string, operation: LoadOperation, callbacks: DataOperationCallbacks): Promise<void>;
|
|
22
|
+
/**
|
|
23
|
+
* Read an offloaded value back out of storage by `workflowId` + `key`, decoding
|
|
24
|
+
* it with the same codec {@link processOffloadOperation} wrote it with.
|
|
25
|
+
*
|
|
26
|
+
* This is the post-completion sibling of the in-workflow `ctx.load()` read
|
|
27
|
+
* (see {@link processLoadOperation}): `ctx.load()` is restricted to the running
|
|
28
|
+
* workflow's own offloads and throws on a miss, whereas this external reader
|
|
29
|
+
* lets a consumer read a *terminal* workflow's offloaded output after
|
|
30
|
+
* `handle.result()` resolves — the artifact survives normal completion
|
|
31
|
+
* (`completeWorkflow`/`failWorkflow` preserve `offload:` keys) and is swept only
|
|
32
|
+
* when the workflow is terminated/cancelled.
|
|
33
|
+
*
|
|
34
|
+
* @returns The decoded offload value, or `null` when no value is stored under
|
|
35
|
+
* that key (either the key was never written, or the artifact was swept).
|
|
36
|
+
*/
|
|
37
|
+
export declare function getOffloadFromInternals(internals: EngineInternals, workflowId: string, key: string): Promise<unknown>;
|
|
22
38
|
export declare function processArchiveOperation(internals: EngineInternals, workflowId: string, operation: ArchiveOperation, callbacks: DataOperationCallbacks): Promise<void>;
|
|
23
39
|
export {};
|
|
@@ -30,6 +30,12 @@ export async function processLoadOperation(internals, workflowId, operation, cal
|
|
|
30
30
|
return decode(raw);
|
|
31
31
|
});
|
|
32
32
|
}
|
|
33
|
+
export async function getOffloadFromInternals(internals, workflowId, key) {
|
|
34
|
+
const raw = await internals.storage.get(KEYS.offload(workflowId, key));
|
|
35
|
+
if (raw === null)
|
|
36
|
+
return null;
|
|
37
|
+
return decode(raw);
|
|
38
|
+
}
|
|
33
39
|
export async function processArchiveOperation(internals, workflowId, operation, callbacks) {
|
|
34
40
|
return callbacks.runOperationWithResult(workflowId, operation, async () => {
|
|
35
41
|
await internals.storage.put(KEYS.archive(workflowId, operation.key), encode(operation.data));
|
|
@@ -19,10 +19,11 @@ export type TimeOperationCallbacks = {
|
|
|
19
19
|
runDeferredTerminalCleanup: (workflowId: string, timerId: string) => Promise<void>;
|
|
20
20
|
handleScheduleTimer: (entry: TimerEntry) => Promise<void>;
|
|
21
21
|
timeout: (workflowId: string) => Promise<void>;
|
|
22
|
+
handleCleanupError: (source: string, error: unknown, workflowId: string) => void;
|
|
22
23
|
};
|
|
23
24
|
export declare function createDelayedStartTimerEntry(_internals: EngineInternals, workflowId: string, scheduledStartAt: number, options: StartOptions | undefined, callbacks: Pick<TimeOperationCallbacks, 'parseStartOptionDuration'>): TimerEntry;
|
|
24
25
|
export declare function processSleepOperation(internals: EngineInternals, workflowId: string, operation: SleepOperation, callbacks: Pick<TimeOperationCallbacks, 'completeOperation' | 'loadWorkflowState'>): Promise<void>;
|
|
25
26
|
export declare function registerSleepResolver(internals: EngineInternals, workflowId: string, operationId: string, resolve: () => void): void;
|
|
26
|
-
export declare function startDelayedWorkflow(internals: EngineInternals, entry: TimerEntry, callbacks: Pick<TimeOperationCallbacks, 'beginWorkflowExecution' | 'failWorkflow' | 'loadWorkflowStartHeaders' | 'loadWorkflowState' | 'runSerializedWorkflowStateWrite' | 'setWorkflowStartHeaders' | 'workflowVersionTupleFromState'>): Promise<void>;
|
|
27
|
-
export declare function handleTimerFired(internals: EngineInternals, entry: TimerEntry, callbacks: Pick<TimeOperationCallbacks, 'failWorkflow' | 'loadWorkflowStartHeaders' | 'loadWorkflowState' | 'runDeferredTerminalCleanup' | 'runSerializedWorkflowStateWrite' | 'handleScheduleTimer' | 'setWorkflowStartHeaders' | 'timeout' | 'beginWorkflowExecution' | 'workflowVersionTupleFromState'>): Promise<void>;
|
|
27
|
+
export declare function startDelayedWorkflow(internals: EngineInternals, entry: TimerEntry, callbacks: Pick<TimeOperationCallbacks, 'beginWorkflowExecution' | 'failWorkflow' | 'handleCleanupError' | 'loadWorkflowStartHeaders' | 'loadWorkflowState' | 'runSerializedWorkflowStateWrite' | 'setWorkflowStartHeaders' | 'workflowVersionTupleFromState'>): Promise<void>;
|
|
28
|
+
export declare function handleTimerFired(internals: EngineInternals, entry: TimerEntry, callbacks: Pick<TimeOperationCallbacks, 'failWorkflow' | 'handleCleanupError' | 'loadWorkflowStartHeaders' | 'loadWorkflowState' | 'runDeferredTerminalCleanup' | 'runSerializedWorkflowStateWrite' | 'handleScheduleTimer' | 'setWorkflowStartHeaders' | 'timeout' | 'beginWorkflowExecution' | 'workflowVersionTupleFromState'>): Promise<void>;
|
|
28
29
|
export {};
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { KEYS } from "../../storage/interface.js";
|
|
1
|
+
import { KEYS, storageHas } from "../../storage/interface.js";
|
|
2
2
|
import { deserializeCheckpoint } from "../checkpoint.js";
|
|
3
3
|
import { encode } from "../codec.js";
|
|
4
4
|
import { buildTimerBatchOperations, normalizeStorageTimestamp } from "../scheduler.js";
|
|
5
|
+
import { reprovideRecoveredServices } from "./lifecycle/recovered-services.js";
|
|
5
6
|
import { buildWorkflowVisibilityIndexTransition } from "./workflow-indexes.js";
|
|
6
7
|
export function createDelayedStartTimerEntry(_internals, workflowId, scheduledStartAt, options, callbacks) {
|
|
7
8
|
return {
|
|
@@ -85,6 +86,10 @@ export async function startDelayedWorkflow(internals, entry, callbacks) {
|
|
|
85
86
|
});
|
|
86
87
|
if (!runningState)
|
|
87
88
|
return;
|
|
89
|
+
if (await reprovideRecoveredServices(internals, runningState, (workflowId, error) => callbacks.failWorkflow(workflowId, error), callbacks.handleCleanupError))
|
|
90
|
+
return;
|
|
91
|
+
if (await storageHas(internals.storage, KEYS.terminalCleanupNeeded(entry.workflowId)))
|
|
92
|
+
internals.workflowsNeedingTerminalCleanup.add(entry.workflowId);
|
|
88
93
|
internals.checkpoints.set(entry.workflowId, checkpoint);
|
|
89
94
|
internals.workflowVersionTuples.set(entry.workflowId, callbacks.workflowVersionTupleFromState(runningState));
|
|
90
95
|
callbacks.setWorkflowStartHeaders(entry.workflowId, await callbacks.loadWorkflowStartHeaders(entry.workflowId));
|
|
@@ -71,6 +71,7 @@ export function cleanupWaiters(internals, workflowId, callbacks) {
|
|
|
71
71
|
cleanupReviewEscalations(internals, workflowId, callbacks);
|
|
72
72
|
internals.workflowNestingDepths.delete(workflowId);
|
|
73
73
|
internals.workflowHeaders.delete(workflowId);
|
|
74
|
+
internals.workflowServices.delete(workflowId);
|
|
74
75
|
internals.workflowTypeByWorkflowId.delete(workflowId);
|
|
75
76
|
}
|
|
76
77
|
export async function cleanupWorkflowStorage(internals, workflowId, includeOutputArtifacts) {
|
|
@@ -86,6 +87,7 @@ export async function cleanupWorkflowStorage(internals, workflowId, includeOutpu
|
|
|
86
87
|
prefixes.push(`offload:${encodedWorkflowId}:`, `blob:${encodedWorkflowId}:`);
|
|
87
88
|
await internals.storage.delete(KEYS.workflowHeaders(workflowId));
|
|
88
89
|
await internals.storage.delete(KEYS.signalSequence(workflowId));
|
|
90
|
+
await internals.storage.delete(KEYS.workflowHasServices(workflowId));
|
|
89
91
|
if (internals.storage.deletePrefix) {
|
|
90
92
|
for (const prefix of prefixes)
|
|
91
93
|
await internals.storage.deletePrefix(prefix);
|
|
@@ -25,6 +25,11 @@ export interface InlineExecutionDependencies {
|
|
|
25
25
|
development?: boolean;
|
|
26
26
|
getComposedWorkflowInterceptor?: () => ComposedWorkflowInterceptor | null;
|
|
27
27
|
registerCancelHandler?: (workflowId: string, handler: () => Promise<void> | void) => () => void;
|
|
28
|
+
/**
|
|
29
|
+
* Look up the non-serialized per-run `services` value for a workflow, exposed
|
|
30
|
+
* to the body as `ctx.services`. Returns `undefined` when none was supplied.
|
|
31
|
+
*/
|
|
32
|
+
getWorkflowServices?: (workflowId: string) => unknown;
|
|
28
33
|
}
|
|
29
34
|
type InlineStartWorkflowParameters = {
|
|
30
35
|
workflowId: string;
|
|
@@ -25,7 +25,8 @@ function createInlineContextOptions(dependencies, registration, parameters, work
|
|
|
25
25
|
...parameters.deadline !== void 0 && { deadline: parameters.deadline },
|
|
26
26
|
...registerCancelHandler !== void 0 && {
|
|
27
27
|
registerCancelHandler: (handler) => registerCancelHandler(parameters.workflowId, handler)
|
|
28
|
-
}
|
|
28
|
+
},
|
|
29
|
+
services: dependencies.getWorkflowServices?.(parameters.workflowId)
|
|
29
30
|
};
|
|
30
31
|
}
|
|
31
32
|
|