@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.
- package/db.d.ts +11 -0
- package/db.js +48 -1
- 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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
33
|
+
"@bobfrankston/mailx-settings": "^0.1.28",
|
|
34
34
|
"@bobfrankston/mailx-bus": "^0.1.2",
|
|
35
35
|
"mailparser": "^3.7.2"
|
|
36
36
|
}
|