flowfn 0.0.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.
Files changed (59) hide show
  1. package/dist/index.d.mts +1305 -0
  2. package/dist/index.d.ts +1305 -0
  3. package/dist/index.js +3180 -0
  4. package/dist/index.js.map +1 -0
  5. package/dist/index.mjs +3088 -0
  6. package/dist/index.mjs.map +1 -0
  7. package/docs/API.md +801 -0
  8. package/docs/USAGE.md +619 -0
  9. package/package.json +75 -0
  10. package/src/adapters/base.ts +46 -0
  11. package/src/adapters/memory.ts +183 -0
  12. package/src/adapters/postgres/index.ts +383 -0
  13. package/src/adapters/postgres/postgres.test.ts +100 -0
  14. package/src/adapters/postgres/schema.ts +110 -0
  15. package/src/adapters/redis.test.ts +124 -0
  16. package/src/adapters/redis.ts +331 -0
  17. package/src/core/flow-fn.test.ts +70 -0
  18. package/src/core/flow-fn.ts +198 -0
  19. package/src/core/metrics.ts +198 -0
  20. package/src/core/scheduler.test.ts +80 -0
  21. package/src/core/scheduler.ts +154 -0
  22. package/src/index.ts +57 -0
  23. package/src/monitoring/health.ts +261 -0
  24. package/src/patterns/backoff.ts +30 -0
  25. package/src/patterns/batching.ts +248 -0
  26. package/src/patterns/circuit-breaker.test.ts +52 -0
  27. package/src/patterns/circuit-breaker.ts +52 -0
  28. package/src/patterns/priority.ts +146 -0
  29. package/src/patterns/rate-limit.ts +290 -0
  30. package/src/patterns/retry.test.ts +62 -0
  31. package/src/queue/batch.test.ts +35 -0
  32. package/src/queue/dependencies.test.ts +33 -0
  33. package/src/queue/dlq.ts +222 -0
  34. package/src/queue/job.ts +67 -0
  35. package/src/queue/queue.ts +243 -0
  36. package/src/queue/types.ts +153 -0
  37. package/src/queue/worker.ts +66 -0
  38. package/src/storage/event-log.ts +205 -0
  39. package/src/storage/job-storage.ts +206 -0
  40. package/src/storage/workflow-storage.ts +182 -0
  41. package/src/stream/stream.ts +194 -0
  42. package/src/stream/types.ts +81 -0
  43. package/src/utils/hashing.ts +29 -0
  44. package/src/utils/id-generator.ts +109 -0
  45. package/src/utils/serialization.ts +142 -0
  46. package/src/utils/time.ts +167 -0
  47. package/src/workflow/advanced.test.ts +43 -0
  48. package/src/workflow/events.test.ts +39 -0
  49. package/src/workflow/types.ts +132 -0
  50. package/src/workflow/workflow.test.ts +55 -0
  51. package/src/workflow/workflow.ts +422 -0
  52. package/tests/dlq.test.ts +205 -0
  53. package/tests/health.test.ts +228 -0
  54. package/tests/integration.test.ts +253 -0
  55. package/tests/stream.test.ts +233 -0
  56. package/tests/workflow.test.ts +286 -0
  57. package/tsconfig.json +17 -0
  58. package/tsup.config.ts +10 -0
  59. package/vitest.config.ts +15 -0
@@ -0,0 +1,43 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
+ import { createFlow } from '../core/flow-fn.js';
3
+
4
+ describe('Advanced Workflow Features', () => {
5
+ let flow;
6
+
7
+ beforeEach(() => {
8
+ flow = createFlow({ adapter: 'memory' });
9
+ vi.useFakeTimers();
10
+ });
11
+
12
+ afterEach(async () => {
13
+ await flow.close();
14
+ vi.useRealTimers();
15
+ });
16
+
17
+ it('should delay until specific time', async () => {
18
+ const targetTime = Date.now() + 1000;
19
+
20
+ const workflow = flow.workflow('delay-until')
21
+ .step('start', async (ctx) => { ctx.set('step1', Date.now()); })
22
+ .delayUntil(() => targetTime)
23
+ .step('end', async (ctx) => { ctx.set('step2', Date.now()); })
24
+ .build();
25
+
26
+ const execution = await workflow.execute({});
27
+
28
+ // Initial part runs
29
+ await vi.advanceTimersByTimeAsync(10);
30
+
31
+ // Wait for delay
32
+ await vi.advanceTimersByTimeAsync(1000);
33
+
34
+ const state = await workflow.getExecution(execution.id);
35
+ expect(state.status).toBe('completed');
36
+
37
+ const step1Time = state.output.step1;
38
+ const step2Time = state.output.step2;
39
+
40
+ expect(step2Time).toBeGreaterThanOrEqual(targetTime);
41
+ expect(step2Time - step1Time).toBeGreaterThanOrEqual(1000);
42
+ });
43
+ });
@@ -0,0 +1,39 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
+ import { createFlow } from '../core/flow-fn.js';
3
+
4
+ describe('Workflow Events', () => {
5
+ let flow;
6
+
7
+ beforeEach(() => {
8
+ flow = createFlow({ adapter: 'memory' });
9
+ vi.useFakeTimers();
10
+ });
11
+
12
+ afterEach(async () => {
13
+ await flow.close();
14
+ vi.useRealTimers();
15
+ });
16
+
17
+ it('should wait for event (timeout simulation)', async () => {
18
+ const workflow = flow.workflow('wait-event')
19
+ .step('start', async (ctx) => { ctx.set('started', true); })
20
+ .waitForEvent('user.signup', { timeout: 1000 })
21
+ .step('end', async (ctx) => { ctx.set('ended', true); })
22
+ .build();
23
+
24
+ const execution = await workflow.execute({});
25
+
26
+ // Start
27
+ await vi.advanceTimersByTimeAsync(10);
28
+
29
+ // Check intermediate state if possible (not easily exposed yet)
30
+
31
+ // Wait for timeout
32
+ await vi.advanceTimersByTimeAsync(1000);
33
+
34
+ const state = await workflow.getExecution(execution.id);
35
+ expect(state.status).toBe('completed');
36
+ expect(state.output.started).toBe(true);
37
+ expect(state.output.ended).toBe(true);
38
+ });
39
+ });
@@ -0,0 +1,132 @@
1
+ export interface WorkflowContext<T = any> {
2
+ workflowId: string;
3
+ executionId: string;
4
+ input: T;
5
+ state: Record<string, any>;
6
+ metadata: Record<string, any>;
7
+
8
+ set(key: string, value: any): void;
9
+ get(key: string): any;
10
+ sleep(ms: number): Promise<void>;
11
+ waitForEvent(event: string, timeout?: number): Promise<any>;
12
+ }
13
+
14
+ export interface Workflow<T = any> {
15
+ id: string;
16
+ name: string;
17
+
18
+ execute(input: T): Promise<WorkflowExecution>;
19
+ getExecution(executionId: string): Promise<WorkflowExecution>;
20
+ listExecutions(options?: ListOptions): Promise<WorkflowExecution[]>;
21
+
22
+ cancelExecution(executionId: string): Promise<void>;
23
+ retryExecution(executionId: string): Promise<WorkflowExecution>;
24
+
25
+ getExecutionHistory(executionId: string): Promise<WorkflowEvent[]>;
26
+ getMetrics(): Promise<WorkflowMetrics>;
27
+ }
28
+
29
+ export interface WorkflowExecution {
30
+ id: string;
31
+ workflowId: string;
32
+ status: ExecutionStatus;
33
+ input: any;
34
+ output?: any;
35
+ error?: any;
36
+ state?: Record<string, any>;
37
+ startedAt: number;
38
+ completedAt?: number;
39
+ createdAt?: number;
40
+ updatedAt?: number;
41
+ }
42
+
43
+ export type ExecutionStatus =
44
+ | "pending"
45
+ | "running"
46
+ | "completed"
47
+ | "failed"
48
+ | "cancelled";
49
+
50
+ export interface WorkflowEvent {
51
+ timestamp: number;
52
+ type: string;
53
+ step?: string;
54
+ data?: any;
55
+ }
56
+
57
+ export interface WorkflowMetrics {
58
+ totalExecutions: number;
59
+ successRate: number;
60
+ avgDuration: number;
61
+ }
62
+
63
+ export interface ListOptions {
64
+ status?: ExecutionStatus;
65
+ limit?: number;
66
+ offset?: number;
67
+ }
68
+
69
+ export type StepHandler<T, R> = (
70
+ ctx: WorkflowContext<T>,
71
+ ...args: any[]
72
+ ) => Promise<R>;
73
+ export type ErrorHandler<T> = (
74
+ ctx: WorkflowContext<T>,
75
+ error: Error
76
+ ) => Promise<void>;
77
+ export type CompensateHandler<T> = (ctx: WorkflowContext<T>) => Promise<void>;
78
+
79
+ export interface BranchOptions<T> {
80
+ condition: (ctx: WorkflowContext<T>) => boolean | Promise<boolean>;
81
+ then: WorkflowBuilder<T>;
82
+ else?: WorkflowBuilder<T>;
83
+ }
84
+
85
+ export interface SagaDefinition<T> {
86
+ execute: StepHandler<T, any>;
87
+ compensate: CompensateHandler<T>;
88
+ }
89
+
90
+ export interface ApprovalOptions<T> {
91
+ timeout: number;
92
+ data?: (ctx: WorkflowContext<T>) => any;
93
+ onTimeout?: (ctx: WorkflowContext<T>) => Promise<void>;
94
+ }
95
+
96
+ export interface EventOptions {
97
+ timeout?: number;
98
+ }
99
+
100
+ export type Duration = {
101
+ ms?: number;
102
+ seconds?: number;
103
+ minutes?: number;
104
+ hours?: number;
105
+ days?: number;
106
+ };
107
+
108
+ export interface WorkflowBuilder<T = any> {
109
+ input<I>(): WorkflowBuilder<I>;
110
+
111
+ step<R>(name: string, handler: StepHandler<T, R>): WorkflowBuilder<T>;
112
+ parallel(steps: StepHandler<T, any>[]): WorkflowBuilder<T>;
113
+ branch(options: BranchOptions<T>): WorkflowBuilder<T>;
114
+
115
+ saga(name: string, saga: SagaDefinition<T>): WorkflowBuilder<T>;
116
+
117
+ delay(duration: Duration): WorkflowBuilder<T>;
118
+ delayUntil(
119
+ condition: (ctx: WorkflowContext<T>) => number
120
+ ): WorkflowBuilder<T>;
121
+
122
+ waitForApproval(
123
+ approver: string,
124
+ options: ApprovalOptions<T>
125
+ ): WorkflowBuilder<T>;
126
+ waitForEvent(event: string, options?: EventOptions): WorkflowBuilder<T>;
127
+
128
+ onError(step: string, handler: ErrorHandler<T>): WorkflowBuilder<T>;
129
+ compensate(step: string, handler: CompensateHandler<T>): WorkflowBuilder<T>;
130
+
131
+ build(): Workflow<T>;
132
+ }
@@ -0,0 +1,55 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import { createFlow } from '../core/flow-fn.js';
3
+
4
+ describe('Workflow Engine', () => {
5
+ let flow;
6
+
7
+ beforeEach(() => {
8
+ flow = createFlow({ adapter: 'memory' });
9
+ });
10
+
11
+ afterEach(async () => {
12
+ await flow.close();
13
+ });
14
+
15
+ it('should execute branching logic (true)', async () => {
16
+ const workflow = flow.workflow('branch-test')
17
+ .input<{ value: number }>()
18
+ .branch({
19
+ condition: (ctx) => ctx.input.value > 10,
20
+ then: flow.workflow('then-branch').step('high', async (ctx) => { ctx.set('result', 'high'); }),
21
+ else: flow.workflow('else-branch').step('low', async (ctx) => { ctx.set('result', 'low'); })
22
+ })
23
+ .build();
24
+
25
+ const execution = await workflow.execute({ value: 20 });
26
+
27
+ await new Promise(r => setTimeout(r, 50));
28
+
29
+ const state = await workflow.getExecution(execution.id);
30
+ expect(state.status).toBe('completed');
31
+ expect(state.output.result).toBe('high');
32
+ });
33
+
34
+ it('should execute saga compensation on failure', async () => {
35
+ let compensationCalled = false;
36
+
37
+ const workflow = flow.workflow('saga-test')
38
+ .saga('step1', {
39
+ execute: async (ctx) => { ctx.set('step1', 'done'); },
40
+ compensate: async (ctx) => { compensationCalled = true; }
41
+ })
42
+ .step('fail-step', async (ctx) => {
43
+ throw new Error('Boom');
44
+ })
45
+ .build();
46
+
47
+ const execution = await workflow.execute({});
48
+
49
+ await new Promise(r => setTimeout(r, 50));
50
+
51
+ const state = await workflow.getExecution(execution.id);
52
+ expect(state.status).toBe('failed');
53
+ expect(compensationCalled).toBe(true);
54
+ });
55
+ });
@@ -0,0 +1,422 @@
1
+ import {
2
+ Workflow,
3
+ WorkflowBuilder,
4
+ WorkflowContext,
5
+ WorkflowExecution,
6
+ StepHandler,
7
+ ErrorHandler,
8
+ CompensateHandler,
9
+ BranchOptions,
10
+ SagaDefinition,
11
+ ApprovalOptions,
12
+ EventOptions,
13
+ Duration,
14
+ WorkflowEvent,
15
+ WorkflowMetrics,
16
+ ListOptions,
17
+ } from "./types.js";
18
+ import { v4 as uuidv4 } from "uuid";
19
+ import { FlowAdapter } from "../adapters/base.js";
20
+ import {
21
+ MemoryWorkflowStorage,
22
+ WorkflowStorage,
23
+ } from "../storage/workflow-storage.js";
24
+ import { MemoryEventLog, EventLog } from "../storage/event-log.js";
25
+
26
+ type StepDefinition = {
27
+ type:
28
+ | "step"
29
+ | "parallel"
30
+ | "branch"
31
+ | "saga"
32
+ | "delay"
33
+ | "delayUntil"
34
+ | "wait";
35
+ name?: string;
36
+ handler?: Function;
37
+ compensate?: Function;
38
+ options?: any;
39
+ steps?: StepDefinition[]; // for parallel/branch-then
40
+ elseSteps?: StepDefinition[]; // for branch-else
41
+ condition?: (
42
+ ctx: WorkflowContext<any>
43
+ ) => boolean | Promise<boolean> | number;
44
+ };
45
+
46
+ export class WorkflowBuilderImpl<T = any> implements WorkflowBuilder<T> {
47
+ public steps: StepDefinition[] = [];
48
+ private name: string;
49
+ private adapter: FlowAdapter;
50
+
51
+ constructor(name: string, adapter: FlowAdapter) {
52
+ this.name = name;
53
+ this.adapter = adapter;
54
+ }
55
+
56
+ input<I>(): WorkflowBuilder<I> {
57
+ return this as unknown as WorkflowBuilder<I>;
58
+ }
59
+
60
+ step<R>(name: string, handler: StepHandler<T, R>): WorkflowBuilder<T> {
61
+ this.steps.push({ type: "step", name, handler });
62
+ return this;
63
+ }
64
+
65
+ parallel(steps: StepHandler<T, any>[]): WorkflowBuilder<T> {
66
+ this.steps.push({
67
+ type: "parallel",
68
+ steps: steps.map((h, i) => ({
69
+ type: "step",
70
+ name: `parallel-${i}`,
71
+ handler: h,
72
+ })),
73
+ });
74
+ return this;
75
+ }
76
+
77
+ branch(options: BranchOptions<T>): WorkflowBuilder<T> {
78
+ const thenSteps = (options.then as unknown as WorkflowBuilderImpl<T>).steps;
79
+ const elseSteps = options.else
80
+ ? (options.else as unknown as WorkflowBuilderImpl<T>).steps
81
+ : undefined;
82
+
83
+ this.steps.push({
84
+ type: "branch",
85
+ condition: options.condition,
86
+ steps: thenSteps,
87
+ elseSteps: elseSteps,
88
+ });
89
+ return this;
90
+ }
91
+
92
+ saga(name: string, saga: SagaDefinition<T>): WorkflowBuilder<T> {
93
+ this.steps.push({
94
+ type: "saga",
95
+ name,
96
+ handler: saga.execute,
97
+ compensate: saga.compensate,
98
+ });
99
+ return this;
100
+ }
101
+
102
+ delay(duration: Duration): WorkflowBuilder<T> {
103
+ this.steps.push({ type: "delay", options: duration });
104
+ return this;
105
+ }
106
+
107
+ delayUntil(
108
+ condition: (ctx: WorkflowContext<T>) => number
109
+ ): WorkflowBuilder<T> {
110
+ this.steps.push({ type: "delayUntil", condition });
111
+ return this;
112
+ }
113
+
114
+ waitForApproval(
115
+ approver: string,
116
+ options: ApprovalOptions<T>
117
+ ): WorkflowBuilder<T> {
118
+ this.steps.push({
119
+ type: "wait",
120
+ name: "approval",
121
+ options: { approver, ...options },
122
+ });
123
+ return this;
124
+ }
125
+
126
+ waitForEvent(event: string, options?: EventOptions): WorkflowBuilder<T> {
127
+ this.steps.push({
128
+ type: "wait",
129
+ name: "event",
130
+ options: { event, ...options },
131
+ });
132
+ return this;
133
+ }
134
+
135
+ onError(step: string, handler: ErrorHandler<T>): WorkflowBuilder<T> {
136
+ const s = this.steps.find((s) => s.name === step);
137
+ if (s) {
138
+ // Attach error handler
139
+ }
140
+ return this;
141
+ }
142
+
143
+ compensate(step: string, handler: CompensateHandler<T>): WorkflowBuilder<T> {
144
+ const s = this.steps.find((s) => s.name === step);
145
+ if (s) {
146
+ s.compensate = handler;
147
+ }
148
+ return this;
149
+ }
150
+
151
+ build(): Workflow<T> {
152
+ return new WorkflowImpl(this.name, this.steps, this.adapter);
153
+ }
154
+ }
155
+
156
+ export class WorkflowImpl<T = any> implements Workflow<T> {
157
+ id: string;
158
+ name: string;
159
+ private steps: StepDefinition[];
160
+ private adapter: FlowAdapter;
161
+ private storage: WorkflowStorage;
162
+ private eventLog: EventLog;
163
+ private executionCount = 0;
164
+ private successCount = 0;
165
+ private totalDuration = 0;
166
+
167
+ constructor(name: string, steps: StepDefinition[], adapter: FlowAdapter) {
168
+ this.id = uuidv4();
169
+ this.name = name;
170
+ this.steps = steps;
171
+ this.adapter = adapter;
172
+ this.storage = new MemoryWorkflowStorage();
173
+ this.eventLog = new MemoryEventLog();
174
+ }
175
+
176
+ async execute(input: T): Promise<WorkflowExecution> {
177
+ const executionId = uuidv4();
178
+ const execution: WorkflowExecution = {
179
+ id: executionId,
180
+ workflowId: this.id,
181
+ status: "running",
182
+ input,
183
+ state: {},
184
+ startedAt: Date.now(),
185
+ createdAt: Date.now(),
186
+ updatedAt: Date.now(),
187
+ };
188
+
189
+ // Save initial state
190
+ await this.storage.save(this.id, execution);
191
+ await this.logEvent(executionId, "execution.started", { input });
192
+
193
+ this.runExecution(execution).catch(console.error);
194
+
195
+ return execution;
196
+ }
197
+
198
+ private async runExecution(execution: WorkflowExecution) {
199
+ const context: WorkflowContext<T> = {
200
+ workflowId: this.id,
201
+ executionId: execution.id,
202
+ input: execution.input,
203
+ state: execution.state || {},
204
+ metadata: {},
205
+ set: (k, v) => {
206
+ context.state[k] = v;
207
+ this.storage
208
+ .updateState(execution.id, context.state)
209
+ .catch(console.error);
210
+ },
211
+ get: (k) => context.state[k],
212
+ sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
213
+ waitForEvent: async (event, timeout) => {
214
+ // Simplified wait: sleep for now
215
+ // In real system, this suspends workflow until external signal
216
+ await new Promise((r) => setTimeout(r, timeout || 100));
217
+ },
218
+ };
219
+
220
+ const executedSagas: StepDefinition[] = [];
221
+
222
+ try {
223
+ await this.executeSteps(this.steps, context, executedSagas);
224
+ execution.status = "completed";
225
+ execution.output = context.state;
226
+ execution.completedAt = Date.now();
227
+
228
+ await this.logEvent(execution.id, "execution.completed", {
229
+ output: execution.output,
230
+ });
231
+ this.executionCount++;
232
+ this.successCount++;
233
+ this.totalDuration += execution.completedAt - execution.startedAt;
234
+ } catch (error) {
235
+ execution.status = "failed";
236
+ execution.error = error;
237
+ execution.completedAt = Date.now();
238
+
239
+ await this.logEvent(execution.id, "execution.failed", {
240
+ error: (error as Error).message,
241
+ });
242
+ await this.compensate(executedSagas, context);
243
+ this.executionCount++;
244
+ this.totalDuration += execution.completedAt - execution.startedAt;
245
+ }
246
+
247
+ // Update storage with final state
248
+ await this.storage.save(this.id, execution);
249
+ await this.adapter.saveWorkflowState(execution.id, execution);
250
+ }
251
+
252
+ private async logEvent(
253
+ executionId: string,
254
+ type: string,
255
+ data: any
256
+ ): Promise<void> {
257
+ await this.eventLog.append({
258
+ type,
259
+ aggregateId: executionId,
260
+ aggregateType: "workflow_execution",
261
+ data,
262
+ });
263
+ }
264
+
265
+ private async executeSteps(
266
+ steps: StepDefinition[],
267
+ context: WorkflowContext<T>,
268
+ executedSagas: StepDefinition[]
269
+ ) {
270
+ for (const step of steps) {
271
+ if (step.type === "step" && step.handler) {
272
+ await this.logEvent(context.executionId, "step.started", {
273
+ step: step.name,
274
+ });
275
+ await step.handler(context);
276
+ await this.logEvent(context.executionId, "step.completed", {
277
+ step: step.name,
278
+ });
279
+ } else if (step.type === "saga" && step.handler) {
280
+ await this.logEvent(context.executionId, "saga.started", {
281
+ step: step.name,
282
+ });
283
+ await step.handler(context);
284
+ executedSagas.push(step);
285
+ await this.logEvent(context.executionId, "saga.completed", {
286
+ step: step.name,
287
+ });
288
+ } else if (step.type === "parallel" && step.steps) {
289
+ await this.logEvent(context.executionId, "parallel.started", {
290
+ count: step.steps.length,
291
+ });
292
+ await Promise.all(
293
+ step.steps.map((s) =>
294
+ s.handler ? s.handler(context) : Promise.resolve()
295
+ )
296
+ );
297
+ await this.logEvent(context.executionId, "parallel.completed", {});
298
+ } else if (step.type === "branch" && step.condition) {
299
+ const conditionMet = await (step.condition as any)(context);
300
+ if (conditionMet && step.steps) {
301
+ await this.executeSteps(step.steps, context, executedSagas);
302
+ } else if (!conditionMet && step.elseSteps) {
303
+ await this.executeSteps(step.elseSteps, context, executedSagas);
304
+ }
305
+ } else if (step.type === "delay") {
306
+ const ms = step.options?.ms || 0;
307
+ await new Promise((r) => setTimeout(r, ms));
308
+ } else if (step.type === "delayUntil" && step.condition) {
309
+ const targetTime = await (step.condition as any)(context);
310
+ if (typeof targetTime === "number") {
311
+ const now = Date.now();
312
+ const delay = Math.max(0, targetTime - now);
313
+ if (delay > 0) {
314
+ // For long delays, we should suspend execution.
315
+ // Here we just sleep.
316
+ await new Promise((r) => setTimeout(r, delay));
317
+ }
318
+ }
319
+ } else if (step.type === "wait") {
320
+ // waitForApproval / waitForEvent
321
+ // Need a mechanism to suspend/resume
322
+ // Currently implementing as simple sleep/mock
323
+ if (step.options?.timeout) {
324
+ await new Promise((r) => setTimeout(r, step.options.timeout));
325
+ }
326
+ }
327
+ }
328
+ }
329
+
330
+ private async compensate(
331
+ executedSagas: StepDefinition[],
332
+ context: WorkflowContext<T>
333
+ ) {
334
+ for (let i = executedSagas.length - 1; i >= 0; i--) {
335
+ const step = executedSagas[i];
336
+ if (step.compensate) {
337
+ try {
338
+ await step.compensate(context);
339
+ } catch (err) {
340
+ console.error(`Compensation failed for step ${step.name}`, err);
341
+ }
342
+ }
343
+ }
344
+ }
345
+
346
+ async getExecution(executionId: string): Promise<WorkflowExecution> {
347
+ const execution = await this.storage.get(executionId);
348
+ if (!execution) {
349
+ // Fallback to adapter
350
+ const adapterExecution =
351
+ await this.adapter.loadWorkflowState(executionId);
352
+ if (!adapterExecution) {
353
+ throw new Error(`Execution ${executionId} not found`);
354
+ }
355
+ return adapterExecution;
356
+ }
357
+ return execution;
358
+ }
359
+
360
+ async listExecutions(options?: ListOptions): Promise<WorkflowExecution[]> {
361
+ return this.storage.list(this.id, options);
362
+ }
363
+
364
+ async cancelExecution(executionId: string): Promise<void> {
365
+ const execution = await this.storage.get(executionId);
366
+ if (!execution) {
367
+ throw new Error(`Execution ${executionId} not found`);
368
+ }
369
+
370
+ if (
371
+ execution.status === "completed" ||
372
+ execution.status === "failed" ||
373
+ execution.status === "cancelled"
374
+ ) {
375
+ throw new Error(`Cannot cancel execution in ${execution.status} state`);
376
+ }
377
+
378
+ await this.storage.updateStatus(executionId, "cancelled");
379
+ await this.logEvent(executionId, "execution.cancelled", {});
380
+ }
381
+
382
+ async retryExecution(executionId: string): Promise<WorkflowExecution> {
383
+ const execution = await this.storage.get(executionId);
384
+ if (!execution) {
385
+ throw new Error(`Execution ${executionId} not found`);
386
+ }
387
+
388
+ if (execution.status !== "failed") {
389
+ throw new Error(
390
+ `Can only retry failed executions, current status: ${execution.status}`
391
+ );
392
+ }
393
+
394
+ // Create new execution with same input
395
+ return this.execute(execution.input);
396
+ }
397
+
398
+ async getExecutionHistory(executionId: string): Promise<WorkflowEvent[]> {
399
+ const events = await this.eventLog.getAggregateEvents(executionId);
400
+ return events.map((e) => ({
401
+ timestamp: e.timestamp,
402
+ type: e.type,
403
+ step: e.data.step,
404
+ data: e.data,
405
+ }));
406
+ }
407
+
408
+ async getMetrics(): Promise<WorkflowMetrics> {
409
+ const successRate =
410
+ this.executionCount > 0
411
+ ? (this.successCount / this.executionCount) * 100
412
+ : 0;
413
+ const avgDuration =
414
+ this.executionCount > 0 ? this.totalDuration / this.executionCount : 0;
415
+
416
+ return {
417
+ totalExecutions: this.executionCount,
418
+ successRate,
419
+ avgDuration,
420
+ };
421
+ }
422
+ }