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,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
|
+
}
|