velocious 1.0.714 → 1.0.716
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/build/background-jobs/local-store.js +3 -0
- package/build/background-jobs/store.js +77 -1
- package/build/background-jobs/sync-replay-job.js +79 -0
- package/build/background-jobs/types.js +2 -0
- package/build/database/drivers/base.js +60 -0
- package/build/database/drivers/mssql/index.js +59 -0
- package/build/database/drivers/mysql/index.js +51 -0
- package/build/database/drivers/pgsql/index.js +61 -0
- package/build/database/initializer-from-require-context.js +31 -7
- package/build/database/schema-metadata-prefetcher.js +108 -0
- package/build/src/background-jobs/local-store.d.ts.map +1 -1
- package/build/src/background-jobs/local-store.js +4 -1
- package/build/src/background-jobs/store.d.ts +24 -0
- package/build/src/background-jobs/store.d.ts.map +1 -1
- package/build/src/background-jobs/store.js +66 -2
- package/build/src/background-jobs/sync-replay-job.d.ts +45 -0
- package/build/src/background-jobs/sync-replay-job.d.ts.map +1 -0
- package/build/src/background-jobs/sync-replay-job.js +73 -0
- package/build/src/background-jobs/types.d.ts +10 -0
- package/build/src/background-jobs/types.d.ts.map +1 -1
- package/build/src/background-jobs/types.js +3 -1
- package/build/src/database/drivers/base.d.ts +29 -0
- package/build/src/database/drivers/base.d.ts.map +1 -1
- package/build/src/database/drivers/base.js +55 -1
- package/build/src/database/drivers/mssql/index.d.ts +9 -0
- package/build/src/database/drivers/mssql/index.d.ts.map +1 -1
- package/build/src/database/drivers/mssql/index.js +54 -1
- package/build/src/database/drivers/mysql/index.d.ts +13 -0
- package/build/src/database/drivers/mysql/index.d.ts.map +1 -1
- package/build/src/database/drivers/mysql/index.js +46 -1
- package/build/src/database/drivers/pgsql/index.d.ts +12 -2
- package/build/src/database/drivers/pgsql/index.d.ts.map +1 -1
- package/build/src/database/drivers/pgsql/index.js +56 -1
- package/build/src/database/initializer-from-require-context.d.ts.map +1 -1
- package/build/src/database/initializer-from-require-context.js +26 -7
- package/build/src/database/schema-metadata-prefetcher.d.ts +53 -0
- package/build/src/database/schema-metadata-prefetcher.d.ts.map +1 -0
- package/build/src/database/schema-metadata-prefetcher.js +95 -0
- package/build/src/sync/sync-client-registry.d.ts +9 -0
- package/build/src/sync/sync-client-registry.d.ts.map +1 -1
- package/build/src/sync/sync-client-registry.js +12 -1
- package/build/src/utils/bounded-task-runner.d.ts +26 -0
- package/build/src/utils/bounded-task-runner.d.ts.map +1 -0
- package/build/src/utils/bounded-task-runner.js +44 -0
- package/build/sync/sync-client-registry.js +12 -0
- package/build/utils/bounded-task-runner.js +51 -0
- package/package.json +1 -1
- package/src/background-jobs/local-store.js +3 -0
- package/src/background-jobs/store.js +77 -1
- package/src/background-jobs/sync-replay-job.js +79 -0
- package/src/background-jobs/types.js +2 -0
- package/src/database/drivers/base.js +60 -0
- package/src/database/drivers/mssql/index.js +59 -0
- package/src/database/drivers/mysql/index.js +51 -0
- package/src/database/drivers/pgsql/index.js +61 -0
- package/src/database/initializer-from-require-context.js +31 -7
- package/src/database/schema-metadata-prefetcher.js +108 -0
- package/src/sync/sync-client-registry.js +12 -0
- package/src/utils/bounded-task-runner.js +51 -0
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import AlterTable from "./sql/alter-table.js"
|
|
4
4
|
import wait from "awaitery/build/wait.js"
|
|
5
5
|
import Base from "../base.js"
|
|
6
|
+
import Column from "./column.js"
|
|
6
7
|
import { Client, types as pgTypes } from "pg"
|
|
7
8
|
import CreateDatabase from "./sql/create-database.js"
|
|
8
9
|
import CreateIndex from "./sql/create-index.js"
|
|
@@ -329,6 +330,10 @@ export default class VelociousDatabaseDriversPgsql extends Base{
|
|
|
329
330
|
return insert.toSql()
|
|
330
331
|
}
|
|
331
332
|
|
|
333
|
+
/**
|
|
334
|
+
* Runs get tables.
|
|
335
|
+
* @returns {Promise<Array<import("../base-table.js").default>>} - Resolves with the tables.
|
|
336
|
+
*/
|
|
332
337
|
async getTables() {
|
|
333
338
|
return await this._cachedSchemaMetadata("tables", async () => {
|
|
334
339
|
const result = await this.query("SELECT * FROM information_schema.tables WHERE table_catalog = CURRENT_DATABASE() AND table_schema = 'public'")
|
|
@@ -344,6 +349,62 @@ export default class VelociousDatabaseDriversPgsql extends Base{
|
|
|
344
349
|
})
|
|
345
350
|
}
|
|
346
351
|
|
|
352
|
+
/**
|
|
353
|
+
* Reads column metadata for many tables in one query instead of one catalog
|
|
354
|
+
* join per table.
|
|
355
|
+
* @param {Array<import("../base-table.js").default>} tables - Tables to load columns for.
|
|
356
|
+
* @returns {Promise<Map<string, Array<import("../base-column.js").default>>>} - Columns keyed by table name.
|
|
357
|
+
*/
|
|
358
|
+
async batchTableColumns(tables) {
|
|
359
|
+
/** @type {Map<string, Array<import("../base-column.js").default>>} */
|
|
360
|
+
const columnsByTable = new Map()
|
|
361
|
+
|
|
362
|
+
if (tables.length === 0) return columnsByTable
|
|
363
|
+
|
|
364
|
+
const tablesByName = new Map(tables.map((table) => [table.getName(), table]))
|
|
365
|
+
const tableNamesSql = [...tablesByName.keys()].map((tableName) => this.quote(tableName)).join(", ")
|
|
366
|
+
const rows = await this.query(`
|
|
367
|
+
SELECT
|
|
368
|
+
columns.*,
|
|
369
|
+
CASE WHEN key_column_usage.column_name IS NOT NULL THEN 1 ELSE 0 END AS is_primary_key,
|
|
370
|
+
col_description((columns.table_schema || '.' || columns.table_name)::regclass, columns.ordinal_position) AS column_comment
|
|
371
|
+
|
|
372
|
+
FROM
|
|
373
|
+
information_schema.columns AS columns
|
|
374
|
+
|
|
375
|
+
LEFT JOIN information_schema.table_constraints AS table_constraints ON
|
|
376
|
+
table_constraints.table_name = columns.table_name AND
|
|
377
|
+
table_constraints.table_schema = columns.table_schema AND
|
|
378
|
+
table_constraints.constraint_type = 'PRIMARY KEY'
|
|
379
|
+
|
|
380
|
+
LEFT JOIN information_schema.key_column_usage AS key_column_usage ON
|
|
381
|
+
key_column_usage.constraint_name = table_constraints.constraint_name AND
|
|
382
|
+
key_column_usage.table_schema = table_constraints.table_schema AND
|
|
383
|
+
key_column_usage.table_name = columns.table_name AND
|
|
384
|
+
key_column_usage.column_name = columns.column_name
|
|
385
|
+
|
|
386
|
+
WHERE
|
|
387
|
+
columns.table_catalog = CURRENT_DATABASE() AND
|
|
388
|
+
columns.table_schema = 'public' AND
|
|
389
|
+
columns.table_name IN (${tableNamesSql})
|
|
390
|
+
ORDER BY columns.table_name, columns.ordinal_position
|
|
391
|
+
`)
|
|
392
|
+
|
|
393
|
+
for (const row of rows) {
|
|
394
|
+
const tableName = /** @type {string} */ (row.table_name)
|
|
395
|
+
const table = tablesByName.get(tableName)
|
|
396
|
+
|
|
397
|
+
if (!table) continue
|
|
398
|
+
|
|
399
|
+
const columns = columnsByTable.get(tableName) || []
|
|
400
|
+
|
|
401
|
+
columns.push(new Column(table, /** @type {Record<string, ReturnType<typeof JSON.parse>>} */ (row)))
|
|
402
|
+
columnsByTable.set(tableName, columns)
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
return columnsByTable
|
|
406
|
+
}
|
|
407
|
+
|
|
347
408
|
/**
|
|
348
409
|
* Truncates all eligible tables in one PostgreSQL request.
|
|
349
410
|
* @param {Array<import("../base-table.js").default>} tables - Eligible tables.
|
|
@@ -10,7 +10,15 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import Logger from "../logger.js"
|
|
13
|
+
import BoundedTaskRunner from "../utils/bounded-task-runner.js"
|
|
13
14
|
import restArgsError from "../utils/rest-args-error.js"
|
|
15
|
+
import SchemaMetadataPrefetcher from "./schema-metadata-prefetcher.js"
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* How many model classes initialize at once. Metadata comes from the prefetched
|
|
19
|
+
* schema cache, so this is CPU-bound attribute mapping rather than database work.
|
|
20
|
+
*/
|
|
21
|
+
const MODEL_INITIALIZATION_CONCURRENCY = 8
|
|
14
22
|
|
|
15
23
|
export default class VelociousDatabaseInitializerFromRequireContext {
|
|
16
24
|
/**
|
|
@@ -34,6 +42,9 @@ export default class VelociousDatabaseInitializerFromRequireContext {
|
|
|
34
42
|
async initialize({configuration, ...restArgs}) {
|
|
35
43
|
restArgsError(restArgs)
|
|
36
44
|
|
|
45
|
+
/** @type {Array<typeof import("./record/index.js").default>} */
|
|
46
|
+
const modelClasses = []
|
|
47
|
+
|
|
37
48
|
for (const fileName of this.requireContext.keys()) {
|
|
38
49
|
const modelClassImport = this.requireContext(fileName)
|
|
39
50
|
|
|
@@ -50,14 +61,27 @@ export default class VelociousDatabaseInitializerFromRequireContext {
|
|
|
50
61
|
continue
|
|
51
62
|
}
|
|
52
63
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
await this._bestEffortInitializeDeferredModel({configuration, modelClass})
|
|
56
|
-
continue
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
await this._initializeModelRecord({configuration, modelClass})
|
|
64
|
+
modelClass.registerRecordClass({configuration})
|
|
65
|
+
modelClasses.push(modelClass)
|
|
60
66
|
}
|
|
67
|
+
|
|
68
|
+
// One catalog read per database instead of one per model, shared by every
|
|
69
|
+
// model initialized below through the driver's schema cache.
|
|
70
|
+
await new SchemaMetadataPrefetcher({configuration, logger: this.logger}).prefetch({modelClasses})
|
|
71
|
+
|
|
72
|
+
await BoundedTaskRunner.run({
|
|
73
|
+
concurrency: MODEL_INITIALIZATION_CONCURRENCY,
|
|
74
|
+
items: modelClasses,
|
|
75
|
+
worker: async (modelClass) => {
|
|
76
|
+
if (!modelClass.getEagerLoadRecordMetadata()) {
|
|
77
|
+
await this._bestEffortInitializeDeferredModel({configuration, modelClass})
|
|
78
|
+
|
|
79
|
+
return
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
await this._initializeModelRecord({configuration, modelClass})
|
|
83
|
+
}
|
|
84
|
+
})
|
|
61
85
|
}
|
|
62
86
|
|
|
63
87
|
/**
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import Logger from "../logger.js"
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Loads table and column metadata for a whole set of model classes up front.
|
|
7
|
+
*
|
|
8
|
+
* Model initialization otherwise reads each table's columns on its own, which
|
|
9
|
+
* turns a boot into one catalog query per model (and per HTTP worker process).
|
|
10
|
+
* Drivers that can answer for many tables in one statement are asked to do so;
|
|
11
|
+
* the resulting columns are seeded into the driver's per-table schema cache, so
|
|
12
|
+
* the initialization pass that follows resolves without querying at all.
|
|
13
|
+
*/
|
|
14
|
+
export default class SchemaMetadataPrefetcher {
|
|
15
|
+
/**
|
|
16
|
+
* Builds a prefetcher for one configuration.
|
|
17
|
+
* @param {object} args - Prefetcher args.
|
|
18
|
+
* @param {import("../configuration.js").default} args.configuration - Configuration owning the database connections.
|
|
19
|
+
* @param {number} [args.concurrency] - Concurrency for drivers that can only read one table per query.
|
|
20
|
+
* @param {import("../logger.js").default} [args.logger] - Logger for debug output.
|
|
21
|
+
*/
|
|
22
|
+
constructor({configuration, concurrency = 8, logger}) {
|
|
23
|
+
this.configuration = configuration
|
|
24
|
+
this.concurrency = concurrency
|
|
25
|
+
this.logger = logger || new Logger(this)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Prefetches column metadata for every model class that has a reachable table.
|
|
30
|
+
*
|
|
31
|
+
* Models whose table or connection is unavailable are skipped: their
|
|
32
|
+
* initialization stays deferred and will initialize lazily on first use, which
|
|
33
|
+
* is the same contract the per-model initialization applies.
|
|
34
|
+
* @param {{modelClasses: Array<typeof import("./record/index.js").default>}} args - Model classes to prefetch for.
|
|
35
|
+
* @returns {Promise<void>}
|
|
36
|
+
*/
|
|
37
|
+
async prefetch({modelClasses}) {
|
|
38
|
+
/** @type {Map<import("./drivers/base.js").default, Map<string, import("./drivers/base-table.js").default>>} */
|
|
39
|
+
const tablesByDriver = new Map()
|
|
40
|
+
|
|
41
|
+
for (const modelClass of modelClasses) {
|
|
42
|
+
const table = await this.tableFor(modelClass)
|
|
43
|
+
|
|
44
|
+
if (!table) continue
|
|
45
|
+
|
|
46
|
+
const driver = table.getDriver()
|
|
47
|
+
let tables = tablesByDriver.get(driver)
|
|
48
|
+
|
|
49
|
+
if (!tables) {
|
|
50
|
+
tables = new Map()
|
|
51
|
+
tablesByDriver.set(driver, tables)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
tables.set(table.getName(), table)
|
|
55
|
+
|
|
56
|
+
// Translated models initialize their translation class as part of the same
|
|
57
|
+
// startup pass, so its table has to be part of the batch as well - otherwise
|
|
58
|
+
// every translated model still costs its own catalog read.
|
|
59
|
+
const translationTable = await this.tableByName(driver, modelClass.getTranslationsTableName())
|
|
60
|
+
|
|
61
|
+
if (translationTable) tables.set(translationTable.getName(), translationTable)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const startedAt = Date.now()
|
|
65
|
+
let tableCount = 0
|
|
66
|
+
|
|
67
|
+
for (const [driver, tables] of tablesByDriver) {
|
|
68
|
+
await driver.prefetchTableColumns([...tables.values()], {concurrency: this.concurrency})
|
|
69
|
+
tableCount += tables.size
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
void this.logger.debug(() => ["schema metadata prefetched", {
|
|
73
|
+
databases: tablesByDriver.size,
|
|
74
|
+
durationMs: Date.now() - startedAt,
|
|
75
|
+
tables: tableCount
|
|
76
|
+
}])
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Resolves a model's table without initializing its record metadata.
|
|
81
|
+
* @param {typeof import("./record/index.js").default} modelClass - Model class to resolve a table for.
|
|
82
|
+
* @returns {Promise<import("./drivers/base-table.js").default | null>} Table, or null when it is not reachable.
|
|
83
|
+
*/
|
|
84
|
+
async tableFor(modelClass) {
|
|
85
|
+
try {
|
|
86
|
+
const driver = modelClass.connection({enforceTenantDatabaseScope: false})
|
|
87
|
+
|
|
88
|
+
return await this.tableByName(driver, modelClass.tableName())
|
|
89
|
+
} catch (error) {
|
|
90
|
+
// A model whose database is unreachable - or whose table is optional and
|
|
91
|
+
// absent - stays deferred rather than failing startup; it initializes
|
|
92
|
+
// lazily on first use.
|
|
93
|
+
void this.logger.debug(() => [`Leaving ${modelClass.name} out of schema prefetch`, error])
|
|
94
|
+
|
|
95
|
+
return null
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Looks a table up in the driver's table list without throwing when absent.
|
|
101
|
+
* @param {import("./drivers/base.js").default} driver - Driver owning the table.
|
|
102
|
+
* @param {string} tableName - Table name to resolve.
|
|
103
|
+
* @returns {Promise<import("./drivers/base-table.js").default | null>} Table, or null when it does not exist.
|
|
104
|
+
*/
|
|
105
|
+
async tableByName(driver, tableName) {
|
|
106
|
+
return await driver.getTableByName(tableName, {throwError: false}) || null
|
|
107
|
+
}
|
|
108
|
+
}
|
|
@@ -28,3 +28,15 @@ export function currentSyncClient() {
|
|
|
28
28
|
|
|
29
29
|
return currentClient
|
|
30
30
|
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Returns the current sync client when one is registered.
|
|
34
|
+
*
|
|
35
|
+
* Background jobs that replay the sync queue can legitimately run before an app has
|
|
36
|
+
* registered its client, or after it cleared it on sign-out. They need a non-throwing
|
|
37
|
+
* lookup so that state is a no-op instead of a failed job.
|
|
38
|
+
* @returns {ReturnType<typeof JSON.parse> | null} Current sync client, or null.
|
|
39
|
+
*/
|
|
40
|
+
export function currentSyncClientOrNull() {
|
|
41
|
+
return currentClient
|
|
42
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Runs one callback per item with a bounded number of callbacks in flight.
|
|
5
|
+
*
|
|
6
|
+
* Used where a startup pass has to touch many independent records (model
|
|
7
|
+
* metadata, per-table schema reads) and running them sequentially multiplies the
|
|
8
|
+
* round-trip latency by the item count, while running them all at once would
|
|
9
|
+
* open an unbounded number of database connections.
|
|
10
|
+
*/
|
|
11
|
+
export default class BoundedTaskRunner {
|
|
12
|
+
/**
|
|
13
|
+
* Runs the worker for every item, keeping at most `concurrency` callbacks in
|
|
14
|
+
* flight and returning the results in input order.
|
|
15
|
+
* @template T, R
|
|
16
|
+
* @param {object} args - Run args.
|
|
17
|
+
* @param {number} [args.concurrency] - Maximum callbacks in flight.
|
|
18
|
+
* @param {T[]} args.items - Items to process.
|
|
19
|
+
* @param {(item: T, index: number) => Promise<R>} args.worker - Callback per item.
|
|
20
|
+
* @returns {Promise<R[]>} - Results in input order.
|
|
21
|
+
*/
|
|
22
|
+
static async run({concurrency = 8, items, worker}) {
|
|
23
|
+
if (!Number.isInteger(concurrency) || concurrency < 1) {
|
|
24
|
+
throw new Error(`Concurrency must be a positive integer but got ${String(concurrency)}`)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** @type {R[]} */
|
|
28
|
+
const results = new Array(items.length)
|
|
29
|
+
let nextIndex = 0
|
|
30
|
+
|
|
31
|
+
const runNext = async () => {
|
|
32
|
+
while (nextIndex < items.length) {
|
|
33
|
+
const index = nextIndex
|
|
34
|
+
|
|
35
|
+
nextIndex += 1
|
|
36
|
+
results[index] = await worker(items[index], index)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** @type {Promise<void>[]} */
|
|
41
|
+
const runners = []
|
|
42
|
+
|
|
43
|
+
for (let index = 0; index < Math.min(concurrency, items.length); index++) {
|
|
44
|
+
runners.push(runNext())
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
await Promise.all(runners)
|
|
48
|
+
|
|
49
|
+
return results
|
|
50
|
+
}
|
|
51
|
+
}
|