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.
@@ -1 +1 @@
1
- {"version":3,"file":"worker.d.ts","sourceRoot":"","sources":["../../src/core/worker.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAIH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAChE,OAAO,KAAK,EAAE,aAAa,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAC;AACvF,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAC5D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAI/D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AAE9D,KAAK,gBAAgB,GAAG,QAAQ,CAAC,cAAc,CAAC,CAAC;AAyBjD,qBAAa,MAAM;IACjB,kDAAkD;IAClD,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IAEpB,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAiB;IACzC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAoB;IAC5C,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAkC;IAC7D,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAmC;IAE1D,OAAO,CAAC,OAAO,CAAwB;IACvC,OAAO,CAAC,WAAW,CAAK;IAExB;;;OAGG;IACH,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAqB;IAElD,OAAO,CAAC,SAAS,CAA+B;IAChD,OAAO,CAAC,YAAY,CAA+B;IAEnD,4DAA4D;IAC5D,OAAO,CAAC,YAAY,CAA6B;gBAG/C,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,cAAc,EACvB,OAAO,EAAE,iBAAiB,EAC1B,UAAU,EAAE,GAAG,CAAC,MAAM,EAAE,SAAS,CAAC,OAAO,CAAC,CAAC,EAC3C,aAAa,EAAE,aAAa,EAC5B,YAAY,EAAE,YAAY,EAC1B,QAAQ,EAAE,gBAAgB;IAc5B,IAAI,MAAM,IAAI,YAAY,CAEzB;IAED,8BAA8B;IAC9B,KAAK,IAAI,IAAI;IAab;;;;;;;;OAQG;IACG,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAqD3B,OAAO,CAAC,YAAY;YAQN,IAAI;YAsBJ,SAAS;YAyCT,UAAU;YAwDV,eAAe;YA4Df,aAAa;IA0C3B,OAAO,CAAC,kBAAkB;YAQZ,kBAAkB;CAkBjC"}
1
+ {"version":3,"file":"worker.d.ts","sourceRoot":"","sources":["../../src/core/worker.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAIH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAChE,OAAO,KAAK,EAAE,aAAa,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAC;AACvF,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAC5D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAI/D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AAE9D,KAAK,gBAAgB,GAAG,QAAQ,CAAC,cAAc,CAAC,CAAC;AAyBjD,qBAAa,MAAM;IACjB,kDAAkD;IAClD,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IAEpB,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAiB;IACzC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAoB;IAC5C,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAkC;IAC7D,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAmC;IAE1D,OAAO,CAAC,OAAO,CAAwB;IACvC,OAAO,CAAC,WAAW,CAAK;IAExB;;;OAGG;IACH,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAqB;IAElD,OAAO,CAAC,SAAS,CAA+B;IAChD,OAAO,CAAC,YAAY,CAA+B;IAEnD,4DAA4D;IAC5D,OAAO,CAAC,YAAY,CAA6B;gBAG/C,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,cAAc,EACvB,OAAO,EAAE,iBAAiB,EAC1B,UAAU,EAAE,GAAG,CAAC,MAAM,EAAE,SAAS,CAAC,OAAO,CAAC,CAAC,EAC3C,aAAa,EAAE,aAAa,EAC5B,YAAY,EAAE,YAAY,EAC1B,QAAQ,EAAE,gBAAgB;IAc5B,IAAI,MAAM,IAAI,YAAY,CAEzB;IAED,8BAA8B;IAC9B,KAAK,IAAI,IAAI;IAab;;;;;;;;OAQG;IACG,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAqD3B,OAAO,CAAC,YAAY;YAQN,IAAI;YAsBJ,SAAS;YAyCT,UAAU;YAqFV,eAAe;YA4Df,aAAa;IA4C3B,OAAO,CAAC,kBAAkB;YAQZ,kBAAkB;CAkBjC"}
package/dist/index.cjs CHANGED
@@ -77,7 +77,7 @@ function hashToJob(h) {
77
77
  if (h["cron"]) job.cron = h["cron"];
78
78
  return job;
79
79
  }
80
- var PREFIX, k, CLAIM_LUA, RECOVER_STALLED_LUA; exports.RedisStorageAdapter = void 0;
80
+ var PREFIX, k, CLAIM_LUA, RECOVER_STALLED_LUA, RENEW_LOCK_LUA; exports.RedisStorageAdapter = void 0;
81
81
  var init_redis_adapter = __esm({
82
82
  "src/storage/redis.adapter.ts"() {
83
83
  PREFIX = "qjw:";
@@ -161,6 +161,24 @@ redis.call('HSET', job_key,
161
161
  redis.call('SREM', active_set, job_id)
162
162
  redis.call('ZADD', waiting_zset, priority_score, job_id)
163
163
  return 1
164
+ `;
165
+ RENEW_LOCK_LUA = `
166
+ local job_key = KEYS[1]
167
+ local lock_id = ARGV[1]
168
+ local new_exp = ARGV[2]
169
+ local now_iso = ARGV[3]
170
+
171
+ local fields = redis.call('HMGET', job_key, 'status', 'lockId', 'lockExpiresAt')
172
+ local status = fields[1] or ''
173
+ local current_lock = fields[2] or ''
174
+ local current_exp = fields[3] or ''
175
+
176
+ if status ~= 'active' then return 0 end
177
+ if current_lock ~= lock_id then return 0 end
178
+ if current_exp == '' or current_exp <= now_iso then return 0 end
179
+
180
+ redis.call('HSET', job_key, 'lockExpiresAt', new_exp, 'updatedAt', now_iso)
181
+ return 1
164
182
  `;
165
183
  exports.RedisStorageAdapter = class {
166
184
  client;
@@ -251,12 +269,28 @@ return 1
251
269
  return hashToJob(hash);
252
270
  }
253
271
  // -------------------------------------------------------------------------
272
+ // Renew lock
273
+ // -------------------------------------------------------------------------
274
+ async renewLock(jobId, lockId, lockDuration) {
275
+ const nowMs = Date.now();
276
+ const nowIso = new Date(nowMs).toISOString();
277
+ const newExpiresAt = new Date(nowMs + lockDuration).toISOString();
278
+ const res = await this.client.eval(RENEW_LOCK_LUA, {
279
+ keys: [k.job(jobId)],
280
+ arguments: [lockId, newExpiresAt, nowIso]
281
+ });
282
+ return Number(res) === 1;
283
+ }
284
+ // -------------------------------------------------------------------------
254
285
  // Complete
255
286
  // -------------------------------------------------------------------------
256
- async complete(jobId) {
287
+ async complete(jobId, lockId) {
257
288
  const now = (/* @__PURE__ */ new Date()).toISOString();
258
289
  const hash = await this.client.hGetAll(k.job(jobId));
259
- if (!hash) return;
290
+ if (!hash || Object.keys(hash).length === 0) return;
291
+ if (lockId !== void 0) {
292
+ if (hash["status"] !== "active" || hash["lockId"] !== lockId) return;
293
+ }
260
294
  const queue = hash["queue"] ?? "";
261
295
  const multi = this.client.multi();
262
296
  multi.hSet(k.job(jobId), {
@@ -276,7 +310,10 @@ return 1
276
310
  async requeue(input) {
277
311
  const now = (/* @__PURE__ */ new Date()).toISOString();
278
312
  const hash = await this.client.hGetAll(k.job(input.jobId));
279
- if (!hash) return;
313
+ if (!hash || Object.keys(hash).length === 0) return;
314
+ if (input.lockId !== void 0) {
315
+ if (hash["status"] !== "active" || hash["lockId"] !== input.lockId) return;
316
+ }
280
317
  const queue = hash["queue"] ?? "";
281
318
  const attempts = JSON.parse(hash["attempts"] ?? "[]");
282
319
  attempts.push({
@@ -309,7 +346,10 @@ return 1
309
346
  async moveToDlq(input) {
310
347
  const now = (/* @__PURE__ */ new Date()).toISOString();
311
348
  const hash = await this.client.hGetAll(k.job(input.jobId));
312
- if (!hash) return;
349
+ if (!hash || Object.keys(hash).length === 0) return;
350
+ if (input.lockId !== void 0) {
351
+ if (hash["status"] !== "active" || hash["lockId"] !== input.lockId) return;
352
+ }
313
353
  const queue = hash["queue"] ?? "";
314
354
  const attempts = JSON.parse(hash["attempts"] ?? "[]");
315
355
  attempts.push({
@@ -336,7 +376,12 @@ return 1
336
376
  // -------------------------------------------------------------------------
337
377
  // Release lock
338
378
  // -------------------------------------------------------------------------
339
- async releaseLock(jobId) {
379
+ async releaseLock(jobId, lockId) {
380
+ const hash = await this.client.hGetAll(k.job(jobId));
381
+ if (!hash || Object.keys(hash).length === 0) return;
382
+ if (lockId !== void 0) {
383
+ if (hash["status"] !== "active" || hash["lockId"] !== lockId) return;
384
+ }
340
385
  const now = (/* @__PURE__ */ new Date()).toISOString();
341
386
  await this.client.hSet(k.job(jobId), {
342
387
  lockId: "",
@@ -663,16 +708,40 @@ CREATE TABLE IF NOT EXISTS qjw_rate_limits (
663
708
  }
664
709
  }
665
710
  // -------------------------------------------------------------------------
666
- // Complete
711
+ // Renew lock
667
712
  // -------------------------------------------------------------------------
668
- async complete(jobId) {
669
- await this.pool.query(
713
+ async renewLock(jobId, lockId, lockDuration) {
714
+ const newLockExpiresAt = new Date(Date.now() + lockDuration).toISOString();
715
+ const res = await this.pool.query(
670
716
  `UPDATE qjw_jobs
671
- SET status = 'completed', lock_id = NULL, lock_expires_at = NULL,
672
- completed_at = NOW(), updated_at = NOW()
673
- WHERE id = $1`,
674
- [jobId]
717
+ SET lock_expires_at = $1::timestamptz,
718
+ updated_at = NOW()
719
+ WHERE id = $2 AND status = 'active' AND lock_id = $3 AND lock_expires_at > NOW()`,
720
+ [newLockExpiresAt, jobId, lockId]
675
721
  );
722
+ return (res.rowCount ?? 0) > 0;
723
+ }
724
+ // -------------------------------------------------------------------------
725
+ // Complete
726
+ // -------------------------------------------------------------------------
727
+ async complete(jobId, lockId) {
728
+ if (lockId !== void 0) {
729
+ await this.pool.query(
730
+ `UPDATE qjw_jobs
731
+ SET status = 'completed', lock_id = NULL, lock_expires_at = NULL,
732
+ completed_at = NOW(), updated_at = NOW()
733
+ WHERE id = $1 AND status = 'active' AND lock_id = $2`,
734
+ [jobId, lockId]
735
+ );
736
+ } else {
737
+ await this.pool.query(
738
+ `UPDATE qjw_jobs
739
+ SET status = 'completed', lock_id = NULL, lock_expires_at = NULL,
740
+ completed_at = NOW(), updated_at = NOW()
741
+ WHERE id = $1`,
742
+ [jobId]
743
+ );
744
+ }
676
745
  }
677
746
  // -------------------------------------------------------------------------
678
747
  // Requeue
@@ -686,18 +755,33 @@ CREATE TABLE IF NOT EXISTS qjw_rate_limits (
686
755
  error: input.error,
687
756
  ...input.stack !== void 0 ? { stack: input.stack } : {}
688
757
  };
689
- await this.pool.query(
690
- `UPDATE qjw_jobs
691
- SET status = 'waiting',
692
- attempts_made = attempts_made + 1,
693
- attempts = attempts || $1::jsonb,
694
- run_at = $2::timestamptz,
695
- lock_id = NULL,
696
- lock_expires_at = NULL,
697
- updated_at = NOW()
698
- WHERE id = $3`,
699
- [JSON.stringify([attempt]), input.runAt, input.jobId]
700
- );
758
+ if (input.lockId !== void 0) {
759
+ await this.pool.query(
760
+ `UPDATE qjw_jobs
761
+ SET status = 'waiting',
762
+ attempts_made = $1,
763
+ attempts = attempts || $2::jsonb,
764
+ run_at = $3::timestamptz,
765
+ lock_id = NULL,
766
+ lock_expires_at = NULL,
767
+ updated_at = NOW()
768
+ WHERE id = $4 AND status = 'active' AND lock_id = $5`,
769
+ [input.attemptNumber, JSON.stringify([attempt]), input.runAt, input.jobId, input.lockId]
770
+ );
771
+ } else {
772
+ await this.pool.query(
773
+ `UPDATE qjw_jobs
774
+ SET status = 'waiting',
775
+ attempts_made = $1,
776
+ attempts = attempts || $2::jsonb,
777
+ run_at = $3::timestamptz,
778
+ lock_id = NULL,
779
+ lock_expires_at = NULL,
780
+ updated_at = NOW()
781
+ WHERE id = $4`,
782
+ [input.attemptNumber, JSON.stringify([attempt]), input.runAt, input.jobId]
783
+ );
784
+ }
701
785
  }
702
786
  // -------------------------------------------------------------------------
703
787
  // Move to DLQ
@@ -711,29 +795,53 @@ CREATE TABLE IF NOT EXISTS qjw_rate_limits (
711
795
  error: input.error,
712
796
  ...input.stack !== void 0 ? { stack: input.stack } : {}
713
797
  };
714
- await this.pool.query(
715
- `UPDATE qjw_jobs
716
- SET status = 'dead',
717
- attempts_made = attempts_made + 1,
718
- attempts = attempts || $1::jsonb,
719
- lock_id = NULL,
720
- lock_expires_at = NULL,
721
- failed_at = NOW(),
722
- updated_at = NOW()
723
- WHERE id = $2`,
724
- [JSON.stringify([attempt]), input.jobId]
725
- );
798
+ if (input.lockId !== void 0) {
799
+ await this.pool.query(
800
+ `UPDATE qjw_jobs
801
+ SET status = 'dead',
802
+ attempts_made = $1,
803
+ attempts = attempts || $2::jsonb,
804
+ lock_id = NULL,
805
+ lock_expires_at = NULL,
806
+ failed_at = NOW(),
807
+ updated_at = NOW()
808
+ WHERE id = $3 AND status = 'active' AND lock_id = $4`,
809
+ [input.attemptNumber, JSON.stringify([attempt]), input.jobId, input.lockId]
810
+ );
811
+ } else {
812
+ await this.pool.query(
813
+ `UPDATE qjw_jobs
814
+ SET status = 'dead',
815
+ attempts_made = $1,
816
+ attempts = attempts || $2::jsonb,
817
+ lock_id = NULL,
818
+ lock_expires_at = NULL,
819
+ failed_at = NOW(),
820
+ updated_at = NOW()
821
+ WHERE id = $3`,
822
+ [input.attemptNumber, JSON.stringify([attempt]), input.jobId]
823
+ );
824
+ }
726
825
  }
727
826
  // -------------------------------------------------------------------------
728
827
  // Release lock
729
828
  // -------------------------------------------------------------------------
730
- async releaseLock(jobId) {
731
- await this.pool.query(
732
- `UPDATE qjw_jobs
733
- SET lock_id = NULL, lock_expires_at = NOW(), updated_at = NOW()
734
- WHERE id = $1`,
735
- [jobId]
736
- );
829
+ async releaseLock(jobId, lockId) {
830
+ if (lockId !== void 0) {
831
+ await this.pool.query(
832
+ `UPDATE qjw_jobs
833
+ SET lock_id = NULL, lock_expires_at = NOW(), updated_at = NOW()
834
+ WHERE id = $1 AND status = 'active' AND lock_id = $2`,
835
+ [jobId, lockId]
836
+ );
837
+ } else {
838
+ await this.pool.query(
839
+ `UPDATE qjw_jobs
840
+ SET lock_id = NULL, lock_expires_at = NOW(), updated_at = NOW()
841
+ WHERE id = $1`,
842
+ [jobId]
843
+ );
844
+ }
737
845
  }
738
846
  // -------------------------------------------------------------------------
739
847
  // Recover stalled jobs
@@ -1014,16 +1122,39 @@ CREATE TABLE IF NOT EXISTS qjw_rate_limits (
1014
1122
  }
1015
1123
  }
1016
1124
  // -------------------------------------------------------------------------
1017
- // Complete
1125
+ // Renew lock
1018
1126
  // -------------------------------------------------------------------------
1019
- async complete(jobId) {
1020
- await this.pool.query(
1127
+ async renewLock(jobId, lockId, lockDuration) {
1128
+ const newLockExpiresAt = new Date(Date.now() + lockDuration).toISOString().slice(0, 23).replace("T", " ");
1129
+ const [result] = await this.pool.query(
1021
1130
  `UPDATE qjw_jobs
1022
- SET status = 'completed', lock_id = NULL, lock_expires_at = NULL,
1023
- completed_at = NOW(3), updated_at = NOW(3)
1024
- WHERE id = ?`,
1025
- [jobId]
1131
+ SET lock_expires_at = ?, updated_at = NOW(3)
1132
+ WHERE id = ? AND status = 'active' AND lock_id = ? AND lock_expires_at > NOW(3)`,
1133
+ [newLockExpiresAt, jobId, lockId]
1026
1134
  );
1135
+ return result.affectedRows > 0;
1136
+ }
1137
+ // -------------------------------------------------------------------------
1138
+ // Complete
1139
+ // -------------------------------------------------------------------------
1140
+ async complete(jobId, lockId) {
1141
+ if (lockId !== void 0) {
1142
+ await this.pool.query(
1143
+ `UPDATE qjw_jobs
1144
+ SET status = 'completed', lock_id = NULL, lock_expires_at = NULL,
1145
+ completed_at = NOW(3), updated_at = NOW(3)
1146
+ WHERE id = ? AND status = 'active' AND lock_id = ?`,
1147
+ [jobId, lockId]
1148
+ );
1149
+ } else {
1150
+ await this.pool.query(
1151
+ `UPDATE qjw_jobs
1152
+ SET status = 'completed', lock_id = NULL, lock_expires_at = NULL,
1153
+ completed_at = NOW(3), updated_at = NOW(3)
1154
+ WHERE id = ?`,
1155
+ [jobId]
1156
+ );
1157
+ }
1027
1158
  }
1028
1159
  // -------------------------------------------------------------------------
1029
1160
  // Requeue
@@ -1037,18 +1168,33 @@ CREATE TABLE IF NOT EXISTS qjw_rate_limits (
1037
1168
  error: input.error,
1038
1169
  ...input.stack !== void 0 ? { stack: input.stack } : {}
1039
1170
  });
1040
- await this.pool.query(
1041
- `UPDATE qjw_jobs
1042
- SET status = 'waiting',
1043
- attempts_made = attempts_made + 1,
1044
- attempts = JSON_ARRAY_APPEND(attempts, '$', CAST(? AS JSON)),
1045
- run_at = ?,
1046
- lock_id = NULL,
1047
- lock_expires_at = NULL,
1048
- updated_at = NOW(3)
1049
- WHERE id = ?`,
1050
- [attempt, input.runAt, input.jobId]
1051
- );
1171
+ if (input.lockId !== void 0) {
1172
+ await this.pool.query(
1173
+ `UPDATE qjw_jobs
1174
+ SET status = 'waiting',
1175
+ attempts_made = attempts_made + 1,
1176
+ attempts = JSON_ARRAY_APPEND(attempts, '$', CAST(? AS JSON)),
1177
+ run_at = ?,
1178
+ lock_id = NULL,
1179
+ lock_expires_at = NULL,
1180
+ updated_at = NOW(3)
1181
+ WHERE id = ? AND status = 'active' AND lock_id = ?`,
1182
+ [attempt, input.runAt, input.jobId, input.lockId]
1183
+ );
1184
+ } else {
1185
+ await this.pool.query(
1186
+ `UPDATE qjw_jobs
1187
+ SET status = 'waiting',
1188
+ attempts_made = attempts_made + 1,
1189
+ attempts = JSON_ARRAY_APPEND(attempts, '$', CAST(? AS JSON)),
1190
+ run_at = ?,
1191
+ lock_id = NULL,
1192
+ lock_expires_at = NULL,
1193
+ updated_at = NOW(3)
1194
+ WHERE id = ?`,
1195
+ [attempt, input.runAt, input.jobId]
1196
+ );
1197
+ }
1052
1198
  }
1053
1199
  // -------------------------------------------------------------------------
1054
1200
  // Move to DLQ
@@ -1062,29 +1208,53 @@ CREATE TABLE IF NOT EXISTS qjw_rate_limits (
1062
1208
  error: input.error,
1063
1209
  ...input.stack !== void 0 ? { stack: input.stack } : {}
1064
1210
  });
1065
- await this.pool.query(
1066
- `UPDATE qjw_jobs
1067
- SET status = 'dead',
1068
- attempts_made = attempts_made + 1,
1069
- attempts = JSON_ARRAY_APPEND(attempts, '$', CAST(? AS JSON)),
1070
- lock_id = NULL,
1071
- lock_expires_at = NULL,
1072
- failed_at = NOW(3),
1073
- updated_at = NOW(3)
1074
- WHERE id = ?`,
1075
- [attempt, input.jobId]
1076
- );
1211
+ if (input.lockId !== void 0) {
1212
+ await this.pool.query(
1213
+ `UPDATE qjw_jobs
1214
+ SET status = 'dead',
1215
+ attempts_made = attempts_made + 1,
1216
+ attempts = JSON_ARRAY_APPEND(attempts, '$', CAST(? AS JSON)),
1217
+ lock_id = NULL,
1218
+ lock_expires_at = NULL,
1219
+ failed_at = NOW(3),
1220
+ updated_at = NOW(3)
1221
+ WHERE id = ? AND status = 'active' AND lock_id = ?`,
1222
+ [attempt, input.jobId, input.lockId]
1223
+ );
1224
+ } else {
1225
+ await this.pool.query(
1226
+ `UPDATE qjw_jobs
1227
+ SET status = 'dead',
1228
+ attempts_made = attempts_made + 1,
1229
+ attempts = JSON_ARRAY_APPEND(attempts, '$', CAST(? AS JSON)),
1230
+ lock_id = NULL,
1231
+ lock_expires_at = NULL,
1232
+ failed_at = NOW(3),
1233
+ updated_at = NOW(3)
1234
+ WHERE id = ?`,
1235
+ [attempt, input.jobId]
1236
+ );
1237
+ }
1077
1238
  }
1078
1239
  // -------------------------------------------------------------------------
1079
1240
  // Release lock
1080
1241
  // -------------------------------------------------------------------------
1081
- async releaseLock(jobId) {
1082
- await this.pool.query(
1083
- `UPDATE qjw_jobs
1084
- SET lock_id = NULL, lock_expires_at = NOW(3), updated_at = NOW(3)
1085
- WHERE id = ?`,
1086
- [jobId]
1087
- );
1242
+ async releaseLock(jobId, lockId) {
1243
+ if (lockId !== void 0) {
1244
+ await this.pool.query(
1245
+ `UPDATE qjw_jobs
1246
+ SET lock_id = NULL, lock_expires_at = NOW(3), updated_at = NOW(3)
1247
+ WHERE id = ? AND status = 'active' AND lock_id = ?`,
1248
+ [jobId, lockId]
1249
+ );
1250
+ } else {
1251
+ await this.pool.query(
1252
+ `UPDATE qjw_jobs
1253
+ SET lock_id = NULL, lock_expires_at = NOW(3), updated_at = NOW(3)
1254
+ WHERE id = ?`,
1255
+ [jobId]
1256
+ );
1257
+ }
1088
1258
  }
1089
1259
  // -------------------------------------------------------------------------
1090
1260
  // Recover stalled jobs (atomic — SELECT … FOR UPDATE inside transaction)
@@ -1475,7 +1645,7 @@ var Worker = class {
1475
1645
  if (this.activeJobIds.size > 0) {
1476
1646
  await Promise.all(
1477
1647
  Array.from(this.activeJobIds).map(
1478
- (jobId) => this.storage.releaseLock(jobId).catch(() => {
1648
+ (jobId) => this.storage.releaseLock(jobId, this.id).catch(() => {
1479
1649
  })
1480
1650
  )
1481
1651
  );
@@ -1527,7 +1697,7 @@ var Worker = class {
1527
1697
  now
1528
1698
  );
1529
1699
  if (!allowed) {
1530
- await this.storage.releaseLock(raw.id);
1700
+ await this.storage.releaseLock(raw.id, this.id);
1531
1701
  return false;
1532
1702
  }
1533
1703
  }
@@ -1547,16 +1717,35 @@ var Worker = class {
1547
1717
  await this.handleFailure(job, error);
1548
1718
  return;
1549
1719
  }
1720
+ const abortController = new AbortController();
1550
1721
  let timeoutHandle = null;
1722
+ let lockRenewTimer = null;
1723
+ const lockRenewInterval = Math.max(100, Math.floor(this.config.lockDuration / 2));
1724
+ lockRenewTimer = setInterval(async () => {
1725
+ try {
1726
+ const renewed = await this.storage.renewLock(job.id, this.id, this.config.lockDuration);
1727
+ if (!renewed && lockRenewTimer) {
1728
+ clearInterval(lockRenewTimer);
1729
+ lockRenewTimer = null;
1730
+ }
1731
+ } catch {
1732
+ }
1733
+ }, lockRenewInterval);
1551
1734
  try {
1552
1735
  await new Promise((resolve, reject) => {
1553
1736
  timeoutHandle = setTimeout(() => {
1554
- reject(new Error(`Job timed out after ${job.timeout}ms`));
1737
+ const timeoutError = new Error(`Job timed out after ${job.timeout}ms`);
1738
+ abortController.abort(timeoutError);
1739
+ reject(timeoutError);
1555
1740
  }, job.timeout);
1556
- Promise.resolve(processor(job)).then(resolve, reject);
1741
+ Promise.resolve(processor(job, abortController.signal)).then(resolve, reject);
1557
1742
  });
1558
1743
  if (timeoutHandle) clearTimeout(timeoutHandle);
1559
- await this.storage.complete(job.id);
1744
+ if (lockRenewTimer) {
1745
+ clearInterval(lockRenewTimer);
1746
+ lockRenewTimer = null;
1747
+ }
1748
+ await this.storage.complete(job.id, this.id);
1560
1749
  const completedData = await this.storage.getJob(job.id) ?? job._data;
1561
1750
  this.emitter.emit("job:completed", completedData);
1562
1751
  if (job.cron) {
@@ -1564,9 +1753,17 @@ var Worker = class {
1564
1753
  }
1565
1754
  } catch (err) {
1566
1755
  if (timeoutHandle) clearTimeout(timeoutHandle);
1756
+ if (lockRenewTimer) {
1757
+ clearInterval(lockRenewTimer);
1758
+ lockRenewTimer = null;
1759
+ }
1567
1760
  const error = err instanceof Error ? err : new Error(String(err));
1568
1761
  await this.handleFailure(job, error);
1569
1762
  } finally {
1763
+ if (lockRenewTimer) {
1764
+ clearInterval(lockRenewTimer);
1765
+ lockRenewTimer = null;
1766
+ }
1570
1767
  this.activeJobIds.delete(job.id);
1571
1768
  this.activeCount -= 1;
1572
1769
  if (this.activeCount === 0 && this.drainResolve) {
@@ -1632,6 +1829,7 @@ var Worker = class {
1632
1829
  runAt,
1633
1830
  error: error.message,
1634
1831
  attemptNumber,
1832
+ lockId: this.id,
1635
1833
  ...error.stack !== void 0 && { stack: error.stack }
1636
1834
  });
1637
1835
  const updated = await this.storage.getJob(job.id);
@@ -1643,6 +1841,7 @@ var Worker = class {
1643
1841
  jobId: job.id,
1644
1842
  error: error.message,
1645
1843
  attemptNumber,
1844
+ lockId: this.id,
1646
1845
  ...error.stack !== void 0 && { stack: error.stack }
1647
1846
  });
1648
1847
  const dead = await this.storage.getJob(job.id);
@@ -1902,10 +2101,29 @@ var InMemoryStorageAdapter = class {
1902
2101
  return job;
1903
2102
  }
1904
2103
  // -------------------------------------------------------------------------
2104
+ // Renew lock
2105
+ // -------------------------------------------------------------------------
2106
+ async renewLock(jobId, lockId, lockDuration) {
2107
+ const job = this.jobs.get(jobId);
2108
+ if (!job) return false;
2109
+ if (job.status !== "active" || job.lockId !== lockId) return false;
2110
+ const nowMs = Date.now();
2111
+ if (job.lockExpiresAt !== null && new Date(job.lockExpiresAt).getTime() <= nowMs) {
2112
+ return false;
2113
+ }
2114
+ job.lockExpiresAt = new Date(nowMs + lockDuration).toISOString();
2115
+ job.updatedAt = new Date(nowMs).toISOString();
2116
+ return true;
2117
+ }
2118
+ // -------------------------------------------------------------------------
1905
2119
  // Complete
1906
2120
  // -------------------------------------------------------------------------
1907
- async complete(jobId) {
1908
- const job = this.requireJob(jobId);
2121
+ async complete(jobId, lockId) {
2122
+ const job = this.jobs.get(jobId);
2123
+ if (!job) return;
2124
+ if (lockId !== void 0 && (job.status !== "active" || job.lockId !== lockId)) {
2125
+ return;
2126
+ }
1909
2127
  const now = (/* @__PURE__ */ new Date()).toISOString();
1910
2128
  job.status = "completed";
1911
2129
  job.lockId = null;
@@ -1917,7 +2135,11 @@ var InMemoryStorageAdapter = class {
1917
2135
  // Requeue (retry)
1918
2136
  // -------------------------------------------------------------------------
1919
2137
  async requeue(input) {
1920
- const job = this.requireJob(input.jobId);
2138
+ const job = this.jobs.get(input.jobId);
2139
+ if (!job) return;
2140
+ if (input.lockId !== void 0 && (job.status !== "active" || job.lockId !== input.lockId)) {
2141
+ return;
2142
+ }
1921
2143
  const now = (/* @__PURE__ */ new Date()).toISOString();
1922
2144
  job.attempts.push({
1923
2145
  attempt: input.attemptNumber,
@@ -1937,7 +2159,11 @@ var InMemoryStorageAdapter = class {
1937
2159
  // Move to DLQ
1938
2160
  // -------------------------------------------------------------------------
1939
2161
  async moveToDlq(input) {
1940
- const job = this.requireJob(input.jobId);
2162
+ const job = this.jobs.get(input.jobId);
2163
+ if (!job) return;
2164
+ if (input.lockId !== void 0 && (job.status !== "active" || job.lockId !== input.lockId)) {
2165
+ return;
2166
+ }
1941
2167
  const now = (/* @__PURE__ */ new Date()).toISOString();
1942
2168
  job.attempts.push({
1943
2169
  attempt: input.attemptNumber,
@@ -1956,9 +2182,12 @@ var InMemoryStorageAdapter = class {
1956
2182
  // -------------------------------------------------------------------------
1957
2183
  // Release lock
1958
2184
  // -------------------------------------------------------------------------
1959
- async releaseLock(jobId) {
2185
+ async releaseLock(jobId, lockId) {
1960
2186
  const job = this.jobs.get(jobId);
1961
2187
  if (!job) return;
2188
+ if (lockId !== void 0 && (job.status !== "active" || job.lockId !== lockId)) {
2189
+ return;
2190
+ }
1962
2191
  const now = (/* @__PURE__ */ new Date()).toISOString();
1963
2192
  job.lockId = null;
1964
2193
  job.lockExpiresAt = now;