@bobfrankston/mailx-store 0.1.46 → 0.1.48
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 +7 -1
- package/db.js +44 -3
- package/package.json +1 -1
package/db.d.ts
CHANGED
|
@@ -9,7 +9,13 @@ 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
|
+
});
|
|
13
19
|
/** Fail loud + early if expected columns are missing. Cheap (PRAGMA only
|
|
14
20
|
* runs at startup). The user-facing message names the recovery command. */
|
|
15
21
|
private verifySchema;
|
package/db.js
CHANGED
|
@@ -403,10 +403,35 @@ 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 (this.readOnly) {
|
|
416
|
+
// Read-worker connection (Phase 0 read isolation). The main-thread
|
|
417
|
+
// MailxDB has ALREADY created the schema and run every migration /
|
|
418
|
+
// backfill before this worker is spawned, so we must NOT touch any
|
|
419
|
+
// of that here — two threads racing writes on one WAL file means
|
|
420
|
+
// SQLITE_BUSY and corruption risk. Strategy:
|
|
421
|
+
// • Do NOT pass {readOnly:true} to DatabaseSync. An OS-read-only
|
|
422
|
+
// handle on a WAL database can't maintain the -shm wal-index
|
|
423
|
+
// and fails to read at all. We open read-WRITE at the OS level
|
|
424
|
+
// so WAL reads work, then forbid writes LOGICALLY below.
|
|
425
|
+
// • PRAGMA query_only = ON makes every statement on this handle
|
|
426
|
+
// read-only — INSERT/UPDATE/DELETE/DDL all throw. That is the
|
|
427
|
+
// guard that lets us reuse the full MailxDB read surface here
|
|
428
|
+
// without any chance of a stray write.
|
|
429
|
+
// WAL mode is a persisted property of the file (the writer set it),
|
|
430
|
+
// so readers see committed snapshots with no journal_mode call.
|
|
431
|
+
this.db.exec("PRAGMA foreign_keys = ON");
|
|
432
|
+
this.db.exec("PRAGMA query_only = ON");
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
410
435
|
this.db.exec("PRAGMA journal_mode = WAL");
|
|
411
436
|
this.db.exec("PRAGMA foreign_keys = ON");
|
|
412
437
|
this.db.exec(SCHEMA);
|
|
@@ -1180,6 +1205,7 @@ export class MailxDB {
|
|
|
1180
1205
|
accountId: r.account_id,
|
|
1181
1206
|
folderId: r.folder_id,
|
|
1182
1207
|
uid: r.uid,
|
|
1208
|
+
uuid: r.uuid || "", // stable identity for row data-uuid / selection restore
|
|
1183
1209
|
messageId: r.message_id || "",
|
|
1184
1210
|
inReplyTo: r.in_reply_to || "",
|
|
1185
1211
|
references: JSON.parse(r.refs || "[]"),
|
|
@@ -1803,17 +1829,24 @@ export class MailxDB {
|
|
|
1803
1829
|
// The PARTITION BY collapses duplicates: keep the newest per
|
|
1804
1830
|
// Message-ID. Empty Message-IDs (rare; non-RFC senders) are
|
|
1805
1831
|
// grouped by row id so each gets its own bucket.
|
|
1832
|
+
//
|
|
1833
|
+
// Dedup is PER INBOX (account_id is in the partition key), not global.
|
|
1834
|
+
// The same Message-ID delivered to two different accounts' inboxes
|
|
1835
|
+
// (e.g. a list that hits both bob.ma and Gmail) is genuinely two
|
|
1836
|
+
// copies the user expects to see in All Inboxes — collapsing across
|
|
1837
|
+
// accounts hid the second one (Bob 2026-06-12). Within a single inbox,
|
|
1838
|
+
// the to-self N-copies case still collapses as before.
|
|
1806
1839
|
const total = this.db.prepare(`SELECT COUNT(*) as cnt FROM (
|
|
1807
1840
|
SELECT 1
|
|
1808
1841
|
FROM messages m
|
|
1809
1842
|
JOIN message_folders mf ON mf.message_row_id = m.id
|
|
1810
1843
|
WHERE mf.folder_id IN (${placeholders})${flagFilter}
|
|
1811
|
-
GROUP BY CASE WHEN COALESCE(m.message_id, '') = '' THEN 'mid-empty:' || m.id ELSE m.message_id END
|
|
1844
|
+
GROUP BY m.account_id, CASE WHEN COALESCE(m.message_id, '') = '' THEN 'mid-empty:' || m.id ELSE m.message_id END
|
|
1812
1845
|
)`).get(...folderIds).cnt;
|
|
1813
1846
|
const rows = this.db.prepare(`WITH ranked AS (
|
|
1814
1847
|
SELECT m.id AS m_id, mf.uid AS mf_uid, mf.folder_id AS mf_folder_id,
|
|
1815
1848
|
ROW_NUMBER() OVER (
|
|
1816
|
-
PARTITION BY CASE WHEN COALESCE(m.message_id, '') = '' THEN 'mid-empty:' || m.id ELSE m.message_id END
|
|
1849
|
+
PARTITION BY m.account_id, CASE WHEN COALESCE(m.message_id, '') = '' THEN 'mid-empty:' || m.id ELSE m.message_id END
|
|
1817
1850
|
ORDER BY m.date DESC, m.id DESC
|
|
1818
1851
|
) AS rn
|
|
1819
1852
|
FROM messages m
|
|
@@ -1866,6 +1899,13 @@ export class MailxDB {
|
|
|
1866
1899
|
// Bcc). The unified-inbox UI shows a small ⇆ badge on these
|
|
1867
1900
|
// rows so the user knows "this is a copy of the same message".
|
|
1868
1901
|
dupeCount: r.dupeCount | 0,
|
|
1902
|
+
// uuid is the stable local identity each message-list row carries as
|
|
1903
|
+
// data-uuid. Without it, restoreSelection() / rememberPosition() /
|
|
1904
|
+
// the set-diff scroll anchor all match on empty string and fail, so
|
|
1905
|
+
// the selected-row highlight vanishes on every reload (Bob
|
|
1906
|
+
// 2026-06-13: "no highlighting in the summary"). getMessages already
|
|
1907
|
+
// maps uuid; this unified-inbox map dropped it. SELECT m.* exposes it.
|
|
1908
|
+
uuid: r.uuid || "",
|
|
1869
1909
|
}));
|
|
1870
1910
|
return { items, total, page, pageSize };
|
|
1871
1911
|
}
|
|
@@ -3125,6 +3165,7 @@ export class MailxDB {
|
|
|
3125
3165
|
folderId: r.folder_id,
|
|
3126
3166
|
folderName: r.folder_name || "",
|
|
3127
3167
|
uid: r.uid,
|
|
3168
|
+
uuid: r.uuid || "", // stable identity for row data-uuid / selection restore
|
|
3128
3169
|
messageId: r.message_id || "",
|
|
3129
3170
|
inReplyTo: r.in_reply_to || "",
|
|
3130
3171
|
references: JSON.parse(r.refs || "[]"),
|