velocious 1.0.631 → 1.0.633

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 (174) hide show
  1. package/README.md +18 -5
  2. package/build/application.js +30 -10
  3. package/build/background-jobs/forked-runner-child.js +26 -30
  4. package/build/background-jobs/job-runner.js +5 -7
  5. package/build/background-jobs/lifecycle-client.js +3 -3
  6. package/build/background-jobs/local-dispatcher.js +29 -18
  7. package/build/background-jobs/main.js +33 -28
  8. package/build/background-jobs/pooled-runner-child.js +17 -9
  9. package/build/background-jobs/runner-graceful-shutdown.js +26 -10
  10. package/build/background-jobs/worker.js +34 -13
  11. package/build/configuration-types.js +8 -0
  12. package/build/configuration.js +267 -32
  13. package/build/database/drivers/base.js +163 -119
  14. package/build/database/drivers/sqlite/index.web.js +19 -11
  15. package/build/database/migrator.js +8 -5
  16. package/build/database/pool/async-tracked-multi-connection.js +73 -24
  17. package/build/database/pool/base.js +5 -0
  18. package/build/database/pool/single-multi-use.js +43 -11
  19. package/build/database/record/attachments/store.js +35 -26
  20. package/build/environment-handlers/base.js +122 -9
  21. package/build/environment-handlers/node/cli/commands/background-jobs-worker.js +47 -16
  22. package/build/environment-handlers/node/cli/commands/server.js +7 -2
  23. package/build/environment-handlers/node.js +15 -9
  24. package/build/error-reporting/request-details.js +28 -31
  25. package/build/frontend-model-controller.js +19 -13
  26. package/build/http-server/client/index.js +5 -13
  27. package/build/http-server/client/request-runner.js +26 -9
  28. package/build/http-server/client/request-timing.js +19 -0
  29. package/build/http-server/client/websocket-session.js +54 -14
  30. package/build/http-server/websocket-events-host.js +10 -8
  31. package/build/http-server/worker-handler/worker-thread.js +13 -6
  32. package/build/initializer.js +19 -1
  33. package/build/log-redactor.js +341 -0
  34. package/build/routes/resolver.js +32 -4
  35. package/build/src/application.d.ts +7 -0
  36. package/build/src/application.d.ts.map +1 -1
  37. package/build/src/application.js +32 -12
  38. package/build/src/background-jobs/forked-runner-child.js +25 -31
  39. package/build/src/background-jobs/job-runner.d.ts +3 -1
  40. package/build/src/background-jobs/job-runner.d.ts.map +1 -1
  41. package/build/src/background-jobs/job-runner.js +6 -9
  42. package/build/src/background-jobs/lifecycle-client.d.ts +1 -0
  43. package/build/src/background-jobs/lifecycle-client.d.ts.map +1 -1
  44. package/build/src/background-jobs/lifecycle-client.js +4 -4
  45. package/build/src/background-jobs/local-dispatcher.d.ts.map +1 -1
  46. package/build/src/background-jobs/local-dispatcher.js +31 -20
  47. package/build/src/background-jobs/main.d.ts +0 -4
  48. package/build/src/background-jobs/main.d.ts.map +1 -1
  49. package/build/src/background-jobs/main.js +34 -32
  50. package/build/src/background-jobs/pooled-runner-child.js +18 -11
  51. package/build/src/background-jobs/runner-graceful-shutdown.d.ts +9 -0
  52. package/build/src/background-jobs/runner-graceful-shutdown.d.ts.map +1 -1
  53. package/build/src/background-jobs/runner-graceful-shutdown.js +27 -13
  54. package/build/src/background-jobs/worker.d.ts +5 -0
  55. package/build/src/background-jobs/worker.d.ts.map +1 -1
  56. package/build/src/background-jobs/worker.js +31 -18
  57. package/build/src/configuration-types.d.ts +21 -0
  58. package/build/src/configuration-types.d.ts.map +1 -1
  59. package/build/src/configuration-types.js +8 -1
  60. package/build/src/configuration.d.ts +94 -0
  61. package/build/src/configuration.d.ts.map +1 -1
  62. package/build/src/configuration.js +239 -31
  63. package/build/src/database/drivers/base.d.ts +5 -1
  64. package/build/src/database/drivers/base.d.ts.map +1 -1
  65. package/build/src/database/drivers/base.js +159 -115
  66. package/build/src/database/drivers/sqlite/index.web.d.ts +2 -0
  67. package/build/src/database/drivers/sqlite/index.web.d.ts.map +1 -1
  68. package/build/src/database/drivers/sqlite/index.web.js +17 -11
  69. package/build/src/database/migrator.d.ts.map +1 -1
  70. package/build/src/database/migrator.js +9 -6
  71. package/build/src/database/pool/async-tracked-multi-connection.d.ts +15 -0
  72. package/build/src/database/pool/async-tracked-multi-connection.d.ts.map +1 -1
  73. package/build/src/database/pool/async-tracked-multi-connection.js +71 -21
  74. package/build/src/database/pool/base.d.ts +2 -0
  75. package/build/src/database/pool/base.d.ts.map +1 -1
  76. package/build/src/database/pool/base.js +5 -1
  77. package/build/src/database/pool/single-multi-use.d.ts.map +1 -1
  78. package/build/src/database/pool/single-multi-use.js +47 -13
  79. package/build/src/database/record/attachments/store.d.ts +6 -0
  80. package/build/src/database/record/attachments/store.d.ts.map +1 -1
  81. package/build/src/database/record/attachments/store.js +31 -23
  82. package/build/src/environment-handlers/base.d.ts +79 -8
  83. package/build/src/environment-handlers/base.d.ts.map +1 -1
  84. package/build/src/environment-handlers/base.js +106 -10
  85. package/build/src/environment-handlers/node/cli/commands/background-jobs-worker.d.ts +36 -0
  86. package/build/src/environment-handlers/node/cli/commands/background-jobs-worker.d.ts.map +1 -1
  87. package/build/src/environment-handlers/node/cli/commands/background-jobs-worker.js +45 -16
  88. package/build/src/environment-handlers/node/cli/commands/server.d.ts.map +1 -1
  89. package/build/src/environment-handlers/node/cli/commands/server.js +8 -3
  90. package/build/src/environment-handlers/node.d.ts +3 -6
  91. package/build/src/environment-handlers/node.d.ts.map +1 -1
  92. package/build/src/environment-handlers/node.js +13 -10
  93. package/build/src/error-reporting/request-details.d.ts +8 -1
  94. package/build/src/error-reporting/request-details.d.ts.map +1 -1
  95. package/build/src/error-reporting/request-details.js +30 -32
  96. package/build/src/frontend-model-controller.d.ts.map +1 -1
  97. package/build/src/frontend-model-controller.js +20 -14
  98. package/build/src/http-server/client/index.d.ts.map +1 -1
  99. package/build/src/http-server/client/index.js +5 -12
  100. package/build/src/http-server/client/request-runner.d.ts.map +1 -1
  101. package/build/src/http-server/client/request-runner.js +23 -10
  102. package/build/src/http-server/client/request-timing.d.ts +13 -0
  103. package/build/src/http-server/client/request-timing.d.ts.map +1 -1
  104. package/build/src/http-server/client/request-timing.js +19 -1
  105. package/build/src/http-server/client/websocket-session.d.ts +9 -2
  106. package/build/src/http-server/client/websocket-session.d.ts.map +1 -1
  107. package/build/src/http-server/client/websocket-session.js +46 -15
  108. package/build/src/http-server/websocket-events-host.d.ts.map +1 -1
  109. package/build/src/http-server/websocket-events-host.js +11 -8
  110. package/build/src/http-server/worker-handler/worker-thread.d.ts.map +1 -1
  111. package/build/src/http-server/worker-handler/worker-thread.js +13 -6
  112. package/build/src/initializer.d.ts +14 -1
  113. package/build/src/initializer.d.ts.map +1 -1
  114. package/build/src/initializer.js +18 -2
  115. package/build/src/log-redactor.d.ts +97 -0
  116. package/build/src/log-redactor.d.ts.map +1 -0
  117. package/build/src/log-redactor.js +297 -0
  118. package/build/src/routes/resolver.d.ts +7 -0
  119. package/build/src/routes/resolver.d.ts.map +1 -1
  120. package/build/src/routes/resolver.js +26 -5
  121. package/build/src/testing/sqljs-test-database.d.ts +41 -0
  122. package/build/src/testing/sqljs-test-database.d.ts.map +1 -0
  123. package/build/src/testing/sqljs-test-database.js +72 -0
  124. package/build/src/testing/test-runner.d.ts +79 -6
  125. package/build/src/testing/test-runner.d.ts.map +1 -1
  126. package/build/src/testing/test-runner.js +498 -139
  127. package/build/src/testing/test.js +2 -2
  128. package/build/src/utils/shutdown-lifecycle.d.ts +13 -0
  129. package/build/src/utils/shutdown-lifecycle.d.ts.map +1 -1
  130. package/build/src/utils/shutdown-lifecycle.js +35 -26
  131. package/build/testing/sqljs-test-database.js +73 -0
  132. package/build/testing/test-runner.js +529 -141
  133. package/build/testing/test.js +1 -1
  134. package/build/tsconfig.tsbuildinfo +1 -1
  135. package/build/utils/shutdown-lifecycle.js +33 -30
  136. package/package.json +1 -1
  137. package/scripts/test-browser.js +16 -29
  138. package/src/application.js +30 -10
  139. package/src/background-jobs/forked-runner-child.js +26 -30
  140. package/src/background-jobs/job-runner.js +5 -7
  141. package/src/background-jobs/lifecycle-client.js +3 -3
  142. package/src/background-jobs/local-dispatcher.js +29 -18
  143. package/src/background-jobs/main.js +33 -28
  144. package/src/background-jobs/pooled-runner-child.js +17 -9
  145. package/src/background-jobs/runner-graceful-shutdown.js +26 -10
  146. package/src/background-jobs/worker.js +34 -13
  147. package/src/configuration-types.js +8 -0
  148. package/src/configuration.js +267 -32
  149. package/src/database/drivers/base.js +163 -119
  150. package/src/database/drivers/sqlite/index.web.js +19 -11
  151. package/src/database/migrator.js +8 -5
  152. package/src/database/pool/async-tracked-multi-connection.js +73 -24
  153. package/src/database/pool/base.js +5 -0
  154. package/src/database/pool/single-multi-use.js +43 -11
  155. package/src/database/record/attachments/store.js +35 -26
  156. package/src/environment-handlers/base.js +122 -9
  157. package/src/environment-handlers/node/cli/commands/background-jobs-worker.js +47 -16
  158. package/src/environment-handlers/node/cli/commands/server.js +7 -2
  159. package/src/environment-handlers/node.js +15 -9
  160. package/src/error-reporting/request-details.js +28 -31
  161. package/src/frontend-model-controller.js +19 -13
  162. package/src/http-server/client/index.js +5 -13
  163. package/src/http-server/client/request-runner.js +26 -9
  164. package/src/http-server/client/request-timing.js +19 -0
  165. package/src/http-server/client/websocket-session.js +54 -14
  166. package/src/http-server/websocket-events-host.js +10 -8
  167. package/src/http-server/worker-handler/worker-thread.js +13 -6
  168. package/src/initializer.js +19 -1
  169. package/src/log-redactor.js +341 -0
  170. package/src/routes/resolver.js +32 -4
  171. package/src/testing/sqljs-test-database.js +73 -0
  172. package/src/testing/test-runner.js +529 -141
  173. package/src/testing/test.js +1 -1
  174. package/src/utils/shutdown-lifecycle.js +33 -30
@@ -170,7 +170,7 @@ import TableData from "../table-data/index.js"
170
170
  import TableColumn from "../table-data/table-column.js"
171
171
  import TableForeignKey from "../table-data/table-foreign-key.js"
172
172
  import wait from "awaitery/build/wait.js"
173
- import { optionalPositiveInteger } from "typanic"
173
+ import { ensureError, optionalPositiveInteger } from "typanic"
174
174
  import {coordinateSharedTransactionConnection, runWithoutSharedTransactionCoordinatorOwner} from "../../testing/shared-transaction-connection-coordinator.js"
175
175
  import { currentTestProfileContext } from "../../testing/test-profile-context.js"
176
176
  import sha256Hex from "../../utils/sha256-hex.js"
@@ -1376,63 +1376,65 @@ export default class VelociousDatabaseDriversBase {
1376
1376
  async transaction(callback, options = {}) {
1377
1377
  await this._waitForOperationLease(options.operationOwner)
1378
1378
 
1379
- if (this._transactionsCount > 0) {
1380
- return await this._runTransactionAttempt(callback, options)
1381
- }
1382
-
1383
- const args = this.getArgs()
1384
- const maxAttempts = optionalPositiveInteger(args.deadlockMaxRetries, "deadlockMaxRetries") ?? 8
1385
- const configuredBaseWaitMs = optionalPositiveInteger(args.deadlockBaseWaitMs, "deadlockBaseWaitMs")
1386
- const deadlockMaxWaitMs = optionalPositiveInteger(args.deadlockMaxWaitMs, "deadlockMaxWaitMs") ?? 1000
1387
- let attempt = 0
1379
+ return await coordinateSharedTransactionConnection(this, async () => {
1380
+ if (this._transactionsCount > 0) {
1381
+ return await this._runTransactionAttempt(callback, options)
1382
+ }
1388
1383
 
1389
- while (true) {
1390
- attempt++
1391
- const attemptStartedAtMs = this._nowMs()
1384
+ const args = this.getArgs()
1385
+ const maxAttempts = optionalPositiveInteger(args.deadlockMaxRetries, "deadlockMaxRetries") ?? 8
1386
+ const configuredBaseWaitMs = optionalPositiveInteger(args.deadlockBaseWaitMs, "deadlockBaseWaitMs")
1387
+ const deadlockMaxWaitMs = optionalPositiveInteger(args.deadlockMaxWaitMs, "deadlockMaxWaitMs") ?? 1000
1388
+ let attempt = 0
1392
1389
 
1393
- try {
1394
- return await this._runTransactionAttempt(callback, options)
1395
- } catch (error) {
1396
- if (error instanceof VelociousDatabaseAfterCommitCallbackError) throw error.callbackError
1397
- if (!(error instanceof Error)) throw error
1390
+ while (true) {
1391
+ attempt++
1392
+ const attemptStartedAtMs = this._nowMs()
1398
1393
 
1399
- const retryInfo = this.retryableDatabaseError(error)
1400
- const willRetry = Boolean(retryInfo.deadlock && attempt < maxAttempts && this._transactionsCount == 0)
1401
-
1402
- if (willRetry) {
1403
- this._reportDeadlockRetryDiagnostic({
1404
- attempt,
1405
- contentionKind: retryInfo.contentionKind || "deadlock",
1406
- error,
1407
- maxAttempts,
1408
- transactionAttemptDurationMs: Math.max(0, this._nowMs() - attemptStartedAtMs),
1409
- willRetry
1410
- })
1394
+ try {
1395
+ return await this._runTransactionAttempt(callback, options)
1396
+ } catch (error) {
1397
+ if (error instanceof VelociousDatabaseAfterCommitCallbackError) throw error.callbackError
1398
+ if (!(error instanceof Error)) throw error
1399
+
1400
+ const retryInfo = this.retryableDatabaseError(error)
1401
+ const willRetry = Boolean(retryInfo.deadlock && attempt < maxAttempts && this._transactionsCount == 0)
1402
+
1403
+ if (willRetry) {
1404
+ this._reportDeadlockRetryDiagnostic({
1405
+ attempt,
1406
+ contentionKind: retryInfo.contentionKind || "deadlock",
1407
+ error,
1408
+ maxAttempts,
1409
+ transactionAttemptDurationMs: Math.max(0, this._nowMs() - attemptStartedAtMs),
1410
+ willRetry
1411
+ })
1412
+
1413
+ // An explicitly-configured base wins so the tuning knob is effective even on drivers
1414
+ // whose classifier supplies its own `waitMs` (MySQL/MariaDB return a fixed 50ms for
1415
+ // deadlocks); otherwise honor that classifier hint, then fall back to 50ms.
1416
+ const baseWaitMs = configuredBaseWaitMs ?? (typeof retryInfo.waitMs == "number" && retryInfo.waitMs > 0 ? retryInfo.waitMs : 50)
1417
+
1418
+ // Full-jitter exponential backoff: wait a uniform-random duration in
1419
+ // [0, min(base * 2^(attempt-1), cap)]. The doubling ceiling spreads retries out as
1420
+ // contention persists, and the jitter de-correlates transactions that deadlocked in
1421
+ // lockstep so they stop re-colliding on the same wait (the linear `base * attempt`
1422
+ // this replaces had every victim retry after an identical delay). `attempt` is
1423
+ // 1-based here, so 2^(attempt-1) is 1, 2, 4, ... The cap keeps the tail sub-second.
1424
+ const ceilingWaitMs = Math.min(baseWaitMs * (2 ** (attempt - 1)), deadlockMaxWaitMs)
1425
+ const jitteredWaitMs = Math.floor(Math.random() * (ceilingWaitMs + 1))
1426
+
1427
+ const loggedContentionKind = retryInfo.contentionKind || "transaction contention"
1428
+
1429
+ this.logger.warn(`Retrying transaction after ${loggedContentionKind} (attempt ${attempt}/${maxAttempts})`)
1430
+ await this._waitMs(jitteredWaitMs)
1431
+ continue
1432
+ }
1411
1433
 
1412
- // An explicitly-configured base wins so the tuning knob is effective even on drivers
1413
- // whose classifier supplies its own `waitMs` (MySQL/MariaDB return a fixed 50ms for
1414
- // deadlocks); otherwise honor that classifier hint, then fall back to 50ms.
1415
- const baseWaitMs = configuredBaseWaitMs ?? (typeof retryInfo.waitMs == "number" && retryInfo.waitMs > 0 ? retryInfo.waitMs : 50)
1416
-
1417
- // Full-jitter exponential backoff: wait a uniform-random duration in
1418
- // [0, min(base * 2^(attempt-1), cap)]. The doubling ceiling spreads retries out as
1419
- // contention persists, and the jitter de-correlates transactions that deadlocked in
1420
- // lockstep so they stop re-colliding on the same wait (the linear `base * attempt`
1421
- // this replaces had every victim retry after an identical delay). `attempt` is
1422
- // 1-based here, so 2^(attempt-1) is 1, 2, 4, ... The cap keeps the tail sub-second.
1423
- const ceilingWaitMs = Math.min(baseWaitMs * (2 ** (attempt - 1)), deadlockMaxWaitMs)
1424
- const jitteredWaitMs = Math.floor(Math.random() * (ceilingWaitMs + 1))
1425
-
1426
- const loggedContentionKind = retryInfo.contentionKind || "transaction contention"
1427
-
1428
- this.logger.warn(`Retrying transaction after ${loggedContentionKind} (attempt ${attempt}/${maxAttempts})`)
1429
- await this._waitMs(jitteredWaitMs)
1430
- continue
1434
+ throw error
1431
1435
  }
1432
-
1433
- throw error
1434
1436
  }
1435
- }
1437
+ }, options.operationOwner)
1436
1438
  }
1437
1439
 
1438
1440
  /**
@@ -1769,34 +1771,36 @@ export default class VelociousDatabaseDriversBase {
1769
1771
  * @returns {Promise<void>} - Resolves when complete.
1770
1772
  */
1771
1773
  async startTransaction(options = {}) {
1772
- while (true) {
1773
- /** @type {import("../operation-lease.js").default | undefined} */
1774
- let blockingOperationLease
1775
-
1776
- await this._transactionsActionsMutex.sync(async () => {
1777
- const operationLease = this._operationLease
1774
+ await coordinateSharedTransactionConnection(this, async () => {
1775
+ while (true) {
1776
+ /** @type {import("../operation-lease.js").default | undefined} */
1777
+ let blockingOperationLease
1778
1778
 
1779
- if (operationLease && options.operationOwner !== operationLease.owner) {
1780
- blockingOperationLease = operationLease
1781
- return
1782
- }
1779
+ await this._transactionsActionsMutex.sync(async () => {
1780
+ const operationLease = this._operationLease
1783
1781
 
1784
- await this._runProfiledTransactionAction("start", async () => {
1785
- await this._startTransactionAction(options)
1786
- })
1787
- this._transactionsCount++
1782
+ if (operationLease && options.operationOwner !== operationLease.owner) {
1783
+ blockingOperationLease = operationLease
1784
+ return
1785
+ }
1788
1786
 
1789
- if (this._transactionsCount === 1) {
1790
- this._transactionCompletionPromise = new Promise((resolve) => {
1791
- this._resolveTransactionCompletion = resolve
1787
+ await this._runProfiledTransactionAction("start", async () => {
1788
+ await this._startTransactionAction(options)
1792
1789
  })
1793
- }
1794
- })
1790
+ this._transactionsCount++
1795
1791
 
1796
- if (!blockingOperationLease) return
1792
+ if (this._transactionsCount === 1) {
1793
+ this._transactionCompletionPromise = new Promise((resolve) => {
1794
+ this._resolveTransactionCompletion = resolve
1795
+ })
1796
+ }
1797
+ })
1797
1798
 
1798
- await blockingOperationLease.wait(options.operationOwner)
1799
- }
1799
+ if (!blockingOperationLease) return
1800
+
1801
+ await blockingOperationLease.wait(options.operationOwner)
1802
+ }
1803
+ }, options.operationOwner)
1800
1804
  }
1801
1805
 
1802
1806
  /**
@@ -1814,13 +1818,15 @@ export default class VelociousDatabaseDriversBase {
1814
1818
  * @returns {Promise<void>} - Resolves when complete.
1815
1819
  */
1816
1820
  async commitTransaction(options = {}) {
1817
- await this._transactionsActionsMutex.sync(async () => {
1818
- await this._runProfiledTransactionAction("commit", async () => {
1819
- await this._commitTransactionAction(options)
1821
+ await coordinateSharedTransactionConnection(this, async () => {
1822
+ await this._transactionsActionsMutex.sync(async () => {
1823
+ await this._runProfiledTransactionAction("commit", async () => {
1824
+ await this._commitTransactionAction(options)
1825
+ })
1826
+ this._transactionsCount--
1827
+ this._resolveCompletedTransaction()
1820
1828
  })
1821
- this._transactionsCount--
1822
- this._resolveCompletedTransaction()
1823
- })
1829
+ }, options.operationOwner)
1824
1830
  }
1825
1831
 
1826
1832
  /** Resolves the current outer transaction completion when it has finished. */
@@ -1998,7 +2004,10 @@ export default class VelociousDatabaseDriversBase {
1998
2004
  const waitMs = typeof retryInfo.waitMs === "number" && Number.isFinite(retryInfo.waitMs) ? retryInfo.waitMs : 100
1999
2005
 
2000
2006
  if (waitMs > 0) await wait(waitMs)
2001
- this.logger.warn(`Retrying query because failed with: ${error.stack}`)
2007
+ const sensitiveValues = requestTiming ? requestTiming.getLogSensitiveValues() : new Set()
2008
+ const loggedError = this.configuration.getLogRedactor().redactString(error.stack || error.message, sensitiveValues)
2009
+
2010
+ this.logger.warn(`Retrying query because failed with: ${loggedError}`)
2002
2011
  // Retry
2003
2012
  } else {
2004
2013
  throw error
@@ -2064,17 +2073,32 @@ export default class VelociousDatabaseDriversBase {
2064
2073
  let result
2065
2074
 
2066
2075
  try {
2067
- const runQueryActualWithHooks = async () => await this._queryActualWithHooks(querySql, options, originalSql)
2076
+ try {
2077
+ const runQueryActualWithHooks = async () => await this._queryActualWithHooks(querySql, options, originalSql)
2068
2078
 
2069
- if (requestTiming && tries === 1) {
2070
- result = await requestTiming.measureDbQuery(runQueryActualWithHooks)
2071
- } else if (requestTiming) {
2072
- result = await requestTiming.measure("db", runQueryActualWithHooks)
2073
- } else {
2074
- result = await runQueryActualWithHooks()
2079
+ if (requestTiming && tries === 1) {
2080
+ result = await requestTiming.measureDbQuery(runQueryActualWithHooks)
2081
+ } else if (requestTiming) {
2082
+ result = await requestTiming.measure("db", runQueryActualWithHooks)
2083
+ } else {
2084
+ result = await runQueryActualWithHooks()
2085
+ }
2086
+ } finally {
2087
+ this._activeQuery = previousActiveQuery
2075
2088
  }
2076
- } finally {
2077
- this._activeQuery = previousActiveQuery
2089
+ } catch (error) {
2090
+ if (options.logQuery !== false) {
2091
+ await this._logQuery({
2092
+ elapsedMs: nowMs() - startedAtMs,
2093
+ error: ensureError(error),
2094
+ logName: options.logName || "SQL",
2095
+ requestTiming,
2096
+ sourceStack: options.sourceStack,
2097
+ sql: originalSql
2098
+ })
2099
+ }
2100
+
2101
+ throw error
2078
2102
  }
2079
2103
 
2080
2104
  const elapsedMs = nowMs() - startedAtMs
@@ -2083,6 +2107,7 @@ export default class VelociousDatabaseDriversBase {
2083
2107
  await this._logQuery({
2084
2108
  elapsedMs,
2085
2109
  logName: options.logName || "SQL",
2110
+ requestTiming,
2086
2111
  sourceStack: options.sourceStack,
2087
2112
  sql: originalSql
2088
2113
  })
@@ -2374,17 +2399,25 @@ export default class VelociousDatabaseDriversBase {
2374
2399
  * Runs log query.
2375
2400
  * @param {object} args - Options object.
2376
2401
  * @param {number} args.elapsedMs - Elapsed milliseconds.
2402
+ * @param {Error} [args.error] - Query failure, when the driver call failed.
2377
2403
  * @param {string} args.logName - Query log subject.
2404
+ * @param {import("../../http-server/client/request-timing.js").default | undefined} args.requestTiming - Request timing.
2378
2405
  * @param {string | undefined} args.sourceStack - Source stack.
2379
2406
  * @param {string} args.sql - SQL string.
2380
2407
  * @returns {Promise<void>} - Resolves when complete.
2381
2408
  */
2382
- async _logQuery({elapsedMs, logName, sourceStack, sql}) {
2409
+ async _logQuery({elapsedMs, error, logName, requestTiming, sourceStack, sql}) {
2383
2410
  const logger = new Logger(logName, {configuration: this.configuration})
2384
2411
  const sourceLine = this._querySourceLine(sourceStack)
2412
+ const sensitiveValues = requestTiming ? requestTiming.getLogSensitiveValues() : new Set()
2413
+ const redactor = this.configuration.getLogRedactor()
2414
+ const loggedSql = redactor.redactString(sql, sensitiveValues)
2415
+ const failure = error
2416
+ ? ` FAILED ${error.name}: ${redactor.redactString(error.message, sensitiveValues)}`
2417
+ : ""
2385
2418
  const message = sourceLine
2386
- ? `(${formatElapsedMs(elapsedMs)}) ${sql}\n ↳ ${sourceLine}`
2387
- : `(${formatElapsedMs(elapsedMs)}) ${sql}`
2419
+ ? `(${formatElapsedMs(elapsedMs)})${failure} ${loggedSql}\n ↳ ${sourceLine}`
2420
+ : `(${formatElapsedMs(elapsedMs)})${failure} ${loggedSql}`
2388
2421
 
2389
2422
  await logger.info(message)
2390
2423
  }
@@ -2533,23 +2566,28 @@ export default class VelociousDatabaseDriversBase {
2533
2566
  * @returns {Promise<void>} - Resolves when complete.
2534
2567
  */
2535
2568
  async rollbackTransaction(options = {}) {
2536
- await this._transactionsActionsMutex.sync(async () => {
2537
- try {
2538
- await this._runProfiledTransactionAction("rollback", async () => {
2539
- await this._rollbackTransactionAction(options)
2540
- })
2541
- } finally {
2542
- this._transactionsCount--
2543
- this._resolveCompletedTransaction()
2544
-
2545
- // A rolled-back transaction may have reverted DDL (e.g. a CREATE TABLE
2546
- // run lazily inside the transaction), so any cached schema metadata is
2547
- // now stale and must be invalidated. Without this, a later tableExists()
2548
- // check can report a table that the rollback already removed, so callers
2549
- // skip recreating it and then fail with "no such table".
2550
- this.clearSchemaCache()
2551
- }
2552
- })
2569
+ await coordinateSharedTransactionConnection(this, async () => {
2570
+ await this._transactionsActionsMutex.sync(async () => {
2571
+ try {
2572
+ await this._runProfiledTransactionAction("rollback", async () => {
2573
+ await this._rollbackTransactionAction(options)
2574
+ })
2575
+ } finally {
2576
+ // Driver recovery may need to clear a stale physical transaction when
2577
+ // no logical transaction is active. Never let that cleanup underflow
2578
+ // the logical depth and turn the next root transaction into a savepoint.
2579
+ if (this._transactionsCount > 0) this._transactionsCount--
2580
+ this._resolveCompletedTransaction()
2581
+
2582
+ // A rolled-back transaction may have reverted DDL (e.g. a CREATE TABLE
2583
+ // run lazily inside the transaction), so any cached schema metadata is
2584
+ // now stale and must be invalidated. Without this, a later tableExists()
2585
+ // check can report a table that the rollback already removed, so callers
2586
+ // skip recreating it and then fail with "no such table".
2587
+ this.clearSchemaCache()
2588
+ }
2589
+ })
2590
+ }, options.operationOwner)
2553
2591
  }
2554
2592
 
2555
2593
  /**
@@ -2576,9 +2614,11 @@ export default class VelociousDatabaseDriversBase {
2576
2614
  * @returns {Promise<void>} - Resolves when complete.
2577
2615
  */
2578
2616
  async startSavePoint(savePointName, options = {}) {
2579
- await this._transactionsActionsMutex.sync(async () => {
2580
- await this._startSavePointAction(savePointName, options)
2581
- })
2617
+ await coordinateSharedTransactionConnection(this, async () => {
2618
+ await this._transactionsActionsMutex.sync(async () => {
2619
+ await this._startSavePointAction(savePointName, options)
2620
+ })
2621
+ }, options.operationOwner)
2582
2622
  }
2583
2623
 
2584
2624
  /**
@@ -2622,9 +2662,11 @@ export default class VelociousDatabaseDriversBase {
2622
2662
  * @returns {Promise<void>} - Resolves when complete.
2623
2663
  */
2624
2664
  async releaseSavePoint(savePointName, options = {}) {
2625
- await this._transactionsActionsMutex.sync(async () => {
2626
- await this._releaseSavePointAction(savePointName, options)
2627
- })
2665
+ await coordinateSharedTransactionConnection(this, async () => {
2666
+ await this._transactionsActionsMutex.sync(async () => {
2667
+ await this._releaseSavePointAction(savePointName, options)
2668
+ })
2669
+ }, options.operationOwner)
2628
2670
  }
2629
2671
 
2630
2672
  /**
@@ -2656,9 +2698,11 @@ export default class VelociousDatabaseDriversBase {
2656
2698
  * @returns {Promise<void>} - Resolves when complete.
2657
2699
  */
2658
2700
  async rollbackSavePoint(savePointName, options = {}) {
2659
- await this._transactionsActionsMutex.sync(async () => {
2660
- await this._rollbackSavePointAction(savePointName, options)
2661
- })
2701
+ await coordinateSharedTransactionConnection(this, async () => {
2702
+ await this._transactionsActionsMutex.sync(async () => {
2703
+ await this._rollbackSavePointAction(savePointName, options)
2704
+ })
2705
+ }, options.operationOwner)
2662
2706
  }
2663
2707
 
2664
2708
  /**
@@ -17,6 +17,9 @@ export default class VelociousDatabaseDriversSqliteWeb extends Base {
17
17
  * @type {ConnectionSqlJs | undefined} */
18
18
  _connection = undefined
19
19
 
20
+ /** @type {SqliteWebConnection | undefined} */
21
+ _externalConnection = undefined
22
+
20
23
  /**
21
24
  * Runs sql js locate file.
22
25
  * @returns {(file: string) => string} - locateFile callback for sql.js.
@@ -34,18 +37,21 @@ export default class VelociousDatabaseDriversSqliteWeb extends Base {
34
37
  async connect() {
35
38
  this.args = this.getArgs()
36
39
 
37
- if (!this.args.getConnection) {
38
- if (this.args.reset) {
39
- await deleteSqliteWebPersistences({databaseName: this.databaseName()})
40
- }
41
-
42
- const persistence = await createSqliteWebPersistence({databaseName: this.databaseName()})
43
- const SQL = await initSqlJs({locateFile: this.sqlJsLocateFile()})
44
- const databaseContent = await persistence.load()
45
- const connectionSqlJs = new ConnectionSqlJs(this, new SQL.Database(databaseContent), persistence)
40
+ if (this.args.getConnection) {
41
+ this._externalConnection = this.args.getConnection()
42
+ return
43
+ }
46
44
 
47
- this._connection = connectionSqlJs
45
+ if (this.args.reset) {
46
+ await deleteSqliteWebPersistences({databaseName: this.databaseName()})
48
47
  }
48
+
49
+ const persistence = await createSqliteWebPersistence({databaseName: this.databaseName()})
50
+ const SQL = await initSqlJs({locateFile: this.sqlJsLocateFile()})
51
+ const databaseContent = await persistence.load()
52
+ const connectionSqlJs = new ConnectionSqlJs(this, new SQL.Database(databaseContent), persistence)
53
+
54
+ this._connection = connectionSqlJs
49
55
  }
50
56
 
51
57
  async _close() {
@@ -152,7 +158,9 @@ export default class VelociousDatabaseDriversSqliteWeb extends Base {
152
158
  */
153
159
  getConnection() {
154
160
  if (this.args?.getConnection) {
155
- return /** @type {SqliteWebConnection} */ (this.args.getConnection())
161
+ if (!this._externalConnection) throw new Error("SQLite web external connection has not been initialized")
162
+
163
+ return this._externalConnection
156
164
  } else {
157
165
  if (!this._connection) throw new Error("SQLite web connection has not been initialized")
158
166
  return this._connection
@@ -265,15 +265,18 @@ export default class VelociousDatabaseMigrator {
265
265
  const environmentHandler = this.configuration.getEnvironmentHandler()
266
266
  const dbs = await this.configuration.getCurrentConnections()
267
267
  const filteredDbs = Object.fromEntries(
268
- Object.entries(dbs).filter(([dbIdentifier]) => this.handlesDatabaseIdentifier(dbIdentifier))
268
+ Object.entries(dbs).filter(([dbIdentifier]) => {
269
+ if (!this.handlesDatabaseIdentifier(dbIdentifier)) return false
270
+
271
+ return Boolean(this.configuration.getDatabaseIdentifier(dbIdentifier).migrations)
272
+ })
269
273
  )
270
274
 
271
275
  if (!environmentHandler || Object.keys(filteredDbs).length == 0) return
272
276
 
273
- // Ensure velocious' own framework schema (background jobs) before the structure
274
- // dump, and unconditionally — the dump is gated to enabled environments but the
275
- // framework schema must exist after every migrate so `db:migrate` (and thus
276
- // schema:load of the dumped SQL) produces a complete DB in every environment.
277
+ // Ensure Velocious' own framework schema before the structure dump. The dump is
278
+ // gated to enabled environments, but migration-enabled databases must include
279
+ // framework tables so `db:migrate` and schema:load produce a complete database.
277
280
  await environmentHandler.ensureFrameworkSchema({dbs: filteredDbs})
278
281
  await environmentHandler.afterMigrations({dbs: filteredDbs})
279
282
  }
@@ -17,6 +17,7 @@ import { currentTestProfileContext } from "../../testing/test-profile-context.js
17
17
  * @property {number | null} timeoutAt - Timestamp when the checkout will time out, or null when disabled.
18
18
  * @property {number | null} timeoutMillis - Milliseconds to wait before rejecting, or null when disabled.
19
19
  * @property {ReturnType<typeof setTimeout> | undefined} timeoutTimer - Timer that rejects the pending checkout.
20
+ * @property {{revoked: boolean} | undefined} [testDatabaseAccessScope] - Database-access scope captured at enqueue.
20
21
  * @property {import("../../testing/test-profiler.js").TestProfileAsyncContext | undefined} [testProfileContext] - Async-safe profile attribution captured at enqueue.
21
22
  */
22
23
  export const CLOSED_CONNECTION = Symbol("velociousClosedConnection")
@@ -227,7 +228,7 @@ export default class VelociousDatabasePoolAsyncTrackedMultiConnection extends Ba
227
228
  const trackedConnection = /** @type {import("../drivers/base.js").default & {[CLOSED_CONNECTION]?: boolean, [CONNECTION_CHECKED_OUT_AT]?: number, [IDLE_CONNECTION_CHECKED_IN_AT]?: number}} */ (connection)
228
229
 
229
230
  if (trackedConnection[CLOSED_CONNECTION]) {
230
- this.untrackConnectionInUse(connection, id)
231
+ if (typeof id === "number") this.untrackConnectionInUse(connection, id)
231
232
  await this.drainPendingCheckouts()
232
233
  return
233
234
  }
@@ -343,6 +344,7 @@ export default class VelociousDatabasePoolAsyncTrackedMultiConnection extends Ba
343
344
  * @returns {Promise<import("../drivers/base.js").default>} - Resolves with the checkout.
344
345
  */
345
346
  async checkout(options = {}) {
347
+ this.assertDatabaseAccessAllowed()
346
348
  let databaseConfig = this.getConfiguration()
347
349
  let reuseKey = this.getConfigurationReuseKey(databaseConfig)
348
350
  let connection = this.takeIdleConnectionForReuseKey(reuseKey)
@@ -380,6 +382,7 @@ export default class VelociousDatabasePoolAsyncTrackedMultiConnection extends Ba
380
382
  * @returns {Promise<import("../drivers/base.js").default>} - Activated pooled connection.
381
383
  */
382
384
  async checkoutForConfiguration(databaseConfig, options = {}) {
385
+ this.assertDatabaseAccessAllowed()
383
386
  const reuseKey = this.getConfigurationReuseKey(databaseConfig)
384
387
  const lifecycleRetainedConnection = this.lifecycleRetainedConnections.get(reuseKey)
385
388
 
@@ -446,6 +449,11 @@ export default class VelociousDatabasePoolAsyncTrackedMultiConnection extends Ba
446
449
  * @returns {Promise<import("../drivers/base.js").default>} - Activated connection.
447
450
  */
448
451
  async activateConnection(connection, options = {}) {
452
+ try {
453
+ this.assertDatabaseAccessAllowed()
454
+ } catch (error) {
455
+ await this.closeRejectedCheckoutAndThrow(connection, error)
456
+ }
449
457
  if (connection.getIdSeq() !== undefined) throw new Error(`Connection already has an ID-seq - is it in use? ${connection.getIdSeq()}`)
450
458
 
451
459
  const id = this.idSeq++
@@ -459,15 +467,48 @@ export default class VelociousDatabasePoolAsyncTrackedMultiConnection extends Ba
459
467
 
460
468
  try {
461
469
  await connection.setConnectionCheckoutName(options.name)
470
+ this.assertDatabaseAccessAllowed()
462
471
  } catch (error) {
463
- delete this.connectionsInUse[id]
464
- connection.setIdSeq(undefined)
472
+ await this.closeRejectedCheckoutAndThrow(connection, error, id)
473
+ }
474
+
475
+ return connection
476
+ }
477
+
478
+ /**
479
+ * Closes a rejected checkout, then hands freed capacity to queued callers.
480
+ * @param {import("../drivers/base.js").default} connection - Rejected connection.
481
+ * @param {ReturnType<typeof JSON.parse>} error - Access revocation error.
482
+ * @param {number} [id] - Assigned checkout id, if activation reached that stage.
483
+ * @returns {Promise<never>} - Always rejects with the access or cleanup errors.
484
+ */
485
+ async closeRejectedCheckoutAndThrow(connection, error, id) {
486
+ if (id !== undefined) this.untrackConnectionInUse(connection, id)
487
+
488
+ /** @type {ReturnType<typeof JSON.parse>[]} */
489
+ const cleanupErrors = []
490
+
491
+ try {
465
492
  await this.closeConnection(connection)
493
+ } catch (closeError) {
494
+ cleanupErrors.push(closeError)
495
+ }
466
496
 
467
- throw error
497
+ try {
498
+ if (this.pendingCheckoutDrainPromise) {
499
+ this.pendingCheckoutDrainRequested = true
500
+ } else {
501
+ await this.drainPendingCheckouts()
502
+ }
503
+ } catch (drainError) {
504
+ cleanupErrors.push(drainError)
468
505
  }
469
506
 
470
- return connection
507
+ if (cleanupErrors.length > 0) {
508
+ throw new AggregateError([error, ...cleanupErrors], "Database checkout rejection cleanup failed", {cause: error})
509
+ }
510
+
511
+ throw error
471
512
  }
472
513
 
473
514
  /**
@@ -588,6 +629,7 @@ export default class VelociousDatabasePoolAsyncTrackedMultiConnection extends Ba
588
629
  timeoutAt: timeoutMillis === null ? null : enqueuedAt + timeoutMillis,
589
630
  timeoutMillis,
590
631
  timeoutTimer: undefined,
632
+ testDatabaseAccessScope: this.configuration.getEnvironmentHandler().currentTestDatabaseAccessScope(),
591
633
  testProfileContext: currentTestProfileContext(this.configuration)
592
634
  }
593
635
 
@@ -887,20 +929,23 @@ export default class VelociousDatabasePoolAsyncTrackedMultiConnection extends Ba
887
929
  const environmentHandler = this.configuration.getEnvironmentHandler()
888
930
 
889
931
  return await environmentHandler.runWithTestProfileContext(checkout.testProfileContext, async () => {
890
- let connection
891
-
892
- try {
893
- connection = await this.spawnConnectionForCheckout(
894
- checkout.databaseConfig,
895
- checkout.reuseKey,
896
- checkout.testProfileContext
897
- )
898
- } catch (error) {
899
- checkout.reject(error instanceof Error ? error : new Error("Failed to spawn database connection.", {cause: error}))
900
- return
901
- }
902
-
903
- await this.resolvePendingCheckout(checkout, connection)
932
+ return await environmentHandler.runWithCapturedTestDatabaseAccessScope(checkout.testDatabaseAccessScope, async () => {
933
+ let connection
934
+
935
+ try {
936
+ this.assertDatabaseAccessAllowed()
937
+ connection = await this.spawnConnectionForCheckout(
938
+ checkout.databaseConfig,
939
+ checkout.reuseKey,
940
+ checkout.testProfileContext
941
+ )
942
+ } catch (error) {
943
+ checkout.reject(error instanceof Error ? error : new Error("Failed to spawn database connection.", {cause: error}))
944
+ return
945
+ }
946
+
947
+ await this.resolvePendingCheckout(checkout, connection)
948
+ })
904
949
  })
905
950
  }
906
951
 
@@ -914,11 +959,13 @@ export default class VelociousDatabasePoolAsyncTrackedMultiConnection extends Ba
914
959
  const environmentHandler = this.configuration.getEnvironmentHandler()
915
960
 
916
961
  return await environmentHandler.runWithTestProfileContext(checkout.testProfileContext, async () => {
917
- try {
918
- checkout.resolve(await this.activateConnection(connection, checkout.options))
919
- } catch (error) {
920
- checkout.reject(error instanceof Error ? error : new Error("Failed to activate database connection.", {cause: error}))
921
- }
962
+ return await environmentHandler.runWithCapturedTestDatabaseAccessScope(checkout.testDatabaseAccessScope, async () => {
963
+ try {
964
+ checkout.resolve(await this.activateConnection(connection, checkout.options))
965
+ } catch (error) {
966
+ checkout.reject(error instanceof Error ? error : new Error("Failed to activate database connection.", {cause: error}))
967
+ }
968
+ })
922
969
  })
923
970
  }
924
971
 
@@ -945,6 +992,7 @@ export default class VelociousDatabasePoolAsyncTrackedMultiConnection extends Ba
945
992
  * @returns {Promise<T>} - Resolves with the callback result.
946
993
  */
947
994
  async withConnection(optionsOrCallback, callback) {
995
+ this.assertDatabaseAccessAllowed()
948
996
  const options = typeof optionsOrCallback == "function" ? {} : optionsOrCallback
949
997
  const actualCallback = typeof optionsOrCallback == "function" ? optionsOrCallback : callback
950
998
 
@@ -1046,6 +1094,7 @@ export default class VelociousDatabasePoolAsyncTrackedMultiConnection extends Ba
1046
1094
  * @returns {import("../drivers/base.js").default} - The current connection.
1047
1095
  */
1048
1096
  getCurrentConnection() {
1097
+ this.assertDatabaseAccessAllowed()
1049
1098
  const id = this.asyncLocalStorage.getStore()
1050
1099
 
1051
1100
  if (id === undefined) return this.currentFallbackConnectionOrFail()
@@ -120,6 +120,11 @@ class VelociousDatabasePoolBase {
120
120
  this.logger = new Logger(this)
121
121
  }
122
122
 
123
+ /** Throws when a revoked test attempt tries to acquire database access. */
124
+ assertDatabaseAccessAllowed() {
125
+ this.configuration.assertDatabaseAccessAllowed()
126
+ }
127
+
123
128
  /**
124
129
  * Runs checkin.
125
130
  * @abstract