@workglow/job-queue 0.0.52
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/LICENSE +201 -0
- package/README.md +694 -0
- package/dist/browser.js +1075 -0
- package/dist/browser.js.map +20 -0
- package/dist/bun.js +1375 -0
- package/dist/bun.js.map +22 -0
- package/dist/common-server.d.ts +9 -0
- package/dist/common-server.d.ts.map +1 -0
- package/dist/common.d.ts +18 -0
- package/dist/common.d.ts.map +1 -0
- package/dist/job/IJobQueue.d.ts +160 -0
- package/dist/job/IJobQueue.d.ts.map +1 -0
- package/dist/job/Job.d.ts +87 -0
- package/dist/job/Job.d.ts.map +1 -0
- package/dist/job/JobError.d.ts +61 -0
- package/dist/job/JobError.d.ts.map +1 -0
- package/dist/job/JobQueue.d.ts +272 -0
- package/dist/job/JobQueue.d.ts.map +1 -0
- package/dist/job/JobQueueEventListeners.d.ts +30 -0
- package/dist/job/JobQueueEventListeners.d.ts.map +1 -0
- package/dist/limiter/CompositeLimiter.d.ts +18 -0
- package/dist/limiter/CompositeLimiter.d.ts.map +1 -0
- package/dist/limiter/ConcurrencyLimiter.d.ts +24 -0
- package/dist/limiter/ConcurrencyLimiter.d.ts.map +1 -0
- package/dist/limiter/DelayLimiter.d.ts +18 -0
- package/dist/limiter/DelayLimiter.d.ts.map +1 -0
- package/dist/limiter/EvenlySpacedRateLimiter.d.ts +35 -0
- package/dist/limiter/EvenlySpacedRateLimiter.d.ts.map +1 -0
- package/dist/limiter/ILimiter.d.ts +27 -0
- package/dist/limiter/ILimiter.d.ts.map +1 -0
- package/dist/limiter/InMemoryRateLimiter.d.ts +32 -0
- package/dist/limiter/InMemoryRateLimiter.d.ts.map +1 -0
- package/dist/limiter/NullLimiter.d.ts +19 -0
- package/dist/limiter/NullLimiter.d.ts.map +1 -0
- package/dist/limiter/PostgresRateLimiter.d.ts +53 -0
- package/dist/limiter/PostgresRateLimiter.d.ts.map +1 -0
- package/dist/limiter/SqliteRateLimiter.d.ts +44 -0
- package/dist/limiter/SqliteRateLimiter.d.ts.map +1 -0
- package/dist/node.js +1374 -0
- package/dist/node.js.map +22 -0
- package/dist/types.d.ts +7 -0
- package/dist/types.d.ts.map +1 -0
- package/package.json +61 -0
package/README.md
ADDED
|
@@ -0,0 +1,694 @@
|
|
|
1
|
+
# @workglow/job-queue
|
|
2
|
+
|
|
3
|
+
A TypeScript-first job queue system for managing and processing asynchronous tasks with rate limiting, progress tracking, and cross-platform persistence.
|
|
4
|
+
|
|
5
|
+
- [Features](#features)
|
|
6
|
+
- [Installation](#installation)
|
|
7
|
+
- [Quick Start](#quick-start)
|
|
8
|
+
- [Core Concepts](#core-concepts)
|
|
9
|
+
- [Jobs](#jobs)
|
|
10
|
+
- [Job Queues](#job-queues)
|
|
11
|
+
- [Storage Backends](#storage-backends)
|
|
12
|
+
- [Rate Limiters](#rate-limiters)
|
|
13
|
+
- [Usage Examples](#usage-examples)
|
|
14
|
+
- [Creating Custom Jobs](#creating-custom-jobs)
|
|
15
|
+
- [Basic Queue Operations](#basic-queue-operations)
|
|
16
|
+
- [Progress Tracking](#progress-tracking)
|
|
17
|
+
- [Error Handling and Retries](#error-handling-and-retries)
|
|
18
|
+
- [Event Listeners](#event-listeners)
|
|
19
|
+
- [Job Completion and Output](#job-completion-and-output)
|
|
20
|
+
- [Storage Configurations](#storage-configurations)
|
|
21
|
+
- [In-Memory Storage](#in-memory-storage)
|
|
22
|
+
- [IndexedDB Storage (Browser)](#indexeddb-storage-browser)
|
|
23
|
+
- [SQLite Storage (Node.js/Bun)](#sqlite-storage-nodejsbun)
|
|
24
|
+
- [PostgreSQL Storage (Node.js/Bun)](#postgresql-storage-nodejsbun)
|
|
25
|
+
- [Rate Limiting Strategies](#rate-limiting-strategies)
|
|
26
|
+
- [Concurrency Limiter](#concurrency-limiter)
|
|
27
|
+
- [Delay Limiter](#delay-limiter)
|
|
28
|
+
- [Rate Limiter](#rate-limiter)
|
|
29
|
+
- [Composite Limiter](#composite-limiter)
|
|
30
|
+
- [Queue Modes](#queue-modes)
|
|
31
|
+
- [API Reference](#api-reference)
|
|
32
|
+
- [JobQueue Methods](#jobqueue-methods)
|
|
33
|
+
- [Job Class](#job-class)
|
|
34
|
+
- [TypeScript Types](#typescript-types)
|
|
35
|
+
- [Testing](#testing)
|
|
36
|
+
- [License](#license)
|
|
37
|
+
|
|
38
|
+
## Features
|
|
39
|
+
|
|
40
|
+
- **Cross-platform**: Works in browsers (IndexedDB), Node.js, and Bun
|
|
41
|
+
- **Multiple storage backends**: In-Memory, IndexedDB, SQLite, PostgreSQL
|
|
42
|
+
- **Rate limiting**: Concurrency, delay, and composite rate limiting strategies
|
|
43
|
+
- **Progress tracking**: Real-time job progress with events and callbacks
|
|
44
|
+
- **Retry logic**: Configurable retry attempts with exponential backoff
|
|
45
|
+
- **Event system**: Comprehensive event listeners for job lifecycle
|
|
46
|
+
- **TypeScript-first**: Full type safety with generic input/output types
|
|
47
|
+
- **Job prioritization**: Support for job scheduling and deadlines
|
|
48
|
+
- **Queue modes**: Client-only, server-only, or both modes of operation
|
|
49
|
+
|
|
50
|
+
## Installation
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
bun add @workglow/job-queue
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
For specific storage backends, you may need additional dependencies:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
# For SQLite support
|
|
60
|
+
bun add @workglow/sqlite
|
|
61
|
+
|
|
62
|
+
# For PostgreSQL support
|
|
63
|
+
bun add pg @types/pg
|
|
64
|
+
|
|
65
|
+
# For comprehensive storage options
|
|
66
|
+
bun add @workglow/storage
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Quick Start
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
import { Job, JobQueue } from "@workglow/job-queue";
|
|
73
|
+
import { InMemoryQueueStorage } from "@workglow/storage";
|
|
74
|
+
|
|
75
|
+
// 1. Define your input/output types
|
|
76
|
+
interface ProcessTextInput {
|
|
77
|
+
text: string;
|
|
78
|
+
options?: { uppercase?: boolean };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
interface ProcessTextOutput {
|
|
82
|
+
processedText: string;
|
|
83
|
+
wordCount: number;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// 2. Create a custom job class
|
|
87
|
+
class ProcessTextJob extends Job<ProcessTextInput, ProcessTextOutput> {
|
|
88
|
+
async execute(input: ProcessTextInput, context: IJobExecuteContext): Promise<ProcessTextOutput> {
|
|
89
|
+
const { text, options = {} } = input;
|
|
90
|
+
|
|
91
|
+
// Simulate work with progress updates
|
|
92
|
+
await context.updateProgress(25, "Starting text processing");
|
|
93
|
+
|
|
94
|
+
await new Promise((resolve) => setTimeout(resolve, 100)); // Simulate work
|
|
95
|
+
await context.updateProgress(50, "Processing text");
|
|
96
|
+
|
|
97
|
+
const processedText = options.uppercase ? text.toUpperCase() : text.toLowerCase();
|
|
98
|
+
await context.updateProgress(75, "Counting words");
|
|
99
|
+
|
|
100
|
+
const wordCount = text.split(/\s+/).filter((word) => word.length > 0).length;
|
|
101
|
+
await context.updateProgress(100, "Complete");
|
|
102
|
+
|
|
103
|
+
return { processedText, wordCount };
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// 3. Create and start the queue
|
|
108
|
+
const queue = new JobQueue("text-processor", ProcessTextJob, {
|
|
109
|
+
storage: new InMemoryQueueStorage("text-processor"),
|
|
110
|
+
deleteAfterCompletionMs: 60_000, // Clean up after 1 minute
|
|
111
|
+
deleteAfterFailureMs: 300_000, // Keep failed jobs for 5 minutes
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
await queue.start();
|
|
115
|
+
|
|
116
|
+
// 4. Add jobs and wait for results
|
|
117
|
+
const job = new ProcessTextJob({
|
|
118
|
+
input: { text: "Hello World", options: { uppercase: true } },
|
|
119
|
+
maxRetries: 3,
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const jobId = await queue.add(job);
|
|
123
|
+
const result = await queue.waitFor(jobId);
|
|
124
|
+
console.log(result); // { processedText: "HELLO WORLD", wordCount: 2 }
|
|
125
|
+
|
|
126
|
+
await queue.stop();
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## Core Concepts
|
|
130
|
+
|
|
131
|
+
### Jobs
|
|
132
|
+
|
|
133
|
+
Jobs are units of work that can be executed by a queue. Each job has:
|
|
134
|
+
|
|
135
|
+
- **Input**: Data needed for execution (strongly typed)
|
|
136
|
+
- **Output**: Result of execution (strongly typed)
|
|
137
|
+
- **Status**: PENDING, RUNNING, COMPLETED, FAILED, ABORTING, DISABLED
|
|
138
|
+
- **Progress**: 0-100 with optional message and details
|
|
139
|
+
- **Retry logic**: Configurable max retries and retry strategies
|
|
140
|
+
|
|
141
|
+
### Job Queues
|
|
142
|
+
|
|
143
|
+
Queues manage job execution with:
|
|
144
|
+
|
|
145
|
+
- **Storage backend**: Where jobs are persisted
|
|
146
|
+
- **Rate limiting**: Controls job execution rate
|
|
147
|
+
- **Event system**: Lifecycle notifications
|
|
148
|
+
- **Queue modes**: CLIENT (submit only), SERVER (process only), BOTH
|
|
149
|
+
|
|
150
|
+
### Storage Backends
|
|
151
|
+
|
|
152
|
+
Storage determines where jobs are persisted:
|
|
153
|
+
|
|
154
|
+
- **InMemoryQueueStorage**: Volatile, lost on restart
|
|
155
|
+
- **IndexedDbQueueStorage**: Browser persistent storage
|
|
156
|
+
- **SqliteQueueStorage**: Local SQLite file
|
|
157
|
+
- **PostgresQueueStorage**: PostgreSQL database
|
|
158
|
+
|
|
159
|
+
### Rate Limiters
|
|
160
|
+
|
|
161
|
+
Control job execution rate:
|
|
162
|
+
|
|
163
|
+
- **ConcurrencyLimiter**: Max concurrent jobs
|
|
164
|
+
- **DelayLimiter**: Minimum delay between jobs
|
|
165
|
+
- **InMemoryRateLimiter**: Requests per time window
|
|
166
|
+
- **CompositeLimiter**: Combine multiple limiters
|
|
167
|
+
|
|
168
|
+
## Usage Examples
|
|
169
|
+
|
|
170
|
+
### Creating Custom Jobs
|
|
171
|
+
|
|
172
|
+
```typescript
|
|
173
|
+
import { Job, IJobExecuteContext } from "@workglow/job-queue";
|
|
174
|
+
|
|
175
|
+
interface DownloadInput {
|
|
176
|
+
url: string;
|
|
177
|
+
filename: string;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
interface DownloadOutput {
|
|
181
|
+
filepath: string;
|
|
182
|
+
size: number;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
class DownloadJob extends Job<DownloadInput, DownloadOutput> {
|
|
186
|
+
async execute(input: DownloadInput, context: IJobExecuteContext): Promise<DownloadOutput> {
|
|
187
|
+
const { url, filename } = input;
|
|
188
|
+
|
|
189
|
+
// Check for abort signal
|
|
190
|
+
if (context.signal.aborted) {
|
|
191
|
+
throw new Error("Job was aborted");
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Update progress
|
|
195
|
+
await context.updateProgress(10, "Starting download");
|
|
196
|
+
|
|
197
|
+
// Simulate download with progress
|
|
198
|
+
for (let i = 20; i <= 90; i += 10) {
|
|
199
|
+
if (context.signal.aborted) throw new Error("Job was aborted");
|
|
200
|
+
|
|
201
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
202
|
+
await context.updateProgress(i, `Downloaded ${i}%`);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
await context.updateProgress(100, "Download complete");
|
|
206
|
+
|
|
207
|
+
return {
|
|
208
|
+
filepath: `/downloads/${filename}`,
|
|
209
|
+
size: 1024 * 1024, // 1MB
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### Basic Queue Operations
|
|
216
|
+
|
|
217
|
+
```typescript
|
|
218
|
+
import { JobQueue, ConcurrencyLimiter } from "@workglow/job-queue";
|
|
219
|
+
import { InMemoryQueueStorage } from "@workglow/storage";
|
|
220
|
+
|
|
221
|
+
// Create queue with concurrency limiting
|
|
222
|
+
const queue = new JobQueue("downloads", DownloadJob, {
|
|
223
|
+
storage: new InMemoryQueueStorage("downloads"),
|
|
224
|
+
limiter: new ConcurrencyLimiter(3), // Max 3 concurrent downloads
|
|
225
|
+
waitDurationInMilliseconds: 500, // Check for new jobs every 500ms
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
// Start the queue
|
|
229
|
+
await queue.start();
|
|
230
|
+
|
|
231
|
+
// Add multiple jobs
|
|
232
|
+
const jobIds = await Promise.all([
|
|
233
|
+
queue.add(
|
|
234
|
+
new DownloadJob({
|
|
235
|
+
input: { url: "https://example.com/file1.zip", filename: "file1.zip" },
|
|
236
|
+
})
|
|
237
|
+
),
|
|
238
|
+
queue.add(
|
|
239
|
+
new DownloadJob({
|
|
240
|
+
input: { url: "https://example.com/file2.zip", filename: "file2.zip" },
|
|
241
|
+
})
|
|
242
|
+
),
|
|
243
|
+
]);
|
|
244
|
+
|
|
245
|
+
// Check queue status
|
|
246
|
+
const queueSize = await queue.size(); // Total jobs
|
|
247
|
+
const pendingJobs = await queue.size(JobStatus.PENDING);
|
|
248
|
+
const runningJobs = await queue.size(JobStatus.RUNNING);
|
|
249
|
+
|
|
250
|
+
// Peek at jobs
|
|
251
|
+
const nextJobs = await queue.peek(JobStatus.PENDING, 5);
|
|
252
|
+
|
|
253
|
+
// Get queue statistics
|
|
254
|
+
const stats = queue.getStats();
|
|
255
|
+
console.log(`Completed: ${stats.completedJobs}, Failed: ${stats.failedJobs}`);
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
### Progress Tracking
|
|
259
|
+
|
|
260
|
+
```typescript
|
|
261
|
+
// Listen to progress for a specific job
|
|
262
|
+
const removeListener = queue.onJobProgress(jobId, (progress, message, details) => {
|
|
263
|
+
console.log(`Job ${jobId}: ${progress}% - ${message}`);
|
|
264
|
+
if (details) {
|
|
265
|
+
console.log("Details:", details);
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
// You can also listen on the job itself
|
|
270
|
+
const job = new DownloadJob({ input: { url: "...", filename: "..." } });
|
|
271
|
+
job.onJobProgress((progress, message, details) => {
|
|
272
|
+
console.log(`Progress: ${progress}% - ${message}`);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
const jobId = await queue.add(job);
|
|
276
|
+
|
|
277
|
+
// Wait for completion
|
|
278
|
+
try {
|
|
279
|
+
const result = await queue.waitFor(jobId);
|
|
280
|
+
console.log("Download completed:", result);
|
|
281
|
+
} finally {
|
|
282
|
+
removeListener(); // Clean up listener
|
|
283
|
+
}
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
### Error Handling and Retries
|
|
287
|
+
|
|
288
|
+
```typescript
|
|
289
|
+
import { RetryableJobError, PermanentJobError } from "@workglow/job-queue";
|
|
290
|
+
|
|
291
|
+
class ApiCallJob extends Job<{ endpoint: string }, { data: any }> {
|
|
292
|
+
async execute(input: { endpoint: string }, context: IJobExecuteContext) {
|
|
293
|
+
try {
|
|
294
|
+
const response = await fetch(input.endpoint);
|
|
295
|
+
|
|
296
|
+
if (response.status === 429) {
|
|
297
|
+
// Rate limited - retry with delay
|
|
298
|
+
throw new RetryableJobError(
|
|
299
|
+
"Rate limited",
|
|
300
|
+
new Date(Date.now() + 60000) // Retry in 1 minute
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (response.status === 404) {
|
|
305
|
+
// Not found - don't retry
|
|
306
|
+
throw new PermanentJobError("Endpoint not found");
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (!response.ok) {
|
|
310
|
+
// Server error - allow retries
|
|
311
|
+
throw new RetryableJobError(`HTTP ${response.status}`);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return { data: await response.json() };
|
|
315
|
+
} catch (error) {
|
|
316
|
+
if (error instanceof RetryableJobError || error instanceof PermanentJobError) {
|
|
317
|
+
throw error;
|
|
318
|
+
}
|
|
319
|
+
// Network errors etc. - allow retries
|
|
320
|
+
throw new RetryableJobError(error.message);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Create job with retry configuration
|
|
326
|
+
const apiJob = new ApiCallJob({
|
|
327
|
+
input: { endpoint: "https://api.example.com/data" },
|
|
328
|
+
maxRetries: 5, // Try up to 5 times
|
|
329
|
+
});
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
### Event Listeners
|
|
333
|
+
|
|
334
|
+
```typescript
|
|
335
|
+
// Listen to all queue events
|
|
336
|
+
queue.on("queue_start", (queueName) => {
|
|
337
|
+
console.log(`Queue ${queueName} started`);
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
queue.on("job_start", (queueName, jobId) => {
|
|
341
|
+
console.log(`Job ${jobId} started in queue ${queueName}`);
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
queue.on("job_complete", (queueName, jobId, output) => {
|
|
345
|
+
console.log(`Job ${jobId} completed with output:`, output);
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
queue.on("job_error", (queueName, jobId, error) => {
|
|
349
|
+
console.error(`Job ${jobId} failed with error: ${error}`);
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
queue.on("job_retry", (queueName, jobId, runAfter) => {
|
|
353
|
+
console.log(`Job ${jobId} will retry at ${runAfter}`);
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
queue.on("job_progress", (queueName, jobId, progress, message, details) => {
|
|
357
|
+
console.log(`Job ${jobId}: ${progress}% - ${message}`);
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
queue.on("queue_stats_update", (queueName, stats) => {
|
|
361
|
+
console.log(`Queue stats:`, stats);
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
// Wait for specific events
|
|
365
|
+
const [queueName] = await queue.waitOn("queue_start");
|
|
366
|
+
const [queueName, jobId, output] = await queue.waitOn("job_complete");
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
### Job Completion and Output
|
|
370
|
+
|
|
371
|
+
```typescript
|
|
372
|
+
// Wait for job completion
|
|
373
|
+
const jobId = await queue.add(job);
|
|
374
|
+
|
|
375
|
+
try {
|
|
376
|
+
// This will resolve with the job output or reject with an error
|
|
377
|
+
const output = await queue.waitFor(jobId);
|
|
378
|
+
console.log("Job completed successfully:", output);
|
|
379
|
+
} catch (error) {
|
|
380
|
+
console.error("Job failed:", error);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Check if output already exists for given input (caching)
|
|
384
|
+
const existingOutput = await queue.outputForInput({
|
|
385
|
+
url: "https://example.com/file.zip",
|
|
386
|
+
filename: "file.zip",
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
if (existingOutput) {
|
|
390
|
+
console.log("Already processed:", existingOutput);
|
|
391
|
+
} else {
|
|
392
|
+
// Add new job
|
|
393
|
+
const newJobId = await queue.add(
|
|
394
|
+
new DownloadJob({
|
|
395
|
+
input: { url: "https://example.com/file.zip", filename: "file.zip" },
|
|
396
|
+
})
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Abort a running job
|
|
401
|
+
await queue.abort(jobId);
|
|
402
|
+
|
|
403
|
+
// Get job details
|
|
404
|
+
const job = await queue.get(jobId);
|
|
405
|
+
if (job) {
|
|
406
|
+
console.log(`Job status: ${job.status}, progress: ${job.progress}%`);
|
|
407
|
+
}
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
## Storage Configurations
|
|
411
|
+
|
|
412
|
+
### In-Memory Storage
|
|
413
|
+
|
|
414
|
+
```typescript
|
|
415
|
+
import { JobQueue } from "@workglow/job-queue";
|
|
416
|
+
import { InMemoryQueueStorage } from "@workglow/storage";
|
|
417
|
+
|
|
418
|
+
const queue = new JobQueue("my-queue", MyJob, {
|
|
419
|
+
storage: new InMemoryQueueStorage("my-queue"),
|
|
420
|
+
// Jobs are lost when the process restarts
|
|
421
|
+
});
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
### IndexedDB Storage (Browser)
|
|
425
|
+
|
|
426
|
+
```typescript
|
|
427
|
+
import { JobQueue } from "@workglow/job-queue";
|
|
428
|
+
import { IndexedDbQueueStorage } from "@workglow/storage";
|
|
429
|
+
|
|
430
|
+
// For browser environments
|
|
431
|
+
const queue = new JobQueue("my-queue", MyJob, {
|
|
432
|
+
storage: new IndexedDbQueueStorage("my-queue"),
|
|
433
|
+
// Jobs persist in browser storage
|
|
434
|
+
});
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
### SQLite Storage (Node.js/Bun)
|
|
438
|
+
|
|
439
|
+
```typescript
|
|
440
|
+
import { JobQueue } from "@workglow/job-queue";
|
|
441
|
+
import { SqliteQueueStorage } from "@workglow/storage";
|
|
442
|
+
|
|
443
|
+
const queue = new JobQueue("my-queue", MyJob, {
|
|
444
|
+
storage: new SqliteQueueStorage("./jobs.db", "my-queue"),
|
|
445
|
+
// Jobs persist in SQLite file
|
|
446
|
+
});
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
### PostgreSQL Storage (Node.js/Bun)
|
|
450
|
+
|
|
451
|
+
```typescript
|
|
452
|
+
import { JobQueue } from "@workglow/job-queue";
|
|
453
|
+
import { PostgresQueueStorage } from "@workglow/storage";
|
|
454
|
+
import { Pool } from "pg";
|
|
455
|
+
|
|
456
|
+
const pool = new Pool({
|
|
457
|
+
host: "localhost",
|
|
458
|
+
port: 5432,
|
|
459
|
+
database: "jobs",
|
|
460
|
+
user: "postgres",
|
|
461
|
+
password: "password",
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
const queue = new JobQueue("my-queue", MyJob, {
|
|
465
|
+
storage: new PostgresQueueStorage(pool, "my-queue"),
|
|
466
|
+
// Jobs persist in PostgreSQL
|
|
467
|
+
});
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
## Rate Limiting Strategies
|
|
471
|
+
|
|
472
|
+
### Concurrency Limiter
|
|
473
|
+
|
|
474
|
+
```typescript
|
|
475
|
+
import { ConcurrencyLimiter } from "@workglow/job-queue";
|
|
476
|
+
|
|
477
|
+
// Limit to 5 concurrent jobs with 1 second minimum between starts
|
|
478
|
+
const limiter = new ConcurrencyLimiter(5, 1000);
|
|
479
|
+
|
|
480
|
+
const queue = new JobQueue("my-queue", MyJob, {
|
|
481
|
+
storage: new InMemoryQueueStorage("my-queue"),
|
|
482
|
+
limiter,
|
|
483
|
+
});
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
### Delay Limiter
|
|
487
|
+
|
|
488
|
+
```typescript
|
|
489
|
+
import { DelayLimiter } from "@workglow/job-queue";
|
|
490
|
+
|
|
491
|
+
// Minimum 500ms delay between job starts
|
|
492
|
+
const limiter = new DelayLimiter(500);
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
### Rate Limiter
|
|
496
|
+
|
|
497
|
+
```typescript
|
|
498
|
+
import { InMemoryRateLimiter } from "@workglow/job-queue";
|
|
499
|
+
|
|
500
|
+
// Max 10 executions per 60-second window
|
|
501
|
+
const limiter = new InMemoryRateLimiter({
|
|
502
|
+
maxExecutions: 10,
|
|
503
|
+
windowSizeInSeconds: 60,
|
|
504
|
+
initialBackoffDelay: 1000, // Start with 1s backoff
|
|
505
|
+
backoffMultiplier: 2, // Double delay each time
|
|
506
|
+
maxBackoffDelay: 60000, // Max 60s backoff
|
|
507
|
+
});
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
### Composite Limiter
|
|
511
|
+
|
|
512
|
+
```typescript
|
|
513
|
+
import { CompositeLimiter, ConcurrencyLimiter, DelayLimiter } from "@workglow/job-queue";
|
|
514
|
+
|
|
515
|
+
// Combine multiple limiting strategies
|
|
516
|
+
const limiter = new CompositeLimiter([
|
|
517
|
+
new ConcurrencyLimiter(3), // Max 3 concurrent
|
|
518
|
+
new DelayLimiter(100), // 100ms between starts
|
|
519
|
+
new InMemoryRateLimiter({
|
|
520
|
+
// Max 20 per minute
|
|
521
|
+
maxExecutions: 20,
|
|
522
|
+
windowSizeInSeconds: 60,
|
|
523
|
+
}),
|
|
524
|
+
]);
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
## Queue Modes
|
|
528
|
+
|
|
529
|
+
```typescript
|
|
530
|
+
import { QueueMode } from "@workglow/job-queue";
|
|
531
|
+
|
|
532
|
+
// Client mode - can add jobs and get progress, but doesn't process them
|
|
533
|
+
await queue.start(QueueMode.CLIENT);
|
|
534
|
+
|
|
535
|
+
// Server mode - processes jobs but can't add new ones
|
|
536
|
+
await queue.start(QueueMode.SERVER);
|
|
537
|
+
|
|
538
|
+
// Both modes - can add and process jobs (default)
|
|
539
|
+
await queue.start(QueueMode.BOTH);
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
## API Reference
|
|
543
|
+
|
|
544
|
+
### JobQueue Methods
|
|
545
|
+
|
|
546
|
+
```typescript
|
|
547
|
+
interface IJobQueue<Input, Output> {
|
|
548
|
+
// Queue management
|
|
549
|
+
start(mode?: QueueMode): Promise<this>;
|
|
550
|
+
stop(): Promise<this>;
|
|
551
|
+
clear(): Promise<this>;
|
|
552
|
+
restart(): Promise<this>;
|
|
553
|
+
|
|
554
|
+
// Job operations
|
|
555
|
+
add(job: Job<Input, Output>): Promise<unknown>;
|
|
556
|
+
get(id: unknown): Promise<Job<Input, Output> | undefined>;
|
|
557
|
+
waitFor(jobId: unknown): Promise<Output | undefined>;
|
|
558
|
+
abort(jobId: unknown): Promise<void>;
|
|
559
|
+
|
|
560
|
+
// Queue inspection
|
|
561
|
+
peek(status?: JobStatus, num?: number): Promise<Job<Input, Output>[]>;
|
|
562
|
+
size(status?: JobStatus): Promise<number>;
|
|
563
|
+
getStats(): JobQueueStats;
|
|
564
|
+
|
|
565
|
+
// Utility
|
|
566
|
+
outputForInput(input: Input): Promise<Output | null>;
|
|
567
|
+
getJobsByRunId(jobRunId: string): Promise<Job<Input, Output>[]>;
|
|
568
|
+
|
|
569
|
+
// Progress tracking
|
|
570
|
+
updateProgress(
|
|
571
|
+
jobId: unknown,
|
|
572
|
+
progress: number,
|
|
573
|
+
message?: string,
|
|
574
|
+
details?: Record<string, any>
|
|
575
|
+
): Promise<void>;
|
|
576
|
+
onJobProgress(jobId: unknown, listener: JobProgressListener): () => void;
|
|
577
|
+
}
|
|
578
|
+
```
|
|
579
|
+
|
|
580
|
+
### Job Class
|
|
581
|
+
|
|
582
|
+
```typescript
|
|
583
|
+
class Job<Input, Output> {
|
|
584
|
+
// Properties
|
|
585
|
+
id: unknown;
|
|
586
|
+
input: Input;
|
|
587
|
+
output: Output | null;
|
|
588
|
+
status: JobStatus;
|
|
589
|
+
progress: number;
|
|
590
|
+
progressMessage: string;
|
|
591
|
+
progressDetails: Record<string, any> | null;
|
|
592
|
+
maxRetries: number;
|
|
593
|
+
runAttempts: number;
|
|
594
|
+
error: string | null;
|
|
595
|
+
createdAt: Date;
|
|
596
|
+
completedAt: Date | null;
|
|
597
|
+
|
|
598
|
+
// Methods
|
|
599
|
+
abstract execute(input: Input, context: IJobExecuteContext): Promise<Output>;
|
|
600
|
+
updateProgress(progress: number, message?: string, details?: Record<string, any>): Promise<void>;
|
|
601
|
+
onJobProgress(listener: JobProgressListener): () => void;
|
|
602
|
+
}
|
|
603
|
+
```
|
|
604
|
+
|
|
605
|
+
## TypeScript Types
|
|
606
|
+
|
|
607
|
+
```typescript
|
|
608
|
+
// Job statuses
|
|
609
|
+
enum JobStatus {
|
|
610
|
+
PENDING = "PENDING",
|
|
611
|
+
RUNNING = "RUNNING",
|
|
612
|
+
COMPLETED = "COMPLETED",
|
|
613
|
+
FAILED = "FAILED",
|
|
614
|
+
ABORTING = "ABORTING",
|
|
615
|
+
DISABLED = "DISABLED",
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// Queue options
|
|
619
|
+
interface JobQueueOptions<Input, Output> {
|
|
620
|
+
deleteAfterCompletionMs?: number;
|
|
621
|
+
deleteAfterFailureMs?: number;
|
|
622
|
+
deleteAfterDisabledMs?: number;
|
|
623
|
+
waitDurationInMilliseconds?: number;
|
|
624
|
+
limiter?: ILimiter;
|
|
625
|
+
storage?: IQueueStorage<Input, Output>;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// Job execution context
|
|
629
|
+
interface IJobExecuteContext {
|
|
630
|
+
signal: AbortSignal;
|
|
631
|
+
updateProgress: (
|
|
632
|
+
progress: number,
|
|
633
|
+
message?: string,
|
|
634
|
+
details?: Record<string, any>
|
|
635
|
+
) => Promise<void>;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
// Progress listener
|
|
639
|
+
type JobProgressListener = (
|
|
640
|
+
progress: number,
|
|
641
|
+
message: string,
|
|
642
|
+
details: Record<string, any> | null
|
|
643
|
+
) => void;
|
|
644
|
+
|
|
645
|
+
// Queue statistics
|
|
646
|
+
interface JobQueueStats {
|
|
647
|
+
totalJobs: number;
|
|
648
|
+
completedJobs: number;
|
|
649
|
+
failedJobs: number;
|
|
650
|
+
abortedJobs: number;
|
|
651
|
+
retriedJobs: number;
|
|
652
|
+
disabledJobs: number;
|
|
653
|
+
averageProcessingTime?: number;
|
|
654
|
+
lastUpdateTime: Date;
|
|
655
|
+
}
|
|
656
|
+
```
|
|
657
|
+
|
|
658
|
+
## Testing
|
|
659
|
+
|
|
660
|
+
Run tests:
|
|
661
|
+
|
|
662
|
+
```bash
|
|
663
|
+
bun test
|
|
664
|
+
```
|
|
665
|
+
|
|
666
|
+
Example test:
|
|
667
|
+
|
|
668
|
+
```typescript
|
|
669
|
+
import { describe, it, expect } from "vitest";
|
|
670
|
+
import { JobQueue } from "@workglow/job-queue";
|
|
671
|
+
import { InMemoryQueueStorage } from "@workglow/storage";
|
|
672
|
+
|
|
673
|
+
describe("JobQueue", () => {
|
|
674
|
+
it("should process jobs successfully", async () => {
|
|
675
|
+
const queue = new JobQueue("test", TestJob, {
|
|
676
|
+
storage: new InMemoryQueueStorage("test"),
|
|
677
|
+
});
|
|
678
|
+
|
|
679
|
+
await queue.start();
|
|
680
|
+
|
|
681
|
+
const job = new TestJob({ input: { data: "test" } });
|
|
682
|
+
const jobId = await queue.add(job);
|
|
683
|
+
const result = await queue.waitFor(jobId);
|
|
684
|
+
|
|
685
|
+
expect(result).toEqual({ processed: "test" });
|
|
686
|
+
|
|
687
|
+
await queue.stop();
|
|
688
|
+
});
|
|
689
|
+
});
|
|
690
|
+
```
|
|
691
|
+
|
|
692
|
+
## License
|
|
693
|
+
|
|
694
|
+
Apache 2.0 - See [LICENSE](./LICENSE) for details
|