@bobfrankston/mailx-store 0.1.49 → 0.1.51
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 +20 -0
- package/db.js +73 -1
- package/package.json +3 -3
package/db.d.ts
CHANGED
|
@@ -400,6 +400,15 @@ export declare class MailxDB {
|
|
|
400
400
|
} | null;
|
|
401
401
|
/** Clear a UID's prefetch-failure record after a successful fetch. */
|
|
402
402
|
clearPrefetchFailure(accountId: string, folderId: number, uid: number): void;
|
|
403
|
+
/** Wipe ALL prefetch-failure backoffs. Called once per boot: the backoff is
|
|
404
|
+
* a within-SESSION optimisation (it stops a poison body being re-fetched
|
|
405
|
+
* every 60s cycle), so a fresh session should give every body another try —
|
|
406
|
+
* the network may have recovered, and crucially a body backed off by a
|
|
407
|
+
* transient cause (a two-writer DB lock, a Dovecot 300s timeout) shouldn't
|
|
408
|
+
* stay suppressed for up to 12h (Bob 2026-06-14: "lots of predownloading
|
|
409
|
+
* not happening"). Genuine poison bodies simply re-fail once and re-back-off
|
|
410
|
+
* for the session — no per-cycle loop. */
|
|
411
|
+
clearAllPrefetchFailures(): number;
|
|
403
412
|
/** Highest server UID we've seen in this folder. After the
|
|
404
413
|
* message_folders refactor, this reads from the membership table
|
|
405
414
|
* rather than messages.uid — a server-side move from this folder
|
|
@@ -470,6 +479,17 @@ export declare class MailxDB {
|
|
|
470
479
|
* already open this just runs `fn` (its writes join the open txn);
|
|
471
480
|
* otherwise it owns a fresh BEGIN/COMMIT. `fn` MUST be synchronous so it
|
|
472
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;
|
|
473
493
|
runInTxn<T>(fn: () => T): T;
|
|
474
494
|
/** Record an address used in sent mail */
|
|
475
495
|
recordSentAddress(name: string, email: string): void;
|
package/db.js
CHANGED
|
@@ -419,7 +419,13 @@ export class MailxDB {
|
|
|
419
419
|
// DDL/backfills concurrently would race on the WAL write lock.
|
|
420
420
|
this.db.exec("PRAGMA journal_mode = WAL");
|
|
421
421
|
this.db.exec("PRAGMA foreign_keys = ON");
|
|
422
|
-
|
|
422
|
+
// 10s (vs the main thread's 5s): this is the BACKGROUND writer (sync,
|
|
423
|
+
// prefetch); waiting a bit longer for the foreground's lock is fine.
|
|
424
|
+
// NOT higher because busy_timeout BLOCKS this thread synchronously
|
|
425
|
+
// (it can't forward sync events while parked) — for longer holds the
|
|
426
|
+
// prefetch write path uses an ASYNC retry that yields between
|
|
427
|
+
// attempts instead. Fixes prefetch "database is locked" (Bob 2026-06-14).
|
|
428
|
+
this.db.exec("PRAGMA busy_timeout = 10000");
|
|
423
429
|
return;
|
|
424
430
|
}
|
|
425
431
|
if (this.readOnly) {
|
|
@@ -446,6 +452,23 @@ export class MailxDB {
|
|
|
446
452
|
}
|
|
447
453
|
this.db.exec("PRAGMA journal_mode = WAL");
|
|
448
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
|
+
}
|
|
449
472
|
// Multi-writer foundation for the sync-worker migration: once sync runs
|
|
450
473
|
// on its own thread with its own write connection, it and the main
|
|
451
474
|
// thread are two writers on one WAL file. WAL serializes writers, so a
|
|
@@ -569,6 +592,15 @@ export class MailxDB {
|
|
|
569
592
|
// this column landed. One UPDATE + an id roundtrip per row — cheap
|
|
570
593
|
// at our row counts, runs once per DB upgrade.
|
|
571
594
|
this.backfillUuids();
|
|
595
|
+
// Per-boot: clear prefetch-failure backoffs so transiently-failed bodies
|
|
596
|
+
// (two-writer DB lock, Dovecot timeout) get a fresh try this session
|
|
597
|
+
// instead of staying suppressed for up to 12h. See clearAllPrefetchFailures.
|
|
598
|
+
try {
|
|
599
|
+
const cleared = this.clearAllPrefetchFailures();
|
|
600
|
+
if (cleared > 0)
|
|
601
|
+
console.log(` [db] cleared ${cleared} prefetch-failure backoff(s) for a fresh session`);
|
|
602
|
+
}
|
|
603
|
+
catch { /* non-fatal */ }
|
|
572
604
|
// One-shot cleanup: the retired insertOptimisticSentRow path wrote
|
|
573
605
|
// synthetic-negative-UID rows into Sent. Those rows are stale (the
|
|
574
606
|
// real server-synced row eventually appears with a positive UID),
|
|
@@ -2235,6 +2267,22 @@ export class MailxDB {
|
|
|
2235
2267
|
clearPrefetchFailure(accountId, folderId, uid) {
|
|
2236
2268
|
this.db.prepare("DELETE FROM prefetch_failures WHERE account_id = ? AND folder_id = ? AND uid = ?").run(accountId, folderId, uid);
|
|
2237
2269
|
}
|
|
2270
|
+
/** Wipe ALL prefetch-failure backoffs. Called once per boot: the backoff is
|
|
2271
|
+
* a within-SESSION optimisation (it stops a poison body being re-fetched
|
|
2272
|
+
* every 60s cycle), so a fresh session should give every body another try —
|
|
2273
|
+
* the network may have recovered, and crucially a body backed off by a
|
|
2274
|
+
* transient cause (a two-writer DB lock, a Dovecot 300s timeout) shouldn't
|
|
2275
|
+
* stay suppressed for up to 12h (Bob 2026-06-14: "lots of predownloading
|
|
2276
|
+
* not happening"). Genuine poison bodies simply re-fail once and re-back-off
|
|
2277
|
+
* for the session — no per-cycle loop. */
|
|
2278
|
+
clearAllPrefetchFailures() {
|
|
2279
|
+
try {
|
|
2280
|
+
return this.db.prepare("DELETE FROM prefetch_failures").run().changes;
|
|
2281
|
+
}
|
|
2282
|
+
catch {
|
|
2283
|
+
return 0;
|
|
2284
|
+
}
|
|
2285
|
+
}
|
|
2238
2286
|
/** Highest server UID we've seen in this folder. After the
|
|
2239
2287
|
* message_folders refactor, this reads from the membership table
|
|
2240
2288
|
* rather than messages.uid — a server-side move from this folder
|
|
@@ -2363,13 +2411,37 @@ export class MailxDB {
|
|
|
2363
2411
|
* already open this just runs `fn` (its writes join the open txn);
|
|
2364
2412
|
* otherwise it owns a fresh BEGIN/COMMIT. `fn` MUST be synchronous so it
|
|
2365
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
|
+
}
|
|
2366
2428
|
runInTxn(fn) {
|
|
2367
2429
|
if (this.db.isTransaction)
|
|
2368
2430
|
return fn();
|
|
2369
2431
|
this.db.exec("BEGIN");
|
|
2432
|
+
const _t0 = Date.now();
|
|
2370
2433
|
try {
|
|
2371
2434
|
const r = fn();
|
|
2372
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
|
+
}
|
|
2373
2445
|
return r;
|
|
2374
2446
|
}
|
|
2375
2447
|
catch (e) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bobfrankston/mailx-store",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.51",
|
|
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
|
}
|