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,66 @@
1
+ import { FlowFn } from '../core/flow-fn.js';
2
+ import { Queue, Job } from './types.js';
3
+ import EventEmitter from 'eventemitter3';
4
+
5
+ export interface WorkerOptions {
6
+ flow: FlowFn;
7
+ queues: string[];
8
+ concurrency?: number | Record<string, number>;
9
+ settings?: {
10
+ stalledInterval?: number;
11
+ maxStalledCount?: number;
12
+ };
13
+ }
14
+
15
+ export class Worker extends EventEmitter {
16
+ private flow: FlowFn;
17
+ private queueNames: string[];
18
+ private concurrency: number | Record<string, number>;
19
+ private queues: Queue[] = [];
20
+
21
+ constructor(options: WorkerOptions) {
22
+ super();
23
+ this.flow = options.flow;
24
+ this.queueNames = options.queues;
25
+ this.concurrency = options.concurrency || 1;
26
+ }
27
+
28
+ async run(): Promise<void> {
29
+ for (const name of this.queueNames) {
30
+ const queue = this.flow.queue(name);
31
+ this.queues.push(queue);
32
+
33
+ const qConcurrency = typeof this.concurrency === 'number'
34
+ ? this.concurrency
35
+ : (this.concurrency[name] || 1);
36
+
37
+ queue.on('active', (job) => this.emit('active', job));
38
+ queue.on('completed', (job, result) => this.emit('completed', job, result));
39
+ queue.on('failed', (job, err) => this.emit('failed', job, err));
40
+
41
+ // This is a bit simplified. In a real system, the worker would
42
+ // have its own processing loop rather than calling queue.process.
43
+ // But for this abstraction it works.
44
+ // We need a way to pass the handler to the worker.
45
+ }
46
+ this.emit('ready');
47
+ }
48
+
49
+ // Helper to register handlers
50
+ register(queueName: string, handler: (job: Job) => Promise<any>) {
51
+ const queue = this.flow.queue(queueName);
52
+ const qConcurrency = typeof this.concurrency === 'number'
53
+ ? this.concurrency
54
+ : (this.concurrency[queueName] || 1);
55
+ queue.process(qConcurrency, handler);
56
+ }
57
+
58
+ async close(): Promise<void> {
59
+ this.emit('closing');
60
+ await Promise.all(this.queues.map(q => q.close()));
61
+ }
62
+ }
63
+
64
+ export function createWorker(options: WorkerOptions): Worker {
65
+ return new Worker(options);
66
+ }
@@ -0,0 +1,205 @@
1
+ /**
2
+ * Event log for event sourcing pattern
3
+ */
4
+
5
+ export interface Event {
6
+ id: string;
7
+ type: string;
8
+ aggregateId: string;
9
+ aggregateType: string;
10
+ data: any;
11
+ metadata?: Record<string, any>;
12
+ timestamp: number;
13
+ version: number;
14
+ }
15
+
16
+ export interface EventFilter {
17
+ aggregateId?: string;
18
+ aggregateType?: string;
19
+ types?: string[];
20
+ fromVersion?: number;
21
+ toVersion?: number;
22
+ fromTimestamp?: number;
23
+ toTimestamp?: number;
24
+ limit?: number;
25
+ offset?: number;
26
+ }
27
+
28
+ export interface EventLog {
29
+ /**
30
+ * Append event to the log
31
+ */
32
+ append(event: Omit<Event, "id" | "timestamp" | "version">): Promise<Event>;
33
+
34
+ /**
35
+ * Get events by filter
36
+ */
37
+ getEvents(filter: EventFilter): Promise<Event[]>;
38
+
39
+ /**
40
+ * Get events for an aggregate
41
+ */
42
+ getAggregateEvents(
43
+ aggregateId: string,
44
+ fromVersion?: number
45
+ ): Promise<Event[]>;
46
+
47
+ /**
48
+ * Get latest version for an aggregate
49
+ */
50
+ getLatestVersion(aggregateId: string): Promise<number>;
51
+
52
+ /**
53
+ * Subscribe to event stream
54
+ */
55
+ subscribe(
56
+ handler: (event: Event) => void | Promise<void>,
57
+ filter?: EventFilter
58
+ ): () => void;
59
+ }
60
+
61
+ /**
62
+ * In-memory event log implementation
63
+ */
64
+ export class MemoryEventLog implements EventLog {
65
+ private events: Event[] = [];
66
+ private aggregateVersions: Map<string, number> = new Map();
67
+ private subscribers: Array<{
68
+ handler: (event: Event) => void | Promise<void>;
69
+ filter?: EventFilter;
70
+ }> = [];
71
+
72
+ async append(
73
+ event: Omit<Event, "id" | "timestamp" | "version">
74
+ ): Promise<Event> {
75
+ const version = (this.aggregateVersions.get(event.aggregateId) || 0) + 1;
76
+ this.aggregateVersions.set(event.aggregateId, version);
77
+
78
+ const fullEvent: Event = {
79
+ ...event,
80
+ id: `evt_${Date.now()}_${Math.random().toString(36).substring(7)}`,
81
+ timestamp: Date.now(),
82
+ version,
83
+ };
84
+
85
+ this.events.push(fullEvent);
86
+
87
+ // Notify subscribers
88
+ for (const subscriber of this.subscribers) {
89
+ if (this.matchesFilter(fullEvent, subscriber.filter)) {
90
+ Promise.resolve(subscriber.handler(fullEvent)).catch((err) =>
91
+ console.error("Event subscriber error:", err)
92
+ );
93
+ }
94
+ }
95
+
96
+ return fullEvent;
97
+ }
98
+
99
+ async getEvents(filter: EventFilter): Promise<Event[]> {
100
+ let results = [...this.events];
101
+
102
+ // Apply filters
103
+ if (filter.aggregateId) {
104
+ results = results.filter((e) => e.aggregateId === filter.aggregateId);
105
+ }
106
+
107
+ if (filter.aggregateType) {
108
+ results = results.filter((e) => e.aggregateType === filter.aggregateType);
109
+ }
110
+
111
+ if (filter.types && filter.types.length > 0) {
112
+ results = results.filter((e) => filter.types!.includes(e.type));
113
+ }
114
+
115
+ if (filter.fromVersion !== undefined) {
116
+ results = results.filter((e) => e.version >= filter.fromVersion!);
117
+ }
118
+
119
+ if (filter.toVersion !== undefined) {
120
+ results = results.filter((e) => e.version <= filter.toVersion!);
121
+ }
122
+
123
+ if (filter.fromTimestamp !== undefined) {
124
+ results = results.filter((e) => e.timestamp >= filter.fromTimestamp!);
125
+ }
126
+
127
+ if (filter.toTimestamp !== undefined) {
128
+ results = results.filter((e) => e.timestamp <= filter.toTimestamp!);
129
+ }
130
+
131
+ // Sort by version
132
+ results.sort((a, b) => a.version - b.version);
133
+
134
+ // Pagination
135
+ const offset = filter.offset || 0;
136
+ const limit = filter.limit || results.length;
137
+
138
+ return results.slice(offset, offset + limit);
139
+ }
140
+
141
+ async getAggregateEvents(
142
+ aggregateId: string,
143
+ fromVersion?: number
144
+ ): Promise<Event[]> {
145
+ return this.getEvents({
146
+ aggregateId,
147
+ fromVersion,
148
+ });
149
+ }
150
+
151
+ async getLatestVersion(aggregateId: string): Promise<number> {
152
+ return this.aggregateVersions.get(aggregateId) || 0;
153
+ }
154
+
155
+ subscribe(
156
+ handler: (event: Event) => void | Promise<void>,
157
+ filter?: EventFilter
158
+ ): () => void {
159
+ const subscriber = { handler, filter };
160
+ this.subscribers.push(subscriber);
161
+
162
+ // Return unsubscribe function
163
+ return () => {
164
+ const index = this.subscribers.indexOf(subscriber);
165
+ if (index > -1) {
166
+ this.subscribers.splice(index, 1);
167
+ }
168
+ };
169
+ }
170
+
171
+ private matchesFilter(event: Event, filter?: EventFilter): boolean {
172
+ if (!filter) return true;
173
+
174
+ if (filter.aggregateId && event.aggregateId !== filter.aggregateId)
175
+ return false;
176
+ if (filter.aggregateType && event.aggregateType !== filter.aggregateType)
177
+ return false;
178
+ if (filter.types && !filter.types.includes(event.type)) return false;
179
+ if (filter.fromVersion !== undefined && event.version < filter.fromVersion)
180
+ return false;
181
+ if (filter.toVersion !== undefined && event.version > filter.toVersion)
182
+ return false;
183
+ if (
184
+ filter.fromTimestamp !== undefined &&
185
+ event.timestamp < filter.fromTimestamp
186
+ )
187
+ return false;
188
+ if (
189
+ filter.toTimestamp !== undefined &&
190
+ event.timestamp > filter.toTimestamp
191
+ )
192
+ return false;
193
+
194
+ return true;
195
+ }
196
+
197
+ /**
198
+ * Clear all events
199
+ */
200
+ clear(): void {
201
+ this.events = [];
202
+ this.aggregateVersions.clear();
203
+ this.subscribers = [];
204
+ }
205
+ }
@@ -0,0 +1,206 @@
1
+ /**
2
+ * Storage abstraction for FlowFn job persistence
3
+ */
4
+
5
+ import { Job, JobStatus } from "../queue/types.js";
6
+
7
+ export interface JobStorage {
8
+ /**
9
+ * Save a job
10
+ */
11
+ save(queue: string, job: Job): Promise<void>;
12
+
13
+ /**
14
+ * Get a job by ID
15
+ */
16
+ get(queue: string, jobId: string): Promise<Job | null>;
17
+
18
+ /**
19
+ * Get jobs by status
20
+ */
21
+ getByStatus(queue: string, status: JobStatus): Promise<Job[]>;
22
+
23
+ /**
24
+ * Get all jobs for a queue
25
+ */
26
+ getAll(queue: string): Promise<Job[]>;
27
+
28
+ /**
29
+ * Update job status
30
+ */
31
+ updateStatus(queue: string, jobId: string, status: JobStatus): Promise<void>;
32
+
33
+ /**
34
+ * Update job data
35
+ */
36
+ update(queue: string, jobId: string, updates: Partial<Job>): Promise<void>;
37
+
38
+ /**
39
+ * Delete a job
40
+ */
41
+ delete(queue: string, jobId: string): Promise<void>;
42
+
43
+ /**
44
+ * Delete jobs older than grace period with given status
45
+ */
46
+ clean(queue: string, grace: number, status: JobStatus): Promise<number>;
47
+
48
+ /**
49
+ * Get job count by status
50
+ */
51
+ count(queue: string, status?: JobStatus): Promise<number>;
52
+
53
+ /**
54
+ * Check if job exists with deduplication key
55
+ */
56
+ existsByDeduplicationKey(queue: string, key: string): Promise<boolean>;
57
+ }
58
+
59
+ /**
60
+ * In-memory implementation of JobStorage
61
+ */
62
+ export class MemoryJobStorage implements JobStorage {
63
+ private jobs: Map<string, Job> = new Map();
64
+ private deduplicationKeys: Map<string, string> = new Map(); // key -> jobId
65
+
66
+ async save(queue: string, job: Job): Promise<void> {
67
+ const key = `${queue}:${job.id}`;
68
+ this.jobs.set(key, { ...job });
69
+
70
+ // Store deduplication key if present
71
+ if (job.opts.deduplicationKey) {
72
+ const dedupKey = `${queue}:${job.opts.deduplicationKey}`;
73
+ this.deduplicationKeys.set(dedupKey, job.id);
74
+ }
75
+ }
76
+
77
+ async get(queue: string, jobId: string): Promise<Job | null> {
78
+ const key = `${queue}:${jobId}`;
79
+ return this.jobs.get(key) || null;
80
+ }
81
+
82
+ async getByStatus(queue: string, status: JobStatus): Promise<Job[]> {
83
+ const result: Job[] = [];
84
+ const prefix = `${queue}:`;
85
+
86
+ for (const [key, job] of this.jobs.entries()) {
87
+ if (key.startsWith(prefix) && job.state === status) {
88
+ result.push(job);
89
+ }
90
+ }
91
+
92
+ return result;
93
+ }
94
+
95
+ async getAll(queue: string): Promise<Job[]> {
96
+ const result: Job[] = [];
97
+ const prefix = `${queue}:`;
98
+
99
+ for (const [key, job] of this.jobs.entries()) {
100
+ if (key.startsWith(prefix)) {
101
+ result.push(job);
102
+ }
103
+ }
104
+
105
+ return result;
106
+ }
107
+
108
+ async updateStatus(
109
+ queue: string,
110
+ jobId: string,
111
+ status: JobStatus
112
+ ): Promise<void> {
113
+ const key = `${queue}:${jobId}`;
114
+ const job = this.jobs.get(key);
115
+
116
+ if (job) {
117
+ job.state = status;
118
+ if (status === "completed" || status === "failed") {
119
+ job.finishedOn = Date.now();
120
+ }
121
+ }
122
+ }
123
+
124
+ async update(
125
+ queue: string,
126
+ jobId: string,
127
+ updates: Partial<Job>
128
+ ): Promise<void> {
129
+ const key = `${queue}:${jobId}`;
130
+ const job = this.jobs.get(key);
131
+
132
+ if (job) {
133
+ Object.assign(job, updates);
134
+ }
135
+ }
136
+
137
+ async delete(queue: string, jobId: string): Promise<void> {
138
+ const key = `${queue}:${jobId}`;
139
+ const job = this.jobs.get(key);
140
+
141
+ if (job?.opts.deduplicationKey) {
142
+ const dedupKey = `${queue}:${job.opts.deduplicationKey}`;
143
+ this.deduplicationKeys.delete(dedupKey);
144
+ }
145
+
146
+ this.jobs.delete(key);
147
+ }
148
+
149
+ async clean(
150
+ queue: string,
151
+ grace: number,
152
+ status: JobStatus
153
+ ): Promise<number> {
154
+ const now = Date.now();
155
+ const prefix = `${queue}:`;
156
+ const toDelete: string[] = [];
157
+
158
+ for (const [key, job] of this.jobs.entries()) {
159
+ if (key.startsWith(prefix) && job.state === status) {
160
+ const timestamp = job.finishedOn || job.timestamp;
161
+ if (now - timestamp > grace) {
162
+ toDelete.push(key);
163
+ }
164
+ }
165
+ }
166
+
167
+ for (const key of toDelete) {
168
+ const job = this.jobs.get(key);
169
+ if (job?.opts.deduplicationKey) {
170
+ const dedupKey = `${queue}:${job.opts.deduplicationKey}`;
171
+ this.deduplicationKeys.delete(dedupKey);
172
+ }
173
+ this.jobs.delete(key);
174
+ }
175
+
176
+ return toDelete.length;
177
+ }
178
+
179
+ async count(queue: string, status?: JobStatus): Promise<number> {
180
+ const prefix = `${queue}:`;
181
+ let count = 0;
182
+
183
+ for (const [key, job] of this.jobs.entries()) {
184
+ if (key.startsWith(prefix)) {
185
+ if (!status || job.state === status) {
186
+ count++;
187
+ }
188
+ }
189
+ }
190
+
191
+ return count;
192
+ }
193
+
194
+ async existsByDeduplicationKey(queue: string, key: string): Promise<boolean> {
195
+ const dedupKey = `${queue}:${key}`;
196
+ return this.deduplicationKeys.has(dedupKey);
197
+ }
198
+
199
+ /**
200
+ * Clear all jobs
201
+ */
202
+ clear(): void {
203
+ this.jobs.clear();
204
+ this.deduplicationKeys.clear();
205
+ }
206
+ }
@@ -0,0 +1,182 @@
1
+ /**
2
+ * Workflow state storage abstraction
3
+ */
4
+
5
+ import { WorkflowExecution, ExecutionStatus } from "../workflow/types.js";
6
+
7
+ export interface WorkflowStorage {
8
+ /**
9
+ * Save workflow execution state
10
+ */
11
+ save(workflowId: string, execution: WorkflowExecution): Promise<void>;
12
+
13
+ /**
14
+ * Get workflow execution by ID
15
+ */
16
+ get(executionId: string): Promise<WorkflowExecution | null>;
17
+
18
+ /**
19
+ * List executions for a workflow
20
+ */
21
+ list(workflowId: string, options?: ListOptions): Promise<WorkflowExecution[]>;
22
+
23
+ /**
24
+ * Update execution status
25
+ */
26
+ updateStatus(executionId: string, status: string): Promise<void>;
27
+
28
+ /**
29
+ * Update execution state
30
+ */
31
+ updateState(executionId: string, state: Record<string, any>): Promise<void>;
32
+
33
+ /**
34
+ * Delete an execution
35
+ */
36
+ delete(executionId: string): Promise<void>;
37
+
38
+ /**
39
+ * Clean old executions
40
+ */
41
+ clean(workflowId: string, grace: number): Promise<number>;
42
+ }
43
+
44
+ export interface ListOptions {
45
+ status?: string;
46
+ limit?: number;
47
+ offset?: number;
48
+ sortBy?: "createdAt" | "updatedAt";
49
+ sortOrder?: "asc" | "desc";
50
+ }
51
+
52
+ /**
53
+ * In-memory workflow storage implementation
54
+ */
55
+ export class MemoryWorkflowStorage implements WorkflowStorage {
56
+ private executions: Map<string, WorkflowExecution> = new Map();
57
+ private workflowIndex: Map<string, Set<string>> = new Map(); // workflowId -> executionIds
58
+
59
+ async save(workflowId: string, execution: WorkflowExecution): Promise<void> {
60
+ this.executions.set(execution.id, { ...execution });
61
+
62
+ // Index by workflow ID
63
+ if (!this.workflowIndex.has(workflowId)) {
64
+ this.workflowIndex.set(workflowId, new Set());
65
+ }
66
+ this.workflowIndex.get(workflowId)!.add(execution.id);
67
+ }
68
+
69
+ async get(executionId: string): Promise<WorkflowExecution | null> {
70
+ return this.executions.get(executionId) || null;
71
+ }
72
+
73
+ async list(
74
+ workflowId: string,
75
+ options: ListOptions = {}
76
+ ): Promise<WorkflowExecution[]> {
77
+ const executionIds = this.workflowIndex.get(workflowId) || new Set();
78
+ let executions: WorkflowExecution[] = [];
79
+
80
+ for (const id of executionIds) {
81
+ const execution = this.executions.get(id);
82
+ if (execution) {
83
+ // Filter by status if specified
84
+ if (!options.status || execution.status === options.status) {
85
+ executions.push(execution);
86
+ }
87
+ }
88
+ }
89
+
90
+ // Sort
91
+ const sortBy = options.sortBy || "createdAt";
92
+ const sortOrder = options.sortOrder || "desc";
93
+
94
+ executions.sort((a, b) => {
95
+ const aVal = a[sortBy] || 0;
96
+ const bVal = b[sortBy] || 0;
97
+ return sortOrder === "asc" ? aVal - bVal : bVal - aVal;
98
+ });
99
+
100
+ // Pagination
101
+ const offset = options.offset || 0;
102
+ const limit = options.limit || executions.length;
103
+
104
+ return executions.slice(offset, offset + limit);
105
+ }
106
+
107
+ async updateStatus(
108
+ executionId: string,
109
+ status: ExecutionStatus
110
+ ): Promise<void> {
111
+ const execution = this.executions.get(executionId);
112
+ if (execution) {
113
+ execution.status = status as ExecutionStatus;
114
+ execution.updatedAt = Date.now();
115
+
116
+ if (
117
+ status === "completed" ||
118
+ status === "failed" ||
119
+ status === "cancelled"
120
+ ) {
121
+ execution.completedAt = Date.now();
122
+ }
123
+ }
124
+ }
125
+
126
+ async updateState(
127
+ executionId: string,
128
+ state: Record<string, any>
129
+ ): Promise<void> {
130
+ const execution = this.executions.get(executionId);
131
+ if (execution) {
132
+ execution.state = { ...execution.state, ...state };
133
+ execution.updatedAt = Date.now();
134
+ }
135
+ }
136
+
137
+ async delete(executionId: string): Promise<void> {
138
+ const execution = this.executions.get(executionId);
139
+ if (execution) {
140
+ // Remove from workflow index
141
+ const workflowId = execution.workflowId;
142
+ const executionIds = this.workflowIndex.get(workflowId);
143
+ if (executionIds) {
144
+ executionIds.delete(executionId);
145
+ if (executionIds.size === 0) {
146
+ this.workflowIndex.delete(workflowId);
147
+ }
148
+ }
149
+
150
+ this.executions.delete(executionId);
151
+ }
152
+ }
153
+
154
+ async clean(workflowId: string, grace: number): Promise<number> {
155
+ const now = Date.now();
156
+ const executionIds = this.workflowIndex.get(workflowId) || new Set();
157
+ const toDelete: string[] = [];
158
+
159
+ for (const id of executionIds) {
160
+ const execution = this.executions.get(id);
161
+ if (execution && execution.completedAt) {
162
+ if (now - execution.completedAt > grace) {
163
+ toDelete.push(id);
164
+ }
165
+ }
166
+ }
167
+
168
+ for (const id of toDelete) {
169
+ await this.delete(id);
170
+ }
171
+
172
+ return toDelete.length;
173
+ }
174
+
175
+ /**
176
+ * Clear all executions
177
+ */
178
+ clear(): void {
179
+ this.executions.clear();
180
+ this.workflowIndex.clear();
181
+ }
182
+ }