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
package/src/queue/dlq.ts
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dead-Letter Queue (DLQ) implementation for FlowFn
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Job, JobOptions } from "../queue/types.js";
|
|
6
|
+
|
|
7
|
+
export interface DLQOptions {
|
|
8
|
+
/**
|
|
9
|
+
* Queue name for dead-letter jobs
|
|
10
|
+
*/
|
|
11
|
+
queueName?: string;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Maximum retries before moving to DLQ
|
|
15
|
+
*/
|
|
16
|
+
maxRetries?: number;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Time to live for DLQ items (ms)
|
|
20
|
+
*/
|
|
21
|
+
ttl?: number;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Handler called when job moves to DLQ
|
|
25
|
+
*/
|
|
26
|
+
onDLQ?: (job: Job, reason: string) => void | Promise<void>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface DLQJob<T = any> extends Job<T> {
|
|
30
|
+
/**
|
|
31
|
+
* Original queue name
|
|
32
|
+
*/
|
|
33
|
+
originalQueue: string;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Reason for DLQ
|
|
37
|
+
*/
|
|
38
|
+
dlqReason: string;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Timestamp when moved to DLQ
|
|
42
|
+
*/
|
|
43
|
+
dlqTimestamp: number;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* All error messages from attempts
|
|
47
|
+
*/
|
|
48
|
+
errors: string[];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface DLQManager {
|
|
52
|
+
/**
|
|
53
|
+
* Move job to DLQ
|
|
54
|
+
*/
|
|
55
|
+
moveToDLQ<T>(job: Job<T>, reason: string): Promise<DLQJob<T>>;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Get all DLQ jobs
|
|
59
|
+
*/
|
|
60
|
+
getAll(): Promise<DLQJob[]>;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Get DLQ jobs by original queue
|
|
64
|
+
*/
|
|
65
|
+
getByQueue(queueName: string): Promise<DLQJob[]>;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Retry a DLQ job
|
|
69
|
+
*/
|
|
70
|
+
retry<T>(jobId: string): Promise<Job<T>>;
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Retry all DLQ jobs from a queue
|
|
74
|
+
*/
|
|
75
|
+
retryAll(queueName: string): Promise<number>;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Delete a DLQ job
|
|
79
|
+
*/
|
|
80
|
+
delete(jobId: string): Promise<void>;
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Clean expired DLQ jobs
|
|
84
|
+
*/
|
|
85
|
+
clean(maxAge: number): Promise<number>;
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Get DLQ stats
|
|
89
|
+
*/
|
|
90
|
+
getStats(): Promise<{ total: number; byQueue: Record<string, number> }>;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* In-memory DLQ implementation
|
|
95
|
+
*/
|
|
96
|
+
export class MemoryDLQManager implements DLQManager {
|
|
97
|
+
private dlqJobs: Map<string, DLQJob> = new Map();
|
|
98
|
+
private options: DLQOptions;
|
|
99
|
+
|
|
100
|
+
constructor(options: DLQOptions = {}) {
|
|
101
|
+
this.options = {
|
|
102
|
+
queueName: "dlq",
|
|
103
|
+
maxRetries: 3,
|
|
104
|
+
ttl: 7 * 24 * 60 * 60 * 1000, // 7 days
|
|
105
|
+
...options,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async moveToDLQ<T>(job: Job<T>, reason: string): Promise<DLQJob<T>> {
|
|
110
|
+
const dlqJob: DLQJob<T> = {
|
|
111
|
+
...job,
|
|
112
|
+
originalQueue: job.name,
|
|
113
|
+
dlqReason: reason,
|
|
114
|
+
dlqTimestamp: Date.now(),
|
|
115
|
+
errors: [...(job.stacktrace || []), job.failedReason || reason].filter(
|
|
116
|
+
Boolean
|
|
117
|
+
),
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
this.dlqJobs.set(job.id, dlqJob);
|
|
121
|
+
|
|
122
|
+
// Call handler if provided
|
|
123
|
+
if (this.options.onDLQ) {
|
|
124
|
+
await Promise.resolve(this.options.onDLQ(job, reason));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return dlqJob;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async getAll(): Promise<DLQJob[]> {
|
|
131
|
+
return Array.from(this.dlqJobs.values());
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async getByQueue(queueName: string): Promise<DLQJob[]> {
|
|
135
|
+
return Array.from(this.dlqJobs.values()).filter(
|
|
136
|
+
(job) => job.originalQueue === queueName
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async retry<T>(jobId: string): Promise<Job<T>> {
|
|
141
|
+
const dlqJob = this.dlqJobs.get(jobId);
|
|
142
|
+
if (!dlqJob) {
|
|
143
|
+
throw new Error(`DLQ job ${jobId} not found`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Remove from DLQ
|
|
147
|
+
this.dlqJobs.delete(jobId);
|
|
148
|
+
|
|
149
|
+
// Reset job state for retry
|
|
150
|
+
const retriedJob: Job<T> = {
|
|
151
|
+
...dlqJob,
|
|
152
|
+
state: "waiting",
|
|
153
|
+
attemptsMade: 0,
|
|
154
|
+
failedReason: undefined,
|
|
155
|
+
stacktrace: [],
|
|
156
|
+
processedOn: undefined,
|
|
157
|
+
finishedOn: undefined,
|
|
158
|
+
} as Job<T>;
|
|
159
|
+
|
|
160
|
+
return retriedJob;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async retryAll(queueName: string): Promise<number> {
|
|
164
|
+
const jobs = await this.getByQueue(queueName);
|
|
165
|
+
let count = 0;
|
|
166
|
+
|
|
167
|
+
for (const job of jobs) {
|
|
168
|
+
try {
|
|
169
|
+
await this.retry(job.id);
|
|
170
|
+
count++;
|
|
171
|
+
} catch (err) {
|
|
172
|
+
console.error(`Failed to retry job ${job.id}:`, err);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return count;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async delete(jobId: string): Promise<void> {
|
|
180
|
+
this.dlqJobs.delete(jobId);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async clean(maxAge: number): Promise<number> {
|
|
184
|
+
const now = Date.now();
|
|
185
|
+
const toDelete: string[] = [];
|
|
186
|
+
|
|
187
|
+
for (const [id, job] of this.dlqJobs.entries()) {
|
|
188
|
+
if (now - job.dlqTimestamp > maxAge) {
|
|
189
|
+
toDelete.push(id);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
for (const id of toDelete) {
|
|
194
|
+
this.dlqJobs.delete(id);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return toDelete.length;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async getStats(): Promise<{
|
|
201
|
+
total: number;
|
|
202
|
+
byQueue: Record<string, number>;
|
|
203
|
+
}> {
|
|
204
|
+
const byQueue: Record<string, number> = {};
|
|
205
|
+
|
|
206
|
+
for (const job of this.dlqJobs.values()) {
|
|
207
|
+
byQueue[job.originalQueue] = (byQueue[job.originalQueue] || 0) + 1;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return {
|
|
211
|
+
total: this.dlqJobs.size,
|
|
212
|
+
byQueue,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Clear all DLQ jobs (for testing)
|
|
218
|
+
*/
|
|
219
|
+
clear(): void {
|
|
220
|
+
this.dlqJobs.clear();
|
|
221
|
+
}
|
|
222
|
+
}
|
package/src/queue/job.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { Job, JobOptions, JobStatus } from './types.js';
|
|
2
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
3
|
+
|
|
4
|
+
export class JobImpl<T = any> implements Job<T> {
|
|
5
|
+
id: string;
|
|
6
|
+
name: string;
|
|
7
|
+
data: T;
|
|
8
|
+
opts: JobOptions;
|
|
9
|
+
|
|
10
|
+
state: JobStatus = 'waiting';
|
|
11
|
+
progress: number = 0;
|
|
12
|
+
returnvalue?: any;
|
|
13
|
+
|
|
14
|
+
timestamp: number;
|
|
15
|
+
processedOn?: number;
|
|
16
|
+
finishedOn?: number;
|
|
17
|
+
delay: number = 0;
|
|
18
|
+
|
|
19
|
+
attemptsMade: number = 0;
|
|
20
|
+
failedReason?: string;
|
|
21
|
+
stacktrace?: string[];
|
|
22
|
+
|
|
23
|
+
constructor(name: string, data: T, opts?: JobOptions) {
|
|
24
|
+
this.id = opts?.jobId || uuidv4();
|
|
25
|
+
this.name = name;
|
|
26
|
+
this.data = data;
|
|
27
|
+
this.opts = opts || {};
|
|
28
|
+
this.timestamp = Date.now();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async update(data: Partial<T>): Promise<void> {
|
|
32
|
+
this.data = { ...this.data, ...data };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async log(message: string): Promise<void> {
|
|
36
|
+
// console.log(`[Job ${this.id}] ${message}`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async updateProgress(progress: number): Promise<void> {
|
|
40
|
+
this.progress = progress;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async moveToCompleted(returnValue: any): Promise<void> {
|
|
44
|
+
this.state = 'completed';
|
|
45
|
+
this.returnvalue = returnValue;
|
|
46
|
+
this.finishedOn = Date.now();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async moveToFailed(error: Error): Promise<void> {
|
|
50
|
+
this.state = 'failed';
|
|
51
|
+
this.failedReason = error.message;
|
|
52
|
+
this.finishedOn = Date.now();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async retry(): Promise<void> {
|
|
56
|
+
// logical retry
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async discard(): Promise<void> {
|
|
60
|
+
// discard
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async waitUntilFinished(): Promise<any> {
|
|
64
|
+
// Poll or wait for event
|
|
65
|
+
return Promise.resolve();
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Queue,
|
|
3
|
+
Job,
|
|
4
|
+
JobOptions,
|
|
5
|
+
QueueOptions,
|
|
6
|
+
JobHandler,
|
|
7
|
+
BatchHandler,
|
|
8
|
+
BatchOptions,
|
|
9
|
+
QueueStats,
|
|
10
|
+
JobStatus,
|
|
11
|
+
} from "./types.js";
|
|
12
|
+
import { FlowAdapter } from "../adapters/base.js";
|
|
13
|
+
import { JobImpl } from "./job.js";
|
|
14
|
+
import EventEmitter from "eventemitter3";
|
|
15
|
+
import { calculateBackoff } from "../patterns/backoff.js";
|
|
16
|
+
|
|
17
|
+
export class QueueImpl<T = any> extends EventEmitter implements Queue<T> {
|
|
18
|
+
name: string;
|
|
19
|
+
private adapter: FlowAdapter;
|
|
20
|
+
private options: QueueOptions;
|
|
21
|
+
private isProcessing = false;
|
|
22
|
+
private currentWorkers: Set<Promise<void>> = new Set();
|
|
23
|
+
|
|
24
|
+
constructor(name: string, adapter: FlowAdapter, options: QueueOptions = {}) {
|
|
25
|
+
super();
|
|
26
|
+
this.name = name;
|
|
27
|
+
this.adapter = adapter;
|
|
28
|
+
this.options = options;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async add(name: string, data: T, opts?: JobOptions): Promise<Job<T>> {
|
|
32
|
+
const job = new JobImpl<T>(name, data, {
|
|
33
|
+
...this.options.defaultJobOptions,
|
|
34
|
+
...opts,
|
|
35
|
+
});
|
|
36
|
+
await this.adapter.enqueue(this.name, job);
|
|
37
|
+
this.emit("waiting", job);
|
|
38
|
+
return job;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async addBulk(
|
|
42
|
+
jobs: Array<{ name: string; data: T; opts?: JobOptions }>
|
|
43
|
+
): Promise<Job<T>[]> {
|
|
44
|
+
return Promise.all(jobs.map((j) => this.add(j.name, j.data, j.opts)));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
process(arg1: any, arg2?: any, arg3?: any): void {
|
|
48
|
+
let handler: JobHandler<T>;
|
|
49
|
+
let concurrency = 1;
|
|
50
|
+
|
|
51
|
+
if (typeof arg1 === "number") {
|
|
52
|
+
concurrency = arg1;
|
|
53
|
+
handler = arg2;
|
|
54
|
+
} else if (typeof arg1 === "string") {
|
|
55
|
+
// name based processing, ignore name for now in simple impl
|
|
56
|
+
if (typeof arg2 === "number") {
|
|
57
|
+
concurrency = arg2;
|
|
58
|
+
handler = arg3;
|
|
59
|
+
} else {
|
|
60
|
+
handler = arg2;
|
|
61
|
+
}
|
|
62
|
+
} else {
|
|
63
|
+
handler = arg1;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
this.resume();
|
|
67
|
+
for (let i = 0; i < concurrency; i++) {
|
|
68
|
+
const worker = this.startProcessing(handler);
|
|
69
|
+
this.currentWorkers.add(worker);
|
|
70
|
+
worker.finally(() => this.currentWorkers.delete(worker));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
private async startProcessing(handler: JobHandler<T>) {
|
|
75
|
+
while (this.isProcessing) {
|
|
76
|
+
const jobData = await this.adapter.dequeue(this.name);
|
|
77
|
+
if (jobData) {
|
|
78
|
+
const job = Object.assign(
|
|
79
|
+
new JobImpl(jobData.name, jobData.data, jobData.opts),
|
|
80
|
+
jobData
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
// Check dependencies
|
|
84
|
+
if (job.opts.waitFor && job.opts.waitFor.length > 0) {
|
|
85
|
+
let allDone = true;
|
|
86
|
+
for (const depId of job.opts.waitFor) {
|
|
87
|
+
const depJob = await this.adapter.getJob(this.name, depId);
|
|
88
|
+
if (!depJob || depJob.state !== "completed") {
|
|
89
|
+
allDone = false;
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (!allDone) {
|
|
95
|
+
// Not ready, requeue
|
|
96
|
+
await this.adapter.enqueue(this.name, job);
|
|
97
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
job.state = "active";
|
|
104
|
+
job.processedOn = Date.now();
|
|
105
|
+
this.emit("active", job);
|
|
106
|
+
|
|
107
|
+
const result = await handler(job);
|
|
108
|
+
|
|
109
|
+
await job.moveToCompleted(result);
|
|
110
|
+
await this.adapter.ack(this.name, job.id);
|
|
111
|
+
this.emit("completed", job, result);
|
|
112
|
+
} catch (err: any) {
|
|
113
|
+
job.attemptsMade++;
|
|
114
|
+
job.stacktrace = job.stacktrace || [];
|
|
115
|
+
job.stacktrace.push(err.stack);
|
|
116
|
+
|
|
117
|
+
const maxAttempts = job.opts.attempts || 1;
|
|
118
|
+
if (job.attemptsMade < maxAttempts) {
|
|
119
|
+
job.state = "delayed";
|
|
120
|
+
const backoff = job.opts.backoff
|
|
121
|
+
? calculateBackoff(job.attemptsMade, job.opts.backoff)
|
|
122
|
+
: 0;
|
|
123
|
+
job.opts.delay = backoff;
|
|
124
|
+
await this.adapter.enqueue(this.name, job);
|
|
125
|
+
this.emit("failed", job, err); // failed but retrying
|
|
126
|
+
} else {
|
|
127
|
+
await job.moveToFailed(err);
|
|
128
|
+
// In real impl, move to DLQ or just ack to remove from active
|
|
129
|
+
await this.adapter.ack(this.name, job.id);
|
|
130
|
+
this.emit("failed", job, err);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
} else {
|
|
134
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
processBatch(
|
|
140
|
+
arg1: string | any,
|
|
141
|
+
arg2: number | BatchOptions | any,
|
|
142
|
+
arg3?: BatchHandler<T>
|
|
143
|
+
): void {
|
|
144
|
+
let handler: BatchHandler<T>;
|
|
145
|
+
let batchSize = 10;
|
|
146
|
+
let maxWait = 1000;
|
|
147
|
+
|
|
148
|
+
if (typeof arg2 === "function") {
|
|
149
|
+
handler = arg2;
|
|
150
|
+
} else {
|
|
151
|
+
handler = arg3!;
|
|
152
|
+
if (typeof arg2 === "number") {
|
|
153
|
+
batchSize = arg2;
|
|
154
|
+
} else {
|
|
155
|
+
batchSize = arg2.batchSize;
|
|
156
|
+
maxWait = arg2.maxWait || 1000;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
this.resume();
|
|
161
|
+
this.startBatchProcessing(handler, batchSize, maxWait);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
private async startBatchProcessing(
|
|
165
|
+
handler: BatchHandler<T>,
|
|
166
|
+
batchSize: number,
|
|
167
|
+
maxWait: number
|
|
168
|
+
) {
|
|
169
|
+
while (this.isProcessing) {
|
|
170
|
+
const batch: Job[] = [];
|
|
171
|
+
const start = Date.now();
|
|
172
|
+
|
|
173
|
+
while (batch.length < batchSize && Date.now() - start < maxWait) {
|
|
174
|
+
const jobData = await this.adapter.dequeue(this.name);
|
|
175
|
+
if (jobData) {
|
|
176
|
+
const job = Object.assign(
|
|
177
|
+
new JobImpl(jobData.name, jobData.data, jobData.opts),
|
|
178
|
+
jobData
|
|
179
|
+
);
|
|
180
|
+
batch.push(job);
|
|
181
|
+
} else {
|
|
182
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (batch.length > 0) {
|
|
187
|
+
try {
|
|
188
|
+
for (const job of batch) {
|
|
189
|
+
(job as any).state = "active";
|
|
190
|
+
(job as any).processedOn = Date.now();
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const results = await handler(batch);
|
|
194
|
+
|
|
195
|
+
for (let i = 0; i < batch.length; i++) {
|
|
196
|
+
await batch[i].moveToCompleted(results[i]);
|
|
197
|
+
await this.adapter.ack(this.name, batch[i].id);
|
|
198
|
+
}
|
|
199
|
+
} catch (err) {
|
|
200
|
+
for (const job of batch) {
|
|
201
|
+
await job.moveToFailed(err as Error);
|
|
202
|
+
await this.adapter.ack(this.name, job.id);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async pause(): Promise<void> {
|
|
210
|
+
this.isProcessing = false;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async resume(): Promise<void> {
|
|
214
|
+
this.isProcessing = true;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async drain(): Promise<void> {
|
|
218
|
+
await Promise.all(this.currentWorkers);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async clean(grace: number, status: JobStatus): Promise<number> {
|
|
222
|
+
return this.adapter.cleanJobs(this.name, grace, status);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
async getJob(jobId: string): Promise<Job<T> | null> {
|
|
226
|
+
const job = await this.adapter.getJob(this.name, jobId);
|
|
227
|
+
return job as Job<T> | null;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
async getJobs(status: JobStatus): Promise<Job<T>[]> {
|
|
231
|
+
const jobs = await this.adapter.getJobs(this.name, status);
|
|
232
|
+
return jobs as Job<T>[];
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async getJobCounts(): Promise<QueueStats> {
|
|
236
|
+
return this.adapter.getQueueStats(this.name);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async close(): Promise<void> {
|
|
240
|
+
this.isProcessing = false;
|
|
241
|
+
await this.drain();
|
|
242
|
+
}
|
|
243
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
export interface Queue<T = any> {
|
|
2
|
+
name: string;
|
|
3
|
+
|
|
4
|
+
// Adding jobs
|
|
5
|
+
add(name: string, data: T, opts?: JobOptions): Promise<Job<T>>;
|
|
6
|
+
addBulk(
|
|
7
|
+
jobs: Array<{ name: string; data: T; opts?: JobOptions }>
|
|
8
|
+
): Promise<Job<T>[]>;
|
|
9
|
+
|
|
10
|
+
// Processing
|
|
11
|
+
process(handler: JobHandler<T>): void;
|
|
12
|
+
process(name: string, handler: JobHandler<T>): void;
|
|
13
|
+
process(concurrency: number, handler: JobHandler<T>): void;
|
|
14
|
+
process(name: string, concurrency: number, handler: JobHandler<T>): void;
|
|
15
|
+
|
|
16
|
+
// Batch processing
|
|
17
|
+
processBatch(name: string, batchSize: number, handler: BatchHandler<T>): void;
|
|
18
|
+
processBatch(
|
|
19
|
+
name: string,
|
|
20
|
+
options: BatchOptions,
|
|
21
|
+
handler: BatchHandler<T>
|
|
22
|
+
): void;
|
|
23
|
+
|
|
24
|
+
// Management
|
|
25
|
+
pause(): Promise<void>;
|
|
26
|
+
resume(): Promise<void>;
|
|
27
|
+
drain(): Promise<void>;
|
|
28
|
+
clean(grace: number, status: JobStatus): Promise<number>;
|
|
29
|
+
|
|
30
|
+
// DLQ methods
|
|
31
|
+
getDLQ?(): any; // Returns DLQManager if configured
|
|
32
|
+
|
|
33
|
+
// Queries
|
|
34
|
+
getJob(jobId: string): Promise<Job<T> | null>;
|
|
35
|
+
getJobs(status: JobStatus): Promise<Job<T>[]>;
|
|
36
|
+
getJobCounts(): Promise<QueueStats>;
|
|
37
|
+
|
|
38
|
+
// Events
|
|
39
|
+
on(event: QueueEvent, handler: (...args: any[]) => void): void;
|
|
40
|
+
|
|
41
|
+
// Lifecycle
|
|
42
|
+
close(): Promise<void>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export type QueueEvent =
|
|
46
|
+
| "completed"
|
|
47
|
+
| "failed"
|
|
48
|
+
| "stalled"
|
|
49
|
+
| "progress"
|
|
50
|
+
| "waiting"
|
|
51
|
+
| "active";
|
|
52
|
+
|
|
53
|
+
export interface Job<T = any> {
|
|
54
|
+
id: string;
|
|
55
|
+
name: string;
|
|
56
|
+
data: T;
|
|
57
|
+
opts: JobOptions;
|
|
58
|
+
|
|
59
|
+
// Status
|
|
60
|
+
state: JobStatus;
|
|
61
|
+
progress: number;
|
|
62
|
+
returnvalue?: any;
|
|
63
|
+
|
|
64
|
+
// Timing
|
|
65
|
+
timestamp: number;
|
|
66
|
+
processedOn?: number;
|
|
67
|
+
finishedOn?: number;
|
|
68
|
+
delay: number;
|
|
69
|
+
|
|
70
|
+
// Retries
|
|
71
|
+
attemptsMade: number;
|
|
72
|
+
failedReason?: string;
|
|
73
|
+
stacktrace?: string[];
|
|
74
|
+
|
|
75
|
+
// Methods
|
|
76
|
+
update(data: Partial<T>): Promise<void>;
|
|
77
|
+
log(message: string): Promise<void>;
|
|
78
|
+
updateProgress(progress: number): Promise<void>;
|
|
79
|
+
moveToCompleted(returnValue: any): Promise<void>;
|
|
80
|
+
moveToFailed(error: Error): Promise<void>;
|
|
81
|
+
retry(): Promise<void>;
|
|
82
|
+
discard(): Promise<void>;
|
|
83
|
+
waitUntilFinished(): Promise<any>;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export type JobStatus =
|
|
87
|
+
| "waiting"
|
|
88
|
+
| "active"
|
|
89
|
+
| "completed"
|
|
90
|
+
| "failed"
|
|
91
|
+
| "delayed"
|
|
92
|
+
| "paused";
|
|
93
|
+
|
|
94
|
+
export interface JobOptions {
|
|
95
|
+
priority?: number;
|
|
96
|
+
delay?: number;
|
|
97
|
+
attempts?: number;
|
|
98
|
+
backoff?: BackoffOptions;
|
|
99
|
+
lifo?: boolean;
|
|
100
|
+
timeout?: number;
|
|
101
|
+
removeOnComplete?: boolean | number;
|
|
102
|
+
removeOnFail?: boolean | number;
|
|
103
|
+
stackTraceLimit?: number;
|
|
104
|
+
jobId?: string;
|
|
105
|
+
preventDuplicates?: boolean;
|
|
106
|
+
deduplicationKey?: string;
|
|
107
|
+
waitFor?: string[]; // Job IDs dependency
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export interface BackoffOptions {
|
|
111
|
+
type: "fixed" | "exponential" | "custom";
|
|
112
|
+
delay: number;
|
|
113
|
+
maxDelay?: number;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export interface QueueOptions {
|
|
117
|
+
defaultJobOptions?: JobOptions;
|
|
118
|
+
limiter?: RateLimiterOptions;
|
|
119
|
+
cache?: CacheOptions;
|
|
120
|
+
dlq?: {
|
|
121
|
+
enabled: boolean;
|
|
122
|
+
maxRetries?: number;
|
|
123
|
+
queueName?: string;
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export interface RateLimiterOptions {
|
|
128
|
+
max: number;
|
|
129
|
+
duration: number;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export interface CacheOptions {
|
|
133
|
+
enabled: boolean;
|
|
134
|
+
ttl: number;
|
|
135
|
+
keyGenerator?: (job: Job) => string;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export interface QueueStats {
|
|
139
|
+
waiting: number;
|
|
140
|
+
active: number;
|
|
141
|
+
completed: number;
|
|
142
|
+
failed: number;
|
|
143
|
+
delayed: number;
|
|
144
|
+
paused: number;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export type JobHandler<T> = (job: Job<T>) => Promise<any>;
|
|
148
|
+
export type BatchHandler<T> = (jobs: Job<T>[]) => Promise<any[]>;
|
|
149
|
+
|
|
150
|
+
export interface BatchOptions {
|
|
151
|
+
batchSize: number;
|
|
152
|
+
maxWait?: number;
|
|
153
|
+
}
|