@workglow/job-queue 0.0.57 → 0.0.59

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bun.js CHANGED
@@ -1,12 +1,7 @@
1
1
  // @bun
2
- // src/job/IJobQueue.ts
2
+ // src/job/Job.ts
3
3
  import { JobStatus } from "@workglow/storage";
4
- var QueueMode;
5
- ((QueueMode2) => {
6
- QueueMode2["CLIENT"] = "CLIENT";
7
- QueueMode2["SERVER"] = "SERVER";
8
- QueueMode2["BOTH"] = "BOTH";
9
- })(QueueMode ||= {});
4
+
10
5
  // src/job/JobError.ts
11
6
  import { BaseError } from "@workglow/util";
12
7
 
@@ -76,7 +71,6 @@ class Job {
76
71
  progress = 0;
77
72
  progressMessage = "";
78
73
  progressDetails = null;
79
- queue;
80
74
  constructor({
81
75
  queueName,
82
76
  input,
@@ -129,7 +123,6 @@ class Job {
129
123
  for (const listener of this.progressListeners) {
130
124
  listener(progress, message, details);
131
125
  }
132
- await this.queue?.updateProgress(this.id, progress, message, details);
133
126
  }
134
127
  onJobProgress(listener) {
135
128
  this.progressListeners.add(listener);
@@ -138,78 +131,81 @@ class Job {
138
131
  };
139
132
  }
140
133
  }
141
- // src/job/JobQueue.ts
142
- import {
143
- InMemoryQueueStorage,
144
- QUEUE_STORAGE
145
- } from "@workglow/storage";
146
- import { EventEmitter, globalServiceRegistry, sleep } from "@workglow/util";
147
-
148
- // src/limiter/ILimiter.ts
149
- import { createServiceToken } from "@workglow/util";
150
- var JOB_LIMITER = createServiceToken("jobqueue.limiter");
151
-
152
- // src/limiter/NullLimiter.ts
153
- import { createServiceToken as createServiceToken2 } from "@workglow/util";
154
- var NULL_JOB_LIMITER = createServiceToken2("jobqueue.limiter.null");
155
-
156
- class NullLimiter {
157
- async canProceed() {
158
- return true;
134
+ // src/job/JobQueueClient.ts
135
+ import { JobStatus as JobStatus2 } from "@workglow/storage";
136
+ import { EventEmitter } from "@workglow/util";
137
+ class JobQueueClient {
138
+ queueName;
139
+ storage;
140
+ events = new EventEmitter;
141
+ server = null;
142
+ storageUnsubscribe = null;
143
+ activeJobPromises = new Map;
144
+ jobProgressListeners = new Map;
145
+ lastKnownProgress = new Map;
146
+ constructor(options) {
147
+ this.queueName = options.queueName;
148
+ this.storage = options.storage;
159
149
  }
160
- async recordJobStart() {}
161
- async recordJobCompletion() {}
162
- async getNextAvailableTime() {
163
- return new Date;
150
+ attach(server) {
151
+ if (this.server) {
152
+ this.detach();
153
+ }
154
+ this.server = server;
155
+ server.addClient(this);
156
+ if (this.storageUnsubscribe) {
157
+ this.storageUnsubscribe();
158
+ this.storageUnsubscribe = null;
159
+ }
164
160
  }
165
- async setNextAvailableTime(date) {}
166
- async clear() {}
167
- }
168
-
169
- // src/job/JobQueue.ts
170
- class JobQueue {
171
- queueName;
172
- jobClass;
173
- options;
174
- constructor(queueName, jobClass, options) {
175
- this.queueName = queueName;
176
- this.jobClass = jobClass;
177
- const { limiter, storage, ...rest } = options;
178
- this.options = {
179
- waitDurationInMilliseconds: 100,
180
- ...rest
181
- };
182
- if (limiter) {
183
- this.limiter = limiter;
184
- } else {
185
- try {
186
- this.limiter = globalServiceRegistry.get(JOB_LIMITER);
187
- } catch (err) {
188
- console.warn("Warning: did not find job limiter in global DI", err);
189
- this.limiter = new NullLimiter;
190
- }
161
+ detach() {
162
+ if (this.server) {
163
+ this.server.removeClient(this);
164
+ this.server = null;
191
165
  }
192
- if (storage) {
193
- this.storage = storage;
194
- } else {
195
- try {
196
- this.storage = globalServiceRegistry.get(QUEUE_STORAGE);
197
- } catch (err) {
198
- console.warn("Warning: did not find queue storage in global DI", err);
199
- this.storage = new InMemoryQueueStorage(queueName);
200
- }
166
+ }
167
+ connect() {
168
+ if (this.server) {
169
+ return;
170
+ }
171
+ if (this.storageUnsubscribe) {
172
+ return;
201
173
  }
202
- this.stats = {
203
- totalJobs: 0,
204
- completedJobs: 0,
205
- failedJobs: 0,
206
- abortedJobs: 0,
207
- retriedJobs: 0,
208
- disabledJobs: 0,
209
- lastUpdateTime: new Date
174
+ this.storageUnsubscribe = this.storage.subscribeToChanges((change) => {
175
+ this.handleStorageChange(change);
176
+ });
177
+ }
178
+ disconnect() {
179
+ if (this.storageUnsubscribe) {
180
+ this.storageUnsubscribe();
181
+ this.storageUnsubscribe = null;
182
+ }
183
+ this.detach();
184
+ }
185
+ async submit(input, options) {
186
+ const job = {
187
+ queue: this.queueName,
188
+ input,
189
+ job_run_id: options?.jobRunId,
190
+ fingerprint: options?.fingerprint,
191
+ max_retries: options?.maxRetries ?? 10,
192
+ run_after: options?.runAfter?.toISOString() ?? new Date().toISOString(),
193
+ deadline_at: options?.deadlineAt?.toISOString() ?? null,
194
+ completed_at: null,
195
+ status: JobStatus2.PENDING
210
196
  };
197
+ const id = await this.storage.add(job);
198
+ return this.createJobHandle(id);
199
+ }
200
+ async submitBatch(inputs, options) {
201
+ const handles = [];
202
+ for (const input of inputs) {
203
+ const handle = await this.submit(input, options);
204
+ handles.push(handle);
205
+ }
206
+ return handles;
211
207
  }
212
- async get(id) {
208
+ async getJob(id) {
213
209
  if (!id)
214
210
  throw new JobNotFoundError("Cannot get undefined job");
215
211
  const job = await this.storage.get(id);
@@ -217,15 +213,11 @@ class JobQueue {
217
213
  return;
218
214
  return this.storageToClass(job);
219
215
  }
220
- async add(job) {
221
- const jobId = await this.storage.add(this.classToStorage(job));
222
- return jobId;
223
- }
224
- async next() {
225
- const job = await this.storage.next();
226
- if (!job)
227
- return;
228
- return this.storageToClass(job);
216
+ async getJobsByRunId(runId) {
217
+ if (!runId)
218
+ throw new JobNotFoundError("Cannot get jobs by undefined runId");
219
+ const jobs = await this.storage.getByRunId(runId);
220
+ return jobs.map((job) => this.storageToClass(job));
229
221
  }
230
222
  async peek(status, num) {
231
223
  const jobs = await this.storage.peek(status, num);
@@ -234,86 +226,48 @@ class JobQueue {
234
226
  async size(status) {
235
227
  return this.storage.size(status);
236
228
  }
237
- async delete(id) {
238
- if (!id)
239
- throw new JobNotFoundError("Cannot delete undefined job");
240
- await this.storage.delete(id);
241
- }
242
- async disable(id) {
243
- if (!id)
244
- throw new JobNotFoundError("Cannot disable undefined job");
245
- const job = await this.get(id);
246
- if (!job)
247
- throw new JobNotFoundError(`Job ${id} not found`);
248
- await this.disableJob(job);
249
- }
250
- getStats() {
251
- return { ...this.stats };
252
- }
253
229
  async outputForInput(input) {
254
230
  if (!input)
255
231
  throw new JobNotFoundError("Cannot get output for undefined input");
256
232
  return this.storage.outputForInput(input);
257
233
  }
258
- async executeJob(job, signal) {
259
- if (!job)
260
- throw new JobNotFoundError("Cannot execute null or undefined job");
261
- return await job.execute(job.input, {
262
- signal,
263
- updateProgress: this.updateProgress.bind(this, job.id)
264
- });
265
- }
266
234
  async waitFor(jobId) {
267
235
  if (!jobId)
268
236
  throw new JobNotFoundError("Cannot wait for undefined job");
269
- const { promise, resolve, reject } = Promise.withResolvers();
270
- promise.catch(() => {});
271
- const promises = this.activeJobPromises.get(jobId) || [];
272
- promises.push({ resolve, reject });
273
- this.activeJobPromises.set(jobId, promises);
274
- const job = await this.get(jobId);
237
+ const job = await this.getJob(jobId);
275
238
  if (!job)
276
239
  throw new JobNotFoundError(`Job ${jobId} not found`);
277
- if (job.status === JobStatus.COMPLETED) {
240
+ if (job.status === JobStatus2.COMPLETED) {
278
241
  return job.output;
279
242
  }
280
- if (job.status === JobStatus.DISABLED) {
281
- return;
243
+ if (job.status === JobStatus2.DISABLED) {
244
+ throw new JobDisabledError(`Job ${jobId} was disabled`);
282
245
  }
283
- if (job.status === JobStatus.FAILED) {
246
+ if (job.status === JobStatus2.FAILED) {
284
247
  throw this.buildErrorFromJob(job);
285
248
  }
249
+ const { promise, resolve, reject } = Promise.withResolvers();
250
+ promise.catch(() => {});
251
+ const promises = this.activeJobPromises.get(jobId) || [];
252
+ promises.push({ resolve, reject });
253
+ this.activeJobPromises.set(jobId, promises);
286
254
  return promise;
287
255
  }
288
- buildErrorFromJob(job) {
289
- const errorMessage = job.error || "Job failed";
290
- if (job.errorCode === "PermanentJobError") {
291
- return new PermanentJobError(errorMessage);
292
- }
293
- if (job.errorCode === "RetryableJobError") {
294
- return new RetryableJobError(errorMessage);
295
- }
296
- if (job.errorCode === "AbortSignalJobError") {
297
- return new AbortSignalJobError(errorMessage);
298
- }
299
- if (job.errorCode === "JobDisabledError") {
300
- return new JobDisabledError(errorMessage);
301
- }
302
- return new JobError(errorMessage);
256
+ async abort(jobId) {
257
+ if (!jobId)
258
+ throw new JobNotFoundError("Cannot abort undefined job");
259
+ await this.storage.abort(jobId);
260
+ this.events.emit("job_aborting", this.queueName, jobId);
303
261
  }
304
- async updateProgress(jobId, progress, message = "", details = null) {
305
- const job = await this.get(jobId);
306
- if (!job)
307
- throw new JobNotFoundError(`Job ${jobId} not found`);
308
- if ([JobStatus.COMPLETED, JobStatus.FAILED, JobStatus.ABORTING, JobStatus.DISABLED].includes(job.status)) {
309
- return;
310
- }
311
- progress = Math.max(0, Math.min(100, progress));
312
- job.progress = progress;
313
- job.progressMessage = message;
314
- job.progressDetails = details;
315
- await this.saveProgress(jobId, progress, message, details ?? null);
316
- this.announceProgress(jobId, progress, message, details ?? null);
262
+ async abortJobRun(jobRunId) {
263
+ if (!jobRunId)
264
+ throw new JobNotFoundError("Cannot abort job run with undefined jobRunId");
265
+ const jobs = await this.getJobsByRunId(jobRunId);
266
+ await Promise.allSettled(jobs.map((job) => {
267
+ if ([JobStatus2.PROCESSING, JobStatus2.PENDING].includes(job.status)) {
268
+ return this.abort(job.id);
269
+ }
270
+ }));
317
271
  }
318
272
  onJobProgress(jobId, listener) {
319
273
  if (!this.jobProgressListeners.has(jobId)) {
@@ -331,78 +285,6 @@ class JobQueue {
331
285
  }
332
286
  };
333
287
  }
334
- async start(mode = "BOTH" /* BOTH */) {
335
- if (this.running) {
336
- return this;
337
- }
338
- this.mode = mode;
339
- this.running = true;
340
- this.events.emit("queue_start", this.queueName);
341
- if (this.mode === "SERVER" /* SERVER */ || this.mode === "BOTH" /* BOTH */) {
342
- await this.fixupJobs();
343
- await this.processJobs();
344
- }
345
- if (this.mode === "CLIENT" /* CLIENT */ || this.mode === "BOTH" /* BOTH */) {
346
- await this.monitorJobs();
347
- }
348
- return this;
349
- }
350
- async stop() {
351
- if (!this.running)
352
- return this;
353
- this.running = false;
354
- const size = await this.size(JobStatus.PROCESSING);
355
- const sleepTime = Math.max(100, size * 2);
356
- await sleep(sleepTime);
357
- for (const [jobId] of this.activeJobAbortControllers.entries()) {
358
- this.abort(jobId);
359
- }
360
- this.activeJobPromises.forEach((promises) => promises.forEach(({ reject }) => reject(new PermanentJobError("Queue Stopped"))));
361
- await sleep(sleepTime);
362
- this.events.emit("queue_stop", this.queueName);
363
- return this;
364
- }
365
- async clear() {
366
- await this.storage.deleteAll();
367
- this.activeJobAbortControllers.clear();
368
- this.activeJobPromises.clear();
369
- this.processingTimes.clear();
370
- this.lastKnownProgress.clear();
371
- this.jobProgressListeners.clear();
372
- this.stats = {
373
- totalJobs: 0,
374
- completedJobs: 0,
375
- failedJobs: 0,
376
- abortedJobs: 0,
377
- retriedJobs: 0,
378
- disabledJobs: 0,
379
- lastUpdateTime: new Date
380
- };
381
- this.emitStatsUpdate();
382
- return this;
383
- }
384
- async restart() {
385
- await this.stop();
386
- await this.clear();
387
- await this.start();
388
- return this;
389
- }
390
- async abortJobRun(jobRunId) {
391
- if (!jobRunId)
392
- throw new JobNotFoundError("Cannot abort job run with undefined jobRunId");
393
- const jobs = await this.getJobsByRunId(jobRunId);
394
- await Promise.allSettled(jobs.map((job) => {
395
- if ([JobStatus.PROCESSING, JobStatus.PENDING].includes(job.status)) {
396
- this.abort(job.id);
397
- }
398
- }));
399
- }
400
- async getJobsByRunId(runId) {
401
- if (!runId)
402
- throw new JobNotFoundError("Cannot get jobs by undefined runId");
403
- const jobs = await this.storage.getByRunId(runId);
404
- return jobs.map((job) => this.storageToClass(job));
405
- }
406
288
  on(event, listener) {
407
289
  this.events.on(event, listener);
408
290
  }
@@ -415,83 +297,44 @@ class JobQueue {
415
297
  waitOn(event) {
416
298
  return this.events.waitOn(event);
417
299
  }
418
- limiter;
419
- storage;
420
- running = false;
421
- stats = {
422
- totalJobs: 0,
423
- completedJobs: 0,
424
- failedJobs: 0,
425
- abortedJobs: 0,
426
- retriedJobs: 0,
427
- disabledJobs: 0,
428
- lastUpdateTime: new Date
429
- };
430
- events = new EventEmitter;
431
- activeJobAbortControllers = new Map;
432
- activeJobPromises = new Map;
433
- processingTimes = new Map;
434
- mode = "BOTH" /* BOTH */;
435
- jobProgressListeners = new Map;
436
- lastKnownProgress = new Map;
437
- async saveProgress(id, progress, message, details) {
438
- if (!id)
439
- throw new JobNotFoundError("Cannot save progress for undefined job");
440
- await this.storage.saveProgress(id, progress, message, details);
441
- }
442
- createAbortController(jobId) {
443
- if (!jobId)
444
- throw new JobNotFoundError("Cannot create abort controller for undefined job");
445
- if (this.activeJobAbortControllers.has(jobId)) {
446
- return this.activeJobAbortControllers.get(jobId);
447
- }
448
- const abortController = new AbortController;
449
- abortController.signal.addEventListener("abort", () => this.handleAbort(jobId));
450
- this.activeJobAbortControllers.set(jobId, abortController);
451
- return abortController;
300
+ handleJobStart(jobId) {
301
+ this.lastKnownProgress.set(jobId, {
302
+ progress: 0,
303
+ message: "",
304
+ details: null
305
+ });
306
+ this.events.emit("job_start", this.queueName, jobId);
452
307
  }
453
- async validateJobState(job) {
454
- if (job.status === JobStatus.COMPLETED) {
455
- throw new PermanentJobError(`Job ${job.id} is already completed`);
456
- }
457
- if (job.status === JobStatus.FAILED) {
458
- throw new PermanentJobError(`Job ${job.id} has failed`);
459
- }
460
- if (job.status === JobStatus.ABORTING || this.activeJobAbortControllers.get(job.id)?.signal.aborted) {
461
- throw new AbortSignalJobError(`Job ${job.id} is being aborted`);
462
- }
463
- if (job.deadlineAt && job.deadlineAt < new Date) {
464
- throw new PermanentJobError(`Job ${job.id} has exceeded its deadline`);
465
- }
466
- if (job.status === JobStatus.DISABLED) {
467
- throw new JobDisabledError(`Job ${job.id} has been disabled`);
308
+ handleJobComplete(jobId, output) {
309
+ this.events.emit("job_complete", this.queueName, jobId, output);
310
+ const promises = this.activeJobPromises.get(jobId);
311
+ if (promises) {
312
+ promises.forEach(({ resolve }) => resolve(output));
468
313
  }
314
+ this.cleanupJob(jobId);
469
315
  }
470
- normalizeError(err) {
471
- if (err instanceof JobError) {
472
- return err;
473
- }
474
- if (err instanceof Error) {
475
- return err;
316
+ handleJobError(jobId, error, errorCode) {
317
+ this.events.emit("job_error", this.queueName, jobId, error);
318
+ const promises = this.activeJobPromises.get(jobId);
319
+ if (promises) {
320
+ const jobError = this.buildErrorFromCode(error, errorCode);
321
+ promises.forEach(({ reject }) => reject(jobError));
476
322
  }
477
- return new PermanentJobError(String(err));
323
+ this.cleanupJob(jobId);
478
324
  }
479
- updateAverageProcessingTime() {
480
- const times = Array.from(this.processingTimes.values());
481
- if (times.length > 0) {
482
- this.stats.averageProcessingTime = times.reduce((a, b) => a + b, 0) / times.length;
325
+ handleJobDisabled(jobId) {
326
+ this.events.emit("job_disabled", this.queueName, jobId);
327
+ const promises = this.activeJobPromises.get(jobId);
328
+ if (promises) {
329
+ promises.forEach(({ reject }) => reject(new JobDisabledError("Job was disabled")));
483
330
  }
331
+ this.cleanupJob(jobId);
484
332
  }
485
- emitStatsUpdate() {
486
- this.stats.lastUpdateTime = new Date;
487
- this.events.emit("queue_stats_update", this.queueName, { ...this.stats });
333
+ handleJobRetry(jobId, runAfter) {
334
+ this.events.emit("job_retry", this.queueName, jobId, runAfter);
488
335
  }
489
- announceProgress(jobId, progress, message, details) {
490
- this.lastKnownProgress.set(jobId, {
491
- progress,
492
- message,
493
- details
494
- });
336
+ handleJobProgress(jobId, progress, message, details) {
337
+ this.lastKnownProgress.set(jobId, { progress, message, details });
495
338
  this.events.emit("job_progress", this.queueName, jobId, progress, message, details);
496
339
  const listeners = this.jobProgressListeners.get(jobId);
497
340
  if (listeners) {
@@ -500,6 +343,48 @@ class JobQueue {
500
343
  }
501
344
  }
502
345
  }
346
+ createJobHandle(id) {
347
+ return {
348
+ id,
349
+ waitFor: () => this.waitFor(id),
350
+ abort: () => this.abort(id),
351
+ onProgress: (callback) => this.onJobProgress(id, callback)
352
+ };
353
+ }
354
+ cleanupJob(jobId) {
355
+ this.activeJobPromises.delete(jobId);
356
+ this.lastKnownProgress.delete(jobId);
357
+ this.jobProgressListeners.delete(jobId);
358
+ }
359
+ handleStorageChange(change) {
360
+ if (!change.new && !change.old)
361
+ return;
362
+ const jobId = change.new?.id ?? change.old?.id;
363
+ if (!jobId)
364
+ return;
365
+ const queueName = change.new?.queue ?? change.old?.queue;
366
+ if (queueName !== this.queueName)
367
+ return;
368
+ if (change.type === "UPDATE" && change.new) {
369
+ const newStatus = change.new.status;
370
+ const oldStatus = change.old?.status;
371
+ if (newStatus === JobStatus2.PROCESSING && oldStatus === JobStatus2.PENDING) {
372
+ this.handleJobStart(jobId);
373
+ } else if (newStatus === JobStatus2.COMPLETED) {
374
+ this.handleJobComplete(jobId, change.new.output);
375
+ } else if (newStatus === JobStatus2.FAILED) {
376
+ this.handleJobError(jobId, change.new.error ?? "Job failed", change.new.error_code ?? undefined);
377
+ } else if (newStatus === JobStatus2.DISABLED) {
378
+ this.handleJobDisabled(jobId);
379
+ } else if (newStatus === JobStatus2.PENDING && oldStatus === JobStatus2.PROCESSING) {
380
+ const runAfter = change.new.run_after ? new Date(change.new.run_after) : new Date;
381
+ this.handleJobRetry(jobId, runAfter);
382
+ }
383
+ if (change.new.progress !== change.old?.progress || change.new.progress_message !== change.old?.progress_message) {
384
+ this.handleJobProgress(jobId, change.new.progress ?? 0, change.new.progress_message ?? "", change.new.progress_details ?? null);
385
+ }
386
+ }
387
+ }
503
388
  storageToClass(details) {
504
389
  const toDate = (date) => {
505
390
  if (!date)
@@ -507,7 +392,7 @@ class JobQueue {
507
392
  const d = new Date(date);
508
393
  return isNaN(d.getTime()) ? null : d;
509
394
  };
510
- const job = new this.jobClass({
395
+ return new Job({
511
396
  id: details.id,
512
397
  jobRunId: details.job_run_id,
513
398
  queueName: details.queue,
@@ -528,173 +413,172 @@ class JobQueue {
528
413
  runAttempts: details.run_attempts ?? 0,
529
414
  maxRetries: details.max_retries ?? 10
530
415
  });
531
- job.queue = this;
532
- return job;
533
416
  }
534
- classToStorage(job) {
535
- const dateToISOString = (date) => {
536
- if (!date)
537
- return null;
538
- return isNaN(date.getTime()) ? null : date.toISOString();
539
- };
540
- const now = new Date().toISOString();
541
- return {
542
- id: job.id,
543
- job_run_id: job.jobRunId,
544
- queue: job.queueName || this.queueName,
545
- fingerprint: job.fingerprint,
546
- input: job.input,
547
- status: job.status,
548
- output: job.output ?? null,
549
- error: job.error === null ? null : String(job.error),
550
- error_code: job.errorCode || null,
551
- run_attempts: job.runAttempts ?? 0,
552
- max_retries: job.maxRetries ?? 10,
553
- run_after: dateToISOString(job.runAfter) ?? now,
554
- created_at: dateToISOString(job.createdAt) ?? now,
555
- deadline_at: dateToISOString(job.deadlineAt),
556
- last_ran_at: dateToISOString(job.lastRanAt),
557
- completed_at: dateToISOString(job.completedAt),
558
- progress: job.progress ?? 0,
559
- progress_message: job.progressMessage ?? "",
560
- progress_details: job.progressDetails ?? null
561
- };
417
+ buildErrorFromJob(job) {
418
+ return this.buildErrorFromCode(job.error || "Job failed", job.errorCode ?? undefined);
562
419
  }
563
- async rescheduleJob(job, retryDate) {
564
- try {
565
- job.status = JobStatus.PENDING;
566
- const nextAvailableTime = await this.limiter.getNextAvailableTime();
567
- job.runAfter = retryDate instanceof Date ? retryDate : nextAvailableTime;
568
- job.progress = 0;
569
- job.progressMessage = "";
570
- job.progressDetails = null;
571
- await this.storage.complete(this.classToStorage(job));
572
- this.stats.retriedJobs++;
573
- this.events.emit("job_retry", this.queueName, job.id, job.runAfter);
574
- } catch (err) {
575
- console.error("rescheduleJob", err);
420
+ buildErrorFromCode(message, errorCode) {
421
+ if (errorCode === "PermanentJobError") {
422
+ return new PermanentJobError(message);
576
423
  }
577
- }
578
- async disableJob(job) {
579
- try {
580
- job.status = JobStatus.DISABLED;
581
- job.progress = 100;
582
- job.completedAt = new Date;
583
- job.progressMessage = "";
584
- job.progressDetails = null;
585
- await this.storage.complete(this.classToStorage(job));
586
- if (this.options.deleteAfterDisabledMs === 0) {
587
- await this.delete(job.id);
588
- }
589
- this.stats.disabledJobs++;
590
- this.events.emit("job_disabled", this.queueName, job.id);
591
- const promises = this.activeJobPromises.get(job.id) || [];
592
- promises.forEach(({ resolve }) => resolve(undefined));
593
- this.activeJobPromises.delete(job.id);
594
- } catch (err) {
595
- console.error("disableJob", err);
424
+ if (errorCode === "RetryableJobError") {
425
+ return new RetryableJobError(message);
426
+ }
427
+ if (errorCode === "AbortSignalJobError") {
428
+ return new AbortSignalJobError(message);
596
429
  }
430
+ if (errorCode === "JobDisabledError") {
431
+ return new JobDisabledError(message);
432
+ }
433
+ return new JobError(message);
597
434
  }
598
- async failJob(job, error) {
599
- try {
600
- job.status = JobStatus.FAILED;
601
- job.progress = 100;
602
- job.completedAt = new Date;
603
- job.progressMessage = "";
604
- job.progressDetails = null;
605
- job.error = error.message;
606
- job.errorCode = error?.constructor?.name ?? null;
607
- await this.storage.complete(this.classToStorage(job));
608
- if (this.options.deleteAfterFailureMs === 0) {
609
- await this.delete(job.id);
610
- }
611
- this.stats.failedJobs++;
612
- this.events.emit("job_error", this.queueName, job.id, `${error.name}: ${error.message}`);
613
- const promises = this.activeJobPromises.get(job.id) || [];
614
- promises.forEach(({ reject }) => reject(error));
615
- this.activeJobPromises.delete(job.id);
616
- } catch (err) {
617
- console.error("failJob errored out?", err);
435
+ }
436
+ // src/job/JobQueueServer.ts
437
+ import { JobStatus as JobStatus4 } from "@workglow/storage";
438
+ import { EventEmitter as EventEmitter3 } from "@workglow/util";
439
+
440
+ // src/limiter/NullLimiter.ts
441
+ import { createServiceToken } from "@workglow/util";
442
+ var NULL_JOB_LIMITER = createServiceToken("jobqueue.limiter.null");
443
+
444
+ class NullLimiter {
445
+ async canProceed() {
446
+ return true;
447
+ }
448
+ async recordJobStart() {}
449
+ async recordJobCompletion() {}
450
+ async getNextAvailableTime() {
451
+ return new Date;
452
+ }
453
+ async setNextAvailableTime(date) {}
454
+ async clear() {}
455
+ }
456
+
457
+ // src/job/JobQueueWorker.ts
458
+ import { JobStatus as JobStatus3 } from "@workglow/storage";
459
+ import { EventEmitter as EventEmitter2, sleep, uuid4 } from "@workglow/util";
460
+ class JobQueueWorker {
461
+ queueName;
462
+ workerId;
463
+ storage;
464
+ jobClass;
465
+ limiter;
466
+ pollIntervalMs;
467
+ events = new EventEmitter2;
468
+ running = false;
469
+ activeJobAbortControllers = new Map;
470
+ processingTimes = new Map;
471
+ constructor(jobClass, options) {
472
+ this.queueName = options.queueName;
473
+ this.workerId = uuid4();
474
+ this.storage = options.storage;
475
+ this.jobClass = jobClass;
476
+ this.limiter = options.limiter ?? new NullLimiter;
477
+ this.pollIntervalMs = options.pollIntervalMs ?? 100;
478
+ }
479
+ async start() {
480
+ if (this.running) {
481
+ return this;
618
482
  }
619
- this.activeJobAbortControllers.delete(job.id);
620
- this.lastKnownProgress.delete(job.id);
621
- this.jobProgressListeners.delete(job.id);
622
- this.activeJobPromises.delete(job.id);
483
+ this.running = true;
484
+ this.events.emit("worker_start");
485
+ this.processJobs();
486
+ return this;
623
487
  }
624
- async completeJob(job, output) {
625
- try {
626
- job.status = JobStatus.COMPLETED;
627
- job.progress = 100;
628
- job.progressMessage = "";
629
- job.progressDetails = null;
630
- job.completedAt = new Date;
631
- job.output = output ?? null;
632
- job.error = null;
633
- job.errorCode = null;
634
- await this.storage.complete(this.classToStorage(job));
635
- if (job && this.options.deleteAfterCompletionMs === 0) {
636
- await this.delete(job.id);
637
- }
638
- this.stats.completedJobs++;
639
- this.events.emit("job_complete", this.queueName, job.id, output);
640
- const promises = this.activeJobPromises.get(job.id);
641
- if (promises) {
642
- promises.forEach(({ resolve }) => resolve(output));
488
+ async stop() {
489
+ if (!this.running) {
490
+ return this;
491
+ }
492
+ this.running = false;
493
+ const size = await this.storage.size(JobStatus3.PROCESSING);
494
+ const sleepTime = Math.max(100, size * 2);
495
+ await sleep(sleepTime);
496
+ for (const controller of this.activeJobAbortControllers.values()) {
497
+ if (!controller.signal.aborted) {
498
+ controller.abort();
643
499
  }
644
- this.activeJobPromises.delete(job.id);
645
- } catch (err) {
646
- console.error("completeJob errored out?", err);
647
500
  }
648
- this.activeJobAbortControllers.delete(job.id);
649
- this.lastKnownProgress.delete(job.id);
650
- this.jobProgressListeners.delete(job.id);
651
- this.activeJobPromises.delete(job.id);
501
+ await sleep(sleepTime);
502
+ this.events.emit("worker_stop");
503
+ return this;
652
504
  }
653
- async abort(jobId) {
654
- if (!jobId)
655
- throw new JobNotFoundError("Cannot abort undefined job");
656
- await this.storage.abort(jobId);
657
- let controller = this.activeJobAbortControllers.get(jobId);
658
- if (!controller) {
659
- controller = this.createAbortController(jobId);
505
+ async processNext() {
506
+ const canProceed = await this.limiter.canProceed();
507
+ if (!canProceed) {
508
+ return false;
660
509
  }
661
- if (!controller.signal.aborted) {
662
- controller.abort();
510
+ const job = await this.next();
511
+ if (!job) {
512
+ return false;
663
513
  }
664
- this.events.emit("job_aborting", this.queueName, jobId);
514
+ await this.processSingleJob(job);
515
+ return true;
665
516
  }
666
- async handleAbort(jobId) {
667
- const promises = this.activeJobPromises.get(jobId);
668
- if (promises) {
669
- const job = await this.get(jobId);
670
- if (!job) {
671
- console.error("handleAbort: job not found", jobId);
672
- return;
517
+ isRunning() {
518
+ return this.running;
519
+ }
520
+ getActiveJobCount() {
521
+ return this.activeJobAbortControllers.size;
522
+ }
523
+ getAverageProcessingTime() {
524
+ const times = Array.from(this.processingTimes.values());
525
+ if (times.length === 0)
526
+ return;
527
+ return times.reduce((a, b) => a + b, 0) / times.length;
528
+ }
529
+ on(event, listener) {
530
+ this.events.on(event, listener);
531
+ }
532
+ off(event, listener) {
533
+ this.events.off(event, listener);
534
+ }
535
+ async next() {
536
+ const job = await this.storage.next(this.workerId);
537
+ if (!job)
538
+ return;
539
+ return this.storageToClass(job);
540
+ }
541
+ async processJobs() {
542
+ if (!this.running) {
543
+ return;
544
+ }
545
+ try {
546
+ await this.checkForAbortingJobs();
547
+ const canProceed = await this.limiter.canProceed();
548
+ if (canProceed) {
549
+ const job = await this.next();
550
+ if (job) {
551
+ this.processSingleJob(job);
552
+ }
553
+ }
554
+ } finally {
555
+ if (this.running) {
556
+ setTimeout(() => this.processJobs(), this.pollIntervalMs);
557
+ }
558
+ }
559
+ }
560
+ async checkForAbortingJobs() {
561
+ const abortingJobs = await this.storage.peek(JobStatus3.ABORTING);
562
+ for (const jobData of abortingJobs) {
563
+ const controller = this.activeJobAbortControllers.get(jobData.id);
564
+ if (controller && !controller.signal.aborted) {
565
+ controller.abort();
673
566
  }
674
- const error = new AbortSignalJobError("Job Aborted");
675
- this.failJob(job, error);
676
567
  }
677
- this.stats.abortedJobs++;
678
568
  }
679
569
  async processSingleJob(job) {
680
- if (!job || !job.id)
570
+ if (!job || !job.id) {
681
571
  throw new JobNotFoundError("Invalid job provided for processing");
572
+ }
682
573
  const startTime = Date.now();
683
574
  try {
684
575
  await this.validateJobState(job);
685
576
  await this.limiter.recordJobStart();
686
- this.emitStatsUpdate();
687
577
  const abortController = this.createAbortController(job.id);
688
- this.lastKnownProgress.set(job.id, {
689
- progress: 0,
690
- message: "",
691
- details: null
692
- });
693
- this.events.emit("job_start", this.queueName, job.id);
578
+ this.events.emit("job_start", job.id);
694
579
  const output = await this.executeJob(job, abortController.signal);
695
580
  await this.completeJob(job, output);
696
581
  this.processingTimes.set(job.id, Date.now() - startTime);
697
- this.updateAverageProcessingTime();
698
582
  } catch (err) {
699
583
  const error = this.normalizeError(err);
700
584
  if (error instanceof RetryableJobError) {
@@ -708,85 +592,485 @@ class JobQueue {
708
592
  }
709
593
  } finally {
710
594
  await this.limiter.recordJobCompletion();
711
- this.emitStatsUpdate();
712
595
  }
713
596
  }
714
- async processJobs() {
715
- if (!this.running) {
716
- return;
597
+ async executeJob(job, signal) {
598
+ if (!job)
599
+ throw new JobNotFoundError("Cannot execute null or undefined job");
600
+ return await job.execute(job.input, {
601
+ signal,
602
+ updateProgress: this.updateProgress.bind(this, job.id)
603
+ });
604
+ }
605
+ async updateProgress(jobId, progress, message = "", details = null) {
606
+ progress = Math.max(0, Math.min(100, progress));
607
+ await this.storage.saveProgress(jobId, progress, message, details);
608
+ this.events.emit("job_progress", jobId, progress, message, details);
609
+ }
610
+ async completeJob(job, output) {
611
+ try {
612
+ job.status = JobStatus3.COMPLETED;
613
+ job.progress = 100;
614
+ job.progressMessage = "";
615
+ job.progressDetails = null;
616
+ job.completedAt = new Date;
617
+ job.output = output ?? null;
618
+ job.error = null;
619
+ job.errorCode = null;
620
+ await this.storage.complete(this.classToStorage(job));
621
+ this.events.emit("job_complete", job.id, output);
622
+ } catch (err) {
623
+ console.error("completeJob errored:", err);
624
+ } finally {
625
+ this.cleanupJob(job.id);
717
626
  }
627
+ }
628
+ async failJob(job, error) {
718
629
  try {
719
- await this.cleanUpJobs();
720
- const canProceed = await this.limiter.canProceed();
721
- if (canProceed) {
722
- const job = await this.next();
723
- if (job) {
724
- this.processSingleJob(job);
630
+ job.status = JobStatus3.FAILED;
631
+ job.progress = 100;
632
+ job.completedAt = new Date;
633
+ job.progressMessage = "";
634
+ job.progressDetails = null;
635
+ job.error = error.message;
636
+ job.errorCode = error?.constructor?.name ?? null;
637
+ await this.storage.complete(this.classToStorage(job));
638
+ this.events.emit("job_error", job.id, error.message, error.constructor.name);
639
+ } catch (err) {
640
+ console.error("failJob errored:", err);
641
+ } finally {
642
+ this.cleanupJob(job.id);
643
+ }
644
+ }
645
+ async disableJob(job) {
646
+ try {
647
+ job.status = JobStatus3.DISABLED;
648
+ job.progress = 100;
649
+ job.completedAt = new Date;
650
+ job.progressMessage = "";
651
+ job.progressDetails = null;
652
+ await this.storage.complete(this.classToStorage(job));
653
+ this.events.emit("job_disabled", job.id);
654
+ } catch (err) {
655
+ console.error("disableJob errored:", err);
656
+ } finally {
657
+ this.cleanupJob(job.id);
658
+ }
659
+ }
660
+ async rescheduleJob(job, retryDate) {
661
+ try {
662
+ job.status = JobStatus3.PENDING;
663
+ const nextAvailableTime = await this.limiter.getNextAvailableTime();
664
+ job.runAfter = retryDate instanceof Date ? retryDate : nextAvailableTime;
665
+ job.progress = 0;
666
+ job.progressMessage = "";
667
+ job.progressDetails = null;
668
+ await this.storage.complete(this.classToStorage(job));
669
+ this.events.emit("job_retry", job.id, job.runAfter);
670
+ } catch (err) {
671
+ console.error("rescheduleJob errored:", err);
672
+ }
673
+ }
674
+ createAbortController(jobId) {
675
+ if (!jobId)
676
+ throw new JobNotFoundError("Cannot create abort controller for undefined job");
677
+ if (this.activeJobAbortControllers.has(jobId)) {
678
+ return this.activeJobAbortControllers.get(jobId);
679
+ }
680
+ const abortController = new AbortController;
681
+ abortController.signal.addEventListener("abort", () => this.handleAbort(jobId));
682
+ this.activeJobAbortControllers.set(jobId, abortController);
683
+ return abortController;
684
+ }
685
+ async handleAbort(jobId) {
686
+ const job = await this.getJob(jobId);
687
+ if (!job) {
688
+ console.error("handleAbort: job not found", jobId);
689
+ return;
690
+ }
691
+ const error = new AbortSignalJobError("Job Aborted");
692
+ await this.failJob(job, error);
693
+ }
694
+ async getJob(id) {
695
+ const job = await this.storage.get(id);
696
+ if (!job)
697
+ return;
698
+ return this.storageToClass(job);
699
+ }
700
+ async validateJobState(job) {
701
+ if (job.status === JobStatus3.COMPLETED) {
702
+ throw new PermanentJobError(`Job ${job.id} is already completed`);
703
+ }
704
+ if (job.status === JobStatus3.FAILED) {
705
+ throw new PermanentJobError(`Job ${job.id} has failed`);
706
+ }
707
+ if (job.status === JobStatus3.ABORTING || this.activeJobAbortControllers.get(job.id)?.signal.aborted) {
708
+ throw new AbortSignalJobError(`Job ${job.id} is being aborted`);
709
+ }
710
+ if (job.deadlineAt && job.deadlineAt < new Date) {
711
+ throw new PermanentJobError(`Job ${job.id} has exceeded its deadline`);
712
+ }
713
+ if (job.status === JobStatus3.DISABLED) {
714
+ throw new JobDisabledError(`Job ${job.id} has been disabled`);
715
+ }
716
+ }
717
+ normalizeError(err) {
718
+ if (err instanceof JobError) {
719
+ return err;
720
+ }
721
+ if (err instanceof Error) {
722
+ return new PermanentJobError(err.message);
723
+ }
724
+ return new PermanentJobError(String(err));
725
+ }
726
+ cleanupJob(jobId) {
727
+ this.activeJobAbortControllers.delete(jobId);
728
+ }
729
+ storageToClass(details) {
730
+ const toDate = (date) => {
731
+ if (!date)
732
+ return null;
733
+ const d = new Date(date);
734
+ return isNaN(d.getTime()) ? null : d;
735
+ };
736
+ return new this.jobClass({
737
+ id: details.id,
738
+ jobRunId: details.job_run_id,
739
+ queueName: details.queue,
740
+ fingerprint: details.fingerprint,
741
+ input: details.input,
742
+ output: details.output,
743
+ runAfter: toDate(details.run_after),
744
+ createdAt: toDate(details.created_at),
745
+ deadlineAt: toDate(details.deadline_at),
746
+ lastRanAt: toDate(details.last_ran_at),
747
+ completedAt: toDate(details.completed_at),
748
+ progress: details.progress || 0,
749
+ progressMessage: details.progress_message || "",
750
+ progressDetails: details.progress_details ?? null,
751
+ status: details.status,
752
+ error: details.error ?? null,
753
+ errorCode: details.error_code ?? null,
754
+ runAttempts: details.run_attempts ?? 0,
755
+ maxRetries: details.max_retries ?? 10
756
+ });
757
+ }
758
+ classToStorage(job) {
759
+ const dateToISOString = (date) => {
760
+ if (!date)
761
+ return null;
762
+ return isNaN(date.getTime()) ? null : date.toISOString();
763
+ };
764
+ const now = new Date().toISOString();
765
+ return {
766
+ id: job.id,
767
+ job_run_id: job.jobRunId,
768
+ queue: job.queueName || this.queueName,
769
+ fingerprint: job.fingerprint,
770
+ input: job.input,
771
+ status: job.status,
772
+ output: job.output ?? null,
773
+ error: job.error === null ? null : String(job.error),
774
+ error_code: job.errorCode || null,
775
+ run_attempts: job.runAttempts ?? 0,
776
+ max_retries: job.maxRetries ?? 10,
777
+ run_after: dateToISOString(job.runAfter) ?? now,
778
+ created_at: dateToISOString(job.createdAt) ?? now,
779
+ deadline_at: dateToISOString(job.deadlineAt),
780
+ last_ran_at: dateToISOString(job.lastRanAt),
781
+ completed_at: dateToISOString(job.completedAt),
782
+ progress: job.progress ?? 0,
783
+ progress_message: job.progressMessage ?? "",
784
+ progress_details: job.progressDetails ?? null
785
+ };
786
+ }
787
+ }
788
+
789
+ // src/job/JobQueueServer.ts
790
+ class JobQueueServer {
791
+ queueName;
792
+ storage;
793
+ jobClass;
794
+ limiter;
795
+ workerCount;
796
+ pollIntervalMs;
797
+ deleteAfterCompletionMs;
798
+ deleteAfterFailureMs;
799
+ deleteAfterDisabledMs;
800
+ cleanupIntervalMs;
801
+ events = new EventEmitter3;
802
+ workers = [];
803
+ clients = new Set;
804
+ running = false;
805
+ cleanupTimer = null;
806
+ stats = {
807
+ totalJobs: 0,
808
+ completedJobs: 0,
809
+ failedJobs: 0,
810
+ abortedJobs: 0,
811
+ retriedJobs: 0,
812
+ disabledJobs: 0,
813
+ lastUpdateTime: new Date
814
+ };
815
+ constructor(jobClass, options) {
816
+ this.queueName = options.queueName;
817
+ this.storage = options.storage;
818
+ this.jobClass = jobClass;
819
+ this.limiter = options.limiter ?? new NullLimiter;
820
+ this.workerCount = options.workerCount ?? 1;
821
+ this.pollIntervalMs = options.pollIntervalMs ?? 100;
822
+ this.deleteAfterCompletionMs = options.deleteAfterCompletionMs;
823
+ this.deleteAfterFailureMs = options.deleteAfterFailureMs;
824
+ this.deleteAfterDisabledMs = options.deleteAfterDisabledMs;
825
+ this.cleanupIntervalMs = options.cleanupIntervalMs ?? 1e4;
826
+ this.initializeWorkers();
827
+ }
828
+ async start() {
829
+ if (this.running) {
830
+ return this;
831
+ }
832
+ this.running = true;
833
+ this.events.emit("server_start", this.queueName);
834
+ await this.fixupJobs();
835
+ await Promise.all(this.workers.map((worker) => worker.start()));
836
+ this.startCleanupLoop();
837
+ return this;
838
+ }
839
+ async stop() {
840
+ if (!this.running) {
841
+ return this;
842
+ }
843
+ this.running = false;
844
+ if (this.cleanupTimer) {
845
+ clearTimeout(this.cleanupTimer);
846
+ this.cleanupTimer = null;
847
+ }
848
+ await Promise.all(this.workers.map((worker) => worker.stop()));
849
+ this.events.emit("server_stop", this.queueName);
850
+ return this;
851
+ }
852
+ getStats() {
853
+ return { ...this.stats };
854
+ }
855
+ getStorage() {
856
+ return this.storage;
857
+ }
858
+ async scaleWorkers(count) {
859
+ if (count < 1) {
860
+ throw new Error("Worker count must be at least 1");
861
+ }
862
+ const currentCount = this.workers.length;
863
+ if (count > currentCount) {
864
+ for (let i = currentCount;i < count; i++) {
865
+ const worker = this.createWorker();
866
+ this.workers.push(worker);
867
+ if (this.running) {
868
+ await worker.start();
725
869
  }
726
870
  }
727
- } finally {
728
- setTimeout(() => this.processJobs(), this.options.waitDurationInMilliseconds);
729
- }
871
+ } else if (count < currentCount) {
872
+ const toRemove = this.workers.splice(count);
873
+ await Promise.all(toRemove.map((worker) => worker.stop()));
874
+ }
875
+ }
876
+ isRunning() {
877
+ return this.running;
878
+ }
879
+ getWorkerCount() {
880
+ return this.workers.length;
881
+ }
882
+ addClient(client) {
883
+ this.clients.add(client);
884
+ }
885
+ removeClient(client) {
886
+ this.clients.delete(client);
887
+ }
888
+ on(event, listener) {
889
+ this.events.on(event, listener);
890
+ }
891
+ off(event, listener) {
892
+ this.events.off(event, listener);
893
+ }
894
+ initializeWorkers() {
895
+ for (let i = 0;i < this.workerCount; i++) {
896
+ const worker = this.createWorker();
897
+ this.workers.push(worker);
898
+ }
899
+ }
900
+ createWorker() {
901
+ const worker = new JobQueueWorker(this.jobClass, {
902
+ storage: this.storage,
903
+ queueName: this.queueName,
904
+ limiter: this.limiter,
905
+ pollIntervalMs: this.pollIntervalMs
906
+ });
907
+ worker.on("job_start", (jobId) => {
908
+ this.stats = { ...this.stats, totalJobs: this.stats.totalJobs + 1 };
909
+ this.events.emit("job_start", this.queueName, jobId);
910
+ this.forwardToClients("handleJobStart", jobId);
911
+ });
912
+ worker.on("job_complete", async (jobId, output) => {
913
+ this.stats = { ...this.stats, completedJobs: this.stats.completedJobs + 1 };
914
+ this.updateAverageProcessingTime();
915
+ this.events.emit("job_complete", this.queueName, jobId, output);
916
+ this.forwardToClients("handleJobComplete", jobId, output);
917
+ if (this.deleteAfterCompletionMs === 0) {
918
+ await this.storage.delete(jobId);
919
+ }
920
+ });
921
+ worker.on("job_error", async (jobId, error, errorCode) => {
922
+ this.stats = { ...this.stats, failedJobs: this.stats.failedJobs + 1 };
923
+ this.events.emit("job_error", this.queueName, jobId, error);
924
+ this.forwardToClients("handleJobError", jobId, error, errorCode);
925
+ if (this.deleteAfterFailureMs === 0) {
926
+ await this.storage.delete(jobId);
927
+ }
928
+ });
929
+ worker.on("job_disabled", async (jobId) => {
930
+ this.stats = { ...this.stats, disabledJobs: this.stats.disabledJobs + 1 };
931
+ this.events.emit("job_disabled", this.queueName, jobId);
932
+ this.forwardToClients("handleJobDisabled", jobId);
933
+ if (this.deleteAfterDisabledMs === 0) {
934
+ await this.storage.delete(jobId);
935
+ }
936
+ });
937
+ worker.on("job_retry", (jobId, runAfter) => {
938
+ this.stats = { ...this.stats, retriedJobs: this.stats.retriedJobs + 1 };
939
+ this.events.emit("job_retry", this.queueName, jobId, runAfter);
940
+ this.forwardToClients("handleJobRetry", jobId, runAfter);
941
+ });
942
+ worker.on("job_progress", (jobId, progress, message, details) => {
943
+ this.events.emit("job_progress", this.queueName, jobId, progress, message, details);
944
+ this.forwardToClients("handleJobProgress", jobId, progress, message, details);
945
+ });
946
+ return worker;
730
947
  }
731
- async cleanUpJobs() {
732
- const abortingJobs = await this.peek(JobStatus.ABORTING);
733
- for (const job of abortingJobs) {
734
- await this.handleAbort(job.id);
948
+ forwardToClients(method, ...args) {
949
+ for (const client of this.clients) {
950
+ const fn = client[method];
951
+ if (typeof fn === "function") {
952
+ fn.apply(client, args);
953
+ }
735
954
  }
736
- if (this.options.deleteAfterCompletionMs) {
737
- await this.storage.deleteJobsByStatusAndAge(JobStatus.COMPLETED, this.options.deleteAfterCompletionMs);
955
+ }
956
+ updateAverageProcessingTime() {
957
+ const times = [];
958
+ for (const worker of this.workers) {
959
+ const avgTime = worker.getAverageProcessingTime();
960
+ if (avgTime !== undefined) {
961
+ times.push(avgTime);
962
+ }
738
963
  }
739
- if (this.options.deleteAfterFailureMs) {
740
- await this.storage.deleteJobsByStatusAndAge(JobStatus.FAILED, this.options.deleteAfterFailureMs);
964
+ if (times.length > 0) {
965
+ const avg = times.reduce((a, b) => a + b, 0) / times.length;
966
+ this.stats = {
967
+ ...this.stats,
968
+ averageProcessingTime: avg,
969
+ lastUpdateTime: new Date
970
+ };
741
971
  }
742
972
  }
743
- async monitorJobs() {
744
- if (!this.running) {
973
+ startCleanupLoop() {
974
+ if (!this.running)
745
975
  return;
746
- }
976
+ this.cleanupJobs().finally(() => {
977
+ if (this.running) {
978
+ this.cleanupTimer = setTimeout(() => this.startCleanupLoop(), this.cleanupIntervalMs);
979
+ }
980
+ });
981
+ }
982
+ async cleanupJobs() {
747
983
  try {
748
- const jobIds = Array.from(this.jobProgressListeners.keys());
749
- for (const jobId of jobIds) {
750
- const job = await this.get(jobId);
751
- if (job) {
752
- const currentProgress = {
753
- progress: job.progress,
754
- message: job.progressMessage,
755
- details: job.progressDetails || null
756
- };
757
- const lastProgress = this.lastKnownProgress.get(jobId);
758
- const hasChanged = !lastProgress || lastProgress.progress !== currentProgress.progress || lastProgress.message !== currentProgress.message;
759
- if (hasChanged && currentProgress.progress !== 0 && currentProgress.message !== "") {
760
- this.announceProgress(jobId, currentProgress.progress, currentProgress.message, currentProgress.details);
761
- }
762
- }
984
+ if (this.deleteAfterCompletionMs !== undefined && this.deleteAfterCompletionMs > 0) {
985
+ await this.storage.deleteJobsByStatusAndAge(JobStatus4.COMPLETED, this.deleteAfterCompletionMs);
763
986
  }
764
- for (const jobId of this.lastKnownProgress.keys()) {
765
- const job = await this.get(jobId);
766
- if (!job || job.status === JobStatus.COMPLETED || job.status === JobStatus.FAILED) {
767
- this.lastKnownProgress.delete(jobId);
768
- }
987
+ if (this.deleteAfterFailureMs !== undefined && this.deleteAfterFailureMs > 0) {
988
+ await this.storage.deleteJobsByStatusAndAge(JobStatus4.FAILED, this.deleteAfterFailureMs);
989
+ }
990
+ if (this.deleteAfterDisabledMs !== undefined && this.deleteAfterDisabledMs > 0) {
991
+ await this.storage.deleteJobsByStatusAndAge(JobStatus4.DISABLED, this.deleteAfterDisabledMs);
769
992
  }
770
993
  } catch (error) {
771
- console.error(`Error in monitorJobs: ${error}`);
994
+ console.error("Error in cleanup:", error);
772
995
  }
773
- setTimeout(() => this.monitorJobs(), this.options.waitDurationInMilliseconds);
774
996
  }
775
997
  async fixupJobs() {
776
- const stuckProcessingJobs = await this.peek(JobStatus.PROCESSING);
777
- const stuckAbortingJobs = await this.peek(JobStatus.ABORTING);
778
- const stuckJobs = [...stuckProcessingJobs, ...stuckAbortingJobs];
779
- for (const job of stuckJobs) {
780
- job.status = JobStatus.PENDING;
781
- job.runAfter = job.lastRanAt || new Date;
782
- job.progress = 0;
783
- job.progressMessage = "";
784
- job.progressDetails = null;
785
- job.error = "Restarting server";
786
- await this.storage.complete(this.classToStorage(job));
787
- await this.rescheduleJob(job, job.lastRanAt);
998
+ try {
999
+ const stuckProcessingJobs = await this.storage.peek(JobStatus4.PROCESSING);
1000
+ const stuckAbortingJobs = await this.storage.peek(JobStatus4.ABORTING);
1001
+ const stuckJobs = [...stuckProcessingJobs, ...stuckAbortingJobs];
1002
+ for (const jobData of stuckJobs) {
1003
+ const job = this.storageToClass(jobData);
1004
+ job.status = JobStatus4.PENDING;
1005
+ job.runAfter = job.lastRanAt || new Date;
1006
+ job.progress = 0;
1007
+ job.progressMessage = "";
1008
+ job.progressDetails = null;
1009
+ job.error = "Server restarted";
1010
+ await this.storage.complete(this.classToStorage(job));
1011
+ }
1012
+ } catch (error) {
1013
+ console.error("Error in fixupJobs:", error);
788
1014
  }
789
1015
  }
1016
+ storageToClass(details) {
1017
+ const toDate = (date) => {
1018
+ if (!date)
1019
+ return null;
1020
+ const d = new Date(date);
1021
+ return isNaN(d.getTime()) ? null : d;
1022
+ };
1023
+ return new this.jobClass({
1024
+ id: details.id,
1025
+ jobRunId: details.job_run_id,
1026
+ queueName: details.queue,
1027
+ fingerprint: details.fingerprint,
1028
+ input: details.input,
1029
+ output: details.output,
1030
+ runAfter: toDate(details.run_after),
1031
+ createdAt: toDate(details.created_at),
1032
+ deadlineAt: toDate(details.deadline_at),
1033
+ lastRanAt: toDate(details.last_ran_at),
1034
+ completedAt: toDate(details.completed_at),
1035
+ progress: details.progress || 0,
1036
+ progressMessage: details.progress_message || "",
1037
+ progressDetails: details.progress_details ?? null,
1038
+ status: details.status,
1039
+ error: details.error ?? null,
1040
+ errorCode: details.error_code ?? null,
1041
+ runAttempts: details.run_attempts ?? 0,
1042
+ maxRetries: details.max_retries ?? 10
1043
+ });
1044
+ }
1045
+ classToStorage(job) {
1046
+ const dateToISOString = (date) => {
1047
+ if (!date)
1048
+ return null;
1049
+ return isNaN(date.getTime()) ? null : date.toISOString();
1050
+ };
1051
+ const now = new Date().toISOString();
1052
+ return {
1053
+ id: job.id,
1054
+ job_run_id: job.jobRunId,
1055
+ queue: job.queueName || this.queueName,
1056
+ fingerprint: job.fingerprint,
1057
+ input: job.input,
1058
+ status: job.status,
1059
+ output: job.output ?? null,
1060
+ error: job.error === null ? null : String(job.error),
1061
+ error_code: job.errorCode || null,
1062
+ run_attempts: job.runAttempts ?? 0,
1063
+ max_retries: job.maxRetries ?? 10,
1064
+ run_after: dateToISOString(job.runAfter) ?? now,
1065
+ created_at: dateToISOString(job.createdAt) ?? now,
1066
+ deadline_at: dateToISOString(job.deadlineAt),
1067
+ last_ran_at: dateToISOString(job.lastRanAt),
1068
+ completed_at: dateToISOString(job.completedAt),
1069
+ progress: job.progress ?? 0,
1070
+ progress_message: job.progressMessage ?? "",
1071
+ progress_details: job.progressDetails ?? null
1072
+ };
1073
+ }
790
1074
  }
791
1075
  // src/limiter/CompositeLimiter.ts
792
1076
  class CompositeLimiter {
@@ -831,8 +1115,8 @@ class CompositeLimiter {
831
1115
  }
832
1116
  }
833
1117
  // src/limiter/ConcurrencyLimiter.ts
834
- import { createServiceToken as createServiceToken3 } from "@workglow/util";
835
- var CONCURRENT_JOB_LIMITER = createServiceToken3("jobqueue.limiter.concurrent");
1118
+ import { createServiceToken as createServiceToken2 } from "@workglow/util";
1119
+ var CONCURRENT_JOB_LIMITER = createServiceToken2("jobqueue.limiter.concurrent");
836
1120
 
837
1121
  class ConcurrencyLimiter {
838
1122
  currentRunningJobs = 0;
@@ -895,8 +1179,8 @@ class DelayLimiter {
895
1179
  }
896
1180
  }
897
1181
  // src/limiter/EvenlySpacedRateLimiter.ts
898
- import { createServiceToken as createServiceToken4 } from "@workglow/util";
899
- var EVENLY_SPACED_JOB_RATE_LIMITER = createServiceToken4("jobqueue.limiter.rate.evenlyspaced");
1182
+ import { createServiceToken as createServiceToken3 } from "@workglow/util";
1183
+ var EVENLY_SPACED_JOB_RATE_LIMITER = createServiceToken3("jobqueue.limiter.rate.evenlyspaced");
900
1184
 
901
1185
  class EvenlySpacedRateLimiter {
902
1186
  maxExecutions;
@@ -955,117 +1239,27 @@ class EvenlySpacedRateLimiter {
955
1239
  this.lastStartTime = 0;
956
1240
  }
957
1241
  }
958
- // src/limiter/InMemoryRateLimiter.ts
959
- import { createServiceToken as createServiceToken5 } from "@workglow/util";
960
- var MEMORY_JOB_RATE_LIMITER = createServiceToken5("jobqueue.limiter.rate.memory");
961
-
962
- class InMemoryRateLimiter {
963
- requests = [];
964
- nextAvailableTime = new Date;
1242
+ // src/limiter/ILimiter.ts
1243
+ import { createServiceToken as createServiceToken4 } from "@workglow/util";
1244
+ var JOB_LIMITER = createServiceToken4("jobqueue.limiter");
1245
+ // src/limiter/RateLimiter.ts
1246
+ class RateLimiter {
1247
+ storage;
1248
+ queueName;
1249
+ windowSizeInMilliseconds;
965
1250
  currentBackoffDelay;
966
1251
  maxExecutions;
967
- windowSizeInMilliseconds;
968
1252
  initialBackoffDelay;
969
1253
  backoffMultiplier;
970
1254
  maxBackoffDelay;
971
- constructor({
972
- maxExecutions,
973
- windowSizeInSeconds,
974
- initialBackoffDelay = 1000,
975
- backoffMultiplier = 2,
976
- maxBackoffDelay = 600000
977
- }) {
978
- if (maxExecutions <= 0) {
979
- throw new Error("maxExecutions must be greater than 0");
980
- }
981
- if (windowSizeInSeconds <= 0) {
982
- throw new Error("windowSizeInSeconds must be greater than 0");
983
- }
984
- if (initialBackoffDelay <= 0) {
985
- throw new Error("initialBackoffDelay must be greater than 0");
986
- }
987
- if (backoffMultiplier <= 1) {
988
- throw new Error("backoffMultiplier must be greater than 1");
989
- }
990
- if (maxBackoffDelay <= initialBackoffDelay) {
991
- throw new Error("maxBackoffDelay must be greater than initialBackoffDelay");
992
- }
993
- this.maxExecutions = maxExecutions;
994
- this.windowSizeInMilliseconds = windowSizeInSeconds * 1000;
995
- this.initialBackoffDelay = initialBackoffDelay;
996
- this.backoffMultiplier = backoffMultiplier;
997
- this.maxBackoffDelay = maxBackoffDelay;
998
- this.currentBackoffDelay = initialBackoffDelay;
999
- }
1000
- removeOldRequests() {
1001
- const now = Date.now();
1002
- const cutoff = now - this.windowSizeInMilliseconds;
1003
- this.requests = this.requests.filter((d) => d.getTime() > cutoff);
1004
- if (this.nextAvailableTime.getTime() < now) {
1005
- this.nextAvailableTime = new Date(now);
1006
- }
1007
- }
1008
- increaseBackoff() {
1009
- this.currentBackoffDelay = Math.min(this.currentBackoffDelay * this.backoffMultiplier, this.maxBackoffDelay);
1010
- }
1011
- addJitter(base) {
1012
- return base + Math.random() * base;
1013
- }
1014
- async canProceed() {
1015
- this.removeOldRequests();
1016
- const now = Date.now();
1017
- const okRequestCount = this.requests.length < this.maxExecutions;
1018
- const okTime = now >= this.nextAvailableTime.getTime();
1019
- const canProceedNow = okRequestCount && okTime;
1020
- if (!canProceedNow) {
1021
- this.increaseBackoff();
1022
- } else {
1023
- this.currentBackoffDelay = this.initialBackoffDelay;
1024
- }
1025
- return canProceedNow;
1026
- }
1027
- async recordJobStart() {
1028
- this.requests.push(new Date);
1029
- if (this.requests.length >= this.maxExecutions) {
1030
- const earliest = this.requests[0].getTime();
1031
- const windowExpires = earliest + this.windowSizeInMilliseconds;
1032
- const backoffExpires = Date.now() + this.addJitter(this.currentBackoffDelay);
1033
- this.nextAvailableTime = new Date(Math.max(windowExpires, backoffExpires));
1034
- }
1035
- }
1036
- async recordJobCompletion() {}
1037
- async getNextAvailableTime() {
1038
- this.removeOldRequests();
1039
- return new Date(Math.max(Date.now(), this.nextAvailableTime.getTime()));
1040
- }
1041
- async setNextAvailableTime(date) {
1042
- if (date.getTime() > this.nextAvailableTime.getTime()) {
1043
- this.nextAvailableTime = date;
1044
- }
1045
- }
1046
- async clear() {
1047
- this.requests = [];
1048
- this.nextAvailableTime = new Date;
1049
- this.currentBackoffDelay = this.initialBackoffDelay;
1050
- }
1051
- }
1052
- // src/limiter/PostgresRateLimiter.ts
1053
- import { createServiceToken as createServiceToken6 } from "@workglow/util";
1054
- var POSTGRES_JOB_RATE_LIMITER = createServiceToken6("jobqueue.limiter.rate.postgres");
1055
-
1056
- class PostgresRateLimiter {
1057
- db;
1058
- queueName;
1059
- windowSizeInMilliseconds;
1060
- currentBackoffDelay;
1061
- constructor(db, queueName, {
1255
+ constructor(storage, queueName, {
1062
1256
  maxExecutions,
1063
1257
  windowSizeInSeconds,
1064
1258
  initialBackoffDelay = 1000,
1065
1259
  backoffMultiplier = 2,
1066
1260
  maxBackoffDelay = 600000
1067
1261
  }) {
1068
- this.db = db;
1262
+ this.storage = storage;
1069
1263
  this.queueName = queueName;
1070
1264
  if (maxExecutions <= 0) {
1071
1265
  throw new Error("maxExecutions must be greater than 0");
@@ -1088,281 +1282,87 @@ class PostgresRateLimiter {
1088
1282
  this.backoffMultiplier = backoffMultiplier;
1089
1283
  this.maxBackoffDelay = maxBackoffDelay;
1090
1284
  this.currentBackoffDelay = initialBackoffDelay;
1091
- this.dbPromise = this.ensureTableExists();
1092
1285
  }
1093
- maxExecutions;
1094
- initialBackoffDelay;
1095
- backoffMultiplier;
1096
- maxBackoffDelay;
1097
- dbPromise;
1098
1286
  addJitter(base) {
1099
1287
  return base + Math.random() * base;
1100
1288
  }
1101
1289
  increaseBackoff() {
1102
1290
  this.currentBackoffDelay = Math.min(this.currentBackoffDelay * this.backoffMultiplier, this.maxBackoffDelay);
1103
1291
  }
1104
- async ensureTableExists() {
1105
- await this.db.query(`
1106
- CREATE TABLE IF NOT EXISTS job_queue_execution_tracking (
1107
- id SERIAL PRIMARY KEY,
1108
- queue_name TEXT NOT NULL,
1109
- executed_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
1110
- )
1111
- `);
1112
- await this.db.query(`
1113
- CREATE TABLE IF NOT EXISTS job_queue_next_available (
1114
- queue_name TEXT PRIMARY KEY,
1115
- next_available_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
1116
- )
1117
- `);
1118
- }
1119
- async clear() {
1120
- await this.dbPromise;
1121
- await this.db.query(`DELETE FROM job_queue_execution_tracking WHERE queue_name = $1`, [
1122
- this.queueName
1123
- ]);
1124
- await this.db.query(`DELETE FROM job_queue_next_available WHERE queue_name = $1`, [
1125
- this.queueName
1126
- ]);
1127
- }
1128
1292
  async canProceed() {
1129
- await this.dbPromise;
1130
- const nextAvailableResult = await this.db.query(`
1131
- SELECT next_available_at
1132
- FROM job_queue_next_available
1133
- WHERE queue_name = $1
1134
- `, [this.queueName]);
1135
- const nextAvailableTime = nextAvailableResult.rows[0]?.next_available_at;
1136
- if (nextAvailableTime && new Date(nextAvailableTime).getTime() > Date.now()) {
1137
- this.increaseBackoff();
1138
- return false;
1139
- }
1140
- const result = await this.db.query(`
1141
- SELECT
1142
- COUNT(*) AS attempt_count
1143
- FROM job_queue_execution_tracking
1144
- WHERE
1145
- queue_name = $1
1146
- AND executed_at > $2
1147
- `, [this.queueName, new Date(Date.now() - this.windowSizeInMilliseconds).toISOString()]);
1148
- const attemptCount = result.rows[0]?.attempt_count;
1293
+ const windowStartTime = new Date(Date.now() - this.windowSizeInMilliseconds).toISOString();
1294
+ const attemptCount = await this.storage.getExecutionCount(this.queueName, windowStartTime);
1149
1295
  const canProceedNow = attemptCount < this.maxExecutions;
1150
- if (!canProceedNow) {
1151
- this.increaseBackoff();
1152
- } else {
1296
+ if (canProceedNow) {
1297
+ const nextAvailableTime2 = await this.storage.getNextAvailableTime(this.queueName);
1298
+ if (nextAvailableTime2 && new Date(nextAvailableTime2).getTime() > Date.now()) {
1299
+ const pastTime = new Date(Date.now() - 1000);
1300
+ await this.storage.setNextAvailableTime(this.queueName, pastTime.toISOString());
1301
+ }
1153
1302
  this.currentBackoffDelay = this.initialBackoffDelay;
1303
+ return true;
1154
1304
  }
1155
- return canProceedNow;
1156
- }
1157
- async recordJobStart() {
1158
- await this.dbPromise;
1159
- await this.db.query(`
1160
- INSERT INTO job_queue_execution_tracking (queue_name)
1161
- VALUES ($1)
1162
- `, [this.queueName]);
1163
- const result = await this.db.query(`
1164
- SELECT COUNT(*) AS attempt_count
1165
- FROM job_queue_execution_tracking
1166
- WHERE queue_name = $1
1167
- `, [this.queueName]);
1168
- if (result.rows[0].attempt_count >= this.maxExecutions) {
1169
- const backoffExpires = new Date(Date.now() + this.addJitter(this.currentBackoffDelay));
1170
- await this.setNextAvailableTime(backoffExpires);
1171
- }
1172
- }
1173
- async recordJobCompletion() {}
1174
- async getNextAvailableTime() {
1175
- await this.dbPromise;
1176
- const result = await this.db.query(`
1177
- SELECT executed_at
1178
- FROM job_queue_execution_tracking
1179
- WHERE queue_name = $1
1180
- ORDER BY executed_at ASC
1181
- LIMIT 1 OFFSET $2
1182
- `, [this.queueName, this.maxExecutions - 1]);
1183
- const oldestExecution = result.rows[0];
1184
- let rateLimitedTime = new Date;
1185
- if (oldestExecution) {
1186
- rateLimitedTime = new Date(oldestExecution.executed_at);
1187
- rateLimitedTime.setSeconds(rateLimitedTime.getSeconds() + this.windowSizeInMilliseconds / 1000);
1188
- }
1189
- const nextAvailableResult = await this.db.query(`
1190
- SELECT next_available_at
1191
- FROM job_queue_next_available
1192
- WHERE queue_name = $1
1193
- `, [this.queueName]);
1194
- let nextAvailableTime = new Date;
1195
- if (nextAvailableResult?.rows[0]?.next_available_at) {
1196
- nextAvailableTime = new Date(nextAvailableResult.rows[0].next_available_at);
1197
- }
1198
- return nextAvailableTime > rateLimitedTime ? nextAvailableTime : rateLimitedTime;
1199
- }
1200
- async setNextAvailableTime(date) {
1201
- await this.dbPromise;
1202
- await this.db.query(`
1203
- INSERT INTO job_queue_next_available (queue_name, next_available_at)
1204
- VALUES ($1, $2)
1205
- ON CONFLICT (queue_name)
1206
- DO UPDATE SET next_available_at = EXCLUDED.next_available_at;
1207
- `, [this.queueName, date.toISOString()]);
1208
- }
1209
- }
1210
- // src/limiter/SqliteRateLimiter.ts
1211
- import { createServiceToken as createServiceToken7, toSQLiteTimestamp } from "@workglow/util";
1212
- var SQLITE_JOB_RATE_LIMITER = createServiceToken7("jobqueue.limiter.rate.sqlite");
1213
-
1214
- class SqliteRateLimiter {
1215
- db;
1216
- queueName;
1217
- windowSizeInMilliseconds;
1218
- currentBackoffDelay;
1219
- constructor(db, queueName, {
1220
- maxExecutions,
1221
- windowSizeInSeconds,
1222
- initialBackoffDelay = 1000,
1223
- backoffMultiplier = 2,
1224
- maxBackoffDelay = 600000
1225
- }) {
1226
- if (maxExecutions <= 0) {
1227
- throw new Error("maxExecutions must be greater than 0");
1228
- }
1229
- if (windowSizeInSeconds <= 0) {
1230
- throw new Error("windowSizeInSeconds must be greater than 0");
1231
- }
1232
- if (initialBackoffDelay <= 0) {
1233
- throw new Error("initialBackoffDelay must be greater than 0");
1234
- }
1235
- if (backoffMultiplier <= 1) {
1236
- throw new Error("backoffMultiplier must be greater than 1");
1237
- }
1238
- if (maxBackoffDelay <= initialBackoffDelay) {
1239
- throw new Error("maxBackoffDelay must be greater than initialBackoffDelay");
1240
- }
1241
- this.db = db;
1242
- this.queueName = queueName;
1243
- this.windowSizeInMilliseconds = windowSizeInSeconds * 1000;
1244
- this.maxExecutions = maxExecutions;
1245
- this.initialBackoffDelay = initialBackoffDelay;
1246
- this.backoffMultiplier = backoffMultiplier;
1247
- this.maxBackoffDelay = maxBackoffDelay;
1248
- this.currentBackoffDelay = initialBackoffDelay;
1249
- this.ensureTableExists();
1250
- }
1251
- maxExecutions;
1252
- initialBackoffDelay;
1253
- backoffMultiplier;
1254
- maxBackoffDelay;
1255
- addJitter(base) {
1256
- return base + Math.random() * base;
1257
- }
1258
- increaseBackoff() {
1259
- this.currentBackoffDelay = Math.min(this.currentBackoffDelay * this.backoffMultiplier, this.maxBackoffDelay);
1260
- }
1261
- ensureTableExists() {
1262
- this.db.exec(`
1263
- CREATE TABLE IF NOT EXISTS job_queue_execution_tracking (
1264
- id INTEGER PRIMARY KEY,
1265
- queue_name TEXT NOT NULL,
1266
- executed_at TEXT DEFAULT CURRENT_TIMESTAMP
1267
- )
1268
- `);
1269
- this.db.exec(`
1270
- CREATE TABLE IF NOT EXISTS job_queue_next_available (
1271
- queue_name TEXT PRIMARY KEY,
1272
- next_available_at TEXT
1273
- )
1274
- `);
1275
- return this;
1276
- }
1277
- async clear() {
1278
- this.db.prepare("DELETE FROM job_queue_execution_tracking WHERE queue_name = ?").run(this.queueName);
1279
- this.db.prepare("DELETE FROM job_queue_next_available WHERE queue_name = ?").run(this.queueName);
1280
- }
1281
- async canProceed() {
1282
- const nextAvailableTimeStmt = this.db.prepare(`
1283
- SELECT next_available_at
1284
- FROM job_queue_next_available
1285
- WHERE queue_name = ?`);
1286
- const nextAvailableResult = nextAvailableTimeStmt.get(this.queueName);
1287
- if (nextAvailableResult && new Date(nextAvailableResult.next_available_at + "Z").getTime() > Date.now()) {
1305
+ const nextAvailableTime = await this.storage.getNextAvailableTime(this.queueName);
1306
+ if (nextAvailableTime && new Date(nextAvailableTime).getTime() > Date.now()) {
1288
1307
  this.increaseBackoff();
1289
1308
  return false;
1290
1309
  }
1291
- const thresholdTime = toSQLiteTimestamp(new Date(Date.now() - this.windowSizeInMilliseconds));
1292
- const result = this.db.prepare(`SELECT COUNT(*) AS attempt_count
1293
- FROM job_queue_execution_tracking
1294
- WHERE queue_name = ? AND executed_at > ?`).get(this.queueName, thresholdTime);
1295
- const canProceedNow = result.attempt_count < this.maxExecutions;
1296
- if (!canProceedNow) {
1297
- this.increaseBackoff();
1298
- } else {
1299
- this.currentBackoffDelay = this.initialBackoffDelay;
1300
- }
1301
- return canProceedNow;
1310
+ this.increaseBackoff();
1311
+ return false;
1302
1312
  }
1303
1313
  async recordJobStart() {
1304
- const stmt = this.db.prepare(`INSERT INTO job_queue_execution_tracking (queue_name)
1305
- VALUES (?)`).run(this.queueName);
1306
- const result = this.db.prepare(`SELECT COUNT(*) AS attempt_count
1307
- FROM job_queue_execution_tracking
1308
- WHERE queue_name = ?`).get(this.queueName);
1309
- if (result.attempt_count >= this.maxExecutions) {
1314
+ await this.storage.recordExecution(this.queueName);
1315
+ const windowStartTime = new Date(Date.now() - this.windowSizeInMilliseconds).toISOString();
1316
+ const attemptCount = await this.storage.getExecutionCount(this.queueName, windowStartTime);
1317
+ if (attemptCount >= this.maxExecutions) {
1310
1318
  const backoffExpires = new Date(Date.now() + this.addJitter(this.currentBackoffDelay));
1311
1319
  await this.setNextAvailableTime(backoffExpires);
1320
+ } else {
1321
+ const nextAvailableTime = await this.storage.getNextAvailableTime(this.queueName);
1322
+ if (nextAvailableTime && new Date(nextAvailableTime).getTime() > Date.now()) {
1323
+ const pastTime = new Date(Date.now() - 1000);
1324
+ await this.storage.setNextAvailableTime(this.queueName, pastTime.toISOString());
1325
+ }
1312
1326
  }
1313
1327
  }
1314
1328
  async recordJobCompletion() {}
1315
1329
  async getNextAvailableTime() {
1316
- const rateLimitedTimeStmt = this.db.prepare(`
1317
- SELECT executed_at
1318
- FROM job_queue_execution_tracking
1319
- WHERE queue_name = ?
1320
- ORDER BY executed_at ASC
1321
- LIMIT 1 OFFSET ?`);
1322
- const oldestExecution = rateLimitedTimeStmt.get(this.queueName, this.maxExecutions - 1);
1330
+ const oldestExecution = await this.storage.getOldestExecutionAtOffset(this.queueName, this.maxExecutions - 1);
1323
1331
  let rateLimitedTime = new Date;
1324
1332
  if (oldestExecution) {
1325
- rateLimitedTime = new Date(oldestExecution.executed_at + "Z");
1333
+ rateLimitedTime = new Date(oldestExecution);
1326
1334
  rateLimitedTime.setSeconds(rateLimitedTime.getSeconds() + this.windowSizeInMilliseconds / 1000);
1327
1335
  }
1328
- const nextAvailableStmt = this.db.prepare(`
1329
- SELECT next_available_at
1330
- FROM job_queue_next_available
1331
- WHERE queue_name = ?`);
1332
- const nextAvailableResult = nextAvailableStmt.get(this.queueName);
1336
+ const nextAvailableStr = await this.storage.getNextAvailableTime(this.queueName);
1333
1337
  let nextAvailableTime = new Date;
1334
- if (nextAvailableResult?.next_available_at) {
1335
- nextAvailableTime = new Date(nextAvailableResult.next_available_at + "Z");
1338
+ if (nextAvailableStr) {
1339
+ nextAvailableTime = new Date(nextAvailableStr);
1336
1340
  }
1337
1341
  return nextAvailableTime > rateLimitedTime ? nextAvailableTime : rateLimitedTime;
1338
1342
  }
1339
1343
  async setNextAvailableTime(date) {
1340
- const nextAvailableAt = date.toISOString();
1341
- this.db.prepare(`
1342
- INSERT INTO job_queue_next_available (queue_name, next_available_at)
1343
- VALUES (?, ?)
1344
- ON CONFLICT(queue_name) DO UPDATE SET next_available_at = excluded.next_available_at`).run(this.queueName, nextAvailableAt);
1344
+ await this.storage.setNextAvailableTime(this.queueName, date.toISOString());
1345
+ }
1346
+ async clear() {
1347
+ await this.storage.clear(this.queueName);
1348
+ this.currentBackoffDelay = this.initialBackoffDelay;
1345
1349
  }
1346
1350
  }
1347
1351
  export {
1348
- SqliteRateLimiter,
1349
- SQLITE_JOB_RATE_LIMITER,
1350
1352
  RetryableJobError,
1351
- QueueMode,
1352
- PostgresRateLimiter,
1353
+ RateLimiter,
1353
1354
  PermanentJobError,
1354
- POSTGRES_JOB_RATE_LIMITER,
1355
1355
  NullLimiter,
1356
1356
  NULL_JOB_LIMITER,
1357
- MEMORY_JOB_RATE_LIMITER,
1358
1357
  JobStatus,
1359
- JobQueue,
1358
+ JobQueueWorker,
1359
+ JobQueueServer,
1360
+ JobQueueClient,
1360
1361
  JobNotFoundError,
1361
1362
  JobError,
1362
1363
  JobDisabledError,
1363
1364
  Job,
1364
1365
  JOB_LIMITER,
1365
- InMemoryRateLimiter,
1366
1366
  EvenlySpacedRateLimiter,
1367
1367
  EVENLY_SPACED_JOB_RATE_LIMITER,
1368
1368
  DelayLimiter,
@@ -1372,4 +1372,4 @@ export {
1372
1372
  AbortSignalJobError
1373
1373
  };
1374
1374
 
1375
- //# debugId=2842240A00E46E7C64756E2164756E21
1375
+ //# debugId=FFD59B0DF6C7A03464756E2164756E21