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.
- package/dist/index.d.mts +1305 -0
- package/dist/index.d.ts +1305 -0
- package/dist/index.js +3180 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +3088 -0
- package/dist/index.mjs.map +1 -0
- package/docs/API.md +801 -0
- package/docs/USAGE.md +619 -0
- package/package.json +75 -0
- package/src/adapters/base.ts +46 -0
- package/src/adapters/memory.ts +183 -0
- package/src/adapters/postgres/index.ts +383 -0
- package/src/adapters/postgres/postgres.test.ts +100 -0
- package/src/adapters/postgres/schema.ts +110 -0
- package/src/adapters/redis.test.ts +124 -0
- package/src/adapters/redis.ts +331 -0
- package/src/core/flow-fn.test.ts +70 -0
- package/src/core/flow-fn.ts +198 -0
- package/src/core/metrics.ts +198 -0
- package/src/core/scheduler.test.ts +80 -0
- package/src/core/scheduler.ts +154 -0
- package/src/index.ts +57 -0
- package/src/monitoring/health.ts +261 -0
- package/src/patterns/backoff.ts +30 -0
- package/src/patterns/batching.ts +248 -0
- package/src/patterns/circuit-breaker.test.ts +52 -0
- package/src/patterns/circuit-breaker.ts +52 -0
- package/src/patterns/priority.ts +146 -0
- package/src/patterns/rate-limit.ts +290 -0
- package/src/patterns/retry.test.ts +62 -0
- package/src/queue/batch.test.ts +35 -0
- package/src/queue/dependencies.test.ts +33 -0
- package/src/queue/dlq.ts +222 -0
- package/src/queue/job.ts +67 -0
- package/src/queue/queue.ts +243 -0
- package/src/queue/types.ts +153 -0
- package/src/queue/worker.ts +66 -0
- package/src/storage/event-log.ts +205 -0
- package/src/storage/job-storage.ts +206 -0
- package/src/storage/workflow-storage.ts +182 -0
- package/src/stream/stream.ts +194 -0
- package/src/stream/types.ts +81 -0
- package/src/utils/hashing.ts +29 -0
- package/src/utils/id-generator.ts +109 -0
- package/src/utils/serialization.ts +142 -0
- package/src/utils/time.ts +167 -0
- package/src/workflow/advanced.test.ts +43 -0
- package/src/workflow/events.test.ts +39 -0
- package/src/workflow/types.ts +132 -0
- package/src/workflow/workflow.test.ts +55 -0
- package/src/workflow/workflow.ts +422 -0
- package/tests/dlq.test.ts +205 -0
- package/tests/health.test.ts +228 -0
- package/tests/integration.test.ts +253 -0
- package/tests/stream.test.ts +233 -0
- package/tests/workflow.test.ts +286 -0
- package/tsconfig.json +17 -0
- package/tsup.config.ts +10 -0
- package/vitest.config.ts +15 -0
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for Stream System
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect, beforeEach } from "vitest";
|
|
6
|
+
import { createFlow } from "../src/core/flow-fn.js";
|
|
7
|
+
import { FlowFn } from "../src/core/flow-fn.js";
|
|
8
|
+
|
|
9
|
+
describe("Stream System", () => {
|
|
10
|
+
let flow: FlowFn;
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
flow = createFlow({ adapter: "memory" });
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
describe("Publishing and Subscribing", () => {
|
|
17
|
+
it("should publish and receive messages", async () => {
|
|
18
|
+
const stream = flow.stream("test-stream");
|
|
19
|
+
const received: any[] = [];
|
|
20
|
+
|
|
21
|
+
await stream.subscribe(async (msg) => {
|
|
22
|
+
received.push(msg.data);
|
|
23
|
+
await msg.ack();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
await stream.publish({ value: "hello" });
|
|
27
|
+
await stream.publish({ value: "world" });
|
|
28
|
+
|
|
29
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
30
|
+
|
|
31
|
+
expect(received.length).toBe(2);
|
|
32
|
+
expect(received[0].value).toBe("hello");
|
|
33
|
+
expect(received[1].value).toBe("world");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("should publish batch messages", async () => {
|
|
37
|
+
const stream = flow.stream("batch-stream");
|
|
38
|
+
const received: any[] = [];
|
|
39
|
+
|
|
40
|
+
await stream.subscribe(async (msg) => {
|
|
41
|
+
received.push(msg.data);
|
|
42
|
+
await msg.ack();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
await stream.publishBatch([
|
|
46
|
+
{ data: { id: 1 } },
|
|
47
|
+
{ data: { id: 2 } },
|
|
48
|
+
{ data: { id: 3 } },
|
|
49
|
+
]);
|
|
50
|
+
|
|
51
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
52
|
+
|
|
53
|
+
expect(received.length).toBe(3);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe("Stream Trimming", () => {
|
|
58
|
+
it("should trim by max length", async () => {
|
|
59
|
+
const stream = flow.stream("trim-stream");
|
|
60
|
+
|
|
61
|
+
// Publish more messages than max
|
|
62
|
+
for (let i = 0; i < 10; i++) {
|
|
63
|
+
await stream.publish({ index: i });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const trimmed = await stream.trim({ maxLength: 5 });
|
|
67
|
+
|
|
68
|
+
expect(trimmed).toBe(5); // Should have removed 5 messages
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("should trim by max age", async () => {
|
|
72
|
+
const stream = flow.stream("age-stream");
|
|
73
|
+
|
|
74
|
+
await stream.publish({ old: true });
|
|
75
|
+
|
|
76
|
+
// Wait a bit
|
|
77
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
78
|
+
|
|
79
|
+
const trimmed = await stream.trim({ maxAgeSeconds: 0.05 }); // 50ms
|
|
80
|
+
|
|
81
|
+
expect(trimmed).toBeGreaterThan(0);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("should auto-trim on maxLength option", async () => {
|
|
85
|
+
const stream = flow.stream("auto-trim", { maxLength: 3 });
|
|
86
|
+
|
|
87
|
+
await stream.publish({ id: 1 });
|
|
88
|
+
await stream.publish({ id: 2 });
|
|
89
|
+
await stream.publish({ id: 3 });
|
|
90
|
+
await stream.publish({ id: 4 }); // Should trigger trim
|
|
91
|
+
|
|
92
|
+
const count = stream.getMessageCount();
|
|
93
|
+
expect(count).toBeLessThanOrEqual(3);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe("Message Retrieval", () => {
|
|
98
|
+
it("should get messages by range", async () => {
|
|
99
|
+
const stream = flow.stream("retrieval-stream");
|
|
100
|
+
|
|
101
|
+
const ids = [];
|
|
102
|
+
ids.push(await stream.publish({ index: 1 }));
|
|
103
|
+
ids.push(await stream.publish({ index: 2 }));
|
|
104
|
+
ids.push(await stream.publish({ index: 3 }));
|
|
105
|
+
|
|
106
|
+
const messages = await stream.getMessages(ids[0], ids[2]);
|
|
107
|
+
|
|
108
|
+
expect(messages.length).toBeGreaterThan(0);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("should limit message count", async () => {
|
|
112
|
+
const stream = flow.stream("limit-stream");
|
|
113
|
+
|
|
114
|
+
await stream.publish({ id: 1 });
|
|
115
|
+
await stream.publish({ id: 2 });
|
|
116
|
+
await stream.publish({ id: 3 });
|
|
117
|
+
|
|
118
|
+
const firstId = (await stream.getMessages("0", "z", 1))[0]?.id || "0";
|
|
119
|
+
const messages = await stream.getMessages(firstId, "z", 2);
|
|
120
|
+
|
|
121
|
+
expect(messages.length).toBeLessThanOrEqual(2);
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
describe("Consumer Groups", () => {
|
|
126
|
+
it("should create consumer with group", async () => {
|
|
127
|
+
const stream = flow.stream("consumer-stream");
|
|
128
|
+
const received: any[] = [];
|
|
129
|
+
|
|
130
|
+
const consumer = stream.createConsumer("consumer-1", {
|
|
131
|
+
groupId: "processors",
|
|
132
|
+
fromBeginning: false,
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
await consumer.subscribe(async (msg) => {
|
|
136
|
+
received.push(msg.data);
|
|
137
|
+
await msg.ack();
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
await stream.publish({ value: "test" });
|
|
141
|
+
|
|
142
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
143
|
+
|
|
144
|
+
expect(received.length).toBeGreaterThan(0);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("should replay from beginning", async () => {
|
|
148
|
+
const stream = flow.stream("replay-stream");
|
|
149
|
+
|
|
150
|
+
// Publish before subscribing
|
|
151
|
+
await stream.publish({ id: 1 });
|
|
152
|
+
await stream.publish({ id: 2 });
|
|
153
|
+
|
|
154
|
+
const received: any[] = [];
|
|
155
|
+
const consumer = stream.createConsumer("replayer", {
|
|
156
|
+
groupId: "group1",
|
|
157
|
+
fromBeginning: true,
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
await consumer.subscribe(async (msg) => {
|
|
161
|
+
received.push(msg.data);
|
|
162
|
+
await msg.ack();
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
166
|
+
|
|
167
|
+
expect(received.length).toBe(2);
|
|
168
|
+
expect(received[0].id).toBe(1);
|
|
169
|
+
expect(received[1].id).toBe(2);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it("should pause and resume consumer", async () => {
|
|
173
|
+
const stream = flow.stream("pause-stream");
|
|
174
|
+
const received: any[] = [];
|
|
175
|
+
|
|
176
|
+
const consumer = stream.createConsumer("pausable", {
|
|
177
|
+
groupId: "group1",
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
await consumer.subscribe(async (msg) => {
|
|
181
|
+
received.push(msg.data);
|
|
182
|
+
await msg.ack();
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
await stream.publish({ id: 1 });
|
|
186
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
187
|
+
|
|
188
|
+
await consumer.pause();
|
|
189
|
+
await stream.publish({ id: 2 }); // Should not be received
|
|
190
|
+
|
|
191
|
+
await consumer.resume();
|
|
192
|
+
await stream.publish({ id: 3 });
|
|
193
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
194
|
+
|
|
195
|
+
// Should have received 1 and 3, but not 2
|
|
196
|
+
expect(received.length).toBeGreaterThan(0);
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
describe("Stream Replay", () => {
|
|
201
|
+
it("should replay messages from timestamp", async () => {
|
|
202
|
+
const stream = flow.stream("replay-ts-stream");
|
|
203
|
+
|
|
204
|
+
const startTime = Date.now();
|
|
205
|
+
|
|
206
|
+
await stream.publish({ id: 1 });
|
|
207
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
208
|
+
await stream.publish({ id: 2 });
|
|
209
|
+
|
|
210
|
+
const received: any[] = [];
|
|
211
|
+
const count = await stream.replay(startTime, async (msg) => {
|
|
212
|
+
received.push(msg.data);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
expect(count).toBe(2);
|
|
216
|
+
expect(received.length).toBe(2);
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
describe("Stream Info", () => {
|
|
221
|
+
it("should get stream info", async () => {
|
|
222
|
+
const stream = flow.stream("info-stream");
|
|
223
|
+
|
|
224
|
+
await stream.publish({ test: true });
|
|
225
|
+
await stream.publish({ test: true });
|
|
226
|
+
|
|
227
|
+
const info = await stream.getInfo();
|
|
228
|
+
|
|
229
|
+
expect(info.name).toBe("info-stream");
|
|
230
|
+
expect(info.length).toBeGreaterThanOrEqual(2);
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
});
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for Workflow System
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect, beforeEach } from "vitest";
|
|
6
|
+
import { createFlow } from "../src/core/flow-fn.js";
|
|
7
|
+
import { FlowFn } from "../src/core/flow-fn.js";
|
|
8
|
+
|
|
9
|
+
describe("Workflow System", () => {
|
|
10
|
+
let flow: FlowFn;
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
flow = createFlow({ adapter: "memory" });
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
describe("Workflow Execution", () => {
|
|
17
|
+
it("should execute sequential steps", async () => {
|
|
18
|
+
const steps: string[] = [];
|
|
19
|
+
|
|
20
|
+
const workflow = flow
|
|
21
|
+
.workflow("sequential")
|
|
22
|
+
.step("step1", async (ctx) => {
|
|
23
|
+
steps.push("step1");
|
|
24
|
+
ctx.set("result1", "done");
|
|
25
|
+
})
|
|
26
|
+
.step("step2", async (ctx) => {
|
|
27
|
+
steps.push("step2");
|
|
28
|
+
expect(ctx.get("result1")).toBe("done");
|
|
29
|
+
})
|
|
30
|
+
.step("step3", async (ctx) => {
|
|
31
|
+
steps.push("step3");
|
|
32
|
+
})
|
|
33
|
+
.build();
|
|
34
|
+
|
|
35
|
+
await workflow.execute({ input: "test" });
|
|
36
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
37
|
+
|
|
38
|
+
expect(steps).toEqual(["step1", "step2", "step3"]);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("should execute parallel steps", async () => {
|
|
42
|
+
const results: number[] = [];
|
|
43
|
+
|
|
44
|
+
const workflow = flow
|
|
45
|
+
.workflow("parallel")
|
|
46
|
+
.parallel([
|
|
47
|
+
async (ctx) => {
|
|
48
|
+
results.push(1);
|
|
49
|
+
},
|
|
50
|
+
async (ctx) => {
|
|
51
|
+
results.push(2);
|
|
52
|
+
},
|
|
53
|
+
async (ctx) => {
|
|
54
|
+
results.push(3);
|
|
55
|
+
},
|
|
56
|
+
])
|
|
57
|
+
.build();
|
|
58
|
+
|
|
59
|
+
await workflow.execute({});
|
|
60
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
61
|
+
|
|
62
|
+
expect(results.length).toBe(3);
|
|
63
|
+
expect(results).toContain(1);
|
|
64
|
+
expect(results).toContain(2);
|
|
65
|
+
expect(results).toContain(3);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe("Workflow State Management", () => {
|
|
70
|
+
it("should persist state across steps", async () => {
|
|
71
|
+
const workflow = flow
|
|
72
|
+
.workflow("state-test")
|
|
73
|
+
.step("set-values", async (ctx) => {
|
|
74
|
+
ctx.set("key1", "value1");
|
|
75
|
+
ctx.set("key2", 42);
|
|
76
|
+
})
|
|
77
|
+
.step("read-values", async (ctx) => {
|
|
78
|
+
expect(ctx.get("key1")).toBe("value1");
|
|
79
|
+
expect(ctx.get("key2")).toBe(42);
|
|
80
|
+
})
|
|
81
|
+
.build();
|
|
82
|
+
|
|
83
|
+
await workflow.execute({});
|
|
84
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("should track execution status", async () => {
|
|
88
|
+
const workflow = flow
|
|
89
|
+
.workflow("status-test")
|
|
90
|
+
.step("work", async (ctx) => {
|
|
91
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
92
|
+
})
|
|
93
|
+
.build();
|
|
94
|
+
|
|
95
|
+
const execution = await workflow.execute({ test: true });
|
|
96
|
+
|
|
97
|
+
expect(execution.status).toBe("running");
|
|
98
|
+
expect(execution.input).toEqual({ test: true });
|
|
99
|
+
|
|
100
|
+
await new Promise((r) => setTimeout(r, 150));
|
|
101
|
+
|
|
102
|
+
const result = await workflow.getExecution(execution.id);
|
|
103
|
+
expect(result.status).toBe("completed");
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
describe("Workflow Error Handling", () => {
|
|
108
|
+
it("should handle step failures", async () => {
|
|
109
|
+
const workflow = flow
|
|
110
|
+
.workflow("error-test")
|
|
111
|
+
.step("failing-step", async (ctx) => {
|
|
112
|
+
throw new Error("Step failed");
|
|
113
|
+
})
|
|
114
|
+
.build();
|
|
115
|
+
|
|
116
|
+
const execution = await workflow.execute({});
|
|
117
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
118
|
+
|
|
119
|
+
const result = await workflow.getExecution(execution.id);
|
|
120
|
+
expect(result.status).toBe("failed");
|
|
121
|
+
expect(result.error).toBeDefined();
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
describe("Workflow Listing and Filtering", () => {
|
|
126
|
+
it("should list executions", async () => {
|
|
127
|
+
const workflow = flow
|
|
128
|
+
.workflow("list-test")
|
|
129
|
+
.step("step1", async () => {})
|
|
130
|
+
.build();
|
|
131
|
+
|
|
132
|
+
await workflow.execute({ id: 1 });
|
|
133
|
+
await workflow.execute({ id: 2 });
|
|
134
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
135
|
+
|
|
136
|
+
const executions = await workflow.listExecutions();
|
|
137
|
+
expect(executions.length).toBeGreaterThanOrEqual(2);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("should filter executions by status", async () => {
|
|
141
|
+
const workflow = flow
|
|
142
|
+
.workflow("filter-test")
|
|
143
|
+
.step("work", async () => {
|
|
144
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
145
|
+
})
|
|
146
|
+
.build();
|
|
147
|
+
|
|
148
|
+
await workflow.execute({ id: 1 });
|
|
149
|
+
await workflow.execute({ id: 2 });
|
|
150
|
+
|
|
151
|
+
await new Promise((r) => setTimeout(r, 20)); // Some running
|
|
152
|
+
|
|
153
|
+
const running = await workflow.listExecutions({ status: "running" });
|
|
154
|
+
expect(running.length).toBeGreaterThanOrEqual(0);
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
describe("Workflow Cancel and Retry", () => {
|
|
159
|
+
it("should cancel running execution", async () => {
|
|
160
|
+
const workflow = flow
|
|
161
|
+
.workflow("cancel-test")
|
|
162
|
+
.step("long-step", async () => {
|
|
163
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
164
|
+
})
|
|
165
|
+
.build();
|
|
166
|
+
|
|
167
|
+
const execution = await workflow.execute({});
|
|
168
|
+
|
|
169
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
170
|
+
await workflow.cancelExecution(execution.id);
|
|
171
|
+
|
|
172
|
+
const result = await workflow.getExecution(execution.id);
|
|
173
|
+
expect(result.status).toBe("cancelled");
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("should retry failed execution", async () => {
|
|
177
|
+
let attempts = 0;
|
|
178
|
+
|
|
179
|
+
const workflow = flow
|
|
180
|
+
.workflow("retry-test")
|
|
181
|
+
.step("flaky-step", async () => {
|
|
182
|
+
attempts++;
|
|
183
|
+
if (attempts === 1) {
|
|
184
|
+
throw new Error("First attempt fails");
|
|
185
|
+
}
|
|
186
|
+
})
|
|
187
|
+
.build();
|
|
188
|
+
|
|
189
|
+
const execution = await workflow.execute({});
|
|
190
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
191
|
+
|
|
192
|
+
// First execution should fail
|
|
193
|
+
let result = await workflow.getExecution(execution.id);
|
|
194
|
+
expect(result.status).toBe("failed");
|
|
195
|
+
|
|
196
|
+
// Retry should create new execution
|
|
197
|
+
const retried = await workflow.retryExecution(execution.id);
|
|
198
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
199
|
+
|
|
200
|
+
result = await workflow.getExecution(retried.id);
|
|
201
|
+
expect(result.status).toBe("completed");
|
|
202
|
+
expect(attempts).toBe(2);
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
describe("Workflow History", () => {
|
|
207
|
+
it("should track execution history", async () => {
|
|
208
|
+
const workflow = flow
|
|
209
|
+
.workflow("history-test")
|
|
210
|
+
.step("step1", async () => {})
|
|
211
|
+
.step("step2", async () => {})
|
|
212
|
+
.build();
|
|
213
|
+
|
|
214
|
+
const execution = await workflow.execute({});
|
|
215
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
216
|
+
|
|
217
|
+
const history = await workflow.getExecutionHistory(execution.id);
|
|
218
|
+
|
|
219
|
+
expect(history.length).toBeGreaterThan(0);
|
|
220
|
+
|
|
221
|
+
const types = history.map((e) => e.type);
|
|
222
|
+
expect(types).toContain("execution.started");
|
|
223
|
+
expect(types).toContain("step.started");
|
|
224
|
+
expect(types).toContain("step.completed");
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
describe("Workflow Metrics", () => {
|
|
229
|
+
it("should collect workflow metrics", async () => {
|
|
230
|
+
const workflow = flow
|
|
231
|
+
.workflow("metrics-test")
|
|
232
|
+
.step("work", async () => {
|
|
233
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
234
|
+
})
|
|
235
|
+
.build();
|
|
236
|
+
|
|
237
|
+
await workflow.execute({ id: 1 });
|
|
238
|
+
await workflow.execute({ id: 2 });
|
|
239
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
240
|
+
|
|
241
|
+
const metrics = await workflow.getMetrics();
|
|
242
|
+
|
|
243
|
+
expect(metrics.totalExecutions).toBe(2);
|
|
244
|
+
expect(metrics.successRate).toBeGreaterThan(0);
|
|
245
|
+
expect(metrics.avgDuration).toBeGreaterThan(0);
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
describe("Conditional Branching", () => {
|
|
250
|
+
it("should execute conditional branches", async () => {
|
|
251
|
+
let branchTaken = "";
|
|
252
|
+
|
|
253
|
+
const thenBranch = flow
|
|
254
|
+
.workflow("then-branch")
|
|
255
|
+
.step("then-step", async () => {
|
|
256
|
+
branchTaken = "then";
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
const elseBranch = flow
|
|
260
|
+
.workflow("else-branch")
|
|
261
|
+
.step("else-step", async () => {
|
|
262
|
+
branchTaken = "else";
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
const workflow = flow
|
|
266
|
+
.workflow("branch-test")
|
|
267
|
+
.branch({
|
|
268
|
+
condition: (ctx) => ctx.input.value > 10,
|
|
269
|
+
then: thenBranch as any,
|
|
270
|
+
else: elseBranch as any,
|
|
271
|
+
})
|
|
272
|
+
.build();
|
|
273
|
+
|
|
274
|
+
await workflow.execute({ value: 15 });
|
|
275
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
276
|
+
|
|
277
|
+
expect(branchTaken).toBe("then");
|
|
278
|
+
|
|
279
|
+
branchTaken = "";
|
|
280
|
+
await workflow.execute({ value: 5 });
|
|
281
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
282
|
+
|
|
283
|
+
expect(branchTaken).toBe("else");
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
});
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"lib": ["ES2020", "DOM"],
|
|
7
|
+
"declaration": true,
|
|
8
|
+
"sourceMap": true,
|
|
9
|
+
"strict": true,
|
|
10
|
+
"esModuleInterop": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"forceConsistentCasingInFileNames": true,
|
|
13
|
+
"outDir": "dist"
|
|
14
|
+
},
|
|
15
|
+
"include": ["src/**/*"],
|
|
16
|
+
"exclude": ["node_modules", "dist", "**/*.test.ts"]
|
|
17
|
+
}
|
package/tsup.config.ts
ADDED
package/vitest.config.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { defineConfig } from 'vitest/config';
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
test: {
|
|
5
|
+
globals: true,
|
|
6
|
+
environment: 'node',
|
|
7
|
+
include: ['src/**/*.test.ts'],
|
|
8
|
+
coverage: {
|
|
9
|
+
provider: 'v8',
|
|
10
|
+
reporter: ['text', 'json', 'html'],
|
|
11
|
+
include: ['src/**/*.ts'],
|
|
12
|
+
exclude: ['src/**/*.test.ts', 'src/index.ts', 'src/queue/types.ts', 'src/stream/types.ts', 'src/workflow/types.ts'],
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
});
|