@plakboek/db 0.4.0 → 0.5.1
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 +8 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +16 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -54,12 +54,20 @@ const result = await runMigrations({
|
|
|
54
54
|
connectionString: process.env.DATABASE_URL!,
|
|
55
55
|
lockWaitMs: 60_000, // optional, default 60000
|
|
56
56
|
lockPollIntervalMs: 250, // optional, default 250
|
|
57
|
+
onLockWait: () => console.log('Waiting for the migration lock'), // optional
|
|
58
|
+
onMigrationStart: (name) => console.log(`Applying ${name}`), // optional
|
|
57
59
|
});
|
|
58
60
|
|
|
59
61
|
// result.applied: names of migrations applied by this call
|
|
60
62
|
// result.alreadyApplied: names that were already applied before this call
|
|
61
63
|
```
|
|
62
64
|
|
|
65
|
+
`onLockWait` fires once, the first time another migrator holds the lock, however
|
|
66
|
+
long the wait lasts. `onMigrationStart(name)` fires once per pending migration,
|
|
67
|
+
just before it applies, and never for an already-applied one. Hooks receive
|
|
68
|
+
migration names only. A hook that throws is ignored: it never aborts, rolls back
|
|
69
|
+
or reorders the run.
|
|
70
|
+
|
|
63
71
|
`runMigrations` is safe to call from every instance of a rolling deploy: it
|
|
64
72
|
opens its own connection, holds the advisory lock for the duration of the
|
|
65
73
|
run, and applies this package's statically registered `MIGRATIONS` in
|
package/dist/index.d.ts
CHANGED
|
@@ -24,6 +24,9 @@ export declare class MigrationLockTimeoutError extends Error {
|
|
|
24
24
|
type LockOptions = {
|
|
25
25
|
readonly waitMs?: number;
|
|
26
26
|
readonly pollIntervalMs?: number;
|
|
27
|
+
/** Called once, the first time the lock is not immediately available. A
|
|
28
|
+
* hook that throws is ignored: it never aborts the wait. */
|
|
29
|
+
readonly onWait?: () => void;
|
|
27
30
|
};
|
|
28
31
|
//#endregion
|
|
29
32
|
//#region src/migrate.d.ts
|
|
@@ -75,6 +78,13 @@ type RunMigrationsOptions = {
|
|
|
75
78
|
readonly connectionString: string;
|
|
76
79
|
readonly lockWaitMs?: number;
|
|
77
80
|
readonly lockPollIntervalMs?: number;
|
|
81
|
+
/** Called once, the first time the migration lock is held by another
|
|
82
|
+
* migrator. A hook that throws is ignored: it never aborts the run. */
|
|
83
|
+
readonly onLockWait?: () => void;
|
|
84
|
+
/** Called with a pending migration's name just before it applies; never for
|
|
85
|
+
* an already-applied migration. A hook that throws is ignored: it never
|
|
86
|
+
* aborts, rolls back or reorders the run. */
|
|
87
|
+
readonly onMigrationStart?: (name: string) => void;
|
|
78
88
|
};
|
|
79
89
|
type RunMigrationsResult = {
|
|
80
90
|
readonly applied: readonly string[];
|
package/dist/index.js
CHANGED
|
@@ -64,10 +64,17 @@ async function tryAcquire(client) {
|
|
|
64
64
|
* is a session setting: it would persist on this client for every statement
|
|
65
65
|
* run afterwards, including the migrations `withMigrationLock` wraps.
|
|
66
66
|
*/
|
|
67
|
-
async function acquire(client, waitMs, pollIntervalMs) {
|
|
67
|
+
async function acquire(client, waitMs, pollIntervalMs, onWait) {
|
|
68
68
|
const start = Date.now();
|
|
69
|
+
let reported = false;
|
|
69
70
|
for (;;) {
|
|
70
71
|
if (await tryAcquire(client)) return;
|
|
72
|
+
if (!reported) {
|
|
73
|
+
reported = true;
|
|
74
|
+
try {
|
|
75
|
+
onWait?.();
|
|
76
|
+
} catch {}
|
|
77
|
+
}
|
|
71
78
|
const elapsedMs = Date.now() - start;
|
|
72
79
|
if (elapsedMs >= waitMs) throw new MigrationLockTimeoutError(elapsedMs);
|
|
73
80
|
await sleep(pollIntervalMs);
|
|
@@ -112,7 +119,7 @@ async function runGuarded(fn) {
|
|
|
112
119
|
* `fn`'s error is what gets thrown regardless of the unlock outcome.
|
|
113
120
|
*/
|
|
114
121
|
async function withMigrationLock(client, fn, options) {
|
|
115
|
-
await acquire(client, options?.waitMs ?? DEFAULT_WAIT_MS, options?.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS);
|
|
122
|
+
await acquire(client, options?.waitMs ?? DEFAULT_WAIT_MS, options?.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS, options?.onWait);
|
|
116
123
|
const outcome = await runGuarded(fn);
|
|
117
124
|
const unlockOutcome = await runGuarded(() => client.query("SELECT pg_advisory_unlock($1::bigint) AS unlocked", [MIGRATION_LOCK_KEY]));
|
|
118
125
|
if (outcome.ok) {
|
|
@@ -841,7 +848,7 @@ async function applyNonTransactional(client, migration) {
|
|
|
841
848
|
* registry order. Does not acquire any lock -- callers that need
|
|
842
849
|
* concurrency safety use `migrateWithRegistry`. Exported directly so tests
|
|
843
850
|
* can prove the unlocked case has no safety (the negative control). */
|
|
844
|
-
async function applyPendingMigrations(client, migrations) {
|
|
851
|
+
async function applyPendingMigrations(client, migrations, onMigrationStart) {
|
|
845
852
|
await ensureBookkeepingTable(client);
|
|
846
853
|
const appliedRows = await readAppliedRows(client);
|
|
847
854
|
validateAppliedHistory(appliedRows, migrations);
|
|
@@ -850,6 +857,9 @@ async function applyPendingMigrations(client, migrations) {
|
|
|
850
857
|
const pending = migrations.filter((migration) => !alreadyAppliedSet.has(migration.name));
|
|
851
858
|
const applied = [];
|
|
852
859
|
for (const migration of pending) {
|
|
860
|
+
try {
|
|
861
|
+
onMigrationStart?.(migration.name);
|
|
862
|
+
} catch {}
|
|
853
863
|
if (migration.transactional) await applyTransactional(client, migration);
|
|
854
864
|
else await applyNonTransactional(client, migration);
|
|
855
865
|
applied.push(migration.name);
|
|
@@ -867,9 +877,10 @@ async function migrateWithRegistry(options) {
|
|
|
867
877
|
const client = new Client({ connectionString: options.connectionString });
|
|
868
878
|
await client.connect();
|
|
869
879
|
try {
|
|
870
|
-
return await withMigrationLock(client, () => applyPendingMigrations(client, options.migrations), {
|
|
880
|
+
return await withMigrationLock(client, () => applyPendingMigrations(client, options.migrations, options.onMigrationStart), {
|
|
871
881
|
waitMs: options.lockWaitMs,
|
|
872
|
-
pollIntervalMs: options.lockPollIntervalMs
|
|
882
|
+
pollIntervalMs: options.lockPollIntervalMs,
|
|
883
|
+
onWait: options.onLockWait
|
|
873
884
|
});
|
|
874
885
|
} finally {
|
|
875
886
|
await client.end();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@plakboek/db",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "Postgres connection factory and migration-lock primitives for Plakboek CMS installations",
|
|
5
5
|
"homepage": "https://github.com/flovan/plakboek/tree/main/packages/db#readme",
|
|
6
6
|
"bugs": {
|