@workglow/job-queue 0.2.26 → 0.2.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/dist/browser.js +650 -73
  2. package/dist/browser.js.map +20 -15
  3. package/dist/bun.js +650 -73
  4. package/dist/bun.js.map +20 -15
  5. package/dist/common.d.ts +5 -0
  6. package/dist/common.d.ts.map +1 -1
  7. package/dist/job/Job.d.ts +1 -1
  8. package/dist/job/Job.d.ts.map +1 -1
  9. package/dist/job/JobQueueClient.d.ts +2 -1
  10. package/dist/job/JobQueueClient.d.ts.map +1 -1
  11. package/dist/job/JobQueueServer.d.ts +1 -1
  12. package/dist/job/JobQueueServer.d.ts.map +1 -1
  13. package/dist/job/JobQueueWorker.d.ts +8 -3
  14. package/dist/job/JobQueueWorker.d.ts.map +1 -1
  15. package/dist/job/JobStorageConverters.d.ts +1 -1
  16. package/dist/job/JobStorageConverters.d.ts.map +1 -1
  17. package/dist/limiter/CompositeLimiter.d.ts +17 -1
  18. package/dist/limiter/CompositeLimiter.d.ts.map +1 -1
  19. package/dist/limiter/ConcurrencyLimiter.d.ts +11 -1
  20. package/dist/limiter/ConcurrencyLimiter.d.ts.map +1 -1
  21. package/dist/limiter/DelayLimiter.d.ts +12 -1
  22. package/dist/limiter/DelayLimiter.d.ts.map +1 -1
  23. package/dist/limiter/EvenlySpacedRateLimiter.d.ts +18 -1
  24. package/dist/limiter/EvenlySpacedRateLimiter.d.ts.map +1 -1
  25. package/dist/limiter/ILimiter.d.ts +56 -0
  26. package/dist/limiter/ILimiter.d.ts.map +1 -1
  27. package/dist/limiter/NullLimiter.d.ts +6 -1
  28. package/dist/limiter/NullLimiter.d.ts.map +1 -1
  29. package/dist/limiter/RateLimiter.d.ts +43 -4
  30. package/dist/limiter/RateLimiter.d.ts.map +1 -1
  31. package/dist/node.js +650 -73
  32. package/dist/node.js.map +20 -15
  33. package/dist/queue-storage/IQueueStorage.d.ts +229 -0
  34. package/dist/queue-storage/IQueueStorage.d.ts.map +1 -0
  35. package/dist/queue-storage/InMemoryQueueStorage.d.ts +149 -0
  36. package/dist/queue-storage/InMemoryQueueStorage.d.ts.map +1 -0
  37. package/dist/queue-storage/TelemetryQueueStorage.d.ts +33 -0
  38. package/dist/queue-storage/TelemetryQueueStorage.d.ts.map +1 -0
  39. package/dist/rate-limiter-storage/IRateLimiterStorage.d.ts +127 -0
  40. package/dist/rate-limiter-storage/IRateLimiterStorage.d.ts.map +1 -0
  41. package/dist/rate-limiter-storage/InMemoryRateLimiterStorage.d.ts +43 -0
  42. package/dist/rate-limiter-storage/InMemoryRateLimiterStorage.d.ts.map +1 -0
  43. package/package.json +3 -8
package/dist/browser.js CHANGED
@@ -1,5 +1,14 @@
1
- // src/job/Job.ts
2
- import { JobStatus } from "@workglow/storage";
1
+ // src/queue-storage/IQueueStorage.ts
2
+ import { createServiceToken } from "@workglow/util";
3
+ var QUEUE_STORAGE = createServiceToken("jobqueue.storage");
4
+ var JobStatus = {
5
+ PENDING: "PENDING",
6
+ PROCESSING: "PROCESSING",
7
+ COMPLETED: "COMPLETED",
8
+ ABORTING: "ABORTING",
9
+ FAILED: "FAILED",
10
+ DISABLED: "DISABLED"
11
+ };
3
12
 
4
13
  // src/job/JobError.ts
5
14
  import { BaseError } from "@workglow/util";
@@ -175,7 +184,6 @@ ${fullMessage}`;
175
184
  }
176
185
 
177
186
  // src/job/JobQueueClient.ts
178
- import { JobStatus as JobStatus2 } from "@workglow/storage";
179
187
  import { EventEmitter } from "@workglow/util";
180
188
 
181
189
  // src/job/JobStorageConverters.ts
@@ -300,7 +308,7 @@ class JobQueueClient {
300
308
  run_after: options?.runAfter?.toISOString() ?? new Date().toISOString(),
301
309
  deadline_at: options?.deadlineAt?.toISOString() ?? null,
302
310
  completed_at: null,
303
- status: JobStatus2.PENDING
311
+ status: JobStatus.PENDING
304
312
  };
305
313
  const id = await this.storage.add(job);
306
314
  this.server?.handleJobAdded(id);
@@ -353,15 +361,15 @@ class JobQueueClient {
353
361
  this.removePromise(jobId, resolve, reject);
354
362
  throw new JobNotFoundError(`Job ${jobId} not found`);
355
363
  }
356
- if (job.status === JobStatus2.COMPLETED) {
364
+ if (job.status === JobStatus.COMPLETED) {
357
365
  this.removePromise(jobId, resolve, reject);
358
366
  return job.output;
359
367
  }
360
- if (job.status === JobStatus2.DISABLED) {
368
+ if (job.status === JobStatus.DISABLED) {
361
369
  this.removePromise(jobId, resolve, reject);
362
370
  throw new JobDisabledError(`Job ${jobId} was disabled`);
363
371
  }
364
- if (job.status === JobStatus2.FAILED) {
372
+ if (job.status === JobStatus.FAILED) {
365
373
  this.removePromise(jobId, resolve, reject);
366
374
  throw this.buildErrorFromJob(job);
367
375
  }
@@ -396,7 +404,7 @@ class JobQueueClient {
396
404
  throw new JobNotFoundError("Cannot abort job run with undefined jobRunId");
397
405
  const jobs = await this.getJobsByRunId(jobRunId);
398
406
  await Promise.allSettled(jobs.map((job) => {
399
- if (job.status === JobStatus2.PROCESSING || job.status === JobStatus2.PENDING) {
407
+ if (job.status === JobStatus.PROCESSING || job.status === JobStatus.PENDING) {
400
408
  return this.abort(job.id);
401
409
  }
402
410
  }));
@@ -503,15 +511,15 @@ class JobQueueClient {
503
511
  if (change.type === "UPDATE" && change.new) {
504
512
  const newStatus = change.new.status;
505
513
  const oldStatus = change.old?.status;
506
- if (newStatus === JobStatus2.PROCESSING && oldStatus === JobStatus2.PENDING) {
514
+ if (newStatus === JobStatus.PROCESSING && oldStatus === JobStatus.PENDING) {
507
515
  this.handleJobStart(jobId);
508
- } else if (newStatus === JobStatus2.COMPLETED) {
516
+ } else if (newStatus === JobStatus.COMPLETED) {
509
517
  this.handleJobComplete(jobId, change.new.output);
510
- } else if (newStatus === JobStatus2.FAILED) {
518
+ } else if (newStatus === JobStatus.FAILED) {
511
519
  this.handleJobError(jobId, change.new.error ?? "Job failed", change.new.error_code ?? undefined);
512
- } else if (newStatus === JobStatus2.DISABLED) {
520
+ } else if (newStatus === JobStatus.DISABLED) {
513
521
  this.handleJobDisabled(jobId);
514
- } else if (newStatus === JobStatus2.PENDING && oldStatus === JobStatus2.PROCESSING) {
522
+ } else if (newStatus === JobStatus.PENDING && oldStatus === JobStatus.PROCESSING) {
515
523
  const runAfter = change.new.run_after ? new Date(change.new.run_after) : new Date;
516
524
  this.handleJobRetry(jobId, runAfter);
517
525
  }
@@ -554,14 +562,19 @@ class JobQueueClient {
554
562
  }
555
563
 
556
564
  // src/job/JobQueueServer.ts
557
- import { JobStatus as JobStatus4 } from "@workglow/storage";
558
565
  import { EventEmitter as EventEmitter3, getLogger as getLogger2 } from "@workglow/util";
559
566
 
560
567
  // src/limiter/NullLimiter.ts
561
- import { createServiceToken } from "@workglow/util";
562
- var NULL_JOB_LIMITER = createServiceToken("jobqueue.limiter.null");
568
+ import { createServiceToken as createServiceToken2 } from "@workglow/util";
569
+ var NULL_JOB_LIMITER = createServiceToken2("jobqueue.limiter.null");
563
570
 
564
571
  class NullLimiter {
572
+ scope = "process";
573
+ static SENTINEL = Symbol("NullLimiter.acquired");
574
+ async tryAcquire() {
575
+ return NullLimiter.SENTINEL;
576
+ }
577
+ async release(_token) {}
565
578
  async canProceed() {
566
579
  return true;
567
580
  }
@@ -575,7 +588,6 @@ class NullLimiter {
575
588
  }
576
589
 
577
590
  // src/job/JobQueueWorker.ts
578
- import { JobStatus as JobStatus3 } from "@workglow/storage";
579
591
  import {
580
592
  EventEmitter as EventEmitter2,
581
593
  getLogger,
@@ -584,6 +596,8 @@ import {
584
596
  SpanStatusCode,
585
597
  uuid4
586
598
  } from "@workglow/util";
599
+ var MAX_LIMITER_WAKE_MS = 30000;
600
+
587
601
  class JobQueueWorker {
588
602
  queueName;
589
603
  workerId;
@@ -667,15 +681,16 @@ class JobQueueWorker {
667
681
  return this;
668
682
  }
669
683
  async processNext() {
670
- const canProceed = await this.limiter.canProceed();
671
- if (!canProceed) {
672
- return false;
673
- }
674
684
  const job = await this.next();
675
685
  if (!job) {
676
686
  return false;
677
687
  }
678
- await this.processSingleJob(job);
688
+ const limiterToken = await this.limiter.tryAcquire();
689
+ if (limiterToken === null || limiterToken === undefined) {
690
+ await this.releaseClaimedJob(job);
691
+ return false;
692
+ }
693
+ await this.processSingleJob(job, limiterToken);
679
694
  return true;
680
695
  }
681
696
  isRunning() {
@@ -706,32 +721,49 @@ class JobQueueWorker {
706
721
  while (this.running) {
707
722
  try {
708
723
  await this.checkForAbortingJobs();
709
- const canProceed = await this.limiter.canProceed();
710
- if (canProceed) {
711
- const job = await this.next();
712
- if (job) {
713
- if (!this.running) {
714
- await this.releaseClaimedJob(job);
715
- return;
716
- }
717
- this.processSingleJob(job);
718
- continue;
719
- }
720
- }
721
- if (canProceed) {
724
+ const job = await this.next();
725
+ if (!job) {
722
726
  const delay = await this.getIdleDelay();
723
727
  await this.waitForWakeOrTimeout(delay);
724
- } else {
725
- await this.waitForWakeOrTimeout(this.pollIntervalMs);
728
+ continue;
729
+ }
730
+ if (!this.running) {
731
+ await this.releaseClaimedJob(job);
732
+ return;
733
+ }
734
+ const limiterToken = await this.limiter.tryAcquire();
735
+ if (limiterToken === null || limiterToken === undefined) {
736
+ await this.releaseClaimedJob(job);
737
+ await this.waitForWakeOrTimeout(await this.getLimiterWakeDelay());
738
+ continue;
739
+ }
740
+ if (!this.running) {
741
+ try {
742
+ await this.limiter.release(limiterToken);
743
+ } catch {}
744
+ await this.releaseClaimedJob(job);
745
+ return;
726
746
  }
747
+ this.processSingleJob(job, limiterToken);
727
748
  } catch {
728
749
  await sleep(this.pollIntervalMs);
729
750
  }
730
751
  }
731
752
  }
753
+ async getLimiterWakeDelay() {
754
+ try {
755
+ const next = await this.limiter.getNextAvailableTime();
756
+ const delay = next.getTime() - Date.now();
757
+ if (delay <= 0)
758
+ return this.pollIntervalMs;
759
+ return Math.min(Math.max(delay, this.pollIntervalMs), MAX_LIMITER_WAKE_MS);
760
+ } catch {
761
+ return this.pollIntervalMs;
762
+ }
763
+ }
732
764
  async getIdleDelay() {
733
765
  try {
734
- const pending = await this.storage.peek(JobStatus3.PENDING, 1);
766
+ const pending = await this.storage.peek(JobStatus.PENDING, 1);
735
767
  if (pending.length > 0 && pending[0].run_after) {
736
768
  const delay = new Date(pending[0].run_after).getTime() - Date.now();
737
769
  if (delay > 0) {
@@ -763,7 +795,7 @@ class JobQueueWorker {
763
795
  if (this.activeJobAbortControllers.size === 0) {
764
796
  return;
765
797
  }
766
- const abortingJobs = await this.storage.peek(JobStatus3.ABORTING);
798
+ const abortingJobs = await this.storage.peek(JobStatus.ABORTING);
767
799
  for (const jobData of abortingJobs) {
768
800
  const controller = this.activeJobAbortControllers.get(jobData.id);
769
801
  if (controller && !controller.signal.aborted) {
@@ -771,7 +803,7 @@ class JobQueueWorker {
771
803
  }
772
804
  }
773
805
  }
774
- async processSingleJob(job) {
806
+ async processSingleJob(job, limiterToken) {
775
807
  if (!job || !job.id) {
776
808
  throw new JobNotFoundError("Invalid job provided for processing");
777
809
  }
@@ -788,9 +820,17 @@ class JobQueueWorker {
788
820
  "workglow.job.max_retries": job.maxRetries
789
821
  }
790
822
  }) : undefined;
823
+ let slotReleased = false;
791
824
  try {
792
- await this.validateJobState(job);
793
- await this.limiter.recordJobStart();
825
+ try {
826
+ await this.validateJobState(job);
827
+ } catch (validationErr) {
828
+ try {
829
+ await this.limiter.release(limiterToken);
830
+ slotReleased = true;
831
+ } catch {}
832
+ throw validationErr;
833
+ }
794
834
  const abortController = this.createAbortController(job.id);
795
835
  this.events.emit("job_start", job.id);
796
836
  const output = await this.executeJob(job, abortController.signal);
@@ -828,7 +868,9 @@ class JobQueueWorker {
828
868
  } finally {
829
869
  span?.end();
830
870
  try {
831
- await this.limiter.recordJobCompletion();
871
+ if (!slotReleased) {
872
+ await this.limiter.recordJobCompletion();
873
+ }
832
874
  } finally {
833
875
  this.inFlight.delete(job.id);
834
876
  resolveInFlight();
@@ -849,7 +891,7 @@ class JobQueueWorker {
849
891
  }
850
892
  async completeJob(job, output) {
851
893
  try {
852
- job.status = JobStatus3.COMPLETED;
894
+ job.status = JobStatus.COMPLETED;
853
895
  job.progress = 100;
854
896
  job.progressMessage = "";
855
897
  job.progressDetails = null;
@@ -867,7 +909,7 @@ class JobQueueWorker {
867
909
  }
868
910
  async failJob(job, error) {
869
911
  try {
870
- job.status = JobStatus3.FAILED;
912
+ job.status = JobStatus.FAILED;
871
913
  job.progress = 100;
872
914
  job.completedAt = new Date;
873
915
  job.progressMessage = "";
@@ -884,7 +926,7 @@ class JobQueueWorker {
884
926
  }
885
927
  async disableJob(job) {
886
928
  try {
887
- job.status = JobStatus3.DISABLED;
929
+ job.status = JobStatus.DISABLED;
888
930
  job.progress = 100;
889
931
  job.completedAt = new Date;
890
932
  job.progressMessage = "";
@@ -906,7 +948,7 @@ class JobQueueWorker {
906
948
  }
907
949
  async rescheduleJob(job, retryDate) {
908
950
  try {
909
- job.status = JobStatus3.PENDING;
951
+ job.status = JobStatus.PENDING;
910
952
  const nextAvailableTime = await this.limiter.getNextAvailableTime();
911
953
  job.runAfter = retryDate instanceof Date ? retryDate : nextAvailableTime;
912
954
  job.progress = 0;
@@ -942,7 +984,7 @@ class JobQueueWorker {
942
984
  getLogger().error("handleAbort: job not found", { jobId });
943
985
  return;
944
986
  }
945
- if (job.status === JobStatus3.COMPLETED || job.status === JobStatus3.FAILED || job.status === JobStatus3.DISABLED) {
987
+ if (job.status === JobStatus.COMPLETED || job.status === JobStatus.FAILED || job.status === JobStatus.DISABLED) {
946
988
  return;
947
989
  }
948
990
  await this.failJob(job, new AbortSignalJobError("Job Aborted"));
@@ -954,19 +996,19 @@ class JobQueueWorker {
954
996
  return this.storageToClass(job);
955
997
  }
956
998
  async validateJobState(job) {
957
- if (job.status === JobStatus3.COMPLETED) {
999
+ if (job.status === JobStatus.COMPLETED) {
958
1000
  throw new PermanentJobError(`Job ${job.id} is already completed`);
959
1001
  }
960
- if (job.status === JobStatus3.FAILED) {
1002
+ if (job.status === JobStatus.FAILED) {
961
1003
  throw new PermanentJobError(`Job ${job.id} has failed`);
962
1004
  }
963
- if (job.status === JobStatus3.ABORTING || this.activeJobAbortControllers.get(job.id)?.signal.aborted) {
1005
+ if (job.status === JobStatus.ABORTING || this.activeJobAbortControllers.get(job.id)?.signal.aborted) {
964
1006
  throw new AbortSignalJobError(`Job ${job.id} is being aborted`);
965
1007
  }
966
1008
  if (job.deadlineAt && job.deadlineAt < new Date) {
967
1009
  throw new PermanentJobError(`Job ${job.id} has exceeded its deadline`);
968
1010
  }
969
- if (job.status === JobStatus3.DISABLED) {
1011
+ if (job.status === JobStatus.DISABLED) {
970
1012
  throw new JobDisabledError(`Job ${job.id} has been disabled`);
971
1013
  }
972
1014
  }
@@ -1038,10 +1080,17 @@ class JobQueueServer {
1038
1080
  }
1039
1081
  this.running = true;
1040
1082
  this.events.emit("server_start", this.queueName);
1083
+ if (this.limiter.scope === "process" && this.storage.scope === "cluster" && !(this.limiter instanceof NullLimiter)) {
1084
+ getLogger2().warn("Process-scoped limiter on cluster-scoped queue storage — limit is enforced per-process, not cluster-wide. Use a cluster-scoped rate limiter storage (Postgres/Supabase) for global enforcement.", {
1085
+ queueName: this.queueName,
1086
+ limiterScope: this.limiter.scope,
1087
+ storage: this.storage.constructor.name
1088
+ });
1089
+ }
1041
1090
  await this.fixupJobs();
1042
1091
  try {
1043
1092
  this.storageUnsubscribe = this.storage.subscribeToChanges((change) => {
1044
- if (change.type === "INSERT" || change.type === "UPDATE" && change.new?.status === JobStatus4.PENDING) {
1093
+ if (change.type === "INSERT" || change.type === "RESYNC" || change.type === "UPDATE" && change.new?.status === JobStatus.PENDING) {
1045
1094
  this.notifyWorkers();
1046
1095
  }
1047
1096
  });
@@ -1236,13 +1285,13 @@ class JobQueueServer {
1236
1285
  async cleanupJobs() {
1237
1286
  try {
1238
1287
  if (this.deleteAfterCompletionMs !== undefined && this.deleteAfterCompletionMs > 0) {
1239
- await this.storage.deleteJobsByStatusAndAge(JobStatus4.COMPLETED, this.deleteAfterCompletionMs);
1288
+ await this.storage.deleteJobsByStatusAndAge(JobStatus.COMPLETED, this.deleteAfterCompletionMs);
1240
1289
  }
1241
1290
  if (this.deleteAfterFailureMs !== undefined && this.deleteAfterFailureMs > 0) {
1242
- await this.storage.deleteJobsByStatusAndAge(JobStatus4.FAILED, this.deleteAfterFailureMs);
1291
+ await this.storage.deleteJobsByStatusAndAge(JobStatus.FAILED, this.deleteAfterFailureMs);
1243
1292
  }
1244
1293
  if (this.deleteAfterDisabledMs !== undefined && this.deleteAfterDisabledMs > 0) {
1245
- await this.storage.deleteJobsByStatusAndAge(JobStatus4.DISABLED, this.deleteAfterDisabledMs);
1294
+ await this.storage.deleteJobsByStatusAndAge(JobStatus.DISABLED, this.deleteAfterDisabledMs);
1246
1295
  }
1247
1296
  } catch (error) {
1248
1297
  console.error("Error in cleanup:", error);
@@ -1250,8 +1299,8 @@ class JobQueueServer {
1250
1299
  }
1251
1300
  async fixupJobs() {
1252
1301
  try {
1253
- const stuckProcessingJobs = await this.storage.peek(JobStatus4.PROCESSING);
1254
- const stuckAbortingJobs = await this.storage.peek(JobStatus4.ABORTING);
1302
+ const stuckProcessingJobs = await this.storage.peek(JobStatus.PROCESSING);
1303
+ const stuckAbortingJobs = await this.storage.peek(JobStatus.ABORTING);
1255
1304
  const stuckJobs = [...stuckProcessingJobs, ...stuckAbortingJobs];
1256
1305
  const currentWorkerIds = new Set(this.getWorkerIds());
1257
1306
  for (const jobData of stuckJobs) {
@@ -1260,12 +1309,12 @@ class JobQueueServer {
1260
1309
  }
1261
1310
  const job = this.storageToClass(jobData);
1262
1311
  if (job.runAttempts >= job.maxRetries) {
1263
- job.status = JobStatus4.FAILED;
1312
+ job.status = JobStatus.FAILED;
1264
1313
  job.error = "Max retries reached";
1265
1314
  job.errorCode = "MAX_RETRIES_REACHED";
1266
1315
  job.workerId = null;
1267
1316
  } else {
1268
- job.status = JobStatus4.PENDING;
1317
+ job.status = JobStatus.PENDING;
1269
1318
  job.runAfter = job.lastRanAt || new Date;
1270
1319
  job.progress = 0;
1271
1320
  job.progressMessage = "";
@@ -1296,6 +1345,9 @@ class CompositeLimiter {
1296
1345
  constructor(limiters = []) {
1297
1346
  this.limiters = limiters;
1298
1347
  }
1348
+ get scope() {
1349
+ return this.limiters.every((l) => l.scope === "cluster") && this.limiters.length > 0 ? "cluster" : "process";
1350
+ }
1299
1351
  addLimiter(limiter) {
1300
1352
  this.limiters.push(limiter);
1301
1353
  }
@@ -1307,6 +1359,27 @@ class CompositeLimiter {
1307
1359
  }
1308
1360
  return true;
1309
1361
  }
1362
+ async tryAcquire() {
1363
+ const tokens = [];
1364
+ for (const limiter of this.limiters) {
1365
+ const t = await limiter.tryAcquire();
1366
+ if (t === null || t === undefined) {
1367
+ for (let i = tokens.length - 1;i >= 0; i--) {
1368
+ try {
1369
+ await this.limiters[i].release(tokens[i]);
1370
+ } catch {}
1371
+ }
1372
+ return null;
1373
+ }
1374
+ tokens.push(t);
1375
+ }
1376
+ return tokens;
1377
+ }
1378
+ async release(token) {
1379
+ if (!Array.isArray(token))
1380
+ return;
1381
+ await Promise.all(this.limiters.map((l, i) => l.release(token[i]).catch(() => {})));
1382
+ }
1310
1383
  async recordJobStart() {
1311
1384
  await Promise.all(this.limiters.map((limiter) => limiter.recordJobStart()));
1312
1385
  }
@@ -1334,10 +1407,11 @@ class CompositeLimiter {
1334
1407
  }
1335
1408
 
1336
1409
  // src/limiter/ConcurrencyLimiter.ts
1337
- import { createServiceToken as createServiceToken2 } from "@workglow/util";
1338
- var CONCURRENT_JOB_LIMITER = createServiceToken2("jobqueue.limiter.concurrent");
1410
+ import { createServiceToken as createServiceToken3 } from "@workglow/util";
1411
+ var CONCURRENT_JOB_LIMITER = createServiceToken3("jobqueue.limiter.concurrent");
1339
1412
 
1340
1413
  class ConcurrencyLimiter {
1414
+ scope = "process";
1341
1415
  currentRunningJobs = 0;
1342
1416
  maxConcurrentJobs;
1343
1417
  nextAllowedStartTime = new Date;
@@ -1347,6 +1421,19 @@ class ConcurrencyLimiter {
1347
1421
  async canProceed() {
1348
1422
  return this.currentRunningJobs < this.maxConcurrentJobs && Date.now() >= this.nextAllowedStartTime.getTime();
1349
1423
  }
1424
+ static SENTINEL = Symbol("ConcurrencyLimiter.acquired");
1425
+ async tryAcquire() {
1426
+ if (this.currentRunningJobs >= this.maxConcurrentJobs || Date.now() < this.nextAllowedStartTime.getTime()) {
1427
+ return null;
1428
+ }
1429
+ this.currentRunningJobs++;
1430
+ return ConcurrencyLimiter.SENTINEL;
1431
+ }
1432
+ async release(token) {
1433
+ if (token !== ConcurrencyLimiter.SENTINEL)
1434
+ return;
1435
+ this.currentRunningJobs = Math.max(0, this.currentRunningJobs - 1);
1436
+ }
1350
1437
  async recordJobStart() {
1351
1438
  this.currentRunningJobs++;
1352
1439
  }
@@ -1370,13 +1457,32 @@ class ConcurrencyLimiter {
1370
1457
  // src/limiter/DelayLimiter.ts
1371
1458
  class DelayLimiter {
1372
1459
  delayInMilliseconds;
1460
+ scope = "process";
1373
1461
  nextAvailableTime = new Date;
1462
+ lastAcquireBaseline = 0;
1374
1463
  constructor(delayInMilliseconds = 50) {
1375
1464
  this.delayInMilliseconds = delayInMilliseconds;
1376
1465
  }
1377
1466
  async canProceed() {
1378
1467
  return Date.now() >= this.nextAvailableTime.getTime();
1379
1468
  }
1469
+ async tryAcquire() {
1470
+ const now = Date.now();
1471
+ if (now < this.nextAvailableTime.getTime()) {
1472
+ return null;
1473
+ }
1474
+ const previous = this.nextAvailableTime.getTime();
1475
+ this.lastAcquireBaseline = previous;
1476
+ this.nextAvailableTime = new Date(now + this.delayInMilliseconds);
1477
+ return previous;
1478
+ }
1479
+ async release(token) {
1480
+ if (typeof token !== "number")
1481
+ return;
1482
+ if (this.nextAvailableTime.getTime() === this.lastAcquireBaseline + this.delayInMilliseconds) {
1483
+ this.nextAvailableTime = new Date(token);
1484
+ }
1485
+ }
1380
1486
  async recordJobStart() {
1381
1487
  this.nextAvailableTime = new Date(Date.now() + this.delayInMilliseconds);
1382
1488
  }
@@ -1395,16 +1501,18 @@ class DelayLimiter {
1395
1501
  }
1396
1502
 
1397
1503
  // src/limiter/EvenlySpacedRateLimiter.ts
1398
- import { createServiceToken as createServiceToken3 } from "@workglow/util";
1399
- var EVENLY_SPACED_JOB_RATE_LIMITER = createServiceToken3("jobqueue.limiter.rate.evenlyspaced");
1504
+ import { createServiceToken as createServiceToken4 } from "@workglow/util";
1505
+ var EVENLY_SPACED_JOB_RATE_LIMITER = createServiceToken4("jobqueue.limiter.rate.evenlyspaced");
1400
1506
 
1401
1507
  class EvenlySpacedRateLimiter {
1508
+ scope = "process";
1402
1509
  maxExecutions;
1403
1510
  windowSizeMs;
1404
1511
  idealInterval;
1405
1512
  nextAvailableTime = Date.now();
1406
1513
  lastStartTime = 0;
1407
1514
  durations = [];
1515
+ acquireChain = Promise.resolve();
1408
1516
  constructor({ maxExecutions, windowSizeInSeconds }) {
1409
1517
  if (maxExecutions <= 0) {
1410
1518
  throw new Error("maxExecutions must be > 0");
@@ -1420,6 +1528,43 @@ class EvenlySpacedRateLimiter {
1420
1528
  const now = Date.now();
1421
1529
  return now >= this.nextAvailableTime;
1422
1530
  }
1531
+ async tryAcquire() {
1532
+ const previous = this.acquireChain;
1533
+ let release;
1534
+ const next = new Promise((r) => {
1535
+ release = r;
1536
+ });
1537
+ this.acquireChain = next;
1538
+ try {
1539
+ await previous;
1540
+ const now = Date.now();
1541
+ if (now < this.nextAvailableTime) {
1542
+ return null;
1543
+ }
1544
+ const priorNextAvailable = this.nextAvailableTime;
1545
+ this.lastStartTime = now;
1546
+ if (this.durations.length === 0) {
1547
+ this.nextAvailableTime = now + this.idealInterval;
1548
+ } else {
1549
+ const sum = this.durations.reduce((a, b) => a + b, 0);
1550
+ const avgDuration = sum / this.durations.length;
1551
+ const waitMs = Math.max(0, this.idealInterval - avgDuration);
1552
+ this.nextAvailableTime = now + waitMs;
1553
+ }
1554
+ return { priorNextAvailable, advancedTo: this.nextAvailableTime };
1555
+ } finally {
1556
+ release(undefined);
1557
+ }
1558
+ }
1559
+ async release(token) {
1560
+ if (!token || typeof token !== "object" || typeof token.advancedTo !== "number") {
1561
+ return;
1562
+ }
1563
+ const t = token;
1564
+ if (this.nextAvailableTime === t.advancedTo) {
1565
+ this.nextAvailableTime = t.priorNextAvailable;
1566
+ }
1567
+ }
1423
1568
  async recordJobStart() {
1424
1569
  const now = Date.now();
1425
1570
  this.lastStartTime = now;
@@ -1457,8 +1602,8 @@ class EvenlySpacedRateLimiter {
1457
1602
  }
1458
1603
 
1459
1604
  // src/limiter/ILimiter.ts
1460
- import { createServiceToken as createServiceToken4 } from "@workglow/util";
1461
- var JOB_LIMITER = createServiceToken4("jobqueue.limiter");
1605
+ import { createServiceToken as createServiceToken5 } from "@workglow/util";
1606
+ var JOB_LIMITER = createServiceToken5("jobqueue.limiter");
1462
1607
 
1463
1608
  // src/limiter/RateLimiter.ts
1464
1609
  class RateLimiter {
@@ -1470,6 +1615,7 @@ class RateLimiter {
1470
1615
  initialBackoffDelay;
1471
1616
  backoffMultiplier;
1472
1617
  maxBackoffDelay;
1618
+ localBackoffUntilMs = 0;
1473
1619
  constructor(storage, queueName, {
1474
1620
  maxExecutions,
1475
1621
  windowSizeInSeconds,
@@ -1501,6 +1647,27 @@ class RateLimiter {
1501
1647
  this.maxBackoffDelay = maxBackoffDelay;
1502
1648
  this.currentBackoffDelay = initialBackoffDelay;
1503
1649
  }
1650
+ get scope() {
1651
+ return this.storage.scope;
1652
+ }
1653
+ async tryAcquire() {
1654
+ const token = await this.storage.tryReserveExecution(this.queueName, this.maxExecutions, this.windowSizeInMilliseconds);
1655
+ if (token !== null && token !== undefined) {
1656
+ this.currentBackoffDelay = this.initialBackoffDelay;
1657
+ this.localBackoffUntilMs = 0;
1658
+ return token;
1659
+ }
1660
+ this.localBackoffUntilMs = Date.now() + this.addJitter(this.currentBackoffDelay);
1661
+ this.increaseBackoff();
1662
+ return null;
1663
+ }
1664
+ async release(token) {
1665
+ if (token === null || token === undefined)
1666
+ return;
1667
+ await this.storage.releaseExecution(this.queueName, token);
1668
+ this.currentBackoffDelay = this.initialBackoffDelay;
1669
+ this.localBackoffUntilMs = 0;
1670
+ }
1504
1671
  addJitter(base) {
1505
1672
  return base + Math.random() * base;
1506
1673
  }
@@ -1546,17 +1713,18 @@ class RateLimiter {
1546
1713
  async recordJobCompletion() {}
1547
1714
  async getNextAvailableTime() {
1548
1715
  const oldestExecution = await this.storage.getOldestExecutionAtOffset(this.queueName, this.maxExecutions - 1);
1549
- let rateLimitedTime = new Date;
1716
+ let latestMs = Date.now();
1550
1717
  if (oldestExecution) {
1551
- rateLimitedTime = new Date(oldestExecution);
1552
- rateLimitedTime.setSeconds(rateLimitedTime.getSeconds() + this.windowSizeInMilliseconds / 1000);
1718
+ latestMs = Math.max(latestMs, new Date(oldestExecution).getTime() + this.windowSizeInMilliseconds);
1553
1719
  }
1554
1720
  const nextAvailableStr = await this.storage.getNextAvailableTime(this.queueName);
1555
- let nextAvailableTime = new Date;
1556
1721
  if (nextAvailableStr) {
1557
- nextAvailableTime = new Date(nextAvailableStr);
1722
+ latestMs = Math.max(latestMs, new Date(nextAvailableStr).getTime());
1723
+ }
1724
+ if (this.localBackoffUntilMs > latestMs) {
1725
+ latestMs = this.localBackoffUntilMs;
1558
1726
  }
1559
- return nextAvailableTime > rateLimitedTime ? nextAvailableTime : rateLimitedTime;
1727
+ return new Date(latestMs);
1560
1728
  }
1561
1729
  async setNextAvailableTime(date) {
1562
1730
  await this.storage.setNextAvailableTime(this.queueName, date.toISOString());
@@ -1564,6 +1732,408 @@ class RateLimiter {
1564
1732
  async clear() {
1565
1733
  await this.storage.clear(this.queueName);
1566
1734
  this.currentBackoffDelay = this.initialBackoffDelay;
1735
+ this.localBackoffUntilMs = 0;
1736
+ }
1737
+ }
1738
+
1739
+ // src/queue-storage/InMemoryQueueStorage.ts
1740
+ import {
1741
+ createServiceToken as createServiceToken6,
1742
+ EventEmitter as EventEmitter4,
1743
+ getLogger as getLogger3,
1744
+ makeFingerprint,
1745
+ sleep as sleep2,
1746
+ uuid4 as uuid42
1747
+ } from "@workglow/util";
1748
+ var IN_MEMORY_QUEUE_STORAGE = createServiceToken6("jobqueue.storage.inMemory");
1749
+
1750
+ class InMemoryQueueStorage {
1751
+ queueName;
1752
+ scope = "process";
1753
+ prefixValues;
1754
+ events = new EventEmitter4;
1755
+ constructor(queueName, options) {
1756
+ this.queueName = queueName;
1757
+ this.jobQueue = [];
1758
+ this.prefixValues = options?.prefixValues ?? {};
1759
+ }
1760
+ jobQueue;
1761
+ matchesPrefixes(job) {
1762
+ for (const [key, value] of Object.entries(this.prefixValues)) {
1763
+ if (job[key] !== value) {
1764
+ return false;
1765
+ }
1766
+ }
1767
+ return true;
1768
+ }
1769
+ pendingQueue() {
1770
+ const now = new Date().toISOString();
1771
+ return this.jobQueue.filter((job) => this.matchesPrefixes(job)).filter((job) => job.status === JobStatus.PENDING).filter((job) => !job.run_after || job.run_after <= now).sort((a, b) => (a.run_after || "").localeCompare(b.run_after || ""));
1772
+ }
1773
+ async add(job) {
1774
+ await sleep2(0);
1775
+ const now = new Date().toISOString();
1776
+ const jobWithPrefixes = job;
1777
+ jobWithPrefixes.id = jobWithPrefixes.id ?? uuid42();
1778
+ jobWithPrefixes.job_run_id = jobWithPrefixes.job_run_id ?? uuid42();
1779
+ jobWithPrefixes.queue = this.queueName;
1780
+ jobWithPrefixes.fingerprint = await makeFingerprint(jobWithPrefixes.input);
1781
+ jobWithPrefixes.status = JobStatus.PENDING;
1782
+ jobWithPrefixes.progress = 0;
1783
+ jobWithPrefixes.progress_message = "";
1784
+ jobWithPrefixes.progress_details = null;
1785
+ jobWithPrefixes.created_at = now;
1786
+ jobWithPrefixes.run_after = now;
1787
+ for (const [key, value] of Object.entries(this.prefixValues)) {
1788
+ jobWithPrefixes[key] = value;
1789
+ }
1790
+ this.jobQueue.push(jobWithPrefixes);
1791
+ this.events.emit("change", { type: "INSERT", new: jobWithPrefixes });
1792
+ return jobWithPrefixes.id;
1793
+ }
1794
+ async get(id) {
1795
+ await sleep2(0);
1796
+ const job = this.jobQueue.find((j) => j.id === id);
1797
+ if (job && this.matchesPrefixes(job)) {
1798
+ return job;
1799
+ }
1800
+ return;
1801
+ }
1802
+ async peek(status = JobStatus.PENDING, num = 100) {
1803
+ await sleep2(0);
1804
+ num = Number(num) || 100;
1805
+ return this.jobQueue.filter((j) => this.matchesPrefixes(j)).sort((a, b) => (a.run_after || "").localeCompare(b.run_after || "")).filter((j) => j.status === status).slice(0, num);
1806
+ }
1807
+ async next(workerId) {
1808
+ await sleep2(0);
1809
+ const top = this.pendingQueue();
1810
+ const job = top[0];
1811
+ if (job) {
1812
+ const oldJob = { ...job };
1813
+ job.status = JobStatus.PROCESSING;
1814
+ job.last_ran_at = new Date().toISOString();
1815
+ job.worker_id = workerId;
1816
+ this.events.emit("change", { type: "UPDATE", old: oldJob, new: job });
1817
+ return job;
1818
+ }
1819
+ }
1820
+ async size(status = JobStatus.PENDING) {
1821
+ await sleep2(0);
1822
+ return this.jobQueue.filter((j) => this.matchesPrefixes(j) && j.status === status).length;
1823
+ }
1824
+ async saveProgress(id, progress, message, details) {
1825
+ await sleep2(0);
1826
+ const job = this.jobQueue.find((j) => j.id === id && this.matchesPrefixes(j));
1827
+ if (!job) {
1828
+ const jobWithAnyPrefix = this.jobQueue.find((j) => j.id === id);
1829
+ getLogger3().warn("Job not found for progress update", {
1830
+ id,
1831
+ reason: jobWithAnyPrefix ? "prefix_mismatch" : "missing",
1832
+ existingStatus: jobWithAnyPrefix?.status,
1833
+ queueName: this.queueName,
1834
+ prefixValues: this.prefixValues
1835
+ });
1836
+ return;
1837
+ }
1838
+ if (job.status === JobStatus.COMPLETED || job.status === JobStatus.FAILED) {
1839
+ getLogger3().warn("Job already completed or failed for progress update", {
1840
+ id,
1841
+ status: job.status,
1842
+ completedAt: job.completed_at,
1843
+ error: job.error
1844
+ });
1845
+ return;
1846
+ }
1847
+ const oldJob = { ...job };
1848
+ job.progress = progress;
1849
+ job.progress_message = message;
1850
+ job.progress_details = details;
1851
+ this.events.emit("change", { type: "UPDATE", old: oldJob, new: job });
1852
+ }
1853
+ async complete(job) {
1854
+ await sleep2(0);
1855
+ const jobWithPrefixes = job;
1856
+ const index = this.jobQueue.findIndex((j) => j.id === job.id && this.matchesPrefixes(j));
1857
+ if (index !== -1) {
1858
+ const existing = this.jobQueue[index];
1859
+ const currentAttempts = existing?.run_attempts ?? 0;
1860
+ jobWithPrefixes.run_attempts = currentAttempts + 1;
1861
+ for (const [key, value] of Object.entries(this.prefixValues)) {
1862
+ jobWithPrefixes[key] = value;
1863
+ }
1864
+ this.jobQueue[index] = jobWithPrefixes;
1865
+ this.events.emit("change", { type: "UPDATE", old: existing, new: jobWithPrefixes });
1866
+ }
1867
+ }
1868
+ async release(id) {
1869
+ await sleep2(0);
1870
+ const job = this.jobQueue.find((j) => j.id === id && this.matchesPrefixes(j));
1871
+ if (job) {
1872
+ const oldJob = { ...job };
1873
+ job.status = JobStatus.PENDING;
1874
+ job.worker_id = null;
1875
+ job.progress = 0;
1876
+ job.progress_message = "";
1877
+ job.progress_details = null;
1878
+ this.events.emit("change", { type: "UPDATE", old: oldJob, new: job });
1879
+ }
1880
+ }
1881
+ async abort(id) {
1882
+ await sleep2(0);
1883
+ const job = this.jobQueue.find((j) => j.id === id && this.matchesPrefixes(j));
1884
+ if (job) {
1885
+ const oldJob = { ...job };
1886
+ job.status = JobStatus.ABORTING;
1887
+ this.events.emit("change", { type: "UPDATE", old: oldJob, new: job });
1888
+ }
1889
+ }
1890
+ async getByRunId(runId) {
1891
+ await sleep2(0);
1892
+ return this.jobQueue.filter((job) => this.matchesPrefixes(job) && job.job_run_id === runId);
1893
+ }
1894
+ async deleteAll() {
1895
+ await sleep2(0);
1896
+ const deletedJobs = this.jobQueue.filter((job) => this.matchesPrefixes(job));
1897
+ this.jobQueue = this.jobQueue.filter((job) => !this.matchesPrefixes(job));
1898
+ for (const job of deletedJobs) {
1899
+ this.events.emit("change", { type: "DELETE", old: job });
1900
+ }
1901
+ }
1902
+ async outputForInput(input) {
1903
+ await sleep2(0);
1904
+ const fingerprint = await makeFingerprint(input);
1905
+ return this.jobQueue.find((j) => this.matchesPrefixes(j) && j.fingerprint === fingerprint && j.status === JobStatus.COMPLETED)?.output ?? null;
1906
+ }
1907
+ async delete(id) {
1908
+ await sleep2(0);
1909
+ const deletedJob = this.jobQueue.find((job) => job.id === id && this.matchesPrefixes(job));
1910
+ this.jobQueue = this.jobQueue.filter((job) => !(job.id === id && this.matchesPrefixes(job)));
1911
+ if (deletedJob) {
1912
+ this.events.emit("change", { type: "DELETE", old: deletedJob });
1913
+ }
1914
+ }
1915
+ async deleteJobsByStatusAndAge(status, olderThanMs) {
1916
+ await sleep2(0);
1917
+ const cutoffDate = new Date(Date.now() - olderThanMs).toISOString();
1918
+ const deletedJobs = this.jobQueue.filter((job) => this.matchesPrefixes(job) && job.status === status && job.completed_at && job.completed_at <= cutoffDate);
1919
+ this.jobQueue = this.jobQueue.filter((job) => !this.matchesPrefixes(job) || job.status !== status || !job.completed_at || job.completed_at > cutoffDate);
1920
+ for (const job of deletedJobs) {
1921
+ this.events.emit("change", { type: "DELETE", old: job });
1922
+ }
1923
+ }
1924
+ async setupDatabase() {}
1925
+ matchesPrefixFilter(job, prefixFilter) {
1926
+ if (prefixFilter && Object.keys(prefixFilter).length === 0) {
1927
+ return true;
1928
+ }
1929
+ const filterValues = prefixFilter ?? this.prefixValues;
1930
+ if (Object.keys(filterValues).length === 0) {
1931
+ return true;
1932
+ }
1933
+ const jobWithPrefixes = job;
1934
+ for (const [key, value] of Object.entries(filterValues)) {
1935
+ if (jobWithPrefixes[key] !== value) {
1936
+ return false;
1937
+ }
1938
+ }
1939
+ return true;
1940
+ }
1941
+ subscribeToChanges(callback, options) {
1942
+ const prefixFilter = options?.prefixFilter;
1943
+ const filteredCallback = (change) => {
1944
+ const newMatches = change.new ? this.matchesPrefixFilter(change.new, prefixFilter) : false;
1945
+ const oldMatches = change.old ? this.matchesPrefixFilter(change.old, prefixFilter) : false;
1946
+ if (!newMatches && !oldMatches) {
1947
+ return;
1948
+ }
1949
+ callback(change);
1950
+ };
1951
+ return this.events.subscribe("change", filteredCallback);
1952
+ }
1953
+ }
1954
+
1955
+ // src/queue-storage/TelemetryQueueStorage.ts
1956
+ import { traced } from "@workglow/util";
1957
+
1958
+ class TelemetryQueueStorage {
1959
+ storageName;
1960
+ inner;
1961
+ constructor(storageName, inner) {
1962
+ this.storageName = storageName;
1963
+ this.inner = inner;
1964
+ }
1965
+ get scope() {
1966
+ return this.inner.scope;
1967
+ }
1968
+ add(job) {
1969
+ return traced("workglow.storage.queue.add", this.storageName, () => this.inner.add(job));
1970
+ }
1971
+ get(id) {
1972
+ return traced("workglow.storage.queue.get", this.storageName, () => this.inner.get(id));
1973
+ }
1974
+ next(workerId) {
1975
+ return traced("workglow.storage.queue.next", this.storageName, () => this.inner.next(workerId));
1976
+ }
1977
+ peek(status, num) {
1978
+ return traced("workglow.storage.queue.peek", this.storageName, () => this.inner.peek(status, num));
1979
+ }
1980
+ size(status) {
1981
+ return traced("workglow.storage.queue.size", this.storageName, () => this.inner.size(status));
1982
+ }
1983
+ complete(job) {
1984
+ return traced("workglow.storage.queue.complete", this.storageName, () => this.inner.complete(job));
1985
+ }
1986
+ release(id) {
1987
+ return traced("workglow.storage.queue.release", this.storageName, () => this.inner.release(id));
1988
+ }
1989
+ deleteAll() {
1990
+ return traced("workglow.storage.queue.deleteAll", this.storageName, () => this.inner.deleteAll());
1991
+ }
1992
+ outputForInput(input) {
1993
+ return traced("workglow.storage.queue.outputForInput", this.storageName, () => this.inner.outputForInput(input));
1994
+ }
1995
+ abort(id) {
1996
+ return traced("workglow.storage.queue.abort", this.storageName, () => this.inner.abort(id));
1997
+ }
1998
+ getByRunId(runId) {
1999
+ return traced("workglow.storage.queue.getByRunId", this.storageName, () => this.inner.getByRunId(runId));
2000
+ }
2001
+ saveProgress(id, progress, message, details) {
2002
+ return traced("workglow.storage.queue.saveProgress", this.storageName, () => this.inner.saveProgress(id, progress, message, details));
2003
+ }
2004
+ delete(id) {
2005
+ return traced("workglow.storage.queue.delete", this.storageName, () => this.inner.delete(id));
2006
+ }
2007
+ deleteJobsByStatusAndAge(status, olderThanMs) {
2008
+ return traced("workglow.storage.queue.deleteJobsByStatusAndAge", this.storageName, () => this.inner.deleteJobsByStatusAndAge(status, olderThanMs));
2009
+ }
2010
+ setupDatabase() {
2011
+ return this.inner.setupDatabase();
2012
+ }
2013
+ subscribeToChanges(callback, options) {
2014
+ return this.inner.subscribeToChanges(callback, options);
2015
+ }
2016
+ }
2017
+
2018
+ // src/rate-limiter-storage/IRateLimiterStorage.ts
2019
+ import { createServiceToken as createServiceToken7 } from "@workglow/util";
2020
+ var RATE_LIMITER_STORAGE = createServiceToken7("ratelimiter.storage");
2021
+
2022
+ // src/rate-limiter-storage/InMemoryRateLimiterStorage.ts
2023
+ import { createServiceToken as createServiceToken8, sleep as sleep3, uuid4 as uuid43 } from "@workglow/util";
2024
+ var IN_MEMORY_RATE_LIMITER_STORAGE = createServiceToken8("ratelimiter.storage.inMemory");
2025
+
2026
+ class InMemoryRateLimiterStorage {
2027
+ scope = "process";
2028
+ prefixValues;
2029
+ executions = new Map;
2030
+ nextAvailableTimes = new Map;
2031
+ reserveChains = new Map;
2032
+ constructor(options) {
2033
+ this.prefixValues = options?.prefixValues ?? {};
2034
+ }
2035
+ makeKey(queueName) {
2036
+ const prefixPart = Object.entries(this.prefixValues).sort(([a], [b]) => a.localeCompare(b)).map(([k, v]) => `${k}:${v}`).join("|");
2037
+ return prefixPart ? `${prefixPart}|${queueName}` : queueName;
2038
+ }
2039
+ async setupDatabase() {}
2040
+ async withKeyLock(key, fn) {
2041
+ const previous = this.reserveChains.get(key) ?? Promise.resolve();
2042
+ let release;
2043
+ const next = new Promise((resolve) => {
2044
+ release = resolve;
2045
+ });
2046
+ this.reserveChains.set(key, next);
2047
+ try {
2048
+ await previous;
2049
+ return await fn();
2050
+ } finally {
2051
+ release(undefined);
2052
+ if (this.reserveChains.get(key) === next) {
2053
+ this.reserveChains.delete(key);
2054
+ }
2055
+ }
2056
+ }
2057
+ async tryReserveExecution(queueName, maxExecutions, windowMs) {
2058
+ const key = this.makeKey(queueName);
2059
+ return this.withKeyLock(key, () => {
2060
+ const now = Date.now();
2061
+ const windowStart = new Date(now - windowMs);
2062
+ const executions = this.executions.get(key) ?? [];
2063
+ const live = executions.filter((e) => e.executedAt > windowStart);
2064
+ if (live.length >= maxExecutions) {
2065
+ if (live.length !== executions.length) {
2066
+ this.executions.set(key, live);
2067
+ }
2068
+ return null;
2069
+ }
2070
+ const next = this.nextAvailableTimes.get(key);
2071
+ if (next && next.getTime() > now) {
2072
+ return null;
2073
+ }
2074
+ const id = uuid43();
2075
+ live.push({ id, queueName, executedAt: new Date(now) });
2076
+ this.executions.set(key, live);
2077
+ return id;
2078
+ });
2079
+ }
2080
+ async releaseExecution(queueName, token) {
2081
+ if (token === null || token === undefined)
2082
+ return;
2083
+ const key = this.makeKey(queueName);
2084
+ await this.withKeyLock(key, () => {
2085
+ const executions = this.executions.get(key);
2086
+ if (!executions || executions.length === 0)
2087
+ return;
2088
+ const idx = executions.findIndex((e) => e.id === token);
2089
+ if (idx === -1)
2090
+ return;
2091
+ executions.splice(idx, 1);
2092
+ this.executions.set(key, executions);
2093
+ });
2094
+ }
2095
+ async recordExecution(queueName) {
2096
+ await sleep3(0);
2097
+ const key = this.makeKey(queueName);
2098
+ const executions = this.executions.get(key) ?? [];
2099
+ executions.push({
2100
+ id: uuid43(),
2101
+ queueName,
2102
+ executedAt: new Date
2103
+ });
2104
+ this.executions.set(key, executions);
2105
+ }
2106
+ async getExecutionCount(queueName, windowStartTime) {
2107
+ await sleep3(0);
2108
+ const key = this.makeKey(queueName);
2109
+ const executions = this.executions.get(key) ?? [];
2110
+ const windowStart = new Date(windowStartTime);
2111
+ return executions.filter((e) => e.executedAt > windowStart).length;
2112
+ }
2113
+ async getOldestExecutionAtOffset(queueName, offset) {
2114
+ await sleep3(0);
2115
+ const key = this.makeKey(queueName);
2116
+ const executions = this.executions.get(key) ?? [];
2117
+ const sorted = [...executions].sort((a, b) => a.executedAt.getTime() - b.executedAt.getTime());
2118
+ const execution = sorted[offset];
2119
+ return execution?.executedAt.toISOString();
2120
+ }
2121
+ async getNextAvailableTime(queueName) {
2122
+ await sleep3(0);
2123
+ const key = this.makeKey(queueName);
2124
+ const time = this.nextAvailableTimes.get(key);
2125
+ return time?.toISOString();
2126
+ }
2127
+ async setNextAvailableTime(queueName, nextAvailableAt) {
2128
+ await sleep3(0);
2129
+ const key = this.makeKey(queueName);
2130
+ this.nextAvailableTimes.set(key, new Date(nextAvailableAt));
2131
+ }
2132
+ async clear(queueName) {
2133
+ await sleep3(0);
2134
+ const key = this.makeKey(queueName);
2135
+ this.executions.delete(key);
2136
+ this.nextAvailableTimes.delete(key);
1567
2137
  }
1568
2138
  }
1569
2139
  export {
@@ -1572,8 +2142,11 @@ export {
1572
2142
  formatErrorChainForDiagnostics,
1573
2143
  classToStorage,
1574
2144
  applyPersistedDiagnosticsToStack,
2145
+ TelemetryQueueStorage,
1575
2146
  RetryableJobError,
1576
2147
  RateLimiter,
2148
+ RATE_LIMITER_STORAGE,
2149
+ QUEUE_STORAGE,
1577
2150
  PermanentJobError,
1578
2151
  NullLimiter,
1579
2152
  NULL_JOB_LIMITER,
@@ -1587,6 +2160,10 @@ export {
1587
2160
  Job,
1588
2161
  JOB_LIMITER,
1589
2162
  JOB_ERROR_DIAGNOSTICS_MARKER,
2163
+ InMemoryRateLimiterStorage,
2164
+ InMemoryQueueStorage,
2165
+ IN_MEMORY_RATE_LIMITER_STORAGE,
2166
+ IN_MEMORY_QUEUE_STORAGE,
1590
2167
  EvenlySpacedRateLimiter,
1591
2168
  EVENLY_SPACED_JOB_RATE_LIMITER,
1592
2169
  DelayLimiter,
@@ -1596,4 +2173,4 @@ export {
1596
2173
  AbortSignalJobError
1597
2174
  };
1598
2175
 
1599
- //# debugId=DEA35BFCE014423364756E2164756E21
2176
+ //# debugId=E30CFA4D7AC3B9B064756E2164756E21