@workglow/job-queue 0.2.23 → 0.2.24

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/browser.js CHANGED
@@ -303,6 +303,7 @@ class JobQueueClient {
303
303
  status: JobStatus2.PENDING
304
304
  };
305
305
  const id = await this.storage.add(job);
306
+ this.server?.handleJobAdded(id);
306
307
  return this.createJobHandle(id);
307
308
  }
308
309
  async submitBatch(inputs, options) {
@@ -342,29 +343,52 @@ class JobQueueClient {
342
343
  async waitFor(jobId) {
343
344
  if (!jobId)
344
345
  throw new JobNotFoundError("Cannot wait for undefined job");
346
+ const { promise, resolve, reject } = Promise.withResolvers();
347
+ promise.catch(() => {});
348
+ const promises = this.activeJobPromises.get(jobId) || [];
349
+ promises.push({ resolve, reject });
350
+ this.activeJobPromises.set(jobId, promises);
345
351
  const job = await this.getJob(jobId);
346
- if (!job)
352
+ if (!job) {
353
+ this.removePromise(jobId, resolve, reject);
347
354
  throw new JobNotFoundError(`Job ${jobId} not found`);
355
+ }
348
356
  if (job.status === JobStatus2.COMPLETED) {
357
+ this.removePromise(jobId, resolve, reject);
349
358
  return job.output;
350
359
  }
351
360
  if (job.status === JobStatus2.DISABLED) {
361
+ this.removePromise(jobId, resolve, reject);
352
362
  throw new JobDisabledError(`Job ${jobId} was disabled`);
353
363
  }
354
364
  if (job.status === JobStatus2.FAILED) {
365
+ this.removePromise(jobId, resolve, reject);
355
366
  throw this.buildErrorFromJob(job);
356
367
  }
357
- const { promise, resolve, reject } = Promise.withResolvers();
358
- promise.catch(() => {});
359
- const promises = this.activeJobPromises.get(jobId) || [];
360
- promises.push({ resolve, reject });
361
- this.activeJobPromises.set(jobId, promises);
362
368
  return promise;
363
369
  }
370
+ removePromise(jobId, resolve, reject) {
371
+ const list = this.activeJobPromises.get(jobId);
372
+ if (!list)
373
+ return;
374
+ const idx = list.findIndex((p) => p.resolve === resolve && p.reject === reject);
375
+ if (idx !== -1)
376
+ list.splice(idx, 1);
377
+ if (list.length === 0)
378
+ this.activeJobPromises.delete(jobId);
379
+ }
364
380
  async abort(jobId) {
365
381
  if (!jobId)
366
382
  throw new JobNotFoundError("Cannot abort undefined job");
367
- await this.storage.abort(jobId);
383
+ const firedLocally = this.server?.abortJob(jobId) ?? false;
384
+ if (!firedLocally) {
385
+ try {
386
+ await this.storage.abort(jobId);
387
+ } finally {
388
+ this.events.emit("job_aborting", this.queueName, jobId);
389
+ }
390
+ return;
391
+ }
368
392
  this.events.emit("job_aborting", this.queueName, jobId);
369
393
  }
370
394
  async abortJobRun(jobRunId) {
@@ -531,7 +555,7 @@ class JobQueueClient {
531
555
 
532
556
  // src/job/JobQueueServer.ts
533
557
  import { JobStatus as JobStatus4 } from "@workglow/storage";
534
- import { EventEmitter as EventEmitter3 } from "@workglow/util";
558
+ import { EventEmitter as EventEmitter3, getLogger as getLogger2 } from "@workglow/util";
535
559
 
536
560
  // src/limiter/NullLimiter.ts
537
561
  import { createServiceToken } from "@workglow/util";
@@ -567,10 +591,14 @@ class JobQueueWorker {
567
591
  jobClass;
568
592
  limiter;
569
593
  pollIntervalMs;
594
+ stopTimeoutMs;
570
595
  events = new EventEmitter2;
571
596
  running = false;
597
+ inFlight = new Map;
572
598
  wakeResolve = null;
573
599
  wakeTimer = null;
600
+ wakePending = false;
601
+ loopPromise = null;
574
602
  activeJobAbortControllers = new Map;
575
603
  processingTimes = new Map;
576
604
  constructor(jobClass, options) {
@@ -580,6 +608,7 @@ class JobQueueWorker {
580
608
  this.jobClass = jobClass;
581
609
  this.limiter = options.limiter ?? new NullLimiter;
582
610
  this.pollIntervalMs = options.pollIntervalMs ?? 100;
611
+ this.stopTimeoutMs = options.stopTimeoutMs ?? 30000;
583
612
  }
584
613
  async start() {
585
614
  if (this.running) {
@@ -587,17 +616,27 @@ class JobQueueWorker {
587
616
  }
588
617
  this.running = true;
589
618
  this.events.emit("worker_start");
590
- this.processJobs();
619
+ this.loopPromise = this.processJobs();
591
620
  return this;
592
621
  }
622
+ requestAbort(jobId) {
623
+ const controller = this.activeJobAbortControllers.get(jobId);
624
+ if (controller && !controller.signal.aborted) {
625
+ controller.abort();
626
+ return true;
627
+ }
628
+ return false;
629
+ }
593
630
  notify() {
631
+ this.wakePending = true;
594
632
  if (this.wakeResolve) {
595
633
  if (this.wakeTimer) {
596
634
  clearTimeout(this.wakeTimer);
597
635
  this.wakeTimer = null;
598
636
  }
599
- this.wakeResolve();
637
+ const resolve = this.wakeResolve;
600
638
  this.wakeResolve = null;
639
+ resolve();
601
640
  }
602
641
  }
603
642
  async stop() {
@@ -606,15 +645,24 @@ class JobQueueWorker {
606
645
  }
607
646
  this.running = false;
608
647
  this.notify();
609
- const size = await this.storage.size(JobStatus3.PROCESSING);
610
- const sleepTime = Math.max(100, size * 2);
611
- await sleep(sleepTime);
612
- for (const controller of this.activeJobAbortControllers.values()) {
613
- if (!controller.signal.aborted) {
614
- controller.abort();
648
+ const loopPromise = this.loopPromise;
649
+ this.loopPromise = null;
650
+ if (loopPromise) {
651
+ await loopPromise;
652
+ }
653
+ if (this.stopTimeoutMs > 0 && this.inFlight.size > 0) {
654
+ const drain = Promise.allSettled([...this.inFlight.values()]);
655
+ await Promise.race([drain, sleep(this.stopTimeoutMs)]);
656
+ }
657
+ if (this.inFlight.size > 0) {
658
+ for (const controller of this.activeJobAbortControllers.values()) {
659
+ if (!controller.signal.aborted) {
660
+ controller.abort();
661
+ }
615
662
  }
663
+ const abortDrain = Promise.allSettled([...this.inFlight.values()]);
664
+ await Promise.race([abortDrain, sleep(1000)]);
616
665
  }
617
- await sleep(sleepTime);
618
666
  this.events.emit("worker_stop");
619
667
  return this;
620
668
  }
@@ -662,6 +710,10 @@ class JobQueueWorker {
662
710
  if (canProceed) {
663
711
  const job = await this.next();
664
712
  if (job) {
713
+ if (!this.running) {
714
+ await this.releaseClaimedJob(job);
715
+ return;
716
+ }
665
717
  this.processSingleJob(job);
666
718
  continue;
667
719
  }
@@ -690,18 +742,27 @@ class JobQueueWorker {
690
742
  return this.pollIntervalMs;
691
743
  }
692
744
  waitForWakeOrTimeout(timeoutMs) {
745
+ if (this.wakePending) {
746
+ this.wakePending = false;
747
+ return Promise.resolve();
748
+ }
693
749
  return new Promise((resolve) => {
694
750
  this.wakeTimer = setTimeout(() => {
695
751
  this.wakeTimer = null;
696
752
  this.wakeResolve = null;
753
+ this.wakePending = false;
697
754
  resolve();
698
755
  }, timeoutMs);
699
756
  this.wakeResolve = () => {
757
+ this.wakePending = false;
700
758
  resolve();
701
759
  };
702
760
  });
703
761
  }
704
762
  async checkForAbortingJobs() {
763
+ if (this.activeJobAbortControllers.size === 0) {
764
+ return;
765
+ }
705
766
  const abortingJobs = await this.storage.peek(JobStatus3.ABORTING);
706
767
  for (const jobData of abortingJobs) {
707
768
  const controller = this.activeJobAbortControllers.get(jobData.id);
@@ -714,6 +775,8 @@ class JobQueueWorker {
714
775
  if (!job || !job.id) {
715
776
  throw new JobNotFoundError("Invalid job provided for processing");
716
777
  }
778
+ const { promise: inFlightPromise, resolve: resolveInFlight } = Promise.withResolvers();
779
+ this.inFlight.set(job.id, inFlightPromise);
717
780
  const startTime = Date.now();
718
781
  const telemetry = getTelemetryProvider();
719
782
  const span = telemetry.isEnabled ? telemetry.startSpan("workglow.job.process", {
@@ -764,7 +827,12 @@ class JobQueueWorker {
764
827
  span?.setAttributes({ "workglow.job.error": spanErrorMessage });
765
828
  } finally {
766
829
  span?.end();
767
- await this.limiter.recordJobCompletion();
830
+ try {
831
+ await this.limiter.recordJobCompletion();
832
+ } finally {
833
+ this.inFlight.delete(job.id);
834
+ resolveInFlight();
835
+ }
768
836
  }
769
837
  }
770
838
  async executeJob(job, signal) {
@@ -777,7 +845,6 @@ class JobQueueWorker {
777
845
  }
778
846
  async updateProgress(jobId, progress, message = "", details = null) {
779
847
  progress = Math.max(0, Math.min(100, progress));
780
- await this.storage.saveProgress(jobId, progress, message, details);
781
848
  this.events.emit("job_progress", jobId, progress, message, details);
782
849
  }
783
850
  async completeJob(job, output) {
@@ -830,6 +897,13 @@ class JobQueueWorker {
830
897
  this.cleanupJob(job.id);
831
898
  }
832
899
  }
900
+ async releaseClaimedJob(job) {
901
+ try {
902
+ await this.storage.release(job.id);
903
+ } catch (err) {
904
+ getLogger().error("releaseClaimedJob errored:", { error: err });
905
+ }
906
+ }
833
907
  async rescheduleJob(job, retryDate) {
834
908
  try {
835
909
  job.status = JobStatus3.PENDING;
@@ -848,6 +922,9 @@ class JobQueueWorker {
848
922
  createAbortController(jobId) {
849
923
  if (!jobId)
850
924
  throw new JobNotFoundError("Cannot create abort controller for undefined job");
925
+ if (!this.inFlight.has(jobId)) {
926
+ throw new Error(`createAbortController invariant violated: jobId ${String(jobId)} is not in inFlight. ` + `Abort controllers must only be created from within processSingleJob.`);
927
+ }
851
928
  if (this.activeJobAbortControllers.has(jobId)) {
852
929
  return this.activeJobAbortControllers.get(jobId);
853
930
  }
@@ -857,13 +934,18 @@ class JobQueueWorker {
857
934
  return abortController;
858
935
  }
859
936
  async handleAbort(jobId) {
937
+ if (this.inFlight.has(jobId)) {
938
+ return;
939
+ }
860
940
  const job = await this.getJob(jobId);
861
941
  if (!job) {
862
942
  getLogger().error("handleAbort: job not found", { jobId });
863
943
  return;
864
944
  }
865
- const error = new AbortSignalJobError("Job Aborted");
866
- await this.failJob(job, error);
945
+ if (job.status === JobStatus3.COMPLETED || job.status === JobStatus3.FAILED || job.status === JobStatus3.DISABLED) {
946
+ return;
947
+ }
948
+ await this.failJob(job, new AbortSignalJobError("Job Aborted"));
867
949
  }
868
950
  async getJob(id) {
869
951
  const job = await this.storage.get(id);
@@ -920,6 +1002,7 @@ class JobQueueServer {
920
1002
  deleteAfterFailureMs;
921
1003
  deleteAfterDisabledMs;
922
1004
  cleanupIntervalMs;
1005
+ stopTimeoutMs;
923
1006
  events = new EventEmitter3;
924
1007
  workers = [];
925
1008
  clients = new Set;
@@ -946,6 +1029,7 @@ class JobQueueServer {
946
1029
  this.deleteAfterFailureMs = options.deleteAfterFailureMs;
947
1030
  this.deleteAfterDisabledMs = options.deleteAfterDisabledMs;
948
1031
  this.cleanupIntervalMs = options.cleanupIntervalMs ?? 1e4;
1032
+ this.stopTimeoutMs = options.stopTimeoutMs;
949
1033
  this.initializeWorkers();
950
1034
  }
951
1035
  async start() {
@@ -961,11 +1045,21 @@ class JobQueueServer {
961
1045
  this.notifyWorkers();
962
1046
  }
963
1047
  });
964
- } catch {}
1048
+ } catch (err) {
1049
+ getLogger2().debug("subscribeToChanges unsupported on this storage", {
1050
+ queueName: this.queueName,
1051
+ error: err
1052
+ });
1053
+ }
965
1054
  await Promise.all(this.workers.map((worker) => worker.start()));
966
- this.startCleanupLoop();
1055
+ if (this.hasRetentionTtls()) {
1056
+ this.startCleanupLoop();
1057
+ }
967
1058
  return this;
968
1059
  }
1060
+ hasRetentionTtls() {
1061
+ return this.deleteAfterCompletionMs !== undefined && this.deleteAfterCompletionMs > 0 || this.deleteAfterFailureMs !== undefined && this.deleteAfterFailureMs > 0 || this.deleteAfterDisabledMs !== undefined && this.deleteAfterDisabledMs > 0;
1062
+ }
969
1063
  async stop() {
970
1064
  if (!this.running) {
971
1065
  return this;
@@ -1024,6 +1118,17 @@ class JobQueueServer {
1024
1118
  worker.notify();
1025
1119
  }
1026
1120
  }
1121
+ handleJobAdded(_jobId) {
1122
+ this.notifyWorkers();
1123
+ }
1124
+ abortJob(jobId) {
1125
+ for (const worker of this.workers) {
1126
+ if (worker.requestAbort(jobId)) {
1127
+ return true;
1128
+ }
1129
+ }
1130
+ return false;
1131
+ }
1027
1132
  on(event, listener) {
1028
1133
  this.events.on(event, listener);
1029
1134
  }
@@ -1041,7 +1146,8 @@ class JobQueueServer {
1041
1146
  storage: this.storage,
1042
1147
  queueName: this.queueName,
1043
1148
  limiter: this.limiter,
1044
- pollIntervalMs: this.pollIntervalMs
1149
+ pollIntervalMs: this.pollIntervalMs,
1150
+ stopTimeoutMs: this.stopTimeoutMs
1045
1151
  });
1046
1152
  worker.on("job_start", (jobId) => {
1047
1153
  this.stats = { ...this.stats, totalJobs: this.stats.totalJobs + 1 };
@@ -1490,4 +1596,4 @@ export {
1490
1596
  AbortSignalJobError
1491
1597
  };
1492
1598
 
1493
- //# debugId=A8F0F2BEFB3C518064756E2164756E21
1599
+ //# debugId=DEA35BFCE014423364756E2164756E21