velocious 1.0.628 → 1.0.629

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.
@@ -43,6 +43,13 @@ import {
43
43
  * @property {number | null} timeoutMs - Per-job timeout override, or null when omitted.
44
44
  */
45
45
 
46
+ /**
47
+ * BackgroundJobOrphanSelection type.
48
+ * @typedef {object} BackgroundJobOrphanSelection
49
+ * @property {Record<string, ReturnType<typeof JSON.parse>>} conditions - Exact update fence.
50
+ * @property {import("./types.js").BackgroundJobRow} job - Selected active handoff.
51
+ */
52
+
46
53
  /**
47
54
  * BackgroundJobTransactionSerializationOptions type.
48
55
  * @typedef {object} BackgroundJobTransactionSerializationOptions
@@ -1096,6 +1103,81 @@ export default class BackgroundJobsStore extends BackgroundJobsAdapter {
1096
1103
  return handoffs
1097
1104
  }
1098
1105
 
1106
+ /**
1107
+ * Snapshots exact, lease-aware active handoffs before a new main generation
1108
+ * starts accepting worker reconnects. Legacy rows without a complete worker,
1109
+ * lease, and timestamp identity stay owned by the age-based orphan sweep.
1110
+ * @returns {Promise<import("./types.js").BackgroundJobHandoffSnapshot[]>} - Exact startup handoffs.
1111
+ */
1112
+ async snapshotHandedOffJobs() {
1113
+ await this.ensureReady()
1114
+
1115
+ const rows = await this._withDb(async (db) => await db
1116
+ .newQuery()
1117
+ .from(JOBS_TABLE)
1118
+ .where({status: "handed_off"})
1119
+ .order("created_at_ms ASC")
1120
+ .order("id ASC")
1121
+ .results())
1122
+ /** @type {import("./types.js").BackgroundJobHandoffSnapshot[]} */
1123
+ const handoffs = []
1124
+
1125
+ for (const row of rows) {
1126
+ const job = this._normalizeJobRow(row)
1127
+
1128
+ if (!job.handoffId || !job.workerId || typeof job.handedOffAtMs !== "number") continue
1129
+
1130
+ handoffs.push({
1131
+ handedOffAtMs: job.handedOffAtMs,
1132
+ handoffId: job.handoffId,
1133
+ jobId: job.id,
1134
+ workerId: job.workerId
1135
+ })
1136
+ }
1137
+
1138
+ return handoffs
1139
+ }
1140
+
1141
+ /**
1142
+ * Reclaims only unchanged exact handoffs selected by a main-generation startup
1143
+ * snapshot. The ordinary orphan failure path owns retries, terminal status,
1144
+ * count transitions, schedule ownership, and concurrency release.
1145
+ * @param {object} args - Options.
1146
+ * @param {import("./types.js").BackgroundJobHandoffSnapshot[]} args.handoffs - Exact startup snapshots.
1147
+ * @param {ReturnType<typeof JSON.parse>} args.error - Orphan reason.
1148
+ * @returns {Promise<import("./types.js").BackgroundJobRow[]>} - Accepted transitions.
1149
+ */
1150
+ async markOrphanedHandoffs({handoffs, error}) {
1151
+ await this.ensureReady()
1152
+
1153
+ return await this._serializedCountMutation(async (db) => {
1154
+ /** @type {BackgroundJobOrphanSelection[]} */
1155
+ const selections = []
1156
+
1157
+ for (const handoff of handoffs) {
1158
+ const job = await this._getJobRowById(db, handoff.jobId)
1159
+
1160
+ if (!job || job.status !== "handed_off") continue
1161
+ if (job.handoffId !== handoff.handoffId) continue
1162
+ if (job.workerId !== handoff.workerId) continue
1163
+ if (job.handedOffAtMs !== handoff.handedOffAtMs) continue
1164
+
1165
+ selections.push({
1166
+ conditions: {
1167
+ handed_off_at_ms: handoff.handedOffAtMs,
1168
+ handoff_id: handoff.handoffId,
1169
+ id: handoff.jobId,
1170
+ status: "handed_off",
1171
+ worker_id: handoff.workerId
1172
+ },
1173
+ job
1174
+ })
1175
+ }
1176
+
1177
+ return await this._markOrphanSelections({db, error, selections})
1178
+ })
1179
+ }
1180
+
1099
1181
  /**
1100
1182
  * Runs mark failed.
1101
1183
  * @param {object} args - Options.
@@ -1141,8 +1223,8 @@ export default class BackgroundJobsStore extends BackgroundJobsAdapter {
1141
1223
 
1142
1224
  const rows = await query.results()
1143
1225
 
1144
- /** @type {import("./types.js").BackgroundJobRow[]} */
1145
- const orphanedJobs = []
1226
+ /** @type {BackgroundJobOrphanSelection[]} */
1227
+ const selections = []
1146
1228
 
1147
1229
  for (const row of rows) {
1148
1230
  const job = this._normalizeJobRow(row)
@@ -1160,28 +1242,55 @@ export default class BackgroundJobsStore extends BackgroundJobsAdapter {
1160
1242
  // wrongly release the concurrency reservation of — that new lease.
1161
1243
  // `handed_off_at_ms` is always set on a handed-off row (and the SELECT
1162
1244
  // required it `<= cutoff`), so it is a reliable null-safe lease pin.
1163
- const orphanedJob = await this._applyFailure({
1164
- db,
1165
- job,
1166
- error: "Job orphaned after timeout",
1167
- markOrphaned: true,
1168
- conditions: {id: job.id, status: "handed_off", handed_off_at_ms: job.handedOffAtMs}
1245
+ selections.push({
1246
+ conditions: {id: job.id, status: "handed_off", handed_off_at_ms: job.handedOffAtMs},
1247
+ job
1169
1248
  })
1170
-
1171
- if (orphanedJob) orphanedJobs.push(orphanedJob)
1172
1249
  }
1173
1250
 
1174
- const statusCounts = this._statusCounts(orphanedJobs)
1175
- const deltas = this._emptyCountBuckets()
1251
+ return await this._markOrphanSelections({
1252
+ db,
1253
+ error: "Job orphaned after timeout",
1254
+ selections
1255
+ })
1256
+ })
1257
+ }
1258
+
1259
+ /**
1260
+ * Applies the common fenced orphan transition and records one aggregate count
1261
+ * delta for the accepted rows.
1262
+ * @param {object} args - Options.
1263
+ * @param {import("../database/drivers/base.js").default} args.db - Transaction connection.
1264
+ * @param {ReturnType<typeof JSON.parse>} args.error - Orphan reason.
1265
+ * @param {BackgroundJobOrphanSelection[]} args.selections - Selected handoffs and exact fences.
1266
+ * @returns {Promise<import("./types.js").BackgroundJobRow[]>} - Accepted transitions.
1267
+ */
1268
+ async _markOrphanSelections({db, error, selections}) {
1269
+ /** @type {import("./types.js").BackgroundJobRow[]} */
1270
+ const orphanedJobs = []
1271
+
1272
+ for (const {conditions, job} of selections) {
1273
+ const orphanedJob = await this._applyFailure({
1274
+ conditions,
1275
+ db,
1276
+ error,
1277
+ job,
1278
+ markOrphaned: true
1279
+ })
1176
1280
 
1177
- for (const [status, count] of Object.entries(statusCounts)) {
1178
- deltas.handed_off -= count
1179
- deltas[status] += count
1180
- }
1181
- await this._recordCountDelta(db, deltas)
1281
+ if (orphanedJob) orphanedJobs.push(orphanedJob)
1282
+ }
1182
1283
 
1183
- return orphanedJobs
1184
- })
1284
+ const statusCounts = this._statusCounts(orphanedJobs)
1285
+ const deltas = this._emptyCountBuckets()
1286
+
1287
+ for (const [status, count] of Object.entries(statusCounts)) {
1288
+ deltas.handed_off -= count
1289
+ deltas[status] += count
1290
+ }
1291
+ await this._recordCountDelta(db, deltas)
1292
+
1293
+ return orphanedJobs
1185
1294
  }
1186
1295
 
1187
1296
  /**
@@ -43,6 +43,13 @@
43
43
  * @property {string} handoffId - Unique handoff lease id.
44
44
  * @property {number} handedOffAtMs - Time handed to a worker in ms.
45
45
  */
46
+ /**
47
+ * @typedef {object} BackgroundJobHandoffSnapshot
48
+ * @property {string} jobId - Job holding the lease.
49
+ * @property {string} handoffId - Exact durable lease id.
50
+ * @property {string} workerId - Stable worker id that received the lease.
51
+ * @property {number} handedOffAtMs - Time handed to the worker in ms.
52
+ */
46
53
  /**
47
54
  * @typedef {object} BackgroundJobHandoffRequest
48
55
  * @property {string} jobId - Job to claim.