@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.
- package/ai-usage.d.ts +32 -0
- package/ai-usage.d.ts.map +1 -0
- package/ai-usage.js +98 -0
- package/ai-usage.js.map +1 -0
- package/charset.d.ts +15 -0
- package/charset.d.ts.map +1 -0
- package/charset.js +61 -0
- package/charset.js.map +1 -0
- package/db-worker-client.d.ts +32 -0
- package/db-worker-client.d.ts.map +1 -0
- package/db-worker-client.js +66 -0
- package/db-worker-client.js.map +1 -0
- package/db-worker.d.ts +39 -0
- package/db-worker.d.ts.map +1 -0
- package/db-worker.js +115 -0
- package/db-worker.js.map +1 -0
- package/google-sync.d.ts +151 -0
- package/google-sync.d.ts.map +1 -0
- package/google-sync.js +259 -0
- package/google-sync.js.map +1 -0
- package/html-to-docx.d.ts +1 -0
- package/index.d.ts +1102 -0
- package/index.d.ts.map +1 -0
- package/index.js +5476 -0
- package/index.js.map +1 -0
- package/jsonrpc.d.ts +29 -0
- package/jsonrpc.d.ts.map +1 -0
- package/jsonrpc.js +481 -0
- package/jsonrpc.js.map +1 -0
- package/local-store.d.ts +10 -0
- package/local-store.d.ts.map +1 -0
- package/local-store.js +9 -0
- package/local-store.js.map +1 -0
- package/package.json +53 -0
- package/reconciler.d.ts +90 -0
- package/reconciler.d.ts.map +1 -0
- package/reconciler.js +277 -0
- package/reconciler.js.map +1 -0
- package/sync-queue.d.ts +127 -0
- package/sync-queue.d.ts.map +1 -0
- package/sync-queue.js +271 -0
- package/sync-queue.js.map +1 -0
- package/sync-worker-client.d.ts +33 -0
- package/sync-worker-client.d.ts.map +1 -0
- package/sync-worker-client.js +89 -0
- package/sync-worker-client.js.map +1 -0
- package/sync-worker.d.ts +33 -0
- package/sync-worker.d.ts.map +1 -0
- package/sync-worker.js +143 -0
- package/sync-worker.js.map +1 -0
- package/word-html.d.ts +41 -0
- package/word-html.d.ts.map +1 -0
- package/word-html.js +122 -0
- 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":"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"}
|
|
@@ -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":"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"}
|
package/sync-worker.d.ts
ADDED
|
@@ -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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"}
|