@workglow/job-queue 0.0.56 → 0.0.58

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