@bobfrankston/mailx-store 0.1.47 → 0.1.49
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 +8 -1
- package/db.js +54 -1
- package/package.json +1 -1
package/db.d.ts
CHANGED
|
@@ -9,7 +9,14 @@ import type { MessageEnvelope, Folder, EmailAddress, PagedResult, MessageQuery }
|
|
|
9
9
|
export declare function setContactsDenyPatterns(patterns: string[]): void;
|
|
10
10
|
export declare class MailxDB {
|
|
11
11
|
private db;
|
|
12
|
-
|
|
12
|
+
/** True for the DB read-worker's connection. Read-only handles never run
|
|
13
|
+
* DDL / migrations / backfills (the main writer already did, before this
|
|
14
|
+
* handle opened) and reject any write at the SQLite level. */
|
|
15
|
+
readonly readOnly: boolean;
|
|
16
|
+
constructor(dbDir: string, opts?: {
|
|
17
|
+
readOnly?: boolean;
|
|
18
|
+
skipMigrations?: boolean;
|
|
19
|
+
});
|
|
13
20
|
/** Fail loud + early if expected columns are missing. Cheap (PRAGMA only
|
|
14
21
|
* runs at startup). The user-facing message names the recovery command. */
|
|
15
22
|
private verifySchema;
|
package/db.js
CHANGED
|
@@ -403,12 +403,56 @@ const SCHEMA = `
|
|
|
403
403
|
`;
|
|
404
404
|
export class MailxDB {
|
|
405
405
|
db;
|
|
406
|
-
|
|
406
|
+
/** True for the DB read-worker's connection. Read-only handles never run
|
|
407
|
+
* DDL / migrations / backfills (the main writer already did, before this
|
|
408
|
+
* handle opened) and reject any write at the SQLite level. */
|
|
409
|
+
readOnly;
|
|
410
|
+
constructor(dbDir, opts = {}) {
|
|
407
411
|
fs.mkdirSync(dbDir, { recursive: true });
|
|
408
412
|
const dbPath = path.join(dbDir, "mailx.db");
|
|
413
|
+
this.readOnly = !!opts.readOnly;
|
|
409
414
|
this.db = new DatabaseSync(dbPath);
|
|
415
|
+
if (opts.skipMigrations && !opts.readOnly) {
|
|
416
|
+
// Write-capable connection that does NOT run schema/migrations —
|
|
417
|
+
// used by the sync worker, which opens its own writer AFTER the
|
|
418
|
+
// main thread has already built the schema. Two threads running the
|
|
419
|
+
// DDL/backfills concurrently would race on the WAL write lock.
|
|
420
|
+
this.db.exec("PRAGMA journal_mode = WAL");
|
|
421
|
+
this.db.exec("PRAGMA foreign_keys = ON");
|
|
422
|
+
this.db.exec("PRAGMA busy_timeout = 5000");
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
if (this.readOnly) {
|
|
426
|
+
// Read-worker connection (Phase 0 read isolation). The main-thread
|
|
427
|
+
// MailxDB has ALREADY created the schema and run every migration /
|
|
428
|
+
// backfill before this worker is spawned, so we must NOT touch any
|
|
429
|
+
// of that here — two threads racing writes on one WAL file means
|
|
430
|
+
// SQLITE_BUSY and corruption risk. Strategy:
|
|
431
|
+
// • Do NOT pass {readOnly:true} to DatabaseSync. An OS-read-only
|
|
432
|
+
// handle on a WAL database can't maintain the -shm wal-index
|
|
433
|
+
// and fails to read at all. We open read-WRITE at the OS level
|
|
434
|
+
// so WAL reads work, then forbid writes LOGICALLY below.
|
|
435
|
+
// • PRAGMA query_only = ON makes every statement on this handle
|
|
436
|
+
// read-only — INSERT/UPDATE/DELETE/DDL all throw. That is the
|
|
437
|
+
// guard that lets us reuse the full MailxDB read surface here
|
|
438
|
+
// without any chance of a stray write.
|
|
439
|
+
// WAL mode is a persisted property of the file (the writer set it),
|
|
440
|
+
// so readers see committed snapshots with no journal_mode call.
|
|
441
|
+
this.db.exec("PRAGMA foreign_keys = ON");
|
|
442
|
+
this.db.exec("PRAGMA query_only = ON");
|
|
443
|
+
// A reader can briefly collide with a WAL checkpoint; wait, don't throw.
|
|
444
|
+
this.db.exec("PRAGMA busy_timeout = 5000");
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
410
447
|
this.db.exec("PRAGMA journal_mode = WAL");
|
|
411
448
|
this.db.exec("PRAGMA foreign_keys = ON");
|
|
449
|
+
// Multi-writer foundation for the sync-worker migration: once sync runs
|
|
450
|
+
// on its own thread with its own write connection, it and the main
|
|
451
|
+
// thread are two writers on one WAL file. WAL serializes writers, so a
|
|
452
|
+
// collision returns SQLITE_BUSY *immediately* by default — busy_timeout
|
|
453
|
+
// makes the loser WAIT for the lock (up to 5s) instead of failing. Safe
|
|
454
|
+
// and inert today (single writer); required before the worker lands.
|
|
455
|
+
this.db.exec("PRAGMA busy_timeout = 5000");
|
|
412
456
|
this.db.exec(SCHEMA);
|
|
413
457
|
this.migrateFtsSchema();
|
|
414
458
|
// Purge phantom uid=0 rows (invalid IMAP UID — empty stub letters, the
|
|
@@ -1180,6 +1224,7 @@ export class MailxDB {
|
|
|
1180
1224
|
accountId: r.account_id,
|
|
1181
1225
|
folderId: r.folder_id,
|
|
1182
1226
|
uid: r.uid,
|
|
1227
|
+
uuid: r.uuid || "", // stable identity for row data-uuid / selection restore
|
|
1183
1228
|
messageId: r.message_id || "",
|
|
1184
1229
|
inReplyTo: r.in_reply_to || "",
|
|
1185
1230
|
references: JSON.parse(r.refs || "[]"),
|
|
@@ -1873,6 +1918,13 @@ export class MailxDB {
|
|
|
1873
1918
|
// Bcc). The unified-inbox UI shows a small ⇆ badge on these
|
|
1874
1919
|
// rows so the user knows "this is a copy of the same message".
|
|
1875
1920
|
dupeCount: r.dupeCount | 0,
|
|
1921
|
+
// uuid is the stable local identity each message-list row carries as
|
|
1922
|
+
// data-uuid. Without it, restoreSelection() / rememberPosition() /
|
|
1923
|
+
// the set-diff scroll anchor all match on empty string and fail, so
|
|
1924
|
+
// the selected-row highlight vanishes on every reload (Bob
|
|
1925
|
+
// 2026-06-13: "no highlighting in the summary"). getMessages already
|
|
1926
|
+
// maps uuid; this unified-inbox map dropped it. SELECT m.* exposes it.
|
|
1927
|
+
uuid: r.uuid || "",
|
|
1876
1928
|
}));
|
|
1877
1929
|
return { items, total, page, pageSize };
|
|
1878
1930
|
}
|
|
@@ -3132,6 +3184,7 @@ export class MailxDB {
|
|
|
3132
3184
|
folderId: r.folder_id,
|
|
3133
3185
|
folderName: r.folder_name || "",
|
|
3134
3186
|
uid: r.uid,
|
|
3187
|
+
uuid: r.uuid || "", // stable identity for row data-uuid / selection restore
|
|
3135
3188
|
messageId: r.message_id || "",
|
|
3136
3189
|
inReplyTo: r.in_reply_to || "",
|
|
3137
3190
|
references: JSON.parse(r.refs || "[]"),
|