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.
- package/README.md +6 -3
- package/build/background-jobs/lifecycle-client.js +3 -3
- package/build/background-jobs/local-dispatcher.js +29 -18
- package/build/configuration.js +38 -1
- package/build/database/drivers/base.js +122 -105
- package/build/database/drivers/sqlite/index.web.js +19 -11
- package/build/database/migrator.js +8 -5
- package/build/database/pool/async-tracked-multi-connection.js +73 -24
- package/build/database/pool/base.js +5 -0
- package/build/database/pool/single-multi-use.js +43 -11
- package/build/database/record/attachments/store.js +35 -26
- package/build/environment-handlers/base.js +122 -9
- package/build/environment-handlers/node.js +15 -9
- package/build/http-server/websocket-events-host.js +10 -8
- package/build/src/background-jobs/lifecycle-client.d.ts +1 -0
- package/build/src/background-jobs/lifecycle-client.d.ts.map +1 -1
- package/build/src/background-jobs/lifecycle-client.js +4 -4
- package/build/src/background-jobs/local-dispatcher.d.ts.map +1 -1
- package/build/src/background-jobs/local-dispatcher.js +31 -20
- package/build/src/configuration.d.ts +19 -0
- package/build/src/configuration.d.ts.map +1 -1
- package/build/src/configuration.js +36 -2
- package/build/src/database/drivers/base.d.ts.map +1 -1
- package/build/src/database/drivers/base.js +118 -100
- package/build/src/database/drivers/sqlite/index.web.d.ts +2 -0
- package/build/src/database/drivers/sqlite/index.web.d.ts.map +1 -1
- package/build/src/database/drivers/sqlite/index.web.js +17 -11
- package/build/src/database/migrator.d.ts.map +1 -1
- package/build/src/database/migrator.js +9 -6
- package/build/src/database/pool/async-tracked-multi-connection.d.ts +15 -0
- package/build/src/database/pool/async-tracked-multi-connection.d.ts.map +1 -1
- package/build/src/database/pool/async-tracked-multi-connection.js +71 -21
- package/build/src/database/pool/base.d.ts +2 -0
- package/build/src/database/pool/base.d.ts.map +1 -1
- package/build/src/database/pool/base.js +5 -1
- package/build/src/database/pool/single-multi-use.d.ts.map +1 -1
- package/build/src/database/pool/single-multi-use.js +47 -13
- package/build/src/database/record/attachments/store.d.ts +6 -0
- package/build/src/database/record/attachments/store.d.ts.map +1 -1
- package/build/src/database/record/attachments/store.js +31 -23
- package/build/src/environment-handlers/base.d.ts +79 -8
- package/build/src/environment-handlers/base.d.ts.map +1 -1
- package/build/src/environment-handlers/base.js +106 -10
- package/build/src/environment-handlers/node.d.ts +3 -6
- package/build/src/environment-handlers/node.d.ts.map +1 -1
- package/build/src/environment-handlers/node.js +13 -10
- package/build/src/http-server/websocket-events-host.d.ts.map +1 -1
- package/build/src/http-server/websocket-events-host.js +11 -8
- package/build/src/testing/sqljs-test-database.d.ts +41 -0
- package/build/src/testing/sqljs-test-database.d.ts.map +1 -0
- package/build/src/testing/sqljs-test-database.js +72 -0
- package/build/src/testing/test-runner.d.ts +79 -6
- package/build/src/testing/test-runner.d.ts.map +1 -1
- package/build/src/testing/test-runner.js +498 -139
- package/build/src/testing/test.js +2 -2
- package/build/testing/sqljs-test-database.js +73 -0
- package/build/testing/test-runner.js +529 -141
- package/build/testing/test.js +1 -1
- package/build/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/scripts/test-browser.js +16 -29
- package/src/background-jobs/lifecycle-client.js +3 -3
- package/src/background-jobs/local-dispatcher.js +29 -18
- package/src/configuration.js +38 -1
- package/src/database/drivers/base.js +122 -105
- package/src/database/drivers/sqlite/index.web.js +19 -11
- package/src/database/migrator.js +8 -5
- package/src/database/pool/async-tracked-multi-connection.js +73 -24
- package/src/database/pool/base.js +5 -0
- package/src/database/pool/single-multi-use.js +43 -11
- package/src/database/record/attachments/store.js +35 -26
- package/src/environment-handlers/base.js +122 -9
- package/src/environment-handlers/node.js +15 -9
- package/src/http-server/websocket-events-host.js +10 -8
- package/src/testing/sqljs-test-database.js +73 -0
- package/src/testing/test-runner.js +529 -141
- 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
|
-
|
|
464
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
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
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
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
|
|
387
|
-
|
|
414
|
+
return await coordinateSharedTransactionConnection(connection, async () => {
|
|
415
|
+
let operationLeaseInstalled = false
|
|
388
416
|
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
417
|
+
try {
|
|
418
|
+
await connection.setOperationLease(operationLease)
|
|
419
|
+
operationLeaseInstalled = true
|
|
392
420
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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(
|
|
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}
|
|
51
|
-
* @param {symbol}
|
|
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(
|
|
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}
|
|
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(
|
|
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) {
|
|
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
|
|
1217
|
-
*
|
|
1218
|
-
*
|
|
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:
|
|
1229
|
-
//
|
|
1230
|
-
const
|
|
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 =
|
|
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 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lifecycle-client.d.ts","sourceRoot":"","sources":["../../../src/background-jobs/lifecycle-client.js"],"names":[],"mappings":"
|
|
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"}
|