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
@@ -50,9 +50,11 @@ async function connectBeacon(configuration) {
50
50
  /**
51
51
  * Runs run job payload.
52
52
  * @param {import("./types.js").BackgroundJobPayload} payload - Payload.
53
+ * @param {object} [options] - Runner options.
54
+ * @param {boolean} [options.closeConnections] - Whether to gracefully close framework connections after the job.
53
55
  * @returns {Promise<void>} - Resolves when complete.
54
56
  */
55
- export default async function runJobPayload(payload) {
57
+ export default async function runJobPayload(payload, {closeConnections = true} = {}) {
56
58
  const configuration = await configurationResolver()
57
59
  configuration.setCurrent()
58
60
  await configuration.initialize({type: "background-jobs-runner"})
@@ -73,22 +75,13 @@ export default async function runJobPayload(payload) {
73
75
  await configuration.withConnections({name: `Background job runner: ${payload.jobName}`}, async () => {
74
76
  await perform.apply(jobInstance, payload.args || [])
75
77
  })
76
-
77
- if (payload.id) {
78
- await reporter.reportWithRetry({
79
- jobId: payload.id,
80
- status: "completed",
81
- workerId: payload.workerId,
82
- handedOffAtMs: payload.handedOffAtMs,
83
- maxDurationMs: 30000
84
- })
85
- }
86
78
  } catch (error) {
87
79
  if (payload.id) {
88
80
  await reporter.reportWithRetry({
89
81
  jobId: payload.id,
90
82
  status: "failed",
91
83
  error,
84
+ handoffId: payload.handoffId,
92
85
  workerId: payload.workerId,
93
86
  handedOffAtMs: payload.handedOffAtMs,
94
87
  maxDurationMs: 30000
@@ -97,11 +90,24 @@ export default async function runJobPayload(payload) {
97
90
 
98
91
  throw error
99
92
  }
93
+
94
+ if (payload.id) {
95
+ await reporter.reportWithRetry({
96
+ jobId: payload.id,
97
+ status: "completed",
98
+ handoffId: payload.handoffId,
99
+ workerId: payload.workerId,
100
+ handedOffAtMs: payload.handedOffAtMs,
101
+ maxDurationMs: 30000
102
+ })
103
+ }
100
104
  } finally {
101
- try {
102
- await configuration.disconnectBeacon()
103
- } finally {
104
- await configuration.closeDatabaseConnections()
105
+ if (closeConnections) {
106
+ try {
107
+ await configuration.disconnectBeacon()
108
+ } finally {
109
+ await configuration.closeDatabaseConnections()
110
+ }
105
111
  }
106
112
  }
107
113
  }
@@ -14,6 +14,10 @@ export default class JsonSocket extends EventEmitter {
14
14
  * Narrows the runtime value to the documented type.
15
15
  * @type {string | undefined} */
16
16
  this.workerId = undefined
17
+ /**
18
+ * Narrows the runtime value to the documented type.
19
+ * @type {boolean} */
20
+ this.supportsHandoffIdReporting = false
17
21
  /**
18
22
  * Narrows the runtime value to the documented type.
19
23
  * @type {boolean} */
@@ -64,25 +64,29 @@ export default class BackgroundJobsMain {
64
64
  * Narrows the runtime value to the documented type.
65
65
  * @type {Set<JsonSocket>} */
66
66
  this.readyWorkers = new Set()
67
+ /**
68
+ * Active durable handoffs keyed by the exact worker socket that received them.
69
+ * @type {Map<JsonSocket, Map<string, string>>} */
70
+ this.workerHandoffs = new Map()
67
71
  /**
68
72
  * Narrows the runtime value to the documented type.
69
73
  * @type {net.Server | undefined} */
70
74
  this.server = undefined
71
75
  /**
72
76
  * Narrows the runtime value to the documented type.
73
- * @type {NodeJS.Timeout | undefined} */
77
+ * @type {ReturnType<typeof setTimeout> | undefined} */
74
78
  this._pollTimer = undefined
75
79
  /**
76
80
  * Narrows the runtime value to the documented type.
77
- * @type {NodeJS.Timeout | undefined} */
81
+ * @type {ReturnType<typeof setTimeout> | undefined} */
78
82
  this._scheduledTimer = undefined
79
83
  /**
80
84
  * Narrows the runtime value to the documented type.
81
- * @type {NodeJS.Timeout | undefined} */
85
+ * @type {ReturnType<typeof setTimeout> | undefined} */
82
86
  this._errorRetryTimer = undefined
83
87
  /**
84
88
  * Narrows the runtime value to the documented type.
85
- * @type {NodeJS.Timeout | undefined} */
89
+ * @type {ReturnType<typeof setTimeout> | undefined} */
86
90
  this._orphanTimer = undefined
87
91
  /**
88
92
  * Narrows the runtime value to the documented type.
@@ -270,7 +274,7 @@ export default class BackgroundJobsMain {
270
274
  _setupDispatchTriggers() {
271
275
  if (this.dispatchStrategy === "polling") {
272
276
  this._pollTimer = setInterval(() => {
273
- void this._drain()
277
+ void this._retryAfterError()
274
278
  }, this.pollIntervalMs)
275
279
  return
276
280
  }
@@ -330,11 +334,12 @@ export default class BackgroundJobsMain {
330
334
  * @type {import("./types.js").BackgroundJobSocketRole | null} */
331
335
  let role = null
332
336
 
337
+ let cleanedUp = false
333
338
  const cleanup = () => {
334
- if (role === "worker") {
335
- this.workers.delete(jsonSocket)
336
- this.readyWorkers.delete(jsonSocket)
337
- }
339
+ if (cleanedUp) return
340
+ cleanedUp = true
341
+
342
+ if (role === "worker") void this._handleWorkerSocketClosed(jsonSocket)
338
343
  }
339
344
 
340
345
  jsonSocket.on("close", cleanup)
@@ -377,7 +382,9 @@ export default class BackgroundJobsMain {
377
382
 
378
383
  if (message.role === "worker") {
379
384
  jsonSocket.workerId = message.workerId
385
+ jsonSocket.supportsHandoffIdReporting = message.supportsHandoffIdReporting === true
380
386
  this.workers.add(jsonSocket)
387
+ this.workerHandoffs.set(jsonSocket, new Map())
381
388
  }
382
389
 
383
390
  return message.role
@@ -446,7 +453,11 @@ export default class BackgroundJobsMain {
446
453
  jsonSocket.acceptsSpawnedJobs = message.acceptsSpawned !== false && message.acceptsForked !== false
447
454
  jsonSocket.acceptsForkedJobs = message.acceptsForked !== false
448
455
  jsonSocket.acceptsInlineJobs = message.acceptsInline !== false
449
- this.readyWorkers.add(jsonSocket)
456
+ if (jsonSocket.supportsHandoffIdReporting) {
457
+ this.readyWorkers.add(jsonSocket)
458
+ } else {
459
+ this.readyWorkers.delete(jsonSocket)
460
+ }
450
461
  void this._drain()
451
462
  }
452
463
 
@@ -463,6 +474,98 @@ export default class BackgroundJobsMain {
463
474
  this.readyWorkers.delete(jsonSocket)
464
475
  }
465
476
 
477
+ /**
478
+ * Removes a lost worker socket and releases only leases dispatched through it.
479
+ * @param {JsonSocket} worker - Disconnected worker socket.
480
+ * @returns {Promise<void>} - Resolves after its active leases are released.
481
+ */
482
+ async _handleWorkerSocketClosed(worker) {
483
+ this.workers.delete(worker)
484
+ this.readyWorkers.delete(worker)
485
+
486
+ if (this._stopped) {
487
+ this.workerHandoffs.delete(worker)
488
+ return
489
+ }
490
+
491
+ try {
492
+ await this._releaseWorkerHandoffs(worker)
493
+ } catch (error) {
494
+ this._reportHandoffReleaseError(error)
495
+ this._scheduleErrorRetry()
496
+ }
497
+ }
498
+
499
+ /**
500
+ * Releases all leases still owned by one exact worker socket.
501
+ * @param {JsonSocket} worker - Worker socket.
502
+ * @returns {Promise<void>} - Resolves after fenced releases and dispatch wake-up.
503
+ */
504
+ async _releaseWorkerHandoffs(worker) {
505
+ const handoffs = this.workerHandoffs.get(worker)
506
+
507
+ if (!handoffs || handoffs.size === 0) {
508
+ this.workerHandoffs.delete(worker)
509
+ return
510
+ }
511
+
512
+ for (const [jobId, handoffId] of handoffs) {
513
+ await this._releaseHandoff({handoffId, jobId, worker})
514
+ }
515
+
516
+ this.workerHandoffs.delete(worker)
517
+ this._notifyEnqueued()
518
+ await this._drain()
519
+ }
520
+
521
+ /**
522
+ * Runs one idempotent conditional lease release.
523
+ * @param {object} args - Options.
524
+ * @param {string} args.handoffId - Handoff lease id.
525
+ * @param {string} args.jobId - Job id.
526
+ * @param {JsonSocket} args.worker - Socket that received the lease.
527
+ * @returns {Promise<void>} - Resolves after the fenced transition.
528
+ */
529
+ async _releaseHandoff({handoffId, jobId, worker}) {
530
+ await this.store.markReturnedToQueue({handoffId, jobId})
531
+
532
+ const handoffs = this.workerHandoffs.get(worker)
533
+
534
+ if (handoffs?.get(jobId) === handoffId) handoffs.delete(jobId)
535
+ }
536
+
537
+ /**
538
+ * Forgets a successfully reported lease without relying on worker ids.
539
+ * @param {object} args - Options.
540
+ * @param {string} args.handoffId - Handoff lease id.
541
+ * @param {string} args.jobId - Job id.
542
+ * @returns {void}
543
+ */
544
+ _forgetHandoff({handoffId, jobId}) {
545
+ for (const [worker, handoffs] of this.workerHandoffs) {
546
+ if (handoffs.get(jobId) !== handoffId) continue
547
+
548
+ handoffs.delete(jobId)
549
+ if (handoffs.size === 0 && !this.workers.has(worker)) this.workerHandoffs.delete(worker)
550
+ return
551
+ }
552
+ }
553
+
554
+ /**
555
+ * Reports an unexpected lease-release failure on framework error channels.
556
+ * @param {?} error - Release failure.
557
+ * @returns {void}
558
+ */
559
+ _reportHandoffReleaseError(error) {
560
+ const normalizedError = error instanceof Error ? error : new Error(String(error))
561
+ const payload = {context: {stage: "background-job-handoff-release"}, error: normalizedError}
562
+ const errorEvents = this.configuration.getErrorEvents()
563
+
564
+ this.logger.error(() => ["Failed to release disconnected worker handoffs:", normalizedError])
565
+ errorEvents.emit("framework-error", payload)
566
+ errorEvents.emit("all-error", {...payload, errorType: "framework-error"})
567
+ }
568
+
466
569
  /**
467
570
  * Runs handle enqueue.
468
571
  * @param {object} args - Options.
@@ -496,11 +599,15 @@ export default class BackgroundJobsMain {
496
599
  */
497
600
  async _handleJobComplete({jsonSocket, message}) {
498
601
  try {
499
- await this.store.markCompleted({
602
+ const accepted = await this.store.markCompleted({
500
603
  jobId: message.jobId,
604
+ handoffId: message.handoffId,
501
605
  workerId: message.workerId,
502
606
  handedOffAtMs: message.handedOffAtMs
503
607
  })
608
+ if (accepted && message.handoffId) {
609
+ this._forgetHandoff({handoffId: message.handoffId, jobId: message.jobId})
610
+ }
504
611
  jsonSocket.send({type: "job-updated", jobId: message.jobId})
505
612
  } catch (error) {
506
613
  this.logger.error(() => ["Failed to update job completion:", error])
@@ -520,13 +627,18 @@ export default class BackgroundJobsMain {
520
627
  const failedJob = await this.store.markFailed({
521
628
  jobId: message.jobId,
522
629
  error: message.error,
630
+ handoffId: message.handoffId,
523
631
  workerId: message.workerId,
524
632
  handedOffAtMs: message.handedOffAtMs
525
633
  })
526
634
 
527
635
  if (failedJob) {
636
+ if (message.handoffId) {
637
+ this._forgetHandoff({handoffId: message.handoffId, jobId: message.jobId})
638
+ }
528
639
  this._emitBackgroundJobFailed({
529
640
  error: message.error,
641
+ handoffId: message.handoffId,
530
642
  handedOffAtMs: message.handedOffAtMs,
531
643
  job: failedJob,
532
644
  workerId: message.workerId
@@ -546,14 +658,15 @@ export default class BackgroundJobsMain {
546
658
 
547
659
  /**
548
660
  * Runs emit background job failed.
549
- * @param {{error: ?, handedOffAtMs?: number, job: import("./types.js").BackgroundJobRow, workerId?: string}} args - Failure event data.
661
+ * @param {{error: ?, handoffId?: string, handedOffAtMs?: number, job: import("./types.js").BackgroundJobRow, workerId?: string}} args - Failure event data.
550
662
  * @returns {void}
551
663
  */
552
- _emitBackgroundJobFailed({error, handedOffAtMs, job, workerId}) {
664
+ _emitBackgroundJobFailed({error, handoffId, handedOffAtMs, job, workerId}) {
553
665
  const normalizedError = this._normalizeFailureError(error)
554
666
  const payload = {
555
667
  context: {
556
668
  attempts: job.attempts,
669
+ handoffId,
557
670
  handedOffAtMs,
558
671
  jobArgs: job.args,
559
672
  jobId: job.id,
@@ -699,6 +812,10 @@ export default class BackgroundJobsMain {
699
812
  * Runs clear error retry timer.
700
813
  * @returns {void} */
701
814
  _clearErrorRetryTimer() {
815
+ for (const worker of this.workerHandoffs.keys()) {
816
+ if (!this.workers.has(worker)) return
817
+ }
818
+
702
819
  if (this._errorRetryTimer) {
703
820
  clearTimeout(this._errorRetryTimer)
704
821
  this._errorRetryTimer = undefined
@@ -771,10 +888,30 @@ export default class BackgroundJobsMain {
771
888
 
772
889
  this._errorRetryTimer = setTimeout(() => {
773
890
  this._errorRetryTimer = undefined
774
- void this._drain()
891
+ void this._retryAfterError()
775
892
  }, this.pollIntervalMs)
776
893
  }
777
894
 
895
+ /**
896
+ * Retries failed disconnected-socket releases before draining queued work.
897
+ * @returns {Promise<void>} - Resolves after retry work.
898
+ */
899
+ async _retryAfterError() {
900
+ if (this._stopped) return
901
+
902
+ try {
903
+ for (const worker of this.workerHandoffs.keys()) {
904
+ if (!this.workers.has(worker)) await this._releaseWorkerHandoffs(worker)
905
+ }
906
+ } catch (error) {
907
+ this._reportHandoffReleaseError(error)
908
+ this._scheduleErrorRetry()
909
+ return
910
+ }
911
+
912
+ await this._drain()
913
+ }
914
+
778
915
  /**
779
916
  * Inner drain loop: pulls eligible queued jobs and hands them off to
780
917
  * ready workers until one of them runs out.
@@ -790,7 +927,23 @@ export default class BackgroundJobsMain {
790
927
 
791
928
  this.readyWorkers.delete(worker)
792
929
 
793
- const handedOffAtMs = await this.store.markHandedOff({jobId: job.id, workerId: worker.workerId})
930
+ const handoff = await this.store.markHandedOff({jobId: job.id, workerId: worker.workerId})
931
+
932
+ if (!handoff) {
933
+ if (this.workers.has(worker)) this.readyWorkers.add(worker)
934
+ continue
935
+ }
936
+
937
+ const handoffs = this.workerHandoffs.get(worker)
938
+
939
+ if (!handoffs || !this.workers.has(worker)) {
940
+ await this.store.markReturnedToQueue({handoffId: handoff.handoffId, jobId: job.id})
941
+ this._notifyEnqueued()
942
+ await this._drain()
943
+ continue
944
+ }
945
+
946
+ handoffs.set(job.id, handoff.handoffId)
794
947
 
795
948
  try {
796
949
  worker.send({
@@ -799,8 +952,9 @@ export default class BackgroundJobsMain {
799
952
  id: job.id,
800
953
  jobName: job.jobName,
801
954
  args: job.args,
955
+ handoffId: handoff.handoffId,
802
956
  workerId: worker.workerId,
803
- handedOffAtMs,
957
+ handedOffAtMs: handoff.handedOffAtMs,
804
958
  options: {
805
959
  executionMode: job.executionMode,
806
960
  forked: job.forked
@@ -809,8 +963,12 @@ export default class BackgroundJobsMain {
809
963
  })
810
964
  } catch (error) {
811
965
  this.logger.warn(() => ["Failed to send job to worker, re-queueing:", error])
812
- await this.store.markReturnedToQueue({jobId: job.id})
813
- this.readyWorkers.add(worker)
966
+ try {
967
+ worker.close()
968
+ } catch (closeError) {
969
+ this.logger.warn(() => ["Failed to close worker after job send failure:", closeError])
970
+ }
971
+ await this._handleWorkerSocketClosed(worker)
814
972
  }
815
973
  }
816
974
  }
@@ -850,6 +1008,8 @@ export default class BackgroundJobsMain {
850
1008
  * @returns {void}
851
1009
  */
852
1010
  _addAcceptedExecutionModes({executionModes, worker}) {
1011
+ if (!worker.supportsHandoffIdReporting) return
1012
+
853
1013
  for (const capability of WORKER_EXECUTION_MODE_CAPABILITIES) {
854
1014
  if (capability.accepts(worker)) executionModes.add(capability.executionMode)
855
1015
  }
@@ -874,6 +1034,8 @@ export default class BackgroundJobsMain {
874
1034
  * @returns {boolean} - Whether the worker accepts the job mode.
875
1035
  */
876
1036
  _workerAcceptsJob({job, worker}) {
1037
+ if (!worker.supportsHandoffIdReporting) return false
1038
+
877
1039
  const capability = WORKER_EXECUTION_MODE_CAPABILITIES_BY_MODE.get(job.executionMode)
878
1040
 
879
1041
  if (!capability) return false
@@ -6,6 +6,8 @@ import Logger from "../logger.js"
6
6
  import normalizeBackgroundJobError from "./normalize-error.js"
7
7
  import BackgroundJobsSocketRequest from "./socket-request.js"
8
8
 
9
+ class BackgroundJobUpdateError extends Error {}
10
+
9
11
  export default class BackgroundJobsStatusReporter {
10
12
  /**
11
13
  * Runs constructor.
@@ -27,11 +29,12 @@ export default class BackgroundJobsStatusReporter {
27
29
  * @param {string} args.jobId - Job id.
28
30
  * @param {"completed" | "failed"} args.status - Status.
29
31
  * @param {?} [args.error] - Error.
32
+ * @param {string} [args.handoffId] - Handoff lease id.
30
33
  * @param {number} [args.handedOffAtMs] - Handed off timestamp.
31
34
  * @param {string} [args.workerId] - Worker id.
32
35
  * @returns {Promise<void>} - Resolves when reported.
33
36
  */
34
- async report({jobId, status, error, handedOffAtMs, workerId}) {
37
+ async report({jobId, status, error, handoffId, handedOffAtMs, workerId}) {
35
38
  const config = this.configuration.getBackgroundJobsConfig()
36
39
  const host = this.host || config.host
37
40
  const port = typeof this.port === "number" ? this.port : config.port
@@ -44,6 +47,7 @@ export default class BackgroundJobsStatusReporter {
44
47
  jsonSocket.send({
45
48
  type: status === "completed" ? "job-complete" : "job-failed",
46
49
  jobId,
50
+ handoffId,
47
51
  workerId,
48
52
  handedOffAtMs,
49
53
  error: error ? normalizeBackgroundJobError(error) : undefined
@@ -56,7 +60,7 @@ export default class BackgroundJobsStatusReporter {
56
60
  }
57
61
 
58
62
  if (message?.type === "job-update-error" && message.jobId === jobId) {
59
- reject(new Error(message.error || "Job update failed"))
63
+ reject(new BackgroundJobUpdateError(message.error || "Job update failed"))
60
64
  }
61
65
  }
62
66
  })
@@ -69,20 +73,23 @@ export default class BackgroundJobsStatusReporter {
69
73
  * @param {string} args.jobId - Job id.
70
74
  * @param {"completed" | "failed"} args.status - Status.
71
75
  * @param {?} [args.error] - Error.
76
+ * @param {string} [args.handoffId] - Handoff lease id.
72
77
  * @param {number} [args.handedOffAtMs] - Handed off timestamp.
73
78
  * @param {string} [args.workerId] - Worker id.
74
79
  * @param {number} [args.maxDurationMs] - Max duration for retries.
75
80
  * @returns {Promise<void>} - Resolves when reported.
76
81
  */
77
- async reportWithRetry({jobId, status, error, handedOffAtMs, workerId, maxDurationMs}) {
82
+ async reportWithRetry({jobId, status, error, handoffId, handedOffAtMs, workerId, maxDurationMs}) {
78
83
  let attempt = 0
79
84
  const startTime = Date.now()
80
85
 
81
86
  while (true) {
82
87
  try {
83
- await this.report({jobId, status, error, handedOffAtMs, workerId})
88
+ await this.report({jobId, status, error, handoffId, handedOffAtMs, workerId})
84
89
  return
85
90
  } catch (error) {
91
+ if (error instanceof BackgroundJobUpdateError) throw error
92
+
86
93
  attempt += 1
87
94
  const delaySeconds = Math.min(30, 0.5 * attempt)
88
95
 
@@ -90,7 +97,7 @@ export default class BackgroundJobsStatusReporter {
90
97
 
91
98
  if (maxDurationMs && Date.now() - startTime >= maxDurationMs) {
92
99
  this.logger.warn(() => ["Background job status report timed out, giving up", error])
93
- return
100
+ throw error
94
101
  }
95
102
 
96
103
  await wait(delaySeconds)