velocious 1.0.542 → 1.0.544

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 (93) hide show
  1. package/README.md +6 -1
  2. package/build/background-jobs/job-runner.js +1 -1
  3. package/build/background-jobs/job.js +9 -0
  4. package/build/background-jobs/worker.js +1 -1
  5. package/build/cli/commands/db/tenants/check.js +1 -1
  6. package/build/cli/commands/db/tenants/migrate.js +12 -6
  7. package/build/configuration.js +21 -9
  8. package/build/database/advisory-lock-runner.js +3 -1
  9. package/build/database/drivers/base.js +137 -13
  10. package/build/database/drivers/mssql/index.js +5 -5
  11. package/build/database/drivers/mysql/index.js +5 -5
  12. package/build/database/drivers/pgsql/index.js +5 -5
  13. package/build/database/drivers/sqlite/base.js +3 -3
  14. package/build/database/drivers/sqlite/index.js +11 -11
  15. package/build/database/drivers/sqlite/index.native.js +1 -1
  16. package/build/database/drivers/sqlite/index.web.js +1 -1
  17. package/build/database/migrator.js +2 -2
  18. package/build/database/pool/async-tracked-multi-connection.js +1 -0
  19. package/build/database/pool/single-multi-use.js +30 -1
  20. package/build/database/record/index.js +6 -3
  21. package/build/http-server/websocket-event-log-store.js +1 -1
  22. package/build/jobs/prune-terminal-background-jobs.js +3 -0
  23. package/build/src/background-jobs/job-runner.js +2 -2
  24. package/build/src/background-jobs/job.d.ts +8 -0
  25. package/build/src/background-jobs/job.d.ts.map +1 -1
  26. package/build/src/background-jobs/job.js +9 -1
  27. package/build/src/background-jobs/worker.js +2 -2
  28. package/build/src/cli/commands/db/tenants/check.js +2 -2
  29. package/build/src/cli/commands/db/tenants/migrate.d.ts.map +1 -1
  30. package/build/src/cli/commands/db/tenants/migrate.js +11 -7
  31. package/build/src/configuration.d.ts +6 -1
  32. package/build/src/configuration.d.ts.map +1 -1
  33. package/build/src/configuration.js +14 -10
  34. package/build/src/database/advisory-lock-runner.d.ts.map +1 -1
  35. package/build/src/database/advisory-lock-runner.js +5 -2
  36. package/build/src/database/drivers/base.d.ts +56 -7
  37. package/build/src/database/drivers/base.d.ts.map +1 -1
  38. package/build/src/database/drivers/base.js +127 -14
  39. package/build/src/database/drivers/mssql/index.js +6 -6
  40. package/build/src/database/drivers/mysql/index.js +6 -6
  41. package/build/src/database/drivers/pgsql/index.js +6 -6
  42. package/build/src/database/drivers/sqlite/base.js +4 -4
  43. package/build/src/database/drivers/sqlite/index.js +12 -12
  44. package/build/src/database/drivers/sqlite/index.native.js +2 -2
  45. package/build/src/database/drivers/sqlite/index.web.js +2 -2
  46. package/build/src/database/migrator.js +3 -3
  47. package/build/src/database/pool/async-tracked-multi-connection.d.ts.map +1 -1
  48. package/build/src/database/pool/async-tracked-multi-connection.js +2 -1
  49. package/build/src/database/pool/single-multi-use.d.ts +1 -0
  50. package/build/src/database/pool/single-multi-use.d.ts.map +1 -1
  51. package/build/src/database/pool/single-multi-use.js +29 -2
  52. package/build/src/database/record/index.d.ts.map +1 -1
  53. package/build/src/database/record/index.js +7 -4
  54. package/build/src/http-server/websocket-event-log-store.js +2 -2
  55. package/build/src/jobs/prune-terminal-background-jobs.d.ts +2 -0
  56. package/build/src/jobs/prune-terminal-background-jobs.d.ts.map +1 -1
  57. package/build/src/jobs/prune-terminal-background-jobs.js +3 -1
  58. package/build/src/sync/server-change-feed.js +2 -2
  59. package/build/src/sync/server-sequence-allocator.js +2 -2
  60. package/build/src/sync/sync-scope-store.js +2 -2
  61. package/build/src/tenants/default-tenant-database-provisioning.d.ts +3 -2
  62. package/build/src/tenants/default-tenant-database-provisioning.d.ts.map +1 -1
  63. package/build/src/tenants/default-tenant-database-provisioning.js +4 -4
  64. package/build/sync/server-change-feed.js +1 -1
  65. package/build/sync/server-sequence-allocator.js +1 -1
  66. package/build/sync/sync-scope-store.js +1 -1
  67. package/build/tenants/default-tenant-database-provisioning.js +3 -3
  68. package/package.json +1 -1
  69. package/src/background-jobs/job-runner.js +1 -1
  70. package/src/background-jobs/job.js +9 -0
  71. package/src/background-jobs/worker.js +1 -1
  72. package/src/cli/commands/db/tenants/check.js +1 -1
  73. package/src/cli/commands/db/tenants/migrate.js +12 -6
  74. package/src/configuration.js +21 -9
  75. package/src/database/advisory-lock-runner.js +3 -1
  76. package/src/database/drivers/base.js +137 -13
  77. package/src/database/drivers/mssql/index.js +5 -5
  78. package/src/database/drivers/mysql/index.js +5 -5
  79. package/src/database/drivers/pgsql/index.js +5 -5
  80. package/src/database/drivers/sqlite/base.js +3 -3
  81. package/src/database/drivers/sqlite/index.js +11 -11
  82. package/src/database/drivers/sqlite/index.native.js +1 -1
  83. package/src/database/drivers/sqlite/index.web.js +1 -1
  84. package/src/database/migrator.js +2 -2
  85. package/src/database/pool/async-tracked-multi-connection.js +1 -0
  86. package/src/database/pool/single-multi-use.js +30 -1
  87. package/src/database/record/index.js +6 -3
  88. package/src/http-server/websocket-event-log-store.js +1 -1
  89. package/src/jobs/prune-terminal-background-jobs.js +3 -0
  90. package/src/sync/server-change-feed.js +1 -1
  91. package/src/sync/server-sequence-allocator.js +1 -1
  92. package/src/sync/sync-scope-store.js +1 -1
  93. package/src/tenants/default-tenant-database-provisioning.js +3 -3
package/README.md CHANGED
@@ -4,6 +4,7 @@
4
4
  * Database framework with familiar MVC concepts
5
5
  * Database models with migrations and validations
6
6
  * Database models that work almost the same in frontend and backend
7
+ * Connection-scoped advisory locks with automatic cleanup before pooled connections are reused or closed (see [docs/advisory-locks.md](docs/advisory-locks.md))
7
8
  * Built-in record auditing for model lifecycle changes (see [docs/auditing.md](docs/auditing.md))
8
9
  * Declarative state machines for models, with typed event methods generated into the base model (see [docs/state-machine.md](docs/state-machine.md))
9
10
  * Migrations for schema changes and UTC datetime storage (see [docs/database-migrations.md](docs/database-migrations.md))
@@ -31,7 +32,7 @@
31
32
  * Trusted reverse proxy handling for `request.remoteAddress()` (see [docs/trusted-proxies.md](docs/trusted-proxies.md))
32
33
  * In-process driver schema metadata caching (see [docs/schema-metadata-cache.md](docs/schema-metadata-cache.md))
33
34
  * Planned local-first shared-resource sync architecture (see [docs/offline-sync.md](docs/offline-sync.md))
34
- * Named database connection checkouts, bounded pool waits, and debugging held connections (see [docs/database-connections.md](docs/database-connections.md))
35
+ * Selective named database connection checkouts, bounded pool waits, and debugging held connections (see [docs/database-connections.md](docs/database-connections.md))
35
36
  * Optional built-in debug endpoint for inspecting server and database connection state (see [docs/debug-endpoint.md](docs/debug-endpoint.md))
36
37
  * Optional built-in API manifest endpoint describing every registered frontend-model resource as human- and machine-readable JSON (see [docs/api-manifest-endpoint.md](docs/api-manifest-endpoint.md))
37
38
 
@@ -2033,6 +2034,8 @@ import VelociousJob from "velocious/build/src/background-jobs/job.js"
2033
2034
 
2034
2035
  /** @augments {VelociousJob<[string, string]>} */
2035
2036
  export default class MyJob extends VelociousJob {
2037
+ static databaseIdentifiers = ["default"]
2038
+
2036
2039
  /**
2037
2040
  * @param {string} arg1
2038
2041
  * @param {string} arg2
@@ -2051,6 +2054,8 @@ Argument-less jobs use a bare `extends VelociousJob` with `async perform()` —
2051
2054
  default empty-tuple type argument keeps that working unchanged. Plain (unchecked)
2052
2055
  JavaScript can ignore the annotation entirely.
2053
2056
 
2057
+ Set `static databaseIdentifiers` when a job only needs specific configured databases. Velocious checks out only those connections around `perform` in every execution mode. Use `[]` for a job that needs no ambient database connection. Leaving the property undefined preserves the existing behavior of checking out every active database. See [Background Jobs](docs/background-jobs.md#database-connection-scopes).
2058
+
2054
2059
  Queue a job:
2055
2060
 
2056
2061
  ```js
@@ -105,7 +105,7 @@ export default async function runJobPayload(payload, {closeConnections = true, m
105
105
 
106
106
  try {
107
107
  try {
108
- await configuration.withConnections({name: `Background job runner: ${payload.jobName}`}, async () => {
108
+ await configuration.withConnections({databaseIdentifiers: JobClass.databaseIdentifiers, name: `Background job runner: ${payload.jobName}`}, async () => {
109
109
  await perform.apply(jobInstance, payload.args || [])
110
110
  })
111
111
  } catch (error) {
@@ -13,6 +13,15 @@ import BackgroundJobsClient from "./client.js"
13
13
  * @template {Array<?>} [TArgs=[]]
14
14
  */
15
15
  export default class VelociousJob {
16
+ /**
17
+ * Database identifiers checked out while this job performs. Set an explicit
18
+ * list to avoid holding unrelated configured database connections, or `[]`
19
+ * when the job establishes any connections it needs itself. Left undefined,
20
+ * jobs retain the existing behavior of checking out every active database.
21
+ * @type {string[] | undefined}
22
+ */
23
+ static databaseIdentifiers = undefined
24
+
16
25
  /**
17
26
  * Queue this job class runs on. Subclasses set e.g. `static queue = "builds"`
18
27
  * to route onto a queue with its own cluster-wide concurrency cap (configured
@@ -886,7 +886,7 @@ export default class BackgroundJobsWorker {
886
886
  * @type {(...args: Array<?>) => Promise<void>} */
887
887
  const perform = jobInstance.perform
888
888
 
889
- await configuration.withConnections({name: `Background job worker inline: ${payload.jobName}`}, async () => {
889
+ await configuration.withConnections({databaseIdentifiers: JobClass.databaseIdentifiers, name: `Background job worker inline: ${payload.jobName}`}, async () => {
890
890
  await perform.apply(jobInstance, payload.args || [])
891
891
  })
892
892
  }
@@ -24,7 +24,7 @@ export default class DbTenantsCheck extends BaseCommand {
24
24
  })
25
25
  }
26
26
 
27
- await this.getConfiguration().ensureConnections({name: `DB tenants check: ${helper.identifier}`}, async (dbs) => {
27
+ await this.getConfiguration().ensureConnections({databaseIdentifiers: [helper.identifier], name: `DB tenants check: ${helper.identifier}`}, async (dbs) => {
28
28
  const db = dbs[helper.identifier]
29
29
 
30
30
  if (!db) throw new Error(`Tenant database identifier ${helper.identifier} did not open a connection`)
@@ -22,20 +22,26 @@ export default class DbTenantsMigrate extends BaseCommand {
22
22
  databaseIdentifiers: [helper.identifier]
23
23
  })
24
24
 
25
- await this.getConfiguration().ensureConnections({name: `DB tenants migrate: ${helper.identifier}`}, async () => {
25
+ let migrationsApplied = 0
26
+
27
+ await this.getConfiguration().ensureConnections({databaseIdentifiers: [helper.identifier], name: `DB tenants migrate: ${helper.identifier}`}, async () => {
26
28
  await migrator.prepare()
27
- const migrationsApplied = await migrator.migrateFiles(migrations, digg(this.getEnvironmentHandler(), "requireMigration"))
29
+ migrationsApplied = await migrator.migrateFiles(migrations, digg(this.getEnvironmentHandler(), "requireMigration"))
30
+ })
31
+
32
+ const afterMigrateTenant = helper.provider.afterMigrateTenant
28
33
 
29
- if (typeof helper.provider.afterMigrateTenant === "function") {
30
- await helper.provider.afterMigrateTenant({
34
+ if (typeof afterMigrateTenant === "function") {
35
+ await this.getConfiguration().ensureConnections({name: `DB tenants after migrate: ${helper.identifier}`}, async () => {
36
+ await afterMigrateTenant({
31
37
  configuration: this.getConfiguration(),
32
38
  databaseConfiguration,
33
39
  identifier: helper.identifier,
34
40
  migrationsApplied,
35
41
  tenant
36
42
  })
37
- }
38
- })
43
+ })
44
+ }
39
45
  })
40
46
 
41
47
  if (this.args.testing) {
@@ -8,6 +8,7 @@
8
8
  /**
9
9
  * WithConnectionsOptionsType type.
10
10
  * @typedef {object} WithConnectionsOptionsType
11
+ * @property {string[]} [databaseIdentifiers] - Database identifiers to include in the connection scope.
11
12
  * @property {string} [name] - Human-readable name for the checked-out database connections.
12
13
  */
13
14
 
@@ -48,16 +49,17 @@ function currentWorkingDirectory() {
48
49
  * @param {WithConnectionsOptionsType | WithConnectionsCallbackType<T>} optionsOrCallback - Checkout options or callback function.
49
50
  * @param {WithConnectionsCallbackType<T> | undefined} callback - Callback function.
50
51
  * @param {string} defaultName - Default checkout name.
51
- * @returns {{name: string, callback: WithConnectionsCallbackType<T> | undefined}} Resolved checkout name and callback.
52
+ * @returns {{databaseIdentifiers: string[] | undefined, name: string, callback: WithConnectionsCallbackType<T> | undefined}} Resolved checkout options and callback.
52
53
  */
53
54
  function resolveWithConnectionsArgs(optionsOrCallback, callback, defaultName) {
54
55
  if (typeof optionsOrCallback == "function") {
55
56
  const actualCallback = /** @type {WithConnectionsCallbackType<T>} */ (optionsOrCallback)
56
57
 
57
- return {name: defaultName, callback: actualCallback}
58
+ return {databaseIdentifiers: undefined, name: defaultName, callback: actualCallback}
58
59
  }
59
60
 
60
61
  return {
62
+ databaseIdentifiers: optionsOrCallback.databaseIdentifiers,
61
63
  name: optionsOrCallback.name || defaultName,
62
64
  callback
63
65
  }
@@ -2802,7 +2804,11 @@ export default class VelociousConfiguration {
2802
2804
  * @returns {Promise<T>} - Resolves with the callback result.
2803
2805
  */
2804
2806
  async withConnections(optionsOrCallback, callback) {
2805
- const {name, callback: actualWithConnectionsCallback} = resolveWithConnectionsArgs(optionsOrCallback, callback, "Configuration.withConnections")
2807
+ const {
2808
+ callback: actualWithConnectionsCallback,
2809
+ databaseIdentifiers,
2810
+ name
2811
+ } = resolveWithConnectionsArgs(optionsOrCallback, callback, "Configuration.withConnections")
2806
2812
 
2807
2813
  if (!actualWithConnectionsCallback) throw new Error("withConnections requires a callback")
2808
2814
 
@@ -2814,7 +2820,7 @@ export default class VelociousConfiguration {
2814
2820
  return await this.withDatabaseIdentifierConnections({
2815
2821
  callback: actualWithConnectionsCallback,
2816
2822
  dbs,
2817
- identifiers: this.getDatabaseIdentifiers(),
2823
+ identifiers: databaseIdentifiers ?? this.getDatabaseIdentifiers(),
2818
2824
  name,
2819
2825
  stackLabel: "withConnections"
2820
2826
  })
@@ -2858,15 +2864,16 @@ export default class VelociousConfiguration {
2858
2864
 
2859
2865
  /**
2860
2866
  * Runs get current connections.
2867
+ * @param {string[]} [databaseIdentifiers] - Database identifiers to include.
2861
2868
  * @returns {Record<string, import("./database/drivers/base.js").default>} A map of database connections with identifier as key
2862
2869
  */
2863
- getCurrentConnections() {
2870
+ getCurrentConnections(databaseIdentifiers = this.getDatabaseIdentifiers()) {
2864
2871
  /**
2865
2872
  * Dbs.
2866
2873
  * @type {{[key: string]: import("./database/drivers/base.js").default}} */
2867
2874
  const dbs = {}
2868
2875
 
2869
- for (const identifier of this.getDatabaseIdentifiers()) {
2876
+ for (const identifier of databaseIdentifiers) {
2870
2877
  try {
2871
2878
  const pool = this.getDatabasePool(identifier)
2872
2879
  const currentConnection = pool.getCurrentContextConnection ? pool.getCurrentContextConnection() : pool.getCurrentConnection()
@@ -2950,12 +2957,17 @@ export default class VelociousConfiguration {
2950
2957
  * @returns {Promise<T>} - Resolves with the callback result.
2951
2958
  */
2952
2959
  async ensureConnections(optionsOrCallback, callback) {
2953
- const {name, callback: actualWithConnectionsCallback} = resolveWithConnectionsArgs(optionsOrCallback, callback, "Configuration.ensureConnections")
2960
+ const {
2961
+ callback: actualWithConnectionsCallback,
2962
+ databaseIdentifiers,
2963
+ name
2964
+ } = resolveWithConnectionsArgs(optionsOrCallback, callback, "Configuration.ensureConnections")
2954
2965
 
2955
2966
  if (!actualWithConnectionsCallback) throw new Error("ensureConnections requires a callback")
2956
2967
 
2957
- const dbs = this.getCurrentConnections()
2958
- const missingIdentifiers = this.getDatabaseIdentifiers().filter((identifier) => {
2968
+ const requestedIdentifiers = databaseIdentifiers ?? this.getDatabaseIdentifiers()
2969
+ const dbs = this.getCurrentConnections(requestedIdentifiers)
2970
+ const missingIdentifiers = requestedIdentifiers.filter((identifier) => {
2959
2971
  if (!dbs[identifier]) return true
2960
2972
 
2961
2973
  return !this.getDatabasePool(identifier).hasCurrentConnectionContext()
@@ -148,7 +148,9 @@ export default class AdvisoryLockRunner {
148
148
  try {
149
149
  return await callback(connection)
150
150
  } finally {
151
- if (!connection.getArgs().getConnection) {
151
+ if (connection.getArgs().getConnection) {
152
+ await connection.releaseHeldAdvisoryLocks()
153
+ } else {
152
154
  await connection.close()
153
155
  }
154
156
  }
@@ -57,6 +57,7 @@
57
57
  * @property {string} [logName] - Query log subject.
58
58
  * @property {boolean} [logQuery] - Whether to log the query.
59
59
  * @property {boolean} [processListComment] - Whether to add process-list comments to the query.
60
+ * @property {boolean} [retry] - Whether retryable errors may retry the query; defaults to true.
60
61
  * @property {boolean} [sessionTimeZone] - Whether to ensure the configured database session time zone before the query.
61
62
  * @property {string} [sourceStack] - Stack captured at the caller boundary.
62
63
  */
@@ -170,6 +171,8 @@ export default class VelociousDatabaseDriversBase {
170
171
  * Active query.
171
172
  * @type {ActiveQueryState | null} */
172
173
  _activeQuery = null
174
+ /** @type {Map<string, number>} */
175
+ _heldAdvisoryLocks = new Map()
173
176
 
174
177
  /**
175
178
  * Runs constructor.
@@ -239,10 +242,40 @@ export default class VelociousDatabaseDriversBase {
239
242
  }
240
243
 
241
244
  /**
242
- * Optional close hook for database drivers.
243
- * @returns {Promise<void>} - Resolves when complete.
245
+ * Releases tracked advisory locks and closes the physical database connection.
246
+ * @returns {Promise<void>} - Resolves when cleanup and close complete.
244
247
  */
245
248
  async close() {
249
+ /** @type {Error | undefined} */
250
+ let advisoryLockError
251
+
252
+ try {
253
+ await this.releaseHeldAdvisoryLocks()
254
+ } catch (error) {
255
+ advisoryLockError = error instanceof Error ? error : new Error("Failed to release held advisory locks", {cause: error})
256
+ }
257
+
258
+ try {
259
+ await this._close()
260
+ this._heldAdvisoryLocks.clear()
261
+ } catch (error) {
262
+ const closeError = error instanceof Error ? error : new Error("Failed to close database connection", {cause: error})
263
+
264
+ if (advisoryLockError) {
265
+ throw new AggregateError([advisoryLockError, closeError], "Failed to release advisory locks and close database connection", {cause: error})
266
+ }
267
+
268
+ throw closeError
269
+ }
270
+
271
+ if (advisoryLockError) throw advisoryLockError
272
+ }
273
+
274
+ /**
275
+ * Driver-specific physical close hook.
276
+ * @returns {Promise<void>} - Resolves when the underlying connection closes.
277
+ */
278
+ async _close() {
246
279
  // No-op by default
247
280
  }
248
281
 
@@ -1127,7 +1160,7 @@ export default class VelociousDatabaseDriversBase {
1127
1160
 
1128
1161
  const retryInfo = this.retryableDatabaseError(error)
1129
1162
 
1130
- if (tries < maxTries && retryInfo.retry) {
1163
+ if (options.retry !== false && tries < maxTries && retryInfo.retry) {
1131
1164
  if (retryInfo.reconnect) {
1132
1165
  if (this._transactionsCount > 0) {
1133
1166
  throw new Error(`Cannot reconnect while a transaction is active (${this._transactionsCount}). Original error: ${error.message}`, {cause: error})
@@ -1773,33 +1806,124 @@ export default class VelociousDatabaseDriversBase {
1773
1806
  * table locks; they are purely cooperative between callers that use the
1774
1807
  * same name and let you serialize functionality without blocking readers
1775
1808
  * or writers that do not participate in the same lock.
1776
- * @abstract
1777
1809
  * @param {string} name - Lock name.
1778
- * @param {{timeoutMs?: number | null}} [_args] - Optional timeout in milliseconds; `null` or undefined blocks forever.
1810
+ * @param {{timeoutMs?: number | null}} [args] - Optional timeout in milliseconds; `null` or undefined blocks forever.
1779
1811
  * @returns {Promise<boolean>} - Resolves to true when the lock has been acquired, false if the timeout elapsed.
1780
1812
  */
1781
- acquireAdvisoryLock(name, _args = {}) {
1782
- throw new Error(`'acquireAdvisoryLock' not implemented for ${this.constructor.name}`)
1813
+ async acquireAdvisoryLock(name, args = {}) {
1814
+ const acquired = await this._acquireAdvisoryLock(name, args)
1815
+
1816
+ if (acquired) this._trackAdvisoryLock(name)
1817
+
1818
+ return acquired
1783
1819
  }
1784
1820
 
1785
1821
  /**
1786
- * Attempts to acquire a named advisory lock without blocking.
1822
+ * Driver-specific blocking advisory-lock acquisition hook.
1787
1823
  * @abstract
1788
1824
  * @param {string} name - Lock name.
1825
+ * @param {{timeoutMs?: number | null}} [_args] - Lock timeout options.
1826
+ * @returns {Promise<boolean>} - Whether the lock was acquired.
1827
+ */
1828
+ _acquireAdvisoryLock(name, _args = {}) {
1829
+ throw new Error(`'_acquireAdvisoryLock' not implemented for ${this.constructor.name}`)
1830
+ }
1831
+
1832
+ /**
1833
+ * Attempts to acquire a named advisory lock without blocking.
1834
+ * @param {string} name - Lock name.
1789
1835
  * @returns {Promise<boolean>} - Resolves to true if the lock was acquired, false if it was already held.
1790
1836
  */
1791
- tryAcquireAdvisoryLock(name) { // eslint-disable-line no-unused-vars
1792
- throw new Error(`'tryAcquireAdvisoryLock' not implemented for ${this.constructor.name}`)
1837
+ async tryAcquireAdvisoryLock(name) {
1838
+ const acquired = await this._tryAcquireAdvisoryLock(name)
1839
+
1840
+ if (acquired) this._trackAdvisoryLock(name)
1841
+
1842
+ return acquired
1793
1843
  }
1794
1844
 
1795
1845
  /**
1796
- * Releases a named advisory lock previously acquired on this connection.
1846
+ * Driver-specific non-blocking advisory-lock acquisition hook.
1797
1847
  * @abstract
1798
1848
  * @param {string} name - Lock name.
1849
+ * @returns {Promise<boolean>} - Whether the lock was acquired.
1850
+ */
1851
+ _tryAcquireAdvisoryLock(name) { // eslint-disable-line no-unused-vars
1852
+ throw new Error(`'_tryAcquireAdvisoryLock' not implemented for ${this.constructor.name}`)
1853
+ }
1854
+
1855
+ /**
1856
+ * Releases a named advisory lock previously acquired on this connection.
1857
+ * @param {string} name - Lock name.
1799
1858
  * @returns {Promise<boolean>} - Resolves to true if the lock was held by this session and has now been released.
1800
1859
  */
1801
- releaseAdvisoryLock(name) { // eslint-disable-line no-unused-vars
1802
- throw new Error(`'releaseAdvisoryLock' not implemented for ${this.constructor.name}`)
1860
+ async releaseAdvisoryLock(name) {
1861
+ const released = await this._releaseAdvisoryLock(name)
1862
+
1863
+ if (released) {
1864
+ this._untrackAdvisoryLock(name)
1865
+ } else {
1866
+ this._heldAdvisoryLocks.delete(name)
1867
+ }
1868
+
1869
+ return released
1870
+ }
1871
+
1872
+ /**
1873
+ * Driver-specific advisory-lock release hook.
1874
+ * @abstract
1875
+ * @param {string} name - Lock name.
1876
+ * @returns {Promise<boolean>} - Whether the lock was released.
1877
+ */
1878
+ _releaseAdvisoryLock(name) { // eslint-disable-line no-unused-vars
1879
+ throw new Error(`'_releaseAdvisoryLock' not implemented for ${this.constructor.name}`)
1880
+ }
1881
+
1882
+ /**
1883
+ * Releases every advisory lock still tracked on this connection.
1884
+ * @returns {Promise<void>} - Resolves when every tracked lock is released.
1885
+ */
1886
+ async releaseHeldAdvisoryLocks() {
1887
+ /** @type {Error[]} */
1888
+ const errors = []
1889
+
1890
+ for (const name of [...this._heldAdvisoryLocks.keys()]) {
1891
+ while (this._heldAdvisoryLocks.has(name)) {
1892
+ try {
1893
+ await this.releaseAdvisoryLock(name)
1894
+ } catch (error) {
1895
+ errors.push(error instanceof Error ? error : new Error(`Failed to release advisory lock ${JSON.stringify(name)}`, {cause: error}))
1896
+ break
1897
+ }
1898
+ }
1899
+ }
1900
+
1901
+ if (errors.length == 1) throw errors[0]
1902
+ if (errors.length > 1) throw new AggregateError(errors, "Failed to release held advisory locks")
1903
+ }
1904
+
1905
+ /**
1906
+ * Records one successful acquisition, including re-entrant acquisitions.
1907
+ * @param {string} name - Lock name.
1908
+ * @returns {void}
1909
+ */
1910
+ _trackAdvisoryLock(name) {
1911
+ this._heldAdvisoryLocks.set(name, (this._heldAdvisoryLocks.get(name) || 0) + 1)
1912
+ }
1913
+
1914
+ /**
1915
+ * Removes one successful acquisition from the connection registry.
1916
+ * @param {string} name - Lock name.
1917
+ * @returns {void}
1918
+ */
1919
+ _untrackAdvisoryLock(name) {
1920
+ const remainingCount = (this._heldAdvisoryLocks.get(name) || 0) - 1
1921
+
1922
+ if (remainingCount > 0) {
1923
+ this._heldAdvisoryLocks.set(name, remainingCount)
1924
+ } else {
1925
+ this._heldAdvisoryLocks.delete(name)
1926
+ }
1803
1927
  }
1804
1928
 
1805
1929
  /**
@@ -47,7 +47,7 @@ export default class VelociousDatabaseDriversMssql extends Base{
47
47
  }
48
48
  }
49
49
 
50
- async close() {
50
+ async _close() {
51
51
  if (!this.connection) return
52
52
 
53
53
  const connection = this.connection
@@ -616,7 +616,7 @@ export default class VelociousDatabaseDriversMssql extends Base{
616
616
  * @param {{timeoutMs?: number | null}} [args] - Optional timeout in milliseconds; `null`, `undefined`, or negative blocks forever.
617
617
  * @returns {Promise<boolean>} - True if the lock was acquired, false if the timeout elapsed.
618
618
  */
619
- async acquireAdvisoryLock(name, {timeoutMs} = {}) {
619
+ async _acquireAdvisoryLock(name, {timeoutMs} = {}) {
620
620
  const timeoutValue = typeof timeoutMs === "number" && timeoutMs >= 0 ? Math.ceil(timeoutMs) : -1
621
621
  const rows = await this.query(
622
622
  `DECLARE @velocious_advisory_lock_result INT; EXEC @velocious_advisory_lock_result = sp_getapplock @Resource = ${this.quote(name)}, @LockMode = 'Exclusive', @LockOwner = 'Session', @LockTimeout = ${timeoutValue}; SELECT @velocious_advisory_lock_result AS velocious_advisory_lock_result`
@@ -634,8 +634,8 @@ export default class VelociousDatabaseDriversMssql extends Base{
634
634
  * @param {string} name - Lock name.
635
635
  * @returns {Promise<boolean>} - True if the lock was acquired, false if it was already held.
636
636
  */
637
- async tryAcquireAdvisoryLock(name) {
638
- return await this.acquireAdvisoryLock(name, {timeoutMs: 0})
637
+ async _tryAcquireAdvisoryLock(name) {
638
+ return await this._acquireAdvisoryLock(name, {timeoutMs: 0})
639
639
  }
640
640
 
641
641
  /**
@@ -643,7 +643,7 @@ export default class VelociousDatabaseDriversMssql extends Base{
643
643
  * @param {string} name - Lock name.
644
644
  * @returns {Promise<boolean>} - True if the lock was held by this session and has now been released.
645
645
  */
646
- async releaseAdvisoryLock(name) {
646
+ async _releaseAdvisoryLock(name) {
647
647
  const rows = await this.query(
648
648
  `DECLARE @velocious_advisory_lock_result INT; EXEC @velocious_advisory_lock_result = sp_releaseapplock @Resource = ${this.quote(name)}, @LockOwner = 'Session'; SELECT @velocious_advisory_lock_result AS velocious_advisory_lock_result`
649
649
  )
@@ -63,7 +63,7 @@ export default class VelociousDatabaseDriversMysql extends Base{
63
63
  * Runs close.
64
64
  * @returns {Promise<void>} - Resolves when complete.
65
65
  */
66
- async close() {
66
+ async _close() {
67
67
  await this.pool?.end()
68
68
  this.pool = undefined
69
69
  this.resetCurrentSessionTimeZone()
@@ -574,7 +574,7 @@ export default class VelociousDatabaseDriversMysql extends Base{
574
574
  * @param {{timeoutMs?: number | null}} [args] - Optional timeout in milliseconds; `null`, `undefined`, or negative blocks for `MYSQL_INDEFINITE_LOCK_TIMEOUT_SECONDS`.
575
575
  * @returns {Promise<boolean>} - True if acquired, false if the timeout elapsed.
576
576
  */
577
- async acquireAdvisoryLock(name, {timeoutMs} = {}) {
577
+ async _acquireAdvisoryLock(name, {timeoutMs} = {}) {
578
578
  const timeoutSeconds = typeof timeoutMs === "number" && timeoutMs >= 0
579
579
  ? Math.ceil(timeoutMs / 1000)
580
580
  : MYSQL_INDEFINITE_LOCK_TIMEOUT_SECONDS
@@ -593,7 +593,7 @@ export default class VelociousDatabaseDriversMysql extends Base{
593
593
  * @param {string} name - Lock name.
594
594
  * @returns {Promise<boolean>} - True if the lock was acquired, false if it was already held.
595
595
  */
596
- async tryAcquireAdvisoryLock(name) {
596
+ async _tryAcquireAdvisoryLock(name) {
597
597
  const rows = await this.query(`SELECT GET_LOCK(${this.quote(name)}, 0) AS velocious_advisory_lock_result`)
598
598
  const result = rows?.[0]?.velocious_advisory_lock_result
599
599
 
@@ -609,8 +609,8 @@ export default class VelociousDatabaseDriversMysql extends Base{
609
609
  * @param {string} name - Lock name.
610
610
  * @returns {Promise<boolean>} - True if the lock was held by this session and has now been released.
611
611
  */
612
- async releaseAdvisoryLock(name) {
613
- const rows = await this.query(`SELECT RELEASE_LOCK(${this.quote(name)}) AS velocious_advisory_lock_result`)
612
+ async _releaseAdvisoryLock(name) {
613
+ const rows = await this.query(`SELECT RELEASE_LOCK(${this.quote(name)}) AS velocious_advisory_lock_result`, {retry: false})
614
614
  const result = rows?.[0]?.velocious_advisory_lock_result
615
615
 
616
616
  return Number(result) === 1
@@ -60,7 +60,7 @@ export default class VelociousDatabaseDriversPgsql extends Base{
60
60
  return connectArgs
61
61
  }
62
62
 
63
- async close() {
63
+ async _close() {
64
64
  await this.connection?.end()
65
65
  this.connection = undefined
66
66
  this._transactionsCount = 0
@@ -411,7 +411,7 @@ export default class VelociousDatabaseDriversPgsql extends Base{
411
411
  * @param {{timeoutMs?: number | null}} [args] - Optional timeout in milliseconds; `null`, `undefined`, or negative blocks forever.
412
412
  * @returns {Promise<boolean>} - True if the lock was acquired, false if the timeout elapsed.
413
413
  */
414
- async acquireAdvisoryLock(name, {timeoutMs} = {}) {
414
+ async _acquireAdvisoryLock(name, {timeoutMs} = {}) {
415
415
  const key = this.advisoryLockKey(name)
416
416
 
417
417
  if (typeof timeoutMs !== "number" || timeoutMs < 0) {
@@ -423,7 +423,7 @@ export default class VelociousDatabaseDriversPgsql extends Base{
423
423
  const pollIntervalMs = 50
424
424
 
425
425
  while (true) {
426
- if (await this.tryAcquireAdvisoryLock(name)) return true
426
+ if (await this._tryAcquireAdvisoryLock(name)) return true
427
427
  if (Date.now() >= deadline) return false
428
428
 
429
429
  const remaining = deadline - Date.now()
@@ -437,7 +437,7 @@ export default class VelociousDatabaseDriversPgsql extends Base{
437
437
  * @param {string} name - Lock name.
438
438
  * @returns {Promise<boolean>} - True if the lock was acquired, false if it was already held.
439
439
  */
440
- async tryAcquireAdvisoryLock(name) {
440
+ async _tryAcquireAdvisoryLock(name) {
441
441
  const key = this.advisoryLockKey(name)
442
442
  const rows = await this.query(`SELECT pg_try_advisory_lock(${key}) AS velocious_advisory_lock_result`)
443
443
  const result = rows?.[0]?.velocious_advisory_lock_result
@@ -450,7 +450,7 @@ export default class VelociousDatabaseDriversPgsql extends Base{
450
450
  * @param {string} name - Lock name.
451
451
  * @returns {Promise<boolean>} - True if the lock was held by this session and has now been released.
452
452
  */
453
- async releaseAdvisoryLock(name) {
453
+ async _releaseAdvisoryLock(name) {
454
454
  const key = this.advisoryLockKey(name)
455
455
  const rows = await this.query(`SELECT pg_advisory_unlock(${key}) AS velocious_advisory_lock_result`)
456
456
  const result = rows?.[0]?.velocious_advisory_lock_result
@@ -385,7 +385,7 @@ export default class VelociousDatabaseDriversSqliteBase extends Base {
385
385
  * @param {{timeoutMs?: number | null}} [args] - Optional timeout in milliseconds; `null`, `undefined`, or negative blocks forever.
386
386
  * @returns {Promise<boolean>} - True if the lock was acquired, false if the timeout elapsed.
387
387
  */
388
- async acquireAdvisoryLock(name, {timeoutMs} = {}) {
388
+ async _acquireAdvisoryLock(name, {timeoutMs} = {}) {
389
389
  const state = VelociousDatabaseDriversSqliteBase._advisoryLockState
390
390
 
391
391
  while (state.ownersByName.has(name)) {
@@ -445,7 +445,7 @@ export default class VelociousDatabaseDriversSqliteBase extends Base {
445
445
  * @param {string} name - Lock name.
446
446
  * @returns {Promise<boolean>} - True if the lock was acquired, false if it was already held.
447
447
  */
448
- async tryAcquireAdvisoryLock(name) {
448
+ async _tryAcquireAdvisoryLock(name) {
449
449
  const state = VelociousDatabaseDriversSqliteBase._advisoryLockState
450
450
 
451
451
  if (state.ownersByName.has(name)) return false
@@ -464,7 +464,7 @@ export default class VelociousDatabaseDriversSqliteBase extends Base {
464
464
  * @param {string} name - Lock name.
465
465
  * @returns {Promise<boolean>} - True if the lock was held by this driver and has now been released.
466
466
  */
467
- async releaseAdvisoryLock(name) {
467
+ async _releaseAdvisoryLock(name) {
468
468
  const state = VelociousDatabaseDriversSqliteBase._advisoryLockState
469
469
  const owner = state.ownersByName.get(name)
470
470
 
@@ -62,7 +62,7 @@ export default class VelociousDatabaseDriversSqliteNode extends Base {
62
62
  return `VelociousDatabaseDriversSqlite---${args.name}`
63
63
  }
64
64
 
65
- async close() {
65
+ async _close() {
66
66
  await this.connection?.close()
67
67
  this.connection = undefined
68
68
  }
@@ -105,10 +105,10 @@ export default class VelociousDatabaseDriversSqliteNode extends Base {
105
105
  * @param {{timeoutMs?: number | null}} [args] - Optional timeout in milliseconds; `null`, `undefined`, or negative blocks forever.
106
106
  * @returns {Promise<boolean>} - Whether the advisory lock was acquired.
107
107
  */
108
- async acquireAdvisoryLock(name, {timeoutMs} = {}) {
108
+ async _acquireAdvisoryLock(name, {timeoutMs} = {}) {
109
109
  const deadline = typeof timeoutMs === "number" && timeoutMs >= 0 ? Date.now() + timeoutMs : null
110
110
  const remainingForInProcess = deadline !== null ? Math.max(0, deadline - Date.now()) : null
111
- const inProcessAcquired = await super.acquireAdvisoryLock(name, {timeoutMs: remainingForInProcess})
111
+ const inProcessAcquired = await super._acquireAdvisoryLock(name, {timeoutMs: remainingForInProcess})
112
112
 
113
113
  if (!inProcessAcquired) return false
114
114
 
@@ -117,11 +117,11 @@ export default class VelociousDatabaseDriversSqliteNode extends Base {
117
117
  const fileAcquired = await this._acquireAdvisoryLockFile(name, {timeoutMs: remainingForFile})
118
118
 
119
119
  if (!fileAcquired) {
120
- await super.releaseAdvisoryLock(name)
120
+ await super._releaseAdvisoryLock(name)
121
121
  return false
122
122
  }
123
123
  } catch (error) {
124
- await super.releaseAdvisoryLock(name)
124
+ await super._releaseAdvisoryLock(name)
125
125
  throw error
126
126
  }
127
127
 
@@ -133,8 +133,8 @@ export default class VelociousDatabaseDriversSqliteNode extends Base {
133
133
  * @param {string} name - Lock name.
134
134
  * @returns {Promise<boolean>} - Whether the advisory lock was acquired immediately.
135
135
  */
136
- async tryAcquireAdvisoryLock(name) {
137
- const inProcessAcquired = await super.tryAcquireAdvisoryLock(name)
136
+ async _tryAcquireAdvisoryLock(name) {
137
+ const inProcessAcquired = await super._tryAcquireAdvisoryLock(name)
138
138
 
139
139
  if (!inProcessAcquired) return false
140
140
 
@@ -142,11 +142,11 @@ export default class VelociousDatabaseDriversSqliteNode extends Base {
142
142
  const fileAcquired = await this._tryAcquireAdvisoryLockFile(name)
143
143
 
144
144
  if (!fileAcquired) {
145
- await super.releaseAdvisoryLock(name)
145
+ await super._releaseAdvisoryLock(name)
146
146
  return false
147
147
  }
148
148
  } catch (error) {
149
- await super.releaseAdvisoryLock(name)
149
+ await super._releaseAdvisoryLock(name)
150
150
  throw error
151
151
  }
152
152
 
@@ -163,8 +163,8 @@ export default class VelociousDatabaseDriversSqliteNode extends Base {
163
163
  * @param {string} name - Lock name.
164
164
  * @returns {Promise<boolean>} - Whether the advisory lock was released.
165
165
  */
166
- async releaseAdvisoryLock(name) {
167
- const inProcessReleased = await super.releaseAdvisoryLock(name)
166
+ async _releaseAdvisoryLock(name) {
167
+ const inProcessReleased = await super._releaseAdvisoryLock(name)
168
168
 
169
169
  if (inProcessReleased) {
170
170
  await this._releaseAdvisoryLockFile(name)
@@ -64,7 +64,7 @@ export default class VelociousDatabaseDriversSqliteNative extends Base {
64
64
  return connectArgs
65
65
  }
66
66
 
67
- async close() {
67
+ async _close() {
68
68
  await this.connection.closeAsync()
69
69
  this.connection = undefined
70
70
  }
@@ -48,7 +48,7 @@ export default class VelociousDatabaseDriversSqliteWeb extends Base {
48
48
  }
49
49
  }
50
50
 
51
- async close() {
51
+ async _close() {
52
52
  await this.getConnection().close()
53
53
  }
54
54