@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
package/sync-queue.js ADDED
@@ -0,0 +1,271 @@
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
+ export class SyncQueue {
23
+ db;
24
+ imapManager;
25
+ bodyFetches = new Map();
26
+ /** Per-draft serialization slot. Autosave fires every ~2 s but an IMAP
27
+ * search-delete-append cycle takes longer; unserialized pushes
28
+ * overlapped, each one's "find the stale copy" running before a
29
+ * sibling's "append" — so neither saw the other's new copy and the
30
+ * Drafts folder filled with dozens of duplicates (Bob 2026-05-19).
31
+ * One push in flight per draft; while it runs, only the LATEST queued
32
+ * raw message is kept (intermediate autosaves are superseded). */
33
+ draftPushes = new Map();
34
+ static DRAFT_PUSH_MIN_INTERVAL_MS = 30_000;
35
+ /** Per-account debounce timers. Multiple rapid enqueues (3 flag toggles,
36
+ * bulk move/delete) coalesce into one processSyncActions drain after
37
+ * DRAIN_DEBOUNCE_MS. Was previously in ImapManager.debounceSyncActions;
38
+ * moved here because the queue surface is now the source of truth for
39
+ * "what needs draining" and it's where the rate limit belongs. */
40
+ drainTimers = new Map();
41
+ static DRAIN_DEBOUNCE_MS = 1000;
42
+ constructor(db, imapManager) {
43
+ this.db = db;
44
+ this.imapManager = imapManager;
45
+ }
46
+ scheduleDrain(accountId) {
47
+ const existing = this.drainTimers.get(accountId);
48
+ if (existing)
49
+ clearTimeout(existing);
50
+ this.drainTimers.set(accountId, setTimeout(() => {
51
+ this.drainTimers.delete(accountId);
52
+ this.imapManager.processSyncActions(accountId).catch(() => { });
53
+ }, SyncQueue.DRAIN_DEBOUNCE_MS));
54
+ }
55
+ // ── Message-mirror enqueues (commit local DB row first; then call here) ──
56
+ /** Queue a server-side move. The local rows have already been moved in
57
+ * the DB; the mirror reaches the server on the next reconciler pass. */
58
+ enqueueMove(accountId, uid, fromFolderId, toFolderId) {
59
+ this.db.queueSyncAction(accountId, "move", uid, fromFolderId, { targetFolderId: toFolderId });
60
+ this.scheduleDrain(accountId);
61
+ }
62
+ /** Queue a server-side flag update (\Seen, \Flagged, \Answered, etc.). */
63
+ enqueueFlag(accountId, uid, folderId, flags) {
64
+ this.db.queueSyncAction(accountId, "flags", uid, folderId, { flags });
65
+ this.scheduleDrain(accountId);
66
+ }
67
+ /** Queue a server-side COPY (message stays in the source folder, a copy
68
+ * is APPENDed to the target). processSyncActions handles it via
69
+ * fetch+append. Unlike move, no local row is removed — the source row
70
+ * stays and the target copy is imported by a target-folder sync. */
71
+ enqueueCopy(accountId, uid, fromFolderId, toFolderId) {
72
+ this.db.queueSyncAction(accountId, "copy", uid, fromFolderId, { targetFolderId: toFolderId });
73
+ this.scheduleDrain(accountId);
74
+ }
75
+ /** Queue a server-side delete (or trash, depending on action). */
76
+ enqueueDelete(accountId, uid, folderId, kind = "delete") {
77
+ this.db.queueSyncAction(accountId, kind, uid, folderId, {});
78
+ this.scheduleDrain(accountId);
79
+ }
80
+ /** Cancel a pending MOVE action if it hasn't drained yet. Returns true
81
+ * when an action was found and marked complete (effectively retracted),
82
+ * false when no matching action exists — caller then queues a
83
+ * counter-move (the server already saw the original move).
84
+ *
85
+ * Used by undelete: if the to-trash MOVE is still queued, retract it
86
+ * rather than queue a redundant trash→original. */
87
+ cancelPendingMove(accountId, uid, fromFolderId, targetFolderId) {
88
+ const pending = this.db.findPendingSyncAction(accountId, "move", uid, fromFolderId, targetFolderId);
89
+ if (!pending)
90
+ return false;
91
+ this.db.completeSyncAction(pending.id);
92
+ return true;
93
+ }
94
+ /** Queue an IMAP APPEND of a draft body. Drafts already have crash
95
+ * recovery via the editing/.eml on disk, so this lane is in-memory
96
+ * fire-and-forget for now — the architectural value is centralizing
97
+ * the call site, not adding a second on-disk store for the same
98
+ * bytes. The reconciler retries on transient IMAP failure via the
99
+ * existing sync_actions backoff after first failure. */
100
+ /** 2026-09-16 — Claude Code (Fable 5.1). The UID of the copy currently on
101
+ * the server (and in the local Drafts row) for this draft, as far as the
102
+ * queue knows. The compose window's own previousDraftUid goes stale after
103
+ * the first save (saveDraft returns draftUid null, so it never learns the
104
+ * re-appended UID); the viewer's refresh matched on that stale number and
105
+ * the preview stopped following the draft after the first checkpoint. */
106
+ currentDraftUid(accountId, draftId) {
107
+ return this.draftPushes.get(`${accountId}:${draftId || ""}`)?.lastUid;
108
+ }
109
+ enqueueDraftPush(accountId, rawMessage, previousDraftUid, draftId) {
110
+ const key = `${accountId}:${draftId || ""}`;
111
+ let slot = this.draftPushes.get(key);
112
+ if (!slot) {
113
+ slot = { inFlight: false, pending: null };
114
+ this.draftPushes.set(key, slot);
115
+ }
116
+ // Seed lastUid from the caller only until WE learn a real one. After the
117
+ // first successful append the queue owns the authoritative UID.
118
+ if (slot.lastUid == null && previousDraftUid)
119
+ slot.lastUid = previousDraftUid;
120
+ if (slot.inFlight) {
121
+ // A push is already running for this draft — supersede: keep only
122
+ // the newest content. The in-flight push finishes, then this runs.
123
+ slot.pending = { rawMessage };
124
+ return;
125
+ }
126
+ // Trailing-edge throttle (see slot.lastPushAt comment).
127
+ const sincePush = Date.now() - (slot.lastPushAt || 0);
128
+ if (slot.lastPushAt && sincePush < SyncQueue.DRAFT_PUSH_MIN_INTERVAL_MS) {
129
+ slot.pending = { rawMessage };
130
+ if (!slot.throttleTimer) {
131
+ slot.throttleTimer = setTimeout(() => {
132
+ slot.throttleTimer = null;
133
+ const held = slot.pending;
134
+ slot.pending = null;
135
+ if (held)
136
+ this.enqueueDraftPush(accountId, held.rawMessage, undefined, draftId);
137
+ }, SyncQueue.DRAFT_PUSH_MIN_INTERVAL_MS - sincePush);
138
+ }
139
+ return;
140
+ }
141
+ slot.lastPushAt = Date.now();
142
+ slot.inFlight = true;
143
+ const run = (raw) => {
144
+ // Delete the copy WE last appended (delete-by-UID is reliable) rather
145
+ // than the client's stale/absent previousDraftUid. The slot is NOT
146
+ // deleted between checkpoints, so lastUid persists across the debounce
147
+ // gaps and every save replaces the single server draft.
148
+ this.imapManager.saveDraft(accountId, raw, slot.lastUid, draftId)
149
+ .then((uid) => {
150
+ if (typeof uid === "number" && uid > 0)
151
+ slot.lastUid = uid;
152
+ if (slot.cancelled && typeof uid === "number" && uid > 0) {
153
+ // Send/discard happened while this push was in flight —
154
+ // reap the copy we just created (server + local row).
155
+ console.log(` [sync-queue] draft push landed after send/discard — deleting own append uid=${uid} (${draftId})`);
156
+ this.imapManager.deleteDraft(accountId, uid, draftId)
157
+ .catch((e) => console.error(` [sync-queue] post-cancel draft reap failed: ${e?.message || e}`));
158
+ }
159
+ })
160
+ .catch((e) => {
161
+ console.error(` [sync-queue] draft push deferred (${draftId}): ${e?.message || e}`);
162
+ this.imapManager.emit("draftSaveDeferred", { accountId, draftId, error: String(e?.message || e) });
163
+ })
164
+ .finally(() => {
165
+ const next = slot.pending;
166
+ slot.pending = null;
167
+ if (next) {
168
+ run(next.rawMessage); // drain the coalesced save
169
+ }
170
+ else {
171
+ // Keep the slot (and its lastUid) alive for the next
172
+ // checkpoint — deleting it here was the bug: each new
173
+ // autosave restarted with no known UID and re-appended.
174
+ slot.inFlight = false;
175
+ }
176
+ });
177
+ };
178
+ run(rawMessage);
179
+ }
180
+ /** Drop the retained draft-push slot once a draft is sent or discarded, so
181
+ * its tracked UID doesn't linger for the compose session's lifetime. */
182
+ forgetDraft(accountId, draftId) {
183
+ const slot = this.draftPushes.get(`${accountId}:${draftId || ""}`);
184
+ if (slot?.throttleTimer)
185
+ clearTimeout(slot.throttleTimer);
186
+ if (slot) {
187
+ slot.pending = null; // a held checkpoint must never push post-send
188
+ slot.cancelled = true; // an in-flight push reaps its own append
189
+ }
190
+ this.draftPushes.delete(`${accountId}:${draftId || ""}`);
191
+ }
192
+ /** The AUTHORITATIVE UID of the draft's current local/server row. The
193
+ * local-insert path assigns a fresh UID on every checkpoint, so the
194
+ * client's remembered draftUid goes stale within seconds of typing —
195
+ * cleanup callers MUST read this BEFORE forgetDraft(). Sending with the
196
+ * stale UID left the newest local Drafts row behind until the 30-min
197
+ * reconcile grace finally swept it (Bob 2026-07-12 "sent but still in
198
+ * drafts"). */
199
+ getDraftLastUid(accountId, draftId) {
200
+ return this.draftPushes.get(`${accountId}:${draftId || ""}`)?.lastUid ?? undefined;
201
+ }
202
+ /** Queue an outbox send. Today the outbox/<acct>/*.ltr directory IS
203
+ * the queue and a separate worker drains it; this seam is reserved
204
+ * for when that worker moves under the reconciler. */
205
+ enqueueSend(_accountId, _outboxId) {
206
+ // No-op — outbox dir-based queue stays as-is per plan decision #5.
207
+ }
208
+ // ── Body fetch lane (in-memory; idempotent) ──
209
+ /** Request a body fetch. Dedupes per (account, folder, uid). The
210
+ * `lane` argument hints urgency: `interactive` is a click; `prefetch`
211
+ * is the background backfiller; `backfill` is the one-time post-sync
212
+ * catch-up. Reconciler picks higher priority first. */
213
+ enqueueBodyFetch(accountId, folderId, uid, lane = "interactive") {
214
+ // Reject invalid UIDs at the door. IMAP UIDs start at 1; 0 / negative /
215
+ // non-finite means an upstream code path lost the value (uninitialized
216
+ // envelope, broken join). Without this guard the queue dispatched
217
+ // `UID FETCH 0` to Dovecot, which returned BAD "Invalid uidset" and
218
+ // burned a connection slot. Log with a stack so the caller can be
219
+ // tracked down (Bob 2026-05-25: "lots fetch and prefetch failures").
220
+ if (!uid || !Number.isFinite(uid) || uid <= 0) {
221
+ console.error(`[queue] enqueueBodyFetch rejected invalid uid=${uid} for ${accountId}/folder${folderId}`, new Error("invalid uid trace").stack);
222
+ return;
223
+ }
224
+ const key = `${accountId}:${folderId}:${uid}`;
225
+ const existing = this.bodyFetches.get(key);
226
+ if (existing) {
227
+ // Upgrade lane if the new caller is more urgent.
228
+ const rank = { interactive: 0, sync: 1, prefetch: 2, backfill: 3 };
229
+ if (rank[lane] < rank[existing.lane])
230
+ existing.lane = lane;
231
+ return;
232
+ }
233
+ this.bodyFetches.set(key, { accountId, folderId, uid, lane, attempts: 0 });
234
+ }
235
+ /** Re-enqueue a transient failure — the reconciler calls this after a
236
+ * fetch errored with a transient code. Returns `true` if the item was
237
+ * put back in the queue (still under retry budget) or `false` if its
238
+ * budget is exhausted (caller surfaces the error to the UI). */
239
+ requeueBodyFetch(key, maxAttempts = 3) {
240
+ if (key.attempts + 1 >= maxAttempts)
241
+ return false;
242
+ const k = `${key.accountId}:${key.folderId}:${key.uid}`;
243
+ // Don't downgrade lane on retry; keep the one the original caller set.
244
+ this.bodyFetches.set(k, { ...key, attempts: key.attempts + 1 });
245
+ return true;
246
+ }
247
+ /** Pop the next body-fetch task at or above `maxLane`. Used by the
248
+ * reconciler to drain interactive clicks before prefetch backfill. */
249
+ nextBodyFetch(maxLane = "backfill") {
250
+ const rank = { interactive: 0, sync: 1, prefetch: 2, backfill: 3 };
251
+ const cap = rank[maxLane];
252
+ let best = null;
253
+ for (const v of this.bodyFetches.values()) {
254
+ if (rank[v.lane] > cap)
255
+ continue;
256
+ if (!best || rank[v.lane] < rank[best.lane])
257
+ best = v;
258
+ }
259
+ if (best)
260
+ this.bodyFetches.delete(`${best.accountId}:${best.folderId}:${best.uid}`);
261
+ return best;
262
+ }
263
+ /** Pending count for diagnostics + the sync-status pill. */
264
+ pendingCount() {
265
+ return {
266
+ messageActions: this.db.getTotalPendingSyncCount(),
267
+ bodyFetches: this.bodyFetches.size,
268
+ };
269
+ }
270
+ }
271
+ //# sourceMappingURL=sync-queue.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sync-queue.js","sourceRoot":"","sources":["sync-queue.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAaH,MAAM,OAAO,SAAS;IA8CN;IACA;IA9CJ,WAAW,GAAG,IAAI,GAAG,EAAwB,CAAC;IACtD;;;;;;uEAMmE;IAC3D,WAAW,GAAG,IAAI,GAAG,EA0BzB,CAAC;IACG,MAAM,CAAU,0BAA0B,GAAG,MAAM,CAAC;IAC5D;;;;uEAImE;IAC3D,WAAW,GAAG,IAAI,GAAG,EAAyC,CAAC;IAC/D,MAAM,CAAU,iBAAiB,GAAG,IAAI,CAAC;IAEjD,YACY,EAAW,EACX,WAAwB;QADxB,OAAE,GAAF,EAAE,CAAS;QACX,gBAAW,GAAX,WAAW,CAAa;IACjC,CAAC;IAEI,aAAa,CAAC,SAAiB;QACnC,MAAM,QAAQ,GAAG,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;QACjD,IAAI,QAAQ;YAAE,YAAY,CAAC,QAAQ,CAAC,CAAC;QACrC,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,SAAS,EAAE,UAAU,CAAC,GAAG,EAAE;YAC5C,IAAI,CAAC,WAAW,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;YACnC,IAAI,CAAC,WAAW,CAAC,kBAAkB,CAAC,SAAS,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,GAA+B,CAAC,CAAC,CAAC;QAChG,CAAC,EAAE,SAAS,CAAC,iBAAiB,CAAC,CAAC,CAAC;IACrC,CAAC;IAED,4EAA4E;IAE5E;6EACyE;IACzE,WAAW,CAAC,SAAiB,EAAE,GAAW,EAAE,YAAoB,EAAE,UAAkB;QAChF,IAAI,CAAC,EAAE,CAAC,eAAe,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,YAAY,EAAE,EAAE,cAAc,EAAE,UAAU,EAAE,CAAC,CAAC;QAC9F,IAAI,CAAC,aAAa,CAAC,SAAS,CAAC,CAAC;IAClC,CAAC;IAED,0EAA0E;IAC1E,WAAW,CAAC,SAAiB,EAAE,GAAW,EAAE,QAAgB,EAAE,KAAe;QACzE,IAAI,CAAC,EAAE,CAAC,eAAe,CAAC,SAAS,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC;QACtE,IAAI,CAAC,aAAa,CAAC,SAAS,CAAC,CAAC;IAClC,CAAC;IAED;;;yEAGqE;IACrE,WAAW,CAAC,SAAiB,EAAE,GAAW,EAAE,YAAoB,EAAE,UAAkB;QAChF,IAAI,CAAC,EAAE,CAAC,eAAe,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,YAAY,EAAE,EAAE,cAAc,EAAE,UAAU,EAAE,CAAC,CAAC;QAC9F,IAAI,CAAC,aAAa,CAAC,SAAS,CAAC,CAAC;IAClC,CAAC;IAED,kEAAkE;IAClE,aAAa,CAAC,SAAiB,EAAE,GAAW,EAAE,QAAgB,EAAE,OAA2B,QAAQ;QAC/F,IAAI,CAAC,EAAE,CAAC,eAAe,CAAC,SAAS,EAAE,IAAI,EAAE,GAAG,EAAE,QAAQ,EAAE,EAAE,CAAC,CAAC;QAC5D,IAAI,CAAC,aAAa,CAAC,SAAS,CAAC,CAAC;IAClC,CAAC;IAED;;;;;;wDAMoD;IACpD,iBAAiB,CAAC,SAAiB,EAAE,GAAW,EAAE,YAAoB,EAAE,cAAsB;QAC1F,MAAM,OAAO,GAAG,IAAI,CAAC,EAAE,CAAC,qBAAqB,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,YAAY,EAAE,cAAc,CAAC,CAAC;QACpG,IAAI,CAAC,OAAO;YAAE,OAAO,KAAK,CAAC;QAC3B,IAAI,CAAC,EAAE,CAAC,kBAAkB,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;QACvC,OAAO,IAAI,CAAC;IAChB,CAAC;IAED;;;;;6DAKyD;IACzD;;;;;8EAK0E;IAC1E,eAAe,CAAC,SAAiB,EAAE,OAAe;QAC9C,OAAO,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,GAAG,SAAS,IAAI,OAAO,IAAI,EAAE,EAAE,CAAC,EAAE,OAAO,CAAC;IAC1E,CAAC;IAED,gBAAgB,CAAC,SAAiB,EAAE,UAAkB,EAAE,gBAAyB,EAAE,OAAgB;QAC/F,MAAM,GAAG,GAAG,GAAG,SAAS,IAAI,OAAO,IAAI,EAAE,EAAE,CAAC;QAC5C,IAAI,IAAI,GAAG,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QACrC,IAAI,CAAC,IAAI,EAAE,CAAC;YAAC,IAAI,GAAG,EAAE,QAAQ,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;YAAC,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;QAAC,CAAC;QAC1F,yEAAyE;QACzE,gEAAgE;QAChE,IAAI,IAAI,CAAC,OAAO,IAAI,IAAI,IAAI,gBAAgB;YAAE,IAAI,CAAC,OAAO,GAAG,gBAAgB,CAAC;QAC9E,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;YAChB,kEAAkE;YAClE,mEAAmE;YACnE,IAAI,CAAC,OAAO,GAAG,EAAE,UAAU,EAAE,CAAC;YAC9B,OAAO;QACX,CAAC;QACD,wDAAwD;QACxD,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,CAAC,IAAI,CAAC,UAAU,IAAI,CAAC,CAAC,CAAC;QACtD,IAAI,IAAI,CAAC,UAAU,IAAI,SAAS,GAAG,SAAS,CAAC,0BAA0B,EAAE,CAAC;YACtE,IAAI,CAAC,OAAO,GAAG,EAAE,UAAU,EAAE,CAAC;YAC9B,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,CAAC;gBACtB,IAAI,CAAC,aAAa,GAAG,UAAU,CAAC,GAAG,EAAE;oBACjC,IAAI,CAAC,aAAa,GAAG,IAAI,CAAC;oBAC1B,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC;oBAC1B,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC;oBACpB,IAAI,IAAI;wBAAE,IAAI,CAAC,gBAAgB,CAAC,SAAS,EAAE,IAAI,CAAC,UAAU,EAAE,SAAS,EAAE,OAAO,CAAC,CAAC;gBACpF,CAAC,EAAE,SAAS,CAAC,0BAA0B,GAAG,SAAS,CAAC,CAAC;YACzD,CAAC;YACD,OAAO;QACX,CAAC;QACD,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QAC7B,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC;QACrB,MAAM,GAAG,GAAG,CAAC,GAAW,EAAQ,EAAE;YAC9B,sEAAsE;YACtE,mEAAmE;YACnE,uEAAuE;YACvE,wDAAwD;YACxD,IAAI,CAAC,WAAW,CAAC,SAAS,CAAC,SAAS,EAAE,GAAG,EAAE,IAAK,CAAC,OAAO,EAAE,OAAO,CAAC;iBAC7D,IAAI,CAAC,CAAC,GAAkB,EAAE,EAAE;gBACzB,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,GAAG,CAAC;oBAAE,IAAK,CAAC,OAAO,GAAG,GAAG,CAAC;gBAC5D,IAAI,IAAK,CAAC,SAAS,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,GAAG,CAAC,EAAE,CAAC;oBACxD,wDAAwD;oBACxD,sDAAsD;oBACtD,OAAO,CAAC,GAAG,CAAC,iFAAiF,GAAG,KAAK,OAAO,GAAG,CAAC,CAAC;oBACjH,IAAI,CAAC,WAAW,CAAC,WAAW,CAAC,SAAS,EAAE,GAAG,EAAE,OAAO,CAAC;yBAChD,KAAK,CAAC,CAAC,CAAM,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,iDAAiD,CAAC,EAAE,OAAO,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC;gBAC9G,CAAC;YACL,CAAC,CAAC;iBACD,KAAK,CAAC,CAAC,CAAM,EAAE,EAAE;gBACd,OAAO,CAAC,KAAK,CAAC,uCAAuC,OAAO,MAAM,CAAC,EAAE,OAAO,IAAI,CAAC,EAAE,CAAC,CAAC;gBACrF,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,mBAAmB,EAAE,EAAE,SAAS,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,CAAC,CAAC,EAAE,OAAO,IAAI,CAAC,CAAC,EAAE,CAAC,CAAC;YACvG,CAAC,CAAC;iBACD,OAAO,CAAC,GAAG,EAAE;gBACV,MAAM,IAAI,GAAG,IAAK,CAAC,OAAO,CAAC;gBAC3B,IAAK,CAAC,OAAO,GAAG,IAAI,CAAC;gBACrB,IAAI,IAAI,EAAE,CAAC;oBACP,GAAG,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,CAAG,2BAA2B;gBACvD,CAAC;qBAAM,CAAC;oBACJ,qDAAqD;oBACrD,sDAAsD;oBACtD,wDAAwD;oBACxD,IAAK,CAAC,QAAQ,GAAG,KAAK,CAAC;gBAC3B,CAAC;YACL,CAAC,CAAC,CAAC;QACX,CAAC,CAAC;QACF,GAAG,CAAC,UAAU,CAAC,CAAC;IACpB,CAAC;IAED;6EACyE;IACzE,WAAW,CAAC,SAAiB,EAAE,OAAgB;QAC3C,MAAM,IAAI,GAAG,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,GAAG,SAAS,IAAI,OAAO,IAAI,EAAE,EAAE,CAAC,CAAC;QACnE,IAAI,IAAI,EAAE,aAAa;YAAE,YAAY,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;QAC1D,IAAI,IAAI,EAAE,CAAC;YACP,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,CAAU,8CAA8C;YAC5E,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,CAAQ,yCAAyC;QAC3E,CAAC;QACD,IAAI,CAAC,WAAW,CAAC,MAAM,CAAC,GAAG,SAAS,IAAI,OAAO,IAAI,EAAE,EAAE,CAAC,CAAC;IAC7D,CAAC;IAED;;;;;;oBAMgB;IAChB,eAAe,CAAC,SAAiB,EAAE,OAAgB;QAC/C,OAAO,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,GAAG,SAAS,IAAI,OAAO,IAAI,EAAE,EAAE,CAAC,EAAE,OAAO,IAAI,SAAS,CAAC;IACvF,CAAC;IAED;;2DAEuD;IACvD,WAAW,CAAC,UAAkB,EAAE,SAAiB;QAC7C,mEAAmE;IACvE,CAAC;IAED,gDAAgD;IAEhD;;;4DAGwD;IACxD,gBAAgB,CAAC,SAAiB,EAAE,QAAgB,EAAE,GAAW,EAAE,OAAa,aAAa;QACzF,wEAAwE;QACxE,uEAAuE;QACvE,kEAAkE;QAClE,oEAAoE;QACpE,kEAAkE;QAClE,qEAAqE;QACrE,IAAI,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,GAAG,IAAI,CAAC,EAAE,CAAC;YAC5C,OAAO,CAAC,KAAK,CAAC,iDAAiD,GAAG,QAAQ,SAAS,UAAU,QAAQ,EAAE,EAAE,IAAI,KAAK,CAAC,mBAAmB,CAAC,CAAC,KAAK,CAAC,CAAC;YAC/I,OAAO;QACX,CAAC;QACD,MAAM,GAAG,GAAG,GAAG,SAAS,IAAI,QAAQ,IAAI,GAAG,EAAE,CAAC;QAC9C,MAAM,QAAQ,GAAG,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAC3C,IAAI,QAAQ,EAAE,CAAC;YACX,iDAAiD;YACjD,MAAM,IAAI,GAAyB,EAAE,WAAW,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE,CAAC;YACzF,IAAI,IAAI,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC;gBAAE,QAAQ,CAAC,IAAI,GAAG,IAAI,CAAC;YAC3D,OAAO;QACX,CAAC;QACD,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,QAAQ,EAAE,GAAG,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC,EAAE,CAAC,CAAC;IAC/E,CAAC;IAED;;;qEAGiE;IACjE,gBAAgB,CAAC,GAAiB,EAAE,WAAW,GAAG,CAAC;QAC/C,IAAI,GAAG,CAAC,QAAQ,GAAG,CAAC,IAAI,WAAW;YAAE,OAAO,KAAK,CAAC;QAClD,MAAM,CAAC,GAAG,GAAG,GAAG,CAAC,SAAS,IAAI,GAAG,CAAC,QAAQ,IAAI,GAAG,CAAC,GAAG,EAAE,CAAC;QACxD,uEAAuE;QACvE,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,GAAG,GAAG,EAAE,QAAQ,EAAE,GAAG,CAAC,QAAQ,GAAG,CAAC,EAAE,CAAC,CAAC;QAChE,OAAO,IAAI,CAAC;IAChB,CAAC;IAED;2EACuE;IACvE,aAAa,CAAC,UAAgB,UAAU;QACpC,MAAM,IAAI,GAAyB,EAAE,WAAW,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE,CAAC;QACzF,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,CAAC;QAC1B,IAAI,IAAI,GAAwB,IAAI,CAAC;QACrC,KAAK,MAAM,CAAC,IAAI,IAAI,CAAC,WAAW,CAAC,MAAM,EAAE,EAAE,CAAC;YACxC,IAAI,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,GAAG;gBAAE,SAAS;YACjC,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC;gBAAE,IAAI,GAAG,CAAC,CAAC;QAC1D,CAAC;QACD,IAAI,IAAI;YAAE,IAAI,CAAC,WAAW,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC,SAAS,IAAI,IAAI,CAAC,QAAQ,IAAI,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC;QACpF,OAAO,IAAI,CAAC;IAChB,CAAC;IAED,4DAA4D;IAC5D,YAAY;QACR,OAAO;YACH,cAAc,EAAE,IAAI,CAAC,EAAE,CAAC,wBAAwB,EAAE;YAClD,WAAW,EAAE,IAAI,CAAC,WAAW,CAAC,IAAI;SACrC,CAAC;IACN,CAAC"}
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Main-thread client for the sync factor worker.
3
+ *
4
+ * Spawns `sync-worker.js`, performs the init handshake, and returns a
5
+ * `SyncProxy` that is a DROP-IN for `ImapManager` from the main thread's point
6
+ * of view: it's an EventEmitter (so `bin`'s `.on(...)` forwarders and the
7
+ * service's `.emit(...)` keep working locally), and any *other* method call is
8
+ * forwarded to the worker over the bus as `{method, args}` and returns the
9
+ * worker's result as a Promise.
10
+ *
11
+ * Two method names are handled locally instead of being forwarded:
12
+ * - the EventEmitter surface (on/once/off/emit/…) — local event routing.
13
+ * - getBodyStore() — returns a local FileMessageStore (a path wrapper; the
14
+ * bytes live on disk, so main can read them without crossing the bus).
15
+ *
16
+ * Worker → main events arrive on the bus topic `sync:event` and are re-emitted
17
+ * on the local EventEmitter, so a `syncProgress` raised inside the worker fires
18
+ * bin's listener exactly as before.
19
+ */
20
+ import { Worker } from "node:worker_threads";
21
+ import type { ImapManager } from "@bobfrankston/mailx-imap";
22
+ export interface SpawnedSyncWorker {
23
+ /** Drop-in for ImapManager. Method calls return Promises (forwarded to the
24
+ * worker); event methods are local. */
25
+ proxy: ImapManager;
26
+ worker: Worker;
27
+ close(): Promise<void>;
28
+ }
29
+ export declare function spawnSyncWorker(opts: {
30
+ configDir: string;
31
+ storePath: string;
32
+ }): Promise<SpawnedSyncWorker>;
33
+ //# sourceMappingURL=sync-worker-client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sync-worker-client.d.ts","sourceRoot":"","sources":["sync-worker-client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAM7C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAE5D,MAAM,WAAW,iBAAiB;IAC9B;4CACwC;IACxC,KAAK,EAAE,WAAW,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1B;AAQD,wBAAsB,eAAe,CAAC,IAAI,EAAE;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAiDhH"}
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Main-thread client for the sync factor worker.
3
+ *
4
+ * Spawns `sync-worker.js`, performs the init handshake, and returns a
5
+ * `SyncProxy` that is a DROP-IN for `ImapManager` from the main thread's point
6
+ * of view: it's an EventEmitter (so `bin`'s `.on(...)` forwarders and the
7
+ * service's `.emit(...)` keep working locally), and any *other* method call is
8
+ * forwarded to the worker over the bus as `{method, args}` and returns the
9
+ * worker's result as a Promise.
10
+ *
11
+ * Two method names are handled locally instead of being forwarded:
12
+ * - the EventEmitter surface (on/once/off/emit/…) — local event routing.
13
+ * - getBodyStore() — returns a local FileMessageStore (a path wrapper; the
14
+ * bytes live on disk, so main can read them without crossing the bus).
15
+ *
16
+ * Worker → main events arrive on the bus topic `sync:event` and are re-emitted
17
+ * on the local EventEmitter, so a `syncProgress` raised inside the worker fires
18
+ * bin's listener exactly as before.
19
+ */
20
+ import { Worker } from "node:worker_threads";
21
+ import { EventEmitter } from "node:events";
22
+ import { fileURLToPath } from "node:url";
23
+ import { dirname, join } from "node:path";
24
+ import { WorkerBus } from "@bobfrankston/mailx-bus";
25
+ import { FileMessageStore } from "@bobfrankston/mailx-store";
26
+ const EMITTER_METHODS = new Set([
27
+ "on", "once", "off", "addListener", "removeListener", "removeAllListeners",
28
+ "emit", "listeners", "rawListeners", "listenerCount", "eventNames",
29
+ "prependListener", "prependOnceListener", "setMaxListeners", "getMaxListeners",
30
+ ]);
31
+ export async function spawnSyncWorker(opts) {
32
+ const here = dirname(fileURLToPath(import.meta.url));
33
+ const workerPath = join(here, "sync-worker.js");
34
+ const worker = new Worker(workerPath);
35
+ worker.unref();
36
+ await new Promise((resolve, reject) => {
37
+ const onMessage = (msg) => {
38
+ if (!msg || typeof msg !== "object")
39
+ return;
40
+ if (msg.kind === "init-ok") {
41
+ worker.off("message", onMessage);
42
+ resolve();
43
+ }
44
+ else if (msg.kind === "init-error") {
45
+ worker.off("message", onMessage);
46
+ reject(new Error(`sync-worker init: ${msg.error}`));
47
+ }
48
+ };
49
+ worker.on("message", onMessage);
50
+ worker.on("error", (e) => { worker.off("message", onMessage); reject(e); });
51
+ worker.postMessage({ kind: "init", configDir: opts.configDir, storePath: opts.storePath });
52
+ });
53
+ const bus = new WorkerBus(worker);
54
+ const emitter = new EventEmitter();
55
+ emitter.setMaxListeners(0);
56
+ const localBodyStore = new FileMessageStore(opts.storePath);
57
+ // Worker events → local re-emit (drives bin's WebView forwarders).
58
+ bus.subscribe("sync:event", ({ event, args }) => {
59
+ try {
60
+ emitter.emit(event, ...(args || []));
61
+ }
62
+ catch (e) {
63
+ console.error(` [sync-worker] event '${event}' handler threw: ${e?.message || e}`);
64
+ }
65
+ });
66
+ const proxy = new Proxy(emitter, {
67
+ get(target, prop) {
68
+ if (typeof prop === "string" && EMITTER_METHODS.has(prop)) {
69
+ const v = target[prop];
70
+ return typeof v === "function" ? v.bind(target) : v;
71
+ }
72
+ if (prop === "getBodyStore")
73
+ return () => localBodyStore;
74
+ if (prop === "then")
75
+ return undefined; // not a thenable
76
+ // Everything else is an ImapManager method → forward to the worker.
77
+ // structured-clone turns a returned Node Buffer into a plain
78
+ // Uint8Array; re-wrap so callers that test Buffer.isBuffer / hand
79
+ // the bytes to simpleParser (fetchMessageBody) still work.
80
+ return (...args) => bus.request("sync:call", { method: prop, args }).then((r) => (r instanceof Uint8Array && !Buffer.isBuffer(r)) ? Buffer.from(r.buffer, r.byteOffset, r.byteLength) : r);
81
+ },
82
+ });
83
+ return {
84
+ proxy,
85
+ worker,
86
+ async close() { await worker.terminate(); },
87
+ };
88
+ }
89
+ //# sourceMappingURL=sync-worker-client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sync-worker-client.js","sourceRoot":"","sources":["sync-worker-client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAC7C,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,SAAS,EAAwB,MAAM,yBAAyB,CAAC;AAC1E,OAAO,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAW7D,MAAM,eAAe,GAAG,IAAI,GAAG,CAAC;IAC5B,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,aAAa,EAAE,gBAAgB,EAAE,oBAAoB;IAC1E,MAAM,EAAE,WAAW,EAAE,cAAc,EAAE,eAAe,EAAE,YAAY;IAClE,iBAAiB,EAAE,qBAAqB,EAAE,iBAAiB,EAAE,iBAAiB;CACjF,CAAC,CAAC;AAEH,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,IAA8C;IAChF,MAAM,IAAI,GAAG,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;IACrD,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,EAAE,gBAAgB,CAAC,CAAC;IAChD,MAAM,MAAM,GAAG,IAAI,MAAM,CAAC,UAAU,CAAC,CAAC;IACtC,MAAM,CAAC,KAAK,EAAE,CAAC;IAEf,MAAM,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACxC,MAAM,SAAS,GAAG,CAAC,GAAQ,EAAQ,EAAE;YACjC,IAAI,CAAC,GAAG,IAAI,OAAO,GAAG,KAAK,QAAQ;gBAAE,OAAO;YAC5C,IAAI,GAAG,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;gBAAC,MAAM,CAAC,GAAG,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC;gBAAC,OAAO,EAAE,CAAC;YAAC,CAAC;iBACvE,IAAI,GAAG,CAAC,IAAI,KAAK,YAAY,EAAE,CAAC;gBAAC,MAAM,CAAC,GAAG,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC;gBAAC,MAAM,CAAC,IAAI,KAAK,CAAC,qBAAqB,GAAG,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;YAAC,CAAC;QAClI,CAAC,CAAC;QACF,MAAM,CAAC,EAAE,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC;QAChC,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,CAAQ,EAAE,EAAE,GAAG,MAAM,CAAC,GAAG,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QACnF,MAAM,CAAC,WAAW,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,IAAI,CAAC,SAAS,EAAE,SAAS,EAAE,IAAI,CAAC,SAAS,EAAE,CAAC,CAAC;IAC/F,CAAC,CAAC,CAAC;IAEH,MAAM,GAAG,GAAG,IAAI,SAAS,CAAC,MAAoC,CAAC,CAAC;IAChE,MAAM,OAAO,GAAG,IAAI,YAAY,EAAE,CAAC;IACnC,OAAO,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC;IAC3B,MAAM,cAAc,GAAG,IAAI,gBAAgB,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IAE5D,mEAAmE;IACnE,GAAG,CAAC,SAAS,CAAiC,YAAY,EAAE,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE,EAAE;QAC5E,IAAI,CAAC;YAAC,OAAO,CAAC,IAAI,CAAC,KAAK,EAAE,GAAG,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC,CAAC;QAAC,CAAC;QAAC,OAAO,CAAM,EAAE,CAAC;YAAC,OAAO,CAAC,KAAK,CAAC,0BAA0B,KAAK,oBAAoB,CAAC,EAAE,OAAO,IAAI,CAAC,EAAE,CAAC,CAAC;QAAC,CAAC;IACzJ,CAAC,CAAC,CAAC;IAEH,MAAM,KAAK,GAAG,IAAI,KAAK,CAAC,OAAO,EAAE;QAC7B,GAAG,CAAC,MAAM,EAAE,IAAY;YACpB,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,eAAe,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;gBACxD,MAAM,CAAC,GAAI,MAAc,CAAC,IAAI,CAAC,CAAC;gBAChC,OAAO,OAAO,CAAC,KAAK,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;YACxD,CAAC;YACD,IAAI,IAAI,KAAK,cAAc;gBAAE,OAAO,GAAG,EAAE,CAAC,cAAc,CAAC;YACzD,IAAI,IAAI,KAAK,MAAM;gBAAE,OAAO,SAAS,CAAC,CAAC,iBAAiB;YACxD,oEAAoE;YACpE,6DAA6D;YAC7D,kEAAkE;YAClE,2DAA2D;YAC3D,OAAO,CAAC,GAAG,IAAW,EAAE,EAAE,CAAC,GAAG,CAAC,OAAO,CAAC,WAAW,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAM,EAAE,EAAE,CACxF,CAAC,CAAC,YAAY,UAAU,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,UAAU,EAAE,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAClH,CAAC;KACJ,CAA2B,CAAC;IAE7B,OAAO;QACH,KAAK;QACL,MAAM;QACN,KAAK,CAAC,KAAK,KAAoB,MAAM,MAAM,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC;KAC7D,CAAC;AACN,CAAC"}
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Sync factor — Node worker thread.
3
+ *
4
+ * Hosts the entire network/sync backend (`ImapManager`: IMAP + Gmail sync,
5
+ * body prefetch, the outbox SMTP/IMAP-append drain, IDLE) on its OWN thread,
6
+ * with its OWN write connection to the SQLite file. The main thread keeps the
7
+ * IPC pump and fast local reads (read-worker); it drives this worker through
8
+ * a proxy (`sync-worker-client.ts`) and receives ImapManager's events back
9
+ * over the bus.
10
+ *
11
+ * **Why.** Until now sync ran on the main event loop. A hung Dovecot FETCH, a
12
+ * 30 MB body pull, a dead-network reconnect storm, a 5-minute outbox hang —
13
+ * any of them froze the main loop, which starved the IPC relay, so clicks and
14
+ * the message list timed out even though the read-worker had the answer ready.
15
+ * On its own thread, sync can stall or thrash forever and the UI never feels
16
+ * it (Bob 2026-06-13: "shouldn't it be a worker for any such operation").
17
+ *
18
+ * **DB.** Opens with `{skipMigrations:true}` — main built the schema before we
19
+ * spawned. WAL + busy_timeout let this writer and the main thread's
20
+ * local-first writer coexist; the read-worker reads concurrently.
21
+ *
22
+ * **RPC.** One generic handler: the proxy sends `{method, args}` and we invoke
23
+ * `imapManager[method](...args)`. Return values must be structured-clone-safe
24
+ * (they are — JSON-ish status objects, tokens, bodies-as-strings). Methods
25
+ * that hand back live objects (createPublicClient) are NOT called this way;
26
+ * the service uses dedicated server-side folder-op methods instead.
27
+ *
28
+ * **Events.** We wrap `imapManager.emit` to also `bus.publish("sync:event")`,
29
+ * so every event ImapManager/Reconciler raise on this thread reaches the main
30
+ * proxy, which re-emits them locally for bin's WebView forwarders.
31
+ */
32
+ export {};
33
+ //# sourceMappingURL=sync-worker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sync-worker.d.ts","sourceRoot":"","sources":["sync-worker.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG"}
package/sync-worker.js ADDED
@@ -0,0 +1,143 @@
1
+ /**
2
+ * Sync factor — Node worker thread.
3
+ *
4
+ * Hosts the entire network/sync backend (`ImapManager`: IMAP + Gmail sync,
5
+ * body prefetch, the outbox SMTP/IMAP-append drain, IDLE) on its OWN thread,
6
+ * with its OWN write connection to the SQLite file. The main thread keeps the
7
+ * IPC pump and fast local reads (read-worker); it drives this worker through
8
+ * a proxy (`sync-worker-client.ts`) and receives ImapManager's events back
9
+ * over the bus.
10
+ *
11
+ * **Why.** Until now sync ran on the main event loop. A hung Dovecot FETCH, a
12
+ * 30 MB body pull, a dead-network reconnect storm, a 5-minute outbox hang —
13
+ * any of them froze the main loop, which starved the IPC relay, so clicks and
14
+ * the message list timed out even though the read-worker had the answer ready.
15
+ * On its own thread, sync can stall or thrash forever and the UI never feels
16
+ * it (Bob 2026-06-13: "shouldn't it be a worker for any such operation").
17
+ *
18
+ * **DB.** Opens with `{skipMigrations:true}` — main built the schema before we
19
+ * spawned. WAL + busy_timeout let this writer and the main thread's
20
+ * local-first writer coexist; the read-worker reads concurrently.
21
+ *
22
+ * **RPC.** One generic handler: the proxy sends `{method, args}` and we invoke
23
+ * `imapManager[method](...args)`. Return values must be structured-clone-safe
24
+ * (they are — JSON-ish status objects, tokens, bodies-as-strings). Methods
25
+ * that hand back live objects (createPublicClient) are NOT called this way;
26
+ * the service uses dedicated server-side folder-op methods instead.
27
+ *
28
+ * **Events.** We wrap `imapManager.emit` to also `bus.publish("sync:event")`,
29
+ * so every event ImapManager/Reconciler raise on this thread reaches the main
30
+ * proxy, which re-emits them locally for bin's WebView forwarders.
31
+ */
32
+ import { parentPort } from "node:worker_threads";
33
+ import * as fs from "node:fs";
34
+ import { join } from "node:path";
35
+ import { WorkerBus } from "@bobfrankston/mailx-bus";
36
+ import { MailxDB, FileMessageStore, Store } from "@bobfrankston/mailx-store";
37
+ import { ImapManager } from "@bobfrankston/mailx-imap";
38
+ import { NodeTcpTransport } from "@bobfrankston/node-tcp-transport";
39
+ if (!parentPort) {
40
+ throw new Error("sync-worker: must be spawned as a worker, parentPort is null");
41
+ }
42
+ let imapManager = null;
43
+ let initialized = false;
44
+ parentPort.once("message", async (msg) => {
45
+ if (msg?.kind !== "init") {
46
+ parentPort.postMessage({ kind: "init-error", error: `expected "init", got ${JSON.stringify(msg)}` });
47
+ return;
48
+ }
49
+ try {
50
+ redirectWorkerConsole(msg.configDir);
51
+ const db = new MailxDB(msg.configDir, { skipMigrations: true });
52
+ const store = new Store(db, new FileMessageStore(msg.storePath));
53
+ imapManager = new ImapManager(store, () => new NodeTcpTransport());
54
+ registerHandlers(imapManager);
55
+ initialized = true;
56
+ parentPort.postMessage({ kind: "init-ok" });
57
+ }
58
+ catch (e) {
59
+ parentPort.postMessage({ kind: "init-error", error: e?.message || String(e) });
60
+ }
61
+ });
62
+ /** Route this worker's console into the daemon's daily log file.
63
+ *
64
+ * The main thread monkey-patches ITS console to the log stream
65
+ * (bin/mailx.ts), but a worker_thread's console writes to the process's
66
+ * real stdout — which is `stdio:"ignore"` in detached daemon mode. Every
67
+ * [outbox]/[smtp]/[sent-sweep] line from the sync worker therefore
68
+ * vanished, which blinded the 2026-07-01 resend-loop investigation ("no
69
+ * [outbox] lines in the log" while the loop appended a duplicate to Sent
70
+ * every 5 minutes). Same daily file, `[w]` tag; append-mode writes
71
+ * interleave safely with the main thread's stream. */
72
+ function redirectWorkerConsole(configDir) {
73
+ try {
74
+ const logDir = join(configDir, "logs");
75
+ const pad2 = (n) => n.toString().padStart(2, "0");
76
+ const pad3 = (n) => n.toString().padStart(3, "0");
77
+ const localDate = () => {
78
+ const d = new Date();
79
+ return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`;
80
+ };
81
+ const ts = () => {
82
+ const d = new Date();
83
+ return `${pad2(d.getHours())}:${pad2(d.getMinutes())}:${pad2(d.getSeconds())}.${pad3(d.getMilliseconds())}`;
84
+ };
85
+ let curDate = localDate();
86
+ let stream = fs.createWriteStream(join(logDir, `rmfmail-${curDate}.log`), { flags: "a" });
87
+ const write = (level, a) => {
88
+ // Roll to the new day's file when the date flips (matches the
89
+ // daemon's per-day naming; the daemon itself re-opens on restart).
90
+ const d = localDate();
91
+ if (d !== curDate) {
92
+ try {
93
+ stream.end();
94
+ }
95
+ catch { /* ignore */ }
96
+ curDate = d;
97
+ stream = fs.createWriteStream(join(logDir, `rmfmail-${curDate}.log`), { flags: "a" });
98
+ }
99
+ try {
100
+ stream.write(`${ts()} [w]${level} ${a.map(x => typeof x === "string" ? x : String(x)).join(" ")}\n`);
101
+ }
102
+ catch { /* ignore */ }
103
+ };
104
+ console.log = (...a) => write("", a);
105
+ console.error = (...a) => write(" ERROR", a);
106
+ console.warn = (...a) => write(" WARN", a);
107
+ }
108
+ catch { /* logging must never break the worker */ }
109
+ }
110
+ function registerHandlers(mgr) {
111
+ const bus = new WorkerBus(parentPort);
112
+ // Forward every ImapManager/Reconciler event to the main proxy. Wrap emit
113
+ // so we don't have to enumerate the event list (it drifts); the proxy
114
+ // re-emits locally and bin's forwarders push to the WebView.
115
+ const origEmit = mgr.emit.bind(mgr);
116
+ mgr.emit = (event, ...args) => {
117
+ try {
118
+ bus.publish("sync:event", { event, args });
119
+ }
120
+ catch { /* bus down — local listeners still fire */ }
121
+ return origEmit(event, ...args);
122
+ };
123
+ // Generic method dispatch. Anything the main thread used to call on
124
+ // `imapManager` now arrives here.
125
+ bus.register("sync:call", async ({ method, args }) => {
126
+ const fn = mgr[method];
127
+ if (typeof fn !== "function") {
128
+ throw new Error(`sync-worker: ImapManager has no method '${method}'`);
129
+ }
130
+ return await fn.apply(mgr, args || []);
131
+ });
132
+ }
133
+ // Pre-init guard: a request that races construction gets a clear error, not a hang.
134
+ parentPort.on("message", (msg) => {
135
+ if (!initialized && msg && typeof msg === "object" && msg.kind === "request") {
136
+ parentPort.postMessage({
137
+ kind: "reply",
138
+ id: msg.id,
139
+ error: "sync-worker: not initialized — send {kind:'init'} first",
140
+ });
141
+ }
142
+ });
143
+ //# sourceMappingURL=sync-worker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sync-worker.js","sourceRoot":"","sources":["sync-worker.ts"],"names":[],"mappings":"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"}