@boringnode/queue 0.5.1 → 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 (37) hide show
  1. package/README.md +103 -20
  2. package/build/chunk-6IO4P6RB.js +145 -0
  3. package/build/chunk-6IO4P6RB.js.map +1 -0
  4. package/build/{chunk-VHN3XZDC.js → chunk-AHUVTAI7.js} +278 -29
  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 +34 -8
  9. package/build/index.js +187 -31
  10. package/build/index.js.map +1 -1
  11. package/build/{job-DImdhRFO.d.ts → job-C4oyCVxR.d.ts} +275 -15
  12. package/build/src/contracts/adapter.d.ts +1 -1
  13. package/build/src/drivers/fake_adapter.d.ts +12 -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 +166 -402
  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/otel.js +3 -0
  32. package/build/src/otel.js.map +1 -1
  33. package/build/src/types/index.d.ts +1 -1
  34. package/build/src/types/main.d.ts +1 -1
  35. package/build/src/types/tracing_channels.d.ts +7 -1
  36. package/package.json +18 -19
  37. package/build/chunk-VHN3XZDC.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,361 +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 schedule_key = KEYS[1]
302
- local now = tonumber(ARGV[1])
303
-
304
- -- Get schedule data
305
- local data = redis.call('HGETALL', schedule_key)
306
- if #data == 0 then
307
- return nil
308
- end
309
-
310
- -- Convert HGETALL result to table
311
- local schedule = {}
312
- for j = 1, #data, 2 do
313
- schedule[data[j]] = data[j + 1]
314
- end
315
-
316
- -- Check if schedule is due
317
- if schedule.status ~= 'active' then
318
- return nil
319
- end
320
-
321
- local next_run_at = tonumber(schedule.next_run_at)
322
- if not next_run_at or next_run_at > now then
323
- return nil
324
- end
325
-
326
- local run_count = tonumber(schedule.run_count or '0')
327
- local run_limit = schedule.run_limit and tonumber(schedule.run_limit) or nil
328
- local to_date = schedule.to_date and tonumber(schedule.to_date) or nil
329
-
330
- -- Check limits
331
- if run_limit and run_count >= run_limit then
332
- return nil
333
- end
334
-
335
- if to_date and now > to_date then
336
- return nil
337
- end
338
-
339
- -- This schedule is claimable - atomically update it
340
- local new_run_count = run_count + 1
341
-
342
- -- Calculate new next_run_at (simple interval-based for now)
343
- -- Complex cron calculation happens in the caller
344
- local new_next_run_at = ''
345
- local every_ms = schedule.every_ms and tonumber(schedule.every_ms) or nil
346
- if every_ms then
347
- new_next_run_at = tostring(now + every_ms)
348
- end
349
-
350
- -- Check if we've hit the limit after this run
351
- if run_limit and new_run_count >= run_limit then
352
- new_next_run_at = ''
353
- end
354
-
355
- -- Check if past end date
356
- if to_date and new_next_run_at ~= '' and tonumber(new_next_run_at) > to_date then
357
- new_next_run_at = ''
358
- end
359
-
360
- -- Update the schedule atomically
361
- redis.call('HSET', schedule_key,
362
- 'next_run_at', new_next_run_at,
363
- 'last_run_at', tostring(now),
364
- 'run_count', tostring(new_run_count))
365
-
366
- -- Return the schedule data (before update) as JSON
367
- return cjson.encode(schedule)
368
- `;
369
31
  function redis(config) {
370
32
  return () => {
371
33
  if (config instanceof Redis) {
@@ -396,12 +58,19 @@ var RedisAdapter = class {
396
58
  pending: `${redisKey}::${queue}::pending`,
397
59
  delayed: `${redisKey}::${queue}::delayed`,
398
60
  active: `${redisKey}::${queue}::active`,
61
+ overlay: `${redisKey}::${queue}::metadata`,
399
62
  completed: `${redisKey}::${queue}::completed`,
400
63
  completedIndex: `${redisKey}::${queue}::completed::index`,
401
64
  failed: `${redisKey}::${queue}::failed`,
402
65
  failedIndex: `${redisKey}::${queue}::failed::index`
403
66
  };
404
67
  }
68
+ #getDedupKey(queue, dedupId) {
69
+ return `${this.#getDedupPrefix(queue)}${dedupId}`;
70
+ }
71
+ #getDedupPrefix(queue) {
72
+ return `${redisKey}::${queue}::dedup::`;
73
+ }
405
74
  setWorkerId(workerId) {
406
75
  this.#workerId = workerId;
407
76
  }
@@ -418,59 +87,83 @@ var RedisAdapter = class {
418
87
  const now = Date.now();
419
88
  const result = await this.#connection.eval(
420
89
  ACQUIRE_JOB_SCRIPT,
421
- 4,
90
+ 5,
422
91
  keys.data,
423
92
  keys.pending,
424
93
  keys.active,
425
94
  keys.delayed,
95
+ keys.overlay,
426
96
  this.#workerId,
427
97
  now.toString()
428
98
  );
429
99
  if (!result) {
430
100
  return null;
431
101
  }
432
- return JSON.parse(result);
102
+ const { data, overlay, acquiredAt } = JSON.parse(result);
103
+ return { ...hydrateRedisJob(data, overlay), acquiredAt };
433
104
  }
434
105
  async completeJob(jobId, queue, removeOnComplete) {
435
106
  const keys = this.#getKeys(queue);
107
+ const dedupPrefix = this.#getDedupPrefix(queue);
436
108
  const { keep, maxAge, maxCount } = resolveRetention(removeOnComplete);
437
109
  if (!keep) {
438
- 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
+ );
439
119
  return;
440
120
  }
441
121
  await this.#connection.eval(
442
122
  FINALIZE_JOB_SCRIPT,
443
- 4,
123
+ 5,
444
124
  keys.data,
445
125
  keys.active,
446
126
  keys.completed,
447
127
  keys.completedIndex,
128
+ keys.overlay,
448
129
  jobId,
449
130
  Date.now().toString(),
450
131
  maxAge.toString(),
451
132
  maxCount.toString(),
452
- ""
133
+ "",
134
+ dedupPrefix
453
135
  );
454
136
  }
455
137
  async failJob(jobId, queue, error, removeOnFail) {
456
138
  const keys = this.#getKeys(queue);
139
+ const dedupPrefix = this.#getDedupPrefix(queue);
457
140
  const { keep, maxAge, maxCount } = resolveRetention(removeOnFail);
458
141
  if (!keep) {
459
- 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
+ );
460
151
  return;
461
152
  }
462
153
  await this.#connection.eval(
463
154
  FINALIZE_JOB_SCRIPT,
464
- 4,
155
+ 5,
465
156
  keys.data,
466
157
  keys.active,
467
158
  keys.failed,
468
159
  keys.failedIndex,
160
+ keys.overlay,
469
161
  jobId,
470
162
  Date.now().toString(),
471
163
  maxAge.toString(),
472
164
  maxCount.toString(),
473
- error?.message || ""
165
+ error?.message || "",
166
+ dedupPrefix
474
167
  );
475
168
  }
476
169
  async retryJob(jobId, queue, retryAt) {
@@ -478,11 +171,12 @@ var RedisAdapter = class {
478
171
  const now = Date.now();
479
172
  await this.#connection.eval(
480
173
  RETRY_JOB_SCRIPT,
481
- 4,
174
+ 5,
482
175
  keys.data,
483
176
  keys.active,
484
177
  keys.pending,
485
178
  keys.delayed,
179
+ keys.overlay,
486
180
  jobId,
487
181
  retryAt ? retryAt.getTime().toString() : "0",
488
182
  now.toString()
@@ -492,19 +186,21 @@ var RedisAdapter = class {
492
186
  const keys = this.#getKeys(queue);
493
187
  const result = await this.#connection.eval(
494
188
  GET_JOB_SCRIPT,
495
- 6,
189
+ 7,
496
190
  keys.data,
497
191
  keys.pending,
498
192
  keys.delayed,
499
193
  keys.active,
500
194
  keys.completed,
501
195
  keys.failed,
196
+ keys.overlay,
502
197
  jobId
503
198
  );
504
199
  if (!result) {
505
200
  return null;
506
201
  }
507
- return JSON.parse(result);
202
+ const record = JSON.parse(result);
203
+ return { ...record, data: hydrateRedisJob(record.data, record.overlay) };
508
204
  }
509
205
  push(jobData) {
510
206
  return this.pushOn("default", jobData);
@@ -515,11 +211,34 @@ var RedisAdapter = class {
515
211
  async pushLaterOn(queue, jobData, delay) {
516
212
  const keys = this.#getKeys(queue);
517
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
+ }
518
236
  await this.#connection.eval(
519
237
  PUSH_DELAYED_JOB_SCRIPT,
520
- 2,
238
+ 3,
521
239
  keys.data,
522
240
  keys.delayed,
241
+ keys.overlay,
523
242
  jobData.id,
524
243
  JSON.stringify(jobData),
525
244
  executeAt.toString()
@@ -530,11 +249,34 @@ var RedisAdapter = class {
530
249
  const priority = jobData.priority ?? DEFAULT_PRIORITY;
531
250
  const timestamp = Date.now();
532
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
+ }
533
274
  await this.#connection.eval(
534
275
  PUSH_JOB_SCRIPT,
535
- 2,
276
+ 3,
536
277
  keys.data,
537
278
  keys.pending,
279
+ keys.overlay,
538
280
  jobData.id,
539
281
  JSON.stringify(jobData),
540
282
  score.toString()
@@ -545,12 +287,16 @@ var RedisAdapter = class {
545
287
  }
546
288
  async pushManyOn(queue, jobs) {
547
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
+ }
548
293
  const keys = this.#getKeys(queue);
549
294
  const now = Date.now();
550
295
  const multi = this.#connection.multi();
551
296
  for (const job of jobs) {
552
297
  const priority = job.priority ?? DEFAULT_PRIORITY;
553
298
  const score = calculateScore(priority, now);
299
+ multi.hdel(keys.overlay, job.id);
554
300
  multi.hset(keys.data, job.id, JSON.stringify(job));
555
301
  multi.zadd(keys.pending, score, job.id);
556
302
  }
@@ -568,16 +314,34 @@ var RedisAdapter = class {
568
314
  const now = Date.now();
569
315
  const recovered = await this.#connection.eval(
570
316
  RECOVER_STALLED_JOBS_SCRIPT,
571
- 3,
317
+ 4,
572
318
  keys.data,
573
319
  keys.active,
574
320
  keys.pending,
321
+ keys.overlay,
575
322
  now.toString(),
576
323
  stalledThreshold.toString(),
577
- maxStalledCount.toString()
324
+ maxStalledCount.toString(),
325
+ this.#getDedupPrefix(queue)
578
326
  );
579
327
  return recovered;
580
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
+ }
581
345
  async upsertSchedule(config) {
582
346
  const id = config.id ?? randomUUID();
583
347
  const now = Date.now();
@@ -665,40 +429,40 @@ var RedisAdapter = class {
665
429
  }
666
430
  async claimDueSchedule() {
667
431
  const now = Date.now();
668
- const ids = await this.#connection.smembers(schedulesIndexKey);
669
- for (const id of ids) {
670
- const scheduleKey = `${schedulesKey}::${id}`;
671
- const result = await this.#connection.eval(
672
- CLAIM_SCHEDULE_SCRIPT,
673
- 1,
674
- scheduleKey,
675
- now.toString()
676
- );
677
- if (!result) {
678
- continue;
679
- }
680
- const data = JSON.parse(result);
681
- if (data.cron_expression) {
682
- const { CronExpressionParser } = await import("cron-parser");
683
- const cron = CronExpressionParser.parse(data.cron_expression, {
684
- currentDate: new Date(now),
685
- tz: data.timezone || "UTC"
686
- });
687
- const nextRun = cron.next().toDate().getTime();
688
- const runCount = Number.parseInt(data.run_count || "0", 10) + 1;
689
- const runLimit = data.run_limit ? Number.parseInt(data.run_limit, 10) : null;
690
- const toDate = data.to_date ? Number.parseInt(data.to_date, 10) : null;
691
- let newNextRunAt = nextRun;
692
- if (runLimit !== null && runCount >= runLimit) {
693
- newNextRunAt = "";
694
- } else if (toDate && nextRun > toDate) {
695
- newNextRunAt = "";
696
- }
697
- await this.#connection.hset(scheduleKey, "next_run_at", newNextRunAt.toString());
432
+ const result = await this.#connection.eval(
433
+ CLAIM_SCHEDULE_SCRIPT,
434
+ 2,
435
+ schedulesIndexKey,
436
+ `${schedulesKey}::`,
437
+ now.toString()
438
+ );
439
+ if (!result) {
440
+ return null;
441
+ }
442
+ const data = JSON.parse(result);
443
+ if (data.cron_expression) {
444
+ const { CronExpressionParser } = await import("cron-parser");
445
+ const cron = CronExpressionParser.parse(data.cron_expression, {
446
+ currentDate: new Date(now),
447
+ tz: data.timezone || "UTC"
448
+ });
449
+ const nextRun = cron.next().toDate().getTime();
450
+ const runCount = Number.parseInt(data.run_count || "0", 10) + 1;
451
+ const runLimit = data.run_limit ? Number.parseInt(data.run_limit, 10) : null;
452
+ const toDate = data.to_date ? Number.parseInt(data.to_date, 10) : null;
453
+ let newNextRunAt = nextRun;
454
+ if (runLimit !== null && runCount >= runLimit) {
455
+ newNextRunAt = "";
456
+ } else if (toDate && nextRun > toDate) {
457
+ newNextRunAt = "";
698
458
  }
699
- return this.#hashToScheduleData(data);
459
+ await this.#connection.hset(
460
+ `${schedulesKey}::${data.id}`,
461
+ "next_run_at",
462
+ newNextRunAt.toString()
463
+ );
700
464
  }
701
- return null;
465
+ return this.#hashToScheduleData(data);
702
466
  }
703
467
  #hashToScheduleData(data) {
704
468
  return {