velocious 1.0.514 → 1.0.516

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 (255) hide show
  1. package/README.md +4 -3
  2. package/build/background-jobs/cron-expression.js +9 -9
  3. package/build/background-jobs/forked-runner-child.js +1 -1
  4. package/build/background-jobs/job-runner.js +21 -15
  5. package/build/background-jobs/json-socket.js +4 -0
  6. package/build/background-jobs/main.js +180 -18
  7. package/build/background-jobs/status-reporter.js +12 -5
  8. package/build/background-jobs/store.js +132 -50
  9. package/build/background-jobs/types.js +11 -3
  10. package/build/background-jobs/worker.js +9 -5
  11. package/build/beacon/client.js +13 -6
  12. package/build/configuration.js +54 -29
  13. package/build/controller.js +9 -3
  14. package/build/database/drivers/sqlite/base.js +1 -1
  15. package/build/database/drivers/sqlite/index.js +15 -14
  16. package/build/database/migrations-ledger.js +16 -16
  17. package/build/database/pool/async-tracked-multi-connection.js +16 -0
  18. package/build/database/pool/base.js +26 -0
  19. package/build/database/query/where-model-class-hash.js +2 -0
  20. package/build/database/query/with-count.js +1 -1
  21. package/build/database/record/acts-as-list.js +2 -2
  22. package/build/database/record/auditing.js +23 -6
  23. package/build/database/record/index.js +7 -7
  24. package/build/database/record/validators/uniqueness.js +3 -3
  25. package/build/database/tenants/data-copier.js +23 -23
  26. package/build/database/tenants/schema-cloner.js +27 -27
  27. package/build/environment-handlers/browser.js +0 -1
  28. package/build/environment-handlers/node/cli/commands/background-jobs-runner.js +2 -1
  29. package/build/environment-handlers/node/cli/commands/lint/relationships.js +2 -2
  30. package/build/environment-handlers/node.js +1 -1
  31. package/build/frontend-model-controller.js +26 -18
  32. package/build/frontend-model-resource/base-resource.js +4 -4
  33. package/build/frontend-models/base.js +5 -5
  34. package/build/frontend-models/query.js +12 -12
  35. package/build/http-server/client/index.js +61 -15
  36. package/build/http-server/client/request-runner.js +7 -1
  37. package/build/http-server/client/response.js +17 -1
  38. package/build/http-server/client/websocket-session.js +13 -13
  39. package/build/http-server/server-client.js +96 -0
  40. package/build/http-server/websocket-channel.js +12 -11
  41. package/build/http-server/websocket-connection.js +12 -11
  42. package/build/http-server/websocket-events-host.js +1 -1
  43. package/build/http-server/worker-handler/in-process.js +48 -9
  44. package/build/http-server/worker-handler/index.js +67 -7
  45. package/build/http-server/worker-handler/worker-thread.js +68 -0
  46. package/build/src/background-jobs/cron-expression.d.ts +2 -2
  47. package/build/src/background-jobs/cron-expression.js +10 -10
  48. package/build/src/background-jobs/forked-runner-child.js +2 -2
  49. package/build/src/background-jobs/job-runner.d.ts +5 -1
  50. package/build/src/background-jobs/job-runner.d.ts.map +1 -1
  51. package/build/src/background-jobs/job-runner.js +22 -16
  52. package/build/src/background-jobs/json-socket.d.ts +4 -0
  53. package/build/src/background-jobs/json-socket.d.ts.map +1 -1
  54. package/build/src/background-jobs/json-socket.js +5 -1
  55. package/build/src/background-jobs/main.d.ts +62 -10
  56. package/build/src/background-jobs/main.d.ts.map +1 -1
  57. package/build/src/background-jobs/main.js +171 -19
  58. package/build/src/background-jobs/status-reporter.d.ts +6 -2
  59. package/build/src/background-jobs/status-reporter.d.ts.map +1 -1
  60. package/build/src/background-jobs/status-reporter.js +13 -6
  61. package/build/src/background-jobs/store.d.ts +48 -25
  62. package/build/src/background-jobs/store.d.ts.map +1 -1
  63. package/build/src/background-jobs/store.js +123 -50
  64. package/build/src/background-jobs/types.d.ts +36 -3
  65. package/build/src/background-jobs/types.d.ts.map +1 -1
  66. package/build/src/background-jobs/types.js +12 -4
  67. package/build/src/background-jobs/worker.d.ts +7 -5
  68. package/build/src/background-jobs/worker.d.ts.map +1 -1
  69. package/build/src/background-jobs/worker.js +10 -6
  70. package/build/src/beacon/client.d.ts +6 -3
  71. package/build/src/beacon/client.d.ts.map +1 -1
  72. package/build/src/beacon/client.js +14 -7
  73. package/build/src/configuration-types.d.ts +1 -1
  74. package/build/src/configuration.d.ts +42 -29
  75. package/build/src/configuration.d.ts.map +1 -1
  76. package/build/src/configuration.js +52 -30
  77. package/build/src/controller.d.ts +3 -0
  78. package/build/src/controller.d.ts.map +1 -1
  79. package/build/src/controller.js +9 -4
  80. package/build/src/database/drivers/sqlite/base.js +2 -2
  81. package/build/src/database/drivers/sqlite/index.d.ts +7 -6
  82. package/build/src/database/drivers/sqlite/index.d.ts.map +1 -1
  83. package/build/src/database/drivers/sqlite/index.js +16 -15
  84. package/build/src/database/migrations-ledger.d.ts +16 -16
  85. package/build/src/database/migrations-ledger.js +17 -17
  86. package/build/src/database/pool/async-tracked-multi-connection.d.ts +0 -11
  87. package/build/src/database/pool/async-tracked-multi-connection.d.ts.map +1 -1
  88. package/build/src/database/pool/async-tracked-multi-connection.js +16 -1
  89. package/build/src/database/pool/base.d.ts +21 -0
  90. package/build/src/database/pool/base.d.ts.map +1 -1
  91. package/build/src/database/pool/base.js +24 -1
  92. package/build/src/database/query/where-model-class-hash.d.ts.map +1 -1
  93. package/build/src/database/query/where-model-class-hash.js +3 -1
  94. package/build/src/database/query/with-count.js +2 -2
  95. package/build/src/database/record/acts-as-list.js +3 -3
  96. package/build/src/database/record/auditing.d.ts +1 -1
  97. package/build/src/database/record/auditing.d.ts.map +1 -1
  98. package/build/src/database/record/auditing.js +24 -7
  99. package/build/src/database/record/index.d.ts +7 -7
  100. package/build/src/database/record/index.js +8 -8
  101. package/build/src/database/record/validators/uniqueness.d.ts +3 -3
  102. package/build/src/database/record/validators/uniqueness.js +4 -4
  103. package/build/src/database/tenants/data-copier.d.ts +18 -18
  104. package/build/src/database/tenants/data-copier.js +24 -24
  105. package/build/src/database/tenants/schema-cloner.d.ts +27 -27
  106. package/build/src/database/tenants/schema-cloner.js +28 -28
  107. package/build/src/environment-handlers/browser.d.ts +9 -0
  108. package/build/src/environment-handlers/browser.d.ts.map +1 -1
  109. package/build/src/environment-handlers/browser.js +1 -2
  110. package/build/src/environment-handlers/node/cli/commands/background-jobs-runner.d.ts.map +1 -1
  111. package/build/src/environment-handlers/node/cli/commands/background-jobs-runner.js +3 -2
  112. package/build/src/environment-handlers/node/cli/commands/lint/relationships.js +3 -3
  113. package/build/src/environment-handlers/node.js +2 -2
  114. package/build/src/frontend-model-controller.d.ts +9 -8
  115. package/build/src/frontend-model-controller.d.ts.map +1 -1
  116. package/build/src/frontend-model-controller.js +26 -19
  117. package/build/src/frontend-model-resource/base-resource.js +5 -5
  118. package/build/src/frontend-models/base.d.ts +4 -4
  119. package/build/src/frontend-models/base.d.ts.map +1 -1
  120. package/build/src/frontend-models/base.js +6 -6
  121. package/build/src/frontend-models/query.d.ts +7 -7
  122. package/build/src/frontend-models/query.js +13 -13
  123. package/build/src/http-server/client/index.d.ts +23 -1
  124. package/build/src/http-server/client/index.d.ts.map +1 -1
  125. package/build/src/http-server/client/index.js +55 -17
  126. package/build/src/http-server/client/request-runner.d.ts.map +1 -1
  127. package/build/src/http-server/client/request-runner.js +8 -2
  128. package/build/src/http-server/client/response.d.ts +11 -1
  129. package/build/src/http-server/client/response.d.ts.map +1 -1
  130. package/build/src/http-server/client/response.js +16 -2
  131. package/build/src/http-server/client/websocket-session.d.ts +13 -13
  132. package/build/src/http-server/client/websocket-session.js +14 -14
  133. package/build/src/http-server/server-client.d.ts +13 -0
  134. package/build/src/http-server/server-client.d.ts.map +1 -1
  135. package/build/src/http-server/server-client.js +94 -1
  136. package/build/src/http-server/websocket-channel.d.ts +12 -11
  137. package/build/src/http-server/websocket-channel.d.ts.map +1 -1
  138. package/build/src/http-server/websocket-channel.js +13 -12
  139. package/build/src/http-server/websocket-connection.d.ts +12 -11
  140. package/build/src/http-server/websocket-connection.d.ts.map +1 -1
  141. package/build/src/http-server/websocket-connection.js +13 -12
  142. package/build/src/http-server/websocket-events-host.d.ts +1 -1
  143. package/build/src/http-server/websocket-events-host.js +2 -2
  144. package/build/src/http-server/worker-handler/in-process.d.ts +2 -0
  145. package/build/src/http-server/worker-handler/in-process.d.ts.map +1 -1
  146. package/build/src/http-server/worker-handler/in-process.js +42 -11
  147. package/build/src/http-server/worker-handler/index.d.ts +21 -0
  148. package/build/src/http-server/worker-handler/index.d.ts.map +1 -1
  149. package/build/src/http-server/worker-handler/index.js +61 -7
  150. package/build/src/http-server/worker-handler/worker-thread.d.ts +30 -0
  151. package/build/src/http-server/worker-handler/worker-thread.d.ts.map +1 -1
  152. package/build/src/http-server/worker-handler/worker-thread.js +61 -1
  153. package/build/src/sync/device-identity.d.ts +2 -2
  154. package/build/src/sync/device-identity.d.ts.map +1 -1
  155. package/build/src/sync/device-identity.js +4 -4
  156. package/build/src/sync/local-mutation-log.d.ts +0 -1
  157. package/build/src/sync/local-mutation-log.d.ts.map +1 -1
  158. package/build/src/sync/local-mutation-log.js +1 -2
  159. package/build/src/sync/query-scope.d.ts +5 -0
  160. package/build/src/sync/query-scope.d.ts.map +1 -1
  161. package/build/src/sync/query-scope.js +7 -2
  162. package/build/src/sync/sync-client-types.d.ts +2 -2
  163. package/build/src/sync/sync-client-types.d.ts.map +1 -1
  164. package/build/src/sync/sync-client-types.js +1 -1
  165. package/build/src/sync/sync-client.d.ts +17 -2
  166. package/build/src/sync/sync-client.d.ts.map +1 -1
  167. package/build/src/sync/sync-client.js +28 -14
  168. package/build/src/sync/sync-realtime-bridge.d.ts +6 -2
  169. package/build/src/sync/sync-realtime-bridge.d.ts.map +1 -1
  170. package/build/src/sync/sync-realtime-bridge.js +14 -3
  171. package/build/src/sync/sync-resource-base.d.ts +14 -3
  172. package/build/src/sync/sync-resource-base.d.ts.map +1 -1
  173. package/build/src/sync/sync-resource-base.js +20 -4
  174. package/build/src/sync/sync-scope-store.d.ts +3 -3
  175. package/build/src/sync/sync-scope-store.d.ts.map +1 -1
  176. package/build/src/sync/sync-scope-store.js +7 -5
  177. package/build/src/sync/sync-websocket-channel.d.ts +10 -0
  178. package/build/src/sync/sync-websocket-channel.d.ts.map +1 -1
  179. package/build/src/sync/sync-websocket-channel.js +20 -3
  180. package/build/src/tenants/tenant-iterator.d.ts +8 -8
  181. package/build/src/tenants/tenant-iterator.js +9 -9
  182. package/build/src/tenants/tenant.d.ts +6 -6
  183. package/build/src/tenants/tenant.js +7 -7
  184. package/build/src/testing/test-runner.d.ts +16 -0
  185. package/build/src/testing/test-runner.d.ts.map +1 -1
  186. package/build/src/testing/test-runner.js +79 -19
  187. package/build/sync/device-identity.js +3 -3
  188. package/build/sync/local-mutation-log.js +0 -1
  189. package/build/sync/query-scope.js +6 -1
  190. package/build/sync/sync-client-types.js +1 -1
  191. package/build/sync/sync-client.js +28 -15
  192. package/build/sync/sync-realtime-bridge.js +13 -2
  193. package/build/sync/sync-resource-base.js +20 -3
  194. package/build/sync/sync-scope-store.js +6 -4
  195. package/build/sync/sync-websocket-channel.js +18 -2
  196. package/build/tenants/tenant-iterator.js +8 -8
  197. package/build/tenants/tenant.js +6 -6
  198. package/build/testing/test-runner.js +86 -20
  199. package/package.json +2 -2
  200. package/src/background-jobs/cron-expression.js +9 -9
  201. package/src/background-jobs/forked-runner-child.js +1 -1
  202. package/src/background-jobs/job-runner.js +21 -15
  203. package/src/background-jobs/json-socket.js +4 -0
  204. package/src/background-jobs/main.js +180 -18
  205. package/src/background-jobs/status-reporter.js +12 -5
  206. package/src/background-jobs/store.js +132 -50
  207. package/src/background-jobs/types.js +11 -3
  208. package/src/background-jobs/worker.js +9 -5
  209. package/src/beacon/client.js +13 -6
  210. package/src/configuration.js +54 -29
  211. package/src/controller.js +9 -3
  212. package/src/database/drivers/sqlite/base.js +1 -1
  213. package/src/database/drivers/sqlite/index.js +15 -14
  214. package/src/database/migrations-ledger.js +16 -16
  215. package/src/database/pool/async-tracked-multi-connection.js +16 -0
  216. package/src/database/pool/base.js +26 -0
  217. package/src/database/query/where-model-class-hash.js +2 -0
  218. package/src/database/query/with-count.js +1 -1
  219. package/src/database/record/acts-as-list.js +2 -2
  220. package/src/database/record/auditing.js +23 -6
  221. package/src/database/record/index.js +7 -7
  222. package/src/database/record/validators/uniqueness.js +3 -3
  223. package/src/database/tenants/data-copier.js +23 -23
  224. package/src/database/tenants/schema-cloner.js +27 -27
  225. package/src/environment-handlers/browser.js +0 -1
  226. package/src/environment-handlers/node/cli/commands/background-jobs-runner.js +2 -1
  227. package/src/environment-handlers/node/cli/commands/lint/relationships.js +2 -2
  228. package/src/environment-handlers/node.js +1 -1
  229. package/src/frontend-model-controller.js +26 -18
  230. package/src/frontend-model-resource/base-resource.js +4 -4
  231. package/src/frontend-models/base.js +5 -5
  232. package/src/frontend-models/query.js +12 -12
  233. package/src/http-server/client/index.js +61 -15
  234. package/src/http-server/client/request-runner.js +7 -1
  235. package/src/http-server/client/response.js +17 -1
  236. package/src/http-server/client/websocket-session.js +13 -13
  237. package/src/http-server/server-client.js +96 -0
  238. package/src/http-server/websocket-channel.js +12 -11
  239. package/src/http-server/websocket-connection.js +12 -11
  240. package/src/http-server/websocket-events-host.js +1 -1
  241. package/src/http-server/worker-handler/in-process.js +48 -9
  242. package/src/http-server/worker-handler/index.js +67 -7
  243. package/src/http-server/worker-handler/worker-thread.js +68 -0
  244. package/src/sync/device-identity.js +3 -3
  245. package/src/sync/local-mutation-log.js +0 -1
  246. package/src/sync/query-scope.js +6 -1
  247. package/src/sync/sync-client-types.js +1 -1
  248. package/src/sync/sync-client.js +28 -15
  249. package/src/sync/sync-realtime-bridge.js +13 -2
  250. package/src/sync/sync-resource-base.js +20 -3
  251. package/src/sync/sync-scope-store.js +6 -4
  252. package/src/sync/sync-websocket-channel.js +18 -2
  253. package/src/tenants/tenant-iterator.js +8 -8
  254. package/src/tenants/tenant.js +6 -6
  255. package/src/testing/test-runner.js +86 -20
@@ -306,44 +306,51 @@ export default class BackgroundJobsStore {
306
306
  * @param {object} args - Options.
307
307
  * @param {string} args.jobId - Job id.
308
308
  * @param {string} [args.workerId] - Worker id.
309
- * @returns {Promise<number>} - Resolves with handed off timestamp.
309
+ * @returns {Promise<import("./types.js").BackgroundJobHandoff | null>} - Claimed handoff lease, or null when no longer queued.
310
310
  */
311
311
  async markHandedOff({jobId, workerId}) {
312
312
  await this.ensureReady()
313
313
 
314
314
  const handedOffAtMs = Date.now()
315
+ const handoffId = randomUUID()
315
316
 
316
- await this._withDb(async (db) => {
317
+ return await this._withDb(async (db) => {
317
318
  await db.update({
318
319
  tableName: JOBS_TABLE,
319
320
  data: {
320
321
  status: "handed_off",
321
322
  handed_off_at_ms: handedOffAtMs,
323
+ handoff_id: handoffId,
322
324
  worker_id: workerId || null
323
325
  },
324
- conditions: {id: jobId}
326
+ conditions: {id: jobId, status: "queued"}
325
327
  })
326
- })
327
328
 
328
- return handedOffAtMs
329
+ const job = await this._getJobRowById(db, jobId)
330
+
331
+ if (job?.status !== "handed_off" || job.handoffId !== handoffId) return null
332
+
333
+ return {handedOffAtMs, handoffId}
334
+ })
329
335
  }
330
336
 
331
337
  /**
332
338
  * Runs mark completed.
333
339
  * @param {object} args - Options.
334
340
  * @param {string} args.jobId - Job id.
341
+ * @param {string} [args.handoffId] - Handoff lease id.
335
342
  * @param {string} [args.workerId] - Worker id.
336
343
  * @param {number} [args.handedOffAtMs] - Handed off timestamp.
337
- * @returns {Promise<void>} - Resolves when updated.
344
+ * @returns {Promise<boolean>} - Whether the fenced report was accepted.
338
345
  */
339
- async markCompleted({jobId, workerId, handedOffAtMs}) {
346
+ async markCompleted({jobId, handoffId, workerId, handedOffAtMs}) {
340
347
  await this.ensureReady()
341
348
 
342
- await this._withDb(async (db) => {
349
+ return await this._withDb(async (db) => await this._withHandoffLock({db, jobId}, async () => {
343
350
  const job = await this._getJobRowById(db, jobId)
344
351
 
345
- if (!job) return
346
- if (!this._shouldAcceptReport({job, workerId, handedOffAtMs})) return
352
+ if (!job) return false
353
+ if (!this._shouldAcceptReport({job, handoffId, workerId, handedOffAtMs})) return false
347
354
 
348
355
  await db.update({
349
356
  tableName: JOBS_TABLE,
@@ -351,18 +358,23 @@ export default class BackgroundJobsStore {
351
358
  status: "completed",
352
359
  completed_at_ms: Date.now()
353
360
  },
354
- conditions: {id: jobId}
361
+ conditions: this._activeHandoffConditions(job)
355
362
  })
356
- })
363
+
364
+ const updatedJob = await this._getJobRowById(db, jobId)
365
+
366
+ return updatedJob?.status === "completed" && updatedJob.handoffId === job.handoffId
367
+ }))
357
368
  }
358
369
 
359
370
  /**
360
371
  * Runs mark returned to queue.
361
372
  * @param {object} args - Options.
362
373
  * @param {string} args.jobId - Job id.
374
+ * @param {string} args.handoffId - Handoff lease id.
363
375
  * @returns {Promise<void>} - Resolves when updated.
364
376
  */
365
- async markReturnedToQueue({jobId}) {
377
+ async markReturnedToQueue({jobId, handoffId}) {
366
378
  await this.ensureReady()
367
379
 
368
380
  await this._withDb(async (db) => {
@@ -372,9 +384,10 @@ export default class BackgroundJobsStore {
372
384
  status: "queued",
373
385
  scheduled_at_ms: Date.now(),
374
386
  handed_off_at_ms: null,
387
+ handoff_id: null,
375
388
  worker_id: null
376
389
  },
377
- conditions: {id: jobId}
390
+ conditions: {handoff_id: handoffId, id: jobId, status: "handed_off"}
378
391
  })
379
392
  })
380
393
  }
@@ -384,21 +397,22 @@ export default class BackgroundJobsStore {
384
397
  * @param {object} args - Options.
385
398
  * @param {string} args.jobId - Job id.
386
399
  * @param {?} args.error - Error.
400
+ * @param {string} [args.handoffId] - Handoff lease id.
387
401
  * @param {string} [args.workerId] - Worker id.
388
402
  * @param {number} [args.handedOffAtMs] - Handed off timestamp.
389
403
  * @returns {Promise<import("./types.js").BackgroundJobRow | null>} - Updated job row when the report was accepted.
390
404
  */
391
- async markFailed({jobId, error, workerId, handedOffAtMs}) {
405
+ async markFailed({jobId, error, handoffId, workerId, handedOffAtMs}) {
392
406
  await this.ensureReady()
393
407
 
394
- return await this._withDb(async (db) => {
408
+ return await this._withDb(async (db) => await this._withHandoffLock({db, jobId}, async () => {
395
409
  const job = await this._getJobRowById(db, jobId)
396
410
 
397
411
  if (!job) return null
398
- if (!this._shouldAcceptReport({job, workerId, handedOffAtMs})) return null
412
+ if (!this._shouldAcceptReport({job, handoffId, workerId, handedOffAtMs})) return null
399
413
 
400
414
  return await this._applyFailure({db, job, error, markOrphaned: false})
401
- })
415
+ }))
402
416
  }
403
417
 
404
418
  /**
@@ -420,18 +434,22 @@ export default class BackgroundJobsStore {
420
434
 
421
435
  const rows = await query.results()
422
436
 
437
+ let orphanedCount = 0
438
+
423
439
  for (const row of rows) {
424
440
  const job = this._normalizeJobRow(row)
425
441
 
426
- await this._applyFailure({
442
+ const orphanedJob = await this._applyFailure({
427
443
  db,
428
444
  job,
429
445
  error: "Job orphaned after timeout",
430
446
  markOrphaned: true
431
447
  })
448
+
449
+ if (orphanedJob) orphanedCount += 1
432
450
  }
433
451
 
434
- return rows.length
452
+ return orphanedCount
435
453
  })
436
454
  }
437
455
 
@@ -562,6 +580,7 @@ export default class BackgroundJobsStore {
562
580
  table.bigint("scheduled_at_ms", {null: false, index: true})
563
581
  table.bigint("created_at_ms", {null: false, index: true})
564
582
  table.bigint("handed_off_at_ms", {null: true, index: true})
583
+ table.string("handoff_id", {null: true})
565
584
  table.bigint("completed_at_ms", {null: true})
566
585
  table.bigint("failed_at_ms", {null: true})
567
586
  table.bigint("orphaned_at_ms", {null: true, index: true})
@@ -594,6 +613,35 @@ export default class BackgroundJobsStore {
594
613
  db.clearSchemaCache()
595
614
  }
596
615
 
616
+ const refreshedTable = await db.getTableByNameOrFail(JOBS_TABLE)
617
+ const handoffIdColumn = await refreshedTable.getColumnByName("handoff_id")
618
+
619
+ if (!handoffIdColumn) {
620
+ const lockName = `${MIGRATION_SCOPE}:handoff_id_column`
621
+ const acquired = await db.acquireAdvisoryLock(lockName)
622
+
623
+ if (!acquired) throw new Error("Failed to acquire background jobs handoff schema lock")
624
+
625
+ try {
626
+ db.clearSchemaCache()
627
+ const lockedTable = await db.getTableByNameOrFail(JOBS_TABLE)
628
+
629
+ if (!(await lockedTable.getColumnByName("handoff_id"))) {
630
+ const tableData = new TableData(JOBS_TABLE)
631
+ tableData.string("handoff_id", {null: true})
632
+ const sqls = await db.alterTableSQLs(tableData)
633
+
634
+ for (const sql of sqls) {
635
+ await db.query(sql)
636
+ }
637
+
638
+ db.clearSchemaCache()
639
+ }
640
+ } finally {
641
+ await db.releaseAdvisoryLock(lockName)
642
+ }
643
+ }
644
+
597
645
  await this._backfillExecutionModesOnce(db)
598
646
  }
599
647
 
@@ -639,14 +687,16 @@ export default class BackgroundJobsStore {
639
687
  * @returns {Promise<void>} - Resolves when complete.
640
688
  */
641
689
  async _recordMigration(db, version) {
642
- await db.insert({
690
+ await db.upsert({
643
691
  tableName: MIGRATIONS_TABLE,
644
692
  data: {
645
693
  key: this._migrationKey(version),
646
694
  scope: MIGRATION_SCOPE,
647
695
  version,
648
696
  applied_at_ms: Date.now()
649
- }
697
+ },
698
+ conflictColumns: ["key"],
699
+ updateColumns: ["scope", "version", "applied_at_ms"]
650
700
  })
651
701
  }
652
702
 
@@ -689,7 +739,7 @@ export default class BackgroundJobsStore {
689
739
  * @param {import("./types.js").BackgroundJobRow} args.job - Job row.
690
740
  * @param {?} args.error - Error.
691
741
  * @param {boolean} args.markOrphaned - Whether marking orphaned.
692
- * @returns {Promise<import("./types.js").BackgroundJobRow>} - Updated job row.
742
+ * @returns {Promise<import("./types.js").BackgroundJobRow | null>} - Updated job row when the lease transition won.
693
743
  */
694
744
  async _applyFailure({db, job, error, markOrphaned}) {
695
745
  const now = Date.now()
@@ -710,10 +760,17 @@ export default class BackgroundJobsStore {
710
760
  await db.update({
711
761
  tableName: JOBS_TABLE,
712
762
  data: update,
713
- conditions: {id: job.id}
763
+ conditions: this._activeHandoffConditions(job)
714
764
  })
715
765
 
716
- return this._jobWithFailureUpdate({failureMessage, job, nextAttempt, update})
766
+ const updatedJob = await this._getJobRowById(db, job.id)
767
+
768
+ if (!updatedJob) return null
769
+ if (updatedJob.handoffId !== job.handoffId) return null
770
+ if (updatedJob.attempts !== nextAttempt) return null
771
+ if (updatedJob.status !== update.status) return null
772
+
773
+ return updatedJob
717
774
  }
718
775
 
719
776
  /**
@@ -782,29 +839,6 @@ export default class BackgroundJobsStore {
782
839
  update.failed_at_ms = now
783
840
  }
784
841
 
785
- /**
786
- * Runs job with failure update.
787
- * @param {object} args - Options.
788
- * @param {string} args.failureMessage - Last failure message.
789
- * @param {import("./types.js").BackgroundJobRow} args.job - Job row.
790
- * @param {number} args.nextAttempt - Next attempt count.
791
- * @param {Record<string, ?>} args.update - Database update data.
792
- * @returns {import("./types.js").BackgroundJobRow} - Updated job row.
793
- */
794
- _jobWithFailureUpdate({failureMessage, job, nextAttempt, update}) {
795
- return {
796
- ...job,
797
- attempts: nextAttempt,
798
- failedAtMs: update.failed_at_ms ?? job.failedAtMs,
799
- handedOffAtMs: null,
800
- lastError: failureMessage,
801
- orphanedAtMs: update.orphaned_at_ms ?? job.orphanedAtMs,
802
- scheduledAtMs: update.scheduled_at_ms ?? job.scheduledAtMs,
803
- status: update.status,
804
- workerId: null
805
- }
806
- }
807
-
808
842
  /**
809
843
  * Runs normalize job row.
810
844
  * @param {Record<string, ?>} row - Raw database row.
@@ -827,6 +861,7 @@ export default class BackgroundJobsStore {
827
861
  scheduledAtMs: this._normalizeNumber(row.scheduled_at_ms),
828
862
  createdAtMs: this._normalizeNumber(row.created_at_ms),
829
863
  handedOffAtMs: this._normalizeNumber(row.handed_off_at_ms),
864
+ handoffId: row.handoff_id ? String(row.handoff_id) : null,
830
865
  completedAtMs: this._normalizeNumber(row.completed_at_ms),
831
866
  failedAtMs: this._normalizeNumber(row.failed_at_ms),
832
867
  orphanedAtMs: this._normalizeNumber(row.orphaned_at_ms),
@@ -937,18 +972,65 @@ export default class BackgroundJobsStore {
937
972
  return /** @type {T} */ (result)
938
973
  }
939
974
 
975
+ /**
976
+ * Serializes reports for one job so duplicate reports cannot both appear accepted.
977
+ * @template T
978
+ * @param {object} args - Options.
979
+ * @param {import("../database/drivers/base.js").default} args.db - Database connection.
980
+ * @param {string} args.jobId - Job id.
981
+ * @param {() => Promise<T>} callback - Locked callback.
982
+ * @returns {Promise<T>} - Callback result.
983
+ */
984
+ async _withHandoffLock({db, jobId}, callback) {
985
+ const lockName = `background-job:${jobId}`
986
+ const acquired = await db.acquireAdvisoryLock(lockName)
987
+
988
+ if (!acquired) throw new Error(`Failed to acquire background job handoff lock: ${jobId}`)
989
+
990
+ try {
991
+ return await callback()
992
+ } finally {
993
+ await db.releaseAdvisoryLock(lockName)
994
+ }
995
+ }
996
+
940
997
  /**
941
998
  * Runs should accept report.
942
999
  * @param {object} args - Options.
943
1000
  * @param {import("./types.js").BackgroundJobRow} args.job - Job row.
1001
+ * @param {string | null | undefined} args.handoffId - Handoff lease id from report.
944
1002
  * @param {string | null | undefined} args.workerId - Worker id from report.
945
1003
  * @param {number | null | undefined} args.handedOffAtMs - Handed off timestamp from report.
946
1004
  * @returns {boolean} - Whether to accept the report.
947
1005
  */
948
- _shouldAcceptReport({job, workerId, handedOffAtMs}) {
1006
+ _shouldAcceptReport({job, handoffId, workerId, handedOffAtMs}) {
949
1007
  if (job.status !== "handed_off") return false
950
1008
 
951
- return this._workerReportMatches({job, workerId}) && this._handoffReportMatches({handedOffAtMs, job})
1009
+ return this._handoffIdReportMatches({handoffId, job})
1010
+ && this._workerReportMatches({job, workerId})
1011
+ && this._handoffReportMatches({handedOffAtMs, job})
1012
+ }
1013
+
1014
+ /**
1015
+ * Runs active handoff conditions.
1016
+ * @param {import("./types.js").BackgroundJobRow} job - Job row.
1017
+ * @returns {Record<string, string | null>} - Conditional transition fence.
1018
+ */
1019
+ _activeHandoffConditions(job) {
1020
+ return {handoff_id: job.handoffId, id: job.id, status: "handed_off"}
1021
+ }
1022
+
1023
+ /**
1024
+ * Runs handoff id report matches.
1025
+ * @param {object} args - Options.
1026
+ * @param {string | null | undefined} args.handoffId - Handoff lease id from report.
1027
+ * @param {import("./types.js").BackgroundJobRow} args.job - Job row.
1028
+ * @returns {boolean} - Whether the handoff lease matches.
1029
+ */
1030
+ _handoffIdReportMatches({handoffId, job}) {
1031
+ if (!job.handoffId) return true
1032
+
1033
+ return handoffId === job.handoffId
952
1034
  }
953
1035
 
954
1036
  /**
@@ -3,6 +3,11 @@
3
3
  /**
4
4
  * @typedef {"inline" | "forked" | "spawned"} BackgroundJobExecutionMode
5
5
  */
6
+ /**
7
+ * @typedef {object} BackgroundJobHandoff
8
+ * @property {string} handoffId - Unique handoff lease id.
9
+ * @property {number} handedOffAtMs - Time handed to a worker in ms.
10
+ */
6
11
  /**
7
12
  * @typedef {object} BackgroundJobOptions
8
13
  * @property {BackgroundJobExecutionMode} [executionMode] - How the job should run. Defaults to `"forked"`.
@@ -14,6 +19,7 @@
14
19
  * @property {string} [id] - Job id.
15
20
  * @property {string} jobName - Job class name.
16
21
  * @property {Array<?>} [args] - Serialized job arguments.
22
+ * @property {string} [handoffId] - Unique handoff lease id.
17
23
  * @property {string} [workerId] - Worker id handling the job.
18
24
  * @property {number} [handedOffAtMs] - Time handed to a worker in ms.
19
25
  * @property {BackgroundJobOptions} [options] - Runtime options.
@@ -31,6 +37,7 @@
31
37
  * @property {number | null} scheduledAtMs - Next scheduled time in ms.
32
38
  * @property {number | null} createdAtMs - Creation time in ms.
33
39
  * @property {number | null} handedOffAtMs - Time handed to worker in ms.
40
+ * @property {string | null} handoffId - Unique latest handoff lease id.
34
41
  * @property {number | null} completedAtMs - Completion time in ms.
35
42
  * @property {number | null} failedAtMs - Failure time in ms.
36
43
  * @property {number | null} orphanedAtMs - Orphaned time in ms.
@@ -44,6 +51,7 @@
44
51
  * @property {number | null} attempts - Updated failure attempts count.
45
52
  * @property {boolean} terminal - Whether this failure ended the job.
46
53
  * @property {boolean} willRetry - Whether the job was returned to the queue.
54
+ * @property {string | undefined} handoffId - Handoff lease id from the worker report.
47
55
  * @property {number | undefined} handedOffAtMs - Handoff timestamp from the worker report.
48
56
  * @property {string | undefined} workerId - Worker id from the worker report.
49
57
  */
@@ -51,15 +59,15 @@
51
59
  * @typedef {"worker" | "client" | "reporter"} BackgroundJobSocketRole
52
60
  */
53
61
  /**
54
- * @typedef {{type: "hello", role: BackgroundJobSocketRole, workerId?: string}} BackgroundJobHelloMessage
62
+ * @typedef {{type: "hello", role: BackgroundJobSocketRole, supportsHandoffIdReporting?: boolean, workerId?: string}} BackgroundJobHelloMessage
55
63
  * @typedef {{type: "ready", acceptsForked?: boolean, acceptsInline?: boolean, acceptsSpawned?: boolean}} BackgroundJobReadyMessage
56
64
  * @typedef {{type: "draining"}} BackgroundJobDrainingMessage
57
65
  * @typedef {{type: "enqueue", jobName: string, args?: Array<?>, options?: BackgroundJobOptions}} BackgroundJobEnqueueMessage
58
66
  * @typedef {{type: "enqueued", jobId: string}} BackgroundJobEnqueuedMessage
59
67
  * @typedef {{type: "enqueue-error", error?: string}} BackgroundJobEnqueueErrorMessage
60
68
  * @typedef {{type: "job", payload: BackgroundJobPayload}} BackgroundJobJobMessage
61
- * @typedef {{type: "job-complete", jobId: string, workerId?: string, handedOffAtMs?: number}} BackgroundJobCompleteMessage
62
- * @typedef {{type: "job-failed", jobId: string, error?: ?, workerId?: string, handedOffAtMs?: number}} BackgroundJobFailedMessage
69
+ * @typedef {{type: "job-complete", jobId: string, handoffId?: string, workerId?: string, handedOffAtMs?: number}} BackgroundJobCompleteMessage
70
+ * @typedef {{type: "job-failed", jobId: string, error?: ?, handoffId?: string, workerId?: string, handedOffAtMs?: number}} BackgroundJobFailedMessage
63
71
  * @typedef {{type: "job-updated", jobId: string}} BackgroundJobUpdatedMessage
64
72
  * @typedef {{type: "job-update-error", jobId: string, error?: string}} BackgroundJobUpdateErrorMessage
65
73
  */
@@ -261,7 +261,7 @@ export default class BackgroundJobsWorker {
261
261
  })
262
262
 
263
263
  socket.on("connect", () => {
264
- jsonSocket.send({type: "hello", role: "worker", workerId: this.workerId})
264
+ jsonSocket.send({type: "hello", role: "worker", supportsHandoffIdReporting: true, workerId: this.workerId})
265
265
  this._sendReadyIfRunning()
266
266
  })
267
267
  }
@@ -399,6 +399,7 @@ export default class BackgroundJobsWorker {
399
399
  await this._reportJobResult({
400
400
  jobId: payload.id,
401
401
  status: "completed",
402
+ handoffId: payload.handoffId,
402
403
  handedOffAtMs: payload.handedOffAtMs,
403
404
  workerId: payload.workerId || this.workerId
404
405
  })
@@ -407,6 +408,7 @@ export default class BackgroundJobsWorker {
407
408
  jobId: payload.id,
408
409
  status: "failed",
409
410
  error,
411
+ handoffId: payload.handoffId,
410
412
  handedOffAtMs: payload.handedOffAtMs,
411
413
  workerId: payload.workerId || this.workerId
412
414
  })
@@ -533,7 +535,7 @@ export default class BackgroundJobsWorker {
533
535
  * @param {object} args - Options.
534
536
  * @param {import("node:child_process").ChildProcess} args.child - Forked child process.
535
537
  * @param {number | null} args.code - Exit code.
536
- * @param {NodeJS.Signals | null} args.signal - Exit signal.
538
+ * @param {keyof typeof import("node:os").constants.signals | null} args.signal - Exit signal.
537
539
  * @param {import("./types.js").BackgroundJobPayload & {id: string}} args.payload - Payload.
538
540
  * @param {(value: void) => void} args.resolve - Promise resolver.
539
541
  * @returns {Promise<void>} - Resolves after failure is reported.
@@ -558,7 +560,7 @@ export default class BackgroundJobsWorker {
558
560
  * Runs forked child exited cleanly.
559
561
  * @param {object} args - Options.
560
562
  * @param {number | null} args.code - Exit code.
561
- * @param {NodeJS.Signals | null} args.signal - Exit signal.
563
+ * @param {keyof typeof import("node:os").constants.signals | null} args.signal - Exit signal.
562
564
  * @returns {boolean} - Whether the child exited cleanly.
563
565
  */
564
566
  _forkedChildExitedCleanly({code, signal}) {
@@ -609,6 +611,7 @@ export default class BackgroundJobsWorker {
609
611
  jobId: payload.id,
610
612
  status: "failed",
611
613
  error,
614
+ handoffId: payload.handoffId,
612
615
  handedOffAtMs: payload.handedOffAtMs,
613
616
  workerId: payload.workerId || this.workerId
614
617
  })
@@ -665,15 +668,16 @@ export default class BackgroundJobsWorker {
665
668
  * @param {string} args.jobId - Job id.
666
669
  * @param {"completed" | "failed"} args.status - Status.
667
670
  * @param {?} [args.error] - Error.
671
+ * @param {string} [args.handoffId] - Handoff lease id.
668
672
  * @param {number} [args.handedOffAtMs] - Handed off timestamp.
669
673
  * @param {string} [args.workerId] - Worker id.
670
674
  * @returns {Promise<void>} - Resolves when reported.
671
675
  */
672
- async _reportJobResult({jobId, status, error, handedOffAtMs, workerId}) {
676
+ async _reportJobResult({jobId, status, error, handoffId, handedOffAtMs, workerId}) {
673
677
  if (!this.statusReporter) return
674
678
 
675
679
  try {
676
- await this.statusReporter.reportWithRetry({jobId, status, error, handedOffAtMs, workerId})
680
+ await this.statusReporter.reportWithRetry({jobId, status, error, handoffId, handedOffAtMs, workerId})
677
681
  } catch (reportError) {
678
682
  console.error("Background job status reporting failed:", reportError)
679
683
  }
@@ -9,6 +9,7 @@ import EventEmitter from "../utils/event-emitter.js"
9
9
 
10
10
  const DEFAULT_RECONNECT_DELAY_MS = 1000
11
11
  const MAX_RECONNECT_DELAY_MS = 30_000
12
+ const DEFAULT_CLOSE_TIMEOUT_MS = 1000
12
13
 
13
14
  /**
14
15
  * BeaconBroadcastHandler type.
@@ -43,8 +44,9 @@ export default class BeaconClient extends EventEmitter {
43
44
  * @param {string} [args.peerId] - Optional explicit peer id (defaults to a random UUID).
44
45
  * @param {number} [args.reconnectDelayMs] - Starting reconnect delay in ms.
45
46
  * @param {number} [args.maxReconnectDelayMs] - Maximum reconnect delay in ms.
47
+ * @param {number} [args.closeTimeoutMs] - Maximum graceful socket close wait in ms.
46
48
  */
47
- constructor({host, port, peerType, peerId, reconnectDelayMs, maxReconnectDelayMs}) {
49
+ constructor({host, port, peerType, peerId, reconnectDelayMs, maxReconnectDelayMs, closeTimeoutMs}) {
48
50
  super()
49
51
  this.host = host
50
52
  this.port = port
@@ -53,6 +55,7 @@ export default class BeaconClient extends EventEmitter {
53
55
  this._initialReconnectDelayMs = reconnectDelayMs ?? DEFAULT_RECONNECT_DELAY_MS
54
56
  this._maxReconnectDelayMs = maxReconnectDelayMs ?? MAX_RECONNECT_DELAY_MS
55
57
  this._reconnectDelayMs = this._initialReconnectDelayMs
58
+ this._closeTimeoutMs = closeTimeoutMs ?? DEFAULT_CLOSE_TIMEOUT_MS
56
59
  /**
57
60
  * Narrows the runtime value to the documented type.
58
61
  * @type {JsonSocket | undefined} */
@@ -66,7 +69,7 @@ export default class BeaconClient extends EventEmitter {
66
69
  this._closed = false
67
70
  /**
68
71
  * Narrows the runtime value to the documented type.
69
- * @type {NodeJS.Timeout | undefined} */
72
+ * @type {ReturnType<typeof setTimeout> | undefined} */
70
73
  this._reconnectTimer = undefined
71
74
  /**
72
75
  * Narrows the runtime value to the documented type.
@@ -235,10 +238,14 @@ export default class BeaconClient extends EventEmitter {
235
238
 
236
239
  if (socket.destroyed) return
237
240
 
238
- await new Promise((resolve) => {
239
- socket.once("close", () => resolve(undefined))
240
- socket.end()
241
- socket.destroySoon()
241
+ await timeout({timeout: this._closeTimeoutMs}, async () => {
242
+ await new Promise((resolve) => {
243
+ socket.once("close", () => resolve(undefined))
244
+ socket.end()
245
+ socket.destroySoon()
246
+ })
247
+ }).catch(() => {
248
+ socket.destroy()
242
249
  })
243
250
  }
244
251