@boringnode/queue 0.5.2 → 0.6.0

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 (35) hide show
  1. package/README.md +94 -14
  2. package/build/chunk-6IO4P6RB.js +145 -0
  3. package/build/chunk-6IO4P6RB.js.map +1 -0
  4. package/build/{chunk-VRXHCWNK.js → chunk-AHUVTAI7.js} +220 -15
  5. package/build/chunk-AHUVTAI7.js.map +1 -0
  6. package/build/chunk-S37X3CBO.js +500 -0
  7. package/build/chunk-S37X3CBO.js.map +1 -0
  8. package/build/index.d.ts +10 -2
  9. package/build/index.js +187 -31
  10. package/build/index.js.map +1 -1
  11. package/build/{job-Z5fBSzRX.d.ts → job-C4oyCVxR.d.ts} +131 -10
  12. package/build/src/contracts/adapter.d.ts +1 -1
  13. package/build/src/drivers/fake_adapter.d.ts +7 -6
  14. package/build/src/drivers/fake_adapter.js +1 -1
  15. package/build/src/drivers/knex_adapter.d.ts +6 -5
  16. package/build/src/drivers/knex_adapter.js +112 -0
  17. package/build/src/drivers/knex_adapter.js.map +1 -1
  18. package/build/src/drivers/redis_adapter.d.ts +6 -5
  19. package/build/src/drivers/redis_adapter.js +134 -368
  20. package/build/src/drivers/redis_adapter.js.map +1 -1
  21. package/build/src/drivers/redis_job_storage.d.ts +17 -0
  22. package/build/src/drivers/redis_job_storage.js +14 -0
  23. package/build/src/drivers/redis_job_storage.js.map +1 -0
  24. package/build/src/drivers/redis_scripts.d.ts +87 -0
  25. package/build/src/drivers/redis_scripts.js +29 -0
  26. package/build/src/drivers/redis_scripts.js.map +1 -0
  27. package/build/src/drivers/sync_adapter.d.ts +2 -1
  28. package/build/src/drivers/sync_adapter.js +7 -1
  29. package/build/src/drivers/sync_adapter.js.map +1 -1
  30. package/build/src/otel.d.ts +2 -2
  31. package/build/src/types/index.d.ts +1 -1
  32. package/build/src/types/main.d.ts +1 -1
  33. package/build/src/types/tracing_channels.d.ts +7 -1
  34. package/package.json +17 -17
  35. package/build/chunk-VRXHCWNK.js.map +0 -1
@@ -3,6 +3,23 @@ import {
3
3
  calculateScore,
4
4
  resolveRetention
5
5
  } from "../../chunk-QEFYHCL7.js";
6
+ import {
7
+ ACQUIRE_JOB_SCRIPT,
8
+ CLAIM_SCHEDULE_SCRIPT,
9
+ FINALIZE_JOB_SCRIPT,
10
+ GET_JOB_SCRIPT,
11
+ PUSH_DEDUP_JOB_SCRIPT,
12
+ PUSH_DELAYED_JOB_SCRIPT,
13
+ PUSH_JOB_SCRIPT,
14
+ RECOVER_STALLED_JOBS_SCRIPT,
15
+ REMOVE_JOB_SCRIPT,
16
+ RENEW_JOBS_SCRIPT,
17
+ RETRY_JOB_SCRIPT
18
+ } from "../../chunk-S37X3CBO.js";
19
+ import {
20
+ encodeRedisJobPayloadOverlay,
21
+ hydrateRedisJob
22
+ } from "../../chunk-6IO4P6RB.js";
6
23
  import "../../chunk-PZ5AY32C.js";
7
24
 
8
25
  // src/drivers/redis_adapter.ts
@@ -11,359 +28,6 @@ import { Redis } from "ioredis";
11
28
  var redisKey = "jobs";
12
29
  var schedulesKey = "schedules";
13
30
  var schedulesIndexKey = "schedules::index";
14
- var PUSH_JOB_SCRIPT = `
15
- local data_key = KEYS[1]
16
- local pending_key = KEYS[2]
17
- local job_id = ARGV[1]
18
- local job_data = ARGV[2]
19
- local score = tonumber(ARGV[3])
20
-
21
- redis.call('HSET', data_key, job_id, job_data)
22
- redis.call('ZADD', pending_key, score, job_id)
23
-
24
- return 1
25
- `;
26
- var PUSH_DELAYED_JOB_SCRIPT = `
27
- local data_key = KEYS[1]
28
- local delayed_key = KEYS[2]
29
- local job_id = ARGV[1]
30
- local job_data = ARGV[2]
31
- local execute_at = tonumber(ARGV[3])
32
-
33
- redis.call('HSET', data_key, job_id, job_data)
34
- redis.call('ZADD', delayed_key, execute_at, job_id)
35
-
36
- return 1
37
- `;
38
- var ACQUIRE_JOB_SCRIPT = `
39
- local data_key = KEYS[1]
40
- local pending_key = KEYS[2]
41
- local active_key = KEYS[3]
42
- local delayed_key = KEYS[4]
43
- local worker_id = ARGV[1]
44
- local now = tonumber(ARGV[2])
45
-
46
- -- Process delayed jobs: move ready jobs to pending
47
- local ready_job_ids = redis.call('ZRANGEBYSCORE', delayed_key, 0, now)
48
- if #ready_job_ids > 0 then
49
- for i = 1, #ready_job_ids do
50
- local job_id = ready_job_ids[i]
51
- local job_data = redis.call('HGET', data_key, job_id)
52
- if job_data then
53
- local job = cjson.decode(job_data)
54
- local priority = job.priority or 5
55
- local score = priority * 10000000000000 + now
56
- redis.call('ZADD', pending_key, score, job_id)
57
- redis.call('ZREM', delayed_key, job_id)
58
- end
59
- end
60
- end
61
-
62
- -- Pop highest priority job (lowest score)
63
- local result = redis.call('ZPOPMIN', pending_key)
64
- if not result or #result == 0 then
65
- return nil
66
- end
67
-
68
- local job_id = result[1]
69
- local job_data = redis.call('HGET', data_key, job_id)
70
- if not job_data then
71
- return nil
72
- end
73
-
74
- -- Store in active hash (without data, it's in data_key)
75
- local active_data = cjson.encode({
76
- workerId = worker_id,
77
- acquiredAt = now
78
- })
79
- redis.call('HSET', active_key, job_id, active_data)
80
-
81
- -- Return job with acquiredAt
82
- local job = cjson.decode(job_data)
83
- job.acquiredAt = now
84
- return cjson.encode(job)
85
- `;
86
- var REMOVE_JOB_SCRIPT = `
87
- local data_key = KEYS[1]
88
- local active_key = KEYS[2]
89
- local job_id = ARGV[1]
90
-
91
- if redis.call('HEXISTS', active_key, job_id) == 0 then
92
- return 0
93
- end
94
-
95
- redis.call('HDEL', active_key, job_id)
96
- redis.call('HDEL', data_key, job_id)
97
-
98
- return 1
99
- `;
100
- var FINALIZE_JOB_SCRIPT = `
101
- local data_key = KEYS[1]
102
- local active_key = KEYS[2]
103
- local history_key = KEYS[3]
104
- local index_key = KEYS[4]
105
- local job_id = ARGV[1]
106
- local now = tonumber(ARGV[2])
107
- local max_age = tonumber(ARGV[3])
108
- local max_count = tonumber(ARGV[4])
109
- local error_message = ARGV[5]
110
-
111
- -- Verify job is active
112
- if redis.call('HEXISTS', active_key, job_id) == 0 then
113
- return 0
114
- end
115
-
116
- -- Remove from active
117
- redis.call('HDEL', active_key, job_id)
118
-
119
- -- Store finalization info (data stays in data_key)
120
- local record = {
121
- finishedAt = now
122
- }
123
- if error_message and error_message ~= '' then
124
- record.error = error_message
125
- end
126
- redis.call('HSET', history_key, job_id, cjson.encode(record))
127
- redis.call('ZADD', index_key, now, job_id)
128
-
129
- -- Prune by age
130
- if max_age and max_age > 0 then
131
- local cutoff = now - max_age
132
- local expired = redis.call('ZRANGEBYSCORE', index_key, 0, cutoff)
133
- if #expired > 0 then
134
- redis.call('ZREM', index_key, unpack(expired))
135
- redis.call('HDEL', history_key, unpack(expired))
136
- redis.call('HDEL', data_key, unpack(expired))
137
- end
138
- end
139
-
140
- -- Prune by count
141
- if max_count and max_count > 0 then
142
- local size = tonumber(redis.call('ZCARD', index_key))
143
- if size > max_count then
144
- local excess = size - max_count
145
- local stale = redis.call('ZRANGE', index_key, 0, excess - 1)
146
- if #stale > 0 then
147
- redis.call('ZREM', index_key, unpack(stale))
148
- redis.call('HDEL', history_key, unpack(stale))
149
- redis.call('HDEL', data_key, unpack(stale))
150
- end
151
- end
152
- end
153
-
154
- return 1
155
- `;
156
- var RETRY_JOB_SCRIPT = `
157
- local data_key = KEYS[1]
158
- local active_key = KEYS[2]
159
- local pending_key = KEYS[3]
160
- local delayed_key = KEYS[4]
161
- local job_id = ARGV[1]
162
- local retry_at = tonumber(ARGV[2])
163
- local now = tonumber(ARGV[3])
164
-
165
- -- Verify job is active
166
- if redis.call('HEXISTS', active_key, job_id) == 0 then
167
- return 0
168
- end
169
-
170
- -- Get job data
171
- local job_data = redis.call('HGET', data_key, job_id)
172
- if not job_data then
173
- return 0
174
- end
175
-
176
- -- Remove from active
177
- redis.call('HDEL', active_key, job_id)
178
-
179
- -- Increment attempts and update data
180
- local job = cjson.decode(job_data)
181
- job.attempts = (job.attempts or 0) + 1
182
- redis.call('HSET', data_key, job_id, cjson.encode(job))
183
-
184
- -- Add back to pending or delayed
185
- if retry_at and retry_at > now then
186
- redis.call('ZADD', delayed_key, retry_at, job_id)
187
- else
188
- -- Score = priority * 1e13 + timestamp
189
- -- Lower score = higher priority, FIFO within same priority
190
- local priority = job.priority or 5
191
- local score = priority * 10000000000000 + now
192
- redis.call('ZADD', pending_key, score, job_id)
193
- end
194
-
195
- return 1
196
- `;
197
- var RECOVER_STALLED_JOBS_SCRIPT = `
198
- local data_key = KEYS[1]
199
- local active_key = KEYS[2]
200
- local pending_key = KEYS[3]
201
- local now = tonumber(ARGV[1])
202
- local stalled_threshold = tonumber(ARGV[2])
203
- local max_stalled_count = tonumber(ARGV[3])
204
-
205
- local recovered = 0
206
- local stalled_cutoff = now - stalled_threshold
207
-
208
- -- Get all active jobs
209
- local active_jobs = redis.call('HGETALL', active_key)
210
-
211
- -- HGETALL returns [field1, value1, field2, value2, ...]
212
- for i = 1, #active_jobs, 2 do
213
- local job_id = active_jobs[i]
214
- local active_data = active_jobs[i + 1]
215
- local active = cjson.decode(active_data)
216
-
217
- -- Check if job is stalled
218
- if active.acquiredAt < stalled_cutoff then
219
- local job_data = redis.call('HGET', data_key, job_id)
220
- if job_data then
221
- local job = cjson.decode(job_data)
222
- local current_stalled_count = job.stalledCount or 0
223
-
224
- -- Remove from active hash
225
- redis.call('HDEL', active_key, job_id)
226
-
227
- -- Check if job has exceeded max stalled count
228
- if current_stalled_count >= max_stalled_count then
229
- -- Job failed permanently, remove data too
230
- redis.call('HDEL', data_key, job_id)
231
- else
232
- -- Recover: increment stalledCount and put back in pending
233
- job.stalledCount = current_stalled_count + 1
234
- redis.call('HSET', data_key, job_id, cjson.encode(job))
235
- -- Score = priority * 1e13 + timestamp
236
- local priority = job.priority or 5
237
- local score = priority * 10000000000000 + now
238
- redis.call('ZADD', pending_key, score, job_id)
239
- recovered = recovered + 1
240
- end
241
- end
242
- end
243
- end
244
-
245
- return recovered
246
- `;
247
- var GET_JOB_SCRIPT = `
248
- local data_key = KEYS[1]
249
- local pending_key = KEYS[2]
250
- local delayed_key = KEYS[3]
251
- local active_key = KEYS[4]
252
- local completed_key = KEYS[5]
253
- local failed_key = KEYS[6]
254
- local job_id = ARGV[1]
255
-
256
- local job_data = redis.call('HGET', data_key, job_id)
257
- if not job_data then
258
- return nil
259
- end
260
-
261
- local status = nil
262
- local finished_at = nil
263
- local error_msg = nil
264
-
265
- -- Check status in order
266
- if redis.call('HEXISTS', active_key, job_id) == 1 then
267
- status = 'active'
268
- elseif redis.call('ZSCORE', pending_key, job_id) then
269
- status = 'pending'
270
- elseif redis.call('ZSCORE', delayed_key, job_id) then
271
- status = 'delayed'
272
- else
273
- local completed_data = redis.call('HGET', completed_key, job_id)
274
- if completed_data then
275
- status = 'completed'
276
- local record = cjson.decode(completed_data)
277
- finished_at = record.finishedAt
278
- else
279
- local failed_data = redis.call('HGET', failed_key, job_id)
280
- if failed_data then
281
- status = 'failed'
282
- local record = cjson.decode(failed_data)
283
- finished_at = record.finishedAt
284
- error_msg = record.error
285
- end
286
- end
287
- end
288
-
289
- if not status then
290
- return nil
291
- end
292
-
293
- return cjson.encode({
294
- status = status,
295
- data = cjson.decode(job_data),
296
- finishedAt = finished_at,
297
- error = error_msg
298
- })
299
- `;
300
- var CLAIM_SCHEDULE_SCRIPT = `
301
- local schedules_index_key = KEYS[1]
302
- local schedule_key_prefix = KEYS[2]
303
- local now = tonumber(ARGV[1])
304
-
305
- local ids = redis.call('SMEMBERS', schedules_index_key)
306
-
307
- for i = 1, #ids do
308
- local schedule_key = schedule_key_prefix .. ids[i]
309
-
310
- -- Get schedule data
311
- local data = redis.call('HGETALL', schedule_key)
312
- if #data > 0 then
313
- -- Convert HGETALL result to table
314
- local schedule = {}
315
- for j = 1, #data, 2 do
316
- schedule[data[j]] = data[j + 1]
317
- end
318
-
319
- -- Check if schedule is due
320
- if schedule.status == 'active' then
321
- local next_run_at = tonumber(schedule.next_run_at)
322
-
323
- if next_run_at and next_run_at <= now then
324
- local run_count = tonumber(schedule.run_count or '0')
325
- local run_limit = schedule.run_limit and tonumber(schedule.run_limit) or nil
326
- local to_date = schedule.to_date and tonumber(schedule.to_date) or nil
327
-
328
- -- Check limits
329
- if not (run_limit and run_count >= run_limit) and not (to_date and now > to_date) then
330
- -- This schedule is claimable - atomically update it
331
- local new_run_count = run_count + 1
332
-
333
- -- Calculate new next_run_at (simple interval-based for now)
334
- -- Complex cron calculation happens in the caller
335
- local new_next_run_at = ''
336
- local every_ms = schedule.every_ms and tonumber(schedule.every_ms) or nil
337
- if every_ms then
338
- new_next_run_at = tostring(now + every_ms)
339
- end
340
-
341
- -- Check if we've hit the limit after this run
342
- if run_limit and new_run_count >= run_limit then
343
- new_next_run_at = ''
344
- end
345
-
346
- -- Check if past end date
347
- if to_date and new_next_run_at ~= '' and tonumber(new_next_run_at) > to_date then
348
- new_next_run_at = ''
349
- end
350
-
351
- -- Update the schedule atomically
352
- redis.call('HSET', schedule_key,
353
- 'next_run_at', new_next_run_at,
354
- 'last_run_at', tostring(now),
355
- 'run_count', tostring(new_run_count))
356
-
357
- -- Return the schedule data (before update) as JSON
358
- return cjson.encode(schedule)
359
- end
360
- end
361
- end
362
- end
363
- end
364
-
365
- return nil
366
- `;
367
31
  function redis(config) {
368
32
  return () => {
369
33
  if (config instanceof Redis) {
@@ -394,12 +58,19 @@ var RedisAdapter = class {
394
58
  pending: `${redisKey}::${queue}::pending`,
395
59
  delayed: `${redisKey}::${queue}::delayed`,
396
60
  active: `${redisKey}::${queue}::active`,
61
+ overlay: `${redisKey}::${queue}::metadata`,
397
62
  completed: `${redisKey}::${queue}::completed`,
398
63
  completedIndex: `${redisKey}::${queue}::completed::index`,
399
64
  failed: `${redisKey}::${queue}::failed`,
400
65
  failedIndex: `${redisKey}::${queue}::failed::index`
401
66
  };
402
67
  }
68
+ #getDedupKey(queue, dedupId) {
69
+ return `${this.#getDedupPrefix(queue)}${dedupId}`;
70
+ }
71
+ #getDedupPrefix(queue) {
72
+ return `${redisKey}::${queue}::dedup::`;
73
+ }
403
74
  setWorkerId(workerId) {
404
75
  this.#workerId = workerId;
405
76
  }
@@ -416,59 +87,83 @@ var RedisAdapter = class {
416
87
  const now = Date.now();
417
88
  const result = await this.#connection.eval(
418
89
  ACQUIRE_JOB_SCRIPT,
419
- 4,
90
+ 5,
420
91
  keys.data,
421
92
  keys.pending,
422
93
  keys.active,
423
94
  keys.delayed,
95
+ keys.overlay,
424
96
  this.#workerId,
425
97
  now.toString()
426
98
  );
427
99
  if (!result) {
428
100
  return null;
429
101
  }
430
- return JSON.parse(result);
102
+ const { data, overlay, acquiredAt } = JSON.parse(result);
103
+ return { ...hydrateRedisJob(data, overlay), acquiredAt };
431
104
  }
432
105
  async completeJob(jobId, queue, removeOnComplete) {
433
106
  const keys = this.#getKeys(queue);
107
+ const dedupPrefix = this.#getDedupPrefix(queue);
434
108
  const { keep, maxAge, maxCount } = resolveRetention(removeOnComplete);
435
109
  if (!keep) {
436
- await this.#connection.eval(REMOVE_JOB_SCRIPT, 2, keys.data, keys.active, jobId);
110
+ await this.#connection.eval(
111
+ REMOVE_JOB_SCRIPT,
112
+ 3,
113
+ keys.data,
114
+ keys.active,
115
+ keys.overlay,
116
+ jobId,
117
+ dedupPrefix
118
+ );
437
119
  return;
438
120
  }
439
121
  await this.#connection.eval(
440
122
  FINALIZE_JOB_SCRIPT,
441
- 4,
123
+ 5,
442
124
  keys.data,
443
125
  keys.active,
444
126
  keys.completed,
445
127
  keys.completedIndex,
128
+ keys.overlay,
446
129
  jobId,
447
130
  Date.now().toString(),
448
131
  maxAge.toString(),
449
132
  maxCount.toString(),
450
- ""
133
+ "",
134
+ dedupPrefix
451
135
  );
452
136
  }
453
137
  async failJob(jobId, queue, error, removeOnFail) {
454
138
  const keys = this.#getKeys(queue);
139
+ const dedupPrefix = this.#getDedupPrefix(queue);
455
140
  const { keep, maxAge, maxCount } = resolveRetention(removeOnFail);
456
141
  if (!keep) {
457
- await this.#connection.eval(REMOVE_JOB_SCRIPT, 2, keys.data, keys.active, jobId);
142
+ await this.#connection.eval(
143
+ REMOVE_JOB_SCRIPT,
144
+ 3,
145
+ keys.data,
146
+ keys.active,
147
+ keys.overlay,
148
+ jobId,
149
+ dedupPrefix
150
+ );
458
151
  return;
459
152
  }
460
153
  await this.#connection.eval(
461
154
  FINALIZE_JOB_SCRIPT,
462
- 4,
155
+ 5,
463
156
  keys.data,
464
157
  keys.active,
465
158
  keys.failed,
466
159
  keys.failedIndex,
160
+ keys.overlay,
467
161
  jobId,
468
162
  Date.now().toString(),
469
163
  maxAge.toString(),
470
164
  maxCount.toString(),
471
- error?.message || ""
165
+ error?.message || "",
166
+ dedupPrefix
472
167
  );
473
168
  }
474
169
  async retryJob(jobId, queue, retryAt) {
@@ -476,11 +171,12 @@ var RedisAdapter = class {
476
171
  const now = Date.now();
477
172
  await this.#connection.eval(
478
173
  RETRY_JOB_SCRIPT,
479
- 4,
174
+ 5,
480
175
  keys.data,
481
176
  keys.active,
482
177
  keys.pending,
483
178
  keys.delayed,
179
+ keys.overlay,
484
180
  jobId,
485
181
  retryAt ? retryAt.getTime().toString() : "0",
486
182
  now.toString()
@@ -490,19 +186,21 @@ var RedisAdapter = class {
490
186
  const keys = this.#getKeys(queue);
491
187
  const result = await this.#connection.eval(
492
188
  GET_JOB_SCRIPT,
493
- 6,
189
+ 7,
494
190
  keys.data,
495
191
  keys.pending,
496
192
  keys.delayed,
497
193
  keys.active,
498
194
  keys.completed,
499
195
  keys.failed,
196
+ keys.overlay,
500
197
  jobId
501
198
  );
502
199
  if (!result) {
503
200
  return null;
504
201
  }
505
- return JSON.parse(result);
202
+ const record = JSON.parse(result);
203
+ return { ...record, data: hydrateRedisJob(record.data, record.overlay) };
506
204
  }
507
205
  push(jobData) {
508
206
  return this.pushOn("default", jobData);
@@ -513,11 +211,34 @@ var RedisAdapter = class {
513
211
  async pushLaterOn(queue, jobData, delay) {
514
212
  const keys = this.#getKeys(queue);
515
213
  const executeAt = Date.now() + delay;
214
+ if (jobData.dedup) {
215
+ const dedupKey = this.#getDedupKey(queue, jobData.dedup.id);
216
+ const [payloadData, payloadIsUndefined] = encodeRedisJobPayloadOverlay(jobData.payload);
217
+ const result = await this.#connection.eval(
218
+ PUSH_DEDUP_JOB_SCRIPT,
219
+ 5,
220
+ keys.data,
221
+ keys.delayed,
222
+ dedupKey,
223
+ keys.pending,
224
+ keys.overlay,
225
+ jobData.id,
226
+ JSON.stringify(jobData),
227
+ executeAt.toString(),
228
+ (jobData.dedup.ttl ?? 0).toString(),
229
+ jobData.dedup.extend ? "1" : "0",
230
+ jobData.dedup.replace ? "1" : "0",
231
+ payloadData,
232
+ payloadIsUndefined
233
+ );
234
+ return { outcome: result[0], jobId: result[1] };
235
+ }
516
236
  await this.#connection.eval(
517
237
  PUSH_DELAYED_JOB_SCRIPT,
518
- 2,
238
+ 3,
519
239
  keys.data,
520
240
  keys.delayed,
241
+ keys.overlay,
521
242
  jobData.id,
522
243
  JSON.stringify(jobData),
523
244
  executeAt.toString()
@@ -528,11 +249,34 @@ var RedisAdapter = class {
528
249
  const priority = jobData.priority ?? DEFAULT_PRIORITY;
529
250
  const timestamp = Date.now();
530
251
  const score = calculateScore(priority, timestamp);
252
+ if (jobData.dedup) {
253
+ const dedupKey = this.#getDedupKey(queue, jobData.dedup.id);
254
+ const [payloadData, payloadIsUndefined] = encodeRedisJobPayloadOverlay(jobData.payload);
255
+ const result = await this.#connection.eval(
256
+ PUSH_DEDUP_JOB_SCRIPT,
257
+ 5,
258
+ keys.data,
259
+ keys.pending,
260
+ dedupKey,
261
+ keys.delayed,
262
+ keys.overlay,
263
+ jobData.id,
264
+ JSON.stringify(jobData),
265
+ score.toString(),
266
+ (jobData.dedup.ttl ?? 0).toString(),
267
+ jobData.dedup.extend ? "1" : "0",
268
+ jobData.dedup.replace ? "1" : "0",
269
+ payloadData,
270
+ payloadIsUndefined
271
+ );
272
+ return { outcome: result[0], jobId: result[1] };
273
+ }
531
274
  await this.#connection.eval(
532
275
  PUSH_JOB_SCRIPT,
533
- 2,
276
+ 3,
534
277
  keys.data,
535
278
  keys.pending,
279
+ keys.overlay,
536
280
  jobData.id,
537
281
  JSON.stringify(jobData),
538
282
  score.toString()
@@ -543,12 +287,16 @@ var RedisAdapter = class {
543
287
  }
544
288
  async pushManyOn(queue, jobs) {
545
289
  if (jobs.length === 0) return;
290
+ if (jobs.some((j) => j.dedup)) {
291
+ throw new Error("dedup is not supported in batch dispatch; use single dispatch");
292
+ }
546
293
  const keys = this.#getKeys(queue);
547
294
  const now = Date.now();
548
295
  const multi = this.#connection.multi();
549
296
  for (const job of jobs) {
550
297
  const priority = job.priority ?? DEFAULT_PRIORITY;
551
298
  const score = calculateScore(priority, now);
299
+ multi.hdel(keys.overlay, job.id);
552
300
  multi.hset(keys.data, job.id, JSON.stringify(job));
553
301
  multi.zadd(keys.pending, score, job.id);
554
302
  }
@@ -566,16 +314,34 @@ var RedisAdapter = class {
566
314
  const now = Date.now();
567
315
  const recovered = await this.#connection.eval(
568
316
  RECOVER_STALLED_JOBS_SCRIPT,
569
- 3,
317
+ 4,
570
318
  keys.data,
571
319
  keys.active,
572
320
  keys.pending,
321
+ keys.overlay,
573
322
  now.toString(),
574
323
  stalledThreshold.toString(),
575
- maxStalledCount.toString()
324
+ maxStalledCount.toString(),
325
+ this.#getDedupPrefix(queue)
576
326
  );
577
327
  return recovered;
578
328
  }
329
+ async renewJobs(queue, jobIds) {
330
+ if (jobIds.length === 0) {
331
+ return 0;
332
+ }
333
+ const keys = this.#getKeys(queue);
334
+ const now = Date.now();
335
+ const renewed = await this.#connection.eval(
336
+ RENEW_JOBS_SCRIPT,
337
+ 1,
338
+ keys.active,
339
+ now.toString(),
340
+ this.#workerId,
341
+ ...jobIds
342
+ );
343
+ return renewed;
344
+ }
579
345
  async upsertSchedule(config) {
580
346
  const id = config.id ?? randomUUID();
581
347
  const now = Date.now();