/** * Reconciles the local mirror against the server. * * Three sync strategies, picked from what the server advertises: * * - **qresync** modseq for changes, as condstore. The QRESYNC fast path * (VANISHED responses via `mailboxOpen(path, { qresync: true })`, * which would make the UID-set diff below unnecessary) is NOT * implemented — no server available here advertises QRESYNC, * and untested protocol code is worse than one extra SEARCH. * - **condstore** modseq gives changed messages cheaply, but says nothing * about deletions — Gmail is exactly this case, advertising * CONDSTORE and X-GM-EXT-1 but never QRESYNC. Expunges are * caught by a UID-set diff instead. * - **baseline** no change tracking at all: fetch above the UID watermark * for new mail, and diff the UID set for deletions. * * All three are correct today; the tiers differ only in how much they * re-transfer. Detection is still recorded per mailbox so `cache status` can * report it and the QRESYNC path can be added later without a migration. * * The governing invariant is UIDVALIDITY. IMAP guarantees a UID is stable * only while a mailbox's UIDVALIDITY is unchanged; when it changes, every * cached UID silently refers to a different message. So it is checked on * every sync and a mismatch purges the generation before anything else runs. */ import type { IConnectionManager } from '../connections/types.js'; import type CacheStore from './store.js'; import type { SyncTier } from './store.js'; export interface SyncResult { ok: boolean; account: string; mailbox: string; /** Messages inserted or updated. */ stored: number; /** Messages removed because the server no longer lists them. */ removed: number; /** True when the mailbox was purged and rebuilt from scratch. */ epochReset: boolean; error?: string; } /** Options controlling how much of a mailbox the mirror takes on. */ export interface SyncEngineOptions { /** * On the first sync of a mailbox, only messages newer than this many days * are mirrored (0 = everything). Later syncs are incremental from the * watermark, so the window only bounds the initial transfer. */ windowDays?: number; /** * How many message bodies the backfill keeps per mailbox (0 = no body * backfill). Bodies fetched on demand by get_email are stored regardless. */ bodyMessages?: number; /** * The cache byte budget, so prefetching stops before it starts competing * with the eviction pass. */ maxSizeMb?: number; } export default class SyncEngine { private connections; private store; private options; private tiers; /** In-flight probes, so concurrent callers share one round trip. */ private tierPending; /** Serializes syncs per mailbox so two runs cannot interleave. */ private inFlight; /** * Change-event epochs per mailbox. A mailbox is dirty while its counter is * non-zero; a sync snapshots the counter at start and clears it on success * only if no further event arrived mid-run — so an event landing during a * sync that cannot have observed it keeps the mailbox dirty. */ private dirtyEpochs; private listening; private readonly onReconnect; private readonly onNewEmail; private readonly onExpunge; private readonly onFlags; private readonly onChanged; constructor(connections: IConnectionManager, store: CacheStore, options?: SyncEngineOptions); /** Subscribe to change notifications. */ start(): void; stop(): void; /** Record that a mailbox changed and the mirror may be behind. */ markDirty(account: string, mailbox: string): void; /** True while a change event has not yet been absorbed by a successful sync. */ isDirty(account: string, mailbox: string): boolean; /** * Which sync strategy this account's server supports. * * Memoized per account with in-flight dedup — the same pattern * `ImapService.getLabelStrategy` uses — and dropped on `imap:reconnect`, * because capabilities belong to a connection, not to an account. */ detectTier(account: string): Promise; /** * Reconcile one mailbox. * * Never throws and never rethrows: a failed sync leaves the mirror exactly * as it was. A stale row can still answer a question, a deleted one cannot, * so a transient network error must not escalate into data loss. */ syncMailbox(account: string, mailbox: string): Promise; private runSync; /** * Fetch whatever changed since the last sync and store it. * * @returns how many messages were written. */ private fetchChanges; /** * Download bodies for recently-mirrored messages, newest first. * * Runs inside the reconcile loop after each sync: up to `batchLimit` per * cycle, bounded overall by `bodyMessages` per mailbox. Never throws — a * failed backfill leaves the mirror exactly as it was, and every attempted * message is marked so failures are not retried every cycle. * * @returns how many bodies were stored. */ backfillBodies(account: string, mailbox: string, batchLimit?: number): Promise; /** Download and store one body; mark the row attempted on any dead end. */ private fetchBody; /** * Refresh the flags of already-mirrored messages. * * FLAGS-only, ranged from the lowest mirrored UID: cheap next to the * envelope fetch, and the only way a baseline server ever reveals a flag * change on an existing message. * * @returns how many rows changed. */ private sweepFlags; /** * Drop messages the server no longer lists. * * Costs one SEARCH — cheap next to a fetch, and on a baseline-tier server it * is the *only* way a deletion is ever revealed: no modseq, no VANISHED, no * notification. CONDSTORE does not help here either, since modseq tracks * changes to existing messages and says nothing about removed ones. * * @returns how many messages were removed. */ private reconcileDeletions; } //# sourceMappingURL=sync-engine.d.ts.map