velocious 1.0.614 → 1.0.617

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.
@@ -1307,26 +1307,33 @@ export default class TestRunner {
1307
1307
  })
1308
1308
  } finally {
1309
1309
  try {
1310
- if (testSharedConnectionsActive) {
1311
- this.clearTestSharedConnections(testSharedConnectionRegistrations)
1312
- testSharedConnectionRegistrations = []
1313
- testSharedConnectionsActive = false
1314
- }
1310
+ // Framework-owned post-commit broadcasts are intentionally
1311
+ // detached; drain them before test cleanup so their DB
1312
+ // checkouts cannot leak into the next test's lifecycle.
1313
+ await this.getConfiguration().awaitPendingBroadcasts()
1315
1314
  } finally {
1316
1315
  try {
1317
- await this.stopSharedTransactionBroker(sharedTransactionBrokerRegistration || sharedTransactionBrokerPreparation)
1318
- sharedTransactionBrokerRegistration = undefined
1319
- sharedTransactionBrokerPreparation = undefined
1316
+ if (testSharedConnectionsActive) {
1317
+ this.clearTestSharedConnections(testSharedConnectionRegistrations)
1318
+ testSharedConnectionRegistrations = []
1319
+ testSharedConnectionsActive = false
1320
+ }
1320
1321
  } finally {
1321
- for (const afterEachData of newAfterEaches) {
1322
- await this.runProfileSpan({
1323
- phase: "afterEach",
1324
- declarationIndex: afterEachData.declarationIndex,
1325
- declarationScopeId: afterEachData.declarationScopeId,
1326
- filePath: afterEachData.ownerFilePath
1327
- }, async () => {
1328
- await afterEachData.callback({configuration: this.getConfiguration(), testArgs, testData})
1329
- })
1322
+ try {
1323
+ await this.stopSharedTransactionBroker(sharedTransactionBrokerRegistration || sharedTransactionBrokerPreparation)
1324
+ sharedTransactionBrokerRegistration = undefined
1325
+ sharedTransactionBrokerPreparation = undefined
1326
+ } finally {
1327
+ for (const afterEachData of newAfterEaches) {
1328
+ await this.runProfileSpan({
1329
+ phase: "afterEach",
1330
+ declarationIndex: afterEachData.declarationIndex,
1331
+ declarationScopeId: afterEachData.declarationScopeId,
1332
+ filePath: afterEachData.ownerFilePath
1333
+ }, async () => {
1334
+ await afterEachData.callback({configuration: this.getConfiguration(), testArgs, testData})
1335
+ })
1336
+ }
1330
1337
  }
1331
1338
  }
1332
1339
  }
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "velocious": "build/bin/velocious.js"
4
4
  },
5
5
  "name": "velocious",
6
- "version": "1.0.614",
6
+ "version": "1.0.617",
7
7
  "main": "build/index.js",
8
8
  "types": "build/index.d.ts",
9
9
  "files": [
@@ -46,6 +46,7 @@ import {
46
46
  const MIGRATIONS_TABLE = "velocious_internal_migrations"
47
47
  const MIGRATION_SCOPE = "background_jobs"
48
48
  const MIGRATION_VERSION = "20250215000000"
49
+ const SCHEMA_RECOVERY_PENDING_VERSION = "schema-recovery-pending"
49
50
  const EXECUTION_MODE_BACKFILL_MIGRATION_VERSION = "20260607131010"
50
51
  // Drops the redundant legacy `forked` boolean column and rewrites pooled rows to
51
52
  // persist `execution_mode = "pooled"` directly (retiring the pooled-as-forked
@@ -186,6 +187,13 @@ export default class BackgroundJobsStore extends BackgroundJobsAdapter {
186
187
  async reconcileQueueConcurrency() {
187
188
  if (this._queueConcurrencyReconciled) return
188
189
 
190
+ const databaseIdentifier = this.getDatabaseIdentifier()
191
+ const startedAtMs = Date.now()
192
+
193
+ await this.logger.info(() => [
194
+ "Starting background jobs queue-concurrency startup reconciliation",
195
+ {databaseIdentifier}
196
+ ])
189
197
  await this.ensureReady()
190
198
 
191
199
  await this._withDb(async (db) => {
@@ -206,6 +214,11 @@ export default class BackgroundJobsStore extends BackgroundJobsAdapter {
206
214
  await db.releaseAdvisoryLock(lockName)
207
215
  }
208
216
  })
217
+
218
+ await this.logger.info(() => [
219
+ "Completed background jobs queue-concurrency startup reconciliation",
220
+ {databaseIdentifier, durationMs: Date.now() - startedAtMs}
221
+ ])
209
222
  }
210
223
 
211
224
  /**
@@ -1514,24 +1527,29 @@ export default class BackgroundJobsStore extends BackgroundJobsAdapter {
1514
1527
  await this._ensureMigrationsTable(db)
1515
1528
 
1516
1529
  const alreadyApplied = await this._hasMigration(db)
1530
+ const schemaRecoveryPending = await this._hasMigration(db, SCHEMA_RECOVERY_PENDING_VERSION)
1531
+ const jobsTableExists = await db.tableExists(JOBS_TABLE)
1517
1532
 
1518
1533
  // Even when the migration row is present, the jobs table itself can have
1519
1534
  // been dropped underneath us by a transaction rollback in another caller
1520
1535
  // (DDL is transactional on SQLite/MSSQL). Verify the table physically
1521
1536
  // exists and recreate it when missing rather than trusting the migration
1522
1537
  // row alone, otherwise later callers fail with "no such table".
1523
- if (alreadyApplied && await db.tableExists(JOBS_TABLE)) {
1538
+ if (alreadyApplied && jobsTableExists && !schemaRecoveryPending) {
1524
1539
  await this._ensureJobsTableColumns(db)
1525
1540
  await this._ensureIdempotencyKeysTable(db)
1526
1541
  await this._ensureMailDeliveryOperationsTable(db)
1527
1542
  await this._ensureScheduleKeysTable(db)
1528
1543
  await this._ensureConcurrencyTable(db)
1529
1544
  await this._ensureCountRevisionTable(db)
1530
- await this._reconcileConcurrency(db)
1531
1545
 
1532
1546
  return
1533
1547
  }
1534
1548
 
1549
+ if (alreadyApplied && !schemaRecoveryPending) {
1550
+ await this._recordMigration(db, SCHEMA_RECOVERY_PENDING_VERSION)
1551
+ }
1552
+
1535
1553
  await this._applyMigrations(db)
1536
1554
  await this._ensureJobsTableColumns(db)
1537
1555
  await this._ensureIdempotencyKeysTable(db)
@@ -1539,9 +1557,18 @@ export default class BackgroundJobsStore extends BackgroundJobsAdapter {
1539
1557
  await this._ensureScheduleKeysTable(db)
1540
1558
  await this._ensureConcurrencyTable(db)
1541
1559
  await this._ensureCountRevisionTable(db)
1542
- await this._reconcileConcurrency(db)
1543
1560
 
1544
- if (alreadyApplied) return
1561
+ if (alreadyApplied) {
1562
+ // The recreated jobs table is empty, but the surviving concurrency table
1563
+ // can still count handoffs that disappeared with the dropped jobs table.
1564
+ await this._reconcileConcurrency(db)
1565
+ await db.delete({
1566
+ tableName: MIGRATIONS_TABLE,
1567
+ conditions: {key: this._migrationKey(SCHEMA_RECOVERY_PENDING_VERSION)}
1568
+ })
1569
+
1570
+ return
1571
+ }
1545
1572
 
1546
1573
  await this._recordMigration(db, MIGRATION_VERSION)
1547
1574
  }
@@ -2578,7 +2605,7 @@ export default class BackgroundJobsStore extends BackgroundJobsAdapter {
2578
2605
  }
2579
2606
 
2580
2607
  /**
2581
- * Rebuilds durable counts from active handoffs after startup.
2608
+ * Rebuilds durable counts from active handoffs.
2582
2609
  * @param {import("../database/drivers/base.js").default} db - Database connection.
2583
2610
  * @returns {Promise<void>} - Resolves when reconciled.
2584
2611
  */
@@ -3992,6 +3992,20 @@ class VelociousDatabaseRecord {
3992
3992
  } else {
3993
3993
  throw new Error(`Unexpected loaded type: ${typeof loadedModels}`)
3994
3994
  }
3995
+ } else if (instanceRelationship.getType() == "hasOne") {
3996
+ if (!instanceRelationship.isLoaded()) {
3997
+ await instanceRelationship.load()
3998
+ }
3999
+
4000
+ const loadedModel = instanceRelationship.loaded()
4001
+
4002
+ if (loadedModel instanceof VelociousDatabaseRecord) {
4003
+ models = [loadedModel]
4004
+ } else if (loadedModel === undefined) {
4005
+ models = []
4006
+ } else {
4007
+ throw new Error(`Unexpected loaded type: ${typeof loadedModel}`)
4008
+ }
3995
4009
  } else {
3996
4010
  throw new Error(`Unhandled relationship type: ${instanceRelationship.getType()}`)
3997
4011
  }
@@ -66,6 +66,12 @@ export default class VelociousDatabaseRecordHasOneInstanceRelationship extends B
66
66
 
67
67
  whereArgs[foreignKey] = primaryModelID
68
68
 
69
+ if (this.getRelationship().getPolymorphic()) {
70
+ const typeColumn = this.getRelationship().getPolymorphicTypeColumn()
71
+
72
+ whereArgs[typeColumn] = this.getModel().getModelClass().getModelName()
73
+ }
74
+
69
75
  let query = this.getModel().queryForModel(TargetModelClass).where(whereArgs)
70
76
 
71
77
  query = this.applyScope(query)
@@ -1307,26 +1307,33 @@ export default class TestRunner {
1307
1307
  })
1308
1308
  } finally {
1309
1309
  try {
1310
- if (testSharedConnectionsActive) {
1311
- this.clearTestSharedConnections(testSharedConnectionRegistrations)
1312
- testSharedConnectionRegistrations = []
1313
- testSharedConnectionsActive = false
1314
- }
1310
+ // Framework-owned post-commit broadcasts are intentionally
1311
+ // detached; drain them before test cleanup so their DB
1312
+ // checkouts cannot leak into the next test's lifecycle.
1313
+ await this.getConfiguration().awaitPendingBroadcasts()
1315
1314
  } finally {
1316
1315
  try {
1317
- await this.stopSharedTransactionBroker(sharedTransactionBrokerRegistration || sharedTransactionBrokerPreparation)
1318
- sharedTransactionBrokerRegistration = undefined
1319
- sharedTransactionBrokerPreparation = undefined
1316
+ if (testSharedConnectionsActive) {
1317
+ this.clearTestSharedConnections(testSharedConnectionRegistrations)
1318
+ testSharedConnectionRegistrations = []
1319
+ testSharedConnectionsActive = false
1320
+ }
1320
1321
  } finally {
1321
- for (const afterEachData of newAfterEaches) {
1322
- await this.runProfileSpan({
1323
- phase: "afterEach",
1324
- declarationIndex: afterEachData.declarationIndex,
1325
- declarationScopeId: afterEachData.declarationScopeId,
1326
- filePath: afterEachData.ownerFilePath
1327
- }, async () => {
1328
- await afterEachData.callback({configuration: this.getConfiguration(), testArgs, testData})
1329
- })
1322
+ try {
1323
+ await this.stopSharedTransactionBroker(sharedTransactionBrokerRegistration || sharedTransactionBrokerPreparation)
1324
+ sharedTransactionBrokerRegistration = undefined
1325
+ sharedTransactionBrokerPreparation = undefined
1326
+ } finally {
1327
+ for (const afterEachData of newAfterEaches) {
1328
+ await this.runProfileSpan({
1329
+ phase: "afterEach",
1330
+ declarationIndex: afterEachData.declarationIndex,
1331
+ declarationScopeId: afterEachData.declarationScopeId,
1332
+ filePath: afterEachData.ownerFilePath
1333
+ }, async () => {
1334
+ await afterEachData.callback({configuration: this.getConfiguration(), testArgs, testData})
1335
+ })
1336
+ }
1330
1337
  }
1331
1338
  }
1332
1339
  }