@plakboek/db 0.4.0 → 0.5.0

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 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.4.0",
3
+ "version": "0.5.0",
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": {