@bobfrankston/rmfmail 1.2.9 → 1.2.10

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.
@@ -442,6 +442,20 @@ export class MailxDB {
442
442
  }
443
443
  this.db.exec("PRAGMA journal_mode = WAL");
444
444
  this.db.exec("PRAGMA foreign_keys = ON");
445
+ // WAL hygiene. With THREE connections (main writer, sync-worker writer,
446
+ // read-worker reader) the WAL was never getting checkpointed — failed
447
+ // writes (two-writer contention) never commit, so the auto-checkpoint
448
+ // that normally fires on commit never ran, and the WAL ballooned to
449
+ // 71 MB (Bob 2026-06-14: every read/write crawled, `database is locked`
450
+ // everywhere, a bulk move took 51s and timed out). A bloated WAL is
451
+ // self-reinforcing: bigger WAL → slower writes → more lock failures →
452
+ // fewer commits → no checkpoint. Boot is the one moment main is the
453
+ // SOLE connection (read/sync workers spawn later), so TRUNCATE here
454
+ // resets the file to empty; `checkpoint()` on a timer keeps it small.
455
+ try {
456
+ const r = this.db.prepare("PRAGMA wal_checkpoint(TRUNCATE)").get() as any;
457
+ console.log(` [db] boot WAL checkpoint: busy=${r?.busy} log=${r?.log} checkpointed=${r?.checkpointed}`);
458
+ } catch (e: any) { console.error(` [db] boot WAL checkpoint failed: ${e?.message || e}`); }
445
459
  // Multi-writer foundation for the sync-worker migration: once sync runs
446
460
  // on its own thread with its own write connection, it and the main
447
461
  // thread are two writers on one WAL file. WAL serializes writers, so a
@@ -2644,6 +2658,17 @@ export class MailxDB {
2644
2658
  * already open this just runs `fn` (its writes join the open txn);
2645
2659
  * otherwise it owns a fresh BEGIN/COMMIT. `fn` MUST be synchronous so it
2646
2660
  * can't span an await and leave a transaction open across a yield. */
2661
+ /** Run a WAL checkpoint to bound the WAL file size. PASSIVE never blocks
2662
+ * on readers — it reclaims frames older than the oldest live snapshot and
2663
+ * returns immediately otherwise. Drive it from ONE connection (main) on a
2664
+ * timer so the WAL can't balloon while three connections share the file.
2665
+ * Returns {busy, log, checkpointed} (log = frames in WAL, checkpointed =
2666
+ * frames moved into the db) or null on error. */
2667
+ checkpoint(mode: "PASSIVE" | "FULL" | "RESTART" | "TRUNCATE" = "PASSIVE"): { busy: number; log: number; checkpointed: number } | null {
2668
+ try { return this.db.prepare(`PRAGMA wal_checkpoint(${mode})`).get() as any; }
2669
+ catch { return null; }
2670
+ }
2671
+
2647
2672
  runInTxn<T>(fn: () => T): T {
2648
2673
  if (this.db.isTransaction) return fn();
2649
2674
  this.db.exec("BEGIN");