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