velocious 1.0.698 → 1.0.700

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 (95) hide show
  1. package/README.md +2 -2
  2. package/build/configuration-types.js +23 -0
  3. package/build/configuration.js +136 -1
  4. package/build/database/advisory-lock-backend-policy.js +204 -0
  5. package/build/database/advisory-lock-file-backend.js +348 -0
  6. package/build/database/advisory-lock-runner.js +239 -16
  7. package/build/database/drivers/base.js +318 -15
  8. package/build/database/drivers/mssql/index.js +1 -1
  9. package/build/database/drivers/mysql/index.js +4 -2
  10. package/build/database/drivers/pgsql/index.js +4 -2
  11. package/build/database/drivers/sqlite/base.js +13 -129
  12. package/build/database/drivers/sqlite/index.js +41 -62
  13. package/build/database/migrator.js +120 -50
  14. package/build/database/record/attachments/store.js +84 -1
  15. package/build/environment-handlers/base.js +13 -0
  16. package/build/environment-handlers/node.js +11 -0
  17. package/build/http-server/server-lock.js +21 -115
  18. package/build/src/configuration-types.d.ts +68 -0
  19. package/build/src/configuration-types.d.ts.map +1 -1
  20. package/build/src/configuration-types.js +21 -1
  21. package/build/src/configuration.d.ts +100 -1
  22. package/build/src/configuration.d.ts.map +1 -1
  23. package/build/src/configuration.js +124 -2
  24. package/build/src/database/advisory-lock-backend-policy.d.ts +145 -0
  25. package/build/src/database/advisory-lock-backend-policy.d.ts.map +1 -0
  26. package/build/src/database/advisory-lock-backend-policy.js +184 -0
  27. package/build/src/database/advisory-lock-file-backend.d.ts +194 -0
  28. package/build/src/database/advisory-lock-file-backend.d.ts.map +1 -0
  29. package/build/src/database/advisory-lock-file-backend.js +313 -0
  30. package/build/src/database/advisory-lock-runner.d.ts +116 -15
  31. package/build/src/database/advisory-lock-runner.d.ts.map +1 -1
  32. package/build/src/database/advisory-lock-runner.js +190 -17
  33. package/build/src/database/drivers/base.d.ts +125 -4
  34. package/build/src/database/drivers/base.d.ts.map +1 -1
  35. package/build/src/database/drivers/base.js +282 -17
  36. package/build/src/database/drivers/mssql/index.d.ts +1 -1
  37. package/build/src/database/drivers/mssql/index.d.ts.map +1 -1
  38. package/build/src/database/drivers/mssql/index.js +2 -2
  39. package/build/src/database/drivers/mysql/index.d.ts +4 -2
  40. package/build/src/database/drivers/mysql/index.d.ts.map +1 -1
  41. package/build/src/database/drivers/mysql/index.js +5 -3
  42. package/build/src/database/drivers/pgsql/index.d.ts +4 -2
  43. package/build/src/database/drivers/pgsql/index.d.ts.map +1 -1
  44. package/build/src/database/drivers/pgsql/index.js +5 -3
  45. package/build/src/database/drivers/sqlite/base.d.ts +11 -49
  46. package/build/src/database/drivers/sqlite/base.d.ts.map +1 -1
  47. package/build/src/database/drivers/sqlite/base.js +14 -114
  48. package/build/src/database/drivers/sqlite/index.d.ts +28 -19
  49. package/build/src/database/drivers/sqlite/index.d.ts.map +1 -1
  50. package/build/src/database/drivers/sqlite/index.js +41 -59
  51. package/build/src/database/migrator.d.ts.map +1 -1
  52. package/build/src/database/migrator.js +110 -56
  53. package/build/src/database/record/attachments/store.d.ts +0 -3
  54. package/build/src/database/record/attachments/store.d.ts.map +1 -1
  55. package/build/src/database/record/attachments/store.js +71 -2
  56. package/build/src/environment-handlers/base.d.ts +14 -0
  57. package/build/src/environment-handlers/base.d.ts.map +1 -1
  58. package/build/src/environment-handlers/base.js +13 -1
  59. package/build/src/environment-handlers/node.d.ts +11 -0
  60. package/build/src/environment-handlers/node.d.ts.map +1 -1
  61. package/build/src/environment-handlers/node.js +11 -1
  62. package/build/src/http-server/server-lock.d.ts +13 -37
  63. package/build/src/http-server/server-lock.d.ts.map +1 -1
  64. package/build/src/http-server/server-lock.js +22 -109
  65. package/build/src/testing/test-runner.d.ts +74 -0
  66. package/build/src/testing/test-runner.d.ts.map +1 -1
  67. package/build/src/testing/test-runner.js +140 -1
  68. package/build/src/testing/test-tenant-database-pool.d.ts +183 -0
  69. package/build/src/testing/test-tenant-database-pool.d.ts.map +1 -0
  70. package/build/src/testing/test-tenant-database-pool.js +388 -0
  71. package/build/src/testing/velocious-attempt-executor.d.ts.map +1 -1
  72. package/build/src/testing/velocious-attempt-executor.js +43 -1
  73. package/build/testing/test-runner.js +152 -0
  74. package/build/testing/test-tenant-database-pool.js +438 -0
  75. package/build/testing/velocious-attempt-executor.js +44 -0
  76. package/package.json +1 -1
  77. package/src/configuration-types.js +23 -0
  78. package/src/configuration.js +136 -1
  79. package/src/database/advisory-lock-backend-policy.js +204 -0
  80. package/src/database/advisory-lock-file-backend.js +348 -0
  81. package/src/database/advisory-lock-runner.js +239 -16
  82. package/src/database/drivers/base.js +318 -15
  83. package/src/database/drivers/mssql/index.js +1 -1
  84. package/src/database/drivers/mysql/index.js +4 -2
  85. package/src/database/drivers/pgsql/index.js +4 -2
  86. package/src/database/drivers/sqlite/base.js +13 -129
  87. package/src/database/drivers/sqlite/index.js +41 -62
  88. package/src/database/migrator.js +120 -50
  89. package/src/database/record/attachments/store.js +84 -1
  90. package/src/environment-handlers/base.js +13 -0
  91. package/src/environment-handlers/node.js +11 -0
  92. package/src/http-server/server-lock.js +21 -115
  93. package/src/testing/test-runner.js +152 -0
  94. package/src/testing/test-tenant-database-pool.js +438 -0
  95. package/src/testing/velocious-attempt-executor.js +44 -0
@@ -1,11 +1,15 @@
1
1
  // @ts-check
2
2
 
3
- import fs from "node:fs/promises"
4
- import os from "node:os"
3
+ import AdvisoryLockFileBackend from "../database/advisory-lock-file-backend.js"
5
4
  import path from "node:path"
6
5
 
7
6
  /**
8
- * Directory-based lock that allows only one HTTP server process per app directory.
7
+ * Directory-based lock that allows only one HTTP server process per app
8
+ * directory. Thin wrapper over the shared file-lock backend
9
+ * (`src/database/advisory-lock-file-backend.js` — the STC-210 `file` tier
10
+ * primitive generalized from this lock's original hand-rolled directory
11
+ * protocol), so the server lock and the advisory-lock file tier share one
12
+ * acquisition, owner-metadata, lease-renewal, and staleness implementation.
9
13
  */
10
14
  export default class VelociousHttpServerLock {
11
15
  /**
@@ -20,53 +24,26 @@ export default class VelociousHttpServerLock {
20
24
  this.host = host
21
25
  this.port = port
22
26
  this.lockPath = path.join(configuration.getDirectory(), "tmp", "server.lock")
27
+ this.backend = new AdvisoryLockFileBackend({
28
+ directory: path.join(configuration.getDirectory(), "tmp"),
29
+ extraOwnerFields: {host: this.host, port: this.port},
30
+ pathStyle: "plain"
31
+ })
23
32
  this.acquired = false
24
33
  }
25
34
 
26
35
  /**
27
- * Acquires the app-directory HTTP server lock before startup side effects run.
36
+ * Acquires the app-directory HTTP server lock before startup side effects
37
+ * run. A stale lock (dead local owner or expired lease) is reclaimed by the
38
+ * backend; a live foreign owner fails loudly with the owner details.
28
39
  * @returns {Promise<void>} - Resolves after the lock has been acquired.
29
40
  */
30
41
  async acquire() {
31
- await fs.mkdir(path.dirname(this.lockPath), {recursive: true})
42
+ const acquired = await this.backend.acquire("server", {timeoutMs: 0})
32
43
 
33
- while (true) {
34
- const acquired = await this.tryAcquire()
44
+ if (!acquired) throw new Error(await this.lockHeldMessage())
35
45
 
36
- if (acquired) {
37
- return
38
- }
39
-
40
- if (await this.isStale()) {
41
- await fs.rm(this.lockPath, {recursive: true, force: true})
42
- continue
43
- }
44
-
45
- throw new Error(await this.lockHeldMessage())
46
- }
47
- }
48
-
49
- /**
50
- * Tries to create the lock directory and write owner metadata.
51
- * @returns {Promise<boolean>} - Whether the lock was acquired.
52
- */
53
- async tryAcquire() {
54
- try {
55
- await fs.mkdir(this.lockPath)
56
- } catch (error) {
57
- if (/** @type {{code?: string}} */ (error).code === "EEXIST") return false
58
- throw error
59
- }
60
-
61
- try {
62
- await this.writeOwnerMetadata()
63
- this.acquired = true
64
-
65
- return true
66
- } catch (error) {
67
- await fs.rm(this.lockPath, {recursive: true, force: true})
68
- throw error
69
- }
46
+ this.acquired = true
70
47
  }
71
48
 
72
49
  /**
@@ -77,86 +54,15 @@ export default class VelociousHttpServerLock {
77
54
  if (!this.acquired) return
78
55
 
79
56
  this.acquired = false
80
- await fs.rm(this.lockPath, {recursive: true, force: true})
81
- }
82
-
83
- /**
84
- * Writes metadata used to explain or reclaim an existing server lock.
85
- * @returns {Promise<void>} - Resolves after owner metadata has been written.
86
- */
87
- async writeOwnerMetadata() {
88
- await fs.writeFile(path.join(this.lockPath, "owner.json"), JSON.stringify({
89
- acquiredAt: new Date().toISOString(),
90
- host: this.host,
91
- hostname: os.hostname(),
92
- pid: process.pid,
93
- port: this.port
94
- }))
95
- }
96
-
97
- /**
98
- * Checks whether the current lock owner is a dead process on this host.
99
- * @returns {Promise<boolean>} - Whether the existing lock belongs to a dead process.
100
- */
101
- async isStale() {
102
- const owner = await this.readOwnerMetadata()
103
-
104
- if (!this.isLocalProcessOwner(owner)) return false
105
-
106
- return this.processIsDead(/** @type {{pid: number}} */ (owner).pid)
107
- }
108
-
109
- /**
110
- * Runs is local process owner.
111
- * @param {Record<string, ReturnType<typeof JSON.parse>> | null} owner - Existing lock owner metadata.
112
- * @returns {boolean} - Whether owner metadata names a local process.
113
- */
114
- isLocalProcessOwner(owner) {
115
- if (!owner) return false
116
- if (typeof owner.pid !== "number") return false
117
-
118
- return this.ownerHostnameMatches(owner)
119
- }
120
-
121
- /**
122
- * Runs owner hostname matches.
123
- * @param {Record<string, ReturnType<typeof JSON.parse>>} owner - Existing lock owner metadata.
124
- * @returns {boolean} - Whether the owner hostname is local or absent.
125
- */
126
- ownerHostnameMatches(owner) {
127
- if (!owner.hostname) return true
128
- if (owner.hostname === os.hostname()) return true
129
-
130
- return false
131
- }
132
-
133
- /**
134
- * Runs process is dead.
135
- * @param {number} pid - Process id.
136
- * @returns {boolean} - Whether the process no longer exists.
137
- */
138
- processIsDead(pid) {
139
- try {
140
- process.kill(pid, 0)
141
-
142
- return false
143
- } catch (error) {
144
- return /** @type {{code?: string}} */ (error).code === "ESRCH"
145
- }
57
+ await this.backend.release("server")
146
58
  }
147
59
 
148
60
  /**
149
61
  * Reads owner metadata from an existing server lock directory.
150
- * @returns {Promise<Record<string, ReturnType<typeof JSON.parse>> | null>} - Parsed owner metadata, when readable.
62
+ * @returns {Promise<import("../database/advisory-lock-file-backend.js").AdvisoryLockFileOwner | null>} - Parsed owner metadata, when readable.
151
63
  */
152
64
  async readOwnerMetadata() {
153
- try {
154
- const rawOwner = await fs.readFile(path.join(this.lockPath, "owner.json"), "utf8")
155
-
156
- return /** @type {Record<string, ReturnType<typeof JSON.parse>>} */ (JSON.parse(rawOwner))
157
- } catch {
158
- return null
159
- }
65
+ return await this.backend.holderInfo("server")
160
66
  }
161
67
 
162
68
  /**
@@ -8,6 +8,7 @@ import { slowestTestResults } from "@velocious/testing/reporters"
8
8
  import { TestRunner as PackageTestRunner } from "@velocious/testing/runner"
9
9
  import Application from "../../src/application.js"
10
10
  import RequestClient from "./request-client.js"
11
+ import Tenant from "../tenants/tenant.js"
11
12
  import picocolors from "picocolors"
12
13
  import restArgsError from "../utils/rest-args-error.js"
13
14
  import { testConfig } from "./test.js"
@@ -17,6 +18,7 @@ import { SHARED_TRANSACTION_BROKER_ENV } from "./shared-transaction-proxy-driver
17
18
  import VelociousAttemptExecutor from "./velocious-attempt-executor.js"
18
19
  import VelociousRunnerReporter, { AbortRemainingTestsError } from "./velocious-runner-reporter.js"
19
20
  import VelociousSuiteHookExecutor from "./velocious-suite-hook-executor.js"
21
+ import TestTenantDatabasePool from "./test-tenant-database-pool.js"
20
22
  import VelociousTestArguments from "./velocious-test-arguments.js"
21
23
 
22
24
  /** @typedef {typeof defaultTestContext} PackageTestContext */
@@ -48,6 +50,18 @@ import VelociousTestArguments from "./velocious-test-arguments.js"
48
50
  * @property {number} [timeoutSeconds] - Timeout in seconds for the test.
49
51
  * @property {string} [type] - Test type identifier.
50
52
  * @property {(args: {databaseIdentifier: string, tenant: object}) => Promise<void>} [registerTransactionalTenant] - Registers one resolved tenant database transaction for this attempt.
53
+ * @property {(args?: {databaseIdentifier?: string, tenant?: object}) => Promise<TestTenantPoolLease>} [acquireTestTenant] - Leases one member of the configured test tenant database pool for this attempt (opt-in `testTenantPools` configuration).
54
+ * @property {(args: {databaseIdentifier: string, tenant: object}) => void} [registerTenantForCleanup] - Registers one active tenant database for the framework-owned end-of-attempt cleanup (drop + provider hooks) without hand-rolled afterEach bookkeeping.
55
+ */
56
+ /**
57
+ * TestTenantPoolLease type (re-exported from the pool for TestArgs consumers).
58
+ * @typedef {import("./test-tenant-database-pool.js").TestTenantPoolLease} TestTenantPoolLease
59
+ */
60
+ /**
61
+ * TenantCleanupRegistration type.
62
+ * @typedef {object} TenantCleanupRegistration
63
+ * @property {string} databaseIdentifier - Tenant database identifier to drop at attempt end.
64
+ * @property {ReturnType<typeof JSON.parse>} tenant - Tenant descriptor whose database must be dropped.
51
65
  */
52
66
  /**
53
67
  * BrowserDummyConnectionRegistration type.
@@ -201,6 +215,10 @@ export default class TestRunner {
201
215
  this._setupFiles = setupFiles || []
202
216
  this._timeoutMs = timeoutMs
203
217
  this._abortRemainingTests = false
218
+ /** @type {Map<string, TestTenantDatabasePool>} */
219
+ this._testTenantPools = new Map()
220
+ /** @type {Promise<void> | undefined} */
221
+ this._testTenantPoolTeardownPromise = undefined
204
222
 
205
223
  this._failedTests = 0
206
224
  this._successfulTests = 0
@@ -632,6 +650,81 @@ export default class TestRunner {
632
650
  return this._application
633
651
  }
634
652
 
653
+ /**
654
+ * Resolves the pool identifier when exactly one `testTenantPools` entry is
655
+ * configured, so `acquireTestTenant()` can omit it. Ambiguous or missing
656
+ * configuration fails loudly instead of guessing.
657
+ * @returns {string} - The single configured pool identifier.
658
+ */
659
+ singleTestTenantPoolIdentifier() {
660
+ const identifiers = Object.keys(this.getConfiguration().getTestTenantPools() || {})
661
+
662
+ if (identifiers.length === 0) {
663
+ throw new Error("acquireTestTenant requires a databaseIdentifier when no testTenantPools configuration is present")
664
+ }
665
+ if (identifiers.length > 1) {
666
+ throw new Error(`acquireTestTenant requires a databaseIdentifier when multiple testTenantPools are configured: ${identifiers.join(", ")}`)
667
+ }
668
+
669
+ return identifiers[0]
670
+ }
671
+
672
+ /**
673
+ * Returns the test tenant database pool for one configured `testTenantPools`
674
+ * identifier, creating it on first use. Throws when the identifier has no pool
675
+ * configuration — the pool stays strictly opt-in.
676
+ * @param {string} databaseIdentifier - Tenant database identifier.
677
+ * @returns {TestTenantDatabasePool} - The runner-owned pool.
678
+ */
679
+ getTestTenantPool(databaseIdentifier) {
680
+ const existingPool = this._testTenantPools.get(databaseIdentifier)
681
+
682
+ if (existingPool) return existingPool
683
+
684
+ const poolConfiguration = this.getConfiguration().getTestTenantPools()?.[databaseIdentifier]
685
+
686
+ if (!poolConfiguration) {
687
+ throw new Error(`No testTenantPools configuration for database identifier: ${databaseIdentifier}`)
688
+ }
689
+
690
+ const pool = new TestTenantDatabasePool({
691
+ configuration: this.getConfiguration(),
692
+ databaseIdentifier,
693
+ poolConfiguration
694
+ })
695
+ this._testTenantPools.set(databaseIdentifier, pool)
696
+
697
+ return pool
698
+ }
699
+
700
+ /**
701
+ * Tears down every test tenant database pool created during this run, exactly once.
702
+ * Safe to call from afterAll-style teardown and from the runner's own cleanup; the
703
+ * same promise is shared by concurrent callers.
704
+ * @returns {Promise<void>} - Resolves after all pools dropped their databases.
705
+ */
706
+ teardownTestTenantPools() {
707
+ this._testTenantPoolTeardownPromise ??= (async () => {
708
+ const pools = [...this._testTenantPools.values()]
709
+ this._testTenantPools.clear()
710
+
711
+ const errors = []
712
+
713
+ for (const pool of pools) {
714
+ try {
715
+ await pool.teardown()
716
+ } catch (error) {
717
+ errors.push(error)
718
+ }
719
+ }
720
+
721
+ if (errors.length === 1) throw errors[0]
722
+ if (errors.length > 1) throw new AggregateError(errors, "Failed to tear down test tenant database pools")
723
+ })()
724
+
725
+ return this._testTenantPoolTeardownPromise
726
+ }
727
+
635
728
  /**
636
729
  * Registers each non-tenant per-test connection as a dynamic candidate for in-process
637
730
  * request sharing. The pool evaluates transaction state when each request is dispatched,
@@ -822,6 +915,52 @@ export default class TestRunner {
822
915
  if (errors.length > 1) throw new AggregateError(errors, "Failed to clean up a transactional tenant test connection")
823
916
  }
824
917
 
918
+ /**
919
+ * Registers one tenant database for framework-owned end-of-attempt cleanup. The
920
+ * registration drops the tenant's database through the provider's `dropDatabase`
921
+ * hook (or the framework default) after the attempt's afterEach hooks and
922
+ * transactional tenant rollbacks finished, so consuming apps do not hand-roll a
923
+ * Map/afterEach bookkeeping layer for DDL that a test transaction cannot roll back.
924
+ * @param {{databaseIdentifier: string, tenant: object}} args - Tenant database identifier and tenant descriptor to drop at attempt end.
925
+ * @param {TenantCleanupRegistration[]} registrations - Current attempt registrations.
926
+ * @returns {void} - No return value.
927
+ */
928
+ registerTenantForCleanup({databaseIdentifier, tenant, ...restArgs}, registrations) {
929
+ restArgsError(restArgs)
930
+ if (!databaseIdentifier) throw new Error("registerTenantForCleanup requires a databaseIdentifier")
931
+ if (!tenant) throw new Error("registerTenantForCleanup requires a tenant")
932
+
933
+ const databaseConfiguration = this.getConfiguration().resolveDatabaseConfiguration(databaseIdentifier, tenant)
934
+
935
+ if (!databaseConfiguration.tenantOnly) {
936
+ throw new Error(`registerTenantForCleanup requires a tenantOnly database: ${databaseIdentifier}`)
937
+ }
938
+
939
+ registrations.push({databaseIdentifier, tenant})
940
+ }
941
+
942
+ /**
943
+ * Drops every tenant database registered for this attempt, in registration order.
944
+ * Each drop is attempted even when an earlier one failed; failures are surfaced
945
+ * (single error or AggregateError), never swallowed.
946
+ * @param {TenantCleanupRegistration[]} registrations - Attempt-owned registrations.
947
+ * @returns {Promise<void>} - Resolves after all registered tenants were dropped.
948
+ */
949
+ async cleanupTenantRegistrations(registrations) {
950
+ const errors = []
951
+
952
+ for (const {databaseIdentifier, tenant} of registrations) {
953
+ try {
954
+ await Tenant.drop({identifier: databaseIdentifier, tenant, configuration: this.getConfiguration()})
955
+ } catch (error) {
956
+ errors.push(error)
957
+ }
958
+ }
959
+
960
+ if (errors.length === 1) throw errors[0]
961
+ if (errors.length > 1) throw new AggregateError(errors, "Failed to drop tenant databases registered for cleanup")
962
+ }
963
+
825
964
  /**
826
965
  * Selects the current non-tenant connections eligible for shared transaction work.
827
966
  * @param {{transactionsOnly: boolean}} args - Selection options.
@@ -1374,6 +1513,12 @@ export default class TestRunner {
1374
1513
  } finally {
1375
1514
  process.off("unhandledRejection", onUnhandledRejection)
1376
1515
  process.off("uncaughtException", onUncaughtException)
1516
+
1517
+ try {
1518
+ await this.teardownTestTenantPools()
1519
+ } catch (error) {
1520
+ this.recordTimeoutCleanupFailure(error, "test tenant pool teardown")
1521
+ }
1377
1522
  }
1378
1523
  }
1379
1524
 
@@ -1385,6 +1530,13 @@ export default class TestRunner {
1385
1530
  const failureStart = this._suiteHookFailures.length
1386
1531
 
1387
1532
  await this._packageRunner?.cleanupActiveSuites()
1533
+
1534
+ try {
1535
+ await this.teardownTestTenantPools()
1536
+ } catch (error) {
1537
+ this.recordTimeoutCleanupFailure(error, "test tenant pool teardown")
1538
+ }
1539
+
1388
1540
  this.throwAfterAllFailures(this._suiteHookFailures.slice(failureStart))
1389
1541
  }
1390
1542