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