velocious 1.0.593 → 1.0.595
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 +3 -2
- package/build/configuration.js +109 -14
- package/build/database/drivers/base.js +30 -0
- package/build/database/migrator.js +90 -24
- package/build/database/operation.js +55 -4
- package/build/database/query/join-object.js +7 -4
- package/build/database/query/model-class-query.js +38 -7
- package/build/database/query/preloader/belongs-to.js +9 -6
- package/build/database/query/preloader/has-many.js +25 -14
- package/build/database/query/preloader/has-one.js +7 -4
- package/build/database/query/preloader/query-for-model.js +16 -1
- package/build/database/query/preloader.js +6 -4
- package/build/database/query/query-data.js +3 -3
- package/build/database/query/where-model-class-hash.js +8 -4
- package/build/database/query/with-count.js +4 -3
- package/build/database/record/index.js +148 -5
- package/build/database/record/instance-relationships/base.js +13 -0
- package/build/database/record/instance-relationships/belongs-to.js +1 -1
- package/build/database/record/instance-relationships/has-many.js +5 -2
- package/build/database/record/instance-relationships/has-one.js +1 -1
- package/build/database/record/relationships/base.js +17 -0
- package/build/database/use-database.js +81 -28
- package/build/src/configuration.d.ts +31 -4
- package/build/src/configuration.d.ts.map +1 -1
- package/build/src/configuration.js +88 -17
- package/build/src/database/drivers/base.d.ts +11 -0
- package/build/src/database/drivers/base.d.ts.map +1 -1
- package/build/src/database/drivers/base.js +28 -1
- package/build/src/database/migrator.d.ts +34 -0
- package/build/src/database/migrator.d.ts.map +1 -1
- package/build/src/database/migrator.js +83 -23
- package/build/src/database/operation.d.ts +15 -1
- package/build/src/database/operation.d.ts.map +1 -1
- package/build/src/database/operation.js +52 -5
- package/build/src/database/query/join-object.d.ts.map +1 -1
- package/build/src/database/query/join-object.js +7 -5
- package/build/src/database/query/model-class-query.d.ts +6 -0
- package/build/src/database/query/model-class-query.d.ts.map +1 -1
- package/build/src/database/query/model-class-query.js +33 -8
- package/build/src/database/query/preloader/belongs-to.d.ts.map +1 -1
- package/build/src/database/query/preloader/belongs-to.js +9 -7
- package/build/src/database/query/preloader/has-many.d.ts.map +1 -1
- package/build/src/database/query/preloader/has-many.js +23 -16
- package/build/src/database/query/preloader/has-one.d.ts.map +1 -1
- package/build/src/database/query/preloader/has-one.js +7 -5
- package/build/src/database/query/preloader/query-for-model.d.ts +8 -0
- package/build/src/database/query/preloader/query-for-model.d.ts.map +1 -1
- package/build/src/database/query/preloader/query-for-model.js +15 -2
- package/build/src/database/query/preloader.d.ts.map +1 -1
- package/build/src/database/query/preloader.js +6 -5
- package/build/src/database/query/query-data.js +4 -4
- package/build/src/database/query/where-model-class-hash.d.ts.map +1 -1
- package/build/src/database/query/where-model-class-hash.js +7 -5
- package/build/src/database/query/with-count.js +5 -4
- package/build/src/database/record/index.d.ts +95 -0
- package/build/src/database/record/index.d.ts.map +1 -1
- package/build/src/database/record/index.js +126 -6
- package/build/src/database/record/instance-relationships/base.d.ts +6 -0
- package/build/src/database/record/instance-relationships/base.d.ts.map +1 -1
- package/build/src/database/record/instance-relationships/base.js +12 -1
- package/build/src/database/record/instance-relationships/belongs-to.js +2 -2
- package/build/src/database/record/instance-relationships/has-many.d.ts.map +1 -1
- package/build/src/database/record/instance-relationships/has-many.js +6 -3
- package/build/src/database/record/instance-relationships/has-one.js +2 -2
- package/build/src/database/record/relationships/base.d.ts +11 -0
- package/build/src/database/record/relationships/base.d.ts.map +1 -1
- package/build/src/database/record/relationships/base.js +15 -1
- package/build/src/database/use-database.d.ts +33 -5
- package/build/src/database/use-database.d.ts.map +1 -1
- package/build/src/database/use-database.js +81 -30
- package/build/src/sync/server-change-feed.d.ts +14 -2
- package/build/src/sync/server-change-feed.d.ts.map +1 -1
- package/build/src/sync/server-change-feed.js +50 -10
- package/build/src/sync/sync-scope-store.d.ts +13 -2
- package/build/src/sync/sync-scope-store.d.ts.map +1 -1
- package/build/src/sync/sync-scope-store.js +48 -7
- package/build/src/tenants/frontend-tenant-sqlite-lifecycle.d.ts +43 -1
- package/build/src/tenants/frontend-tenant-sqlite-lifecycle.d.ts.map +1 -1
- package/build/src/tenants/frontend-tenant-sqlite-lifecycle.js +141 -17
- package/build/src/tenants/tenant-handle.d.ts +36 -0
- package/build/src/tenants/tenant-handle.d.ts.map +1 -1
- package/build/src/tenants/tenant-handle.js +73 -3
- package/build/src/testing/browser-test-app.js +3 -1
- package/build/src/testing/browser-use-database-hook-scenarios.d.ts +16 -0
- package/build/src/testing/browser-use-database-hook-scenarios.d.ts.map +1 -0
- package/build/src/testing/browser-use-database-hook-scenarios.js +182 -0
- package/build/src/testing/test-runner.d.ts.map +1 -1
- package/build/src/testing/test-runner.js +9 -8
- package/build/src/utils/model-scope.d.ts +2 -2
- package/build/src/utils/model-scope.d.ts.map +1 -1
- package/build/src/utils/model-scope.js +5 -3
- package/build/src/utils/ransack.d.ts.map +1 -1
- package/build/src/utils/ransack.js +5 -3
- package/build/sync/server-change-feed.js +58 -8
- package/build/sync/sync-scope-store.js +55 -5
- package/build/tenants/frontend-tenant-sqlite-lifecycle.js +150 -15
- package/build/tenants/tenant-handle.js +90 -6
- package/build/testing/browser-test-app.js +2 -0
- package/build/testing/browser-use-database-hook-scenarios.js +204 -0
- package/build/testing/test-runner.js +8 -7
- package/build/tsconfig.tsbuildinfo +1 -1
- package/build/utils/model-scope.js +5 -2
- package/build/utils/ransack.js +5 -2
- package/package.json +2 -1
- package/scripts/browser-test-session.js +60 -0
- package/scripts/test-browser.js +7 -7
- package/src/configuration.js +109 -14
- package/src/database/drivers/base.js +30 -0
- package/src/database/migrator.js +90 -24
- package/src/database/operation.js +55 -4
- package/src/database/query/join-object.js +7 -4
- package/src/database/query/model-class-query.js +38 -7
- package/src/database/query/preloader/belongs-to.js +9 -6
- package/src/database/query/preloader/has-many.js +25 -14
- package/src/database/query/preloader/has-one.js +7 -4
- package/src/database/query/preloader/query-for-model.js +16 -1
- package/src/database/query/preloader.js +6 -4
- package/src/database/query/query-data.js +3 -3
- package/src/database/query/where-model-class-hash.js +8 -4
- package/src/database/query/with-count.js +4 -3
- package/src/database/record/index.js +148 -5
- package/src/database/record/instance-relationships/base.js +13 -0
- package/src/database/record/instance-relationships/belongs-to.js +1 -1
- package/src/database/record/instance-relationships/has-many.js +5 -2
- package/src/database/record/instance-relationships/has-one.js +1 -1
- package/src/database/record/relationships/base.js +17 -0
- package/src/database/use-database.js +81 -28
- package/src/sync/server-change-feed.js +58 -8
- package/src/sync/sync-scope-store.js +55 -5
- package/src/tenants/frontend-tenant-sqlite-lifecycle.js +150 -15
- package/src/tenants/tenant-handle.js +90 -6
- package/src/testing/browser-test-app.js +2 -0
- package/src/testing/browser-use-database-hook-scenarios.js +204 -0
- package/src/testing/test-runner.js +8 -7
- package/src/utils/model-scope.js +5 -2
- package/src/utils/ransack.js +5 -2
|
@@ -8,41 +8,94 @@ import Migrator from "./migrator.js"
|
|
|
8
8
|
import restArgsError from "../utils/rest-args-error.js"
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* @param {
|
|
14
|
-
* @
|
|
11
|
+
* Initializes either the legacy ambient frontend database or one immutable
|
|
12
|
+
* tenant-bound physical database generation.
|
|
13
|
+
* @param {object} args - Initialization options.
|
|
14
|
+
* @param {import("../configuration.js").default} args.configuration - Current frontend configuration.
|
|
15
|
+
* @param {string} [args.databaseIdentifier] - Tenant-only logical database identifier.
|
|
16
|
+
* @param {() => Promise<import("./migrator/types.js").RequireMigrationContextType>} args.migrationsRequireContextCallback - Migrations loader.
|
|
17
|
+
* @param {string} [args.schemaGeneration] - Tenant schema generation.
|
|
18
|
+
* @param {import("../tenants/tenant-handle.js").default} [args.tenantHandle] - Immutable tenant handle.
|
|
19
|
+
* @returns {Promise<void>} - Resolves when the selected database is ready.
|
|
15
20
|
*/
|
|
16
|
-
export
|
|
17
|
-
|
|
21
|
+
export async function initializeFrontendDatabase({configuration, databaseIdentifier, migrationsRequireContextCallback, schemaGeneration, tenantHandle}) {
|
|
22
|
+
if (tenantHandle) {
|
|
23
|
+
tenantHandle.assertConfiguration(configuration)
|
|
24
|
+
if (!databaseIdentifier) throw new Error("Tenant frontend database initialization requires databaseIdentifier")
|
|
25
|
+
if (!schemaGeneration) throw new Error("Tenant frontend database initialization requires schemaGeneration")
|
|
26
|
+
|
|
27
|
+
await tenantHandle.initialize({
|
|
28
|
+
databaseIdentifier,
|
|
29
|
+
migrations: await migrationsRequireContextCallback(),
|
|
30
|
+
schemaGeneration
|
|
31
|
+
})
|
|
32
|
+
await configuration.initialize()
|
|
33
|
+
return
|
|
34
|
+
}
|
|
35
|
+
if (databaseIdentifier || schemaGeneration) {
|
|
36
|
+
throw new Error("Tenant frontend database initialization requires tenantHandle")
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
await configuration.ensureConnections({name: "React database migration loader"}, async () => {
|
|
40
|
+
const migrator = new Migrator({configuration})
|
|
41
|
+
|
|
42
|
+
await migrator.prepare()
|
|
43
|
+
await migrator.migrateFilesFromRequireContext(await migrationsRequireContextCallback())
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
await configuration.initialize()
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* React lifecycle hook for frontend database readiness. With `tenantHandle`,
|
|
51
|
+
* readiness follows that immutable physical tenant plus `schemaGeneration`;
|
|
52
|
+
* changing either cancels the stale render result without cancelling shared
|
|
53
|
+
* lifecycle work needed by another caller.
|
|
54
|
+
* @param {object} args - Hook options.
|
|
55
|
+
* @param {string} [args.databaseIdentifier] - Tenant-only logical database identifier.
|
|
56
|
+
* @param {() => Promise<import("./migrator/types.js").RequireMigrationContextType>} args.migrationsRequireContextCallback - Migrations loader.
|
|
57
|
+
* @param {string} [args.schemaGeneration] - Tenant schema generation.
|
|
58
|
+
* @param {import("../tenants/tenant-handle.js").default} [args.tenantHandle] - Immutable tenant handle.
|
|
59
|
+
* @returns {{error: Error | null, loaded: boolean}} - Selected database readiness.
|
|
60
|
+
*/
|
|
61
|
+
export default function useDatabase({databaseIdentifier, migrationsRequireContextCallback, schemaGeneration, tenantHandle, ...restArgs}) {
|
|
62
|
+
restArgsError(restArgs)
|
|
63
|
+
|
|
18
64
|
const {isServer} = useEnvSense()
|
|
19
|
-
const
|
|
65
|
+
const migrationsRequireContextCallbackRef = React.useRef(migrationsRequireContextCallback)
|
|
20
66
|
|
|
21
|
-
|
|
22
|
-
instance.running = true
|
|
67
|
+
migrationsRequireContextCallbackRef.current = migrationsRequireContextCallback
|
|
23
68
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
const migrator = new Migrator({configuration: Configuration.current()})
|
|
69
|
+
const selection = React.useMemo(() => {
|
|
70
|
+
if (isServer) return undefined
|
|
27
71
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
72
|
+
return {configuration: Configuration.current(), databaseIdentifier, schemaGeneration, tenantHandle}
|
|
73
|
+
}, [isServer, databaseIdentifier, schemaGeneration, tenantHandle])
|
|
74
|
+
const [state, setState] = React.useState({error: /** @type {Error | null} */ (null), loaded: false, selection})
|
|
31
75
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
} finally {
|
|
35
|
-
instance.running = false
|
|
36
|
-
}
|
|
37
|
-
}, [])
|
|
76
|
+
React.useEffect(() => {
|
|
77
|
+
if (!selection) return undefined
|
|
38
78
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
loadDatabase()
|
|
42
|
-
}
|
|
43
|
-
}, [loaded])
|
|
79
|
+
let current = true
|
|
80
|
+
const migrationsLoader = migrationsRequireContextCallbackRef.current
|
|
44
81
|
|
|
45
|
-
|
|
82
|
+
setState({error: null, loaded: false, selection})
|
|
83
|
+
void initializeFrontendDatabase({
|
|
84
|
+
configuration: selection.configuration,
|
|
85
|
+
databaseIdentifier: selection.databaseIdentifier,
|
|
86
|
+
migrationsRequireContextCallback: migrationsLoader,
|
|
87
|
+
schemaGeneration: selection.schemaGeneration,
|
|
88
|
+
tenantHandle: selection.tenantHandle
|
|
89
|
+
}).then(() => {
|
|
90
|
+
if (current) setState({error: null, loaded: true, selection})
|
|
91
|
+
}, (error) => {
|
|
92
|
+
if (current) setState({error: error instanceof Error ? error : new Error(String(error)), loaded: false, selection})
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
return () => { current = false }
|
|
96
|
+
}, [selection])
|
|
97
|
+
|
|
98
|
+
if (state.selection !== selection) return {error: null, loaded: false}
|
|
46
99
|
|
|
47
|
-
return {loaded}
|
|
100
|
+
return {error: state.error, loaded: state.loaded}
|
|
48
101
|
}
|
|
@@ -76,7 +76,10 @@ export default class ServerChangeFeedStore {
|
|
|
76
76
|
this._memoryChanges = []
|
|
77
77
|
this._memorySequence = 0
|
|
78
78
|
this._isReady = false
|
|
79
|
+
/** @type {Promise<void> | null} */
|
|
79
80
|
this._readyPromise = null
|
|
81
|
+
/** @type {WeakMap<import("../database/drivers/base.js").default, {completion: Promise<void>, promise: Promise<void>}>} */
|
|
82
|
+
this._transactionReadyPromises = new WeakMap()
|
|
80
83
|
}
|
|
81
84
|
|
|
82
85
|
/**
|
|
@@ -90,15 +93,60 @@ export default class ServerChangeFeedStore {
|
|
|
90
93
|
}
|
|
91
94
|
|
|
92
95
|
if (await this._schemaReady()) return
|
|
93
|
-
if (this._readyPromise) return await this._readyPromise
|
|
94
96
|
|
|
95
|
-
this.
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
97
|
+
this.configuration.setCurrent()
|
|
98
|
+
await this._withDb(async (db) => await this._ensureReadyWithDb(db))
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Coordinates durable and transaction-local readiness on one connection.
|
|
103
|
+
* @param {import("../database/drivers/base.js").default} db - Database connection.
|
|
104
|
+
* @returns {Promise<void>} Resolves when this caller can use the table.
|
|
105
|
+
*/
|
|
106
|
+
async _ensureReadyWithDb(db) {
|
|
107
|
+
if (this._isReady) return
|
|
108
|
+
|
|
109
|
+
const transactionCompletion = db.insideTransaction() ? db.transactionCompletion() : null
|
|
110
|
+
const transactionReady = this._transactionReadyPromises.get(db)
|
|
111
|
+
|
|
112
|
+
if (transactionCompletion && transactionReady?.completion === transactionCompletion) {
|
|
113
|
+
await transactionReady.promise
|
|
114
|
+
return
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (this._readyPromise) {
|
|
118
|
+
const readyPromise = this._readyPromise
|
|
119
|
+
|
|
120
|
+
await readyPromise
|
|
121
|
+
if (this._readyPromise === readyPromise) this._readyPromise = null
|
|
122
|
+
if (this._isReady) return
|
|
123
|
+
|
|
124
|
+
await this.ensureReady()
|
|
125
|
+
return
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (transactionCompletion) {
|
|
129
|
+
const tableReadyPromise = this._ensureChangesTable(db)
|
|
130
|
+
const transactionReadyPromise = tableReadyPromise.then(() => undefined)
|
|
131
|
+
|
|
132
|
+
const durableReadyPromise = tableReadyPromise.then(async (created) => {
|
|
133
|
+
if (!created) {
|
|
134
|
+
this._isReady = true
|
|
135
|
+
return
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
await transactionCompletion
|
|
99
139
|
})
|
|
140
|
+
|
|
141
|
+
this._transactionReadyPromises.set(db, {completion: transactionCompletion, promise: transactionReadyPromise})
|
|
142
|
+
this._readyPromise = durableReadyPromise
|
|
143
|
+
await transactionReadyPromise
|
|
144
|
+
return
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
this._readyPromise = this._ensureChangesTable(db).then(() => {
|
|
100
148
|
this._isReady = true
|
|
101
|
-
})
|
|
149
|
+
})
|
|
102
150
|
|
|
103
151
|
try {
|
|
104
152
|
await this._readyPromise
|
|
@@ -257,10 +305,10 @@ export default class ServerChangeFeedStore {
|
|
|
257
305
|
/**
|
|
258
306
|
* Ensures changes table exists.
|
|
259
307
|
* @param {import("../database/drivers/base.js").default} db - Database connection.
|
|
260
|
-
* @returns {Promise<
|
|
308
|
+
* @returns {Promise<boolean>} - Whether the table had to be created.
|
|
261
309
|
*/
|
|
262
310
|
async _ensureChangesTable(db) {
|
|
263
|
-
if (await db.tableExists(TABLE_NAME)) return
|
|
311
|
+
if (await db.tableExists(TABLE_NAME)) return false
|
|
264
312
|
|
|
265
313
|
const table = new TableData(TABLE_NAME, {ifNotExists: true})
|
|
266
314
|
|
|
@@ -279,6 +327,8 @@ export default class ServerChangeFeedStore {
|
|
|
279
327
|
table.datetime("created_at", {index: true, null: false})
|
|
280
328
|
|
|
281
329
|
await db.createTable(table)
|
|
330
|
+
|
|
331
|
+
return true
|
|
282
332
|
}
|
|
283
333
|
|
|
284
334
|
/**
|
|
@@ -50,6 +50,8 @@ export default class SyncScopeStore {
|
|
|
50
50
|
this._isReady = false
|
|
51
51
|
/** @type {Promise<void> | null} */
|
|
52
52
|
this._readyPromise = null
|
|
53
|
+
/** @type {WeakMap<import("../database/drivers/base.js").default, {completion: Promise<void>, promise: Promise<void>}>} */
|
|
54
|
+
this._transactionReadyPromises = new WeakMap()
|
|
53
55
|
}
|
|
54
56
|
|
|
55
57
|
/**
|
|
@@ -64,10 +66,56 @@ export default class SyncScopeStore {
|
|
|
64
66
|
return
|
|
65
67
|
}
|
|
66
68
|
|
|
67
|
-
|
|
69
|
+
await this._withDb(async (db) => await this._ensureReadyWithDb(db))
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Coordinates durable and transaction-local readiness on one connection.
|
|
74
|
+
* @param {import("../database/drivers/base.js").default} db - Database connection.
|
|
75
|
+
* @returns {Promise<void>} Resolves when this caller can use the table.
|
|
76
|
+
*/
|
|
77
|
+
async _ensureReadyWithDb(db) {
|
|
78
|
+
if (this._isReady) return
|
|
79
|
+
|
|
80
|
+
const transactionCompletion = db.insideTransaction() ? db.transactionCompletion() : null
|
|
81
|
+
const transactionReady = this._transactionReadyPromises.get(db)
|
|
68
82
|
|
|
69
|
-
|
|
70
|
-
await
|
|
83
|
+
if (transactionCompletion && transactionReady?.completion === transactionCompletion) {
|
|
84
|
+
await transactionReady.promise
|
|
85
|
+
return
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (this._readyPromise) {
|
|
89
|
+
const readyPromise = this._readyPromise
|
|
90
|
+
|
|
91
|
+
await readyPromise
|
|
92
|
+
if (this._readyPromise === readyPromise) this._readyPromise = null
|
|
93
|
+
if (this._isReady) return
|
|
94
|
+
|
|
95
|
+
await this.ensureReady()
|
|
96
|
+
return
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (transactionCompletion) {
|
|
100
|
+
const tableReadyPromise = this._ensureScopesTable(db)
|
|
101
|
+
const transactionReadyPromise = tableReadyPromise.then(() => undefined)
|
|
102
|
+
|
|
103
|
+
const durableReadyPromise = tableReadyPromise.then(async (created) => {
|
|
104
|
+
if (!created) {
|
|
105
|
+
this._isReady = true
|
|
106
|
+
return
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
await transactionCompletion
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
this._transactionReadyPromises.set(db, {completion: transactionCompletion, promise: transactionReadyPromise})
|
|
113
|
+
this._readyPromise = durableReadyPromise
|
|
114
|
+
await transactionReadyPromise
|
|
115
|
+
return
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
this._readyPromise = this._ensureScopesTable(db).then(() => {
|
|
71
119
|
this._isReady = true
|
|
72
120
|
})
|
|
73
121
|
|
|
@@ -274,10 +322,10 @@ export default class SyncScopeStore {
|
|
|
274
322
|
/**
|
|
275
323
|
* Ensures the scopes table exists.
|
|
276
324
|
* @param {import("../database/drivers/base.js").default} db - Database connection.
|
|
277
|
-
* @returns {Promise<
|
|
325
|
+
* @returns {Promise<boolean>} Whether the table had to be created.
|
|
278
326
|
*/
|
|
279
327
|
async _ensureScopesTable(db) {
|
|
280
|
-
if (await db.tableExists(TABLE_NAME)) return
|
|
328
|
+
if (await db.tableExists(TABLE_NAME)) return false
|
|
281
329
|
|
|
282
330
|
const table = new TableData(TABLE_NAME, {ifNotExists: true})
|
|
283
331
|
|
|
@@ -291,6 +339,8 @@ export default class SyncScopeStore {
|
|
|
291
339
|
table.datetime("updated_at", {null: false})
|
|
292
340
|
|
|
293
341
|
await db.createTable(table)
|
|
342
|
+
|
|
343
|
+
return true
|
|
294
344
|
}
|
|
295
345
|
|
|
296
346
|
/**
|
|
@@ -8,6 +8,9 @@
|
|
|
8
8
|
* @property {boolean} dirty - Whether delayed writes remain.
|
|
9
9
|
* @property {number} lastUsed - Monotonic recency sequence.
|
|
10
10
|
* @property {number} pinCount - Active scoped pins.
|
|
11
|
+
* @property {Promise<void> | undefined} readinessPromise - In-progress schema readiness.
|
|
12
|
+
* @property {boolean} ready - Whether migrations and model metadata are ready.
|
|
13
|
+
* @property {string | undefined} schemaGeneration - Ready or in-progress schema generation.
|
|
11
14
|
* @property {LifecycleState} state - Current lifecycle state.
|
|
12
15
|
*/
|
|
13
16
|
|
|
@@ -55,7 +58,17 @@ export default class FrontendTenantSqliteLifecycle {
|
|
|
55
58
|
const key = this.key(databaseIdentifier, databaseConfiguration)
|
|
56
59
|
let entry = this.entries.get(key)
|
|
57
60
|
if (!entry) {
|
|
58
|
-
entry = {
|
|
61
|
+
entry = {
|
|
62
|
+
databaseConfiguration,
|
|
63
|
+
databaseIdentifier,
|
|
64
|
+
dirty: false,
|
|
65
|
+
lastUsed: 0,
|
|
66
|
+
pinCount: 0,
|
|
67
|
+
readinessPromise: undefined,
|
|
68
|
+
ready: false,
|
|
69
|
+
schemaGeneration: undefined,
|
|
70
|
+
state: "closed"
|
|
71
|
+
}
|
|
59
72
|
this.entries.set(key, entry)
|
|
60
73
|
}
|
|
61
74
|
return entry
|
|
@@ -67,6 +80,8 @@ export default class FrontendTenantSqliteLifecycle {
|
|
|
67
80
|
dirty: entry.dirty,
|
|
68
81
|
lastUsed: entry.lastUsed,
|
|
69
82
|
pinCount: entry.pinCount,
|
|
83
|
+
ready: entry.ready,
|
|
84
|
+
schemaGeneration: entry.schemaGeneration,
|
|
70
85
|
state: entry.state
|
|
71
86
|
})
|
|
72
87
|
}
|
|
@@ -94,6 +109,78 @@ export default class FrontendTenantSqliteLifecycle {
|
|
|
94
109
|
})
|
|
95
110
|
}
|
|
96
111
|
|
|
112
|
+
/**
|
|
113
|
+
* Opens and prepares one captured physical tenant database generation. The
|
|
114
|
+
* readiness callback runs outside the lifecycle bookkeeping lock, so distinct
|
|
115
|
+
* tenant databases can migrate concurrently while matching callers share one
|
|
116
|
+
* promise.
|
|
117
|
+
* @param {string} databaseIdentifier - Logical database identifier.
|
|
118
|
+
* @param {import("../configuration-types.js").DatabaseConfigurationType} databaseConfiguration - Captured physical configuration.
|
|
119
|
+
* @param {string} schemaGeneration - Application schema generation.
|
|
120
|
+
* @param {() => Promise<void>} callback - Migration and metadata initialization.
|
|
121
|
+
* @returns {Promise<Readonly<ReturnType<FrontendTenantSqliteLifecycle["snapshot"]>>>} - Ready lifecycle snapshot.
|
|
122
|
+
*/
|
|
123
|
+
async initialize(databaseIdentifier, databaseConfiguration, schemaGeneration, callback) {
|
|
124
|
+
this.assertSqlite(databaseConfiguration)
|
|
125
|
+
if (!databaseConfiguration.tenantOnly) throw new Error("Frontend tenant database initialization requires a tenant-only SQLite database")
|
|
126
|
+
if (typeof schemaGeneration !== "string" || schemaGeneration.length === 0) {
|
|
127
|
+
throw new TypeError("Frontend tenant database initialization requires a non-empty schemaGeneration")
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const readiness = await this.serialize(async () => {
|
|
131
|
+
const entry = this.entry(databaseIdentifier, databaseConfiguration)
|
|
132
|
+
|
|
133
|
+
if (entry.readinessPromise) {
|
|
134
|
+
if (entry.schemaGeneration !== schemaGeneration) {
|
|
135
|
+
throw new Error(`Frontend tenant database is already initializing schema generation ${JSON.stringify(entry.schemaGeneration)}; cannot initialize mismatched generation ${JSON.stringify(schemaGeneration)}`)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return {entry, promise: entry.readinessPromise}
|
|
139
|
+
}
|
|
140
|
+
if (entry.ready && entry.schemaGeneration === schemaGeneration) {
|
|
141
|
+
return {entry, promise: Promise.resolve()}
|
|
142
|
+
}
|
|
143
|
+
if (entry.schemaGeneration && entry.schemaGeneration !== schemaGeneration && (entry.pinCount > 0 || this.configuration.getDatabasePool(databaseIdentifier).capturedConnectionInUse(databaseConfiguration))) {
|
|
144
|
+
throw new Error(`Cannot replace frontend tenant schema generation ${JSON.stringify(entry.schemaGeneration)} while its physical database is in use`)
|
|
145
|
+
}
|
|
146
|
+
if (entry.state !== "open") await this.openUnlocked(entry)
|
|
147
|
+
if (entry.schemaGeneration && entry.schemaGeneration !== schemaGeneration) {
|
|
148
|
+
this.configuration.clearRecordMetadataForDatabaseIdentity(this.key(databaseIdentifier, databaseConfiguration))
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
entry.ready = false
|
|
152
|
+
entry.schemaGeneration = schemaGeneration
|
|
153
|
+
entry.pinCount++
|
|
154
|
+
const promise = Promise.resolve().then(callback)
|
|
155
|
+
|
|
156
|
+
entry.readinessPromise = promise
|
|
157
|
+
|
|
158
|
+
return {entry, promise}
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
try {
|
|
162
|
+
await readiness.promise
|
|
163
|
+
await this.serialize(async () => {
|
|
164
|
+
if (readiness.entry.readinessPromise === readiness.promise) {
|
|
165
|
+
readiness.entry.readinessPromise = undefined
|
|
166
|
+
readiness.entry.ready = true
|
|
167
|
+
readiness.entry.pinCount--
|
|
168
|
+
}
|
|
169
|
+
})
|
|
170
|
+
} catch (error) {
|
|
171
|
+
await this.serialize(async () => {
|
|
172
|
+
if (readiness.entry.readinessPromise === readiness.promise) {
|
|
173
|
+
readiness.entry.readinessPromise = undefined
|
|
174
|
+
readiness.entry.ready = false
|
|
175
|
+
readiness.entry.pinCount--
|
|
176
|
+
}
|
|
177
|
+
})
|
|
178
|
+
throw error
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return this.snapshot(readiness.entry)
|
|
182
|
+
}
|
|
183
|
+
|
|
97
184
|
async flush(/** @type {string} */ databaseIdentifier, /** @type {import("../configuration-types.js").DatabaseConfigurationType} */ databaseConfiguration) {
|
|
98
185
|
this.assertSqlite(databaseConfiguration)
|
|
99
186
|
return await this.serialize(async () => {
|
|
@@ -117,7 +204,10 @@ export default class FrontendTenantSqliteLifecycle {
|
|
|
117
204
|
try {
|
|
118
205
|
if (flush) await this.configuration.getDatabasePool(databaseIdentifier).flushCapturedConnection(databaseConfiguration)
|
|
119
206
|
await this.configuration.getDatabasePool(databaseIdentifier).closeCapturedConnection(databaseConfiguration)
|
|
207
|
+
this.configuration.clearRecordMetadataForDatabaseIdentity(this.key(databaseIdentifier, databaseConfiguration))
|
|
120
208
|
entry.dirty = false
|
|
209
|
+
entry.ready = false
|
|
210
|
+
entry.schemaGeneration = undefined
|
|
121
211
|
entry.state = "closed"
|
|
122
212
|
this.entries.delete(this.key(databaseIdentifier, databaseConfiguration))
|
|
123
213
|
return this.snapshot(entry)
|
|
@@ -136,11 +226,16 @@ export default class FrontendTenantSqliteLifecycle {
|
|
|
136
226
|
entry.state = "deleting"
|
|
137
227
|
try {
|
|
138
228
|
await this.configuration.getDatabasePool(databaseIdentifier).deleteCapturedDatabase(databaseConfiguration)
|
|
229
|
+
this.configuration.clearRecordMetadataForDatabaseIdentity(this.key(databaseIdentifier, databaseConfiguration))
|
|
139
230
|
entry.dirty = false
|
|
231
|
+
entry.ready = false
|
|
232
|
+
entry.schemaGeneration = undefined
|
|
140
233
|
entry.state = "closed"
|
|
141
234
|
this.entries.delete(this.key(databaseIdentifier, databaseConfiguration))
|
|
142
235
|
return this.snapshot(entry)
|
|
143
236
|
} catch (error) {
|
|
237
|
+
this.configuration.clearRecordMetadataForDatabaseIdentity(this.key(databaseIdentifier, databaseConfiguration))
|
|
238
|
+
entry.ready = false
|
|
144
239
|
entry.state = "closed"
|
|
145
240
|
throw error
|
|
146
241
|
}
|
|
@@ -150,7 +245,7 @@ export default class FrontendTenantSqliteLifecycle {
|
|
|
150
245
|
inspect(/** @type {string} */ databaseIdentifier, /** @type {import("../configuration-types.js").DatabaseConfigurationType} */ databaseConfiguration) {
|
|
151
246
|
this.assertSqlite(databaseConfiguration)
|
|
152
247
|
const entry = this.entries.get(this.key(databaseIdentifier, databaseConfiguration))
|
|
153
|
-
return entry ? this.snapshot(entry) : Object.freeze({databaseIdentifier, dirty: false, lastUsed: 0, pinCount: 0, state: "closed"})
|
|
248
|
+
return entry ? this.snapshot(entry) : Object.freeze({databaseIdentifier, dirty: false, lastUsed: 0, pinCount: 0, ready: false, schemaGeneration: undefined, state: "closed"})
|
|
154
249
|
}
|
|
155
250
|
|
|
156
251
|
inspectAll() {
|
|
@@ -158,7 +253,13 @@ export default class FrontendTenantSqliteLifecycle {
|
|
|
158
253
|
return Object.freeze({handles: Object.freeze(handles), maxOpenHandles: this.maxOpenHandles, openCount: handles.filter(({state}) => state === "open").length})
|
|
159
254
|
}
|
|
160
255
|
|
|
161
|
-
reset() {
|
|
256
|
+
reset() {
|
|
257
|
+
for (const entry of this.entries.values()) {
|
|
258
|
+
this.configuration.clearRecordMetadataForDatabaseIdentity(this.key(entry.databaseIdentifier, entry.databaseConfiguration))
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
this.entries.clear()
|
|
262
|
+
}
|
|
162
263
|
|
|
163
264
|
async withPin(/** @type {string} */ databaseIdentifier, /** @type {import("../configuration-types.js").DatabaseConfigurationType} */ databaseConfiguration, /** @type {() => Promise<ReturnType<typeof JSON.parse>>} */ callback) {
|
|
164
265
|
this.assertSqlite(databaseConfiguration)
|
|
@@ -176,21 +277,52 @@ export default class FrontendTenantSqliteLifecycle {
|
|
|
176
277
|
}
|
|
177
278
|
}
|
|
178
279
|
|
|
179
|
-
|
|
180
|
-
|
|
280
|
+
/**
|
|
281
|
+
* Atomically validates readiness, captures the schema generation, and pins
|
|
282
|
+
* one lifecycle entry before starting database work.
|
|
283
|
+
* @param {string} databaseIdentifier - Logical database identifier.
|
|
284
|
+
* @param {import("../configuration-types.js").DatabaseConfigurationType} databaseConfiguration - Captured physical configuration.
|
|
285
|
+
* @param {{requireReady: boolean, schemaGeneration?: string}} options - Operation readiness requirements.
|
|
286
|
+
* @param {(schemaGeneration: string | undefined) => Promise<ReturnType<typeof JSON.parse>>} callback - Pinned operation callback.
|
|
287
|
+
* @returns {Promise<ReturnType<typeof JSON.parse>>} - Operation result.
|
|
288
|
+
*/
|
|
289
|
+
async databaseOperation(databaseIdentifier, databaseConfiguration, {requireReady, schemaGeneration}, callback) {
|
|
290
|
+
if (databaseConfiguration.type !== "sqlite") return await callback(schemaGeneration)
|
|
181
291
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
292
|
+
const entry = await this.serialize(async () => {
|
|
293
|
+
const operationEntry = this.entries.get(this.key(databaseIdentifier, databaseConfiguration))
|
|
294
|
+
|
|
295
|
+
if (requireReady && databaseConfiguration.tenantOnly && databaseConfiguration.migrations && !operationEntry?.ready) {
|
|
296
|
+
const generation = operationEntry?.schemaGeneration ? ` for schema generation ${JSON.stringify(operationEntry.schemaGeneration)}` : ""
|
|
297
|
+
|
|
298
|
+
throw new Error(`Frontend tenant database ${JSON.stringify(databaseIdentifier)} is not ready${generation}`)
|
|
299
|
+
}
|
|
300
|
+
if (!operationEntry) return undefined
|
|
301
|
+
if (schemaGeneration && operationEntry.schemaGeneration && schemaGeneration !== operationEntry.schemaGeneration) {
|
|
302
|
+
throw new Error(`Frontend tenant database ${JSON.stringify(databaseIdentifier)} is on schema generation ${JSON.stringify(operationEntry.schemaGeneration)}, not ${JSON.stringify(schemaGeneration)}`)
|
|
192
303
|
}
|
|
304
|
+
if (operationEntry.state !== "open") await this.openUnlocked(operationEntry)
|
|
305
|
+
|
|
306
|
+
operationEntry.pinCount++
|
|
307
|
+
operationEntry.lastUsed = ++this.sequence
|
|
308
|
+
|
|
309
|
+
return operationEntry
|
|
193
310
|
})
|
|
311
|
+
const operationSchemaGeneration = schemaGeneration || entry?.schemaGeneration
|
|
312
|
+
|
|
313
|
+
if (!entry) return await callback(operationSchemaGeneration)
|
|
314
|
+
|
|
315
|
+
try {
|
|
316
|
+
return await callback(operationSchemaGeneration)
|
|
317
|
+
} finally {
|
|
318
|
+
const dirty = this.configuration.getDatabasePool(databaseIdentifier).capturedConnectionHasPendingWrites(databaseConfiguration)
|
|
319
|
+
|
|
320
|
+
await this.serialize(async () => {
|
|
321
|
+
entry.dirty ||= dirty
|
|
322
|
+
entry.lastUsed = ++this.sequence
|
|
323
|
+
entry.pinCount--
|
|
324
|
+
})
|
|
325
|
+
}
|
|
194
326
|
}
|
|
195
327
|
|
|
196
328
|
async openUnlocked(/** @type {LifecycleEntry} */ entry) {
|
|
@@ -222,6 +354,9 @@ export default class FrontendTenantSqliteLifecycle {
|
|
|
222
354
|
const victim = candidates[0]
|
|
223
355
|
if (!victim) throw new Error(`Frontend tenant SQLite handle capacity ${this.maxOpenHandles} reached; every handle is dirty, pinned, or in use`)
|
|
224
356
|
await this.configuration.getDatabasePool(victim.databaseIdentifier).closeCapturedConnection(victim.databaseConfiguration)
|
|
357
|
+
this.configuration.clearRecordMetadataForDatabaseIdentity(this.key(victim.databaseIdentifier, victim.databaseConfiguration))
|
|
358
|
+
victim.ready = false
|
|
359
|
+
victim.schemaGeneration = undefined
|
|
225
360
|
victim.state = "closed"
|
|
226
361
|
this.entries.delete(this.key(victim.databaseIdentifier, victim.databaseConfiguration))
|
|
227
362
|
}
|