@bobfrankston/mailx-service 0.1.47

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.
Files changed (54) hide show
  1. package/ai-usage.d.ts +32 -0
  2. package/ai-usage.d.ts.map +1 -0
  3. package/ai-usage.js +98 -0
  4. package/ai-usage.js.map +1 -0
  5. package/charset.d.ts +15 -0
  6. package/charset.d.ts.map +1 -0
  7. package/charset.js +61 -0
  8. package/charset.js.map +1 -0
  9. package/db-worker-client.d.ts +32 -0
  10. package/db-worker-client.d.ts.map +1 -0
  11. package/db-worker-client.js +66 -0
  12. package/db-worker-client.js.map +1 -0
  13. package/db-worker.d.ts +39 -0
  14. package/db-worker.d.ts.map +1 -0
  15. package/db-worker.js +115 -0
  16. package/db-worker.js.map +1 -0
  17. package/google-sync.d.ts +151 -0
  18. package/google-sync.d.ts.map +1 -0
  19. package/google-sync.js +259 -0
  20. package/google-sync.js.map +1 -0
  21. package/html-to-docx.d.ts +1 -0
  22. package/index.d.ts +1102 -0
  23. package/index.d.ts.map +1 -0
  24. package/index.js +5476 -0
  25. package/index.js.map +1 -0
  26. package/jsonrpc.d.ts +29 -0
  27. package/jsonrpc.d.ts.map +1 -0
  28. package/jsonrpc.js +481 -0
  29. package/jsonrpc.js.map +1 -0
  30. package/local-store.d.ts +10 -0
  31. package/local-store.d.ts.map +1 -0
  32. package/local-store.js +9 -0
  33. package/local-store.js.map +1 -0
  34. package/package.json +53 -0
  35. package/reconciler.d.ts +90 -0
  36. package/reconciler.d.ts.map +1 -0
  37. package/reconciler.js +277 -0
  38. package/reconciler.js.map +1 -0
  39. package/sync-queue.d.ts +127 -0
  40. package/sync-queue.d.ts.map +1 -0
  41. package/sync-queue.js +271 -0
  42. package/sync-queue.js.map +1 -0
  43. package/sync-worker-client.d.ts +33 -0
  44. package/sync-worker-client.d.ts.map +1 -0
  45. package/sync-worker-client.js +89 -0
  46. package/sync-worker-client.js.map +1 -0
  47. package/sync-worker.d.ts +33 -0
  48. package/sync-worker.d.ts.map +1 -0
  49. package/sync-worker.js +143 -0
  50. package/sync-worker.js.map +1 -0
  51. package/word-html.d.ts +41 -0
  52. package/word-html.d.ts.map +1 -0
  53. package/word-html.js +122 -0
  54. package/word-html.js.map +1 -0
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Reconciler — single background loop that drains the SyncQueue, polls
3
+ * for server changes, and emits state events the UI listens for.
4
+ *
5
+ * Local-first contract: the Reconciler is the ONLY code path that touches
6
+ * IMAP / Gmail API on behalf of UI actions. UI handlers commit locally
7
+ * and enqueue; the Reconciler picks up and mirrors. Reads from the UI
8
+ * never await the Reconciler.
9
+ *
10
+ * What this owns today:
11
+ * - body-fetch lane: drains `SyncQueue.nextBodyFetch()`, calls
12
+ * `imapManager.fetchMessageBody`, emits `bodyAvailable` / `bodyFetchError`.
13
+ * This is the new piece; previously body fetches happened inline on
14
+ * every getMessage call.
15
+ * - sync-state pill events: emits `syncStateChanged` so the UI can show
16
+ * "Sync OK / Syncing N items".
17
+ *
18
+ * What it delegates (intentional — no duplication):
19
+ * - message-action drain: ImapManager already runs a 30s actionsInterval
20
+ * that processes both outbox (processSendActions) and sync_actions
21
+ * (processSyncActions) per account. Per plan decision #5, outbox stays
22
+ * under ImapManager; we don't fork that work.
23
+ * - poll loops (per-account quick check, full sync, prefetch, tombstone
24
+ * prune): all owned by ImapManager's startPeriodicSync. The Reconciler
25
+ * does NOT shadow them — back-pressure on those timers can be added
26
+ * later via a single `setBackPressure(level)` hook.
27
+ *
28
+ * Part of docs/local-first-plan.md (step 4).
29
+ */
30
+ import type { ImapManager } from "@bobfrankston/mailx-imap";
31
+ import type { MailxDB } from "@bobfrankston/mailx-store";
32
+ import type { SyncQueue } from "./sync-queue.js";
33
+ export interface ReconcilerOptions {
34
+ /** Max concurrent body fetches across all accounts (in addition to
35
+ * per-account semaphore in ImapManager). Keeps the interactive lane
36
+ * responsive even when prefetch is hammering. */
37
+ bodyFetchConcurrency?: number;
38
+ /** Periodic emit of syncStateChanged — drives the UI status pill. */
39
+ statusEmitIntervalMs?: number;
40
+ /** How often to fire the prefetch tick. Default 60s. */
41
+ prefetchIntervalMs?: number;
42
+ /** First prefetch fires this soon after start so the "not downloaded"
43
+ * dots fill in without making the user wait a minute. */
44
+ prefetchInitialDelayMs?: number;
45
+ /** Skip prefetch when the interactive body-fetch lane has at least
46
+ * this many items pending. Back-pressure keeps clicks responsive. */
47
+ prefetchBackpressureThreshold?: number;
48
+ /** Tombstone prune interval — bookkeeping only, hourly default. */
49
+ tombstonePruneIntervalMs?: number;
50
+ /** Tombstone retention — local-delete records older than this are
51
+ * removed by the periodic prune. */
52
+ tombstoneRetentionDays?: number;
53
+ }
54
+ export declare class Reconciler {
55
+ private db;
56
+ private imapManager;
57
+ private queue;
58
+ private opts;
59
+ private running;
60
+ private statusTimer;
61
+ private prefetchTimer;
62
+ private tombstoneTimer;
63
+ private inFlightFetches;
64
+ /** Last status payload — used to suppress no-op syncStateChanged events.
65
+ * During a heavy multi-folder sync the 5s tick fires a lot of identical
66
+ * events; the WebView's event handler runs DOM work for each, which
67
+ * stacks up under load. Diff-only emits keep the channel quiet. */
68
+ private lastStatus;
69
+ constructor(db: MailxDB, imapManager: ImapManager, queue: SyncQueue, opts?: ReconcilerOptions);
70
+ start(): void;
71
+ stop(): void;
72
+ /** Hourly bookkeeping — drop tombstones older than the retention window
73
+ * so the deleted-message lookup stays fast. */
74
+ private runTombstonePrune;
75
+ /** Fire prefetchBodies on each account unless the interactive body-fetch
76
+ * lane has user-clicked work pending — in which case skip this tick.
77
+ * ImapManager's per-account `prefetchingAccounts` guard de-dupes if
78
+ * the previous tick is still in flight. */
79
+ private runPrefetchTick;
80
+ /** User-visible "sync now" — runs an immediate poll on all accounts
81
+ * (or just one) without waiting for the next periodic tick. */
82
+ syncNow(accountId?: string): void;
83
+ /** Drain the body-fetch lane up to `bodyFetchConcurrency`. Each
84
+ * fetch is fire-and-forget; on completion it kicks the pump again
85
+ * so a queue that grows during work stays drained. */
86
+ private pumpBodyFetches;
87
+ /** External nudge — call after enqueueing a high-priority body fetch. */
88
+ kick(): void;
89
+ }
90
+ //# sourceMappingURL=reconciler.d.ts.map
@@ -0,0 +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;IAmHvB,yEAAyE;IACzE,IAAI,IAAI,IAAI;CAGf"}
package/reconciler.js ADDED
@@ -0,0 +1,277 @@
1
+ /**
2
+ * Reconciler — single background loop that drains the SyncQueue, polls
3
+ * for server changes, and emits state events the UI listens for.
4
+ *
5
+ * Local-first contract: the Reconciler is the ONLY code path that touches
6
+ * IMAP / Gmail API on behalf of UI actions. UI handlers commit locally
7
+ * and enqueue; the Reconciler picks up and mirrors. Reads from the UI
8
+ * never await the Reconciler.
9
+ *
10
+ * What this owns today:
11
+ * - body-fetch lane: drains `SyncQueue.nextBodyFetch()`, calls
12
+ * `imapManager.fetchMessageBody`, emits `bodyAvailable` / `bodyFetchError`.
13
+ * This is the new piece; previously body fetches happened inline on
14
+ * every getMessage call.
15
+ * - sync-state pill events: emits `syncStateChanged` so the UI can show
16
+ * "Sync OK / Syncing N items".
17
+ *
18
+ * What it delegates (intentional — no duplication):
19
+ * - message-action drain: ImapManager already runs a 30s actionsInterval
20
+ * that processes both outbox (processSendActions) and sync_actions
21
+ * (processSyncActions) per account. Per plan decision #5, outbox stays
22
+ * under ImapManager; we don't fork that work.
23
+ * - poll loops (per-account quick check, full sync, prefetch, tombstone
24
+ * prune): all owned by ImapManager's startPeriodicSync. The Reconciler
25
+ * does NOT shadow them — back-pressure on those timers can be added
26
+ * later via a single `setBackPressure(level)` hook.
27
+ *
28
+ * Part of docs/local-first-plan.md (step 4).
29
+ */
30
+ import { storeBus } from "@bobfrankston/mailx-store";
31
+ const DEFAULTS = {
32
+ bodyFetchConcurrency: 2,
33
+ statusEmitIntervalMs: 5_000,
34
+ prefetchIntervalMs: 60_000,
35
+ prefetchInitialDelayMs: 2_000,
36
+ prefetchBackpressureThreshold: 1,
37
+ tombstonePruneIntervalMs: 3600_000, // hourly
38
+ tombstoneRetentionDays: 30,
39
+ };
40
+ export class Reconciler {
41
+ db;
42
+ imapManager;
43
+ queue;
44
+ opts;
45
+ running = false;
46
+ statusTimer = null;
47
+ prefetchTimer = null;
48
+ tombstoneTimer = null;
49
+ inFlightFetches = 0;
50
+ /** Last status payload — used to suppress no-op syncStateChanged events.
51
+ * During a heavy multi-folder sync the 5s tick fires a lot of identical
52
+ * events; the WebView's event handler runs DOM work for each, which
53
+ * stacks up under load. Diff-only emits keep the channel quiet. */
54
+ lastStatus = { messageActions: -1, bodyFetches: -1, inFlightFetches: -1 };
55
+ constructor(db, imapManager, queue, opts = {}) {
56
+ this.db = db;
57
+ this.imapManager = imapManager;
58
+ this.queue = queue;
59
+ this.opts = { ...DEFAULTS, ...opts };
60
+ }
61
+ start() {
62
+ if (this.running)
63
+ return;
64
+ this.running = true;
65
+ // Status pill: emit current totals every few seconds so the UI
66
+ // can show "Syncing N items" without polling. Diff-only — emit
67
+ // ONLY when something actually changed since the last tick so
68
+ // we don't pipe a constant idle stream through the WebView's
69
+ // event channel during a heavy sync (where the WebView main
70
+ // thread is already busy painting list updates).
71
+ this.statusTimer = setInterval(() => {
72
+ const counts = this.queue.pendingCount();
73
+ const next = { ...counts, inFlightFetches: this.inFlightFetches };
74
+ if (next.messageActions === this.lastStatus.messageActions
75
+ && next.bodyFetches === this.lastStatus.bodyFetches
76
+ && next.inFlightFetches === this.lastStatus.inFlightFetches) {
77
+ return;
78
+ }
79
+ this.lastStatus = next;
80
+ this.imapManager.emit("syncStateChanged", next);
81
+ }, this.opts.statusEmitIntervalMs);
82
+ // Periodic prefetch tick — owned here (not in ImapManager) so it
83
+ // can back off when the interactive body-fetch lane has work.
84
+ // First tick fires soon after startup so "not downloaded" dots
85
+ // start filling in promptly.
86
+ const prefetchTick = () => this.runPrefetchTick();
87
+ setTimeout(prefetchTick, this.opts.prefetchInitialDelayMs);
88
+ this.prefetchTimer = setInterval(prefetchTick, this.opts.prefetchIntervalMs);
89
+ // Tombstone prune — bookkeeping only. Trivial cost (one indexed
90
+ // DELETE per hour), kept off the IMAP-touching code path because
91
+ // it has nothing to do with sync.
92
+ const pruneTick = () => this.runTombstonePrune();
93
+ setTimeout(pruneTick, 30_000); // first run after startup settles
94
+ this.tombstoneTimer = setInterval(pruneTick, this.opts.tombstonePruneIntervalMs);
95
+ // Kick the body-fetch loop.
96
+ this.pumpBodyFetches();
97
+ }
98
+ stop() {
99
+ this.running = false;
100
+ if (this.statusTimer) {
101
+ clearInterval(this.statusTimer);
102
+ this.statusTimer = null;
103
+ }
104
+ if (this.prefetchTimer) {
105
+ clearInterval(this.prefetchTimer);
106
+ this.prefetchTimer = null;
107
+ }
108
+ if (this.tombstoneTimer) {
109
+ clearInterval(this.tombstoneTimer);
110
+ this.tombstoneTimer = null;
111
+ }
112
+ }
113
+ /** Hourly bookkeeping — drop tombstones older than the retention window
114
+ * so the deleted-message lookup stays fast. */
115
+ runTombstonePrune() {
116
+ const cutoff = Date.now() - this.opts.tombstoneRetentionDays * 86400_000;
117
+ const n = this.db.pruneTombstones(cutoff);
118
+ if (n > 0)
119
+ console.log(` [tombstones] pruned ${n} older than ${this.opts.tombstoneRetentionDays} days`);
120
+ }
121
+ /** Fire prefetchBodies on each account unless the interactive body-fetch
122
+ * lane has user-clicked work pending — in which case skip this tick.
123
+ * ImapManager's per-account `prefetchingAccounts` guard de-dupes if
124
+ * the previous tick is still in flight. */
125
+ runPrefetchTick() {
126
+ if (!this.running)
127
+ return;
128
+ const counts = this.queue.pendingCount();
129
+ if (counts.bodyFetches >= this.opts.prefetchBackpressureThreshold) {
130
+ // Interactive work pending — let it drain. Next tick reconsiders.
131
+ return;
132
+ }
133
+ for (const acct of this.db.getAccounts()) {
134
+ this.imapManager.prefetchBodies(acct.id).catch(e => console.error(` [prefetch] ${acct.id}: ${e?.message || e}`));
135
+ // Local-disk reparse runs in the same tick as prefetch — same
136
+ // back-pressure rules apply (interactive lane has priority).
137
+ // Pure local work, no IMAP / Gmail API touch.
138
+ this.imapManager.backfillBodyMeta(acct.id).catch(e => console.error(` [backfill] ${acct.id}: ${e?.message || e}`));
139
+ }
140
+ }
141
+ /** User-visible "sync now" — runs an immediate poll on all accounts
142
+ * (or just one) without waiting for the next periodic tick. */
143
+ syncNow(accountId) {
144
+ const accts = accountId ? [{ id: accountId }] : this.db.getAccounts();
145
+ for (const a of accts) {
146
+ this.imapManager.processSyncActions(a.id).catch(() => { });
147
+ }
148
+ }
149
+ /** Drain the body-fetch lane up to `bodyFetchConcurrency`. Each
150
+ * fetch is fire-and-forget; on completion it kicks the pump again
151
+ * so a queue that grows during work stays drained. */
152
+ pumpBodyFetches() {
153
+ if (!this.running)
154
+ return;
155
+ while (this.inFlightFetches < this.opts.bodyFetchConcurrency) {
156
+ const next = this.queue.nextBodyFetch();
157
+ if (!next)
158
+ return;
159
+ this.inFlightFetches++;
160
+ (async () => {
161
+ try {
162
+ // Wall-clock cap so a wedged fetchMessageBody (TCP half-
163
+ // open, server stalled mid-FETCH, parser blocked on a
164
+ // literal that never completes) can't pin inFlightFetches
165
+ // forever. Without this, one stuck UID drains the
166
+ // concurrency budget and every subsequent click sits
167
+ // behind it for the lifetime of the daemon. 90 s is
168
+ // generous (slow Dovecot reads of a 10 MB body legitimately
169
+ // approach 60 s) but bounds the wedge — beyond 90 s the
170
+ // retry path is more likely to recover than waiting on
171
+ // the same dead socket.
172
+ const FETCH_TIMEOUT_MS = 90_000;
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";
179
+ const raw = await Promise.race([
180
+ this.imapManager.fetchMessageBody(next.accountId, next.folderId, next.uid, force),
181
+ new Promise((_, rej) => {
182
+ timeoutHandle = setTimeout(() => rej(new Error(`body fetch timeout after ${FETCH_TIMEOUT_MS / 1000}s`)), FETCH_TIMEOUT_MS);
183
+ }),
184
+ ]);
185
+ if (timeoutHandle)
186
+ clearTimeout(timeoutHandle);
187
+ if (raw) {
188
+ // Look up uuid for the message topic; the body has just
189
+ // landed, so the DB row's body_path was updated by
190
+ // fetchMessageBody and the envelope is fresh.
191
+ const env = this.db.getMessageByUid(next.accountId, next.uid, next.folderId);
192
+ const msgUuid = env?.uuid;
193
+ storeBus.publish({
194
+ topic: msgUuid ? `message:${msgUuid}` : `folder:${next.folderId}`,
195
+ kind: "bodyAvailable",
196
+ accountId: next.accountId, folderId: next.folderId, uid: next.uid, msgUuid,
197
+ });
198
+ }
199
+ else {
200
+ // fetchMessageBody returned null without throwing.
201
+ // Causes seen in production: Gmail format=raw cap on
202
+ // big messages (~10MB), UID hash collision picking a
203
+ // stub, IMAP source extraction got a non-error empty
204
+ // response. Without an event the viewer hangs on
205
+ // "preview placeholder" forever — surface as a
206
+ // (non-transient) bodyFetchError so the message-
207
+ // viewer's listener can show a clear failure banner
208
+ // instead of a silent permanent blank.
209
+ const env = this.db.getMessageByUid(next.accountId, next.uid, next.folderId);
210
+ const msgUuid = env?.uuid;
211
+ storeBus.publish({
212
+ topic: msgUuid ? `message:${msgUuid}` : `folder:${next.folderId}`,
213
+ kind: "bodyFetchError",
214
+ accountId: next.accountId, folderId: next.folderId, uid: next.uid, msgUuid,
215
+ error: "body fetch returned no content (message too large for the API or stub)",
216
+ transient: false,
217
+ });
218
+ }
219
+ }
220
+ catch (err) {
221
+ if (err?.isNotFound) {
222
+ try {
223
+ const env = this.db.getMessageByUid(next.accountId, next.uid, next.folderId);
224
+ const msgUuid = env?.uuid;
225
+ this.db.deleteMessage(next.accountId, next.folderId, next.uid, "reconciler: server returned isNotFound", "mailx-service reconciler");
226
+ this.db.recalcFolderCounts(next.folderId);
227
+ storeBus.publish({
228
+ topic: msgUuid ? `message:${msgUuid}` : `folder:${next.folderId}`,
229
+ kind: "messageRemoved",
230
+ accountId: next.accountId, folderId: next.folderId, uid: next.uid, msgUuid,
231
+ });
232
+ }
233
+ catch { /* */ }
234
+ }
235
+ else {
236
+ const msg = err?.message || "body fetch failed";
237
+ // `IMAP command leaked into response stream` is the
238
+ // file-store's defensive rejection of a desync'd
239
+ // body. Treat as transient: the next fetch on a
240
+ // fresh client will likely produce a clean payload.
241
+ const transient = /connection|Too many|UNAVAILABLE|rate|429|5\d\d|timeout|ENOTFOUND|ECONNRESET|ETIMEDOUT|IMAP command leaked/i.test(msg);
242
+ // Transient: re-enqueue (budget=3) so the next pump
243
+ // tick retries. Surface the error only when the
244
+ // budget is exhausted — otherwise we'd flash banners
245
+ // for every blip on a slow server.
246
+ if (transient && this.queue.requeueBodyFetch(next)) {
247
+ // Backoff so we don't immediately re-hit the same
248
+ // wedge. Body-fetch lane is in-memory; setTimeout
249
+ // is fine.
250
+ const backoffMs = 500 * Math.pow(2, next.attempts);
251
+ setTimeout(() => this.pumpBodyFetches(), backoffMs);
252
+ }
253
+ else {
254
+ const env = this.db.getMessageByUid(next.accountId, next.uid, next.folderId);
255
+ const msgUuid = env?.uuid;
256
+ storeBus.publish({
257
+ topic: msgUuid ? `message:${msgUuid}` : `folder:${next.folderId}`,
258
+ kind: "bodyFetchError",
259
+ accountId: next.accountId, folderId: next.folderId, uid: next.uid, msgUuid,
260
+ error: msg, transient,
261
+ });
262
+ }
263
+ }
264
+ }
265
+ finally {
266
+ this.inFlightFetches--;
267
+ this.pumpBodyFetches();
268
+ }
269
+ })();
270
+ }
271
+ }
272
+ /** External nudge — call after enqueueing a high-priority body fetch. */
273
+ kick() {
274
+ this.pumpBodyFetches();
275
+ }
276
+ }
277
+ //# sourceMappingURL=reconciler.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reconciler.js","sourceRoot":"","sources":["reconciler.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,127 @@
1
+ /**
2
+ * SyncQueue — the formal enqueue surface for server-side mirroring of
3
+ * local actions.
4
+ *
5
+ * Local-first contract: every UI write commits to the local store first,
6
+ * then hands off here. Nothing in this file does network I/O; the
7
+ * Reconciler drains the queue.
8
+ *
9
+ * Implementation note: the underlying persistence already exists. Message
10
+ * ops live in `sync_actions` (drained by ImapManager.processSyncActions),
11
+ * non-message ops in `store_sync` (drained by the per-domain workers).
12
+ * This class is the public API surface — call sites switch to it so the
13
+ * architecture's seam is visible, the rules are enforceable, and we can
14
+ * later bolt on body-fetch / draft-push / send-push lanes without
15
+ * touching every call site again.
16
+ *
17
+ * Priority lanes (interactive > sync > prefetch > backfill) are tracked
18
+ * here as ordering hints; the actual scheduler is in the Reconciler.
19
+ *
20
+ * Part of docs/local-first-plan.md (steps 3-4).
21
+ */
22
+ import type { MailxDB } from "@bobfrankston/mailx-store";
23
+ import type { ImapManager } from "@bobfrankston/mailx-imap";
24
+ export type Lane = "interactive" | "sync" | "prefetch" | "backfill";
25
+ /** In-memory body-fetch dedupe + ordering. We keep this as a Set rather
26
+ * than persisted rows because body fetches are idempotent (the file is
27
+ * named after the (folder, uid) pair) and a re-fetch on next click is
28
+ * cheap if a process crash drops the queued item. */
29
+ interface BodyFetchKey {
30
+ accountId: string;
31
+ folderId: number;
32
+ uid: number;
33
+ lane: Lane;
34
+ attempts: number;
35
+ }
36
+ export declare class SyncQueue {
37
+ private db;
38
+ private imapManager;
39
+ private bodyFetches;
40
+ /** Per-draft serialization slot. Autosave fires every ~2 s but an IMAP
41
+ * search-delete-append cycle takes longer; unserialized pushes
42
+ * overlapped, each one's "find the stale copy" running before a
43
+ * sibling's "append" — so neither saw the other's new copy and the
44
+ * Drafts folder filled with dozens of duplicates (Bob 2026-05-19).
45
+ * One push in flight per draft; while it runs, only the LATEST queued
46
+ * raw message is kept (intermediate autosaves are superseded). */
47
+ private draftPushes;
48
+ private static readonly DRAFT_PUSH_MIN_INTERVAL_MS;
49
+ /** Per-account debounce timers. Multiple rapid enqueues (3 flag toggles,
50
+ * bulk move/delete) coalesce into one processSyncActions drain after
51
+ * DRAIN_DEBOUNCE_MS. Was previously in ImapManager.debounceSyncActions;
52
+ * moved here because the queue surface is now the source of truth for
53
+ * "what needs draining" and it's where the rate limit belongs. */
54
+ private drainTimers;
55
+ private static readonly DRAIN_DEBOUNCE_MS;
56
+ constructor(db: MailxDB, imapManager: ImapManager);
57
+ private scheduleDrain;
58
+ /** Queue a server-side move. The local rows have already been moved in
59
+ * the DB; the mirror reaches the server on the next reconciler pass. */
60
+ enqueueMove(accountId: string, uid: number, fromFolderId: number, toFolderId: number): void;
61
+ /** Queue a server-side flag update (\Seen, \Flagged, \Answered, etc.). */
62
+ enqueueFlag(accountId: string, uid: number, folderId: number, flags: string[]): void;
63
+ /** Queue a server-side COPY (message stays in the source folder, a copy
64
+ * is APPENDed to the target). processSyncActions handles it via
65
+ * fetch+append. Unlike move, no local row is removed — the source row
66
+ * stays and the target copy is imported by a target-folder sync. */
67
+ enqueueCopy(accountId: string, uid: number, fromFolderId: number, toFolderId: number): void;
68
+ /** Queue a server-side delete (or trash, depending on action). */
69
+ enqueueDelete(accountId: string, uid: number, folderId: number, kind?: "delete" | "trash"): void;
70
+ /** Cancel a pending MOVE action if it hasn't drained yet. Returns true
71
+ * when an action was found and marked complete (effectively retracted),
72
+ * false when no matching action exists — caller then queues a
73
+ * counter-move (the server already saw the original move).
74
+ *
75
+ * Used by undelete: if the to-trash MOVE is still queued, retract it
76
+ * rather than queue a redundant trash→original. */
77
+ cancelPendingMove(accountId: string, uid: number, fromFolderId: number, targetFolderId: number): boolean;
78
+ /** Queue an IMAP APPEND of a draft body. Drafts already have crash
79
+ * recovery via the editing/.eml on disk, so this lane is in-memory
80
+ * fire-and-forget for now — the architectural value is centralizing
81
+ * the call site, not adding a second on-disk store for the same
82
+ * bytes. The reconciler retries on transient IMAP failure via the
83
+ * existing sync_actions backoff after first failure. */
84
+ /** 2026-09-16 — Claude Code (Fable 5.1). The UID of the copy currently on
85
+ * the server (and in the local Drafts row) for this draft, as far as the
86
+ * queue knows. The compose window's own previousDraftUid goes stale after
87
+ * the first save (saveDraft returns draftUid null, so it never learns the
88
+ * re-appended UID); the viewer's refresh matched on that stale number and
89
+ * the preview stopped following the draft after the first checkpoint. */
90
+ currentDraftUid(accountId: string, draftId: string): number | undefined;
91
+ enqueueDraftPush(accountId: string, rawMessage: string, previousDraftUid?: number, draftId?: string): void;
92
+ /** Drop the retained draft-push slot once a draft is sent or discarded, so
93
+ * its tracked UID doesn't linger for the compose session's lifetime. */
94
+ forgetDraft(accountId: string, draftId?: string): void;
95
+ /** The AUTHORITATIVE UID of the draft's current local/server row. The
96
+ * local-insert path assigns a fresh UID on every checkpoint, so the
97
+ * client's remembered draftUid goes stale within seconds of typing —
98
+ * cleanup callers MUST read this BEFORE forgetDraft(). Sending with the
99
+ * stale UID left the newest local Drafts row behind until the 30-min
100
+ * reconcile grace finally swept it (Bob 2026-07-12 "sent but still in
101
+ * drafts"). */
102
+ getDraftLastUid(accountId: string, draftId?: string): number | undefined;
103
+ /** Queue an outbox send. Today the outbox/<acct>/*.ltr directory IS
104
+ * the queue and a separate worker drains it; this seam is reserved
105
+ * for when that worker moves under the reconciler. */
106
+ enqueueSend(_accountId: string, _outboxId: string): void;
107
+ /** Request a body fetch. Dedupes per (account, folder, uid). The
108
+ * `lane` argument hints urgency: `interactive` is a click; `prefetch`
109
+ * is the background backfiller; `backfill` is the one-time post-sync
110
+ * catch-up. Reconciler picks higher priority first. */
111
+ enqueueBodyFetch(accountId: string, folderId: number, uid: number, lane?: Lane): void;
112
+ /** Re-enqueue a transient failure — the reconciler calls this after a
113
+ * fetch errored with a transient code. Returns `true` if the item was
114
+ * put back in the queue (still under retry budget) or `false` if its
115
+ * budget is exhausted (caller surfaces the error to the UI). */
116
+ requeueBodyFetch(key: BodyFetchKey, maxAttempts?: number): boolean;
117
+ /** Pop the next body-fetch task at or above `maxLane`. Used by the
118
+ * reconciler to drain interactive clicks before prefetch backfill. */
119
+ nextBodyFetch(maxLane?: Lane): BodyFetchKey | null;
120
+ /** Pending count for diagnostics + the sync-status pill. */
121
+ pendingCount(): {
122
+ messageActions: number;
123
+ bodyFetches: number;
124
+ };
125
+ }
126
+ export {};
127
+ //# sourceMappingURL=sync-queue.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sync-queue.d.ts","sourceRoot":"","sources":["sync-queue.ts"],"names":[],"mappings":"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"}