queue-jobs-worker 1.0.2 → 1.0.3

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
@@ -75,7 +75,7 @@ function hashToJob(h) {
75
75
  if (h["cron"]) job.cron = h["cron"];
76
76
  return job;
77
77
  }
78
- var PREFIX, k, CLAIM_LUA, RECOVER_STALLED_LUA, RedisStorageAdapter;
78
+ var PREFIX, k, CLAIM_LUA, RECOVER_STALLED_LUA, RENEW_LOCK_LUA, RedisStorageAdapter;
79
79
  var init_redis_adapter = __esm({
80
80
  "src/storage/redis.adapter.ts"() {
81
81
  PREFIX = "qjw:";
@@ -159,6 +159,24 @@ redis.call('HSET', job_key,
159
159
  redis.call('SREM', active_set, job_id)
160
160
  redis.call('ZADD', waiting_zset, priority_score, job_id)
161
161
  return 1
162
+ `;
163
+ RENEW_LOCK_LUA = `
164
+ local job_key = KEYS[1]
165
+ local lock_id = ARGV[1]
166
+ local new_exp = ARGV[2]
167
+ local now_iso = ARGV[3]
168
+
169
+ local fields = redis.call('HMGET', job_key, 'status', 'lockId', 'lockExpiresAt')
170
+ local status = fields[1] or ''
171
+ local current_lock = fields[2] or ''
172
+ local current_exp = fields[3] or ''
173
+
174
+ if status ~= 'active' then return 0 end
175
+ if current_lock ~= lock_id then return 0 end
176
+ if current_exp == '' or current_exp <= now_iso then return 0 end
177
+
178
+ redis.call('HSET', job_key, 'lockExpiresAt', new_exp, 'updatedAt', now_iso)
179
+ return 1
162
180
  `;
163
181
  RedisStorageAdapter = class {
164
182
  client;
@@ -249,12 +267,28 @@ return 1
249
267
  return hashToJob(hash);
250
268
  }
251
269
  // -------------------------------------------------------------------------
270
+ // Renew lock
271
+ // -------------------------------------------------------------------------
272
+ async renewLock(jobId, lockId, lockDuration) {
273
+ const nowMs = Date.now();
274
+ const nowIso = new Date(nowMs).toISOString();
275
+ const newExpiresAt = new Date(nowMs + lockDuration).toISOString();
276
+ const res = await this.client.eval(RENEW_LOCK_LUA, {
277
+ keys: [k.job(jobId)],
278
+ arguments: [lockId, newExpiresAt, nowIso]
279
+ });
280
+ return Number(res) === 1;
281
+ }
282
+ // -------------------------------------------------------------------------
252
283
  // Complete
253
284
  // -------------------------------------------------------------------------
254
- async complete(jobId) {
285
+ async complete(jobId, lockId) {
255
286
  const now = (/* @__PURE__ */ new Date()).toISOString();
256
287
  const hash = await this.client.hGetAll(k.job(jobId));
257
- if (!hash) return;
288
+ if (!hash || Object.keys(hash).length === 0) return;
289
+ if (lockId !== void 0) {
290
+ if (hash["status"] !== "active" || hash["lockId"] !== lockId) return;
291
+ }
258
292
  const queue = hash["queue"] ?? "";
259
293
  const multi = this.client.multi();
260
294
  multi.hSet(k.job(jobId), {
@@ -274,7 +308,10 @@ return 1
274
308
  async requeue(input) {
275
309
  const now = (/* @__PURE__ */ new Date()).toISOString();
276
310
  const hash = await this.client.hGetAll(k.job(input.jobId));
277
- if (!hash) return;
311
+ if (!hash || Object.keys(hash).length === 0) return;
312
+ if (input.lockId !== void 0) {
313
+ if (hash["status"] !== "active" || hash["lockId"] !== input.lockId) return;
314
+ }
278
315
  const queue = hash["queue"] ?? "";
279
316
  const attempts = JSON.parse(hash["attempts"] ?? "[]");
280
317
  attempts.push({
@@ -307,7 +344,10 @@ return 1
307
344
  async moveToDlq(input) {
308
345
  const now = (/* @__PURE__ */ new Date()).toISOString();
309
346
  const hash = await this.client.hGetAll(k.job(input.jobId));
310
- if (!hash) return;
347
+ if (!hash || Object.keys(hash).length === 0) return;
348
+ if (input.lockId !== void 0) {
349
+ if (hash["status"] !== "active" || hash["lockId"] !== input.lockId) return;
350
+ }
311
351
  const queue = hash["queue"] ?? "";
312
352
  const attempts = JSON.parse(hash["attempts"] ?? "[]");
313
353
  attempts.push({
@@ -334,7 +374,12 @@ return 1
334
374
  // -------------------------------------------------------------------------
335
375
  // Release lock
336
376
  // -------------------------------------------------------------------------
337
- async releaseLock(jobId) {
377
+ async releaseLock(jobId, lockId) {
378
+ const hash = await this.client.hGetAll(k.job(jobId));
379
+ if (!hash || Object.keys(hash).length === 0) return;
380
+ if (lockId !== void 0) {
381
+ if (hash["status"] !== "active" || hash["lockId"] !== lockId) return;
382
+ }
338
383
  const now = (/* @__PURE__ */ new Date()).toISOString();
339
384
  await this.client.hSet(k.job(jobId), {
340
385
  lockId: "",
@@ -661,16 +706,40 @@ CREATE TABLE IF NOT EXISTS qjw_rate_limits (
661
706
  }
662
707
  }
663
708
  // -------------------------------------------------------------------------
664
- // Complete
709
+ // Renew lock
665
710
  // -------------------------------------------------------------------------
666
- async complete(jobId) {
667
- await this.pool.query(
711
+ async renewLock(jobId, lockId, lockDuration) {
712
+ const newLockExpiresAt = new Date(Date.now() + lockDuration).toISOString();
713
+ const res = await this.pool.query(
668
714
  `UPDATE qjw_jobs
669
- SET status = 'completed', lock_id = NULL, lock_expires_at = NULL,
670
- completed_at = NOW(), updated_at = NOW()
671
- WHERE id = $1`,
672
- [jobId]
715
+ SET lock_expires_at = $1::timestamptz,
716
+ updated_at = NOW()
717
+ WHERE id = $2 AND status = 'active' AND lock_id = $3 AND lock_expires_at > NOW()`,
718
+ [newLockExpiresAt, jobId, lockId]
673
719
  );
720
+ return (res.rowCount ?? 0) > 0;
721
+ }
722
+ // -------------------------------------------------------------------------
723
+ // Complete
724
+ // -------------------------------------------------------------------------
725
+ async complete(jobId, lockId) {
726
+ if (lockId !== void 0) {
727
+ await this.pool.query(
728
+ `UPDATE qjw_jobs
729
+ SET status = 'completed', lock_id = NULL, lock_expires_at = NULL,
730
+ completed_at = NOW(), updated_at = NOW()
731
+ WHERE id = $1 AND status = 'active' AND lock_id = $2`,
732
+ [jobId, lockId]
733
+ );
734
+ } else {
735
+ await this.pool.query(
736
+ `UPDATE qjw_jobs
737
+ SET status = 'completed', lock_id = NULL, lock_expires_at = NULL,
738
+ completed_at = NOW(), updated_at = NOW()
739
+ WHERE id = $1`,
740
+ [jobId]
741
+ );
742
+ }
674
743
  }
675
744
  // -------------------------------------------------------------------------
676
745
  // Requeue
@@ -684,18 +753,33 @@ CREATE TABLE IF NOT EXISTS qjw_rate_limits (
684
753
  error: input.error,
685
754
  ...input.stack !== void 0 ? { stack: input.stack } : {}
686
755
  };
687
- await this.pool.query(
688
- `UPDATE qjw_jobs
689
- SET status = 'waiting',
690
- attempts_made = attempts_made + 1,
691
- attempts = attempts || $1::jsonb,
692
- run_at = $2::timestamptz,
693
- lock_id = NULL,
694
- lock_expires_at = NULL,
695
- updated_at = NOW()
696
- WHERE id = $3`,
697
- [JSON.stringify([attempt]), input.runAt, input.jobId]
698
- );
756
+ if (input.lockId !== void 0) {
757
+ await this.pool.query(
758
+ `UPDATE qjw_jobs
759
+ SET status = 'waiting',
760
+ attempts_made = $1,
761
+ attempts = attempts || $2::jsonb,
762
+ run_at = $3::timestamptz,
763
+ lock_id = NULL,
764
+ lock_expires_at = NULL,
765
+ updated_at = NOW()
766
+ WHERE id = $4 AND status = 'active' AND lock_id = $5`,
767
+ [input.attemptNumber, JSON.stringify([attempt]), input.runAt, input.jobId, input.lockId]
768
+ );
769
+ } else {
770
+ await this.pool.query(
771
+ `UPDATE qjw_jobs
772
+ SET status = 'waiting',
773
+ attempts_made = $1,
774
+ attempts = attempts || $2::jsonb,
775
+ run_at = $3::timestamptz,
776
+ lock_id = NULL,
777
+ lock_expires_at = NULL,
778
+ updated_at = NOW()
779
+ WHERE id = $4`,
780
+ [input.attemptNumber, JSON.stringify([attempt]), input.runAt, input.jobId]
781
+ );
782
+ }
699
783
  }
700
784
  // -------------------------------------------------------------------------
701
785
  // Move to DLQ
@@ -709,29 +793,53 @@ CREATE TABLE IF NOT EXISTS qjw_rate_limits (
709
793
  error: input.error,
710
794
  ...input.stack !== void 0 ? { stack: input.stack } : {}
711
795
  };
712
- await this.pool.query(
713
- `UPDATE qjw_jobs
714
- SET status = 'dead',
715
- attempts_made = attempts_made + 1,
716
- attempts = attempts || $1::jsonb,
717
- lock_id = NULL,
718
- lock_expires_at = NULL,
719
- failed_at = NOW(),
720
- updated_at = NOW()
721
- WHERE id = $2`,
722
- [JSON.stringify([attempt]), input.jobId]
723
- );
796
+ if (input.lockId !== void 0) {
797
+ await this.pool.query(
798
+ `UPDATE qjw_jobs
799
+ SET status = 'dead',
800
+ attempts_made = $1,
801
+ attempts = attempts || $2::jsonb,
802
+ lock_id = NULL,
803
+ lock_expires_at = NULL,
804
+ failed_at = NOW(),
805
+ updated_at = NOW()
806
+ WHERE id = $3 AND status = 'active' AND lock_id = $4`,
807
+ [input.attemptNumber, JSON.stringify([attempt]), input.jobId, input.lockId]
808
+ );
809
+ } else {
810
+ await this.pool.query(
811
+ `UPDATE qjw_jobs
812
+ SET status = 'dead',
813
+ attempts_made = $1,
814
+ attempts = attempts || $2::jsonb,
815
+ lock_id = NULL,
816
+ lock_expires_at = NULL,
817
+ failed_at = NOW(),
818
+ updated_at = NOW()
819
+ WHERE id = $3`,
820
+ [input.attemptNumber, JSON.stringify([attempt]), input.jobId]
821
+ );
822
+ }
724
823
  }
725
824
  // -------------------------------------------------------------------------
726
825
  // Release lock
727
826
  // -------------------------------------------------------------------------
728
- async releaseLock(jobId) {
729
- await this.pool.query(
730
- `UPDATE qjw_jobs
731
- SET lock_id = NULL, lock_expires_at = NOW(), updated_at = NOW()
732
- WHERE id = $1`,
733
- [jobId]
734
- );
827
+ async releaseLock(jobId, lockId) {
828
+ if (lockId !== void 0) {
829
+ await this.pool.query(
830
+ `UPDATE qjw_jobs
831
+ SET lock_id = NULL, lock_expires_at = NOW(), updated_at = NOW()
832
+ WHERE id = $1 AND status = 'active' AND lock_id = $2`,
833
+ [jobId, lockId]
834
+ );
835
+ } else {
836
+ await this.pool.query(
837
+ `UPDATE qjw_jobs
838
+ SET lock_id = NULL, lock_expires_at = NOW(), updated_at = NOW()
839
+ WHERE id = $1`,
840
+ [jobId]
841
+ );
842
+ }
735
843
  }
736
844
  // -------------------------------------------------------------------------
737
845
  // Recover stalled jobs
@@ -1012,16 +1120,39 @@ CREATE TABLE IF NOT EXISTS qjw_rate_limits (
1012
1120
  }
1013
1121
  }
1014
1122
  // -------------------------------------------------------------------------
1015
- // Complete
1123
+ // Renew lock
1016
1124
  // -------------------------------------------------------------------------
1017
- async complete(jobId) {
1018
- await this.pool.query(
1125
+ async renewLock(jobId, lockId, lockDuration) {
1126
+ const newLockExpiresAt = new Date(Date.now() + lockDuration).toISOString().slice(0, 23).replace("T", " ");
1127
+ const [result] = await this.pool.query(
1019
1128
  `UPDATE qjw_jobs
1020
- SET status = 'completed', lock_id = NULL, lock_expires_at = NULL,
1021
- completed_at = NOW(3), updated_at = NOW(3)
1022
- WHERE id = ?`,
1023
- [jobId]
1129
+ SET lock_expires_at = ?, updated_at = NOW(3)
1130
+ WHERE id = ? AND status = 'active' AND lock_id = ? AND lock_expires_at > NOW(3)`,
1131
+ [newLockExpiresAt, jobId, lockId]
1024
1132
  );
1133
+ return result.affectedRows > 0;
1134
+ }
1135
+ // -------------------------------------------------------------------------
1136
+ // Complete
1137
+ // -------------------------------------------------------------------------
1138
+ async complete(jobId, lockId) {
1139
+ if (lockId !== void 0) {
1140
+ await this.pool.query(
1141
+ `UPDATE qjw_jobs
1142
+ SET status = 'completed', lock_id = NULL, lock_expires_at = NULL,
1143
+ completed_at = NOW(3), updated_at = NOW(3)
1144
+ WHERE id = ? AND status = 'active' AND lock_id = ?`,
1145
+ [jobId, lockId]
1146
+ );
1147
+ } else {
1148
+ await this.pool.query(
1149
+ `UPDATE qjw_jobs
1150
+ SET status = 'completed', lock_id = NULL, lock_expires_at = NULL,
1151
+ completed_at = NOW(3), updated_at = NOW(3)
1152
+ WHERE id = ?`,
1153
+ [jobId]
1154
+ );
1155
+ }
1025
1156
  }
1026
1157
  // -------------------------------------------------------------------------
1027
1158
  // Requeue
@@ -1035,18 +1166,33 @@ CREATE TABLE IF NOT EXISTS qjw_rate_limits (
1035
1166
  error: input.error,
1036
1167
  ...input.stack !== void 0 ? { stack: input.stack } : {}
1037
1168
  });
1038
- await this.pool.query(
1039
- `UPDATE qjw_jobs
1040
- SET status = 'waiting',
1041
- attempts_made = attempts_made + 1,
1042
- attempts = JSON_ARRAY_APPEND(attempts, '$', CAST(? AS JSON)),
1043
- run_at = ?,
1044
- lock_id = NULL,
1045
- lock_expires_at = NULL,
1046
- updated_at = NOW(3)
1047
- WHERE id = ?`,
1048
- [attempt, input.runAt, input.jobId]
1049
- );
1169
+ if (input.lockId !== void 0) {
1170
+ await this.pool.query(
1171
+ `UPDATE qjw_jobs
1172
+ SET status = 'waiting',
1173
+ attempts_made = attempts_made + 1,
1174
+ attempts = JSON_ARRAY_APPEND(attempts, '$', CAST(? AS JSON)),
1175
+ run_at = ?,
1176
+ lock_id = NULL,
1177
+ lock_expires_at = NULL,
1178
+ updated_at = NOW(3)
1179
+ WHERE id = ? AND status = 'active' AND lock_id = ?`,
1180
+ [attempt, input.runAt, input.jobId, input.lockId]
1181
+ );
1182
+ } else {
1183
+ await this.pool.query(
1184
+ `UPDATE qjw_jobs
1185
+ SET status = 'waiting',
1186
+ attempts_made = attempts_made + 1,
1187
+ attempts = JSON_ARRAY_APPEND(attempts, '$', CAST(? AS JSON)),
1188
+ run_at = ?,
1189
+ lock_id = NULL,
1190
+ lock_expires_at = NULL,
1191
+ updated_at = NOW(3)
1192
+ WHERE id = ?`,
1193
+ [attempt, input.runAt, input.jobId]
1194
+ );
1195
+ }
1050
1196
  }
1051
1197
  // -------------------------------------------------------------------------
1052
1198
  // Move to DLQ
@@ -1060,29 +1206,53 @@ CREATE TABLE IF NOT EXISTS qjw_rate_limits (
1060
1206
  error: input.error,
1061
1207
  ...input.stack !== void 0 ? { stack: input.stack } : {}
1062
1208
  });
1063
- await this.pool.query(
1064
- `UPDATE qjw_jobs
1065
- SET status = 'dead',
1066
- attempts_made = attempts_made + 1,
1067
- attempts = JSON_ARRAY_APPEND(attempts, '$', CAST(? AS JSON)),
1068
- lock_id = NULL,
1069
- lock_expires_at = NULL,
1070
- failed_at = NOW(3),
1071
- updated_at = NOW(3)
1072
- WHERE id = ?`,
1073
- [attempt, input.jobId]
1074
- );
1209
+ if (input.lockId !== void 0) {
1210
+ await this.pool.query(
1211
+ `UPDATE qjw_jobs
1212
+ SET status = 'dead',
1213
+ attempts_made = attempts_made + 1,
1214
+ attempts = JSON_ARRAY_APPEND(attempts, '$', CAST(? AS JSON)),
1215
+ lock_id = NULL,
1216
+ lock_expires_at = NULL,
1217
+ failed_at = NOW(3),
1218
+ updated_at = NOW(3)
1219
+ WHERE id = ? AND status = 'active' AND lock_id = ?`,
1220
+ [attempt, input.jobId, input.lockId]
1221
+ );
1222
+ } else {
1223
+ await this.pool.query(
1224
+ `UPDATE qjw_jobs
1225
+ SET status = 'dead',
1226
+ attempts_made = attempts_made + 1,
1227
+ attempts = JSON_ARRAY_APPEND(attempts, '$', CAST(? AS JSON)),
1228
+ lock_id = NULL,
1229
+ lock_expires_at = NULL,
1230
+ failed_at = NOW(3),
1231
+ updated_at = NOW(3)
1232
+ WHERE id = ?`,
1233
+ [attempt, input.jobId]
1234
+ );
1235
+ }
1075
1236
  }
1076
1237
  // -------------------------------------------------------------------------
1077
1238
  // Release lock
1078
1239
  // -------------------------------------------------------------------------
1079
- async releaseLock(jobId) {
1080
- await this.pool.query(
1081
- `UPDATE qjw_jobs
1082
- SET lock_id = NULL, lock_expires_at = NOW(3), updated_at = NOW(3)
1083
- WHERE id = ?`,
1084
- [jobId]
1085
- );
1240
+ async releaseLock(jobId, lockId) {
1241
+ if (lockId !== void 0) {
1242
+ await this.pool.query(
1243
+ `UPDATE qjw_jobs
1244
+ SET lock_id = NULL, lock_expires_at = NOW(3), updated_at = NOW(3)
1245
+ WHERE id = ? AND status = 'active' AND lock_id = ?`,
1246
+ [jobId, lockId]
1247
+ );
1248
+ } else {
1249
+ await this.pool.query(
1250
+ `UPDATE qjw_jobs
1251
+ SET lock_id = NULL, lock_expires_at = NOW(3), updated_at = NOW(3)
1252
+ WHERE id = ?`,
1253
+ [jobId]
1254
+ );
1255
+ }
1086
1256
  }
1087
1257
  // -------------------------------------------------------------------------
1088
1258
  // Recover stalled jobs (atomic — SELECT … FOR UPDATE inside transaction)
@@ -1473,7 +1643,7 @@ var Worker = class {
1473
1643
  if (this.activeJobIds.size > 0) {
1474
1644
  await Promise.all(
1475
1645
  Array.from(this.activeJobIds).map(
1476
- (jobId) => this.storage.releaseLock(jobId).catch(() => {
1646
+ (jobId) => this.storage.releaseLock(jobId, this.id).catch(() => {
1477
1647
  })
1478
1648
  )
1479
1649
  );
@@ -1525,7 +1695,7 @@ var Worker = class {
1525
1695
  now
1526
1696
  );
1527
1697
  if (!allowed) {
1528
- await this.storage.releaseLock(raw.id);
1698
+ await this.storage.releaseLock(raw.id, this.id);
1529
1699
  return false;
1530
1700
  }
1531
1701
  }
@@ -1545,16 +1715,35 @@ var Worker = class {
1545
1715
  await this.handleFailure(job, error);
1546
1716
  return;
1547
1717
  }
1718
+ const abortController = new AbortController();
1548
1719
  let timeoutHandle = null;
1720
+ let lockRenewTimer = null;
1721
+ const lockRenewInterval = Math.max(100, Math.floor(this.config.lockDuration / 2));
1722
+ lockRenewTimer = setInterval(async () => {
1723
+ try {
1724
+ const renewed = await this.storage.renewLock(job.id, this.id, this.config.lockDuration);
1725
+ if (!renewed && lockRenewTimer) {
1726
+ clearInterval(lockRenewTimer);
1727
+ lockRenewTimer = null;
1728
+ }
1729
+ } catch {
1730
+ }
1731
+ }, lockRenewInterval);
1549
1732
  try {
1550
1733
  await new Promise((resolve, reject) => {
1551
1734
  timeoutHandle = setTimeout(() => {
1552
- reject(new Error(`Job timed out after ${job.timeout}ms`));
1735
+ const timeoutError = new Error(`Job timed out after ${job.timeout}ms`);
1736
+ abortController.abort(timeoutError);
1737
+ reject(timeoutError);
1553
1738
  }, job.timeout);
1554
- Promise.resolve(processor(job)).then(resolve, reject);
1739
+ Promise.resolve(processor(job, abortController.signal)).then(resolve, reject);
1555
1740
  });
1556
1741
  if (timeoutHandle) clearTimeout(timeoutHandle);
1557
- await this.storage.complete(job.id);
1742
+ if (lockRenewTimer) {
1743
+ clearInterval(lockRenewTimer);
1744
+ lockRenewTimer = null;
1745
+ }
1746
+ await this.storage.complete(job.id, this.id);
1558
1747
  const completedData = await this.storage.getJob(job.id) ?? job._data;
1559
1748
  this.emitter.emit("job:completed", completedData);
1560
1749
  if (job.cron) {
@@ -1562,9 +1751,17 @@ var Worker = class {
1562
1751
  }
1563
1752
  } catch (err) {
1564
1753
  if (timeoutHandle) clearTimeout(timeoutHandle);
1754
+ if (lockRenewTimer) {
1755
+ clearInterval(lockRenewTimer);
1756
+ lockRenewTimer = null;
1757
+ }
1565
1758
  const error = err instanceof Error ? err : new Error(String(err));
1566
1759
  await this.handleFailure(job, error);
1567
1760
  } finally {
1761
+ if (lockRenewTimer) {
1762
+ clearInterval(lockRenewTimer);
1763
+ lockRenewTimer = null;
1764
+ }
1568
1765
  this.activeJobIds.delete(job.id);
1569
1766
  this.activeCount -= 1;
1570
1767
  if (this.activeCount === 0 && this.drainResolve) {
@@ -1630,6 +1827,7 @@ var Worker = class {
1630
1827
  runAt,
1631
1828
  error: error.message,
1632
1829
  attemptNumber,
1830
+ lockId: this.id,
1633
1831
  ...error.stack !== void 0 && { stack: error.stack }
1634
1832
  });
1635
1833
  const updated = await this.storage.getJob(job.id);
@@ -1641,6 +1839,7 @@ var Worker = class {
1641
1839
  jobId: job.id,
1642
1840
  error: error.message,
1643
1841
  attemptNumber,
1842
+ lockId: this.id,
1644
1843
  ...error.stack !== void 0 && { stack: error.stack }
1645
1844
  });
1646
1845
  const dead = await this.storage.getJob(job.id);
@@ -1900,10 +2099,29 @@ var InMemoryStorageAdapter = class {
1900
2099
  return job;
1901
2100
  }
1902
2101
  // -------------------------------------------------------------------------
2102
+ // Renew lock
2103
+ // -------------------------------------------------------------------------
2104
+ async renewLock(jobId, lockId, lockDuration) {
2105
+ const job = this.jobs.get(jobId);
2106
+ if (!job) return false;
2107
+ if (job.status !== "active" || job.lockId !== lockId) return false;
2108
+ const nowMs = Date.now();
2109
+ if (job.lockExpiresAt !== null && new Date(job.lockExpiresAt).getTime() <= nowMs) {
2110
+ return false;
2111
+ }
2112
+ job.lockExpiresAt = new Date(nowMs + lockDuration).toISOString();
2113
+ job.updatedAt = new Date(nowMs).toISOString();
2114
+ return true;
2115
+ }
2116
+ // -------------------------------------------------------------------------
1903
2117
  // Complete
1904
2118
  // -------------------------------------------------------------------------
1905
- async complete(jobId) {
1906
- const job = this.requireJob(jobId);
2119
+ async complete(jobId, lockId) {
2120
+ const job = this.jobs.get(jobId);
2121
+ if (!job) return;
2122
+ if (lockId !== void 0 && (job.status !== "active" || job.lockId !== lockId)) {
2123
+ return;
2124
+ }
1907
2125
  const now = (/* @__PURE__ */ new Date()).toISOString();
1908
2126
  job.status = "completed";
1909
2127
  job.lockId = null;
@@ -1915,7 +2133,11 @@ var InMemoryStorageAdapter = class {
1915
2133
  // Requeue (retry)
1916
2134
  // -------------------------------------------------------------------------
1917
2135
  async requeue(input) {
1918
- const job = this.requireJob(input.jobId);
2136
+ const job = this.jobs.get(input.jobId);
2137
+ if (!job) return;
2138
+ if (input.lockId !== void 0 && (job.status !== "active" || job.lockId !== input.lockId)) {
2139
+ return;
2140
+ }
1919
2141
  const now = (/* @__PURE__ */ new Date()).toISOString();
1920
2142
  job.attempts.push({
1921
2143
  attempt: input.attemptNumber,
@@ -1935,7 +2157,11 @@ var InMemoryStorageAdapter = class {
1935
2157
  // Move to DLQ
1936
2158
  // -------------------------------------------------------------------------
1937
2159
  async moveToDlq(input) {
1938
- const job = this.requireJob(input.jobId);
2160
+ const job = this.jobs.get(input.jobId);
2161
+ if (!job) return;
2162
+ if (input.lockId !== void 0 && (job.status !== "active" || job.lockId !== input.lockId)) {
2163
+ return;
2164
+ }
1939
2165
  const now = (/* @__PURE__ */ new Date()).toISOString();
1940
2166
  job.attempts.push({
1941
2167
  attempt: input.attemptNumber,
@@ -1954,9 +2180,12 @@ var InMemoryStorageAdapter = class {
1954
2180
  // -------------------------------------------------------------------------
1955
2181
  // Release lock
1956
2182
  // -------------------------------------------------------------------------
1957
- async releaseLock(jobId) {
2183
+ async releaseLock(jobId, lockId) {
1958
2184
  const job = this.jobs.get(jobId);
1959
2185
  if (!job) return;
2186
+ if (lockId !== void 0 && (job.status !== "active" || job.lockId !== lockId)) {
2187
+ return;
2188
+ }
1960
2189
  const now = (/* @__PURE__ */ new Date()).toISOString();
1961
2190
  job.lockId = null;
1962
2191
  job.lockExpiresAt = now;