velocious 1.0.548 → 1.0.551
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 +23 -0
- package/build/database/advisory-lock-runner.js +17 -8
- package/build/database/structure-sql-loader.js +68 -0
- package/build/environment-handlers/node/cli/commands/db/schema/load.js +3 -45
- package/build/src/database/advisory-lock-runner.d.ts +25 -8
- package/build/src/database/advisory-lock-runner.d.ts.map +1 -1
- package/build/src/database/advisory-lock-runner.js +18 -9
- package/build/src/database/structure-sql-loader.d.ts +35 -0
- package/build/src/database/structure-sql-loader.d.ts.map +1 -0
- package/build/src/database/structure-sql-loader.js +63 -0
- package/build/src/environment-handlers/node/cli/commands/db/schema/load.d.ts +0 -19
- package/build/src/environment-handlers/node/cli/commands/db/schema/load.d.ts.map +1 -1
- package/build/src/environment-handlers/node/cli/commands/db/schema/load.js +4 -42
- package/build/src/testing/test.d.ts +2 -1
- package/build/src/testing/test.d.ts.map +1 -1
- package/build/src/testing/test.js +3 -2
- package/build/src/testing/wait-for-event.d.ts +46 -0
- package/build/src/testing/wait-for-event.d.ts.map +1 -0
- package/build/src/testing/wait-for-event.js +64 -0
- package/build/testing/test.js +2 -1
- package/build/testing/wait-for-event.js +72 -0
- package/build/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/src/database/advisory-lock-runner.js +17 -8
- package/src/database/structure-sql-loader.js +68 -0
- package/src/environment-handlers/node/cli/commands/db/schema/load.js +3 -45
- package/src/testing/test.js +2 -1
- package/src/testing/wait-for-event.js +72 -0
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"velocious": "build/bin/velocious.js"
|
|
4
4
|
},
|
|
5
5
|
"name": "velocious",
|
|
6
|
-
"version": "1.0.
|
|
6
|
+
"version": "1.0.551",
|
|
7
7
|
"main": "build/index.js",
|
|
8
8
|
"types": "build/index.d.ts",
|
|
9
9
|
"files": [
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"description": "",
|
|
53
53
|
"dependencies": {
|
|
54
54
|
"@js-temporal/polyfill": "^0.5.1",
|
|
55
|
-
"awaitery": "^1.0.
|
|
55
|
+
"awaitery": "^1.0.17",
|
|
56
56
|
"bcryptjs": "^3.0.2",
|
|
57
57
|
"better-localstorage": "^1.0.7",
|
|
58
58
|
"debounce": "^3.0.0",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
|
|
3
|
-
import timeout from "awaitery/build/timeout.js"
|
|
3
|
+
import timeout, {TimeoutError} from "awaitery/build/timeout.js"
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Thrown when an advisory lock could not be acquired before `timeoutMs` elapsed.
|
|
@@ -67,9 +67,12 @@ export default class AdvisoryLockRunner {
|
|
|
67
67
|
|
|
68
68
|
/**
|
|
69
69
|
* Runs a callback after acquiring the advisory lock, waiting up to `timeoutMs`.
|
|
70
|
+
* When a `holdTimeoutMs` is set the callback receives a `TimeoutControl` from
|
|
71
|
+
* awaitery for cooperative cancellation (`control.check()`, `control.signal`,
|
|
72
|
+
* `control.timedOut`, `control.remaining()`).
|
|
70
73
|
* @template T
|
|
71
74
|
* @param {string} name - Lock name.
|
|
72
|
-
* @param {() => Promise<T>} callback - Callback to invoke while the lock is held.
|
|
75
|
+
* @param {(args?: {control: import("awaitery/build/timeout.js").TimeoutControl}) => Promise<T>} callback - Callback to invoke while the lock is held.
|
|
73
76
|
* @param {{timeoutMs?: number | null, holdTimeoutMs?: number | null}} [args] - Lock and hold timeout options.
|
|
74
77
|
* @returns {Promise<T>} - Resolves with the callback result.
|
|
75
78
|
*/
|
|
@@ -87,9 +90,11 @@ export default class AdvisoryLockRunner {
|
|
|
87
90
|
|
|
88
91
|
/**
|
|
89
92
|
* Runs a callback only if the advisory lock can be acquired immediately.
|
|
93
|
+
* When a `holdTimeoutMs` is set the callback receives a `TimeoutControl` from
|
|
94
|
+
* awaitery for cooperative cancellation.
|
|
90
95
|
* @template T
|
|
91
96
|
* @param {string} name - Lock name.
|
|
92
|
-
* @param {() => Promise<T>} callback - Callback to invoke while the lock is held.
|
|
97
|
+
* @param {(args?: {control: import("awaitery/build/timeout.js").TimeoutControl}) => Promise<T>} callback - Callback to invoke while the lock is held.
|
|
93
98
|
* @param {{holdTimeoutMs?: number | null}} [args] - Hold timeout options.
|
|
94
99
|
* @returns {Promise<T>} - Resolves with the callback result.
|
|
95
100
|
*/
|
|
@@ -108,7 +113,7 @@ export default class AdvisoryLockRunner {
|
|
|
108
113
|
/**
|
|
109
114
|
* Runs the lock holder callback and releases the lock from its owning connection.
|
|
110
115
|
* @template T
|
|
111
|
-
* @param {{callback: () => Promise<T>, connection: import("./drivers/base.js").default, holdTimeoutMs?: number | null, name: string}} args - Locked callback args.
|
|
116
|
+
* @param {{callback: (args?: {control: import("awaitery/build/timeout.js").TimeoutControl}) => Promise<T>, connection: import("./drivers/base.js").default, holdTimeoutMs?: number | null, name: string}} args - Locked callback args.
|
|
112
117
|
* @returns {Promise<T>} - Resolves with the callback result.
|
|
113
118
|
*/
|
|
114
119
|
async runLockedCallback({callback, connection, holdTimeoutMs, name}) {
|
|
@@ -160,9 +165,13 @@ export default class AdvisoryLockRunner {
|
|
|
160
165
|
* Runs `callback`, rejecting with `AdvisoryLockHoldTimeoutError` if it has
|
|
161
166
|
* not settled within `holdTimeoutMs`. The callback is not cancelled; callers
|
|
162
167
|
* use a dedicated advisory-lock connection so the lock can still be released.
|
|
168
|
+
*
|
|
169
|
+
* The callback receives a `TimeoutControl` from awaitery, enabling cooperative
|
|
170
|
+
* cancellation via `control.check()`, `control.signal`, `control.timedOut`,
|
|
171
|
+
* and `control.remaining()`.
|
|
163
172
|
* @template T
|
|
164
173
|
* @param {string} name - Lock name (for the error message).
|
|
165
|
-
* @param {() => Promise<T>} callback - Callback holding the lock.
|
|
174
|
+
* @param {(args?: {control: import("awaitery/build/timeout.js").TimeoutControl}) => Promise<T>} callback - Callback holding the lock.
|
|
166
175
|
* @param {number | null} [holdTimeoutMs] - Max hold time; falsy disables the timeout.
|
|
167
176
|
* @returns {Promise<T>} - Resolves with the callback result.
|
|
168
177
|
*/
|
|
@@ -174,15 +183,15 @@ export default class AdvisoryLockRunner {
|
|
|
174
183
|
let callbackSettled = false
|
|
175
184
|
|
|
176
185
|
try {
|
|
177
|
-
return await timeout({timeout: holdTimeoutMs}, async () => {
|
|
186
|
+
return await timeout({timeout: holdTimeoutMs}, async ({control}) => {
|
|
178
187
|
try {
|
|
179
|
-
return await callback()
|
|
188
|
+
return await callback({control})
|
|
180
189
|
} finally {
|
|
181
190
|
callbackSettled = true
|
|
182
191
|
}
|
|
183
192
|
})
|
|
184
193
|
} catch (error) {
|
|
185
|
-
if (!callbackSettled) {
|
|
194
|
+
if (!callbackSettled || error instanceof TimeoutError) {
|
|
186
195
|
throw new AdvisoryLockHoldTimeoutError(`Advisory lock ${JSON.stringify(name)} held longer than ${holdTimeoutMs}ms`, {name})
|
|
187
196
|
}
|
|
188
197
|
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import splitSqlStatements from "../utils/split-sql-statements.js"
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Loads a database structure SQL dump into a connection in one pass. Foreign keys are
|
|
7
|
+
* disabled around the load so tables can be created in any order, and the driver's
|
|
8
|
+
* native multi-statement `exec` is used when available (a single round-trip) instead
|
|
9
|
+
* of issuing every statement separately. This is much faster than materializing a
|
|
10
|
+
* schema table by table (e.g. cloning), so it is the preferred way to provision a
|
|
11
|
+
* tenant/test database from a structure dump.
|
|
12
|
+
*
|
|
13
|
+
* Used by the `db:schema:load` CLI command and reusable by any caller that needs to
|
|
14
|
+
* apply a structure dump to a specific connection.
|
|
15
|
+
*/
|
|
16
|
+
export default class StructureSqlLoader {
|
|
17
|
+
/**
|
|
18
|
+
* Loads `structureSql` into `db`.
|
|
19
|
+
* @param {object} args - Options object.
|
|
20
|
+
* @param {import("./drivers/base.js").default} args.db - Target database connection.
|
|
21
|
+
* @param {string} args.structureSql - Structure SQL to load.
|
|
22
|
+
* @returns {Promise<void>} - Resolves when the structure has been loaded.
|
|
23
|
+
*/
|
|
24
|
+
async load({db, structureSql}) {
|
|
25
|
+
const statements = splitSqlStatements(structureSql)
|
|
26
|
+
|
|
27
|
+
if (statements.length == 0) return
|
|
28
|
+
|
|
29
|
+
const executableConnection = this.executableConnection(db)
|
|
30
|
+
|
|
31
|
+
await db.disableForeignKeys()
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
if (executableConnection) {
|
|
35
|
+
await executableConnection.exec(structureSql)
|
|
36
|
+
} else {
|
|
37
|
+
for (const statement of statements) {
|
|
38
|
+
await db.query(statement)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
} finally {
|
|
42
|
+
await db.enableForeignKeys()
|
|
43
|
+
|
|
44
|
+
// The native `exec` path mutates the schema outside `Base#query`, so the
|
|
45
|
+
// usual post-DDL schema-cache invalidation never runs. Clear it here so a
|
|
46
|
+
// caller that read schema metadata before provisioning (e.g. an empty table
|
|
47
|
+
// list) does not keep seeing the pre-load schema afterwards. Harmless for the
|
|
48
|
+
// per-statement path, which already invalidates as each DDL statement runs.
|
|
49
|
+
db.clearSchemaCache()
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Returns the underlying connection when it exposes a native multi-statement
|
|
55
|
+
* `exec` (a single round-trip for the whole dump), otherwise undefined so the
|
|
56
|
+
* caller falls back to per-statement execution.
|
|
57
|
+
* @param {import("./drivers/base.js").default} db - Database connection.
|
|
58
|
+
* @returns {{exec: (sql: string) => Promise<?>} | undefined} - Connection with exec support.
|
|
59
|
+
*/
|
|
60
|
+
executableConnection(db) {
|
|
61
|
+
const dbWithConnection = /** @type {import("./drivers/base.js").default & {connection?: ?}} */ (db)
|
|
62
|
+
const connection = dbWithConnection.connection
|
|
63
|
+
|
|
64
|
+
if (connection && typeof connection == "object" && "exec" in connection && typeof connection.exec == "function") {
|
|
65
|
+
return /** @type {{exec: (sql: string) => Promise<?>}} */ (connection)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import BaseCommand from "../../../../../../cli/base-command.js"
|
|
2
2
|
import fs from "fs/promises"
|
|
3
3
|
import path from "path"
|
|
4
|
-
import
|
|
4
|
+
import StructureSqlLoader from "../../../../../../database/structure-sql-loader.js"
|
|
5
5
|
|
|
6
6
|
/** Node CLI command for loading DB structure SQL files. */
|
|
7
7
|
export default class DbSchemaLoad extends BaseCommand {
|
|
@@ -11,57 +11,15 @@ export default class DbSchemaLoad extends BaseCommand {
|
|
|
11
11
|
async execute() {
|
|
12
12
|
await this.getConfiguration().ensureConnections({name: "DB schema load"}, async (dbs) => {
|
|
13
13
|
const dbDir = path.join(this.directory(), "db")
|
|
14
|
+
const loader = new StructureSqlLoader()
|
|
14
15
|
|
|
15
16
|
for (const identifier of Object.keys(dbs)) {
|
|
16
17
|
const db = dbs[identifier]
|
|
17
18
|
const structureFilePath = path.join(dbDir, `structure-${identifier}.sql`)
|
|
18
19
|
const structureSql = await fs.readFile(structureFilePath, "utf8")
|
|
19
20
|
|
|
20
|
-
await
|
|
21
|
+
await loader.load({db, structureSql})
|
|
21
22
|
}
|
|
22
23
|
})
|
|
23
24
|
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Runs load structure sql.
|
|
27
|
-
* @param {object} args - Options object.
|
|
28
|
-
* @param {import("../../../../../../database/drivers/base.js").default} args.db - Database connection.
|
|
29
|
-
* @param {string} args.structureSql - Structure SQL to load.
|
|
30
|
-
* @returns {Promise<void>} - Resolves when complete.
|
|
31
|
-
*/
|
|
32
|
-
async loadStructureSql({db, structureSql}) {
|
|
33
|
-
const statements = splitSqlStatements(structureSql)
|
|
34
|
-
|
|
35
|
-
if (statements.length == 0) return
|
|
36
|
-
|
|
37
|
-
const executableConnection = this.executableConnection(db)
|
|
38
|
-
|
|
39
|
-
await db.disableForeignKeys()
|
|
40
|
-
|
|
41
|
-
try {
|
|
42
|
-
if (executableConnection) {
|
|
43
|
-
await executableConnection.exec(structureSql)
|
|
44
|
-
} else {
|
|
45
|
-
for (const statement of statements) {
|
|
46
|
-
await db.query(statement)
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
} finally {
|
|
50
|
-
await db.enableForeignKeys()
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Runs executable connection.
|
|
56
|
-
* @param {import("../../../../../../database/drivers/base.js").default} db - Database connection.
|
|
57
|
-
* @returns {{exec: (sql: string) => Promise<?>} | undefined} - Connection with exec support.
|
|
58
|
-
*/
|
|
59
|
-
executableConnection(db) {
|
|
60
|
-
const dbWithConnection = /** @type {import("../../../../../../database/drivers/base.js").default & {connection?: ?}} */ (db)
|
|
61
|
-
const connection = dbWithConnection.connection
|
|
62
|
-
|
|
63
|
-
if (connection && typeof connection == "object" && "exec" in connection && typeof connection.exec == "function") {
|
|
64
|
-
return /** @type {{exec: (sql: string) => Promise<?>}} */ (connection)
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
25
|
}
|
package/src/testing/test.js
CHANGED
|
@@ -4,6 +4,7 @@ import path from "path"
|
|
|
4
4
|
import {fileURLToPath} from "url"
|
|
5
5
|
import EventEmitter from "../utils/event-emitter.js"
|
|
6
6
|
import Expect from "./expect.js"
|
|
7
|
+
import waitForEvent from "./wait-for-event.js"
|
|
7
8
|
import {arrayContaining, objectContaining} from "./expect-utils.js"
|
|
8
9
|
|
|
9
10
|
/**
|
|
@@ -351,7 +352,7 @@ globalThis.fit = fit
|
|
|
351
352
|
globalThis.testEvents = testEvents
|
|
352
353
|
globalThis.configureTests = configureTests
|
|
353
354
|
|
|
354
|
-
export {afterAll, afterEach, beforeAll, beforeEach, configureTests, describe, expect, fit, it, arrayContaining, objectContaining, testConfig, testEvents, tests}
|
|
355
|
+
export {afterAll, afterEach, beforeAll, beforeEach, configureTests, describe, expect, fit, it, arrayContaining, objectContaining, testConfig, testEvents, tests, waitForEvent}
|
|
355
356
|
/**
|
|
356
357
|
* VelociousTestConfig type.
|
|
357
358
|
* @typedef {object} VelociousTestConfig
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @typedef {object} WaitForEventOptions
|
|
5
|
+
* @property {number} [timeoutMs] Timeout in milliseconds (default: 5000).
|
|
6
|
+
* @property {(...args: Array<?>) => boolean} [filter] Only resolve when this predicate returns true for the emitted arguments.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @typedef {object} EventEmitterLike
|
|
11
|
+
* @property {(event: string, listener: (...args: Array<?>) => void) => void} on Registers a listener.
|
|
12
|
+
* @property {(event: string, listener: (...args: Array<?>) => void) => void} off Removes a listener.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Resolves the moment `eventName` fires on `emitter` — optionally only when `filter`
|
|
17
|
+
* matches the emitted arguments — instead of sleeping a fixed duration. Rejects with a
|
|
18
|
+
* timeout error if the event does not fire within `timeoutMs`. The listener is always
|
|
19
|
+
* removed (on resolve and on timeout).
|
|
20
|
+
*
|
|
21
|
+
* Use this to await a real signal (a background job finishing, a model update, a
|
|
22
|
+
* websocket message) rather than guessing a delay. To poll an arbitrary condition
|
|
23
|
+
* instead of a discrete event, use awaitery's `waitFor`.
|
|
24
|
+
* @param {EventEmitterLike} emitter - Event emitter exposing `on`/`off` (Node EventEmitter, eventemitter3, velocious `testEvents`, ...).
|
|
25
|
+
* @param {string} eventName - The event to wait for.
|
|
26
|
+
* @param {WaitForEventOptions} [options] - Options.
|
|
27
|
+
* @returns {Promise<?>} - Resolves with the single emitted argument, an array when the event emits multiple, or undefined when it emits none.
|
|
28
|
+
*/
|
|
29
|
+
export default function waitForEvent(emitter, eventName, options = {}) {
|
|
30
|
+
const {timeoutMs = 5000, filter} = options
|
|
31
|
+
|
|
32
|
+
return new Promise((resolve, reject) => {
|
|
33
|
+
/** @type {ReturnType<typeof setTimeout>} */
|
|
34
|
+
let timer
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Resolves the wait when a matching event fires, removing itself first. A filter
|
|
38
|
+
* that throws (e.g. it assumes an event shape an intermediate emission doesn't
|
|
39
|
+
* have) rejects immediately rather than leaving the waiter pending until timeout.
|
|
40
|
+
* @param {...?} args - Emitted arguments.
|
|
41
|
+
* @returns {void}
|
|
42
|
+
*/
|
|
43
|
+
const listener = (...args) => {
|
|
44
|
+
if (filter) {
|
|
45
|
+
let matched
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
matched = filter(...args)
|
|
49
|
+
} catch (error) {
|
|
50
|
+
clearTimeout(timer)
|
|
51
|
+
emitter.off(eventName, listener)
|
|
52
|
+
reject(error)
|
|
53
|
+
|
|
54
|
+
return
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (!matched) return
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
clearTimeout(timer)
|
|
61
|
+
emitter.off(eventName, listener)
|
|
62
|
+
resolve(args.length > 1 ? args : args[0])
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
timer = setTimeout(() => {
|
|
66
|
+
emitter.off(eventName, listener)
|
|
67
|
+
reject(new Error(`Timed out after ${timeoutMs}ms waiting for event ${JSON.stringify(eventName)}`))
|
|
68
|
+
}, timeoutMs)
|
|
69
|
+
|
|
70
|
+
emitter.on(eventName, listener)
|
|
71
|
+
})
|
|
72
|
+
}
|