@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
@@ -0,0 +1,500 @@
1
+ import {
2
+ REDIS_DEDUP_LUA,
3
+ REDIS_JOB_STORAGE_LUA
4
+ } from "./chunk-6IO4P6RB.js";
5
+
6
+ // src/drivers/redis_scripts.ts
7
+ var PUSH_JOB_SCRIPT = `
8
+ local data_key = KEYS[1]
9
+ local pending_key = KEYS[2]
10
+ local overlay_key = KEYS[3]
11
+ local job_id = ARGV[1]
12
+ local job_data = ARGV[2]
13
+ local score = tonumber(ARGV[3])
14
+
15
+ ${REDIS_JOB_STORAGE_LUA}
16
+
17
+ store_job_data(data_key, overlay_key, job_id, job_data)
18
+ redis.call('ZADD', pending_key, score, job_id)
19
+
20
+ return 1
21
+ `;
22
+ var PUSH_DEDUP_JOB_SCRIPT = `
23
+ local data_key = KEYS[1]
24
+ local target_key = KEYS[2]
25
+ local dedup_key = KEYS[3]
26
+ local other_state_key = KEYS[4]
27
+ local overlay_key = KEYS[5]
28
+ local job_id = ARGV[1]
29
+ local job_data = ARGV[2]
30
+ local score = tonumber(ARGV[3])
31
+ local ttl = tonumber(ARGV[4])
32
+ local extend = tonumber(ARGV[5])
33
+ local replace = tonumber(ARGV[6])
34
+ local payload_data = ARGV[7]
35
+ local payload_is_undefined = tonumber(ARGV[8])
36
+
37
+ ${REDIS_JOB_STORAGE_LUA}
38
+ ${REDIS_DEDUP_LUA}
39
+
40
+ local existing_result = resolve_dedup_existing_job(
41
+ data_key,
42
+ target_key,
43
+ other_state_key,
44
+ overlay_key,
45
+ dedup_key,
46
+ extend,
47
+ replace,
48
+ payload_data,
49
+ payload_is_undefined
50
+ )
51
+ if existing_result then
52
+ return existing_result
53
+ end
54
+
55
+ store_job_data(data_key, overlay_key, job_id, job_data)
56
+ redis.call('ZADD', target_key, score, job_id)
57
+ redis.call('SET', dedup_key, job_id)
58
+ if ttl > 0 then
59
+ redis.call('PEXPIRE', dedup_key, ttl)
60
+ end
61
+ return {'added', job_id}
62
+ `;
63
+ var PUSH_DELAYED_JOB_SCRIPT = `
64
+ local data_key = KEYS[1]
65
+ local delayed_key = KEYS[2]
66
+ local overlay_key = KEYS[3]
67
+ local job_id = ARGV[1]
68
+ local job_data = ARGV[2]
69
+ local execute_at = tonumber(ARGV[3])
70
+
71
+ ${REDIS_JOB_STORAGE_LUA}
72
+
73
+ store_job_data(data_key, overlay_key, job_id, job_data)
74
+ redis.call('ZADD', delayed_key, execute_at, job_id)
75
+
76
+ return 1
77
+ `;
78
+ var ACQUIRE_JOB_SCRIPT = `
79
+ local data_key = KEYS[1]
80
+ local pending_key = KEYS[2]
81
+ local active_key = KEYS[3]
82
+ local delayed_key = KEYS[4]
83
+ local overlay_key = KEYS[5]
84
+ local worker_id = ARGV[1]
85
+ local now = tonumber(ARGV[2])
86
+
87
+ ${REDIS_JOB_STORAGE_LUA}
88
+
89
+ -- Process delayed jobs: move ready jobs to pending
90
+ local ready_job_ids = redis.call('ZRANGEBYSCORE', delayed_key, 0, now)
91
+ if #ready_job_ids > 0 then
92
+ for i = 1, #ready_job_ids do
93
+ local job_id = ready_job_ids[i]
94
+ local job_data = redis.call('HGET', data_key, job_id)
95
+ if job_data then
96
+ local job = cjson.decode(job_data)
97
+ local priority = job.priority or 5
98
+ local score = priority * 10000000000000 + now
99
+ redis.call('ZADD', pending_key, score, job_id)
100
+ redis.call('ZREM', delayed_key, job_id)
101
+ end
102
+ end
103
+ end
104
+
105
+ -- Pop highest priority job (lowest score)
106
+ local result = redis.call('ZPOPMIN', pending_key)
107
+ if not result or #result == 0 then
108
+ return nil
109
+ end
110
+
111
+ local job_id = result[1]
112
+ local job_data = redis.call('HGET', data_key, job_id)
113
+ if not job_data then
114
+ return nil
115
+ end
116
+
117
+ -- Store in active hash (without data, it's in data_key)
118
+ local active_data = cjson.encode({
119
+ workerId = worker_id,
120
+ acquiredAt = now
121
+ })
122
+ redis.call('HSET', active_key, job_id, active_data)
123
+
124
+ return encode_job_result(job_data, overlay_key, job_id, {
125
+ acquiredAt = now
126
+ })
127
+ `;
128
+ var REMOVE_JOB_SCRIPT = `
129
+ local data_key = KEYS[1]
130
+ local active_key = KEYS[2]
131
+ local overlay_key = KEYS[3]
132
+ local job_id = ARGV[1]
133
+ local dedup_prefix = ARGV[2]
134
+
135
+ ${REDIS_JOB_STORAGE_LUA}
136
+
137
+ if redis.call('HEXISTS', active_key, job_id) == 0 then
138
+ return 0
139
+ end
140
+
141
+ -- Read job data to extract dedup.id before deleting
142
+ local job_data = redis.call('HGET', data_key, job_id)
143
+ if job_data then
144
+ local ok, job = pcall(cjson.decode, job_data)
145
+ if ok and job and job.dedup and job.dedup.id then
146
+ local dkey = dedup_prefix .. job.dedup.id
147
+ if redis.call('GET', dkey) == job_id then
148
+ redis.call('DEL', dkey)
149
+ end
150
+ end
151
+ end
152
+
153
+ redis.call('HDEL', active_key, job_id)
154
+ delete_job_data(data_key, overlay_key, job_id)
155
+
156
+ return 1
157
+ `;
158
+ var FINALIZE_JOB_SCRIPT = `
159
+ local data_key = KEYS[1]
160
+ local active_key = KEYS[2]
161
+ local history_key = KEYS[3]
162
+ local index_key = KEYS[4]
163
+ local overlay_key = KEYS[5]
164
+ local job_id = ARGV[1]
165
+ local now = tonumber(ARGV[2])
166
+ local max_age = tonumber(ARGV[3])
167
+ local max_count = tonumber(ARGV[4])
168
+ local error_message = ARGV[5]
169
+ local dedup_prefix = ARGV[6]
170
+
171
+ ${REDIS_JOB_STORAGE_LUA}
172
+
173
+ -- Verify job is active
174
+ if redis.call('HEXISTS', active_key, job_id) == 0 then
175
+ return 0
176
+ end
177
+
178
+ -- Remove from active
179
+ redis.call('HDEL', active_key, job_id)
180
+
181
+ -- Store finalization info (data stays in data_key)
182
+ local record = {
183
+ finishedAt = now
184
+ }
185
+ if error_message and error_message ~= '' then
186
+ record.error = error_message
187
+ end
188
+ redis.call('HSET', history_key, job_id, cjson.encode(record))
189
+ redis.call('ZADD', index_key, now, job_id)
190
+
191
+ local function delete_dedup_for(ids)
192
+ for i = 1, #ids do
193
+ local id = ids[i]
194
+ local d = redis.call('HGET', data_key, id)
195
+ if d then
196
+ local ok, job = pcall(cjson.decode, d)
197
+ if ok and job and job.dedup and job.dedup.id then
198
+ local dkey = dedup_prefix .. job.dedup.id
199
+ if redis.call('GET', dkey) == id then
200
+ redis.call('DEL', dkey)
201
+ end
202
+ end
203
+ end
204
+ end
205
+ end
206
+
207
+ -- Prune by age
208
+ if max_age and max_age > 0 then
209
+ local cutoff = now - max_age
210
+ local expired = redis.call('ZRANGEBYSCORE', index_key, 0, cutoff)
211
+ if #expired > 0 then
212
+ delete_dedup_for(expired)
213
+ redis.call('ZREM', index_key, unpack(expired))
214
+ redis.call('HDEL', history_key, unpack(expired))
215
+ delete_jobs_data(data_key, overlay_key, expired)
216
+ end
217
+ end
218
+
219
+ -- Prune by count
220
+ if max_count and max_count > 0 then
221
+ local size = tonumber(redis.call('ZCARD', index_key))
222
+ if size > max_count then
223
+ local excess = size - max_count
224
+ local stale = redis.call('ZRANGE', index_key, 0, excess - 1)
225
+ if #stale > 0 then
226
+ delete_dedup_for(stale)
227
+ redis.call('ZREM', index_key, unpack(stale))
228
+ redis.call('HDEL', history_key, unpack(stale))
229
+ delete_jobs_data(data_key, overlay_key, stale)
230
+ end
231
+ end
232
+ end
233
+
234
+ return 1
235
+ `;
236
+ var RETRY_JOB_SCRIPT = `
237
+ local data_key = KEYS[1]
238
+ local active_key = KEYS[2]
239
+ local pending_key = KEYS[3]
240
+ local delayed_key = KEYS[4]
241
+ local overlay_key = KEYS[5]
242
+ local job_id = ARGV[1]
243
+ local retry_at = tonumber(ARGV[2])
244
+ local now = tonumber(ARGV[3])
245
+
246
+ ${REDIS_JOB_STORAGE_LUA}
247
+
248
+ -- Verify job is active
249
+ if redis.call('HEXISTS', active_key, job_id) == 0 then
250
+ return 0
251
+ end
252
+
253
+ -- Get job data
254
+ local job_data = redis.call('HGET', data_key, job_id)
255
+ if not job_data then
256
+ return 0
257
+ end
258
+
259
+ -- Remove from active
260
+ redis.call('HDEL', active_key, job_id)
261
+
262
+ -- Increment attempts without rewriting opaque job JSON.
263
+ local job = cjson.decode(job_data)
264
+ local overlay = read_job_overlay(overlay_key, job_id)
265
+ overlay.attempts = (overlay.attempts or job.attempts or 0) + 1
266
+ write_job_overlay(overlay_key, job_id, overlay)
267
+
268
+ -- Add back to pending or delayed
269
+ if retry_at and retry_at > now then
270
+ redis.call('ZADD', delayed_key, retry_at, job_id)
271
+ else
272
+ -- Score = priority * 1e13 + timestamp
273
+ -- Lower score = higher priority, FIFO within same priority
274
+ local priority = job.priority or 5
275
+ local score = priority * 10000000000000 + now
276
+ redis.call('ZADD', pending_key, score, job_id)
277
+ end
278
+
279
+ return 1
280
+ `;
281
+ var RECOVER_STALLED_JOBS_SCRIPT = `
282
+ local data_key = KEYS[1]
283
+ local active_key = KEYS[2]
284
+ local pending_key = KEYS[3]
285
+ local overlay_key = KEYS[4]
286
+ local now = tonumber(ARGV[1])
287
+ local stalled_threshold = tonumber(ARGV[2])
288
+ local max_stalled_count = tonumber(ARGV[3])
289
+ local dedup_prefix = ARGV[4]
290
+
291
+ ${REDIS_JOB_STORAGE_LUA}
292
+
293
+ local recovered = 0
294
+ local stalled_cutoff = now - stalled_threshold
295
+
296
+ -- Get all active jobs
297
+ local active_jobs = redis.call('HGETALL', active_key)
298
+
299
+ -- HGETALL returns [field1, value1, field2, value2, ...]
300
+ for i = 1, #active_jobs, 2 do
301
+ local job_id = active_jobs[i]
302
+ local active_data = active_jobs[i + 1]
303
+ local active = cjson.decode(active_data)
304
+
305
+ -- Check if job is stalled
306
+ if active.acquiredAt < stalled_cutoff then
307
+ local job_data = redis.call('HGET', data_key, job_id)
308
+ if job_data then
309
+ local job = cjson.decode(job_data)
310
+ local overlay = read_job_overlay(overlay_key, job_id)
311
+ local current_stalled_count = overlay.stalledCount or job.stalledCount or 0
312
+
313
+ -- Remove from active hash
314
+ redis.call('HDEL', active_key, job_id)
315
+
316
+ -- Check if job has exceeded max stalled count
317
+ if current_stalled_count >= max_stalled_count then
318
+ -- Job failed permanently, remove data + dedup key (only if pointer still ours)
319
+ if job.dedup and job.dedup.id then
320
+ local dkey = dedup_prefix .. job.dedup.id
321
+ if redis.call('GET', dkey) == job_id then
322
+ redis.call('DEL', dkey)
323
+ end
324
+ end
325
+ delete_job_data(data_key, overlay_key, job_id)
326
+ else
327
+ -- Recover: increment stalledCount without rewriting opaque job JSON.
328
+ overlay.stalledCount = current_stalled_count + 1
329
+ write_job_overlay(overlay_key, job_id, overlay)
330
+ -- Score = priority * 1e13 + timestamp
331
+ local priority = job.priority or 5
332
+ local score = priority * 10000000000000 + now
333
+ redis.call('ZADD', pending_key, score, job_id)
334
+ recovered = recovered + 1
335
+ end
336
+ end
337
+ end
338
+ end
339
+
340
+ return recovered
341
+ `;
342
+ var RENEW_JOBS_SCRIPT = `
343
+ local active_key = KEYS[1]
344
+ local now = tonumber(ARGV[1])
345
+ local worker_id = ARGV[2]
346
+
347
+ local renewed = 0
348
+ for i = 3, #ARGV do
349
+ local job_id = ARGV[i]
350
+ local active_data = redis.call('HGET', active_key, job_id)
351
+ if active_data then
352
+ local active = cjson.decode(active_data)
353
+ -- Only the worker that currently owns the lease may renew it.
354
+ if active.workerId == worker_id then
355
+ active.acquiredAt = now
356
+ redis.call('HSET', active_key, job_id, cjson.encode(active))
357
+ renewed = renewed + 1
358
+ end
359
+ end
360
+ end
361
+
362
+ return renewed
363
+ `;
364
+ var GET_JOB_SCRIPT = `
365
+ local data_key = KEYS[1]
366
+ local pending_key = KEYS[2]
367
+ local delayed_key = KEYS[3]
368
+ local active_key = KEYS[4]
369
+ local completed_key = KEYS[5]
370
+ local failed_key = KEYS[6]
371
+ local overlay_key = KEYS[7]
372
+ local job_id = ARGV[1]
373
+
374
+ ${REDIS_JOB_STORAGE_LUA}
375
+
376
+ local job_data = redis.call('HGET', data_key, job_id)
377
+ if not job_data then
378
+ return nil
379
+ end
380
+
381
+ local status = nil
382
+ local finished_at = nil
383
+ local error_msg = nil
384
+
385
+ -- Check status in order
386
+ if redis.call('HEXISTS', active_key, job_id) == 1 then
387
+ status = 'active'
388
+ elseif redis.call('ZSCORE', pending_key, job_id) then
389
+ status = 'pending'
390
+ elseif redis.call('ZSCORE', delayed_key, job_id) then
391
+ status = 'delayed'
392
+ else
393
+ local completed_data = redis.call('HGET', completed_key, job_id)
394
+ if completed_data then
395
+ status = 'completed'
396
+ local record = cjson.decode(completed_data)
397
+ finished_at = record.finishedAt
398
+ else
399
+ local failed_data = redis.call('HGET', failed_key, job_id)
400
+ if failed_data then
401
+ status = 'failed'
402
+ local record = cjson.decode(failed_data)
403
+ finished_at = record.finishedAt
404
+ error_msg = record.error
405
+ end
406
+ end
407
+ end
408
+
409
+ if not status then
410
+ return nil
411
+ end
412
+
413
+ return encode_job_result(job_data, overlay_key, job_id, {
414
+ status = status,
415
+ finishedAt = finished_at,
416
+ error = error_msg
417
+ })
418
+ `;
419
+ var CLAIM_SCHEDULE_SCRIPT = `
420
+ local schedules_index_key = KEYS[1]
421
+ local schedule_key_prefix = KEYS[2]
422
+ local now = tonumber(ARGV[1])
423
+
424
+ local ids = redis.call('SMEMBERS', schedules_index_key)
425
+
426
+ for i = 1, #ids do
427
+ local schedule_key = schedule_key_prefix .. ids[i]
428
+
429
+ -- Get schedule data
430
+ local data = redis.call('HGETALL', schedule_key)
431
+ if #data > 0 then
432
+ -- Convert HGETALL result to table
433
+ local schedule = {}
434
+ for j = 1, #data, 2 do
435
+ schedule[data[j]] = data[j + 1]
436
+ end
437
+
438
+ -- Check if schedule is due
439
+ if schedule.status == 'active' then
440
+ local next_run_at = tonumber(schedule.next_run_at)
441
+
442
+ if next_run_at and next_run_at <= now then
443
+ local run_count = tonumber(schedule.run_count or '0')
444
+ local run_limit = schedule.run_limit and tonumber(schedule.run_limit) or nil
445
+ local to_date = schedule.to_date and tonumber(schedule.to_date) or nil
446
+
447
+ -- Check limits
448
+ if not (run_limit and run_count >= run_limit) and not (to_date and now > to_date) then
449
+ -- This schedule is claimable - atomically update it
450
+ local new_run_count = run_count + 1
451
+
452
+ -- Calculate new next_run_at (simple interval-based for now)
453
+ -- Complex cron calculation happens in the caller
454
+ local new_next_run_at = ''
455
+ local every_ms = schedule.every_ms and tonumber(schedule.every_ms) or nil
456
+ if every_ms then
457
+ new_next_run_at = tostring(now + every_ms)
458
+ end
459
+
460
+ -- Check if we've hit the limit after this run
461
+ if run_limit and new_run_count >= run_limit then
462
+ new_next_run_at = ''
463
+ end
464
+
465
+ -- Check if past end date
466
+ if to_date and new_next_run_at ~= '' and tonumber(new_next_run_at) > to_date then
467
+ new_next_run_at = ''
468
+ end
469
+
470
+ -- Update the schedule atomically
471
+ redis.call('HSET', schedule_key,
472
+ 'next_run_at', new_next_run_at,
473
+ 'last_run_at', tostring(now),
474
+ 'run_count', tostring(new_run_count))
475
+
476
+ -- Return the schedule data (before update) as JSON
477
+ return cjson.encode(schedule)
478
+ end
479
+ end
480
+ end
481
+ end
482
+ end
483
+
484
+ return nil
485
+ `;
486
+
487
+ export {
488
+ PUSH_JOB_SCRIPT,
489
+ PUSH_DEDUP_JOB_SCRIPT,
490
+ PUSH_DELAYED_JOB_SCRIPT,
491
+ ACQUIRE_JOB_SCRIPT,
492
+ REMOVE_JOB_SCRIPT,
493
+ FINALIZE_JOB_SCRIPT,
494
+ RETRY_JOB_SCRIPT,
495
+ RECOVER_STALLED_JOBS_SCRIPT,
496
+ RENEW_JOBS_SCRIPT,
497
+ GET_JOB_SCRIPT,
498
+ CLAIM_SCHEDULE_SCRIPT
499
+ };
500
+ //# sourceMappingURL=chunk-S37X3CBO.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/drivers/redis_scripts.ts"],"sourcesContent":["import { REDIS_DEDUP_LUA, REDIS_JOB_STORAGE_LUA } from './redis_job_storage.js'\n\n/**\n * Lua script for pushing a job to the queue.\n * Stores job data in the central hash and adds jobId to pending ZSET.\n */\nexport const PUSH_JOB_SCRIPT = `\n local data_key = KEYS[1]\n local pending_key = KEYS[2]\n local overlay_key = KEYS[3]\n local job_id = ARGV[1]\n local job_data = ARGV[2]\n local score = tonumber(ARGV[3])\n\n${REDIS_JOB_STORAGE_LUA}\n\n store_job_data(data_key, overlay_key, job_id, job_data)\n redis.call('ZADD', pending_key, score, job_id)\n\n return 1\n`\n\n/**\n * Lua script for pushing a dedup job.\n *\n * Behavior:\n * - If dedup key exists AND job still exists AND within TTL: apply replace/extend, skip insert.\n * - If dedup key exists but job data missing (orphan): proceed to insert new.\n * - If TTL expired or no prior entry: insert new job, record dedup key with TTL.\n *\n * Replace only applies to jobs in pending or delayed state. Active and\n * retained completed/failed jobs are left untouched (returns 'skipped').\n * Replace swaps the payload only — priority/queue/delay/groupId/dedup\n * options of the existing job are preserved.\n *\n * Extend uses the ORIGINAL ttl recorded on the existing job (stored in\n * its dedup field), not the ttl arg of the current dispatch. Matches\n * Knex/Fake behavior: extend resets the clock but never changes the\n * window length.\n *\n * Returns {outcome, job_id}: outcome ∈ 'added' | 'skipped' | 'replaced' | 'extended'.\n */\nexport const PUSH_DEDUP_JOB_SCRIPT = `\n local data_key = KEYS[1]\n local target_key = KEYS[2]\n local dedup_key = KEYS[3]\n local other_state_key = KEYS[4]\n local overlay_key = KEYS[5]\n local job_id = ARGV[1]\n local job_data = ARGV[2]\n local score = tonumber(ARGV[3])\n local ttl = tonumber(ARGV[4])\n local extend = tonumber(ARGV[5])\n local replace = tonumber(ARGV[6])\n local payload_data = ARGV[7]\n local payload_is_undefined = tonumber(ARGV[8])\n\n${REDIS_JOB_STORAGE_LUA}\n${REDIS_DEDUP_LUA}\n\n local existing_result = resolve_dedup_existing_job(\n data_key,\n target_key,\n other_state_key,\n overlay_key,\n dedup_key,\n extend,\n replace,\n payload_data,\n payload_is_undefined\n )\n if existing_result then\n return existing_result\n end\n\n store_job_data(data_key, overlay_key, job_id, job_data)\n redis.call('ZADD', target_key, score, job_id)\n redis.call('SET', dedup_key, job_id)\n if ttl > 0 then\n redis.call('PEXPIRE', dedup_key, ttl)\n end\n return {'added', job_id}\n`\n\n/**\n * Lua script for pushing a delayed job.\n * Stores job data in the central hash and adds jobId to delayed ZSET.\n */\nexport const PUSH_DELAYED_JOB_SCRIPT = `\n local data_key = KEYS[1]\n local delayed_key = KEYS[2]\n local overlay_key = KEYS[3]\n local job_id = ARGV[1]\n local job_data = ARGV[2]\n local execute_at = tonumber(ARGV[3])\n\n${REDIS_JOB_STORAGE_LUA}\n\n store_job_data(data_key, overlay_key, job_id, job_data)\n redis.call('ZADD', delayed_key, execute_at, job_id)\n\n return 1\n`\n\n/**\n * Lua script for atomic job acquisition.\n * 1. Check and process delayed jobs\n * 2. Pop from pending queue\n * 3. Add to active hash with worker info\n * 4. Return job data\n */\nexport const ACQUIRE_JOB_SCRIPT = `\n local data_key = KEYS[1]\n local pending_key = KEYS[2]\n local active_key = KEYS[3]\n local delayed_key = KEYS[4]\n local overlay_key = KEYS[5]\n local worker_id = ARGV[1]\n local now = tonumber(ARGV[2])\n\n${REDIS_JOB_STORAGE_LUA}\n\n -- Process delayed jobs: move ready jobs to pending\n local ready_job_ids = redis.call('ZRANGEBYSCORE', delayed_key, 0, now)\n if #ready_job_ids > 0 then\n for i = 1, #ready_job_ids do\n local job_id = ready_job_ids[i]\n local job_data = redis.call('HGET', data_key, job_id)\n if job_data then\n local job = cjson.decode(job_data)\n local priority = job.priority or 5\n local score = priority * 10000000000000 + now\n redis.call('ZADD', pending_key, score, job_id)\n redis.call('ZREM', delayed_key, job_id)\n end\n end\n end\n\n -- Pop highest priority job (lowest score)\n local result = redis.call('ZPOPMIN', pending_key)\n if not result or #result == 0 then\n return nil\n end\n\n local job_id = result[1]\n local job_data = redis.call('HGET', data_key, job_id)\n if not job_data then\n return nil\n end\n\n -- Store in active hash (without data, it's in data_key)\n local active_data = cjson.encode({\n workerId = worker_id,\n acquiredAt = now\n })\n redis.call('HSET', active_key, job_id, active_data)\n\n return encode_job_result(job_data, overlay_key, job_id, {\n acquiredAt = now\n })\n`\n\n/**\n * Lua script for removing a job completely (no history).\n * Also cleans up the dedup key if the job had dedup metadata.\n */\nexport const REMOVE_JOB_SCRIPT = `\n local data_key = KEYS[1]\n local active_key = KEYS[2]\n local overlay_key = KEYS[3]\n local job_id = ARGV[1]\n local dedup_prefix = ARGV[2]\n\n${REDIS_JOB_STORAGE_LUA}\n\n if redis.call('HEXISTS', active_key, job_id) == 0 then\n return 0\n end\n\n -- Read job data to extract dedup.id before deleting\n local job_data = redis.call('HGET', data_key, job_id)\n if job_data then\n local ok, job = pcall(cjson.decode, job_data)\n if ok and job and job.dedup and job.dedup.id then\n local dkey = dedup_prefix .. job.dedup.id\n if redis.call('GET', dkey) == job_id then\n redis.call('DEL', dkey)\n end\n end\n end\n\n redis.call('HDEL', active_key, job_id)\n delete_job_data(data_key, overlay_key, job_id)\n\n return 1\n`\n\n/**\n * Lua script for finalizing a job in history.\n * Removes from active, stores finalization info, and prunes old records.\n * When pruning removes job data, also deletes the associated dedup key.\n */\nexport const FINALIZE_JOB_SCRIPT = `\n local data_key = KEYS[1]\n local active_key = KEYS[2]\n local history_key = KEYS[3]\n local index_key = KEYS[4]\n local overlay_key = KEYS[5]\n local job_id = ARGV[1]\n local now = tonumber(ARGV[2])\n local max_age = tonumber(ARGV[3])\n local max_count = tonumber(ARGV[4])\n local error_message = ARGV[5]\n local dedup_prefix = ARGV[6]\n\n${REDIS_JOB_STORAGE_LUA}\n\n -- Verify job is active\n if redis.call('HEXISTS', active_key, job_id) == 0 then\n return 0\n end\n\n -- Remove from active\n redis.call('HDEL', active_key, job_id)\n\n -- Store finalization info (data stays in data_key)\n local record = {\n finishedAt = now\n }\n if error_message and error_message ~= '' then\n record.error = error_message\n end\n redis.call('HSET', history_key, job_id, cjson.encode(record))\n redis.call('ZADD', index_key, now, job_id)\n\n local function delete_dedup_for(ids)\n for i = 1, #ids do\n local id = ids[i]\n local d = redis.call('HGET', data_key, id)\n if d then\n local ok, job = pcall(cjson.decode, d)\n if ok and job and job.dedup and job.dedup.id then\n local dkey = dedup_prefix .. job.dedup.id\n if redis.call('GET', dkey) == id then\n redis.call('DEL', dkey)\n end\n end\n end\n end\n end\n\n -- Prune by age\n if max_age and max_age > 0 then\n local cutoff = now - max_age\n local expired = redis.call('ZRANGEBYSCORE', index_key, 0, cutoff)\n if #expired > 0 then\n delete_dedup_for(expired)\n redis.call('ZREM', index_key, unpack(expired))\n redis.call('HDEL', history_key, unpack(expired))\n delete_jobs_data(data_key, overlay_key, expired)\n end\n end\n\n -- Prune by count\n if max_count and max_count > 0 then\n local size = tonumber(redis.call('ZCARD', index_key))\n if size > max_count then\n local excess = size - max_count\n local stale = redis.call('ZRANGE', index_key, 0, excess - 1)\n if #stale > 0 then\n delete_dedup_for(stale)\n redis.call('ZREM', index_key, unpack(stale))\n redis.call('HDEL', history_key, unpack(stale))\n delete_jobs_data(data_key, overlay_key, stale)\n end\n end\n end\n\n return 1\n`\n\n/**\n * Lua script for retrying a job.\n * 1. Verify job is active\n * 2. Remove from active hash\n * 3. Increment attempts in data\n * 4. Add back to pending (or delayed if retryAt is set)\n */\nexport const RETRY_JOB_SCRIPT = `\n local data_key = KEYS[1]\n local active_key = KEYS[2]\n local pending_key = KEYS[3]\n local delayed_key = KEYS[4]\n local overlay_key = KEYS[5]\n local job_id = ARGV[1]\n local retry_at = tonumber(ARGV[2])\n local now = tonumber(ARGV[3])\n\n${REDIS_JOB_STORAGE_LUA}\n\n -- Verify job is active\n if redis.call('HEXISTS', active_key, job_id) == 0 then\n return 0\n end\n\n -- Get job data\n local job_data = redis.call('HGET', data_key, job_id)\n if not job_data then\n return 0\n end\n\n -- Remove from active\n redis.call('HDEL', active_key, job_id)\n\n -- Increment attempts without rewriting opaque job JSON.\n local job = cjson.decode(job_data)\n local overlay = read_job_overlay(overlay_key, job_id)\n overlay.attempts = (overlay.attempts or job.attempts or 0) + 1\n write_job_overlay(overlay_key, job_id, overlay)\n\n -- Add back to pending or delayed\n if retry_at and retry_at > now then\n redis.call('ZADD', delayed_key, retry_at, job_id)\n else\n -- Score = priority * 1e13 + timestamp\n -- Lower score = higher priority, FIFO within same priority\n local priority = job.priority or 5\n local score = priority * 10000000000000 + now\n redis.call('ZADD', pending_key, score, job_id)\n end\n\n return 1\n`\n\n/**\n * Lua script for recovering stalled jobs.\n * Scans the active hash for jobs that have been active too long.\n * - Jobs within maxStalledCount: move back to pending with incremented stalledCount\n * - Jobs exceeding maxStalledCount: remove permanently (fail)\n * Returns the number of recovered jobs (not including failed ones).\n */\nexport const RECOVER_STALLED_JOBS_SCRIPT = `\n local data_key = KEYS[1]\n local active_key = KEYS[2]\n local pending_key = KEYS[3]\n local overlay_key = KEYS[4]\n local now = tonumber(ARGV[1])\n local stalled_threshold = tonumber(ARGV[2])\n local max_stalled_count = tonumber(ARGV[3])\n local dedup_prefix = ARGV[4]\n\n${REDIS_JOB_STORAGE_LUA}\n\n local recovered = 0\n local stalled_cutoff = now - stalled_threshold\n\n -- Get all active jobs\n local active_jobs = redis.call('HGETALL', active_key)\n\n -- HGETALL returns [field1, value1, field2, value2, ...]\n for i = 1, #active_jobs, 2 do\n local job_id = active_jobs[i]\n local active_data = active_jobs[i + 1]\n local active = cjson.decode(active_data)\n\n -- Check if job is stalled\n if active.acquiredAt < stalled_cutoff then\n local job_data = redis.call('HGET', data_key, job_id)\n if job_data then\n local job = cjson.decode(job_data)\n local overlay = read_job_overlay(overlay_key, job_id)\n local current_stalled_count = overlay.stalledCount or job.stalledCount or 0\n\n -- Remove from active hash\n redis.call('HDEL', active_key, job_id)\n\n -- Check if job has exceeded max stalled count\n if current_stalled_count >= max_stalled_count then\n -- Job failed permanently, remove data + dedup key (only if pointer still ours)\n if job.dedup and job.dedup.id then\n local dkey = dedup_prefix .. job.dedup.id\n if redis.call('GET', dkey) == job_id then\n redis.call('DEL', dkey)\n end\n end\n delete_job_data(data_key, overlay_key, job_id)\n else\n -- Recover: increment stalledCount without rewriting opaque job JSON.\n overlay.stalledCount = current_stalled_count + 1\n write_job_overlay(overlay_key, job_id, overlay)\n -- Score = priority * 1e13 + timestamp\n local priority = job.priority or 5\n local score = priority * 10000000000000 + now\n redis.call('ZADD', pending_key, score, job_id)\n recovered = recovered + 1\n end\n end\n end\n end\n\n return recovered\n`\n\n/**\n * Lua script for renewing the acquired timestamp of in-flight jobs (heartbeat).\n * Only entries still present in the active hash AND still owned by the calling\n * worker are renewed, so a job that was already recovered, finalized, or\n * re-acquired by another worker is never resurrected by a late heartbeat.\n * Preserves the existing worker info, updating only acquiredAt.\n * Returns the number of jobs renewed.\n */\nexport const RENEW_JOBS_SCRIPT = `\n local active_key = KEYS[1]\n local now = tonumber(ARGV[1])\n local worker_id = ARGV[2]\n\n local renewed = 0\n for i = 3, #ARGV do\n local job_id = ARGV[i]\n local active_data = redis.call('HGET', active_key, job_id)\n if active_data then\n local active = cjson.decode(active_data)\n -- Only the worker that currently owns the lease may renew it.\n if active.workerId == worker_id then\n active.acquiredAt = now\n redis.call('HSET', active_key, job_id, cjson.encode(active))\n renewed = renewed + 1\n end\n end\n end\n\n return renewed\n`\n\n/**\n * Lua script for getting a job record with its status.\n */\nexport const GET_JOB_SCRIPT = `\n local data_key = KEYS[1]\n local pending_key = KEYS[2]\n local delayed_key = KEYS[3]\n local active_key = KEYS[4]\n local completed_key = KEYS[5]\n local failed_key = KEYS[6]\n local overlay_key = KEYS[7]\n local job_id = ARGV[1]\n\n${REDIS_JOB_STORAGE_LUA}\n\n local job_data = redis.call('HGET', data_key, job_id)\n if not job_data then\n return nil\n end\n\n local status = nil\n local finished_at = nil\n local error_msg = nil\n\n -- Check status in order\n if redis.call('HEXISTS', active_key, job_id) == 1 then\n status = 'active'\n elseif redis.call('ZSCORE', pending_key, job_id) then\n status = 'pending'\n elseif redis.call('ZSCORE', delayed_key, job_id) then\n status = 'delayed'\n else\n local completed_data = redis.call('HGET', completed_key, job_id)\n if completed_data then\n status = 'completed'\n local record = cjson.decode(completed_data)\n finished_at = record.finishedAt\n else\n local failed_data = redis.call('HGET', failed_key, job_id)\n if failed_data then\n status = 'failed'\n local record = cjson.decode(failed_data)\n finished_at = record.finishedAt\n error_msg = record.error\n end\n end\n end\n\n if not status then\n return nil\n end\n\n return encode_job_result(job_data, overlay_key, job_id, {\n status = status,\n finishedAt = finished_at,\n error = error_msg\n })\n`\n\n/**\n * Lua script for atomically claiming a due schedule.\n * Iterates the schedule index server-side and claims the first due schedule.\n * Returns the schedule data if claimed, nil otherwise.\n */\nexport const CLAIM_SCHEDULE_SCRIPT = `\n local schedules_index_key = KEYS[1]\n local schedule_key_prefix = KEYS[2]\n local now = tonumber(ARGV[1])\n\n local ids = redis.call('SMEMBERS', schedules_index_key)\n\n for i = 1, #ids do\n local schedule_key = schedule_key_prefix .. ids[i]\n\n -- Get schedule data\n local data = redis.call('HGETALL', schedule_key)\n if #data > 0 then\n -- Convert HGETALL result to table\n local schedule = {}\n for j = 1, #data, 2 do\n schedule[data[j]] = data[j + 1]\n end\n\n -- Check if schedule is due\n if schedule.status == 'active' then\n local next_run_at = tonumber(schedule.next_run_at)\n\n if next_run_at and next_run_at <= now then\n local run_count = tonumber(schedule.run_count or '0')\n local run_limit = schedule.run_limit and tonumber(schedule.run_limit) or nil\n local to_date = schedule.to_date and tonumber(schedule.to_date) or nil\n\n -- Check limits\n if not (run_limit and run_count >= run_limit) and not (to_date and now > to_date) then\n -- This schedule is claimable - atomically update it\n local new_run_count = run_count + 1\n\n -- Calculate new next_run_at (simple interval-based for now)\n -- Complex cron calculation happens in the caller\n local new_next_run_at = ''\n local every_ms = schedule.every_ms and tonumber(schedule.every_ms) or nil\n if every_ms then\n new_next_run_at = tostring(now + every_ms)\n end\n\n -- Check if we've hit the limit after this run\n if run_limit and new_run_count >= run_limit then\n new_next_run_at = ''\n end\n\n -- Check if past end date\n if to_date and new_next_run_at ~= '' and tonumber(new_next_run_at) > to_date then\n new_next_run_at = ''\n end\n\n -- Update the schedule atomically\n redis.call('HSET', schedule_key,\n 'next_run_at', new_next_run_at,\n 'last_run_at', tostring(now),\n 'run_count', tostring(new_run_count))\n\n -- Return the schedule data (before update) as JSON\n return cjson.encode(schedule)\n end\n end\n end\n end\n end\n\n return nil\n`\n"],"mappings":"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","names":[]}
package/build/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { Q as QueueManagerConfig, W as WorkerCycle, R as RetryConfig, g as JobOptions, h as QueueConfig, D as Duration, A as Adapter, i as JobFactory, L as Logger, b as AcquiredJob, j as Job, a as JobClass, e as ScheduleData, k as ScheduleStatus, f as ScheduleListOptions } from './job-Z5fBSzRX.js';
2
- export { m as JobBatchDispatcher, l as ScheduleBuilder, n as customBackoff, p as exponentialBackoff, q as fixedBackoff, o as linearBackoff } from './job-Z5fBSzRX.js';
1
+ import { Q as QueueManagerConfig, W as WorkerCycle, R as RetryConfig, g as JobOptions, h as QueueConfig, i as Duration, A as Adapter, j as JobFactory, L as Logger, b as AcquiredJob, k as Job, a as JobClass, e as ScheduleData, l as ScheduleStatus, f as ScheduleListOptions } from './job-C4oyCVxR.js';
2
+ export { m as JobBatchDispatcher, n as ScheduleBuilder, o as customBackoff, p as exponentialBackoff, q as fixedBackoff, r as linearBackoff } from './job-C4oyCVxR.js';
3
3
  import { FakeAdapter } from './src/drivers/fake_adapter.js';
4
4
  import { Knex } from 'knex';
5
5
  import * as _poppinss_utils_exception from '@poppinss/utils/exception';
@@ -513,6 +513,14 @@ declare class QueueSchemaService {
513
513
  * The optional callback allows adding custom columns.
514
514
  */
515
515
  createJobsTable(tableName?: string, extend?: (table: Knex.CreateTableBuilder) => void): Promise<void>;
516
+ /**
517
+ * Idempotent migration: adds dedup columns (dedup_id, dedup_at, dedup_ttl)
518
+ * and a (queue, dedup_id) index to an existing jobs table.
519
+ *
520
+ * Safe to run multiple times. Uses hasColumn checks so it won't fail on re-runs.
521
+ * For large Postgres tables, consider pausing workers during the run.
522
+ */
523
+ addDedupColumns(tableName?: string): Promise<void>;
516
524
  /**
517
525
  * Creates the schedules table with the default schema.
518
526
  * The optional callback allows adding custom columns.