@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
@@ -0,0 +1,1075 @@
1
+ // src/job/IJobQueue.ts
2
+ import { JobStatus } from "@workglow/storage";
3
+ var QueueMode;
4
+ ((QueueMode2) => {
5
+ QueueMode2["CLIENT"] = "CLIENT";
6
+ QueueMode2["SERVER"] = "SERVER";
7
+ QueueMode2["BOTH"] = "BOTH";
8
+ })(QueueMode ||= {});
9
+ // src/job/JobError.ts
10
+ import { BaseError } from "@workglow/util";
11
+
12
+ class JobError extends BaseError {
13
+ message;
14
+ static type = "JobError";
15
+ retryable = false;
16
+ constructor(message) {
17
+ super(message);
18
+ this.message = message;
19
+ }
20
+ }
21
+
22
+ class JobNotFoundError extends JobError {
23
+ static type = "JobNotFoundError";
24
+ constructor(message = "Job not found") {
25
+ super(message);
26
+ }
27
+ }
28
+
29
+ class RetryableJobError extends JobError {
30
+ retryDate;
31
+ static type = "RetryableJobError";
32
+ constructor(message, retryDate) {
33
+ super(message);
34
+ this.retryDate = retryDate;
35
+ this.retryable = true;
36
+ }
37
+ }
38
+
39
+ class PermanentJobError extends JobError {
40
+ static type = "PermanentJobError";
41
+ constructor(message) {
42
+ super(message);
43
+ }
44
+ }
45
+
46
+ class AbortSignalJobError extends PermanentJobError {
47
+ static type = "AbortSignalJobError";
48
+ constructor(message) {
49
+ super(message);
50
+ }
51
+ }
52
+
53
+ class JobDisabledError extends PermanentJobError {
54
+ static type = "JobDisabledError";
55
+ }
56
+
57
+ // src/job/Job.ts
58
+ class Job {
59
+ id;
60
+ jobRunId;
61
+ queueName;
62
+ input;
63
+ maxRetries;
64
+ createdAt;
65
+ fingerprint;
66
+ status = JobStatus.PENDING;
67
+ runAfter;
68
+ output = null;
69
+ runAttempts = 0;
70
+ lastRanAt = null;
71
+ completedAt = null;
72
+ deadlineAt = null;
73
+ error = null;
74
+ errorCode = null;
75
+ progress = 0;
76
+ progressMessage = "";
77
+ progressDetails = null;
78
+ queue;
79
+ constructor({
80
+ queueName,
81
+ input,
82
+ jobRunId,
83
+ id,
84
+ error = null,
85
+ errorCode = null,
86
+ fingerprint = undefined,
87
+ output = null,
88
+ maxRetries = 10,
89
+ createdAt = new Date,
90
+ completedAt = null,
91
+ status = JobStatus.PENDING,
92
+ deadlineAt = null,
93
+ runAttempts = 0,
94
+ lastRanAt = null,
95
+ runAfter = new Date,
96
+ progress = 0,
97
+ progressMessage = "",
98
+ progressDetails = null
99
+ }) {
100
+ this.runAfter = runAfter ?? new Date;
101
+ this.createdAt = createdAt ?? new Date;
102
+ this.lastRanAt = lastRanAt ?? null;
103
+ this.deadlineAt = deadlineAt ?? null;
104
+ this.completedAt = completedAt ?? null;
105
+ this.queueName = queueName;
106
+ this.id = id;
107
+ this.jobRunId = jobRunId;
108
+ this.status = status;
109
+ this.fingerprint = fingerprint;
110
+ this.input = input;
111
+ this.maxRetries = maxRetries;
112
+ this.runAttempts = runAttempts;
113
+ this.output = output;
114
+ this.error = error;
115
+ this.errorCode = errorCode;
116
+ this.progress = progress;
117
+ this.progressMessage = progressMessage;
118
+ this.progressDetails = progressDetails;
119
+ }
120
+ async execute(input, context) {
121
+ throw new JobError("Method not implemented.");
122
+ }
123
+ progressListeners = new Set;
124
+ async updateProgress(progress, message = "", details = null) {
125
+ this.progress = progress;
126
+ this.progressMessage = message;
127
+ this.progressDetails = details;
128
+ for (const listener of this.progressListeners) {
129
+ listener(progress, message, details);
130
+ }
131
+ await this.queue?.updateProgress(this.id, progress, message, details);
132
+ }
133
+ onJobProgress(listener) {
134
+ this.progressListeners.add(listener);
135
+ return () => {
136
+ this.progressListeners.delete(listener);
137
+ };
138
+ }
139
+ }
140
+ // src/job/JobQueue.ts
141
+ import {
142
+ InMemoryQueueStorage,
143
+ QUEUE_STORAGE
144
+ } from "@workglow/storage";
145
+ import { EventEmitter, globalServiceRegistry, sleep } from "@workglow/util";
146
+
147
+ // src/limiter/ILimiter.ts
148
+ import { createServiceToken } from "@workglow/util";
149
+ var JOB_LIMITER = createServiceToken("jobqueue.limiter");
150
+
151
+ // src/limiter/NullLimiter.ts
152
+ import { createServiceToken as createServiceToken2 } from "@workglow/util";
153
+ var NULL_JOB_LIMITER = createServiceToken2("jobqueue.limiter.null");
154
+
155
+ class NullLimiter {
156
+ async canProceed() {
157
+ return true;
158
+ }
159
+ async recordJobStart() {}
160
+ async recordJobCompletion() {}
161
+ async getNextAvailableTime() {
162
+ return new Date;
163
+ }
164
+ async setNextAvailableTime(date) {}
165
+ async clear() {}
166
+ }
167
+
168
+ // src/job/JobQueue.ts
169
+ class JobQueue {
170
+ queueName;
171
+ jobClass;
172
+ options;
173
+ constructor(queueName, jobClass, options) {
174
+ this.queueName = queueName;
175
+ this.jobClass = jobClass;
176
+ const { limiter, storage, ...rest } = options;
177
+ this.options = {
178
+ waitDurationInMilliseconds: 100,
179
+ ...rest
180
+ };
181
+ if (limiter) {
182
+ this.limiter = limiter;
183
+ } else {
184
+ try {
185
+ this.limiter = globalServiceRegistry.get(JOB_LIMITER);
186
+ } catch (err) {
187
+ console.warn("Warning: did not find job limiter in global DI", err);
188
+ this.limiter = new NullLimiter;
189
+ }
190
+ }
191
+ if (storage) {
192
+ this.storage = storage;
193
+ } else {
194
+ try {
195
+ this.storage = globalServiceRegistry.get(QUEUE_STORAGE);
196
+ } catch (err) {
197
+ console.warn("Warning: did not find queue storage in global DI", err);
198
+ this.storage = new InMemoryQueueStorage(queueName);
199
+ }
200
+ }
201
+ this.stats = {
202
+ totalJobs: 0,
203
+ completedJobs: 0,
204
+ failedJobs: 0,
205
+ abortedJobs: 0,
206
+ retriedJobs: 0,
207
+ disabledJobs: 0,
208
+ lastUpdateTime: new Date
209
+ };
210
+ }
211
+ async get(id) {
212
+ if (!id)
213
+ throw new JobNotFoundError("Cannot get undefined job");
214
+ const job = await this.storage.get(id);
215
+ if (!job)
216
+ return;
217
+ return this.storageToClass(job);
218
+ }
219
+ async add(job) {
220
+ const jobId = await this.storage.add(this.classToStorage(job));
221
+ return jobId;
222
+ }
223
+ async next() {
224
+ const job = await this.storage.next();
225
+ if (!job)
226
+ return;
227
+ return this.storageToClass(job);
228
+ }
229
+ async peek(status, num) {
230
+ const jobs = await this.storage.peek(status, num);
231
+ return jobs.map((job) => this.storageToClass(job));
232
+ }
233
+ async size(status) {
234
+ return this.storage.size(status);
235
+ }
236
+ async delete(id) {
237
+ if (!id)
238
+ throw new JobNotFoundError("Cannot delete undefined job");
239
+ await this.storage.delete(id);
240
+ }
241
+ async disable(id) {
242
+ if (!id)
243
+ throw new JobNotFoundError("Cannot disable undefined job");
244
+ const job = await this.get(id);
245
+ if (!job)
246
+ throw new JobNotFoundError(`Job ${id} not found`);
247
+ await this.disableJob(job);
248
+ }
249
+ getStats() {
250
+ return { ...this.stats };
251
+ }
252
+ async outputForInput(input) {
253
+ if (!input)
254
+ throw new JobNotFoundError("Cannot get output for undefined input");
255
+ return this.storage.outputForInput(input);
256
+ }
257
+ async executeJob(job, signal) {
258
+ if (!job)
259
+ throw new JobNotFoundError("Cannot execute null or undefined job");
260
+ return await job.execute(job.input, {
261
+ signal,
262
+ updateProgress: this.updateProgress.bind(this, job.id)
263
+ });
264
+ }
265
+ async waitFor(jobId) {
266
+ if (!jobId)
267
+ throw new JobNotFoundError("Cannot wait for undefined job");
268
+ const { promise, resolve, reject } = Promise.withResolvers();
269
+ promise.catch(() => {});
270
+ const promises = this.activeJobPromises.get(jobId) || [];
271
+ promises.push({ resolve, reject });
272
+ this.activeJobPromises.set(jobId, promises);
273
+ const job = await this.get(jobId);
274
+ if (!job)
275
+ throw new JobNotFoundError(`Job ${jobId} not found`);
276
+ if (job.status === JobStatus.COMPLETED) {
277
+ return job.output;
278
+ }
279
+ if (job.status === JobStatus.DISABLED) {
280
+ return;
281
+ }
282
+ if (job.status === JobStatus.FAILED) {
283
+ throw this.buildErrorFromJob(job);
284
+ }
285
+ return promise;
286
+ }
287
+ buildErrorFromJob(job) {
288
+ const errorMessage = job.error || "Job failed";
289
+ if (job.errorCode === "PermanentJobError") {
290
+ return new PermanentJobError(errorMessage);
291
+ }
292
+ if (job.errorCode === "RetryableJobError") {
293
+ return new RetryableJobError(errorMessage);
294
+ }
295
+ if (job.errorCode === "AbortSignalJobError") {
296
+ return new AbortSignalJobError(errorMessage);
297
+ }
298
+ if (job.errorCode === "JobDisabledError") {
299
+ return new JobDisabledError(errorMessage);
300
+ }
301
+ return new JobError(errorMessage);
302
+ }
303
+ async updateProgress(jobId, progress, message = "", details = null) {
304
+ const job = await this.get(jobId);
305
+ if (!job)
306
+ throw new JobNotFoundError(`Job ${jobId} not found`);
307
+ if ([JobStatus.COMPLETED, JobStatus.FAILED, JobStatus.ABORTING, JobStatus.DISABLED].includes(job.status)) {
308
+ return;
309
+ }
310
+ progress = Math.max(0, Math.min(100, progress));
311
+ job.progress = progress;
312
+ job.progressMessage = message;
313
+ job.progressDetails = details;
314
+ await this.saveProgress(jobId, progress, message, details ?? null);
315
+ this.announceProgress(jobId, progress, message, details ?? null);
316
+ }
317
+ onJobProgress(jobId, listener) {
318
+ if (!this.jobProgressListeners.has(jobId)) {
319
+ this.jobProgressListeners.set(jobId, new Set);
320
+ }
321
+ const listeners = this.jobProgressListeners.get(jobId);
322
+ listeners.add(listener);
323
+ return () => {
324
+ const listeners2 = this.jobProgressListeners.get(jobId);
325
+ if (listeners2) {
326
+ listeners2.delete(listener);
327
+ if (listeners2.size === 0) {
328
+ this.jobProgressListeners.delete(jobId);
329
+ }
330
+ }
331
+ };
332
+ }
333
+ async start(mode = "BOTH" /* BOTH */) {
334
+ if (this.running) {
335
+ return this;
336
+ }
337
+ this.mode = mode;
338
+ this.running = true;
339
+ this.events.emit("queue_start", this.queueName);
340
+ if (this.mode === "SERVER" /* SERVER */ || this.mode === "BOTH" /* BOTH */) {
341
+ await this.fixupJobs();
342
+ await this.processJobs();
343
+ }
344
+ if (this.mode === "CLIENT" /* CLIENT */ || this.mode === "BOTH" /* BOTH */) {
345
+ await this.monitorJobs();
346
+ }
347
+ return this;
348
+ }
349
+ async stop() {
350
+ if (!this.running)
351
+ return this;
352
+ this.running = false;
353
+ const size = await this.size(JobStatus.PROCESSING);
354
+ const sleepTime = Math.max(100, size * 2);
355
+ await sleep(sleepTime);
356
+ for (const [jobId] of this.activeJobAbortControllers.entries()) {
357
+ this.abort(jobId);
358
+ }
359
+ this.activeJobPromises.forEach((promises) => promises.forEach(({ reject }) => reject(new PermanentJobError("Queue Stopped"))));
360
+ await sleep(sleepTime);
361
+ this.events.emit("queue_stop", this.queueName);
362
+ return this;
363
+ }
364
+ async clear() {
365
+ await this.storage.deleteAll();
366
+ this.activeJobAbortControllers.clear();
367
+ this.activeJobPromises.clear();
368
+ this.processingTimes.clear();
369
+ this.lastKnownProgress.clear();
370
+ this.jobProgressListeners.clear();
371
+ this.stats = {
372
+ totalJobs: 0,
373
+ completedJobs: 0,
374
+ failedJobs: 0,
375
+ abortedJobs: 0,
376
+ retriedJobs: 0,
377
+ disabledJobs: 0,
378
+ lastUpdateTime: new Date
379
+ };
380
+ this.emitStatsUpdate();
381
+ return this;
382
+ }
383
+ async restart() {
384
+ await this.stop();
385
+ await this.clear();
386
+ await this.start();
387
+ return this;
388
+ }
389
+ async abortJobRun(jobRunId) {
390
+ if (!jobRunId)
391
+ throw new JobNotFoundError("Cannot abort job run with undefined jobRunId");
392
+ const jobs = await this.getJobsByRunId(jobRunId);
393
+ await Promise.allSettled(jobs.map((job) => {
394
+ if ([JobStatus.PROCESSING, JobStatus.PENDING].includes(job.status)) {
395
+ this.abort(job.id);
396
+ }
397
+ }));
398
+ }
399
+ async getJobsByRunId(runId) {
400
+ if (!runId)
401
+ throw new JobNotFoundError("Cannot get jobs by undefined runId");
402
+ const jobs = await this.storage.getByRunId(runId);
403
+ return jobs.map((job) => this.storageToClass(job));
404
+ }
405
+ on(event, listener) {
406
+ this.events.on(event, listener);
407
+ }
408
+ off(event, listener) {
409
+ this.events.off(event, listener);
410
+ }
411
+ once(event, listener) {
412
+ this.events.once(event, listener);
413
+ }
414
+ waitOn(event) {
415
+ return this.events.waitOn(event);
416
+ }
417
+ limiter;
418
+ storage;
419
+ running = false;
420
+ stats = {
421
+ totalJobs: 0,
422
+ completedJobs: 0,
423
+ failedJobs: 0,
424
+ abortedJobs: 0,
425
+ retriedJobs: 0,
426
+ disabledJobs: 0,
427
+ lastUpdateTime: new Date
428
+ };
429
+ events = new EventEmitter;
430
+ activeJobAbortControllers = new Map;
431
+ activeJobPromises = new Map;
432
+ processingTimes = new Map;
433
+ mode = "BOTH" /* BOTH */;
434
+ jobProgressListeners = new Map;
435
+ lastKnownProgress = new Map;
436
+ async saveProgress(id, progress, message, details) {
437
+ if (!id)
438
+ throw new JobNotFoundError("Cannot save progress for undefined job");
439
+ await this.storage.saveProgress(id, progress, message, details);
440
+ }
441
+ createAbortController(jobId) {
442
+ if (!jobId)
443
+ throw new JobNotFoundError("Cannot create abort controller for undefined job");
444
+ if (this.activeJobAbortControllers.has(jobId)) {
445
+ return this.activeJobAbortControllers.get(jobId);
446
+ }
447
+ const abortController = new AbortController;
448
+ abortController.signal.addEventListener("abort", () => this.handleAbort(jobId));
449
+ this.activeJobAbortControllers.set(jobId, abortController);
450
+ return abortController;
451
+ }
452
+ async validateJobState(job) {
453
+ if (job.status === JobStatus.COMPLETED) {
454
+ throw new PermanentJobError(`Job ${job.id} is already completed`);
455
+ }
456
+ if (job.status === JobStatus.FAILED) {
457
+ throw new PermanentJobError(`Job ${job.id} has failed`);
458
+ }
459
+ if (job.status === JobStatus.ABORTING || this.activeJobAbortControllers.get(job.id)?.signal.aborted) {
460
+ throw new AbortSignalJobError(`Job ${job.id} is being aborted`);
461
+ }
462
+ if (job.deadlineAt && job.deadlineAt < new Date) {
463
+ throw new PermanentJobError(`Job ${job.id} has exceeded its deadline`);
464
+ }
465
+ if (job.status === JobStatus.DISABLED) {
466
+ throw new JobDisabledError(`Job ${job.id} has been disabled`);
467
+ }
468
+ }
469
+ normalizeError(err) {
470
+ if (err instanceof JobError) {
471
+ return err;
472
+ }
473
+ if (err instanceof Error) {
474
+ return err;
475
+ }
476
+ return new PermanentJobError(String(err));
477
+ }
478
+ updateAverageProcessingTime() {
479
+ const times = Array.from(this.processingTimes.values());
480
+ if (times.length > 0) {
481
+ this.stats.averageProcessingTime = times.reduce((a, b) => a + b, 0) / times.length;
482
+ }
483
+ }
484
+ emitStatsUpdate() {
485
+ this.stats.lastUpdateTime = new Date;
486
+ this.events.emit("queue_stats_update", this.queueName, { ...this.stats });
487
+ }
488
+ announceProgress(jobId, progress, message, details) {
489
+ this.lastKnownProgress.set(jobId, {
490
+ progress,
491
+ message,
492
+ details
493
+ });
494
+ this.events.emit("job_progress", this.queueName, jobId, progress, message, details);
495
+ const listeners = this.jobProgressListeners.get(jobId);
496
+ if (listeners) {
497
+ for (const listener of listeners) {
498
+ listener(progress, message, details);
499
+ }
500
+ }
501
+ }
502
+ storageToClass(details) {
503
+ const toDate = (date) => {
504
+ if (!date)
505
+ return null;
506
+ const d = new Date(date);
507
+ return isNaN(d.getTime()) ? null : d;
508
+ };
509
+ const job = new this.jobClass({
510
+ id: details.id,
511
+ jobRunId: details.job_run_id,
512
+ queueName: details.queue,
513
+ fingerprint: details.fingerprint,
514
+ input: details.input,
515
+ output: details.output,
516
+ runAfter: toDate(details.run_after),
517
+ createdAt: toDate(details.created_at),
518
+ deadlineAt: toDate(details.deadline_at),
519
+ lastRanAt: toDate(details.last_ran_at),
520
+ completedAt: toDate(details.completed_at),
521
+ progress: details.progress || 0,
522
+ progressMessage: details.progress_message || "",
523
+ progressDetails: details.progress_details ?? null,
524
+ status: details.status,
525
+ error: details.error ?? null,
526
+ errorCode: details.error_code ?? null,
527
+ runAttempts: details.run_attempts ?? 0,
528
+ maxRetries: details.max_retries ?? 10
529
+ });
530
+ job.queue = this;
531
+ return job;
532
+ }
533
+ classToStorage(job) {
534
+ const dateToISOString = (date) => {
535
+ if (!date)
536
+ return null;
537
+ return isNaN(date.getTime()) ? null : date.toISOString();
538
+ };
539
+ const now = new Date().toISOString();
540
+ return {
541
+ id: job.id,
542
+ job_run_id: job.jobRunId,
543
+ queue: job.queueName || this.queueName,
544
+ fingerprint: job.fingerprint,
545
+ input: job.input,
546
+ status: job.status,
547
+ output: job.output ?? null,
548
+ error: job.error === null ? null : String(job.error),
549
+ error_code: job.errorCode || null,
550
+ run_attempts: job.runAttempts ?? 0,
551
+ max_retries: job.maxRetries ?? 10,
552
+ run_after: dateToISOString(job.runAfter) ?? now,
553
+ created_at: dateToISOString(job.createdAt) ?? now,
554
+ deadline_at: dateToISOString(job.deadlineAt),
555
+ last_ran_at: dateToISOString(job.lastRanAt),
556
+ completed_at: dateToISOString(job.completedAt),
557
+ progress: job.progress ?? 0,
558
+ progress_message: job.progressMessage ?? "",
559
+ progress_details: job.progressDetails ?? null
560
+ };
561
+ }
562
+ async rescheduleJob(job, retryDate) {
563
+ try {
564
+ job.status = JobStatus.PENDING;
565
+ const nextAvailableTime = await this.limiter.getNextAvailableTime();
566
+ job.runAfter = retryDate instanceof Date ? retryDate : nextAvailableTime;
567
+ job.progress = 0;
568
+ job.progressMessage = "";
569
+ job.progressDetails = null;
570
+ await this.storage.complete(this.classToStorage(job));
571
+ this.stats.retriedJobs++;
572
+ this.events.emit("job_retry", this.queueName, job.id, job.runAfter);
573
+ } catch (err) {
574
+ console.error("rescheduleJob", err);
575
+ }
576
+ }
577
+ async disableJob(job) {
578
+ try {
579
+ job.status = JobStatus.DISABLED;
580
+ job.progress = 100;
581
+ job.completedAt = new Date;
582
+ job.progressMessage = "";
583
+ job.progressDetails = null;
584
+ await this.storage.complete(this.classToStorage(job));
585
+ if (this.options.deleteAfterDisabledMs === 0) {
586
+ await this.delete(job.id);
587
+ }
588
+ this.stats.disabledJobs++;
589
+ this.events.emit("job_disabled", this.queueName, job.id);
590
+ const promises = this.activeJobPromises.get(job.id) || [];
591
+ promises.forEach(({ resolve }) => resolve(undefined));
592
+ this.activeJobPromises.delete(job.id);
593
+ } catch (err) {
594
+ console.error("disableJob", err);
595
+ }
596
+ }
597
+ async failJob(job, error) {
598
+ try {
599
+ job.status = JobStatus.FAILED;
600
+ job.progress = 100;
601
+ job.completedAt = new Date;
602
+ job.progressMessage = "";
603
+ job.progressDetails = null;
604
+ job.error = error.message;
605
+ job.errorCode = error?.constructor?.name ?? null;
606
+ await this.storage.complete(this.classToStorage(job));
607
+ if (this.options.deleteAfterFailureMs === 0) {
608
+ await this.delete(job.id);
609
+ }
610
+ this.stats.failedJobs++;
611
+ this.events.emit("job_error", this.queueName, job.id, `${error.name}: ${error.message}`);
612
+ const promises = this.activeJobPromises.get(job.id) || [];
613
+ promises.forEach(({ reject }) => reject(error));
614
+ this.activeJobPromises.delete(job.id);
615
+ } catch (err) {
616
+ console.error("failJob errored out?", err);
617
+ }
618
+ this.activeJobAbortControllers.delete(job.id);
619
+ this.lastKnownProgress.delete(job.id);
620
+ this.jobProgressListeners.delete(job.id);
621
+ this.activeJobPromises.delete(job.id);
622
+ }
623
+ async completeJob(job, output) {
624
+ try {
625
+ job.status = JobStatus.COMPLETED;
626
+ job.progress = 100;
627
+ job.progressMessage = "";
628
+ job.progressDetails = null;
629
+ job.completedAt = new Date;
630
+ job.output = output ?? null;
631
+ job.error = null;
632
+ job.errorCode = null;
633
+ await this.storage.complete(this.classToStorage(job));
634
+ if (job && this.options.deleteAfterCompletionMs === 0) {
635
+ await this.delete(job.id);
636
+ }
637
+ this.stats.completedJobs++;
638
+ this.events.emit("job_complete", this.queueName, job.id, output);
639
+ const promises = this.activeJobPromises.get(job.id);
640
+ if (promises) {
641
+ promises.forEach(({ resolve }) => resolve(output));
642
+ }
643
+ this.activeJobPromises.delete(job.id);
644
+ } catch (err) {
645
+ console.error("completeJob errored out?", err);
646
+ }
647
+ this.activeJobAbortControllers.delete(job.id);
648
+ this.lastKnownProgress.delete(job.id);
649
+ this.jobProgressListeners.delete(job.id);
650
+ this.activeJobPromises.delete(job.id);
651
+ }
652
+ async abort(jobId) {
653
+ if (!jobId)
654
+ throw new JobNotFoundError("Cannot abort undefined job");
655
+ await this.storage.abort(jobId);
656
+ let controller = this.activeJobAbortControllers.get(jobId);
657
+ if (!controller) {
658
+ controller = this.createAbortController(jobId);
659
+ }
660
+ if (!controller.signal.aborted) {
661
+ controller.abort();
662
+ }
663
+ this.events.emit("job_aborting", this.queueName, jobId);
664
+ }
665
+ async handleAbort(jobId) {
666
+ const promises = this.activeJobPromises.get(jobId);
667
+ if (promises) {
668
+ const job = await this.get(jobId);
669
+ if (!job) {
670
+ console.error("handleAbort: job not found", jobId);
671
+ return;
672
+ }
673
+ const error = new AbortSignalJobError("Job Aborted");
674
+ this.failJob(job, error);
675
+ }
676
+ this.stats.abortedJobs++;
677
+ }
678
+ async processSingleJob(job) {
679
+ if (!job || !job.id)
680
+ throw new JobNotFoundError("Invalid job provided for processing");
681
+ const startTime = Date.now();
682
+ try {
683
+ await this.validateJobState(job);
684
+ await this.limiter.recordJobStart();
685
+ this.emitStatsUpdate();
686
+ const abortController = this.createAbortController(job.id);
687
+ this.lastKnownProgress.set(job.id, {
688
+ progress: 0,
689
+ message: "",
690
+ details: null
691
+ });
692
+ this.events.emit("job_start", this.queueName, job.id);
693
+ const output = await this.executeJob(job, abortController.signal);
694
+ await this.completeJob(job, output);
695
+ this.processingTimes.set(job.id, Date.now() - startTime);
696
+ this.updateAverageProcessingTime();
697
+ } catch (err) {
698
+ const error = this.normalizeError(err);
699
+ if (error instanceof RetryableJobError) {
700
+ if (job.runAttempts > job.maxRetries) {
701
+ await this.failJob(job, error);
702
+ } else {
703
+ await this.rescheduleJob(job, error.retryDate);
704
+ }
705
+ } else {
706
+ await this.failJob(job, error);
707
+ }
708
+ } finally {
709
+ await this.limiter.recordJobCompletion();
710
+ this.emitStatsUpdate();
711
+ }
712
+ }
713
+ async processJobs() {
714
+ if (!this.running) {
715
+ return;
716
+ }
717
+ try {
718
+ await this.cleanUpJobs();
719
+ const canProceed = await this.limiter.canProceed();
720
+ if (canProceed) {
721
+ const job = await this.next();
722
+ if (job) {
723
+ this.processSingleJob(job);
724
+ }
725
+ }
726
+ } finally {
727
+ setTimeout(() => this.processJobs(), this.options.waitDurationInMilliseconds);
728
+ }
729
+ }
730
+ async cleanUpJobs() {
731
+ const abortingJobs = await this.peek(JobStatus.ABORTING);
732
+ for (const job of abortingJobs) {
733
+ await this.handleAbort(job.id);
734
+ }
735
+ if (this.options.deleteAfterCompletionMs) {
736
+ await this.storage.deleteJobsByStatusAndAge(JobStatus.COMPLETED, this.options.deleteAfterCompletionMs);
737
+ }
738
+ if (this.options.deleteAfterFailureMs) {
739
+ await this.storage.deleteJobsByStatusAndAge(JobStatus.FAILED, this.options.deleteAfterFailureMs);
740
+ }
741
+ }
742
+ async monitorJobs() {
743
+ if (!this.running) {
744
+ return;
745
+ }
746
+ try {
747
+ const jobIds = Array.from(this.jobProgressListeners.keys());
748
+ for (const jobId of jobIds) {
749
+ const job = await this.get(jobId);
750
+ if (job) {
751
+ const currentProgress = {
752
+ progress: job.progress,
753
+ message: job.progressMessage,
754
+ details: job.progressDetails || null
755
+ };
756
+ const lastProgress = this.lastKnownProgress.get(jobId);
757
+ const hasChanged = !lastProgress || lastProgress.progress !== currentProgress.progress || lastProgress.message !== currentProgress.message;
758
+ if (hasChanged && currentProgress.progress !== 0 && currentProgress.message !== "") {
759
+ this.announceProgress(jobId, currentProgress.progress, currentProgress.message, currentProgress.details);
760
+ }
761
+ }
762
+ }
763
+ for (const jobId of this.lastKnownProgress.keys()) {
764
+ const job = await this.get(jobId);
765
+ if (!job || job.status === JobStatus.COMPLETED || job.status === JobStatus.FAILED) {
766
+ this.lastKnownProgress.delete(jobId);
767
+ }
768
+ }
769
+ } catch (error) {
770
+ console.error(`Error in monitorJobs: ${error}`);
771
+ }
772
+ setTimeout(() => this.monitorJobs(), this.options.waitDurationInMilliseconds);
773
+ }
774
+ async fixupJobs() {
775
+ const stuckProcessingJobs = await this.peek(JobStatus.PROCESSING);
776
+ const stuckAbortingJobs = await this.peek(JobStatus.ABORTING);
777
+ const stuckJobs = [...stuckProcessingJobs, ...stuckAbortingJobs];
778
+ for (const job of stuckJobs) {
779
+ job.status = JobStatus.PENDING;
780
+ job.runAfter = job.lastRanAt || new Date;
781
+ job.progress = 0;
782
+ job.progressMessage = "";
783
+ job.progressDetails = null;
784
+ job.error = "Restarting server";
785
+ await this.storage.complete(this.classToStorage(job));
786
+ await this.rescheduleJob(job, job.lastRanAt);
787
+ }
788
+ }
789
+ }
790
+ // src/limiter/CompositeLimiter.ts
791
+ class CompositeLimiter {
792
+ limiters = [];
793
+ constructor(limiters = []) {
794
+ this.limiters = limiters;
795
+ }
796
+ addLimiter(limiter) {
797
+ this.limiters.push(limiter);
798
+ }
799
+ async canProceed() {
800
+ for (const limiter of this.limiters) {
801
+ if (!await limiter.canProceed()) {
802
+ return false;
803
+ }
804
+ }
805
+ return true;
806
+ }
807
+ async recordJobStart() {
808
+ this.limiters.forEach((limiter) => limiter.recordJobStart());
809
+ }
810
+ async recordJobCompletion() {
811
+ this.limiters.forEach((limiter) => limiter.recordJobCompletion());
812
+ }
813
+ async getNextAvailableTime() {
814
+ let maxDate = new Date;
815
+ for (const limiter of this.limiters) {
816
+ const limiterNextTime = await limiter.getNextAvailableTime();
817
+ if (limiterNextTime > maxDate) {
818
+ maxDate = limiterNextTime;
819
+ }
820
+ }
821
+ return maxDate;
822
+ }
823
+ async setNextAvailableTime(date) {
824
+ for (const limiter of this.limiters) {
825
+ await limiter.setNextAvailableTime(date);
826
+ }
827
+ }
828
+ async clear() {
829
+ this.limiters.forEach((limiter) => limiter.clear());
830
+ }
831
+ }
832
+ // src/limiter/ConcurrencyLimiter.ts
833
+ import { createServiceToken as createServiceToken3 } from "@workglow/util";
834
+ var CONCURRENT_JOB_LIMITER = createServiceToken3("jobqueue.limiter.concurrent");
835
+
836
+ class ConcurrencyLimiter {
837
+ currentRunningJobs = 0;
838
+ maxConcurrentJobs;
839
+ timeSliceInMilliseconds;
840
+ nextAllowedStartTime = new Date;
841
+ constructor(maxConcurrentJobs, timeSliceInMilliseconds = 1000) {
842
+ this.maxConcurrentJobs = maxConcurrentJobs;
843
+ this.timeSliceInMilliseconds = timeSliceInMilliseconds;
844
+ }
845
+ async canProceed() {
846
+ return this.currentRunningJobs < this.maxConcurrentJobs && Date.now() >= this.nextAllowedStartTime.getTime();
847
+ }
848
+ async recordJobStart() {
849
+ if (this.currentRunningJobs < this.maxConcurrentJobs) {
850
+ this.currentRunningJobs++;
851
+ this.nextAllowedStartTime = new Date(Date.now() + this.timeSliceInMilliseconds);
852
+ }
853
+ }
854
+ async recordJobCompletion() {
855
+ this.currentRunningJobs = Math.max(0, this.currentRunningJobs - 1);
856
+ }
857
+ async getNextAvailableTime() {
858
+ return this.currentRunningJobs < this.maxConcurrentJobs ? new Date : new Date(Date.now() + this.timeSliceInMilliseconds);
859
+ }
860
+ async setNextAvailableTime(date) {
861
+ if (date > this.nextAllowedStartTime) {
862
+ this.nextAllowedStartTime = date;
863
+ }
864
+ }
865
+ async clear() {
866
+ this.currentRunningJobs = 0;
867
+ this.nextAllowedStartTime = new Date;
868
+ }
869
+ }
870
+ // src/limiter/DelayLimiter.ts
871
+ class DelayLimiter {
872
+ delayInMilliseconds;
873
+ nextAvailableTime = new Date;
874
+ constructor(delayInMilliseconds = 50) {
875
+ this.delayInMilliseconds = delayInMilliseconds;
876
+ }
877
+ async canProceed() {
878
+ return Date.now() >= this.nextAvailableTime.getTime();
879
+ }
880
+ async recordJobStart() {
881
+ this.nextAvailableTime = new Date(Date.now() + this.delayInMilliseconds);
882
+ }
883
+ async recordJobCompletion() {}
884
+ async getNextAvailableTime() {
885
+ return this.nextAvailableTime;
886
+ }
887
+ async setNextAvailableTime(date) {
888
+ if (date > this.nextAvailableTime) {
889
+ this.nextAvailableTime = date;
890
+ }
891
+ }
892
+ async clear() {
893
+ this.nextAvailableTime = new Date;
894
+ }
895
+ }
896
+ // src/limiter/EvenlySpacedRateLimiter.ts
897
+ import { createServiceToken as createServiceToken4 } from "@workglow/util";
898
+ var EVENLY_SPACED_JOB_RATE_LIMITER = createServiceToken4("jobqueue.limiter.rate.evenlyspaced");
899
+
900
+ class EvenlySpacedRateLimiter {
901
+ maxExecutions;
902
+ windowSizeMs;
903
+ idealInterval;
904
+ nextAvailableTime = Date.now();
905
+ lastStartTime = 0;
906
+ durations = [];
907
+ constructor({ maxExecutions, windowSizeInSeconds }) {
908
+ if (maxExecutions <= 0) {
909
+ throw new Error("maxExecutions must be > 0");
910
+ }
911
+ if (windowSizeInSeconds <= 0) {
912
+ throw new Error("windowSizeInSeconds must be > 0");
913
+ }
914
+ this.maxExecutions = maxExecutions;
915
+ this.windowSizeMs = windowSizeInSeconds * 1000;
916
+ this.idealInterval = this.windowSizeMs / this.maxExecutions;
917
+ }
918
+ async canProceed() {
919
+ const now = Date.now();
920
+ return now >= this.nextAvailableTime;
921
+ }
922
+ async recordJobStart() {
923
+ const now = Date.now();
924
+ this.lastStartTime = now;
925
+ if (this.durations.length === 0) {
926
+ this.nextAvailableTime = now + this.idealInterval;
927
+ } else {
928
+ const sum = this.durations.reduce((a, b) => a + b, 0);
929
+ const avgDuration = sum / this.durations.length;
930
+ const waitMs = Math.max(0, this.idealInterval - avgDuration);
931
+ this.nextAvailableTime = now + waitMs;
932
+ }
933
+ }
934
+ async recordJobCompletion() {
935
+ const now = Date.now();
936
+ const duration = now - this.lastStartTime;
937
+ this.durations.push(duration);
938
+ if (this.durations.length > this.maxExecutions) {
939
+ this.durations.shift();
940
+ }
941
+ }
942
+ async getNextAvailableTime() {
943
+ return new Date(this.nextAvailableTime);
944
+ }
945
+ async setNextAvailableTime(date) {
946
+ const t = date.getTime();
947
+ if (t > this.nextAvailableTime) {
948
+ this.nextAvailableTime = t;
949
+ }
950
+ }
951
+ async clear() {
952
+ this.durations = [];
953
+ this.nextAvailableTime = Date.now();
954
+ this.lastStartTime = 0;
955
+ }
956
+ }
957
+ // src/limiter/InMemoryRateLimiter.ts
958
+ import { createServiceToken as createServiceToken5 } from "@workglow/util";
959
+ var MEMORY_JOB_RATE_LIMITER = createServiceToken5("jobqueue.limiter.rate.memory");
960
+
961
+ class InMemoryRateLimiter {
962
+ requests = [];
963
+ nextAvailableTime = new Date;
964
+ currentBackoffDelay;
965
+ maxExecutions;
966
+ windowSizeInMilliseconds;
967
+ initialBackoffDelay;
968
+ backoffMultiplier;
969
+ maxBackoffDelay;
970
+ constructor({
971
+ maxExecutions,
972
+ windowSizeInSeconds,
973
+ initialBackoffDelay = 1000,
974
+ backoffMultiplier = 2,
975
+ maxBackoffDelay = 600000
976
+ }) {
977
+ if (maxExecutions <= 0) {
978
+ throw new Error("maxExecutions must be greater than 0");
979
+ }
980
+ if (windowSizeInSeconds <= 0) {
981
+ throw new Error("windowSizeInSeconds must be greater than 0");
982
+ }
983
+ if (initialBackoffDelay <= 0) {
984
+ throw new Error("initialBackoffDelay must be greater than 0");
985
+ }
986
+ if (backoffMultiplier <= 1) {
987
+ throw new Error("backoffMultiplier must be greater than 1");
988
+ }
989
+ if (maxBackoffDelay <= initialBackoffDelay) {
990
+ throw new Error("maxBackoffDelay must be greater than initialBackoffDelay");
991
+ }
992
+ this.maxExecutions = maxExecutions;
993
+ this.windowSizeInMilliseconds = windowSizeInSeconds * 1000;
994
+ this.initialBackoffDelay = initialBackoffDelay;
995
+ this.backoffMultiplier = backoffMultiplier;
996
+ this.maxBackoffDelay = maxBackoffDelay;
997
+ this.currentBackoffDelay = initialBackoffDelay;
998
+ }
999
+ removeOldRequests() {
1000
+ const now = Date.now();
1001
+ const cutoff = now - this.windowSizeInMilliseconds;
1002
+ this.requests = this.requests.filter((d) => d.getTime() > cutoff);
1003
+ if (this.nextAvailableTime.getTime() < now) {
1004
+ this.nextAvailableTime = new Date(now);
1005
+ }
1006
+ }
1007
+ increaseBackoff() {
1008
+ this.currentBackoffDelay = Math.min(this.currentBackoffDelay * this.backoffMultiplier, this.maxBackoffDelay);
1009
+ }
1010
+ addJitter(base) {
1011
+ return base + Math.random() * base;
1012
+ }
1013
+ async canProceed() {
1014
+ this.removeOldRequests();
1015
+ const now = Date.now();
1016
+ const okRequestCount = this.requests.length < this.maxExecutions;
1017
+ const okTime = now >= this.nextAvailableTime.getTime();
1018
+ const canProceedNow = okRequestCount && okTime;
1019
+ if (!canProceedNow) {
1020
+ this.increaseBackoff();
1021
+ } else {
1022
+ this.currentBackoffDelay = this.initialBackoffDelay;
1023
+ }
1024
+ return canProceedNow;
1025
+ }
1026
+ async recordJobStart() {
1027
+ this.requests.push(new Date);
1028
+ if (this.requests.length >= this.maxExecutions) {
1029
+ const earliest = this.requests[0].getTime();
1030
+ const windowExpires = earliest + this.windowSizeInMilliseconds;
1031
+ const backoffExpires = Date.now() + this.addJitter(this.currentBackoffDelay);
1032
+ this.nextAvailableTime = new Date(Math.max(windowExpires, backoffExpires));
1033
+ }
1034
+ }
1035
+ async recordJobCompletion() {}
1036
+ async getNextAvailableTime() {
1037
+ this.removeOldRequests();
1038
+ return new Date(Math.max(Date.now(), this.nextAvailableTime.getTime()));
1039
+ }
1040
+ async setNextAvailableTime(date) {
1041
+ if (date.getTime() > this.nextAvailableTime.getTime()) {
1042
+ this.nextAvailableTime = date;
1043
+ }
1044
+ }
1045
+ async clear() {
1046
+ this.requests = [];
1047
+ this.nextAvailableTime = new Date;
1048
+ this.currentBackoffDelay = this.initialBackoffDelay;
1049
+ }
1050
+ }
1051
+ export {
1052
+ RetryableJobError,
1053
+ QueueMode,
1054
+ PermanentJobError,
1055
+ NullLimiter,
1056
+ NULL_JOB_LIMITER,
1057
+ MEMORY_JOB_RATE_LIMITER,
1058
+ JobStatus,
1059
+ JobQueue,
1060
+ JobNotFoundError,
1061
+ JobError,
1062
+ JobDisabledError,
1063
+ Job,
1064
+ JOB_LIMITER,
1065
+ InMemoryRateLimiter,
1066
+ EvenlySpacedRateLimiter,
1067
+ EVENLY_SPACED_JOB_RATE_LIMITER,
1068
+ DelayLimiter,
1069
+ ConcurrencyLimiter,
1070
+ CompositeLimiter,
1071
+ CONCURRENT_JOB_LIMITER,
1072
+ AbortSignalJobError
1073
+ };
1074
+
1075
+ //# debugId=4FA097AF2EE0650C64756E2164756E21