@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.
- package/README.md +1 -0
- package/bin/mailx.js +1 -1
- package/client/app.js +336 -7
- package/client/components/message-viewer.js +76 -18
- package/client/compose/compose.css +50 -0
- package/client/compose/compose.js +49 -11
- package/client/index.html +1 -0
- package/client/lib/api-client.js +6 -0
- package/client/lib/mailxapi.js +13 -7
- package/client/styles/components.css +115 -0
- package/package.json +6 -3
- package/packages/mailx-core/index.d.ts +3 -0
- package/packages/mailx-core/index.js +45 -7
- package/packages/mailx-host/index.d.ts +20 -0
- package/packages/mailx-host/index.js +30 -0
- package/packages/mailx-host/package.json +20 -0
- package/packages/mailx-service/index.d.ts +12 -0
- package/packages/mailx-service/index.js +98 -6
- package/packages/mailx-service/jsonrpc.js +4 -0
- package/packages/mailx-store/db.js +47 -5
- package/packages/mailx-store-web/android-bootstrap.js +91 -2
- package/packages/mailx-store-web/db.js +4 -1
- package/packages/mailx-store-web/main-thread-host.js +2 -2
- package/tempfix.cmd +77 -0
|
@@ -33,8 +33,8 @@ function emitEvent(event) {
|
|
|
33
33
|
}
|
|
34
34
|
catch { /* ignore */ }
|
|
35
35
|
}
|
|
36
|
-
if (typeof window.
|
|
37
|
-
window.
|
|
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
|
-
|
|
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
|
|
166
|
-
const cb = window.
|
|
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 --------------------------------------------------------------------
|