@bobfrankston/mailx-store 0.1.43 → 0.1.45
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 +10 -0
- package/db.js +52 -4
- package/package.json +1 -1
package/db.d.ts
CHANGED
|
@@ -375,6 +375,16 @@ export declare class MailxDB {
|
|
|
375
375
|
uid: number;
|
|
376
376
|
folderId: number;
|
|
377
377
|
}[];
|
|
378
|
+
/** Record a prefetch failure (0-body fetch / store-write fail) for a UID,
|
|
379
|
+
* incrementing its backoff count. Persisted so it survives restarts. */
|
|
380
|
+
recordPrefetchFailure(accountId: string, folderId: number, uid: number): void;
|
|
381
|
+
/** Read a UID's prefetch-failure record (count + lastTried), or null. */
|
|
382
|
+
getPrefetchFailure(accountId: string, folderId: number, uid: number): {
|
|
383
|
+
count: number;
|
|
384
|
+
lastTried: number;
|
|
385
|
+
} | null;
|
|
386
|
+
/** Clear a UID's prefetch-failure record after a successful fetch. */
|
|
387
|
+
clearPrefetchFailure(accountId: string, folderId: number, uid: number): void;
|
|
378
388
|
/** Highest server UID we've seen in this folder. After the
|
|
379
389
|
* message_folders refactor, this reads from the membership table
|
|
380
390
|
* rather than messages.uid — a server-side move from this folder
|
package/db.js
CHANGED
|
@@ -315,6 +315,23 @@ const SCHEMA = `
|
|
|
315
315
|
PRIMARY KEY(scope, key)
|
|
316
316
|
);
|
|
317
317
|
|
|
318
|
+
-- Prefetch-failure backoff, PERSISTED (was an in-memory Map that every
|
|
319
|
+
-- daemon restart wiped — so un-fetchable "ghost" UIDs, e.g. stale Outbox
|
|
320
|
+
-- rows for already-sent messages that return 0 bodies, got re-tried in
|
|
321
|
+
-- full on every launch and re-clogged the size-asc prefetch queue,
|
|
322
|
+
-- starving healthy folders. "So much unfetched, never drains" — Bob
|
|
323
|
+
-- 2026-06-01. Persisting the count+timestamp lets the 5min→12h backoff
|
|
324
|
+
-- survive restarts so zombies stay sidelined. Pure cache-control state;
|
|
325
|
+
-- safe to lose (worst case = one extra retry), so no reconstruction needed.
|
|
326
|
+
CREATE TABLE IF NOT EXISTS prefetch_failures (
|
|
327
|
+
account_id TEXT NOT NULL,
|
|
328
|
+
folder_id INTEGER NOT NULL,
|
|
329
|
+
uid INTEGER NOT NULL,
|
|
330
|
+
count INTEGER NOT NULL,
|
|
331
|
+
last_tried INTEGER NOT NULL,
|
|
332
|
+
PRIMARY KEY(account_id, folder_id, uid)
|
|
333
|
+
);
|
|
334
|
+
|
|
318
335
|
-- Audit trail of every destructive DB operation. Writes ONLY — the row
|
|
319
336
|
-- inserted here records the deletion the caller is about to (or just
|
|
320
337
|
-- did) commit on the messages table. Lets the user retroactively answer
|
|
@@ -1666,7 +1683,12 @@ export class MailxDB {
|
|
|
1666
1683
|
EXISTS(
|
|
1667
1684
|
SELECT 1 FROM sync_actions sa
|
|
1668
1685
|
WHERE sa.account_id = m.account_id AND sa.uid = r.mf_uid
|
|
1669
|
-
) AS pending
|
|
1686
|
+
) AS pending,
|
|
1687
|
+
-- Replied state OR'd across the Message-ID group (see
|
|
1688
|
+
-- getUnifiedInbox) so the reply arrow is correct regardless of
|
|
1689
|
+
-- which duplicate survives dedup. Bob 2026-06-01.
|
|
1690
|
+
COALESCE((SELECT MAX(CASE WHEN m3.is_replied = 1 OR m3.flags_json LIKE '%Answered%' THEN 1 ELSE 0 END)
|
|
1691
|
+
FROM messages m3 WHERE m3.message_id = m.message_id AND COALESCE(m.message_id, '') != ''), m.is_replied) AS groupReplied
|
|
1670
1692
|
FROM ranked r
|
|
1671
1693
|
JOIN messages m ON m.id = r.m_id
|
|
1672
1694
|
WHERE r.rn = 1
|
|
@@ -1689,7 +1711,7 @@ export class MailxDB {
|
|
|
1689
1711
|
flags: JSON.parse(r.flags_json),
|
|
1690
1712
|
size: r.size,
|
|
1691
1713
|
hasAttachments: !!r.has_attachments,
|
|
1692
|
-
isReplied: !!r.
|
|
1714
|
+
isReplied: !!r.groupReplied,
|
|
1693
1715
|
preview: r.preview,
|
|
1694
1716
|
bodyPath: r.body_path || "",
|
|
1695
1717
|
pending: !!r.pending,
|
|
@@ -1737,7 +1759,16 @@ export class MailxDB {
|
|
|
1737
1759
|
WHERE sa.account_id = m.account_id AND sa.uid = r.mf_uid
|
|
1738
1760
|
) AS pending,
|
|
1739
1761
|
(SELECT COUNT(DISTINCT account_id) FROM messages m2
|
|
1740
|
-
WHERE m2.message_id = m.message_id AND COALESCE(m.message_id, '') != '') AS dupeCount
|
|
1762
|
+
WHERE m2.message_id = m.message_id AND COALESCE(m.message_id, '') != '') AS dupeCount,
|
|
1763
|
+
-- Replied state aggregated across the whole Message-ID group, not
|
|
1764
|
+
-- just the dedup survivor. When duplicates collapse (same message
|
|
1765
|
+
-- delivered to multiple self-addresses), the \Answered flag /
|
|
1766
|
+
-- is_replied can live on a NON-surviving copy — so the survivor
|
|
1767
|
+
-- showed no reply arrow even though TB did. OR it across the group
|
|
1768
|
+
-- so the marker is correct regardless of which row wins dedup
|
|
1769
|
+
-- (Bob 2026-06-01). Empty message_id → falls back to the row's own.
|
|
1770
|
+
COALESCE((SELECT MAX(CASE WHEN m3.is_replied = 1 OR m3.flags_json LIKE '%Answered%' THEN 1 ELSE 0 END)
|
|
1771
|
+
FROM messages m3 WHERE m3.message_id = m.message_id AND COALESCE(m.message_id, '') != ''), m.is_replied) AS groupReplied
|
|
1741
1772
|
FROM ranked r
|
|
1742
1773
|
JOIN messages m ON m.id = r.m_id
|
|
1743
1774
|
WHERE r.rn = 1
|
|
@@ -1759,7 +1790,7 @@ export class MailxDB {
|
|
|
1759
1790
|
flags: JSON.parse(r.flags_json),
|
|
1760
1791
|
size: r.size,
|
|
1761
1792
|
hasAttachments: !!r.has_attachments,
|
|
1762
|
-
isReplied: !!r.
|
|
1793
|
+
isReplied: !!r.groupReplied,
|
|
1763
1794
|
preview: r.preview,
|
|
1764
1795
|
bodyPath: r.body_path || "",
|
|
1765
1796
|
pending: !!r.pending,
|
|
@@ -2017,6 +2048,23 @@ export class MailxDB {
|
|
|
2017
2048
|
: "";
|
|
2018
2049
|
return this.db.prepare(`SELECT uid, folder_id as folderId FROM messages WHERE account_id = ? AND (body_path IS NULL OR body_path = '')${exclusion} ORDER BY (size IS NULL OR size = 0), size ASC, date DESC LIMIT ?`).all(accountId, ...excludeFolderIds, limit);
|
|
2019
2050
|
}
|
|
2051
|
+
/** Record a prefetch failure (0-body fetch / store-write fail) for a UID,
|
|
2052
|
+
* incrementing its backoff count. Persisted so it survives restarts. */
|
|
2053
|
+
recordPrefetchFailure(accountId, folderId, uid) {
|
|
2054
|
+
this.db.prepare(`INSERT INTO prefetch_failures (account_id, folder_id, uid, count, last_tried)
|
|
2055
|
+
VALUES (?, ?, ?, 1, ?)
|
|
2056
|
+
ON CONFLICT(account_id, folder_id, uid)
|
|
2057
|
+
DO UPDATE SET count = count + 1, last_tried = excluded.last_tried`).run(accountId, folderId, uid, Date.now());
|
|
2058
|
+
}
|
|
2059
|
+
/** Read a UID's prefetch-failure record (count + lastTried), or null. */
|
|
2060
|
+
getPrefetchFailure(accountId, folderId, uid) {
|
|
2061
|
+
const r = this.db.prepare("SELECT count, last_tried AS lastTried FROM prefetch_failures WHERE account_id = ? AND folder_id = ? AND uid = ?").get(accountId, folderId, uid);
|
|
2062
|
+
return r ? { count: r.count, lastTried: r.lastTried } : null;
|
|
2063
|
+
}
|
|
2064
|
+
/** Clear a UID's prefetch-failure record after a successful fetch. */
|
|
2065
|
+
clearPrefetchFailure(accountId, folderId, uid) {
|
|
2066
|
+
this.db.prepare("DELETE FROM prefetch_failures WHERE account_id = ? AND folder_id = ? AND uid = ?").run(accountId, folderId, uid);
|
|
2067
|
+
}
|
|
2020
2068
|
/** Highest server UID we've seen in this folder. After the
|
|
2021
2069
|
* message_folders refactor, this reads from the membership table
|
|
2022
2070
|
* rather than messages.uid — a server-side move from this folder
|