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,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
@@ -0,0 +1,10 @@
1
+ import { defineConfig } from 'tsup';
2
+
3
+ export default defineConfig({
4
+ entry: ['src/index.ts'],
5
+ format: ['cjs', 'esm'],
6
+ dts: true,
7
+ splitting: false,
8
+ sourcemap: true,
9
+ clean: true,
10
+ });
@@ -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
+ });