@trigger.dev/redis-worker 4.7.3 → 4.8.0-webhooks.20261015065339

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/index.js CHANGED
@@ -9634,6 +9634,39 @@ function createRedisClient(options, handlers) {
9634
9634
  });
9635
9635
  return client;
9636
9636
  }
9637
+ /**
9638
+ * Cluster-mode client. `defaultOptions` go on the INNER per-node options, so a role swap gets the
9639
+ * same reconnect-and-retry treatment a single-node client already gets.
9640
+ */
9641
+ function createRedisClusterClient(options, handlers) {
9642
+ const client = new import_built.Redis.Cluster(options.nodes, {
9643
+ ...options.failFast && { enableOfflineQueue: false },
9644
+ ...options.clusterOptions,
9645
+ redisOptions: {
9646
+ ...defaultOptions,
9647
+ ...options.failFast && { enableOfflineQueue: false },
9648
+ ...options.redisOptions
9649
+ }
9650
+ });
9651
+ if (process.env.VITEST) {
9652
+ client.on("error", () => {});
9653
+ return client;
9654
+ }
9655
+ client.on("error", (error) => {
9656
+ if (handlers?.onError) handlers.onError(error);
9657
+ else logger.error(`Redis cluster client error:`, {
9658
+ error,
9659
+ keyPrefix: options.redisOptions?.keyPrefix
9660
+ });
9661
+ });
9662
+ client.on("node error", (error, node) => {
9663
+ logger.warn(`Redis cluster node error:`, {
9664
+ node,
9665
+ error: error.message
9666
+ });
9667
+ });
9668
+ return client;
9669
+ }
9637
9670
 
9638
9671
  //#endregion
9639
9672
  //#region ../../node_modules/.pnpm/nanoid@5.1.16/node_modules/nanoid/url-alphabet/index.js
@@ -9719,6 +9752,23 @@ var SimpleQueue = class {
9719
9752
  throw e;
9720
9753
  }
9721
9754
  }
9755
+ /**
9756
+ * Re-enqueues an item for a retry, unless since it was dequeued a newer enqueue replaced it (its
9757
+ * stored deduplication key no longer matches) or it was removed by an ack or cancel. Returns
9758
+ * false when it skipped, so a failed attempt never overwrites the newer item or revives a removed
9759
+ * one. A newer enqueue starts its own retry budget: an item a handler re-enqueues before failing
9760
+ * restarts at attempt 0 rather than moving towards the DLQ.
9761
+ */
9762
+ async requeue({ id, job, item, attempt, availableAt, visibilityTimeoutMs, deduplicationKey }) {
9763
+ const serializedItem = JSON.stringify({
9764
+ job,
9765
+ item,
9766
+ visibilityTimeoutMs,
9767
+ attempt,
9768
+ deduplicationKey: nanoid()
9769
+ });
9770
+ return await this.redis.requeueItem(`queue`, `items`, id, availableAt.getTime(), serializedItem, deduplicationKey ?? "") === 1;
9771
+ }
9722
9772
  async enqueueOnce({ id, job, item, attempt, availableAt, visibilityTimeoutMs }) {
9723
9773
  if (!id) throw new Error("enqueueOnce requires an id");
9724
9774
  try {
@@ -9886,14 +9936,21 @@ var SimpleQueue = class {
9886
9936
  deduplicationKey: item.deduplicationKey ?? void 0
9887
9937
  };
9888
9938
  }
9889
- async moveToDeadLetterQueue(id, errorMessage) {
9939
+ /**
9940
+ * Moves an item to the dead letter queue. With a deduplication key, an item a newer enqueue has
9941
+ * replaced is left in the queue and false is returned.
9942
+ */
9943
+ async moveToDeadLetterQueue(id, errorMessage, deduplicationKey) {
9890
9944
  try {
9891
9945
  this.logger.debug(`SimpleQueue ${this.name}.moveToDeadLetterQueue(): moving item to DLQ`, {
9892
9946
  queue: this.name,
9893
9947
  id,
9894
9948
  errorMessage
9895
9949
  });
9896
- if (await this.redis.moveToDeadLetterQueue(`queue`, `items`, `dlq`, `dlq:items`, id, errorMessage) !== 1) throw new Error("Move to Dead Letter Queue operation failed");
9950
+ const result = await this.redis.moveToDeadLetterQueue(`queue`, `items`, `dlq`, `dlq:items`, id, errorMessage, deduplicationKey ?? "");
9951
+ if (result === 2) return false;
9952
+ if (result !== 1) throw new Error("Move to Dead Letter Queue operation failed");
9953
+ return true;
9897
9954
  } catch (e) {
9898
9955
  this.logger.error(`SimpleQueue ${this.name}.moveToDeadLetterQueue(): error moving item to DLQ`, {
9899
9956
  queue: this.name,
@@ -10079,6 +10136,35 @@ var SimpleQueue = class {
10079
10136
  -- Remove from sorted set and hash
10080
10137
  redis.call('ZREM', queueKey, id)
10081
10138
  redis.call('HDEL', itemsKey, id)
10139
+ return 1
10140
+ `
10141
+ });
10142
+ this.redis.defineCommand("requeueItem", {
10143
+ numberOfKeys: 2,
10144
+ lua: `
10145
+ local queue = KEYS[1]
10146
+ local items = KEYS[2]
10147
+ local id = ARGV[1]
10148
+ local score = ARGV[2]
10149
+ local serializedItem = ARGV[3]
10150
+ local deduplicationKey = ARGV[4]
10151
+
10152
+ -- Leave the item alone if, while this attempt ran, it was removed (acked or cancelled)
10153
+ -- or a newer enqueue replaced it.
10154
+ if deduplicationKey ~= "" then
10155
+ local current = redis.call('HGET', items, id)
10156
+ if not current then
10157
+ return 2
10158
+ end
10159
+ local success, parsed = pcall(cjson.decode, current)
10160
+ if success and parsed.deduplicationKey and parsed.deduplicationKey ~= deduplicationKey then
10161
+ return 2
10162
+ end
10163
+ end
10164
+
10165
+ redis.call('ZADD', queue, score, id)
10166
+ redis.call('HSET', items, id, serializedItem)
10167
+
10082
10168
  return 1
10083
10169
  `
10084
10170
  });
@@ -10091,6 +10177,7 @@ var SimpleQueue = class {
10091
10177
  local dlqItems = KEYS[4]
10092
10178
  local id = ARGV[1]
10093
10179
  local errorMessage = ARGV[2]
10180
+ local deduplicationKey = ARGV[3]
10094
10181
 
10095
10182
  local item = redis.call('HGET', items, id)
10096
10183
  if not item then
@@ -10098,6 +10185,9 @@ var SimpleQueue = class {
10098
10185
  end
10099
10186
 
10100
10187
  local parsedItem = cjson.decode(item)
10188
+ if deduplicationKey ~= "" and parsedItem.deduplicationKey and parsedItem.deduplicationKey ~= deduplicationKey then
10189
+ return 2
10190
+ end
10101
10191
  parsedItem.errorMessage = errorMessage
10102
10192
 
10103
10193
  local time = redis.call('TIME')
@@ -11821,7 +11911,7 @@ var Worker = class Worker {
11821
11911
  else if (errorLogLevel === "warn") this.logger.warn(`Worker batch item reached max attempts. Moving to DLQ.`, dlqLogAttributes);
11822
11912
  else if (errorLogLevel === "info") this.logger.info(`Worker batch item reached max attempts. Moving to DLQ.`, dlqLogAttributes);
11823
11913
  else this.logger.error(`Worker batch item reached max attempts. Moving to DLQ.`, dlqLogAttributes);
11824
- await this.queue.moveToDeadLetterQueue(item.id, errorMessage);
11914
+ if (!await this.queue.moveToDeadLetterQueue(item.id, errorMessage, item.deduplicationKey)) this.#logSuperseded("dead-letter", item.id, jobType);
11825
11915
  continue;
11826
11916
  }
11827
11917
  const retryDate = new Date(Date.now() + retryDelay);
@@ -11833,14 +11923,15 @@ var Worker = class Worker {
11833
11923
  retryDelay,
11834
11924
  attempt: newAttempt
11835
11925
  });
11836
- await this.queue.enqueue({
11926
+ if (!await this.queue.requeue({
11837
11927
  id: item.id,
11838
11928
  job: item.job,
11839
11929
  item: item.item,
11840
11930
  availableAt: retryDate,
11841
11931
  attempt: newAttempt,
11842
- visibilityTimeoutMs: item.visibilityTimeoutMs
11843
- });
11932
+ visibilityTimeoutMs: item.visibilityTimeoutMs,
11933
+ deduplicationKey: item.deduplicationKey
11934
+ })) this.#logSuperseded("retry", item.id, jobType);
11844
11935
  } catch (requeueError) {
11845
11936
  this.logger.error(`Worker failed to requeue batch item. It will be retried after the visibility timeout.`, {
11846
11937
  name: this.options.name,
@@ -11855,6 +11946,13 @@ var Worker = class Worker {
11855
11946
  /**
11856
11947
  * Processes a single item.
11857
11948
  */
11949
+ #logSuperseded(action, id, job) {
11950
+ this.logger.info(`Worker skipped ${action} for an item replaced or removed while it ran`, {
11951
+ name: this.options.name,
11952
+ id,
11953
+ job
11954
+ });
11955
+ }
11858
11956
  async processItem({ id, job, item, visibilityTimeoutMs, attempt, timestamp, deduplicationKey }, batchSize, workerId, limiter) {
11859
11957
  const catalogItem = this.options.catalog[job];
11860
11958
  const handler = this.jobs[job];
@@ -11931,7 +12029,7 @@ var Worker = class Worker {
11931
12029
  ...logAttributes,
11932
12030
  attempt: newAttempt
11933
12031
  });
11934
- await this.queue.moveToDeadLetterQueue(id, errorMessage);
12032
+ if (!await this.queue.moveToDeadLetterQueue(id, errorMessage, deduplicationKey)) this.#logSuperseded("dead-letter", id, job);
11935
12033
  if (catalogItem.cron) await this.rescheduleCronJob(job, catalogItem, item);
11936
12034
  return;
11937
12035
  }
@@ -11945,14 +12043,15 @@ var Worker = class Worker {
11945
12043
  visibilityTimeoutMs,
11946
12044
  attempt: newAttempt
11947
12045
  });
11948
- await this.queue.enqueue({
12046
+ if (!await this.queue.requeue({
11949
12047
  id,
11950
12048
  job,
11951
12049
  item,
11952
12050
  availableAt: retryDate,
11953
12051
  attempt: newAttempt,
11954
- visibilityTimeoutMs
11955
- });
12052
+ visibilityTimeoutMs,
12053
+ deduplicationKey
12054
+ })) this.#logSuperseded("retry", id, job);
11956
12055
  } catch (requeueError) {
11957
12056
  this.logger.error(`Worker failed to requeue item. It will be retried after the visibility timeout.`, {
11958
12057
  name: this.options.name,
@@ -12111,6 +12210,33 @@ function isAbortError(error) {
12111
12210
  return error instanceof Error && (error.name === "AbortError" || error.message === "AbortError");
12112
12211
  }
12113
12212
 
12213
+ //#endregion
12214
+ //#region src/fair-queue/connection.ts
12215
+ function isClusterConnection(options) {
12216
+ return "cluster" in options && options.cluster !== void 0;
12217
+ }
12218
+ function createFairQueueRedisClient(options) {
12219
+ return isClusterConnection(options) ? createRedisClusterClient(options.cluster) : createRedisClient(options);
12220
+ }
12221
+ function keyPrefixOf(options) {
12222
+ return isClusterConnection(options) ? options.cluster.clusterOptions?.keyPrefix ?? "" : options.keyPrefix ?? "";
12223
+ }
12224
+ /**
12225
+ * Every key matching `pattern` (which must include the key prefix), page by page. A cluster doesn't
12226
+ * fan SCAN out to its nodes, so this scans each primary in turn.
12227
+ */
12228
+ async function* scanKeys(client, pattern, count = 1e3) {
12229
+ const nodes = client instanceof import_built.Cluster ? client.nodes("master") : [client];
12230
+ for (const node of nodes) {
12231
+ let cursor = "0";
12232
+ do {
12233
+ const [next, keys] = await node.scan(cursor, "MATCH", pattern, "COUNT", count);
12234
+ cursor = next;
12235
+ if (keys.length > 0) yield keys;
12236
+ } while (cursor !== "0");
12237
+ }
12238
+ }
12239
+
12114
12240
  //#endregion
12115
12241
  //#region src/fair-queue/concurrency.ts
12116
12242
  /**
@@ -12137,7 +12263,7 @@ var ConcurrencyManager = class {
12137
12263
  logger;
12138
12264
  constructor(options) {
12139
12265
  this.options = options;
12140
- this.redis = createRedisClient(options.redis);
12266
+ this.redis = createFairQueueRedisClient(options.redis);
12141
12267
  this.keys = options.keys;
12142
12268
  this.groups = options.groups;
12143
12269
  this.groupsByName = new Map(options.groups.map((g) => [g.name, g]));
@@ -12225,7 +12351,10 @@ var ConcurrencyManager = class {
12225
12351
  * record can only be a leak; if the message is about to be re-claimed, reserve simply
12226
12352
  * re-adds the member.
12227
12353
  *
12228
- * @param inflightDataKeys - The in-flight data hash keys for every shard. The sweep
12354
+ * With a tenant-sharded key producer, each shard's sets are checked against that shard's
12355
+ * in-flight data only, so the check stays in one cluster slot.
12356
+ *
12357
+ * @param inflightDataKeys - The in-flight data hash keys for every shard, in shard order. The sweep
12229
12358
  * refuses to run when this is empty, since with nowhere to look for running messages
12230
12359
  * every member would look orphaned and all concurrency accounting would be erased.
12231
12360
  * @returns The message ids that were removed, and how many sets were checked
@@ -12238,46 +12367,49 @@ var ConcurrencyManager = class {
12238
12367
  removed: []
12239
12368
  };
12240
12369
  }
12241
- const keyPrefix = this.options.redis.keyPrefix ?? "";
12370
+ const keyPrefix = keyPrefixOf(this.options.redis);
12242
12371
  let scannedSets = 0;
12243
12372
  const removed = [];
12244
- for (const group of this.groups) {
12245
- const pattern = `${keyPrefix}${this.keys.concurrencyKey(group.name, "*")}`;
12246
- let cursor = "0";
12247
- do {
12248
- const [nextCursor, foundKeys] = await this.redis.scan(cursor, "MATCH", pattern, "COUNT", 1e3);
12249
- cursor = nextCursor;
12250
- for (const fullKey of foundKeys) {
12251
- const key = keyPrefix && fullKey.startsWith(keyPrefix) ? fullKey.slice(keyPrefix.length) : fullKey;
12252
- try {
12253
- let sawMembers = false;
12254
- let memberCursor = "0";
12255
- do {
12256
- const [nextMemberCursor, page] = await this.redis.sscan(key, memberCursor, "COUNT", SWEEP_MEMBER_CHUNK_SIZE);
12257
- memberCursor = nextMemberCursor;
12258
- if (page.length === 0) continue;
12259
- sawMembers = true;
12260
- for (let i = 0; i < page.length; i += SWEEP_MEMBER_CHUNK_SIZE) {
12261
- const chunk = page.slice(i, i + SWEEP_MEMBER_CHUNK_SIZE);
12262
- const removedIds = await this.redis.removeOrphanedConcurrencySlots(1 + inflightDataKeys.length, [key, ...inflightDataKeys], ...chunk);
12263
- removed.push(...removedIds);
12264
- }
12265
- } while (memberCursor !== "0");
12266
- if (sawMembers) scannedSets++;
12267
- } catch (error) {
12268
- this.logger.error("Failed to sweep concurrency set, skipping it", {
12269
- key,
12270
- error: error instanceof Error ? error.message : String(error)
12271
- });
12373
+ for (const scope of this.#sweepScopes(inflightDataKeys)) for await (const foundKeys of scanKeys(this.redis, `${keyPrefix}${scope.pattern}`)) for (const fullKey of foundKeys) {
12374
+ const key = keyPrefix && fullKey.startsWith(keyPrefix) ? fullKey.slice(keyPrefix.length) : fullKey;
12375
+ try {
12376
+ let sawMembers = false;
12377
+ let memberCursor = "0";
12378
+ do {
12379
+ const [nextMemberCursor, page] = await this.redis.sscan(key, memberCursor, "COUNT", SWEEP_MEMBER_CHUNK_SIZE);
12380
+ memberCursor = nextMemberCursor;
12381
+ if (page.length === 0) continue;
12382
+ sawMembers = true;
12383
+ for (let i = 0; i < page.length; i += SWEEP_MEMBER_CHUNK_SIZE) {
12384
+ const chunk = page.slice(i, i + SWEEP_MEMBER_CHUNK_SIZE);
12385
+ const removedIds = await this.redis.removeOrphanedConcurrencySlots(1 + scope.inflightDataKeys.length, [key, ...scope.inflightDataKeys], ...chunk);
12386
+ removed.push(...removedIds);
12272
12387
  }
12273
- }
12274
- } while (cursor !== "0");
12388
+ } while (memberCursor !== "0");
12389
+ if (sawMembers) scannedSets++;
12390
+ } catch (error) {
12391
+ this.logger.error("Failed to sweep concurrency set, skipping it", {
12392
+ key,
12393
+ error: error instanceof Error ? error.message : String(error)
12394
+ });
12395
+ }
12275
12396
  }
12276
12397
  return {
12277
12398
  scannedSets,
12278
12399
  removed
12279
12400
  };
12280
12401
  }
12402
+ #sweepScopes(inflightDataKeys) {
12403
+ const keys = this.keys;
12404
+ if (keys.tenantShard && keys.concurrencyKeyPattern) return this.groups.flatMap((group) => inflightDataKeys.map((inflightDataKey, shardId) => ({
12405
+ pattern: keys.concurrencyKeyPattern(group.name, shardId),
12406
+ inflightDataKeys: [inflightDataKey]
12407
+ })));
12408
+ return this.groups.map((group) => ({
12409
+ pattern: keys.concurrencyKey(group.name, "*"),
12410
+ inflightDataKeys
12411
+ }));
12412
+ }
12281
12413
  /**
12282
12414
  * Get current concurrency for a specific group.
12283
12415
  */
@@ -12438,7 +12570,7 @@ var MasterQueue = class {
12438
12570
  shardCount;
12439
12571
  constructor(options) {
12440
12572
  this.options = options;
12441
- this.redis = createRedisClient(options.redis);
12573
+ this.redis = createFairQueueRedisClient(options.redis);
12442
12574
  this.keys = options.keys;
12443
12575
  this.shardCount = Math.max(1, options.shardCount);
12444
12576
  this.#registerCommands();
@@ -12558,6 +12690,8 @@ var MasterQueue = class {
12558
12690
  * Provides better distribution than djb2 and minimal reshuffling when shard count changes.
12559
12691
  */
12560
12692
  #hashToShard(queueId) {
12693
+ const keys = this.options.keys;
12694
+ if (keys.tenantShard) return keys.tenantShard(keys.extractTenantId(queueId));
12561
12695
  return jumpHash(queueId, this.shardCount);
12562
12696
  }
12563
12697
  #registerCommands() {
@@ -12800,6 +12934,16 @@ var FairQueueTelemetry = class {
12800
12934
  }
12801
12935
  });
12802
12936
  }
12937
+ if (callbacks.getOldestMessageAge && callbacks.shardCount) {
12938
+ const getOldestMessageAge = callbacks.getOldestMessageAge;
12939
+ const shardCount = callbacks.shardCount;
12940
+ this.metrics.oldestMessageAge.addCallback(async (observableResult) => {
12941
+ for (let shardId = 0; shardId < shardCount; shardId++) {
12942
+ const age = await getOldestMessageAge(shardId);
12943
+ observableResult.observe(age, { [FairQueueAttributes.SHARD_ID]: shardId.toString() });
12944
+ }
12945
+ });
12946
+ }
12803
12947
  if (callbacks.getDLQLength && callbacks.observedTenants) {
12804
12948
  const getDLQLength = callbacks.getDLQLength;
12805
12949
  const tenants = callbacks.observedTenants;
@@ -12893,6 +13037,10 @@ var FairQueueTelemetry = class {
12893
13037
  dlqLength: this.meter.createObservableGauge(`${this.name}.dlq.length`, {
12894
13038
  description: "Number of messages in dead letter queue",
12895
13039
  unit: "messages"
13040
+ }),
13041
+ oldestMessageAge: this.meter.createObservableGauge(`${this.name}.oldest_message_age`, {
13042
+ description: "Age of the oldest due message not yet completed in a shard, including claimed messages until their queue next completes or empties",
13043
+ unit: "ms"
12896
13044
  })
12897
13045
  };
12898
13046
  }
@@ -13110,7 +13258,7 @@ var TenantDispatch = class {
13110
13258
  shardCount;
13111
13259
  constructor(options) {
13112
13260
  this.options = options;
13113
- this.redis = createRedisClient(options.redis);
13261
+ this.redis = createFairQueueRedisClient(options.redis);
13114
13262
  this.keys = options.keys;
13115
13263
  this.shardCount = Math.max(1, options.shardCount);
13116
13264
  }
@@ -13120,6 +13268,7 @@ var TenantDispatch = class {
13120
13268
  * always maps to exactly one dispatch shard.
13121
13269
  */
13122
13270
  getShardForTenant(tenantId) {
13271
+ if (this.options.keys.tenantShard) return this.options.keys.tenantShard(tenantId);
13123
13272
  return jumpHash(tenantId, this.shardCount);
13124
13273
  }
13125
13274
  /**
@@ -13235,7 +13384,7 @@ var VisibilityManager = class {
13235
13384
  logger;
13236
13385
  constructor(options) {
13237
13386
  this.options = options;
13238
- this.redis = createRedisClient(options.redis);
13387
+ this.redis = createFairQueueRedisClient(options.redis);
13239
13388
  this.keys = options.keys;
13240
13389
  this.shardCount = options.shardCount;
13241
13390
  this.defaultTimeoutMs = options.defaultTimeoutMs;
@@ -13548,6 +13697,7 @@ var VisibilityManager = class {
13548
13697
  * Must use same algorithm as MasterQueue for consistency.
13549
13698
  */
13550
13699
  #getShardForQueue(queueId) {
13700
+ if (this.keys.tenantShard) return this.keys.tenantShard(this.keys.extractTenantId(queueId));
13551
13701
  return jumpHash(queueId, this.shardCount);
13552
13702
  }
13553
13703
  #makeMember(messageId, queueId) {
@@ -13826,7 +13976,7 @@ var WorkerQueueManager = class {
13826
13976
  logger;
13827
13977
  constructor(options) {
13828
13978
  this.options = options;
13829
- this.redis = createRedisClient(options.redis);
13979
+ this.redis = createFairQueueRedisClient(options.redis);
13830
13980
  this.keys = options.keys;
13831
13981
  this.logger = options.logger ?? {
13832
13982
  debug: () => {},
@@ -13877,6 +14027,9 @@ var WorkerQueueManager = class {
13877
14027
  */
13878
14028
  async blockingPop(workerQueueId, timeoutSeconds, signal) {
13879
14029
  const workerQueueKey = this.keys.workerQueueKey(workerQueueId);
14030
+ if (signal?.aborted) return null;
14031
+ const readyMessageKey = await this.redis.lpop(workerQueueKey);
14032
+ if (readyMessageKey) return readyMessageKey;
13880
14033
  const blockingClient = this.redis.duplicate();
13881
14034
  const cleanup = signal ? () => {
13882
14035
  blockingClient.disconnect();
@@ -14122,6 +14275,100 @@ var CallbackFairQueueKeyProducer = class extends DefaultFairQueueKeyProducer {
14122
14275
  return this.groupExtractor(groupName, queueId);
14123
14276
  }
14124
14277
  };
14278
+ /**
14279
+ * Key producer for running a fair queue on a Redis cluster. Each tenant belongs to one shard,
14280
+ * `jumpHash(tenantId, shardCount)`, and every key of a shard carries the hash tag
14281
+ * `{prefix:shard}`, so a shard's queues, items, payloads, tenant indexes, dispatch index,
14282
+ * in-flight set and concurrency sets share a slot and every script stays single-slot.
14283
+ *
14284
+ * Concurrency groups must be keyed by tenant id. The shard count can't change without moving keys.
14285
+ *
14286
+ * Key structure (S = the tenant's shard):
14287
+ * - Dispatch index: {prefix:S}:dispatch
14288
+ * - Tenant queue index: {prefix:S}:tenantq:{tenantId}
14289
+ * - Queue: {prefix:S}:queue:{queueId}, items {prefix:S}:queue:{queueId}:items
14290
+ * - Payload: {prefix:S}:payload:{queueId}:{messageId}
14291
+ * - Payload bytes per tenant: {prefix:S}:payloadbytes:{tenantId}
14292
+ * - Concurrency: {prefix:S}:concurrency:{groupName}:{tenantId}
14293
+ * - In-flight: {prefix:S}:inflight, data {prefix:S}:inflight:data
14294
+ * - Dead letter queue: {prefix:S}:dlq:{tenantId}, data {prefix:S}:dlq:{tenantId}:data
14295
+ * - Legacy master queue: {prefix:S}:master
14296
+ * - Worker queue: {prefix}:worker:{consumerId} (its own slot)
14297
+ * - DRR deficits: {prefix}:drr:deficit (its own slot)
14298
+ */
14299
+ var TenantShardedFairQueueKeyProducer = class {
14300
+ shardCount;
14301
+ prefix;
14302
+ tenantExtractor;
14303
+ groupExtractor;
14304
+ constructor(options) {
14305
+ if (!Number.isInteger(options.shardCount) || options.shardCount < 1) throw new Error(`shardCount must be a positive integer, got ${options.shardCount}`);
14306
+ this.prefix = options.prefix;
14307
+ this.shardCount = options.shardCount;
14308
+ this.tenantExtractor = options.extractTenantId;
14309
+ this.groupExtractor = options.extractGroupId;
14310
+ }
14311
+ tenantShard(tenantId) {
14312
+ return jumpHash(tenantId, this.shardCount);
14313
+ }
14314
+ masterQueueKey(shardId) {
14315
+ return `${this.#tag(shardId)}:master`;
14316
+ }
14317
+ queueKey(queueId) {
14318
+ return `${this.#queueTag(queueId)}:queue:${queueId}`;
14319
+ }
14320
+ queueItemsKey(queueId) {
14321
+ return `${this.#queueTag(queueId)}:queue:${queueId}:items`;
14322
+ }
14323
+ payloadKey(queueId, messageId) {
14324
+ return `${this.#queueTag(queueId)}:payload:${queueId}:${messageId}`;
14325
+ }
14326
+ tenantPayloadBytesKey(tenantId) {
14327
+ return `${this.#tag(this.tenantShard(tenantId))}:payloadbytes:${tenantId}`;
14328
+ }
14329
+ concurrencyKey(groupName, groupId) {
14330
+ return `${this.#tag(this.tenantShard(groupId))}:concurrency:${groupName}:${groupId}`;
14331
+ }
14332
+ concurrencyKeyPattern(groupName, shardId) {
14333
+ return `${this.#tag(shardId)}:concurrency:${groupName}:*`;
14334
+ }
14335
+ inflightKey(shardId) {
14336
+ return `${this.#tag(shardId)}:inflight`;
14337
+ }
14338
+ inflightDataKey(shardId) {
14339
+ return `${this.#tag(shardId)}:inflight:data`;
14340
+ }
14341
+ workerQueueKey(consumerId) {
14342
+ return `${this.prefix}:worker:${consumerId}`;
14343
+ }
14344
+ deadLetterQueueKey(tenantId) {
14345
+ return `${this.#tag(this.tenantShard(tenantId))}:dlq:${tenantId}`;
14346
+ }
14347
+ deadLetterQueueDataKey(tenantId) {
14348
+ return `${this.#tag(this.tenantShard(tenantId))}:dlq:${tenantId}:data`;
14349
+ }
14350
+ dispatchKey(shardId) {
14351
+ return `${this.#tag(shardId)}:dispatch`;
14352
+ }
14353
+ tenantQueueIndexKey(tenantId) {
14354
+ return `${this.#tag(this.tenantShard(tenantId))}:tenantq:${tenantId}`;
14355
+ }
14356
+ drrDeficitKey() {
14357
+ return `${this.prefix}:drr:deficit`;
14358
+ }
14359
+ extractTenantId(queueId) {
14360
+ return this.tenantExtractor(queueId);
14361
+ }
14362
+ extractGroupId(groupName, queueId) {
14363
+ return this.groupExtractor(groupName, queueId);
14364
+ }
14365
+ #queueTag(queueId) {
14366
+ return this.#tag(this.tenantShard(this.tenantExtractor(queueId)));
14367
+ }
14368
+ #tag(shardId) {
14369
+ return `{${this.prefix}:${shardId}}`;
14370
+ }
14371
+ };
14125
14372
 
14126
14373
  //#endregion
14127
14374
  //#region src/fair-queue/retry.ts
@@ -14338,7 +14585,7 @@ var DRRScheduler = class extends BaseScheduler {
14338
14585
  constructor(config) {
14339
14586
  super();
14340
14587
  this.config = config;
14341
- this.redis = createRedisClient(config.redis);
14588
+ this.redis = createFairQueueRedisClient(config.redis);
14342
14589
  this.keys = config.keys;
14343
14590
  this.quantum = config.quantum;
14344
14591
  this.maxDeficit = config.maxDeficit;
@@ -14481,6 +14728,7 @@ var DRRScheduler = class extends BaseScheduler {
14481
14728
  return result;
14482
14729
  }
14483
14730
  #deficitKey() {
14731
+ if (this.keys.drrDeficitKey) return this.keys.drrDeficitKey();
14484
14732
  return `${this.keys.masterQueueKey(0).split(":")[0]}:drr:deficit`;
14485
14733
  }
14486
14734
  async #getTenantsFromDispatch(dispatchKey) {
@@ -14639,7 +14887,7 @@ var WeightedScheduler = class extends BaseScheduler {
14639
14887
  constructor(config) {
14640
14888
  super();
14641
14889
  this.config = config;
14642
- this.redis = createRedisClient(config.redis);
14890
+ this.redis = createFairQueueRedisClient(config.redis);
14643
14891
  this.keys = config.keys;
14644
14892
  this.rng = seedrandom(config.seed);
14645
14893
  this.biases = config.biases ?? defaultBiases;
@@ -14894,7 +15142,7 @@ var RoundRobinScheduler = class extends BaseScheduler {
14894
15142
  constructor(config) {
14895
15143
  super();
14896
15144
  this.config = config;
14897
- this.redis = createRedisClient(config.redis);
15145
+ this.redis = createFairQueueRedisClient(config.redis);
14898
15146
  this.keys = config.keys;
14899
15147
  this.masterQueueLimit = config.masterQueueLimit ?? 1e3;
14900
15148
  }
@@ -14966,6 +15214,17 @@ var RoundRobinScheduler = class extends BaseScheduler {
14966
15214
 
14967
15215
  //#endregion
14968
15216
  //#region src/fair-queue/index.ts
15217
+ /** An enqueue that would take a tenant past `tenantPayloadBytesLimit`. */
15218
+ var TenantQueueFullError = class extends Error {
15219
+ tenantId;
15220
+ limitBytes;
15221
+ constructor(tenantId, limitBytes) {
15222
+ super(`Tenant ${tenantId} is at its limit of ${limitBytes} queued payload bytes`);
15223
+ this.tenantId = tenantId;
15224
+ this.limitBytes = limitBytes;
15225
+ this.name = "TenantQueueFullError";
15226
+ }
15227
+ };
14969
15228
  /**
14970
15229
  * FairQueue is the main orchestrator for fair queue message routing.
14971
15230
  *
@@ -14984,7 +15243,7 @@ var RoundRobinScheduler = class extends BaseScheduler {
14984
15243
  *
14985
15244
  * @typeParam TPayloadSchema - Zod schema for message payload validation
14986
15245
  */
14987
- var FairQueue = class {
15246
+ var FairQueue = class FairQueue {
14988
15247
  options;
14989
15248
  redis;
14990
15249
  keys;
@@ -15008,6 +15267,7 @@ var FairQueue = class {
15008
15267
  reconcileIntervalMs;
15009
15268
  workerQueueResolver;
15010
15269
  batchClaimSize;
15270
+ separatePayloads;
15011
15271
  cooloffEnabled;
15012
15272
  cooloffThreshold;
15013
15273
  cooloffPeriodMs;
@@ -15027,8 +15287,10 @@ var FairQueue = class {
15027
15287
  tenantDispatch;
15028
15288
  constructor(options) {
15029
15289
  this.options = options;
15030
- this.redis = createRedisClient(options.redis);
15290
+ FairQueue.#validateLayout(options);
15291
+ this.redis = createFairQueueRedisClient(options.redis);
15031
15292
  this.keys = options.keys;
15293
+ this.separatePayloads = options.separatePayloads ?? false;
15032
15294
  this.scheduler = options.scheduler;
15033
15295
  this.logger = options.logger ?? new Logger("FairQueue", "info");
15034
15296
  this.abortController = new AbortController();
@@ -15036,7 +15298,7 @@ var FairQueue = class {
15036
15298
  this.validateOnEnqueue = options.validateOnEnqueue ?? false;
15037
15299
  this.retryStrategy = options.retry?.strategy;
15038
15300
  this.deadLetterQueueEnabled = options.retry?.deadLetterQueue ?? true;
15039
- this.shardCount = options.shardCount ?? 1;
15301
+ this.shardCount = options.keys.shardCount ?? options.shardCount ?? 1;
15040
15302
  this.consumerCount = options.consumerCount ?? 1;
15041
15303
  this.consumerIntervalMs = options.consumerIntervalMs ?? 100;
15042
15304
  this.visibilityTimeoutMs = options.visibilityTimeoutMs ?? 3e4;
@@ -15128,6 +15390,9 @@ var FairQueue = class {
15128
15390
  getDLQLength: async (tenantId) => {
15129
15391
  return await this.getDeadLetterQueueLength(tenantId);
15130
15392
  },
15393
+ getOldestMessageAge: async (shardId) => {
15394
+ return await this.getOldestMessageAge(shardId);
15395
+ },
15131
15396
  shardCount: this.shardCount,
15132
15397
  observedTenants: options?.observedTenants
15133
15398
  });
@@ -15172,7 +15437,13 @@ var FairQueue = class {
15172
15437
  }) : void 0,
15173
15438
  metadata: options.metadata
15174
15439
  };
15175
- await this.redis.enqueueMessageAtomicV2(queueKey, queueItemsKey, tenantQueueIndexKey, dispatchKey, options.queueId, messageId, timestamp.toString(), JSON.stringify(storedMessage), options.tenantId);
15440
+ if (this.separatePayloads) {
15441
+ const { payload, ...envelope } = storedMessage;
15442
+ const limit = options.ignorePayloadBytesLimit ? 0 : await this.options.tenantPayloadBytesLimit?.(options.tenantId) ?? 0;
15443
+ const result = await this.redis.enqueueMessageSeparatePayload(queueKey, queueItemsKey, tenantQueueIndexKey, dispatchKey, this.keys.payloadKey(options.queueId, messageId), this.keys.inflightKey(this.masterQueue.getShardForQueue(options.queueId)), this.keys.tenantPayloadBytesKey(options.tenantId), options.queueId, messageId, timestamp.toString(), JSON.stringify(envelope), options.tenantId, JSON.stringify(payload), `${messageId}:${options.queueId}`, String(limit));
15444
+ if (result === -1) throw new TenantQueueFullError(options.tenantId, limit);
15445
+ span.setAttribute("enqueued", result === 1);
15446
+ } else await this.redis.enqueueMessageAtomicV2(queueKey, queueItemsKey, tenantQueueIndexKey, dispatchKey, options.queueId, messageId, timestamp.toString(), JSON.stringify(storedMessage), options.tenantId);
15176
15447
  span.setAttributes({
15177
15448
  [FairQueueAttributes.QUEUE_ID]: options.queueId,
15178
15449
  [FairQueueAttributes.TENANT_ID]: options.tenantId,
@@ -15196,6 +15467,7 @@ var FairQueue = class {
15196
15467
  */
15197
15468
  async enqueueBatch(options) {
15198
15469
  return this.telemetry.trace("enqueueBatch", async (span) => {
15470
+ if (this.separatePayloads) throw new Error("enqueueBatch isn't supported with separatePayloads");
15199
15471
  const queueKey = this.keys.queueKey(options.queueId);
15200
15472
  const queueItemsKey = this.keys.queueItemsKey(options.queueId);
15201
15473
  const dispatchShardId = this.tenantDispatch.getShardForTenant(options.tenantId);
@@ -15450,6 +15722,17 @@ var FairQueue = class {
15450
15722
  return dispatchCount + legacyCount;
15451
15723
  }
15452
15724
  /**
15725
+ * Get the age in ms of the oldest due message not yet completed in a shard, or 0 if none.
15726
+ * Claims don't refresh the index scores, so claimed messages count until their queue next completes or empties.
15727
+ */
15728
+ async getOldestMessageAge(shardId) {
15729
+ const now = Date.now();
15730
+ const [[oldestTenant], [oldestLegacyQueue]] = await Promise.all([this.tenantDispatch.getTenantsFromShard(shardId, 1, now), this.masterQueue.getQueuesFromShard(shardId, 1, now)]);
15731
+ const oldestScore = Math.min(oldestTenant?.score ?? Infinity, oldestLegacyQueue?.score ?? Infinity);
15732
+ if (!Number.isFinite(oldestScore)) return 0;
15733
+ return Math.max(0, now - oldestScore);
15734
+ }
15735
+ /**
15453
15736
  * Get total in-flight message count.
15454
15737
  */
15455
15738
  async getTotalInflightCount() {
@@ -15712,10 +15995,22 @@ var FairQueue = class {
15712
15995
  async getMessageData(messageId, queueId) {
15713
15996
  const shardId = this.masterQueue.getShardForQueue(queueId);
15714
15997
  const inflightDataKey = this.keys.inflightDataKey(shardId);
15715
- const dataJson = await this.redis.hget(inflightDataKey, messageId);
15998
+ const [dataJson, payloadJson] = await Promise.all([this.redis.hget(inflightDataKey, messageId), this.separatePayloads ? this.redis.get(this.keys.payloadKey(queueId, messageId)) : Promise.resolve(null)]);
15716
15999
  if (!dataJson) return null;
15717
16000
  try {
15718
- return JSON.parse(dataJson);
16001
+ const message = JSON.parse(dataJson);
16002
+ if (!this.separatePayloads) return message;
16003
+ if (payloadJson === null) {
16004
+ this.logger.error("In-flight message has no payload", {
16005
+ messageId,
16006
+ queueId
16007
+ });
16008
+ return null;
16009
+ }
16010
+ return {
16011
+ ...message,
16012
+ payload: JSON.parse(payloadJson)
16013
+ };
15719
16014
  } catch {
15720
16015
  this.logger.error("Failed to parse message data", {
15721
16016
  messageId,
@@ -15756,7 +16051,7 @@ var FairQueue = class {
15756
16051
  metadata: storedMessage?.metadata ?? {}
15757
16052
  };
15758
16053
  await this.#releaseConcurrencySafely(descriptor, messageId, "completeMessage");
15759
- await this.visibilityManager.complete(messageId, queueId);
16054
+ await this.#completeInVisibility(messageId, queueId, descriptor.tenantId);
15760
16055
  const { queueEmpty } = await this.#updateAllIndexesAfterDequeue(queueId, descriptor.tenantId);
15761
16056
  if (queueEmpty) {
15762
16057
  this.queueDescriptorCache.delete(queueId);
@@ -15906,9 +16201,16 @@ var FairQueue = class {
15906
16201
  }
15907
16202
  async #moveToDeadLetterQueue(storedMessage, errorMessage) {
15908
16203
  if (!this.deadLetterQueueEnabled) {
15909
- await this.visibilityManager.complete(storedMessage.id, storedMessage.queueId);
16204
+ await this.#completeInVisibility(storedMessage.id, storedMessage.queueId, storedMessage.tenantId);
15910
16205
  return;
15911
16206
  }
16207
+ if (this.separatePayloads) {
16208
+ const payloadJson = await this.redis.get(this.keys.payloadKey(storedMessage.queueId, storedMessage.id));
16209
+ storedMessage = {
16210
+ ...storedMessage,
16211
+ payload: payloadJson === null ? void 0 : JSON.parse(payloadJson)
16212
+ };
16213
+ }
15912
16214
  const dlqKey = this.keys.deadLetterQueueKey(storedMessage.tenantId);
15913
16215
  const dlqDataKey = this.keys.deadLetterQueueDataKey(storedMessage.tenantId);
15914
16216
  this.masterQueue.getShardForQueue(storedMessage.queueId);
@@ -15922,7 +16224,7 @@ var FairQueue = class {
15922
16224
  lastError: errorMessage,
15923
16225
  originalTimestamp: storedMessage.timestamp
15924
16226
  };
15925
- await this.visibilityManager.complete(storedMessage.id, storedMessage.queueId);
16227
+ await this.#completeInVisibility(storedMessage.id, storedMessage.queueId, storedMessage.tenantId);
15926
16228
  const pipeline = this.redis.pipeline();
15927
16229
  pipeline.zadd(dlqKey, dlqMessage.deadLetteredAt, storedMessage.id);
15928
16230
  pipeline.hset(dlqDataKey, storedMessage.id, JSON.stringify(dlqMessage));
@@ -16068,6 +16370,40 @@ var FairQueue = class {
16068
16370
  const removedFromDispatch = await this.redis.updateDispatchIndexes(queueKey, tenantQueueIndexKey, dispatchKey, queueId, tenantId);
16069
16371
  return { queueEmpty: removedFromMaster === 1 || removedFromDispatch === 1 };
16070
16372
  }
16373
+ /**
16374
+ * Remove a settled message from in-flight. With separate payloads this also deletes the payload
16375
+ * and takes its bytes off the tenant's count, in one script, so the count never drifts.
16376
+ */
16377
+ async #completeInVisibility(messageId, queueId, tenantId) {
16378
+ if (!this.separatePayloads) {
16379
+ await this.visibilityManager.complete(messageId, queueId);
16380
+ return;
16381
+ }
16382
+ const shardId = this.masterQueue.getShardForQueue(queueId);
16383
+ await this.redis.completeSeparatePayload(this.keys.inflightKey(shardId), this.keys.inflightDataKey(shardId), this.keys.payloadKey(queueId, messageId), this.keys.tenantPayloadBytesKey(tenantId), `${messageId}:${queueId}`, messageId);
16384
+ }
16385
+ /** A tenant's queued and in-flight payload bytes, with `separatePayloads`. */
16386
+ async getTenantPayloadBytes(tenantId) {
16387
+ if (!this.separatePayloads) return 0;
16388
+ return Number(await this.redis.get(this.keys.tenantPayloadBytesKey(tenantId)) ?? 0);
16389
+ }
16390
+ /**
16391
+ * A cluster needs every script's keys in one slot, which only a tenant-sharded key producer
16392
+ * gives, and the concurrency reservation touches every group's set at once, so it allows one
16393
+ * group, keyed by tenant.
16394
+ */
16395
+ static #validateLayout(options) {
16396
+ const keys = options.keys;
16397
+ if (options.separatePayloads && (!keys.payloadKey || !keys.tenantPayloadBytesKey)) throw new Error("separatePayloads needs a key producer with payloadKey and tenantPayloadBytesKey");
16398
+ if (keys.tenantShard) {
16399
+ if (!keys.shardCount) throw new Error("A key producer with tenantShard must set shardCount");
16400
+ if (options.shardCount !== void 0 && options.shardCount !== keys.shardCount) throw new Error(`shardCount ${options.shardCount} doesn't match the key producer's ${keys.shardCount}`);
16401
+ }
16402
+ if (isClusterConnection(options.redis)) {
16403
+ if (!keys.tenantShard) throw new Error("A cluster connection needs a tenant-sharded key producer");
16404
+ if ((options.concurrencyGroups?.length ?? 0) > 1) throw new Error("A cluster connection supports at most one concurrency group");
16405
+ }
16406
+ }
16071
16407
  #createSchedulerContext() {
16072
16408
  return {
16073
16409
  getCurrentConcurrency: async (groupName, groupId) => {
@@ -16226,6 +16562,77 @@ if #tenantOldest >= 2 then
16226
16562
  end
16227
16563
 
16228
16564
  return (#ARGV - 2) / 3
16565
+ `
16566
+ });
16567
+ this.redis.defineCommand("enqueueMessageSeparatePayload", {
16568
+ numberOfKeys: 7,
16569
+ lua: `#!lua
16570
+ local queueKey = KEYS[1]
16571
+ local queueItemsKey = KEYS[2]
16572
+ local tenantQueueIndexKey = KEYS[3]
16573
+ local dispatchKey = KEYS[4]
16574
+ local payloadKey = KEYS[5]
16575
+ local inflightKey = KEYS[6]
16576
+ local bytesKey = KEYS[7]
16577
+
16578
+ local queueId = ARGV[1]
16579
+ local messageId = ARGV[2]
16580
+ local timestamp = tonumber(ARGV[3])
16581
+ local envelope = ARGV[4]
16582
+ local tenantId = ARGV[5]
16583
+ local payload = ARGV[6]
16584
+ local inflightMember = ARGV[7]
16585
+ local limit = tonumber(ARGV[8])
16586
+
16587
+ if redis.call('HEXISTS', queueItemsKey, messageId) == 1 then
16588
+ return 0
16589
+ end
16590
+ if redis.call('ZSCORE', inflightKey, inflightMember) then
16591
+ return 0
16592
+ end
16593
+
16594
+ local size = string.len(payload)
16595
+ if limit > 0 and tonumber(redis.call('GET', bytesKey) or '0') + size > limit then
16596
+ return -1
16597
+ end
16598
+
16599
+ redis.call('SET', payloadKey, payload)
16600
+ redis.call('INCRBY', bytesKey, size)
16601
+ redis.call('ZADD', queueKey, timestamp, messageId)
16602
+ redis.call('HSET', queueItemsKey, messageId, envelope)
16603
+
16604
+ local oldest = redis.call('ZRANGE', queueKey, 0, 0, 'WITHSCORES')
16605
+ if #oldest >= 2 then
16606
+ redis.call('ZADD', tenantQueueIndexKey, oldest[2], queueId)
16607
+ end
16608
+
16609
+ local tenantOldest = redis.call('ZRANGE', tenantQueueIndexKey, 0, 0, 'WITHSCORES')
16610
+ if #tenantOldest >= 2 then
16611
+ redis.call('ZADD', dispatchKey, tenantOldest[2], tenantId)
16612
+ end
16613
+
16614
+ return 1
16615
+ `
16616
+ });
16617
+ this.redis.defineCommand("completeSeparatePayload", {
16618
+ numberOfKeys: 4,
16619
+ lua: `
16620
+ local inflightKey = KEYS[1]
16621
+ local inflightDataKey = KEYS[2]
16622
+ local payloadKey = KEYS[3]
16623
+ local bytesKey = KEYS[4]
16624
+
16625
+ redis.call('ZREM', inflightKey, ARGV[1])
16626
+ redis.call('HDEL', inflightDataKey, ARGV[2])
16627
+
16628
+ local size = redis.call('STRLEN', payloadKey)
16629
+ if size > 0 then
16630
+ redis.call('DEL', payloadKey)
16631
+ if redis.call('DECRBY', bytesKey, size) <= 0 then
16632
+ redis.call('DEL', bytesKey)
16633
+ end
16634
+ end
16635
+ return size
16229
16636
  `
16230
16637
  });
16231
16638
  this.redis.defineCommand("updateDispatchIndexes", {
@@ -17348,5 +17755,5 @@ var MollifierDrainer = class {
17348
17755
  };
17349
17756
 
17350
17757
  //#endregion
17351
- export { BaseScheduler, BatchedSpanManager, BufferEntryError, BufferEntrySchema, BufferEntryStatus, CallbackFairQueueKeyProducer, ConcurrencyManager, CronSchema, CustomRetry, DRRScheduler, DefaultFairQueueKeyProducer, ExponentialBackoffRetry, FairQueue, FairQueueAttributes, FairQueueTelemetry, FixedDelayRetry, ImmediateRetry, LinearBackoffRetry, MasterQueue, MessagingAttributes, MollifierBuffer, MollifierDrainer, NoRetry, NoopScheduler, RoundRobinScheduler, SimpleQueue, TenantDispatch, VisibilityManager, WeightedScheduler, Worker, WorkerQueueManager, createDefaultRetryStrategy, defaultRetryOptions, deserialiseSnapshot, idempotencyLookupKeyFor, isAbortError, makeIdempotencyClaimKey, noopTelemetry, serialiseSnapshot };
17758
+ export { BaseScheduler, BatchedSpanManager, BufferEntryError, BufferEntrySchema, BufferEntryStatus, CallbackFairQueueKeyProducer, ConcurrencyManager, CronSchema, CustomRetry, DRRScheduler, DefaultFairQueueKeyProducer, ExponentialBackoffRetry, FairQueue, FairQueueAttributes, FairQueueTelemetry, FixedDelayRetry, ImmediateRetry, LinearBackoffRetry, MasterQueue, MessagingAttributes, MollifierBuffer, MollifierDrainer, NoRetry, NoopScheduler, RoundRobinScheduler, SimpleQueue, TenantDispatch, TenantQueueFullError, TenantShardedFairQueueKeyProducer, VisibilityManager, WeightedScheduler, Worker, WorkerQueueManager, createDefaultRetryStrategy, createFairQueueRedisClient, defaultRetryOptions, deserialiseSnapshot, idempotencyLookupKeyFor, isAbortError, isClusterConnection, keyPrefixOf, makeIdempotencyClaimKey, noopTelemetry, scanKeys, serialiseSnapshot };
17352
17759
  //# sourceMappingURL=index.js.map