@bobfrankston/mailx-store 0.1.50 → 0.1.52

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.
Files changed (3) hide show
  1. package/db.d.ts +11 -0
  2. package/db.js +48 -1
  3. package/package.json +3 -3
package/db.d.ts CHANGED
@@ -479,6 +479,17 @@ export declare class MailxDB {
479
479
  * already open this just runs `fn` (its writes join the open txn);
480
480
  * otherwise it owns a fresh BEGIN/COMMIT. `fn` MUST be synchronous so it
481
481
  * can't span an await and leave a transaction open across a yield. */
482
+ /** Run a WAL checkpoint to bound the WAL file size. PASSIVE never blocks
483
+ * on readers — it reclaims frames older than the oldest live snapshot and
484
+ * returns immediately otherwise. Drive it from ONE connection (main) on a
485
+ * timer so the WAL can't balloon while three connections share the file.
486
+ * Returns {busy, log, checkpointed} (log = frames in WAL, checkpointed =
487
+ * frames moved into the db) or null on error. */
488
+ checkpoint(mode?: "PASSIVE" | "FULL" | "RESTART" | "TRUNCATE"): {
489
+ busy: number;
490
+ log: number;
491
+ checkpointed: number;
492
+ } | null;
482
493
  runInTxn<T>(fn: () => T): T;
483
494
  /** Record an address used in sent mail */
484
495
  recordSentAddress(name: string, email: string): void;
package/db.js CHANGED
@@ -452,6 +452,23 @@ export class MailxDB {
452
452
  }
453
453
  this.db.exec("PRAGMA journal_mode = WAL");
454
454
  this.db.exec("PRAGMA foreign_keys = ON");
455
+ // WAL hygiene. With THREE connections (main writer, sync-worker writer,
456
+ // read-worker reader) the WAL was never getting checkpointed — failed
457
+ // writes (two-writer contention) never commit, so the auto-checkpoint
458
+ // that normally fires on commit never ran, and the WAL ballooned to
459
+ // 71 MB (Bob 2026-06-14: every read/write crawled, `database is locked`
460
+ // everywhere, a bulk move took 51s and timed out). A bloated WAL is
461
+ // self-reinforcing: bigger WAL → slower writes → more lock failures →
462
+ // fewer commits → no checkpoint. Boot is the one moment main is the
463
+ // SOLE connection (read/sync workers spawn later), so TRUNCATE here
464
+ // resets the file to empty; `checkpoint()` on a timer keeps it small.
465
+ try {
466
+ const r = this.db.prepare("PRAGMA wal_checkpoint(TRUNCATE)").get();
467
+ console.log(` [db] boot WAL checkpoint: busy=${r?.busy} log=${r?.log} checkpointed=${r?.checkpointed}`);
468
+ }
469
+ catch (e) {
470
+ console.error(` [db] boot WAL checkpoint failed: ${e?.message || e}`);
471
+ }
455
472
  // Multi-writer foundation for the sync-worker migration: once sync runs
456
473
  // on its own thread with its own write connection, it and the main
457
474
  // thread are two writers on one WAL file. WAL serializes writers, so a
@@ -2394,13 +2411,37 @@ export class MailxDB {
2394
2411
  * already open this just runs `fn` (its writes join the open txn);
2395
2412
  * otherwise it owns a fresh BEGIN/COMMIT. `fn` MUST be synchronous so it
2396
2413
  * can't span an await and leave a transaction open across a yield. */
2414
+ /** Run a WAL checkpoint to bound the WAL file size. PASSIVE never blocks
2415
+ * on readers — it reclaims frames older than the oldest live snapshot and
2416
+ * returns immediately otherwise. Drive it from ONE connection (main) on a
2417
+ * timer so the WAL can't balloon while three connections share the file.
2418
+ * Returns {busy, log, checkpointed} (log = frames in WAL, checkpointed =
2419
+ * frames moved into the db) or null on error. */
2420
+ checkpoint(mode = "PASSIVE") {
2421
+ try {
2422
+ return this.db.prepare(`PRAGMA wal_checkpoint(${mode})`).get();
2423
+ }
2424
+ catch {
2425
+ return null;
2426
+ }
2427
+ }
2397
2428
  runInTxn(fn) {
2398
2429
  if (this.db.isTransaction)
2399
2430
  return fn();
2400
2431
  this.db.exec("BEGIN");
2432
+ const _t0 = Date.now();
2401
2433
  try {
2402
2434
  const r = fn();
2403
2435
  this.db.exec("COMMIT");
2436
+ // Diagnostic: a write transaction held >1s on the MAIN connection is
2437
+ // what locks out the sync-worker's prefetch write (Bob 2026-06-14
2438
+ // "find the long main-thread transaction"). Log the call site so the
2439
+ // next occurrence names itself instead of needing code archaeology.
2440
+ const _ms = Date.now() - _t0;
2441
+ if (_ms > 1000) {
2442
+ const where = (new Error().stack || "").split("\n")[2]?.trim() || "?";
2443
+ console.warn(` [db] SLOW write txn ${_ms}ms (readOnly=${this.readOnly}) at ${where}`);
2444
+ }
2404
2445
  return r;
2405
2446
  }
2406
2447
  catch (e) {
@@ -2620,7 +2661,13 @@ export class MailxDB {
2620
2661
  // straddling an `await` — the wrapper is nesting-safe (savepoints)
2621
2662
  // so it can't collide with the sync backfill's own transactions
2622
2663
  // when the two interleave across the yield below.
2623
- const WRITE_CHUNK = 500;
2664
+ // 100, not 500: each chunk is one held write transaction, and the
2665
+ // slow-txn logger caught these running 2+ SECONDS apiece under WAL
2666
+ // pressure — long enough to lock out the sync worker's storeMessages
2667
+ // ("database is locked"). Smaller chunks = shorter lock holds + a
2668
+ // yield (below) after each, giving the worker write windows (Bob
2669
+ // 2026-06-16, live debug).
2670
+ const WRITE_CHUNK = 100;
2624
2671
  const entries = [...agg.entries()];
2625
2672
  for (let i = 0; i < entries.length; i += WRITE_CHUNK) {
2626
2673
  const slice = entries.slice(i, i + WRITE_CHUNK);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/mailx-store",
3
- "version": "0.1.50",
3
+ "version": "0.1.52",
4
4
  "type": "module",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -10,7 +10,7 @@
10
10
  "license": "ISC",
11
11
  "dependencies": {
12
12
  "@bobfrankston/mailx-types": "^0.1.19",
13
- "@bobfrankston/mailx-settings": "^0.1.27",
13
+ "@bobfrankston/mailx-settings": "^0.1.28",
14
14
  "@bobfrankston/mailx-bus": "^0.1.2",
15
15
  "mailparser": "^3.7.2"
16
16
  },
@@ -30,7 +30,7 @@
30
30
  ".transformedSnapshot": {
31
31
  "dependencies": {
32
32
  "@bobfrankston/mailx-types": "^0.1.19",
33
- "@bobfrankston/mailx-settings": "^0.1.27",
33
+ "@bobfrankston/mailx-settings": "^0.1.28",
34
34
  "@bobfrankston/mailx-bus": "^0.1.2",
35
35
  "mailparser": "^3.7.2"
36
36
  }