@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.
Files changed (108) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +57 -0
  3. package/dist/authoring.d.ts +7 -0
  4. package/dist/authoring.d.ts.map +1 -0
  5. package/dist/authoring.js +8 -0
  6. package/dist/authoring.js.map +1 -0
  7. package/dist/cli/integrations.d.ts +3 -0
  8. package/dist/cli/integrations.d.ts.map +1 -0
  9. package/dist/cli/integrations.js +40 -0
  10. package/dist/cli/integrations.js.map +1 -0
  11. package/dist/cli/main.d.ts +3 -0
  12. package/dist/cli/main.d.ts.map +1 -0
  13. package/dist/cli/main.js +591 -0
  14. package/dist/cli/main.js.map +1 -0
  15. package/dist/core.d.ts +198 -0
  16. package/dist/core.d.ts.map +1 -0
  17. package/dist/core.js +1061 -0
  18. package/dist/core.js.map +1 -0
  19. package/dist/errors.d.ts +3 -0
  20. package/dist/errors.d.ts.map +1 -0
  21. package/dist/errors.js +13 -0
  22. package/dist/errors.js.map +1 -0
  23. package/dist/events.d.ts +154 -0
  24. package/dist/events.d.ts.map +1 -0
  25. package/dist/events.js +24 -0
  26. package/dist/events.js.map +1 -0
  27. package/dist/index.d.ts +15 -0
  28. package/dist/index.d.ts.map +1 -0
  29. package/dist/index.js +11 -0
  30. package/dist/index.js.map +1 -0
  31. package/dist/replay.d.ts +3 -0
  32. package/dist/replay.d.ts.map +1 -0
  33. package/dist/replay.js +28 -0
  34. package/dist/replay.js.map +1 -0
  35. package/dist/runtime.d.ts +70 -0
  36. package/dist/runtime.d.ts.map +1 -0
  37. package/dist/runtime.js +183 -0
  38. package/dist/runtime.js.map +1 -0
  39. package/dist/schema.d.ts +18 -0
  40. package/dist/schema.d.ts.map +1 -0
  41. package/dist/schema.js +19 -0
  42. package/dist/schema.js.map +1 -0
  43. package/dist/schemas.d.ts +1198 -0
  44. package/dist/schemas.d.ts.map +1 -0
  45. package/dist/schemas.js +383 -0
  46. package/dist/schemas.js.map +1 -0
  47. package/dist/sdk/artifact.d.ts +41 -0
  48. package/dist/sdk/artifact.d.ts.map +1 -0
  49. package/dist/sdk/artifact.js +94 -0
  50. package/dist/sdk/artifact.js.map +1 -0
  51. package/dist/sdk/graph.d.ts +15 -0
  52. package/dist/sdk/graph.d.ts.map +1 -0
  53. package/dist/sdk/graph.js +388 -0
  54. package/dist/sdk/graph.js.map +1 -0
  55. package/dist/sdk/index.d.ts +14 -0
  56. package/dist/sdk/index.d.ts.map +1 -0
  57. package/dist/sdk/index.js +8 -0
  58. package/dist/sdk/index.js.map +1 -0
  59. package/dist/sdk/integrations.d.ts +18 -0
  60. package/dist/sdk/integrations.d.ts.map +1 -0
  61. package/dist/sdk/integrations.js +20 -0
  62. package/dist/sdk/integrations.js.map +1 -0
  63. package/dist/sdk/json.d.ts +4 -0
  64. package/dist/sdk/json.d.ts.map +1 -0
  65. package/dist/sdk/json.js +31 -0
  66. package/dist/sdk/json.js.map +1 -0
  67. package/dist/sdk/loader.d.ts +10 -0
  68. package/dist/sdk/loader.d.ts.map +1 -0
  69. package/dist/sdk/loader.js +75 -0
  70. package/dist/sdk/loader.js.map +1 -0
  71. package/dist/sdk/sdk.d.ts +102 -0
  72. package/dist/sdk/sdk.d.ts.map +1 -0
  73. package/dist/sdk/sdk.js +669 -0
  74. package/dist/sdk/sdk.js.map +1 -0
  75. package/dist/sdk/sqlite.d.ts +8 -0
  76. package/dist/sdk/sqlite.d.ts.map +1 -0
  77. package/dist/sdk/sqlite.js +288 -0
  78. package/dist/sdk/sqlite.js.map +1 -0
  79. package/dist/signal.d.ts +17 -0
  80. package/dist/signal.d.ts.map +1 -0
  81. package/dist/signal.js +78 -0
  82. package/dist/signal.js.map +1 -0
  83. package/dist/testing/index.d.ts +35 -0
  84. package/dist/testing/index.d.ts.map +1 -0
  85. package/dist/testing/index.js +244 -0
  86. package/dist/testing/index.js.map +1 -0
  87. package/package.json +67 -0
  88. package/src/authoring.ts +32 -0
  89. package/src/cli/integrations.ts +75 -0
  90. package/src/cli/main.ts +793 -0
  91. package/src/core.ts +1515 -0
  92. package/src/errors.ts +19 -0
  93. package/src/events.ts +37 -0
  94. package/src/index.ts +109 -0
  95. package/src/replay.ts +29 -0
  96. package/src/runtime.ts +314 -0
  97. package/src/schema.ts +34 -0
  98. package/src/schemas.ts +473 -0
  99. package/src/sdk/artifact.ts +176 -0
  100. package/src/sdk/graph.ts +486 -0
  101. package/src/sdk/index.ts +64 -0
  102. package/src/sdk/integrations.ts +49 -0
  103. package/src/sdk/json.ts +40 -0
  104. package/src/sdk/loader.ts +99 -0
  105. package/src/sdk/sdk.ts +911 -0
  106. package/src/sdk/sqlite.ts +405 -0
  107. package/src/signal.ts +116 -0
  108. 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