@bobfrankston/rmfmail 1.2.41 → 1.2.46
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/client/app.bundle.js +6 -1
- package/client/app.bundle.js.map +2 -2
- package/client/components/message-list.js +20 -1
- package/client/components/message-list.js.map +1 -1
- package/client/components/message-list.ts +19 -1
- package/package.json +5 -5
- package/packages/mailx-imap/index.d.ts +21 -1
- package/packages/mailx-imap/index.d.ts.map +1 -1
- package/packages/mailx-imap/index.js +132 -88
- package/packages/mailx-imap/index.js.map +1 -1
- package/packages/mailx-imap/index.ts +144 -85
- package/packages/mailx-imap/package-lock.json +2 -2
- package/packages/mailx-imap/package.json +1 -1
- package/packages/mailx-service/reconciler.d.ts.map +1 -1
- package/packages/mailx-service/reconciler.js +6 -1
- package/packages/mailx-service/reconciler.js.map +1 -1
- package/packages/mailx-service/reconciler.ts +6 -1
- package/packages/mailx-settings/docs/instance-model-plan.md +75 -0
- package/packages/mailx-settings/package.json +1 -1
|
@@ -44,6 +44,12 @@ interface HostSemaphore {
|
|
|
44
44
|
* lost-ack doesn't arrive while the first copy is still being processed. */
|
|
45
45
|
const OUTBOX_RETRY_DELAY_MS = 60000;
|
|
46
46
|
|
|
47
|
+
/** How often the QRESYNC fast path runs a full set-difference reconcile to
|
|
48
|
+
* heal gaps the high-water-mark fetch can't see. The first sync of a folder
|
|
49
|
+
* after boot always runs one (the map is empty); thereafter it's throttled to
|
|
50
|
+
* this interval so the QRESYNC speedup is preserved in steady state. */
|
|
51
|
+
const RECONCILE_THROTTLE_MS = 15 * 60 * 1000;
|
|
52
|
+
|
|
47
53
|
/** Parse X-Mailx-Retry* tracking headers from a raw RFC822 message. */
|
|
48
54
|
function parseRetryInfo(raw: string): { attemptCount: number; nextAttemptAt: number } {
|
|
49
55
|
const headerEnd = raw.search(/\r?\n\r?\n/);
|
|
@@ -1316,6 +1322,83 @@ export class ImapManager extends EventEmitter {
|
|
|
1316
1322
|
* expect their syncFolder for `Sent` to actually run. */
|
|
1317
1323
|
private syncFolderLocks = new Map<string, Promise<number>>();
|
|
1318
1324
|
|
|
1325
|
+
/** Per-folder timestamp of the last set-difference reconcile. Drives the
|
|
1326
|
+
* throttle for the QRESYNC-path gap backfill (see fetchServerOnlyUids /
|
|
1327
|
+
* syncFolder QRESYNC branch). Empty at boot, so the first sync of each
|
|
1328
|
+
* folder after a restart always runs a reconcile — which is exactly when
|
|
1329
|
+
* a daemon-was-down gap needs to be healed. */
|
|
1330
|
+
private lastReconcileMs = new Map<number, number>();
|
|
1331
|
+
|
|
1332
|
+
/**
|
|
1333
|
+
* Set-difference backfill. Fetch every server UID (within the history
|
|
1334
|
+
* window) that we don't already have locally and aren't fetching this
|
|
1335
|
+
* cycle. This is the ONLY mechanism that recovers a message which is on
|
|
1336
|
+
* the server but missing locally — the high-water-mark fetch
|
|
1337
|
+
* (fetchMessagesSinceUid) cannot, because it only looks above the local
|
|
1338
|
+
* highest UID. Gaps it heals: a message that arrived while the daemon was
|
|
1339
|
+
* down (so it sits below the watermark once the next poll advances it), or
|
|
1340
|
+
* one dropped mid-stream by a response desync. Returns the recovered
|
|
1341
|
+
* messages (caller stores them) plus the server UID list it fetched, so the
|
|
1342
|
+
* caller's deletion-reconcile can reuse it instead of paying for a second
|
|
1343
|
+
* UID SEARCH round-trip.
|
|
1344
|
+
*/
|
|
1345
|
+
private async fetchServerOnlyUids(
|
|
1346
|
+
client: any,
|
|
1347
|
+
accountId: string,
|
|
1348
|
+
folderId: number,
|
|
1349
|
+
folderPath: string,
|
|
1350
|
+
statusMessageCount: number | null,
|
|
1351
|
+
excludeUids: Set<number>,
|
|
1352
|
+
): Promise<{ recovered: any[]; serverUids: number[] | null; dateBounded: boolean }> {
|
|
1353
|
+
const existingUids = this.db.getUidsForFolder(accountId, folderId);
|
|
1354
|
+
// Small folders (Drafts, Sent, …) get a FULL UID fetch so server-side
|
|
1355
|
+
// deletions of OLDER messages are reflected; large folders (134k INBOX)
|
|
1356
|
+
// get a date-bounded query so we don't enumerate the whole mailbox.
|
|
1357
|
+
const SMALL_FOLDER_THRESHOLD = 500;
|
|
1358
|
+
const isSmallFolder = statusMessageCount !== null && statusMessageCount <= SMALL_FOLDER_THRESHOLD;
|
|
1359
|
+
const historyDays = getHistoryDays(accountId);
|
|
1360
|
+
const SINCE_DAYS = historyDays > 0 ? historyDays : 1825;
|
|
1361
|
+
const sinceDate = new Date(Date.now() - SINCE_DAYS * 86400000);
|
|
1362
|
+
let serverUids: number[];
|
|
1363
|
+
let dateBounded: boolean;
|
|
1364
|
+
const __t0 = Date.now();
|
|
1365
|
+
if (isSmallFolder) {
|
|
1366
|
+
console.log(` [sync-uids] ${accountId}/${folderPath}: small folder (server=${statusMessageCount}) — getUids ALL`);
|
|
1367
|
+
serverUids = await client.getUids(folderPath);
|
|
1368
|
+
dateBounded = false;
|
|
1369
|
+
} else {
|
|
1370
|
+
console.log(` [sync-uids] ${accountId}/${folderPath}: getUidsSince ${sinceDate.toISOString().slice(0, 10)}...`);
|
|
1371
|
+
serverUids = typeof client.getUidsSince === "function"
|
|
1372
|
+
? await client.getUidsSince(folderPath, sinceDate)
|
|
1373
|
+
: await client.getUids(folderPath);
|
|
1374
|
+
dateBounded = true;
|
|
1375
|
+
}
|
|
1376
|
+
console.log(` [sync-uids] ${accountId}/${folderPath}: ${serverUids.length} UIDs in ${Date.now() - __t0}ms`);
|
|
1377
|
+
const existingSet = new Set(existingUids);
|
|
1378
|
+
const missingUids = serverUids.filter((uid: number) => !existingSet.has(uid) && !excludeUids.has(uid));
|
|
1379
|
+
const recovered: any[] = [];
|
|
1380
|
+
if (missingUids.length === 0) return { recovered, serverUids, dateBounded };
|
|
1381
|
+
// Cap so a misbehaving response / brand-new account doesn't pull tens
|
|
1382
|
+
// of thousands at once; the rest resumes next cycle. Higher UID ≈ more
|
|
1383
|
+
// recent under stable UIDVALIDITY, so prefer newest when capping.
|
|
1384
|
+
const BACKFILL_CHUNK_SIZE = 100;
|
|
1385
|
+
const RECONCILE_FETCH_CAP = 5000;
|
|
1386
|
+
let toFetch = missingUids;
|
|
1387
|
+
if (missingUids.length > RECONCILE_FETCH_CAP) {
|
|
1388
|
+
console.log(` ${folderPath}: ${missingUids.length} server-only UIDs — capped at ${RECONCILE_FETCH_CAP}; will resume next cycle`);
|
|
1389
|
+
toFetch = missingUids.slice().sort((a: number, b: number) => b - a).slice(0, RECONCILE_FETCH_CAP);
|
|
1390
|
+
} else {
|
|
1391
|
+
console.log(` ${folderPath}: ${missingUids.length} server-only UIDs — fetching`);
|
|
1392
|
+
}
|
|
1393
|
+
for (let i = 0; i < toFetch.length; i += BACKFILL_CHUNK_SIZE) {
|
|
1394
|
+
const chunk = toFetch.slice(i, i + BACKFILL_CHUNK_SIZE);
|
|
1395
|
+
const got = await client.fetchMessages(folderPath, chunk.join(","), { source: false });
|
|
1396
|
+
recovered.push(...got);
|
|
1397
|
+
console.log(` ${folderPath}: backfill ${recovered.length}/${toFetch.length}`);
|
|
1398
|
+
}
|
|
1399
|
+
return { recovered, serverUids, dateBounded };
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1319
1402
|
async syncFolder(accountId: string, folderId: number, client?: any): Promise<number> {
|
|
1320
1403
|
const lockKey = `${accountId}:${folderId}`;
|
|
1321
1404
|
const inflight = this.syncFolderLocks.get(lockKey);
|
|
@@ -1515,6 +1598,38 @@ export class ImapManager extends EventEmitter {
|
|
|
1515
1598
|
console.log(` [qr-phase] ${folder.path}: storeMessages(${newOnes.length}) in ${Date.now() - __t2}ms`);
|
|
1516
1599
|
}
|
|
1517
1600
|
|
|
1601
|
+
// SET-DIFF GAP RECOVERY (bounded + throttled). The
|
|
1602
|
+
// fetchMessagesSinceUid above is a high-water-mark fetch —
|
|
1603
|
+
// it CANNOT recover a server UID that's missing below our
|
|
1604
|
+
// highest (a message that arrived while the daemon was down,
|
|
1605
|
+
// now under the watermark the next poll advances) or one
|
|
1606
|
+
// dropped mid-stream by a response desync. QRESYNC otherwise
|
|
1607
|
+
// returns here, making such gaps permanent (Bob 2026-06-20:
|
|
1608
|
+
// "messages before 7am are missing" — daemon crashed
|
|
1609
|
+
// overnight; the catch-up since-fetch lost the low UIDs of
|
|
1610
|
+
// the range and QRESYNC never reconciled). Run a full
|
|
1611
|
+
// set-diff on the first sync after boot, then at most once
|
|
1612
|
+
// per RECONCILE_THROTTLE_MS so the fast path stays fast.
|
|
1613
|
+
let backfilledCount = 0;
|
|
1614
|
+
const lastRecon = this.lastReconcileMs.get(folderId) ?? 0;
|
|
1615
|
+
if (Date.now() - lastRecon > RECONCILE_THROTTLE_MS) {
|
|
1616
|
+
this.lastReconcileMs.set(folderId, Date.now());
|
|
1617
|
+
try {
|
|
1618
|
+
const sd = await this.fetchServerOnlyUids(
|
|
1619
|
+
client, accountId, folderId, folder.path, statusMessageCount,
|
|
1620
|
+
new Set(newOnes.map((m: any) => m.uid)),
|
|
1621
|
+
);
|
|
1622
|
+
if (sd.recovered.length > 0) {
|
|
1623
|
+
backfilledCount = await this.storeMessages(accountId, folderId, folder, sd.recovered, highestUid);
|
|
1624
|
+
console.log(` [qresync] ${accountId}/${folder.path}: set-diff backfilled ${backfilledCount} server-only UID(s)`);
|
|
1625
|
+
this.db.recalcFolderCounts(folderId);
|
|
1626
|
+
this.emit("folderCountsChanged", accountId, {});
|
|
1627
|
+
}
|
|
1628
|
+
} catch (e: any) {
|
|
1629
|
+
console.error(` [qresync] ${accountId}/${folder.path}: set-diff backfill failed: ${e?.message || e}`);
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1518
1633
|
// Persist new watermark — next resync starts here.
|
|
1519
1634
|
if (qr.newHighestModSeq !== undefined) {
|
|
1520
1635
|
this.db.updateFolderSync(folderId, qr.exists ? prevUidValidity : prevUidValidity, String(qr.newHighestModSeq));
|
|
@@ -1531,7 +1646,7 @@ export class ImapManager extends EventEmitter {
|
|
|
1531
1646
|
|
|
1532
1647
|
this.emit("syncProgress", accountId, `sync:${folder.path}`, 100);
|
|
1533
1648
|
console.log(` [qresync] ${accountId}/${folder.path}: done in ${Date.now() - __sfStart}ms (path: QRESYNC)`);
|
|
1534
|
-
return newOnes.length;
|
|
1649
|
+
return newOnes.length + backfilledCount;
|
|
1535
1650
|
}
|
|
1536
1651
|
} catch (qrErr: any) {
|
|
1537
1652
|
// QRESYNC failed (server quirk, network glitch, etc.) — log
|
|
@@ -1588,93 +1703,24 @@ export class ImapManager extends EventEmitter {
|
|
|
1588
1703
|
// brand-new account doesn't try to pull tens of thousands of
|
|
1589
1704
|
// historical messages in one cycle. Hit the cap and the next
|
|
1590
1705
|
// sync picks up the rest.
|
|
1591
|
-
const existingUids = this.db.getUidsForFolder(accountId, folderId);
|
|
1592
1706
|
try {
|
|
1593
|
-
//
|
|
1594
|
-
//
|
|
1595
|
-
//
|
|
1596
|
-
//
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
// miss an in-flight message, without enumerating decades of
|
|
1601
|
-
// archive every cycle. First sync gets all UIDs (no anchor
|
|
1602
|
-
// yet so we have to compare against the empty local set
|
|
1603
|
-
// anyway).
|
|
1604
|
-
// Small folders (Drafts, Sent, Outbox, Trash, …) get a FULL
|
|
1605
|
-
// UID fetch instead of date-bounded. Without this, server-side
|
|
1606
|
-
// deletions of OLDER messages (e.g., user emptied Trash from
|
|
1607
|
-
// Thunderbird) are never reflected — date-bound filters them
|
|
1608
|
-
// out of the comparison entirely. Bob 2026-05-12: "I deleted
|
|
1609
|
-
// my drafts and Thunderbird shows that but rmfmail is not
|
|
1610
|
-
// acknowledging the deletions." Threshold tuned conservatively
|
|
1611
|
-
// — UID SEARCH ALL on a 500-message folder is sub-second; the
|
|
1612
|
-
// pathology being avoided is the same call on 130k+ INBOX.
|
|
1613
|
-
const SMALL_FOLDER_THRESHOLD = 500;
|
|
1614
|
-
const isSmallFolder = statusMessageCount !== null && statusMessageCount <= SMALL_FOLDER_THRESHOLD;
|
|
1615
|
-
const SINCE_DAYS = effectiveDays > 0 ? effectiveDays : 1825;
|
|
1616
|
-
const sinceDate = new Date(Date.now() - SINCE_DAYS * 86400000);
|
|
1617
|
-
let allServerUids: number[];
|
|
1618
|
-
const __uidsT0 = Date.now();
|
|
1619
|
-
if (isSmallFolder) {
|
|
1620
|
-
console.log(` [sync-uids] ${accountId}/${folder.path}: small folder (server=${statusMessageCount}) — getUids ALL`);
|
|
1621
|
-
allServerUids = await (client as any).getUids(folder.path);
|
|
1622
|
-
serverUidsAreDateBounded = false;
|
|
1623
|
-
} else {
|
|
1624
|
-
console.log(` [sync-uids] ${accountId}/${folder.path}: getUidsSince ${sinceDate.toISOString().slice(0,10)}...`);
|
|
1625
|
-
allServerUids = typeof (client as any).getUidsSince === "function"
|
|
1626
|
-
? await (client as any).getUidsSince(folder.path, sinceDate)
|
|
1627
|
-
: await (client as any).getUids(folder.path);
|
|
1628
|
-
serverUidsAreDateBounded = true;
|
|
1629
|
-
}
|
|
1630
|
-
console.log(` [sync-uids] ${accountId}/${folder.path}: ${allServerUids.length} UIDs in ${Date.now() - __uidsT0}ms`);
|
|
1631
|
-
// Stash for the deletion-reconciliation block below — we
|
|
1632
|
-
// already have the server UID list, no point hitting the
|
|
1633
|
-
// server a second time.
|
|
1634
|
-
serverUidsCached = allServerUids;
|
|
1635
|
-
const existingSet = new Set(existingUids);
|
|
1636
|
-
const newSet = new Set(messages.map(m => m.uid));
|
|
1637
|
-
const missingUids = allServerUids.filter((uid: number) =>
|
|
1638
|
-
!existingSet.has(uid) && !newSet.has(uid)
|
|
1707
|
+
// Shared with the QRESYNC fast path (fetchServerOnlyUids).
|
|
1708
|
+
// Exclude the UIDs we just fetched via fetchMessagesSinceUid —
|
|
1709
|
+
// they're in `messages` but not yet in the DB, so they'd
|
|
1710
|
+
// otherwise look "missing" and get re-fetched.
|
|
1711
|
+
const sd = await this.fetchServerOnlyUids(
|
|
1712
|
+
client, accountId, folderId, folder.path, statusMessageCount,
|
|
1713
|
+
new Set(messages.map(m => m.uid)),
|
|
1639
1714
|
);
|
|
1640
|
-
|
|
1641
|
-
//
|
|
1642
|
-
//
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
const BACKFILL_CHUNK_SIZE = 100;
|
|
1646
|
-
if (missingUids.length > 0 && missingUids.length <= 5000) {
|
|
1647
|
-
let minU = existingUids[0] ?? 0;
|
|
1648
|
-
for (let i = 1; i < existingUids.length; i++) if (existingUids[i] < minU) minU = existingUids[i];
|
|
1649
|
-
console.log(` ${folder.path}: ${missingUids.length} server-only UIDs (local lowest=${minU}, highest=${highestUid}) — fetching`);
|
|
1650
|
-
let recoveredTotal = 0;
|
|
1651
|
-
for (let i = 0; i < missingUids.length; i += BACKFILL_CHUNK_SIZE) {
|
|
1652
|
-
const chunk = missingUids.slice(i, i + BACKFILL_CHUNK_SIZE);
|
|
1653
|
-
const range = chunk.join(",");
|
|
1654
|
-
const recovered = await (client as any).fetchMessages(folder.path, range, { source: false });
|
|
1655
|
-
messages.push(...recovered);
|
|
1656
|
-
recoveredTotal += recovered.length;
|
|
1657
|
-
console.log(` ${folder.path}: fetch ${recoveredTotal}/${missingUids.length}`);
|
|
1658
|
-
}
|
|
1659
|
-
} else if (missingUids.length > 5000) {
|
|
1660
|
-
console.log(` ${folder.path}: ${missingUids.length} server-only UIDs — capped; will resume next cycle`);
|
|
1661
|
-
// Vanilla IMAP under stable UIDVALIDITY: higher UID =
|
|
1662
|
-
// later assignment ≈ more recent message (Dovecot/
|
|
1663
|
-
// Cyrus). Gmail-API path is separate (no temporal
|
|
1664
|
-
// meaning to its hash-UIDs).
|
|
1665
|
-
const cappedSlice = missingUids.sort((a: number, b: number) => b - a).slice(0, 5000);
|
|
1666
|
-
let recoveredTotal = 0;
|
|
1667
|
-
for (let i = 0; i < cappedSlice.length; i += BACKFILL_CHUNK_SIZE) {
|
|
1668
|
-
const chunk = cappedSlice.slice(i, i + BACKFILL_CHUNK_SIZE);
|
|
1669
|
-
const recovered = await (client as any).fetchMessages(folder.path, chunk.join(","), { source: false });
|
|
1670
|
-
messages.push(...recovered);
|
|
1671
|
-
recoveredTotal += recovered.length;
|
|
1672
|
-
console.log(` ${folder.path}: fetch ${recoveredTotal}/5000 (capped)`);
|
|
1673
|
-
}
|
|
1674
|
-
}
|
|
1715
|
+
messages.push(...sd.recovered);
|
|
1716
|
+
// Stash the server UID list for the deletion-reconciliation
|
|
1717
|
+
// block below — no point hitting the server a second time.
|
|
1718
|
+
serverUidsCached = sd.serverUids;
|
|
1719
|
+
serverUidsAreDateBounded = sd.dateBounded;
|
|
1675
1720
|
} catch (e: any) {
|
|
1676
1721
|
console.error(` ${folder.path}: reconciliation failed: ${e.message}`);
|
|
1677
1722
|
}
|
|
1723
|
+
this.lastReconcileMs.set(folderId, Date.now());
|
|
1678
1724
|
|
|
1679
1725
|
// Date-based backfill via SEARCH SINCE was here. Removed —
|
|
1680
1726
|
// set-diff above already covers the same case (any server UID
|
|
@@ -3018,7 +3064,7 @@ export class ImapManager extends EventEmitter {
|
|
|
3018
3064
|
* Server fetch goes through the unified ops queue on the fast lane —
|
|
3019
3065
|
* the user clicked, they're waiting, this jumps ahead of any background
|
|
3020
3066
|
* prefetch sitting in the slow lane. */
|
|
3021
|
-
async fetchMessageBody(accountId: string, folderId: number, uid: number): Promise<Buffer | null> {
|
|
3067
|
+
async fetchMessageBody(accountId: string, folderId: number, uid: number, force = false): Promise<Buffer | null> {
|
|
3022
3068
|
// Belt-and-braces against `UID FETCH 0`. The IMAP server rejects it as
|
|
3023
3069
|
// BAD "Invalid uidset" and the connection slot is consumed for the
|
|
3024
3070
|
// round-trip. The enqueue path now guards too — this catches direct
|
|
@@ -3043,7 +3089,17 @@ export class ImapManager extends EventEmitter {
|
|
|
3043
3089
|
// delayed until the client timed out. The 5min→12h backoff (shared with
|
|
3044
3090
|
// prefetch via isPrefetchEmpty) stops the tight re-fetch loop; the body
|
|
3045
3091
|
// simply shows as unavailable until the backoff lapses and one retry runs.
|
|
3046
|
-
|
|
3092
|
+
// EXCEPT an interactive click (`force`): the user is staring at a blank
|
|
3093
|
+
// preview demanding THIS body NOW. Honoring a *background* prefetch
|
|
3094
|
+
// failure's backoff for an explicit user action is a local-first
|
|
3095
|
+
// violation — it left a real INBOX message (bobma uid 4967554,
|
|
3096
|
+
// 2026-06-19) showing an eternal "getting body" spinner for up to 12 h
|
|
3097
|
+
// because one earlier prefetch chunk returned 0 bodies (an iflow parser
|
|
3098
|
+
// miss, not a real expunge — siblings recovered on retry). A forced
|
|
3099
|
+
// single-UID fetch re-attempts immediately; if it genuinely fails it
|
|
3100
|
+
// re-arms the backoff below and the reconciler emits bodyFetchError so
|
|
3101
|
+
// the viewer shows a banner instead of spinning forever.
|
|
3102
|
+
if (!force && this.isPrefetchEmpty(accountId, folderId, uid)) return null;
|
|
3047
3103
|
|
|
3048
3104
|
const folder = this.db.getFolders(accountId).find(f => f.id === folderId);
|
|
3049
3105
|
if (!folder) return null;
|
|
@@ -3066,6 +3122,9 @@ export class ImapManager extends EventEmitter {
|
|
|
3066
3122
|
if (!raw) return null;
|
|
3067
3123
|
const bodyPath = await this.bodyStore.putMessage(accountId, folderId, uid, raw);
|
|
3068
3124
|
this.db.updateBodyPath(accountId, folderId, uid, bodyPath);
|
|
3125
|
+
// A forced fetch that beat a stale backoff just healed the row;
|
|
3126
|
+
// drop the prefetch-empty record so it doesn't suppress the next tick.
|
|
3127
|
+
this.clearPrefetchEmpty(accountId, folderId, uid);
|
|
3069
3128
|
this.emit("bodyCached", accountId, uid);
|
|
3070
3129
|
return raw;
|
|
3071
3130
|
} catch (e: any) {
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bobfrankston/mailx-imap",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.102",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bobfrankston/mailx-imap",
|
|
9
|
-
"version": "0.1.
|
|
9
|
+
"version": "0.1.102",
|
|
10
10
|
"license": "ISC",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@bobfrankston/iflow-direct": "^0.1.27",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"reconciler.d.ts","sourceRoot":"","sources":["reconciler.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAC5D,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,2BAA2B,CAAC;AAEzD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAEjD,MAAM,WAAW,iBAAiB;IAC9B;;sDAEkD;IAClD,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,qEAAqE;IACrE,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,wDAAwD;IACxD,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B;8DAC0D;IAC1D,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC;0EACsE;IACtE,6BAA6B,CAAC,EAAE,MAAM,CAAC;IACvC,mEAAmE;IACnE,wBAAwB,CAAC,EAAE,MAAM,CAAC;IAClC;yCACqC;IACrC,sBAAsB,CAAC,EAAE,MAAM,CAAC;CACnC;AAYD,qBAAa,UAAU;IAcf,OAAO,CAAC,EAAE;IACV,OAAO,CAAC,WAAW;IACnB,OAAO,CAAC,KAAK;IAfjB,OAAO,CAAC,IAAI,CAA8B;IAC1C,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,WAAW,CAA+C;IAClE,OAAO,CAAC,aAAa,CAA+C;IACpE,OAAO,CAAC,cAAc,CAA+C;IACrE,OAAO,CAAC,eAAe,CAAK;IAC5B;;;wEAGoE;IACpE,OAAO,CAAC,UAAU,CAAgE;gBAGtE,EAAE,EAAE,OAAO,EACX,WAAW,EAAE,WAAW,EACxB,KAAK,EAAE,SAAS,EACxB,IAAI,GAAE,iBAAsB;IAKhC,KAAK,IAAI,IAAI;IAyCb,IAAI,IAAI,IAAI;IAOZ;oDACgD;IAChD,OAAO,CAAC,iBAAiB;IAMzB;;;gDAG4C;IAC5C,OAAO,CAAC,eAAe;IAoBvB;oEACgE;IAChE,OAAO,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI;IAOjC;;2DAEuD;IACvD,OAAO,CAAC,eAAe;
|
|
1
|
+
{"version":3,"file":"reconciler.d.ts","sourceRoot":"","sources":["reconciler.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAC5D,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,2BAA2B,CAAC;AAEzD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAEjD,MAAM,WAAW,iBAAiB;IAC9B;;sDAEkD;IAClD,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,qEAAqE;IACrE,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,wDAAwD;IACxD,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B;8DAC0D;IAC1D,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC;0EACsE;IACtE,6BAA6B,CAAC,EAAE,MAAM,CAAC;IACvC,mEAAmE;IACnE,wBAAwB,CAAC,EAAE,MAAM,CAAC;IAClC;yCACqC;IACrC,sBAAsB,CAAC,EAAE,MAAM,CAAC;CACnC;AAYD,qBAAa,UAAU;IAcf,OAAO,CAAC,EAAE;IACV,OAAO,CAAC,WAAW;IACnB,OAAO,CAAC,KAAK;IAfjB,OAAO,CAAC,IAAI,CAA8B;IAC1C,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,WAAW,CAA+C;IAClE,OAAO,CAAC,aAAa,CAA+C;IACpE,OAAO,CAAC,cAAc,CAA+C;IACrE,OAAO,CAAC,eAAe,CAAK;IAC5B;;;wEAGoE;IACpE,OAAO,CAAC,UAAU,CAAgE;gBAGtE,EAAE,EAAE,OAAO,EACX,WAAW,EAAE,WAAW,EACxB,KAAK,EAAE,SAAS,EACxB,IAAI,GAAE,iBAAsB;IAKhC,KAAK,IAAI,IAAI;IAyCb,IAAI,IAAI,IAAI;IAOZ;oDACgD;IAChD,OAAO,CAAC,iBAAiB;IAMzB;;;gDAG4C;IAC5C,OAAO,CAAC,eAAe;IAoBvB;oEACgE;IAChE,OAAO,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI;IAOjC;;2DAEuD;IACvD,OAAO,CAAC,eAAe;IAmHvB,yEAAyE;IACzE,IAAI,IAAI,IAAI;CAGf"}
|
|
@@ -171,8 +171,13 @@ export class Reconciler {
|
|
|
171
171
|
// the same dead socket.
|
|
172
172
|
const FETCH_TIMEOUT_MS = 90_000;
|
|
173
173
|
let timeoutHandle;
|
|
174
|
+
// An interactive click forces the fetch past the per-message
|
|
175
|
+
// prefetch-empty backoff — the user explicitly wants THIS
|
|
176
|
+
// body now, and a background prefetch miss must not suppress
|
|
177
|
+
// it (see fetchMessageBody's `force` note, bobma 4967554).
|
|
178
|
+
const force = next.lane === "interactive";
|
|
174
179
|
const raw = await Promise.race([
|
|
175
|
-
this.imapManager.fetchMessageBody(next.accountId, next.folderId, next.uid),
|
|
180
|
+
this.imapManager.fetchMessageBody(next.accountId, next.folderId, next.uid, force),
|
|
176
181
|
new Promise((_, rej) => {
|
|
177
182
|
timeoutHandle = setTimeout(() => rej(new Error(`body fetch timeout after ${FETCH_TIMEOUT_MS / 1000}s`)), FETCH_TIMEOUT_MS);
|
|
178
183
|
}),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"reconciler.js","sourceRoot":"","sources":["reconciler.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"reconciler.js","sourceRoot":"","sources":["reconciler.ts"],"names":[],"mappings":"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"}
|
|
@@ -199,8 +199,13 @@ export class Reconciler {
|
|
|
199
199
|
// the same dead socket.
|
|
200
200
|
const FETCH_TIMEOUT_MS = 90_000;
|
|
201
201
|
let timeoutHandle: ReturnType<typeof setTimeout> | undefined;
|
|
202
|
+
// An interactive click forces the fetch past the per-message
|
|
203
|
+
// prefetch-empty backoff — the user explicitly wants THIS
|
|
204
|
+
// body now, and a background prefetch miss must not suppress
|
|
205
|
+
// it (see fetchMessageBody's `force` note, bobma 4967554).
|
|
206
|
+
const force = next.lane === "interactive";
|
|
202
207
|
const raw = await Promise.race<Uint8Array | string | null>([
|
|
203
|
-
this.imapManager.fetchMessageBody(next.accountId, next.folderId, next.uid),
|
|
208
|
+
this.imapManager.fetchMessageBody(next.accountId, next.folderId, next.uid, force),
|
|
204
209
|
new Promise<null>((_, rej) => {
|
|
205
210
|
timeoutHandle = setTimeout(
|
|
206
211
|
() => rej(new Error(`body fetch timeout after ${FETCH_TIMEOUT_MS / 1000}s`)),
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# Instance model — design plan (2026-06-16)
|
|
2
|
+
|
|
3
|
+
## Decision (Bob)
|
|
4
|
+
Model **B**: one record **per instance**, not per Message-ID. A copy of a message in
|
|
5
|
+
two folders is **two independent instances** with independent state (read/flag/star,
|
|
6
|
+
body, and — critically — **editable content**). Message-IDs are therefore **NOT globally
|
|
7
|
+
unique**; every code path that assumed "Message-ID → one row" must be made deliberate.
|
|
8
|
+
Bob's emphasis: **database integrity**.
|
|
9
|
+
|
|
10
|
+
## Identity
|
|
11
|
+
- **Instance = one `messages` row.** Natural key `(account_id, folder_id, uid)` (already
|
|
12
|
+
the schema's UNIQUE). Each row keeps its own stable `uuid`, `flags_json`, `body_path`,
|
|
13
|
+
and content (subject/body/etc. — may diverge once edited).
|
|
14
|
+
- `message_id` becomes a **non-unique indexed attribute**, used for threading and for
|
|
15
|
+
*display* dedup only — never as an identity key.
|
|
16
|
+
- **Drop the move-detect Message-ID collapse.** A new `(folder, uid)` is ALWAYS its own
|
|
17
|
+
instance, even when the same `message_id` exists in another folder.
|
|
18
|
+
|
|
19
|
+
## message_folders
|
|
20
|
+
Redundant under B (instance already = a `messages` row with folder_id/uid). It becomes
|
|
21
|
+
1:1 with `messages`. Plan: **retire it** — revert reads to `messages.folder_id`/`uid`,
|
|
22
|
+
removing the dual-representation integrity hazard. Staged so reads never break mid-flight.
|
|
23
|
+
|
|
24
|
+
## Server truth & move vs copy (reconcile)
|
|
25
|
+
- An instance exists iff the server lists `(folder, uid)`. Per-folder reconcile drops
|
|
26
|
+
instances the server no longer confirms. This cleans accidental stale-move-sources
|
|
27
|
+
while **preserving real copies** (server confirms both) — no exclusivity.
|
|
28
|
+
- **Server-side MOVE** (uid vanished from A, appeared in B, same message_id, A had exactly
|
|
29
|
+
ONE such instance): transfer the old instance's local state (uuid, flags, body_path,
|
|
30
|
+
local edits) to the new instance, then drop the old. Ambiguous case (copies present) →
|
|
31
|
+
treat as a fresh instance + re-fetch. Edits survive unambiguous moves.
|
|
32
|
+
|
|
33
|
+
## Copy / Edit operations
|
|
34
|
+
- **Copy to folder** (user action or server-side sieve copy): a new instance. User copy
|
|
35
|
+
queues a server `COPY`; sieve copy simply appears as a second server-confirmed instance.
|
|
36
|
+
- **Edit instance**: mutates only that instance's row + its own body file. No other
|
|
37
|
+
instance is touched. (Whether edits push to the server is a separate question.)
|
|
38
|
+
|
|
39
|
+
## Display dedup
|
|
40
|
+
- Unified "All Inboxes" still collapses copies by `message_id` for display, but each
|
|
41
|
+
instance is independently selectable/openable/editable.
|
|
42
|
+
- An **edited** instance (content diverged from its siblings) is shown **separately** —
|
|
43
|
+
flag `edited=1` excludes it from the dedup group.
|
|
44
|
+
|
|
45
|
+
## No migration — rebuild
|
|
46
|
+
Per the standing rule (local store is a cache): **wipe local + re-sync** into the new
|
|
47
|
+
model rather than backfill 136k rows. Avoids fragile migration code. One full re-sync.
|
|
48
|
+
|
|
49
|
+
## Integrity safeguards (Bob's ask)
|
|
50
|
+
- `verifyInstances()` self-check on boot + a `-verify` command: every `messages` row has a
|
|
51
|
+
valid folder; no two rows share `(account, folder, uid)`; `uuid` globally unique; every
|
|
52
|
+
referenced `body_path` exists; `thread_id` resolvable. Log + count violations.
|
|
53
|
+
- All instance writes go through one chokepoint (`upsertInstance`) — no scattered INSERTs.
|
|
54
|
+
- Audit-log every state transfer on move and every reconcile drop (already partly done).
|
|
55
|
+
|
|
56
|
+
## Staged execution (each stage compiles, ships, is verifiable; no broken intermediates)
|
|
57
|
+
1. **Plan + integrity scaffold** (this doc) + `verifyInstances()` read-only checker (no
|
|
58
|
+
behavior change) — measure current violations as a baseline.
|
|
59
|
+
2. **Stop the collapse**: `upsertMessage` creates a new instance per `(folder,uid)`;
|
|
60
|
+
remove cross-folder rebind. Keep same-folder uid-update. Gate behind rebuild.
|
|
61
|
+
3. **Reconcile = server truth** per folder (drop unconfirmed; preserve copies); move-state
|
|
62
|
+
transfer for the unambiguous single-instance move.
|
|
63
|
+
4. **Retire `message_folders`**: point reads back at `messages.folder_id`/`uid`; drop the
|
|
64
|
+
join + its boot migration.
|
|
65
|
+
5. **Copy/Edit**: explicit copy-to-folder (new instance + server COPY) and per-instance
|
|
66
|
+
edit; `edited` flag + display-dedup exclusion.
|
|
67
|
+
6. **Rebuild + verify** on the live DB; confirm copies preserved, accidents gone, 0
|
|
68
|
+
integrity violations.
|
|
69
|
+
|
|
70
|
+
## Open sub-decisions (confirm before stage 2)
|
|
71
|
+
- **A.** Retire `message_folders` (recommended — kills the dual representation) vs keep it.
|
|
72
|
+
- **B.** Move-state transfer for unambiguous moves (recommended — preserves edits) vs
|
|
73
|
+
always-fresh-instance on move (simpler, loses local read-state/edits on a server move).
|
|
74
|
+
- **C.** Do instance **edits push to the server** (APPEND new + delete old), or stay
|
|
75
|
+
local-only annotations? (Affects whether an edit changes the server message_id.)
|