velocious 1.0.632 → 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 (77) hide show
  1. package/README.md +6 -3
  2. package/build/background-jobs/lifecycle-client.js +3 -3
  3. package/build/background-jobs/local-dispatcher.js +29 -18
  4. package/build/configuration.js +38 -1
  5. package/build/database/drivers/base.js +122 -105
  6. package/build/database/drivers/sqlite/index.web.js +19 -11
  7. package/build/database/migrator.js +8 -5
  8. package/build/database/pool/async-tracked-multi-connection.js +73 -24
  9. package/build/database/pool/base.js +5 -0
  10. package/build/database/pool/single-multi-use.js +43 -11
  11. package/build/database/record/attachments/store.js +35 -26
  12. package/build/environment-handlers/base.js +122 -9
  13. package/build/environment-handlers/node.js +15 -9
  14. package/build/http-server/websocket-events-host.js +10 -8
  15. package/build/src/background-jobs/lifecycle-client.d.ts +1 -0
  16. package/build/src/background-jobs/lifecycle-client.d.ts.map +1 -1
  17. package/build/src/background-jobs/lifecycle-client.js +4 -4
  18. package/build/src/background-jobs/local-dispatcher.d.ts.map +1 -1
  19. package/build/src/background-jobs/local-dispatcher.js +31 -20
  20. package/build/src/configuration.d.ts +19 -0
  21. package/build/src/configuration.d.ts.map +1 -1
  22. package/build/src/configuration.js +36 -2
  23. package/build/src/database/drivers/base.d.ts.map +1 -1
  24. package/build/src/database/drivers/base.js +118 -100
  25. package/build/src/database/drivers/sqlite/index.web.d.ts +2 -0
  26. package/build/src/database/drivers/sqlite/index.web.d.ts.map +1 -1
  27. package/build/src/database/drivers/sqlite/index.web.js +17 -11
  28. package/build/src/database/migrator.d.ts.map +1 -1
  29. package/build/src/database/migrator.js +9 -6
  30. package/build/src/database/pool/async-tracked-multi-connection.d.ts +15 -0
  31. package/build/src/database/pool/async-tracked-multi-connection.d.ts.map +1 -1
  32. package/build/src/database/pool/async-tracked-multi-connection.js +71 -21
  33. package/build/src/database/pool/base.d.ts +2 -0
  34. package/build/src/database/pool/base.d.ts.map +1 -1
  35. package/build/src/database/pool/base.js +5 -1
  36. package/build/src/database/pool/single-multi-use.d.ts.map +1 -1
  37. package/build/src/database/pool/single-multi-use.js +47 -13
  38. package/build/src/database/record/attachments/store.d.ts +6 -0
  39. package/build/src/database/record/attachments/store.d.ts.map +1 -1
  40. package/build/src/database/record/attachments/store.js +31 -23
  41. package/build/src/environment-handlers/base.d.ts +79 -8
  42. package/build/src/environment-handlers/base.d.ts.map +1 -1
  43. package/build/src/environment-handlers/base.js +106 -10
  44. package/build/src/environment-handlers/node.d.ts +3 -6
  45. package/build/src/environment-handlers/node.d.ts.map +1 -1
  46. package/build/src/environment-handlers/node.js +13 -10
  47. package/build/src/http-server/websocket-events-host.d.ts.map +1 -1
  48. package/build/src/http-server/websocket-events-host.js +11 -8
  49. package/build/src/testing/sqljs-test-database.d.ts +41 -0
  50. package/build/src/testing/sqljs-test-database.d.ts.map +1 -0
  51. package/build/src/testing/sqljs-test-database.js +72 -0
  52. package/build/src/testing/test-runner.d.ts +79 -6
  53. package/build/src/testing/test-runner.d.ts.map +1 -1
  54. package/build/src/testing/test-runner.js +498 -139
  55. package/build/src/testing/test.js +2 -2
  56. package/build/testing/sqljs-test-database.js +73 -0
  57. package/build/testing/test-runner.js +529 -141
  58. package/build/testing/test.js +1 -1
  59. package/build/tsconfig.tsbuildinfo +1 -1
  60. package/package.json +1 -1
  61. package/scripts/test-browser.js +16 -29
  62. package/src/background-jobs/lifecycle-client.js +3 -3
  63. package/src/background-jobs/local-dispatcher.js +29 -18
  64. package/src/configuration.js +38 -1
  65. package/src/database/drivers/base.js +122 -105
  66. package/src/database/drivers/sqlite/index.web.js +19 -11
  67. package/src/database/migrator.js +8 -5
  68. package/src/database/pool/async-tracked-multi-connection.js +73 -24
  69. package/src/database/pool/base.js +5 -0
  70. package/src/database/pool/single-multi-use.js +43 -11
  71. package/src/database/record/attachments/store.js +35 -26
  72. package/src/environment-handlers/base.js +122 -9
  73. package/src/environment-handlers/node.js +15 -9
  74. package/src/http-server/websocket-events-host.js +10 -8
  75. package/src/testing/sqljs-test-database.js +73 -0
  76. package/src/testing/test-runner.js +529 -141
  77. package/src/testing/test.js +1 -1
@@ -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
@@ -2,6 +2,7 @@
2
2
 
3
3
  import BasePool from "./base.js"
4
4
  import OperationLease from "../operation-lease.js"
5
+ import {coordinateSharedTransactionConnection} from "../../testing/shared-transaction-connection-coordinator.js"
5
6
 
6
7
  /**
7
8
  * SinglePoolConnectionEntry type.
@@ -118,6 +119,7 @@ export default class VelociousDatabasePoolSingleMultiUser extends BasePool {
118
119
  * @returns {Promise<import("../drivers/base.js").default>} - Checked-out connection.
119
120
  */
120
121
  async checkoutForConfiguration(databaseConfiguration, options = {}, {retain} = {retain: false}) {
122
+ this.assertDatabaseAccessAllowed()
121
123
  const reuseKey = this.getConfigurationReuseKey(databaseConfiguration)
122
124
  let entry = this.connectionEntries.get(reuseKey)
123
125
 
@@ -156,6 +158,22 @@ export default class VelociousDatabasePoolSingleMultiUser extends BasePool {
156
158
  }
157
159
  }
158
160
  if (!entry) throw new Error("Database connection entry was not created")
161
+ if (this.connectionEntries.get(reuseKey) !== entry) {
162
+ return await this.checkoutForConfiguration(databaseConfiguration, options, {retain})
163
+ }
164
+
165
+ try {
166
+ this.assertDatabaseAccessAllowed()
167
+ } catch (error) {
168
+ if (entry.activeCheckoutCount === 0 && this.capacityWaiters.size > 0) {
169
+ try {
170
+ await this.removeAndCloseEntry(entry)
171
+ } catch (closeError) {
172
+ throw new AggregateError([error, closeError], "Database access revocation and connection close both failed", {cause: closeError})
173
+ }
174
+ }
175
+ throw error
176
+ }
159
177
 
160
178
  if (retain) {
161
179
  const previousConnection = this.connection
@@ -178,6 +196,17 @@ export default class VelociousDatabasePoolSingleMultiUser extends BasePool {
178
196
  entry.activeCheckoutCount++
179
197
  this.activeCheckoutCount++
180
198
 
199
+ try {
200
+ this.assertDatabaseAccessAllowed()
201
+ } catch (error) {
202
+ try {
203
+ await this.checkin(entry.connection)
204
+ } catch (checkinError) {
205
+ throw new AggregateError([error, checkinError], "Database access revocation and connection check-in both failed", {cause: checkinError})
206
+ }
207
+ throw error
208
+ }
209
+
181
210
  return entry.connection
182
211
  }
183
212
 
@@ -380,21 +409,23 @@ export default class VelociousDatabasePoolSingleMultiUser extends BasePool {
380
409
  const owner = Symbol("single-pool-operation-owner")
381
410
  const operationLease = new OperationLease(owner)
382
411
  const connection = await this.checkoutForConfiguration(databaseConfiguration, options, checkoutArgs)
383
- let operationLeaseInstalled = false
384
412
 
385
413
  try {
386
- await connection.setOperationLease(operationLease)
387
- operationLeaseInstalled = true
414
+ return await coordinateSharedTransactionConnection(connection, async () => {
415
+ let operationLeaseInstalled = false
388
416
 
389
- return await callback(connection, owner)
390
- } finally {
391
- operationLease.release()
417
+ try {
418
+ await connection.setOperationLease(operationLease)
419
+ operationLeaseInstalled = true
392
420
 
393
- try {
394
- if (operationLeaseInstalled) connection.clearOperationLease(operationLease)
395
- } finally {
396
- await this.checkin(connection)
397
- }
421
+ return await callback(connection, owner)
422
+ } finally {
423
+ operationLease.release()
424
+ if (operationLeaseInstalled) connection.clearOperationLease(operationLease)
425
+ }
426
+ })
427
+ } finally {
428
+ await this.checkin(connection)
398
429
  }
399
430
  }
400
431
 
@@ -492,6 +523,7 @@ export default class VelociousDatabasePoolSingleMultiUser extends BasePool {
492
523
  * @returns {import("../drivers/base.js").default} - Mutable ambient fallback connection.
493
524
  */
494
525
  getCurrentConnection() {
526
+ this.assertDatabaseAccessAllowed()
495
527
  if (!this.connection) throw new Error("A connection hasn't been made yet")
496
528
 
497
529
  return this.connection
@@ -103,32 +103,7 @@ export default class RecordAttachmentsStore {
103
103
 
104
104
  this._readyPromise = (async () => {
105
105
  await this._withDb(async (db) => {
106
- db.clearSchemaCache()
107
-
108
- if (await db.tableExists(ATTACHMENTS_TABLE)) {
109
- await this.ensureAttachmentStoreSchema({db})
110
- return
111
- }
112
-
113
- const table = new TableData(ATTACHMENTS_TABLE, {ifNotExists: true})
114
-
115
- table.string("id", {null: false, primaryKey: true})
116
- table.string("record_type", {null: false, index: true})
117
- table.string("record_id", {null: false, index: true})
118
- table.string("name", {null: false, index: true})
119
- table.integer("position", {null: false})
120
- table.string("filename", {null: false})
121
- table.string("content_type", {null: true})
122
- table.bigint("byte_size", {null: false})
123
- table.string("driver", {null: true})
124
- table.string("storage_key", {null: true})
125
- table.text("content_base64", {null: true})
126
- table.bigint("created_at_ms", {null: false})
127
- table.bigint("updated_at_ms", {null: false})
128
-
129
- await db.createTable(table)
130
- this._driverColumnsAvailable = true
131
- this._contentBase64Nullable = true
106
+ await this.ensureSchema(db)
132
107
  }, model)
133
108
  })()
134
109
 
@@ -139,6 +114,40 @@ export default class RecordAttachmentsStore {
139
114
  }
140
115
  }
141
116
 
117
+ /**
118
+ * Ensures attachment schema through an already-owned connection.
119
+ * @param {import("../../drivers/base.js").default} db - Database connection.
120
+ * @returns {Promise<void>} - Resolves when schema is ready.
121
+ */
122
+ async ensureSchema(db) {
123
+ db.clearSchemaCache()
124
+
125
+ if (await db.tableExists(ATTACHMENTS_TABLE)) {
126
+ await this.ensureAttachmentStoreSchema({db})
127
+ return
128
+ }
129
+
130
+ const table = new TableData(ATTACHMENTS_TABLE, {ifNotExists: true})
131
+
132
+ table.string("id", {null: false, primaryKey: true})
133
+ table.string("record_type", {null: false, index: true})
134
+ table.string("record_id", {null: false, index: true})
135
+ table.string("name", {null: false, index: true})
136
+ table.integer("position", {null: false})
137
+ table.string("filename", {null: false})
138
+ table.string("content_type", {null: true})
139
+ table.bigint("byte_size", {null: false})
140
+ table.string("driver", {null: true})
141
+ table.string("storage_key", {null: true})
142
+ table.text("content_base64", {null: true})
143
+ table.bigint("created_at_ms", {null: false})
144
+ table.bigint("updated_at_ms", {null: false})
145
+
146
+ await db.createTable(table)
147
+ this._driverColumnsAvailable = true
148
+ this._contentBase64Nullable = true
149
+ }
150
+
142
151
  /**
143
152
  * Runs attach.
144
153
  * @param {object} args - Options.
@@ -18,6 +18,14 @@ import { validateTimeZone } from "../time-zone.js"
18
18
  * @property {string} migrationClassName - Exported migration class name.
19
19
  * @property {string} file - Migration filename.
20
20
  */
21
+ /**
22
+ * TestDatabaseAccessScopeStorage type.
23
+ * @typedef {object} TestDatabaseAccessScopeStorage
24
+ * @property {() => ({revoked: boolean} | undefined)} getStore - Gets the inherited scope.
25
+ * @property {(scope: {revoked: boolean} | undefined, callback: () => ReturnType<typeof JSON.parse>) => ReturnType<typeof JSON.parse>} run - Runs in the scope.
26
+ */
27
+
28
+ export class TestDatabaseAccessRevokedError extends Error {}
21
29
 
22
30
  export default class VelociousEnvironmentHandlerBase {
23
31
  /**
@@ -38,30 +46,49 @@ export default class VelociousEnvironmentHandlerBase {
38
46
 
39
47
  /**
40
48
  * Gets the active shared-transaction coordinator owner for a connection.
41
- * Browser runtimes never install the Node test broker.
42
- * @param {object} _connection - Parent physical connection.
49
+ * @param {object} connection - Parent physical connection.
43
50
  * @returns {symbol | undefined} - Active coordinator owner.
44
51
  */
45
- getSharedTransactionCoordinatorOwner(_connection) { return undefined }
52
+ getSharedTransactionCoordinatorOwner(connection) {
53
+ return this._sharedTransactionCoordinatorOwnerStorage?.getStore()?.get(connection)
54
+ }
46
55
 
47
56
  /**
48
57
  * Runs work as the current shared-transaction coordinator owner.
49
58
  * @template T
50
- * @param {object} _connection - Parent physical connection.
51
- * @param {symbol} _owner - Coordinator owner.
59
+ * @param {object} connection - Parent physical connection.
60
+ * @param {symbol} owner - Coordinator owner.
52
61
  * @param {() => T} callback - Owned work.
53
62
  * @returns {T} - Callback result.
54
63
  */
55
- runWithSharedTransactionCoordinatorOwner(_connection, _owner, callback) { return callback() }
64
+ runWithSharedTransactionCoordinatorOwner(connection, owner, callback) {
65
+ const storage = this._sharedTransactionCoordinatorOwnerStorage
66
+
67
+ if (!storage) return callback()
68
+
69
+ const owners = new Map(storage.getStore())
70
+
71
+ owners.set(connection, owner)
72
+ return storage.run(owners, callback)
73
+ }
56
74
 
57
75
  /**
58
76
  * Runs work without inherited shared-transaction ownership for one connection.
59
77
  * @template T
60
- * @param {import("../database/drivers/base.js").default} _connection - Physical connection whose owner is cleared.
78
+ * @param {import("../database/drivers/base.js").default} connection - Physical connection whose owner is cleared.
61
79
  * @param {() => T} callback - Physical connection work.
62
80
  * @returns {T} - Callback result.
63
81
  */
64
- runWithoutSharedTransactionCoordinatorOwner(_connection, callback) { return callback() }
82
+ runWithoutSharedTransactionCoordinatorOwner(connection, callback) {
83
+ const storage = this._sharedTransactionCoordinatorOwnerStorage
84
+
85
+ if (!storage) return callback()
86
+
87
+ const owners = new Map(storage.getStore())
88
+
89
+ owners.delete(connection)
90
+ return storage.run(owners, callback)
91
+ }
65
92
 
66
93
  /**
67
94
  * Runs work without inherited shared-transaction coordinator ownership.
@@ -69,7 +96,24 @@ export default class VelociousEnvironmentHandlerBase {
69
96
  * @param {() => T} callback - Detached work.
70
97
  * @returns {T} - Callback result.
71
98
  */
72
- runWithoutSharedTransactionCoordinatorOwners(callback) { return callback() }
99
+ runWithoutSharedTransactionCoordinatorOwners(callback) {
100
+ const storage = this._sharedTransactionCoordinatorOwnerStorage
101
+
102
+ if (!storage) return callback()
103
+
104
+ return storage.run(new Map(), callback)
105
+ }
106
+
107
+ /**
108
+ * Installs async-context storage when this handler is driven by the Node test runner.
109
+ * @param {import("node:async_hooks").AsyncLocalStorage<Map<object, symbol>>} storage - Coordinator owner storage.
110
+ */
111
+ installSharedTransactionCoordinatorOwnerStorage(storage) {
112
+ this._sharedTransactionCoordinatorOwnerStorage ??= storage
113
+ }
114
+
115
+ /** @type {import("node:async_hooks").AsyncLocalStorage<Map<object, symbol>> | undefined} */
116
+ _sharedTransactionCoordinatorOwnerStorage = undefined
73
117
 
74
118
  /**
75
119
  * Runs work with test-profile attribution. Runtimes without async-context
@@ -87,6 +131,75 @@ export default class VelociousEnvironmentHandlerBase {
87
131
  */
88
132
  getCurrentTestProfileContext() { return undefined }
89
133
 
134
+ /**
135
+ * Runs work in a revocable test database-access scope.
136
+ * @template T
137
+ * @param {{revoked: boolean}} scope - Attempt-owned access scope.
138
+ * @param {() => T | Promise<T>} callback - Attempt work.
139
+ * @returns {Promise<T>} - Callback result.
140
+ */
141
+ async runWithTestDatabaseAccessScope(scope, callback) {
142
+ return await this.runWithCapturedTestDatabaseAccessScope(scope, callback)
143
+ }
144
+
145
+ /**
146
+ * Runs work with an explicitly captured test database-access scope.
147
+ * @template T
148
+ * @param {{revoked: boolean} | undefined} scope - Captured access scope.
149
+ * @param {() => T | Promise<T>} callback - Work to run.
150
+ * @returns {Promise<T>} - Callback result.
151
+ */
152
+ async runWithCapturedTestDatabaseAccessScope(scope, callback) {
153
+ if (this._testDatabaseAccessScopeStorage) {
154
+ return await this._testDatabaseAccessScopeStorage.run(scope, callback)
155
+ }
156
+
157
+ const entry = {owner: Symbol("test-database-access-scope"), scope}
158
+
159
+ this._testDatabaseAccessScopes.push(entry)
160
+
161
+ try {
162
+ return await callback()
163
+ } finally {
164
+ const index = this._testDatabaseAccessScopes.findIndex((candidate) => candidate.owner === entry.owner)
165
+
166
+ if (index !== -1) this._testDatabaseAccessScopes.splice(index, 1)
167
+ }
168
+ }
169
+
170
+ /**
171
+ * Gets the current test database-access scope.
172
+ * @returns {{revoked: boolean} | undefined} - Current scope.
173
+ */
174
+ currentTestDatabaseAccessScope() {
175
+ if (this._testDatabaseAccessScopeStorage) return this._testDatabaseAccessScopeStorage.getStore()
176
+
177
+ return this._testDatabaseAccessScopes[this._testDatabaseAccessScopes.length - 1]?.scope
178
+ }
179
+
180
+ /** Throws when the current test attempt no longer owns database access. */
181
+ assertTestDatabaseAccessAllowed() {
182
+ const scope = this.currentTestDatabaseAccessScope()
183
+
184
+ if (scope?.revoked) {
185
+ throw new TestDatabaseAccessRevokedError("Database access is no longer allowed for this test attempt")
186
+ }
187
+ }
188
+
189
+ /**
190
+ * Installs async-context storage owned by the first Node test runner.
191
+ * @param {TestDatabaseAccessScopeStorage} storage - Scope storage.
192
+ */
193
+ installTestDatabaseAccessScopeStorage(storage) {
194
+ this._testDatabaseAccessScopeStorage ??= storage
195
+ }
196
+
197
+ /** @type {Array<{owner: symbol, scope: {revoked: boolean} | undefined}>} */
198
+ _testDatabaseAccessScopes = []
199
+
200
+ /** @type {TestDatabaseAccessScopeStorage | undefined} */
201
+ _testDatabaseAccessScopeStorage = undefined
202
+
90
203
  /**
91
204
  * Mutable ambient tenant used by runtimes without async-context storage.
92
205
  * @type {ReturnType<typeof JSON.parse> | undefined}
@@ -39,6 +39,7 @@ import { timingSafeEqual } from "node:crypto"
39
39
  import requireContext from "require-context"
40
40
  import AsyncTrackedMultiConnectionPool from "../database/pool/async-tracked-multi-connection.js"
41
41
  import InitializerFromRequireContext from "../database/initializer-from-require-context.js"
42
+ import RecordAttachmentsStore from "../database/record/attachments/store.js"
42
43
  import toImportSpecifier from "../utils/to-import-specifier.js"
43
44
  import { validateTimeZone } from "../time-zone.js"
44
45
  import AttachmentPathSource from "./node/attachment-path-source.js"
@@ -1213,21 +1214,26 @@ export default class VelociousEnvironmentHandlerNode extends Base{
1213
1214
  }
1214
1215
 
1215
1216
  /**
1216
- * Ensures velocious' background-jobs schema exists on its configured database
1217
- * as part of `db:migrate`, so the framework's own tables (background_jobs +
1218
- * background_job_concurrency, execution_mode etc.) are created deterministically
1219
- * alongside app migrations and captured in the dumped structure SQL — rather than
1220
- * only appearing once a runtime store boots. Reuses the store's idempotent
1221
- * `_ensureSchema`, so it's safe to re-run against DBs that already have it.
1217
+ * Ensures Velocious framework schema exists as part of `db:migrate`, so internal
1218
+ * tables are created deterministically alongside app migrations and captured in
1219
+ * the dumped structure SQL instead of appearing only when runtime stores boot.
1222
1220
  * @param {object} args - Options object.
1223
1221
  * @param {Record<string, import("../database/drivers/base.js").default>} args.dbs - Dbs being migrated.
1224
1222
  * @returns {Promise<void>} - Resolves when complete.
1225
1223
  */
1226
1224
  async ensureFrameworkSchema({dbs}) {
1227
1225
  // Migration passes its already checked-out DB into the adapter hook. Do not
1228
- // run runtime readiness here: SQL readiness would open a nested checkout and
1229
- // can deadlock a single-connection migration pool.
1230
- const adapter = this.getConfiguration().getBackgroundJobsAdapter()
1226
+ // run runtime readiness here: it would open a nested checkout and can deadlock
1227
+ // a single-connection migration pool.
1228
+ const configuration = this.getConfiguration()
1229
+
1230
+ for (const [databaseIdentifier, db] of Object.entries(dbs)) {
1231
+ const attachmentStore = new RecordAttachmentsStore({configuration, databaseIdentifier})
1232
+
1233
+ await attachmentStore.ensureSchema(db)
1234
+ }
1235
+
1236
+ const adapter = configuration.getBackgroundJobsAdapter()
1231
1237
 
1232
1238
  await adapter.ensureFrameworkSchema({dbs})
1233
1239
  }
@@ -155,15 +155,17 @@ export class VelociousHttpServerWebsocketEventsHost {
155
155
  const handler = this.handlers.values().next().value
156
156
  const configuration = originatingConfiguration || handler?.configuration
157
157
  const previousTail = this.publishQueuesByChannel.get(channel) || Promise.resolve()
158
+ let queuedPublish
159
+
160
+ if (configuration) {
161
+ queuedPublish = previousTail.then(() => configuration.withoutCurrentTestDatabaseAccessScope(() => {
162
+ return configuration.withoutCurrentConnectionContexts(callback)
163
+ }))
164
+ } else {
165
+ queuedPublish = previousTail.then(callback)
166
+ }
158
167
 
159
- const tail = previousTail
160
- .then(async () => {
161
- if (configuration) {
162
- return await configuration.withoutCurrentConnectionContexts(callback)
163
- }
164
-
165
- return await callback()
166
- })
168
+ const tail = queuedPublish
167
169
  .catch((error) => {
168
170
  console.error(errorMessage, error)
169
171
  throw error
@@ -1,3 +1,4 @@
1
+ export declare const MAX_LIFECYCLE_REQUEST_TIMEOUT_MS = 60000;
1
2
  /** One-request acknowledged lifecycle client. */
2
3
  export default class BackgroundJobsLifecycleClient {
3
4
  generationId: string;
@@ -1 +1 @@
1
- {"version":3,"file":"lifecycle-client.d.ts","sourceRoot":"","sources":["../../../src/background-jobs/lifecycle-client.js"],"names":[],"mappings":"AAUA,iDAAiD;AACjD,MAAM,CAAC,OAAO,OAAO,6BAA6B;IAezC,YAAY;IACZ,UAAU;IAIV,gBAAgB;IAnBvB;;;;;;;OAOG;IACH,YAAY,EAAC,aAAa,EAAE,YAAY,EAAE,UAAU,EAAE,gBAA6C,EAAC,EALjG;QAAoD,aAAa,EAAzD,OAAO,qBAAqB,EAAE,OAAO,CAC7C;QAAsB,YAAY,AAAlC,CACA,EADQ,MAAM,CACd;QAAsB,UAAU,AAAhC,CACA,EADQ,MAAM,CACd;QAAsB,gBAAgB,AAAtC,CACF,EADU,MAAM,CAChB;KACmG,EAcnG;IAED;;;OAGG;IACG,QAAQ,IAFD,OAAO,CAAC,OAAO,YAAY,EAAE,sCAAsC,CAAC,CAEtB;IAE3D;;;OAGG;IACG,MAAM,IAFC,OAAO,CAAC,OAAO,YAAY,EAAE,sCAAsC,CAAC,CAE1B;IAEvD;;;;OAIG;IACG,QAAQ,CAAC,MAAM,EAHV,UAAU,GAAG,QAGH,GAFR,OAAO,CAAC,OAAO,YAAY,EAAE,sCAAsC,CAAC,CAOhF;IAED;;;;;;OAMG;IACG,WAAW,CAAC,EAAC,MAAM,EAAE,MAAM,EAAC,EAJ/B;QAAoC,MAAM,EAAlC,UAAU,GAAG,QAAQ,CAC7B;QAA0B,MAAM,EAAxB,WAAW,CACnB;KAE+B,GAFrB,OAAO,CAAC,OAAO,YAAY,EAAE,sCAAsC,CAAC,CA4DhF;CACF"}
1
+ {"version":3,"file":"lifecycle-client.d.ts","sourceRoot":"","sources":["../../../src/background-jobs/lifecycle-client.js"],"names":[],"mappings":"AAQA,eAAO,MAAM,gCAAgC,QAAQ,CAAA;AAErD,iDAAiD;AACjD,MAAM,CAAC,OAAO,OAAO,6BAA6B;IAezC,YAAY;IACZ,UAAU;IAIV,gBAAgB;IAnBvB;;;;;;;OAOG;IACH,YAAY,EAAC,aAAa,EAAE,YAAY,EAAE,UAAU,EAAE,gBAA6C,EAAC,EALjG;QAAoD,aAAa,EAAzD,OAAO,qBAAqB,EAAE,OAAO,CAC7C;QAAsB,YAAY,AAAlC,CACA,EADQ,MAAM,CACd;QAAsB,UAAU,AAAhC,CACA,EADQ,MAAM,CACd;QAAsB,gBAAgB,AAAtC,CACF,EADU,MAAM,CAChB;KACmG,EAcnG;IAED;;;OAGG;IACG,QAAQ,IAFD,OAAO,CAAC,OAAO,YAAY,EAAE,sCAAsC,CAAC,CAEtB;IAE3D;;;OAGG;IACG,MAAM,IAFC,OAAO,CAAC,OAAO,YAAY,EAAE,sCAAsC,CAAC,CAE1B;IAEvD;;;;OAIG;IACG,QAAQ,CAAC,MAAM,EAHV,UAAU,GAAG,QAGH,GAFR,OAAO,CAAC,OAAO,YAAY,EAAE,sCAAsC,CAAC,CAOhF;IAED;;;;;;OAMG;IACG,WAAW,CAAC,EAAC,MAAM,EAAE,MAAM,EAAC,EAJ/B;QAAoC,MAAM,EAAlC,UAAU,GAAG,QAAQ,CAC7B;QAA0B,MAAM,EAAxB,WAAW,CACnB;KAE+B,GAFrB,OAAO,CAAC,OAAO,YAAY,EAAE,sCAAsC,CAAC,CA4DhF;CACF"}