@bobfrankston/mailx 1.0.306 → 1.0.310

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.
@@ -33,8 +33,8 @@ function emitEvent(event) {
33
33
  }
34
34
  catch { /* ignore */ }
35
35
  }
36
- if (typeof window._mailxapiEvent === "function") {
37
- window._mailxapiEvent(event);
36
+ if (typeof window._msgapiServiceEvent === "function") {
37
+ window._msgapiServiceEvent(event);
38
38
  }
39
39
  }
40
40
  // ── Helpers ──
@@ -54,6 +54,10 @@ class AndroidSyncManager {
54
54
  bodyStore;
55
55
  providers = new Map();
56
56
  tokenProviders = new Map();
57
+ // One prefetch session per account — prevents every syncAll tick from
58
+ // spawning parallel fetch loops that race on IndexedDB and blow through
59
+ // Gmail's per-user quota.
60
+ prefetchingAccounts = new Set();
57
61
  constructor(db, bodyStore) {
58
62
  this.db = db;
59
63
  this.bodyStore = bodyStore;
@@ -150,6 +154,91 @@ class AndroidSyncManager {
150
154
  emitEvent({ type: "syncError", accountId: account.id, error: e.message });
151
155
  }
152
156
  }
157
+ // Phase 3: background body prefetch. Fire-and-forget — sync itself is
158
+ // already done and the UI doesn't wait on this. Per-account guard means
159
+ // a slow account can't block a fast one.
160
+ for (const account of accounts) {
161
+ if (!this.providers.has(account.id))
162
+ continue;
163
+ this.prefetchBodies(account.id).catch(e => console.error(`[prefetch] ${account.id}: ${e.message}`));
164
+ }
165
+ }
166
+ /** Background body prefetch — download bodies for messages that don't have
167
+ * them yet, so tapping a message in the list opens instantly from cache. */
168
+ async prefetchBodies(accountId) {
169
+ if (this.prefetchingAccounts.has(accountId))
170
+ return;
171
+ this.prefetchingAccounts.add(accountId);
172
+ try {
173
+ const BATCH_SIZE = 20;
174
+ const THROTTLE_MS = 150;
175
+ const RATE_LIMIT_PAUSE_MS = 30000;
176
+ const ERROR_BUDGET = 10;
177
+ let totalFetched = 0;
178
+ let errors = 0;
179
+ let announced = false;
180
+ while (true) {
181
+ const missing = this.db.getMessagesWithoutBody(accountId, BATCH_SIZE);
182
+ if (missing.length === 0)
183
+ break;
184
+ if (!announced) {
185
+ console.log(`[prefetch] ${accountId}: ${missing.length}+ bodies to fetch`);
186
+ vlog(`prefetch ${accountId} start: ${missing.length}+ pending`);
187
+ announced = true;
188
+ }
189
+ let progressedThisBatch = false;
190
+ for (const m of missing) {
191
+ // Sync the DB path if the body is already in IndexedDB — common
192
+ // when upgrading from a build that didn't set body_path on cache.
193
+ if (await this.bodyStore.hasMessage(accountId, m.folderId, m.uid)) {
194
+ this.db.updateBodyPath(accountId, m.uid, `idb:${accountId}/${m.folderId}/${m.uid}`);
195
+ progressedThisBatch = true;
196
+ continue;
197
+ }
198
+ try {
199
+ const result = await this.fetchMessageBody(accountId, m.folderId, m.uid);
200
+ if (result) {
201
+ totalFetched++;
202
+ progressedThisBatch = true;
203
+ emitEvent({ type: "bodyCached", accountId, uid: m.uid, folderId: m.folderId });
204
+ }
205
+ else {
206
+ errors++;
207
+ }
208
+ }
209
+ catch (e) {
210
+ errors++;
211
+ const msg = String(e?.message || "");
212
+ if (/429|rate|too many/i.test(msg)) {
213
+ console.log(`[prefetch] ${accountId}: rate-limited — pausing ${RATE_LIMIT_PAUSE_MS / 1000}s`);
214
+ await new Promise(r => setTimeout(r, RATE_LIMIT_PAUSE_MS));
215
+ }
216
+ else {
217
+ console.error(`[prefetch] ${accountId}/${m.uid}: ${msg}`);
218
+ }
219
+ }
220
+ if (errors >= ERROR_BUDGET) {
221
+ console.error(`[prefetch] ${accountId}: stopping after ${errors} errors (${totalFetched} cached)`);
222
+ vlog(`prefetch ${accountId} aborted: ${errors} errors, ${totalFetched} cached`);
223
+ return;
224
+ }
225
+ await new Promise(r => setTimeout(r, THROTTLE_MS));
226
+ }
227
+ // If a full batch made no progress, bail out to avoid an infinite
228
+ // loop on messages the server can't deliver.
229
+ if (!progressedThisBatch) {
230
+ console.warn(`[prefetch] ${accountId}: batch made no progress, stopping`);
231
+ break;
232
+ }
233
+ }
234
+ if (totalFetched > 0) {
235
+ console.log(`[prefetch] ${accountId}: done — cached ${totalFetched} bodies`);
236
+ vlog(`prefetch ${accountId} done: ${totalFetched} cached`);
237
+ }
238
+ }
239
+ finally {
240
+ this.prefetchingAccounts.delete(accountId);
241
+ }
153
242
  }
154
243
  async syncFolders(accountId) {
155
244
  const provider = this.getProvider(accountId);
@@ -368,7 +368,10 @@ export class WebMailxDB {
368
368
  this.run("UPDATE messages SET body_path = ? WHERE account_id = ? AND uid = ?", [bodyPath, accountId, uid]);
369
369
  }
370
370
  getMessagesWithoutBody(accountId, limit = 50) {
371
- return this.all("SELECT uid, folder_id as folderId FROM messages WHERE account_id = ? AND (body_path IS NULL OR body_path = '') ORDER BY date DESC LIMIT ?", [accountId, limit]);
371
+ // "idb:<acct>/<folder>/<uid>" means the body is cached in IndexedDB via
372
+ // WebMessageStore. Anything else (NULL, "", "gmail:<id>", legacy paths)
373
+ // still needs fetching.
374
+ return this.all("SELECT uid, folder_id as folderId FROM messages WHERE account_id = ? AND (body_path IS NULL OR body_path NOT LIKE 'idb:%') ORDER BY date DESC LIMIT ?", [accountId, limit]);
372
375
  }
373
376
  getHighestUid(accountId, folderId) {
374
377
  const r = this.get("SELECT MAX(uid) as maxUid FROM messages WHERE account_id = ? AND folder_id = ?", [accountId, folderId]);
@@ -162,8 +162,8 @@ function handleWorkerEvent(event) {
162
162
  }
163
163
  catch { /* ignore */ }
164
164
  }
165
- // Also dispatch to the global mailxapi event callback
166
- const cb = window._mailxapiEvent;
165
+ // Also dispatch to the global host service-channel event callback
166
+ const cb = window._msgapiServiceEvent;
167
167
  if (cb)
168
168
  try {
169
169
  cb(event);
package/tempfix.cmd ADDED
@@ -0,0 +1,77 @@
1
+ @echo off
2
+ rem --------------------------------------------------------------------
3
+ rem tempfix.cmd — recover Claude Code + msger build after session wedge
4
+ rem
5
+ rem Problem: Claude Code's Bash tool keeps throwing EEXIST on
6
+ rem C:\Users\Bob\.claude\session-env\<uuid> because the current session
7
+ rem left a stale dir and the tool's mkdir isn't recursive.
8
+ rem Separately: msger's Windows build and stable-hardlink update need
9
+ rem mailx NOT to be holding msgernative.exe when npmglobalize runs.
10
+ rem (Fixed in source via rename-aside, but we still want a clean slate
11
+ rem so the first rebuild doesn't have to juggle three generations of
12
+ rem .old-<ts> files on top of a running process.)
13
+ rem
14
+ rem What this script does, in order:
15
+ rem 1. Kill Claude Code so it releases its session-env lock.
16
+ rem 2. Kill mailx + any running msger window so msgernative.exe can be
17
+ rem replaced (strictly not required with the rename-aside fix, but
18
+ rem cleaner for this one-time recovery).
19
+ rem 3. Nuke the entire session-env dir so the NEXT Claude start gets
20
+ rem a fresh UUID subdir.
21
+ rem 4. Remind you of the next commands to run.
22
+ rem
23
+ rem Safe to re-run. Everything taskkill'd is optional — not-found is fine.
24
+ rem --------------------------------------------------------------------
25
+
26
+ echo.
27
+ echo === 1/3 Killing Claude Code ===
28
+ taskkill /F /IM claude.exe 2>nul
29
+ if %errorlevel% equ 0 ( echo claude.exe terminated ) else ( echo claude.exe was not running )
30
+
31
+ echo.
32
+ echo === 2/3 Killing mailx + msger so msgernative.exe is free ===
33
+ rem mailx runs as node.exe but node.exe runs EVERYTHING on this box, so
34
+ rem we do NOT blanket-kill node. Instead kill the msger native window and
35
+ rem any mailx launcher by image name. mailx's own node process stays up
36
+ rem unless you want to close the UI — uncomment the last line to kill it.
37
+ taskkill /F /IM msgernative.exe 2>nul
38
+ if %errorlevel% equ 0 ( echo msgernative.exe terminated ) else ( echo msgernative.exe was not running )
39
+ rem To also stop mailx's node service, uncomment:
40
+ rem taskkill /F /FI "WINDOWTITLE eq mailx*" 2>nul
41
+
42
+ echo.
43
+ echo === 3/3 Removing stale Claude session-env dir ===
44
+ if exist "%USERPROFILE%\.claude\session-env" (
45
+ rmdir /s /q "%USERPROFILE%\.claude\session-env"
46
+ if exist "%USERPROFILE%\.claude\session-env" (
47
+ echo WARNING: session-env still exists — something was holding it open
48
+ ) else (
49
+ echo session-env removed
50
+ )
51
+ ) else (
52
+ echo session-env already gone
53
+ )
54
+
55
+ echo.
56
+ echo --------------------------------------------------------------------
57
+ echo Done. Next steps:
58
+ echo.
59
+ echo 1. Start Claude Code again. Your in-flight state is saved in memory
60
+ echo at C:\Users\Bob\.claude\projects\Y--dev-email-mailx\memory\
61
+ echo (see project_session_apr20_state.md for the full handoff).
62
+ echo.
63
+ echo 2. Rebuild msger so the new binary is installed:
64
+ echo cd /d y:\dev\utils\msgx\msger
65
+ echo npmglobalize
66
+ echo.
67
+ echo 3. Once that succeeds, test keyboard input:
68
+ echo msger y:\dev\projects\Cards\bbt\bbs.json
69
+ echo Try typing + Ctrl+C. If fixed, stale-binary theory was correct.
70
+ echo If still broken, tell Claude and we'll dig into focus/MoveFocus.
71
+ echo.
72
+ echo 4. In mailx root, wire up the new mailx-host workspace:
73
+ echo cd /d y:\dev\email\mailx
74
+ echo npm install
75
+ echo cd packages\mailx-host
76
+ echo tsc
77
+ echo --------------------------------------------------------------------