@mokronos/wfkit 0.1.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/LICENSE +21 -0
- package/README.md +57 -0
- package/dist/authoring.d.ts +7 -0
- package/dist/authoring.d.ts.map +1 -0
- package/dist/authoring.js +8 -0
- package/dist/authoring.js.map +1 -0
- package/dist/cli/integrations.d.ts +3 -0
- package/dist/cli/integrations.d.ts.map +1 -0
- package/dist/cli/integrations.js +40 -0
- package/dist/cli/integrations.js.map +1 -0
- package/dist/cli/main.d.ts +3 -0
- package/dist/cli/main.d.ts.map +1 -0
- package/dist/cli/main.js +591 -0
- package/dist/cli/main.js.map +1 -0
- package/dist/core.d.ts +198 -0
- package/dist/core.d.ts.map +1 -0
- package/dist/core.js +1061 -0
- package/dist/core.js.map +1 -0
- package/dist/errors.d.ts +3 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +13 -0
- package/dist/errors.js.map +1 -0
- package/dist/events.d.ts +154 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +24 -0
- package/dist/events.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/replay.d.ts +3 -0
- package/dist/replay.d.ts.map +1 -0
- package/dist/replay.js +28 -0
- package/dist/replay.js.map +1 -0
- package/dist/runtime.d.ts +70 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +183 -0
- package/dist/runtime.js.map +1 -0
- package/dist/schema.d.ts +18 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +19 -0
- package/dist/schema.js.map +1 -0
- package/dist/schemas.d.ts +1198 -0
- package/dist/schemas.d.ts.map +1 -0
- package/dist/schemas.js +383 -0
- package/dist/schemas.js.map +1 -0
- package/dist/sdk/artifact.d.ts +41 -0
- package/dist/sdk/artifact.d.ts.map +1 -0
- package/dist/sdk/artifact.js +94 -0
- package/dist/sdk/artifact.js.map +1 -0
- package/dist/sdk/graph.d.ts +15 -0
- package/dist/sdk/graph.d.ts.map +1 -0
- package/dist/sdk/graph.js +388 -0
- package/dist/sdk/graph.js.map +1 -0
- package/dist/sdk/index.d.ts +14 -0
- package/dist/sdk/index.d.ts.map +1 -0
- package/dist/sdk/index.js +8 -0
- package/dist/sdk/index.js.map +1 -0
- package/dist/sdk/integrations.d.ts +18 -0
- package/dist/sdk/integrations.d.ts.map +1 -0
- package/dist/sdk/integrations.js +20 -0
- package/dist/sdk/integrations.js.map +1 -0
- package/dist/sdk/json.d.ts +4 -0
- package/dist/sdk/json.d.ts.map +1 -0
- package/dist/sdk/json.js +31 -0
- package/dist/sdk/json.js.map +1 -0
- package/dist/sdk/loader.d.ts +10 -0
- package/dist/sdk/loader.d.ts.map +1 -0
- package/dist/sdk/loader.js +75 -0
- package/dist/sdk/loader.js.map +1 -0
- package/dist/sdk/sdk.d.ts +102 -0
- package/dist/sdk/sdk.d.ts.map +1 -0
- package/dist/sdk/sdk.js +669 -0
- package/dist/sdk/sdk.js.map +1 -0
- package/dist/sdk/sqlite.d.ts +8 -0
- package/dist/sdk/sqlite.d.ts.map +1 -0
- package/dist/sdk/sqlite.js +288 -0
- package/dist/sdk/sqlite.js.map +1 -0
- package/dist/signal.d.ts +17 -0
- package/dist/signal.d.ts.map +1 -0
- package/dist/signal.js +78 -0
- package/dist/signal.js.map +1 -0
- package/dist/testing/index.d.ts +35 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +244 -0
- package/dist/testing/index.js.map +1 -0
- package/package.json +67 -0
- package/src/authoring.ts +32 -0
- package/src/cli/integrations.ts +75 -0
- package/src/cli/main.ts +793 -0
- package/src/core.ts +1515 -0
- package/src/errors.ts +19 -0
- package/src/events.ts +37 -0
- package/src/index.ts +109 -0
- package/src/replay.ts +29 -0
- package/src/runtime.ts +314 -0
- package/src/schema.ts +34 -0
- package/src/schemas.ts +473 -0
- package/src/sdk/artifact.ts +176 -0
- package/src/sdk/graph.ts +486 -0
- package/src/sdk/index.ts +64 -0
- package/src/sdk/integrations.ts +49 -0
- package/src/sdk/json.ts +40 -0
- package/src/sdk/loader.ts +99 -0
- package/src/sdk/sdk.ts +911 -0
- package/src/sdk/sqlite.ts +405 -0
- package/src/signal.ts +116 -0
- package/src/testing/index.ts +341 -0
package/dist/core.js
ADDED
|
@@ -0,0 +1,1061 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { Activity, DurableClock, DurableDeferred, Workflow, WorkflowEngine } from "effect/unstable/workflow";
|
|
3
|
+
import { Cause, Context, Effect, Exit, Option, Schedule, Schema } from "effect";
|
|
4
|
+
import { currentWorkflowEventSink, emitWorkflowEvent } from "./events.js";
|
|
5
|
+
import { ExecutionId, jsonSchemaOf } from "./schemas.js";
|
|
6
|
+
import { awaitSignal, registerSignalSchema, SignalDeliveryError, takeBufferedSignal } from "./signal.js";
|
|
7
|
+
const TerminalFailureTypeId = Symbol.for("wf/TerminalFailure");
|
|
8
|
+
export const defineStep = (config) => ({
|
|
9
|
+
...config,
|
|
10
|
+
errors: config.errors ?? Schema.Never
|
|
11
|
+
});
|
|
12
|
+
const SecretRefPrefix = "secret:";
|
|
13
|
+
export const SecretRef = Schema.declare((value) => typeof value === "string" && value.startsWith(SecretRefPrefix)).pipe(Schema.brand("SecretRef"));
|
|
14
|
+
export const secret = (name) => SecretRef.make(`${SecretRefPrefix}${name}`);
|
|
15
|
+
const defaultSecretEnvName = (name) => name.replace(/[^a-zA-Z0-9]+/g, "_").replace(/^_+|_+$/g, "").toUpperCase();
|
|
16
|
+
export const envSecretResolver = (options = {}) => ({
|
|
17
|
+
resolve: (name) => {
|
|
18
|
+
const envName = options.mapping?.[name] ?? defaultSecretEnvName(name);
|
|
19
|
+
const value = process.env[envName];
|
|
20
|
+
if (value !== undefined) {
|
|
21
|
+
return value;
|
|
22
|
+
}
|
|
23
|
+
if (options.fallback !== undefined) {
|
|
24
|
+
return options.fallback;
|
|
25
|
+
}
|
|
26
|
+
throw new Error(`Secret "${name}" not found: set env var ${envName}`);
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
export const isSecretRef = (value) => Schema.is(SecretRef)(value);
|
|
30
|
+
const secretName = (value) => value.slice(SecretRefPrefix.length);
|
|
31
|
+
export const currentSecretResolver = Context.Reference("wf/currentSecretResolver", { defaultValue: () => undefined });
|
|
32
|
+
// Durable executions run on engine entity fibers that don't inherit the
|
|
33
|
+
// caller's context reference, so the runtime also registers resolvers per execution.
|
|
34
|
+
const executionSecretResolvers = new Map();
|
|
35
|
+
export const setExecutionSecretResolver = (executionId, resolver) => {
|
|
36
|
+
executionSecretResolvers.set(executionId, resolver);
|
|
37
|
+
};
|
|
38
|
+
export const removeExecutionSecretResolver = (executionId) => {
|
|
39
|
+
executionSecretResolvers.delete(executionId);
|
|
40
|
+
};
|
|
41
|
+
export const getExecutionSecretResolver = (executionId) => executionSecretResolvers.get(executionId);
|
|
42
|
+
export class Cancelled extends Error {
|
|
43
|
+
_tag = "Cancelled";
|
|
44
|
+
_wfSkipCompensation;
|
|
45
|
+
constructor(options) {
|
|
46
|
+
super("Workflow execution cancelled");
|
|
47
|
+
this.name = "Cancelled";
|
|
48
|
+
if (!options.compensate) {
|
|
49
|
+
this._wfSkipCompensation = true;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/** Reserved per-execution deferred the durable client completes to request
|
|
54
|
+
* cancellation. Every suspension point races against it. */
|
|
55
|
+
export const cancellationDeferredName = "wf:cancel";
|
|
56
|
+
const CancellationRequestSchema = Schema.Struct({
|
|
57
|
+
compensate: Schema.Boolean,
|
|
58
|
+
actor: Schema.optional(Schema.String)
|
|
59
|
+
});
|
|
60
|
+
export class NonDeterminismError extends Error {
|
|
61
|
+
_tag = "NonDeterminismError";
|
|
62
|
+
expected;
|
|
63
|
+
actual;
|
|
64
|
+
constructor(options) {
|
|
65
|
+
super(`Non-deterministic workflow replay: expected ${formatCall(options.expected)} but saw ${formatCall(options.actual)}`);
|
|
66
|
+
this.name = "NonDeterminismError";
|
|
67
|
+
this.expected = options.expected;
|
|
68
|
+
this.actual = options.actual;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
export const DefinedWorkflowTypeId = Symbol.for("wf/DefinedWorkflow");
|
|
72
|
+
class AsyncFailure extends Error {
|
|
73
|
+
_tag = "AsyncFailure";
|
|
74
|
+
error;
|
|
75
|
+
constructor(error) {
|
|
76
|
+
super(error instanceof Error ? error.message : "Async operation failed");
|
|
77
|
+
this.name = "AsyncFailure";
|
|
78
|
+
this.error = error;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
const OrchestrationCallSchema = Schema.Struct({
|
|
82
|
+
kind: Schema.Union([
|
|
83
|
+
Schema.Literal("step"),
|
|
84
|
+
Schema.Literal("sleep"),
|
|
85
|
+
Schema.Literal("signal"),
|
|
86
|
+
Schema.Literal("now"),
|
|
87
|
+
Schema.Literal("random"),
|
|
88
|
+
Schema.Literal("code"),
|
|
89
|
+
Schema.Literal("all")
|
|
90
|
+
]),
|
|
91
|
+
name: Schema.String,
|
|
92
|
+
counter: Schema.Number,
|
|
93
|
+
branches: Schema.optional(Schema.Number)
|
|
94
|
+
});
|
|
95
|
+
export const createInMemoryDeterminismState = () => ({
|
|
96
|
+
calls: [],
|
|
97
|
+
blocks: [],
|
|
98
|
+
values: new Map()
|
|
99
|
+
});
|
|
100
|
+
const formatCall = (call) => `${call.kind}:${call.name}#${call.counter}${call.branches === undefined ? "" : ` branches=${call.branches}`}`;
|
|
101
|
+
const callsEqual = (left, right) => left.kind === right.kind &&
|
|
102
|
+
left.name === right.name &&
|
|
103
|
+
left.counter === right.counter &&
|
|
104
|
+
left.branches === right.branches;
|
|
105
|
+
const verifyCall = (expected, actual) => {
|
|
106
|
+
if (!callsEqual(expected, actual)) {
|
|
107
|
+
throw new NonDeterminismError({ expected, actual });
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
const valueKey = (call) => formatCall(call);
|
|
111
|
+
const skipsCompensation = (error) => typeof error === "object" &&
|
|
112
|
+
error !== null &&
|
|
113
|
+
error._wfSkipCompensation === true;
|
|
114
|
+
const isTerminalFailure = (value) => typeof value === "object" &&
|
|
115
|
+
value !== null &&
|
|
116
|
+
value[TerminalFailureTypeId] ===
|
|
117
|
+
TerminalFailureTypeId;
|
|
118
|
+
const isActivityFailure = (value) => typeof value === "object" &&
|
|
119
|
+
value !== null &&
|
|
120
|
+
(value._wfFailureType === "terminal" ||
|
|
121
|
+
value._wfFailureType === "transient");
|
|
122
|
+
const unwrapActivityFailure = (error) => isActivityFailure(error) ? error.error : error;
|
|
123
|
+
const unwrapAsyncFailure = (error) => error instanceof AsyncFailure ? error.error : error;
|
|
124
|
+
const makeStepContext = (executionId, attempt) => ({
|
|
125
|
+
attempt,
|
|
126
|
+
executionId,
|
|
127
|
+
fail: (error) => ({ [TerminalFailureTypeId]: TerminalFailureTypeId, error })
|
|
128
|
+
});
|
|
129
|
+
const nextInvocation = (counters, name) => {
|
|
130
|
+
const invocation = (counters.get(name) ?? 0) + 1;
|
|
131
|
+
counters.set(name, invocation);
|
|
132
|
+
return invocation;
|
|
133
|
+
};
|
|
134
|
+
const decodeSync = (schema, value) => Schema.decodeUnknownSync(schema)(value);
|
|
135
|
+
const encodeSync = (schema, value) => Schema.encodeSync(schema)(value);
|
|
136
|
+
const resolveSecretRefs = async (value, resolver) => {
|
|
137
|
+
if (isSecretRef(value)) {
|
|
138
|
+
if (resolver === undefined) {
|
|
139
|
+
throw new Error(`No secret resolver configured for ${secretName(value)}`);
|
|
140
|
+
}
|
|
141
|
+
return await resolver.resolve(secretName(value));
|
|
142
|
+
}
|
|
143
|
+
if (Array.isArray(value)) {
|
|
144
|
+
return await Promise.all(value.map((item) => resolveSecretRefs(item, resolver)));
|
|
145
|
+
}
|
|
146
|
+
if (value instanceof Date || typeof value !== "object" || value === null) {
|
|
147
|
+
return value;
|
|
148
|
+
}
|
|
149
|
+
const entries = await Promise.all(Object.entries(value).map(async ([key, entry]) => [key, await resolveSecretRefs(entry, resolver)]));
|
|
150
|
+
return Object.fromEntries(entries);
|
|
151
|
+
};
|
|
152
|
+
const concurrencySemaphores = new Map();
|
|
153
|
+
const acquireConcurrency = async (step, input) => {
|
|
154
|
+
const limit = step.concurrency?.limit;
|
|
155
|
+
if (limit === undefined) {
|
|
156
|
+
return () => undefined;
|
|
157
|
+
}
|
|
158
|
+
if (!Number.isInteger(limit) || limit < 1) {
|
|
159
|
+
throw new Error(`Invalid concurrency limit for step ${step.name}: ${limit}`);
|
|
160
|
+
}
|
|
161
|
+
const key = step.concurrency?.key?.(input) ?? step.name;
|
|
162
|
+
const semaphoreKey = `${step.name}\0${key}`;
|
|
163
|
+
const state = concurrencySemaphores.get(semaphoreKey) ?? { active: 0, queue: [] };
|
|
164
|
+
concurrencySemaphores.set(semaphoreKey, state);
|
|
165
|
+
if (state.active >= limit) {
|
|
166
|
+
await new Promise((resolve) => {
|
|
167
|
+
state.queue.push(resolve);
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
state.active++;
|
|
171
|
+
let released = false;
|
|
172
|
+
return () => {
|
|
173
|
+
if (released) {
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
released = true;
|
|
177
|
+
state.active--;
|
|
178
|
+
const next = state.queue.shift();
|
|
179
|
+
if (next !== undefined) {
|
|
180
|
+
next();
|
|
181
|
+
}
|
|
182
|
+
if (state.active === 0 && state.queue.length === 0) {
|
|
183
|
+
concurrencySemaphores.delete(semaphoreKey);
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
};
|
|
187
|
+
// Durable race with a persisted winner. This deliberately does NOT use
|
|
188
|
+
// DurableDeferred.raceAll: its replay path runs `Effect.flatten(exit)` over
|
|
189
|
+
// the stored winner, which dies with "Not a valid effect" for plain (non-
|
|
190
|
+
// Effect) winner values. We store/unwrap the same way DurableDeferred.await
|
|
191
|
+
// does — a single `yield*` of the persisted exit.
|
|
192
|
+
const raceDurable = (name, effects) => Effect.gen(function* () {
|
|
193
|
+
const deferred = DurableDeferred.make(name, { success: Schema.Unknown });
|
|
194
|
+
const engine = yield* WorkflowEngine.WorkflowEngine;
|
|
195
|
+
const exit = yield* Workflow.wrapActivityResult(engine.deferredResult(deferred), Option.isNone);
|
|
196
|
+
if (Option.isSome(exit)) {
|
|
197
|
+
return yield* exit.value;
|
|
198
|
+
}
|
|
199
|
+
return yield* DurableDeferred.into(Effect.raceAll(effects), deferred);
|
|
200
|
+
});
|
|
201
|
+
const retrySchedule = (retry) => {
|
|
202
|
+
const attempts = Math.max(1, retry?.attempts ?? 1);
|
|
203
|
+
const recurs = Schedule.recurs(attempts - 1);
|
|
204
|
+
return retry?.backoff === "exponential"
|
|
205
|
+
? Schedule.exponential("10 millis").pipe(Schedule.both(recurs))
|
|
206
|
+
: recurs;
|
|
207
|
+
};
|
|
208
|
+
const transientAttempts = (retry) => Math.max(1, retry?.attempts ?? 1);
|
|
209
|
+
const makeCtx = (wf, executionId, workflowErrors) => {
|
|
210
|
+
const counters = new Map();
|
|
211
|
+
let journalPosition = 0;
|
|
212
|
+
let parallelDepth = 0;
|
|
213
|
+
const recordCall = (actual) => {
|
|
214
|
+
const position = ++journalPosition;
|
|
215
|
+
const activityName = parallelDepth > 0
|
|
216
|
+
? `determinism:${actual.kind}:${actual.name}#${actual.counter}`
|
|
217
|
+
: `determinism#${position}`;
|
|
218
|
+
return Activity.make({
|
|
219
|
+
name: activityName,
|
|
220
|
+
success: OrchestrationCallSchema,
|
|
221
|
+
execute: Effect.succeed(actual)
|
|
222
|
+
}).pipe(Effect.flatMap((expected) => callsEqual(expected, actual)
|
|
223
|
+
? Effect.void
|
|
224
|
+
: Effect.fail(new NonDeterminismError({ expected, actual }))));
|
|
225
|
+
};
|
|
226
|
+
const cancellationDeferred = DurableDeferred.make(cancellationDeferredName, {
|
|
227
|
+
success: CancellationRequestSchema
|
|
228
|
+
});
|
|
229
|
+
// A suspension point races its own durable operation against the reserved
|
|
230
|
+
// cancellation deferred, so a cancel request wakes the execution and unwinds
|
|
231
|
+
// it instead of leaving it parked forever.
|
|
232
|
+
const cancellationBranch = DurableDeferred.await(cancellationDeferred).pipe(Effect.map((request) => ({
|
|
233
|
+
type: "cancelled",
|
|
234
|
+
compensate: request.compensate,
|
|
235
|
+
actor: request.actor
|
|
236
|
+
})));
|
|
237
|
+
const failCancelled = (outcome) => Effect.gen(function* () {
|
|
238
|
+
yield* emitWorkflowEvent({
|
|
239
|
+
type: "cancellation.received",
|
|
240
|
+
executionId,
|
|
241
|
+
compensate: outcome.compensate,
|
|
242
|
+
...(outcome.actor === undefined ? {} : { actor: outcome.actor })
|
|
243
|
+
});
|
|
244
|
+
// A plain failure exit: withCompensation finalizers run for compensate:
|
|
245
|
+
// true. compensate: false never reaches here (the client interrupts the
|
|
246
|
+
// engine directly), but failing is still the safe fallback.
|
|
247
|
+
return yield* Effect.fail(new Cancelled({ compensate: outcome.compensate }));
|
|
248
|
+
});
|
|
249
|
+
return {
|
|
250
|
+
executionId,
|
|
251
|
+
run(step, rawInput) {
|
|
252
|
+
const invocation = nextInvocation(counters, step.name);
|
|
253
|
+
const activityName = `${step.name}#${invocation}`;
|
|
254
|
+
const call = { kind: "step", name: step.name, counter: invocation };
|
|
255
|
+
const input = decodeSync(step.input, rawInput);
|
|
256
|
+
const execute = Effect.gen(function* () {
|
|
257
|
+
const attempt = yield* Activity.CurrentAttempt;
|
|
258
|
+
yield* emitWorkflowEvent({
|
|
259
|
+
type: "step.started",
|
|
260
|
+
executionId,
|
|
261
|
+
stepName: step.name,
|
|
262
|
+
invocation,
|
|
263
|
+
activityName,
|
|
264
|
+
attempt,
|
|
265
|
+
input
|
|
266
|
+
});
|
|
267
|
+
const contextResolver = yield* currentSecretResolver;
|
|
268
|
+
const resolver = getExecutionSecretResolver(executionId) ?? contextResolver;
|
|
269
|
+
const result = yield* Effect.tryPromise({
|
|
270
|
+
try: async () => {
|
|
271
|
+
const release = await acquireConcurrency(step, input);
|
|
272
|
+
try {
|
|
273
|
+
const executeInput = await resolveSecretRefs(input, resolver);
|
|
274
|
+
const value = await step.execute(executeInput, makeStepContext(executionId, attempt));
|
|
275
|
+
if (isTerminalFailure(value)) {
|
|
276
|
+
throw value;
|
|
277
|
+
}
|
|
278
|
+
return decodeSync(step.output, value);
|
|
279
|
+
}
|
|
280
|
+
finally {
|
|
281
|
+
release();
|
|
282
|
+
}
|
|
283
|
+
},
|
|
284
|
+
catch: (error) => {
|
|
285
|
+
if (isTerminalFailure(error)) {
|
|
286
|
+
return {
|
|
287
|
+
_wfFailureType: "terminal",
|
|
288
|
+
error: decodeSync(step.errors, error.error)
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
return { _wfFailureType: "transient", error };
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
yield* emitWorkflowEvent({
|
|
295
|
+
type: "step.completed",
|
|
296
|
+
executionId,
|
|
297
|
+
stepName: step.name,
|
|
298
|
+
invocation,
|
|
299
|
+
activityName,
|
|
300
|
+
attempt,
|
|
301
|
+
result
|
|
302
|
+
});
|
|
303
|
+
return result;
|
|
304
|
+
}).pipe(Effect.tapError((error) => emitWorkflowEvent({
|
|
305
|
+
type: "step.failed",
|
|
306
|
+
executionId,
|
|
307
|
+
stepName: step.name,
|
|
308
|
+
invocation,
|
|
309
|
+
activityName,
|
|
310
|
+
error: unwrapActivityFailure(error)
|
|
311
|
+
})));
|
|
312
|
+
let activity = Activity.make({
|
|
313
|
+
name: activityName,
|
|
314
|
+
success: step.output,
|
|
315
|
+
error: Schema.Unknown,
|
|
316
|
+
execute
|
|
317
|
+
});
|
|
318
|
+
activity = activity.pipe(Activity.retry({
|
|
319
|
+
schedule: retrySchedule(step.retry),
|
|
320
|
+
while: (error) => isActivityFailure(error) && error._wfFailureType === "transient"
|
|
321
|
+
}), Effect.mapError(unwrapActivityFailure));
|
|
322
|
+
if (step.compensate !== undefined) {
|
|
323
|
+
activity = activity.pipe(wf.withCompensation((value, cause) => Effect.gen(function* () {
|
|
324
|
+
yield* emitWorkflowEvent({
|
|
325
|
+
type: "compensation.started",
|
|
326
|
+
executionId,
|
|
327
|
+
stepName: step.name,
|
|
328
|
+
invocation,
|
|
329
|
+
activityName,
|
|
330
|
+
result: value,
|
|
331
|
+
input,
|
|
332
|
+
reason: cause
|
|
333
|
+
});
|
|
334
|
+
yield* Effect.tryPromise({
|
|
335
|
+
try: () => Promise.resolve(step.compensate(value, input, cause)),
|
|
336
|
+
catch: (error) => new AsyncFailure(error)
|
|
337
|
+
}).pipe(Effect.tapError((error) => emitWorkflowEvent({
|
|
338
|
+
type: "compensation.failed",
|
|
339
|
+
executionId,
|
|
340
|
+
stepName: step.name,
|
|
341
|
+
invocation,
|
|
342
|
+
activityName,
|
|
343
|
+
error: unwrapAsyncFailure(error)
|
|
344
|
+
})), Effect.orDie);
|
|
345
|
+
yield* emitWorkflowEvent({
|
|
346
|
+
type: "compensation.completed",
|
|
347
|
+
executionId,
|
|
348
|
+
stepName: step.name,
|
|
349
|
+
invocation,
|
|
350
|
+
activityName
|
|
351
|
+
});
|
|
352
|
+
})));
|
|
353
|
+
}
|
|
354
|
+
return Effect.gen(function* () {
|
|
355
|
+
yield* recordCall(call);
|
|
356
|
+
return yield* activity;
|
|
357
|
+
});
|
|
358
|
+
},
|
|
359
|
+
all(effects, options) {
|
|
360
|
+
const name = options?.name ?? "all";
|
|
361
|
+
const invocation = nextInvocation(counters, name);
|
|
362
|
+
const activityName = `${name}#${invocation}`;
|
|
363
|
+
const branches = effects.length;
|
|
364
|
+
const call = { kind: "all", name, counter: invocation, branches };
|
|
365
|
+
return Effect.gen(function* () {
|
|
366
|
+
yield* recordCall(call);
|
|
367
|
+
yield* emitWorkflowEvent({
|
|
368
|
+
type: "all.started",
|
|
369
|
+
executionId,
|
|
370
|
+
name,
|
|
371
|
+
invocation,
|
|
372
|
+
activityName,
|
|
373
|
+
branches
|
|
374
|
+
});
|
|
375
|
+
yield* Effect.sync(() => {
|
|
376
|
+
parallelDepth++;
|
|
377
|
+
});
|
|
378
|
+
const combined = Effect.all(effects, {
|
|
379
|
+
concurrency: options?.concurrency ?? "unbounded"
|
|
380
|
+
});
|
|
381
|
+
return yield* combined.pipe(Effect.ensuring(Effect.sync(() => {
|
|
382
|
+
parallelDepth--;
|
|
383
|
+
})), Effect.tap(() => emitWorkflowEvent({
|
|
384
|
+
type: "all.completed",
|
|
385
|
+
executionId,
|
|
386
|
+
name,
|
|
387
|
+
invocation,
|
|
388
|
+
activityName,
|
|
389
|
+
branches
|
|
390
|
+
})), Effect.tapError((error) => emitWorkflowEvent({
|
|
391
|
+
type: "all.failed",
|
|
392
|
+
executionId,
|
|
393
|
+
name,
|
|
394
|
+
invocation,
|
|
395
|
+
activityName,
|
|
396
|
+
branches,
|
|
397
|
+
error
|
|
398
|
+
})));
|
|
399
|
+
});
|
|
400
|
+
},
|
|
401
|
+
sleep(duration, name) {
|
|
402
|
+
const baseName = name ?? `sleep:${String(duration)}`;
|
|
403
|
+
const invocation = nextInvocation(counters, baseName);
|
|
404
|
+
const sleepName = `${baseName}#${invocation}`;
|
|
405
|
+
const call = { kind: "sleep", name: baseName, counter: invocation };
|
|
406
|
+
return Effect.gen(function* () {
|
|
407
|
+
yield* recordCall(call);
|
|
408
|
+
yield* emitWorkflowEvent({
|
|
409
|
+
type: "sleep.started",
|
|
410
|
+
executionId,
|
|
411
|
+
name: baseName,
|
|
412
|
+
invocation,
|
|
413
|
+
activityName: sleepName,
|
|
414
|
+
duration
|
|
415
|
+
});
|
|
416
|
+
const outcome = (yield* raceDurable(`race:${sleepName}`, [
|
|
417
|
+
// Sleeps under the engine's in-memory threshold (60s) run inside
|
|
418
|
+
// an activity that holds the entity mailbox, so a cancellation
|
|
419
|
+
// delivered mid-sleep is consumed at the NEXT suspension point,
|
|
420
|
+
// not instantly — bounded by the threshold. Longer sleeps go
|
|
421
|
+
// durable and wake immediately on cancellation.
|
|
422
|
+
DurableClock.sleep({ name: sleepName, duration }).pipe(Effect.map(() => ({ type: "slept" }))),
|
|
423
|
+
cancellationBranch
|
|
424
|
+
]));
|
|
425
|
+
if (outcome.type === "cancelled") {
|
|
426
|
+
return yield* failCancelled(outcome);
|
|
427
|
+
}
|
|
428
|
+
yield* emitWorkflowEvent({
|
|
429
|
+
type: "sleep.completed",
|
|
430
|
+
executionId,
|
|
431
|
+
name: baseName,
|
|
432
|
+
invocation,
|
|
433
|
+
activityName: sleepName,
|
|
434
|
+
duration
|
|
435
|
+
});
|
|
436
|
+
});
|
|
437
|
+
},
|
|
438
|
+
waitForSignal(name, schema, opts) {
|
|
439
|
+
const invocation = nextInvocation(counters, name);
|
|
440
|
+
const waitName = `${name}#${invocation}`;
|
|
441
|
+
const call = { kind: "signal", name, counter: invocation };
|
|
442
|
+
const payloadSchema = jsonSchemaOf(schema);
|
|
443
|
+
return Effect.gen(function* () {
|
|
444
|
+
yield* recordCall(call);
|
|
445
|
+
// Delivery-side validation needs the schema of the wait the run is
|
|
446
|
+
// parked at; replay re-registers it in a fresh process.
|
|
447
|
+
registerSignalSchema(executionId, name, schema);
|
|
448
|
+
yield* emitWorkflowEvent({
|
|
449
|
+
type: "signal.waiting",
|
|
450
|
+
executionId,
|
|
451
|
+
name,
|
|
452
|
+
invocation,
|
|
453
|
+
activityName: waitName,
|
|
454
|
+
timeout: opts?.timeout,
|
|
455
|
+
...(payloadSchema === undefined ? {} : { payloadSchema })
|
|
456
|
+
});
|
|
457
|
+
const deferredName = `signal:${waitName}`;
|
|
458
|
+
const deferred = DurableDeferred.make(deferredName, { success: schema });
|
|
459
|
+
// The race winner is persisted, so the signal value crosses replay as
|
|
460
|
+
// its encoded form and is re-decoded below.
|
|
461
|
+
const signalBranch = DurableDeferred.await(deferred).pipe(Effect.map((value) => ({
|
|
462
|
+
type: "signal",
|
|
463
|
+
encoded: encodeSync(schema, value)
|
|
464
|
+
})));
|
|
465
|
+
const timeoutBranch = opts?.timeout === undefined
|
|
466
|
+
? []
|
|
467
|
+
: [
|
|
468
|
+
DurableClock.sleep({
|
|
469
|
+
name: `signal-timeout:${waitName}`,
|
|
470
|
+
duration: opts.timeout,
|
|
471
|
+
inMemoryThreshold: "1 milli"
|
|
472
|
+
}).pipe(Effect.map(() => ({ type: "timeout" })))
|
|
473
|
+
];
|
|
474
|
+
const outcome = (yield* raceDurable(`race:${waitName}`, [
|
|
475
|
+
signalBranch,
|
|
476
|
+
...timeoutBranch,
|
|
477
|
+
cancellationBranch
|
|
478
|
+
]));
|
|
479
|
+
if (outcome.type === "cancelled") {
|
|
480
|
+
return yield* failCancelled(outcome);
|
|
481
|
+
}
|
|
482
|
+
if (outcome.type === "timeout") {
|
|
483
|
+
yield* emitWorkflowEvent({
|
|
484
|
+
type: "signal.timeout",
|
|
485
|
+
executionId,
|
|
486
|
+
name,
|
|
487
|
+
invocation,
|
|
488
|
+
activityName: waitName,
|
|
489
|
+
timeout: opts?.timeout
|
|
490
|
+
});
|
|
491
|
+
return { type: "timeout" };
|
|
492
|
+
}
|
|
493
|
+
const value = decodeSync(schema, outcome.encoded);
|
|
494
|
+
yield* emitWorkflowEvent({
|
|
495
|
+
type: "signal.received",
|
|
496
|
+
executionId,
|
|
497
|
+
name,
|
|
498
|
+
invocation,
|
|
499
|
+
activityName: waitName,
|
|
500
|
+
payload: value
|
|
501
|
+
});
|
|
502
|
+
return { type: "signal", value };
|
|
503
|
+
});
|
|
504
|
+
},
|
|
505
|
+
now() {
|
|
506
|
+
const invocation = nextInvocation(counters, "now");
|
|
507
|
+
const activityName = `now#${invocation}`;
|
|
508
|
+
const call = { kind: "now", name: "now", counter: invocation };
|
|
509
|
+
const activity = Activity.make({
|
|
510
|
+
name: activityName,
|
|
511
|
+
success: Schema.Date,
|
|
512
|
+
execute: Effect.sync(() => new Date())
|
|
513
|
+
});
|
|
514
|
+
return Effect.gen(function* () {
|
|
515
|
+
yield* recordCall(call);
|
|
516
|
+
return yield* activity;
|
|
517
|
+
});
|
|
518
|
+
},
|
|
519
|
+
random() {
|
|
520
|
+
const invocation = nextInvocation(counters, "random");
|
|
521
|
+
const activityName = `random#${invocation}`;
|
|
522
|
+
const call = { kind: "random", name: "random", counter: invocation };
|
|
523
|
+
const activity = Activity.make({
|
|
524
|
+
name: activityName,
|
|
525
|
+
success: Schema.Number,
|
|
526
|
+
execute: Effect.sync(() => Math.random())
|
|
527
|
+
});
|
|
528
|
+
return Effect.gen(function* () {
|
|
529
|
+
yield* recordCall(call);
|
|
530
|
+
return yield* activity;
|
|
531
|
+
});
|
|
532
|
+
},
|
|
533
|
+
code(name, options) {
|
|
534
|
+
const invocation = nextInvocation(counters, name);
|
|
535
|
+
const activityName = `${name}#${invocation}`;
|
|
536
|
+
const call = { kind: "code", name, counter: invocation };
|
|
537
|
+
const execute = Effect.gen(function* () {
|
|
538
|
+
yield* emitWorkflowEvent({
|
|
539
|
+
type: "code.started",
|
|
540
|
+
executionId,
|
|
541
|
+
name,
|
|
542
|
+
invocation,
|
|
543
|
+
activityName,
|
|
544
|
+
...(options.reason === undefined ? {} : { reason: options.reason })
|
|
545
|
+
});
|
|
546
|
+
const result = yield* Effect.tryPromise({
|
|
547
|
+
try: async () => options.run(),
|
|
548
|
+
catch: (error) => new AsyncFailure(error)
|
|
549
|
+
});
|
|
550
|
+
yield* emitWorkflowEvent({
|
|
551
|
+
type: "code.completed",
|
|
552
|
+
executionId,
|
|
553
|
+
name,
|
|
554
|
+
invocation,
|
|
555
|
+
activityName,
|
|
556
|
+
...(options.reason === undefined ? {} : { reason: options.reason }),
|
|
557
|
+
result
|
|
558
|
+
});
|
|
559
|
+
return result;
|
|
560
|
+
}).pipe(Effect.tapError((error) => emitWorkflowEvent({
|
|
561
|
+
type: "code.failed",
|
|
562
|
+
executionId,
|
|
563
|
+
name,
|
|
564
|
+
invocation,
|
|
565
|
+
activityName,
|
|
566
|
+
...(options.reason === undefined ? {} : { reason: options.reason }),
|
|
567
|
+
error: unwrapAsyncFailure(error)
|
|
568
|
+
})));
|
|
569
|
+
const activity = Activity.make({
|
|
570
|
+
name: activityName,
|
|
571
|
+
success: Schema.Unknown,
|
|
572
|
+
error: Schema.Unknown,
|
|
573
|
+
execute
|
|
574
|
+
}).pipe(Effect.mapError(unwrapAsyncFailure));
|
|
575
|
+
return Effect.gen(function* () {
|
|
576
|
+
yield* recordCall(call);
|
|
577
|
+
return yield* activity;
|
|
578
|
+
});
|
|
579
|
+
},
|
|
580
|
+
fail(error) {
|
|
581
|
+
return Effect.fail(decodeSync(workflowErrors, error));
|
|
582
|
+
},
|
|
583
|
+
effect(effect) {
|
|
584
|
+
return effect;
|
|
585
|
+
}
|
|
586
|
+
};
|
|
587
|
+
};
|
|
588
|
+
const makeInMemoryCtx = (executionId, workflowErrors, compensations, determinism, emit, options = {}) => {
|
|
589
|
+
const counters = new Map();
|
|
590
|
+
let journalPosition = 0;
|
|
591
|
+
let blockPosition = 0;
|
|
592
|
+
const branchCollectors = [];
|
|
593
|
+
const recordCall = async (actual) => {
|
|
594
|
+
const index = journalPosition++;
|
|
595
|
+
const expected = determinism.calls[index];
|
|
596
|
+
if (expected === undefined) {
|
|
597
|
+
determinism.calls.push(actual);
|
|
598
|
+
}
|
|
599
|
+
else {
|
|
600
|
+
verifyCall(expected, actual);
|
|
601
|
+
}
|
|
602
|
+
branchCollectors[branchCollectors.length - 1]?.push(actual);
|
|
603
|
+
};
|
|
604
|
+
return {
|
|
605
|
+
executionId,
|
|
606
|
+
run(step, rawInput) {
|
|
607
|
+
const invocation = nextInvocation(counters, step.name);
|
|
608
|
+
const activityName = `${step.name}#${invocation}`;
|
|
609
|
+
const input = decodeSync(step.input, rawInput);
|
|
610
|
+
return Effect.tryPromise({
|
|
611
|
+
try: async () => {
|
|
612
|
+
await recordCall({ kind: "step", name: step.name, counter: invocation });
|
|
613
|
+
const attempts = transientAttempts(step.retry);
|
|
614
|
+
let lastTransient;
|
|
615
|
+
for (let attempt = 1; attempt <= attempts; attempt++) {
|
|
616
|
+
await emit({
|
|
617
|
+
type: "step.started",
|
|
618
|
+
executionId,
|
|
619
|
+
stepName: step.name,
|
|
620
|
+
invocation,
|
|
621
|
+
activityName,
|
|
622
|
+
attempt,
|
|
623
|
+
input
|
|
624
|
+
});
|
|
625
|
+
try {
|
|
626
|
+
const stepContext = makeStepContext(executionId, attempt);
|
|
627
|
+
const executeStep = options.stepExecutors?.get(step);
|
|
628
|
+
const release = await acquireConcurrency(step, input);
|
|
629
|
+
try {
|
|
630
|
+
const executeInput = await resolveSecretRefs(input, options.secrets);
|
|
631
|
+
const value = await (options.stepExecutor?.({ step, input: executeInput, invocation, activityName, context: stepContext }) ??
|
|
632
|
+
executeStep?.(executeInput, stepContext) ??
|
|
633
|
+
step.execute(executeInput, stepContext));
|
|
634
|
+
if (isTerminalFailure(value)) {
|
|
635
|
+
const terminal = decodeSync(step.errors, value.error);
|
|
636
|
+
throw terminal;
|
|
637
|
+
}
|
|
638
|
+
const result = decodeSync(step.output, value);
|
|
639
|
+
encodeSync(step.output, result);
|
|
640
|
+
await emit({
|
|
641
|
+
type: "step.completed",
|
|
642
|
+
executionId,
|
|
643
|
+
stepName: step.name,
|
|
644
|
+
invocation,
|
|
645
|
+
activityName,
|
|
646
|
+
attempt,
|
|
647
|
+
result
|
|
648
|
+
});
|
|
649
|
+
if (step.compensate !== undefined) {
|
|
650
|
+
compensations.push({
|
|
651
|
+
stepName: step.name,
|
|
652
|
+
invocation,
|
|
653
|
+
result,
|
|
654
|
+
input,
|
|
655
|
+
compensate: step.compensate
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
return result;
|
|
659
|
+
}
|
|
660
|
+
finally {
|
|
661
|
+
release();
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
catch (error) {
|
|
665
|
+
if (attempt === attempts || isDeclaredTerminal(step.errors, error)) {
|
|
666
|
+
await emit({
|
|
667
|
+
type: "step.failed",
|
|
668
|
+
executionId,
|
|
669
|
+
stepName: step.name,
|
|
670
|
+
invocation,
|
|
671
|
+
activityName,
|
|
672
|
+
error
|
|
673
|
+
});
|
|
674
|
+
throw error;
|
|
675
|
+
}
|
|
676
|
+
lastTransient = error;
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
throw lastTransient;
|
|
680
|
+
},
|
|
681
|
+
catch: (error) => new AsyncFailure(error)
|
|
682
|
+
}).pipe(Effect.mapError(unwrapAsyncFailure));
|
|
683
|
+
},
|
|
684
|
+
sleep(duration, name) {
|
|
685
|
+
const baseName = name ?? `sleep:${String(duration)}`;
|
|
686
|
+
const invocation = nextInvocation(counters, baseName);
|
|
687
|
+
const activityName = `${baseName}#${invocation}`;
|
|
688
|
+
return Effect.promise(async () => {
|
|
689
|
+
await recordCall({ kind: "sleep", name: baseName, counter: invocation });
|
|
690
|
+
await emit({
|
|
691
|
+
type: "sleep.started",
|
|
692
|
+
executionId,
|
|
693
|
+
name: baseName,
|
|
694
|
+
invocation,
|
|
695
|
+
activityName,
|
|
696
|
+
duration
|
|
697
|
+
});
|
|
698
|
+
await options.sleep?.({ executionId, name: activityName, duration });
|
|
699
|
+
await emit({
|
|
700
|
+
type: "sleep.completed",
|
|
701
|
+
executionId,
|
|
702
|
+
name: baseName,
|
|
703
|
+
invocation,
|
|
704
|
+
activityName,
|
|
705
|
+
duration
|
|
706
|
+
});
|
|
707
|
+
});
|
|
708
|
+
},
|
|
709
|
+
waitForSignal(name, schema, opts) {
|
|
710
|
+
const invocation = nextInvocation(counters, name);
|
|
711
|
+
const activityName = `${name}#${invocation}`;
|
|
712
|
+
const payloadSchema = jsonSchemaOf(schema);
|
|
713
|
+
return Effect.tryPromise({
|
|
714
|
+
try: async () => {
|
|
715
|
+
await recordCall({ kind: "signal", name, counter: invocation });
|
|
716
|
+
registerSignalSchema(executionId, name, schema);
|
|
717
|
+
await emit({
|
|
718
|
+
type: "signal.waiting",
|
|
719
|
+
executionId,
|
|
720
|
+
name,
|
|
721
|
+
invocation,
|
|
722
|
+
activityName,
|
|
723
|
+
timeout: opts?.timeout,
|
|
724
|
+
...(payloadSchema === undefined ? {} : { payloadSchema })
|
|
725
|
+
});
|
|
726
|
+
const buffered = takeBufferedSignal(executionId, name, schema);
|
|
727
|
+
if (buffered !== undefined) {
|
|
728
|
+
await emit({
|
|
729
|
+
type: "signal.received",
|
|
730
|
+
executionId,
|
|
731
|
+
name,
|
|
732
|
+
invocation,
|
|
733
|
+
activityName,
|
|
734
|
+
payload: buffered
|
|
735
|
+
});
|
|
736
|
+
return { type: "signal", value: buffered };
|
|
737
|
+
}
|
|
738
|
+
if (options.signalValue !== undefined) {
|
|
739
|
+
const value = decodeSync(schema, await options.signalValue({ executionId, name, schema }));
|
|
740
|
+
await emit({
|
|
741
|
+
type: "signal.received",
|
|
742
|
+
executionId,
|
|
743
|
+
name,
|
|
744
|
+
invocation,
|
|
745
|
+
activityName,
|
|
746
|
+
payload: value
|
|
747
|
+
});
|
|
748
|
+
return { type: "signal", value };
|
|
749
|
+
}
|
|
750
|
+
if (opts?.timeout !== undefined) {
|
|
751
|
+
if (options.signalTimeout !== undefined) {
|
|
752
|
+
const outcome = await Promise.race([
|
|
753
|
+
awaitSignal(executionId, name, schema).then((value) => ({ type: "signal", value })),
|
|
754
|
+
options.signalTimeout({ executionId, name: activityName, duration: opts.timeout })
|
|
755
|
+
.then(() => ({ type: "timeout" }))
|
|
756
|
+
]);
|
|
757
|
+
if (outcome.type === "signal") {
|
|
758
|
+
await emit({
|
|
759
|
+
type: "signal.received",
|
|
760
|
+
executionId,
|
|
761
|
+
name,
|
|
762
|
+
invocation,
|
|
763
|
+
activityName,
|
|
764
|
+
payload: outcome.value
|
|
765
|
+
});
|
|
766
|
+
}
|
|
767
|
+
else {
|
|
768
|
+
await emit({
|
|
769
|
+
type: "signal.timeout",
|
|
770
|
+
executionId,
|
|
771
|
+
name,
|
|
772
|
+
invocation,
|
|
773
|
+
activityName,
|
|
774
|
+
timeout: opts.timeout
|
|
775
|
+
});
|
|
776
|
+
}
|
|
777
|
+
return outcome;
|
|
778
|
+
}
|
|
779
|
+
await emit({
|
|
780
|
+
type: "signal.timeout",
|
|
781
|
+
executionId,
|
|
782
|
+
name,
|
|
783
|
+
invocation,
|
|
784
|
+
activityName,
|
|
785
|
+
timeout: opts.timeout
|
|
786
|
+
});
|
|
787
|
+
return { type: "timeout" };
|
|
788
|
+
}
|
|
789
|
+
const value = await awaitSignal(executionId, name, schema);
|
|
790
|
+
await emit({
|
|
791
|
+
type: "signal.received",
|
|
792
|
+
executionId,
|
|
793
|
+
name,
|
|
794
|
+
invocation,
|
|
795
|
+
activityName,
|
|
796
|
+
payload: value
|
|
797
|
+
});
|
|
798
|
+
return { type: "signal", value };
|
|
799
|
+
},
|
|
800
|
+
catch: (error) => new AsyncFailure(error)
|
|
801
|
+
}).pipe(Effect.mapError(unwrapAsyncFailure));
|
|
802
|
+
},
|
|
803
|
+
now() {
|
|
804
|
+
const invocation = nextInvocation(counters, "now");
|
|
805
|
+
const call = { kind: "now", name: "now", counter: invocation };
|
|
806
|
+
return Effect.promise(async () => {
|
|
807
|
+
await recordCall(call);
|
|
808
|
+
const key = valueKey(call);
|
|
809
|
+
const existing = determinism.values.get(key);
|
|
810
|
+
if (existing instanceof Date) {
|
|
811
|
+
return existing;
|
|
812
|
+
}
|
|
813
|
+
const value = new Date();
|
|
814
|
+
determinism.values.set(key, value);
|
|
815
|
+
return value;
|
|
816
|
+
});
|
|
817
|
+
},
|
|
818
|
+
random() {
|
|
819
|
+
const invocation = nextInvocation(counters, "random");
|
|
820
|
+
const call = { kind: "random", name: "random", counter: invocation };
|
|
821
|
+
return Effect.promise(async () => {
|
|
822
|
+
await recordCall(call);
|
|
823
|
+
const key = valueKey(call);
|
|
824
|
+
const existing = determinism.values.get(key);
|
|
825
|
+
if (typeof existing === "number") {
|
|
826
|
+
return existing;
|
|
827
|
+
}
|
|
828
|
+
const value = Math.random();
|
|
829
|
+
determinism.values.set(key, value);
|
|
830
|
+
return value;
|
|
831
|
+
});
|
|
832
|
+
},
|
|
833
|
+
code(name, options) {
|
|
834
|
+
const invocation = nextInvocation(counters, name);
|
|
835
|
+
const activityName = `${name}#${invocation}`;
|
|
836
|
+
const call = { kind: "code", name, counter: invocation };
|
|
837
|
+
return Effect.tryPromise({
|
|
838
|
+
try: async () => {
|
|
839
|
+
await recordCall(call);
|
|
840
|
+
await emit({
|
|
841
|
+
type: "code.started",
|
|
842
|
+
executionId,
|
|
843
|
+
name,
|
|
844
|
+
invocation,
|
|
845
|
+
activityName,
|
|
846
|
+
...(options.reason === undefined ? {} : { reason: options.reason })
|
|
847
|
+
});
|
|
848
|
+
const key = valueKey(call);
|
|
849
|
+
if (determinism.values.has(key)) {
|
|
850
|
+
const result = determinism.values.get(key);
|
|
851
|
+
await emit({
|
|
852
|
+
type: "code.completed",
|
|
853
|
+
executionId,
|
|
854
|
+
name,
|
|
855
|
+
invocation,
|
|
856
|
+
activityName,
|
|
857
|
+
...(options.reason === undefined ? {} : { reason: options.reason }),
|
|
858
|
+
result
|
|
859
|
+
});
|
|
860
|
+
return result;
|
|
861
|
+
}
|
|
862
|
+
try {
|
|
863
|
+
const result = await options.run();
|
|
864
|
+
determinism.values.set(key, result);
|
|
865
|
+
await emit({
|
|
866
|
+
type: "code.completed",
|
|
867
|
+
executionId,
|
|
868
|
+
name,
|
|
869
|
+
invocation,
|
|
870
|
+
activityName,
|
|
871
|
+
...(options.reason === undefined ? {} : { reason: options.reason }),
|
|
872
|
+
result
|
|
873
|
+
});
|
|
874
|
+
return result;
|
|
875
|
+
}
|
|
876
|
+
catch (error) {
|
|
877
|
+
await emit({
|
|
878
|
+
type: "code.failed",
|
|
879
|
+
executionId,
|
|
880
|
+
name,
|
|
881
|
+
invocation,
|
|
882
|
+
activityName,
|
|
883
|
+
...(options.reason === undefined ? {} : { reason: options.reason }),
|
|
884
|
+
error
|
|
885
|
+
});
|
|
886
|
+
throw error;
|
|
887
|
+
}
|
|
888
|
+
},
|
|
889
|
+
catch: (error) => new AsyncFailure(error)
|
|
890
|
+
}).pipe(Effect.mapError(unwrapAsyncFailure));
|
|
891
|
+
},
|
|
892
|
+
all(effects, options) {
|
|
893
|
+
const name = options?.name ?? "all";
|
|
894
|
+
const invocation = nextInvocation(counters, name);
|
|
895
|
+
const activityName = `${name}#${invocation}`;
|
|
896
|
+
const branches = effects.length;
|
|
897
|
+
const call = { kind: "all", name, counter: invocation, branches };
|
|
898
|
+
const record = Effect.tryPromise({
|
|
899
|
+
try: () => recordCall(call),
|
|
900
|
+
catch: (error) => error
|
|
901
|
+
});
|
|
902
|
+
const emitEvent = (event) => Effect.promise(() => emit(event));
|
|
903
|
+
const persistBlock = (branchCalls) => Effect.sync(() => {
|
|
904
|
+
if (determinism.blocks[blockPosition++] === undefined) {
|
|
905
|
+
determinism.blocks.push({ call, branches: branchCalls });
|
|
906
|
+
}
|
|
907
|
+
});
|
|
908
|
+
return Effect.gen(function* () {
|
|
909
|
+
yield* record;
|
|
910
|
+
yield* emitEvent({
|
|
911
|
+
type: "all.started",
|
|
912
|
+
executionId,
|
|
913
|
+
name,
|
|
914
|
+
invocation,
|
|
915
|
+
activityName,
|
|
916
|
+
branches
|
|
917
|
+
});
|
|
918
|
+
const branchCalls = [];
|
|
919
|
+
const wrapped = effects.map((effect) => Effect.acquireUseRelease(Effect.sync(() => {
|
|
920
|
+
const calls = [];
|
|
921
|
+
branchCalls.push(calls);
|
|
922
|
+
branchCollectors.push(calls);
|
|
923
|
+
}), () => effect, () => Effect.sync(() => {
|
|
924
|
+
branchCollectors.pop();
|
|
925
|
+
})));
|
|
926
|
+
return yield* Effect.all(wrapped, { concurrency: 1 }).pipe(Effect.tap(() => Effect.gen(function* () {
|
|
927
|
+
yield* persistBlock(branchCalls);
|
|
928
|
+
yield* emitEvent({
|
|
929
|
+
type: "all.completed",
|
|
930
|
+
executionId,
|
|
931
|
+
name,
|
|
932
|
+
invocation,
|
|
933
|
+
activityName,
|
|
934
|
+
branches
|
|
935
|
+
});
|
|
936
|
+
})), Effect.tapError((error) => Effect.gen(function* () {
|
|
937
|
+
yield* persistBlock(branchCalls);
|
|
938
|
+
yield* emitEvent({
|
|
939
|
+
type: "all.failed",
|
|
940
|
+
executionId,
|
|
941
|
+
name,
|
|
942
|
+
invocation,
|
|
943
|
+
activityName,
|
|
944
|
+
branches,
|
|
945
|
+
error
|
|
946
|
+
});
|
|
947
|
+
})));
|
|
948
|
+
});
|
|
949
|
+
},
|
|
950
|
+
fail(error) {
|
|
951
|
+
return Effect.fail(decodeSync(workflowErrors, error));
|
|
952
|
+
},
|
|
953
|
+
effect(effect) {
|
|
954
|
+
return effect;
|
|
955
|
+
}
|
|
956
|
+
};
|
|
957
|
+
};
|
|
958
|
+
const isDeclaredTerminal = (schema, error) => {
|
|
959
|
+
try {
|
|
960
|
+
decodeSync(schema, error);
|
|
961
|
+
return true;
|
|
962
|
+
}
|
|
963
|
+
catch {
|
|
964
|
+
return false;
|
|
965
|
+
}
|
|
966
|
+
};
|
|
967
|
+
export const defineWorkflow = (config) => {
|
|
968
|
+
const errors = config.errors ?? Schema.Never;
|
|
969
|
+
const engineName = `${config.name}@v${config.version}`;
|
|
970
|
+
const sourceHash = createHash("sha256")
|
|
971
|
+
.update(config.name)
|
|
972
|
+
.update("\0")
|
|
973
|
+
.update(String(config.version))
|
|
974
|
+
.update("\0")
|
|
975
|
+
.update(config.run.toString())
|
|
976
|
+
.digest("hex");
|
|
977
|
+
const workflow = Workflow.make(engineName, {
|
|
978
|
+
payload: config.input,
|
|
979
|
+
idempotencyKey: (payload) => JSON.stringify(payload),
|
|
980
|
+
success: config.output,
|
|
981
|
+
error: Schema.Unknown
|
|
982
|
+
});
|
|
983
|
+
const layer = workflow.toLayer(Effect.fn(function* (payload, executionId) {
|
|
984
|
+
const input = decodeSync(config.input, payload);
|
|
985
|
+
const result = yield* config.run(input, makeCtx(workflow, ExecutionId.make(executionId), errors));
|
|
986
|
+
return decodeSync(config.output, result);
|
|
987
|
+
}));
|
|
988
|
+
const executeInMemory = async (payload, options = {}) => {
|
|
989
|
+
const executionId = ExecutionId.make(options.executionId ?? `memory-${crypto.randomUUID()}`);
|
|
990
|
+
const compensations = [];
|
|
991
|
+
const determinism = options.determinism ?? createInMemoryDeterminismState();
|
|
992
|
+
const input = decodeSync(config.input, payload);
|
|
993
|
+
const emit = async (event) => {
|
|
994
|
+
await options.onEvent?.(event);
|
|
995
|
+
};
|
|
996
|
+
const ctx = makeInMemoryCtx(executionId, errors, compensations, determinism, emit, {
|
|
997
|
+
...(options.stepExecutors === undefined ? {} : { stepExecutors: options.stepExecutors }),
|
|
998
|
+
...(options.stepExecutor === undefined ? {} : { stepExecutor: options.stepExecutor }),
|
|
999
|
+
...(options.sleep === undefined ? {} : { sleep: options.sleep }),
|
|
1000
|
+
...(options.signalTimeout === undefined ? {} : { signalTimeout: options.signalTimeout }),
|
|
1001
|
+
...(options.signalValue === undefined ? {} : { signalValue: options.signalValue }),
|
|
1002
|
+
...(options.secrets === undefined ? {} : { secrets: options.secrets })
|
|
1003
|
+
});
|
|
1004
|
+
const effect = Effect.gen(function* () {
|
|
1005
|
+
return yield* config.run(input, ctx);
|
|
1006
|
+
}).pipe(Effect.map((result) => decodeSync(config.output, result)), Effect.catch((error) => Effect.gen(function* () {
|
|
1007
|
+
if (skipsCompensation(error)) {
|
|
1008
|
+
return yield* Effect.fail(error);
|
|
1009
|
+
}
|
|
1010
|
+
for (const compensation of compensations.slice().reverse()) {
|
|
1011
|
+
yield* emitWorkflowEvent({
|
|
1012
|
+
type: "compensation.started",
|
|
1013
|
+
executionId,
|
|
1014
|
+
stepName: compensation.stepName,
|
|
1015
|
+
invocation: compensation.invocation,
|
|
1016
|
+
activityName: `${compensation.stepName}#${compensation.invocation}`,
|
|
1017
|
+
result: compensation.result,
|
|
1018
|
+
input: compensation.input,
|
|
1019
|
+
reason: error
|
|
1020
|
+
});
|
|
1021
|
+
yield* Effect.promise(() => Promise.resolve(compensation.compensate(compensation.result, compensation.input, error))).pipe(Effect.tapError((compensationError) => emitWorkflowEvent({
|
|
1022
|
+
type: "compensation.failed",
|
|
1023
|
+
executionId,
|
|
1024
|
+
stepName: compensation.stepName,
|
|
1025
|
+
invocation: compensation.invocation,
|
|
1026
|
+
activityName: `${compensation.stepName}#${compensation.invocation}`,
|
|
1027
|
+
error: compensationError
|
|
1028
|
+
})), Effect.orDie);
|
|
1029
|
+
yield* emitWorkflowEvent({
|
|
1030
|
+
type: "compensation.completed",
|
|
1031
|
+
executionId,
|
|
1032
|
+
stepName: compensation.stepName,
|
|
1033
|
+
invocation: compensation.invocation,
|
|
1034
|
+
activityName: `${compensation.stepName}#${compensation.invocation}`
|
|
1035
|
+
});
|
|
1036
|
+
}
|
|
1037
|
+
return yield* Effect.fail(error);
|
|
1038
|
+
})));
|
|
1039
|
+
const exit = await Effect.runPromiseExit(effect.pipe(Effect.provideService(currentWorkflowEventSink, options.onEvent)));
|
|
1040
|
+
if (Exit.isSuccess(exit)) {
|
|
1041
|
+
return exit.value;
|
|
1042
|
+
}
|
|
1043
|
+
const failure = Option.getOrUndefined(Cause.findErrorOption(exit.cause));
|
|
1044
|
+
throw failure ?? Cause.squash(exit.cause);
|
|
1045
|
+
};
|
|
1046
|
+
return {
|
|
1047
|
+
[DefinedWorkflowTypeId]: DefinedWorkflowTypeId,
|
|
1048
|
+
name: config.name,
|
|
1049
|
+
version: config.version,
|
|
1050
|
+
engineName,
|
|
1051
|
+
sourceHash,
|
|
1052
|
+
input: config.input,
|
|
1053
|
+
output: config.output,
|
|
1054
|
+
errors,
|
|
1055
|
+
workflow,
|
|
1056
|
+
layer,
|
|
1057
|
+
execute: (payload) => workflow.execute(payload),
|
|
1058
|
+
executeInMemory
|
|
1059
|
+
};
|
|
1060
|
+
};
|
|
1061
|
+
//# sourceMappingURL=core.js.map
|