@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.
Files changed (43) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +694 -0
  3. package/dist/browser.js +1075 -0
  4. package/dist/browser.js.map +20 -0
  5. package/dist/bun.js +1375 -0
  6. package/dist/bun.js.map +22 -0
  7. package/dist/common-server.d.ts +9 -0
  8. package/dist/common-server.d.ts.map +1 -0
  9. package/dist/common.d.ts +18 -0
  10. package/dist/common.d.ts.map +1 -0
  11. package/dist/job/IJobQueue.d.ts +160 -0
  12. package/dist/job/IJobQueue.d.ts.map +1 -0
  13. package/dist/job/Job.d.ts +87 -0
  14. package/dist/job/Job.d.ts.map +1 -0
  15. package/dist/job/JobError.d.ts +61 -0
  16. package/dist/job/JobError.d.ts.map +1 -0
  17. package/dist/job/JobQueue.d.ts +272 -0
  18. package/dist/job/JobQueue.d.ts.map +1 -0
  19. package/dist/job/JobQueueEventListeners.d.ts +30 -0
  20. package/dist/job/JobQueueEventListeners.d.ts.map +1 -0
  21. package/dist/limiter/CompositeLimiter.d.ts +18 -0
  22. package/dist/limiter/CompositeLimiter.d.ts.map +1 -0
  23. package/dist/limiter/ConcurrencyLimiter.d.ts +24 -0
  24. package/dist/limiter/ConcurrencyLimiter.d.ts.map +1 -0
  25. package/dist/limiter/DelayLimiter.d.ts +18 -0
  26. package/dist/limiter/DelayLimiter.d.ts.map +1 -0
  27. package/dist/limiter/EvenlySpacedRateLimiter.d.ts +35 -0
  28. package/dist/limiter/EvenlySpacedRateLimiter.d.ts.map +1 -0
  29. package/dist/limiter/ILimiter.d.ts +27 -0
  30. package/dist/limiter/ILimiter.d.ts.map +1 -0
  31. package/dist/limiter/InMemoryRateLimiter.d.ts +32 -0
  32. package/dist/limiter/InMemoryRateLimiter.d.ts.map +1 -0
  33. package/dist/limiter/NullLimiter.d.ts +19 -0
  34. package/dist/limiter/NullLimiter.d.ts.map +1 -0
  35. package/dist/limiter/PostgresRateLimiter.d.ts +53 -0
  36. package/dist/limiter/PostgresRateLimiter.d.ts.map +1 -0
  37. package/dist/limiter/SqliteRateLimiter.d.ts +44 -0
  38. package/dist/limiter/SqliteRateLimiter.d.ts.map +1 -0
  39. package/dist/node.js +1374 -0
  40. package/dist/node.js.map +22 -0
  41. package/dist/types.d.ts +7 -0
  42. package/dist/types.d.ts.map +1 -0
  43. 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