@bobfrankston/rmfmail 1.2.5 → 1.2.6
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/client/app.bundle.js +15 -3
- package/client/app.bundle.js.map +2 -2
- package/client/app.js +13 -1
- package/client/app.js.map +1 -1
- package/client/app.ts +11 -1
- package/client/components/message-viewer.js +7 -2
- package/client/components/message-viewer.js.map +1 -1
- package/client/components/message-viewer.ts +7 -2
- package/client/compose/compose.bundle.js +41 -5
- package/client/compose/compose.bundle.js.map +2 -2
- package/client/compose/compose.js +3 -1
- package/client/compose/compose.js.map +1 -1
- package/client/compose/compose.ts +3 -1
- package/client/compose/editor.js +4 -4
- package/client/compose/editor.js.map +1 -1
- package/client/compose/editor.ts +10 -5
- package/client/lib/rmf-tiny.js +47 -0
- package/package.json +3 -3
- package/packages/mailx-imap/index.d.ts.map +1 -1
- package/packages/mailx-imap/index.js +15 -0
- package/packages/mailx-imap/index.js.map +1 -1
- package/packages/mailx-imap/index.ts +15 -0
- package/packages/mailx-imap/package-lock.json +2 -2
- package/packages/mailx-imap/package.json +1 -1
- package/test/service-boot.mjs +62 -0
- package/test/sync-worker-isolation.mjs +71 -0
|
@@ -3401,6 +3401,21 @@ export class ImapManager extends EventEmitter {
|
|
|
3401
3401
|
console.error(` [prefetch] ${accountId} folder ${folder.path} chunk ${chunkStart / PREFETCH_CHUNK_SIZE}: batch fetch failed: ${msg}`);
|
|
3402
3402
|
counters.errors++;
|
|
3403
3403
|
this.recordFolderError(accountId, folder.path);
|
|
3404
|
+
// A dead connection / network outage won't heal
|
|
3405
|
+
// mid-run — every further chunk just re-fails on the
|
|
3406
|
+
// same dead socket and burns the error budget (Bob
|
|
3407
|
+
// 2026-06-13: prefetch "Not connected" thrash on a
|
|
3408
|
+
// flaky network). Stop this folder's remaining chunks
|
|
3409
|
+
// NOW; the next reconciler tick reconnects fresh.
|
|
3410
|
+
// These are transient, so the UIDs are NOT backed off
|
|
3411
|
+
// (markPrefetchEmpty is per-message poison only) —
|
|
3412
|
+
// they retry next tick. Genuine per-body errors are
|
|
3413
|
+
// handled per-UID above and never reach here.
|
|
3414
|
+
const dead = /not connected|ETIMEDOUT|ECONNRESET|ECONNREFUSED|EPIPE|socket|connection (closed|reset|lost)|disconnect/i.test(msg);
|
|
3415
|
+
if (dead) {
|
|
3416
|
+
console.log(` [prefetch] ${accountId}/${folder.path}: connection down — deferring rest to next tick`);
|
|
3417
|
+
break;
|
|
3418
|
+
}
|
|
3404
3419
|
if (counters.errors >= ERROR_BUDGET) break;
|
|
3405
3420
|
}
|
|
3406
3421
|
// Prune missing UIDs only when the batch actually
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bobfrankston/mailx-imap",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.94",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bobfrankston/mailx-imap",
|
|
9
|
-
"version": "0.1.
|
|
9
|
+
"version": "0.1.94",
|
|
10
10
|
"license": "ISC",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@bobfrankston/iflow-direct": "^0.1.27",
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/** Daemon-boot integration: construct the FULL MailxService with the sync
|
|
2
|
+
* proxy + read-worker exactly as bin/mailx.ts does, then verify reads work and
|
|
3
|
+
* nothing crashes while the reconciler's timers fire. Catches boot wiring bugs
|
|
4
|
+
* the component tests can't (service ↔ proxy ↔ reconciler interplay). */
|
|
5
|
+
import fs from "node:fs";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import os from "node:os";
|
|
8
|
+
import { spawnSyncWorker, MailxService } from "../packages/mailx-service/index.js";
|
|
9
|
+
import { MailxDB, Store, FileMessageStore } from "../packages/mailx-store/index.js";
|
|
10
|
+
|
|
11
|
+
const liveDir = path.join(os.homedir(), ".rmfmail");
|
|
12
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "rmf-boot-"));
|
|
13
|
+
const tmpStore = path.join(tmp, "store");
|
|
14
|
+
fs.mkdirSync(tmpStore, { recursive: true });
|
|
15
|
+
for (const s of ["", "-wal", "-shm"]) {
|
|
16
|
+
const src = path.join(liveDir, "mailx.db" + s);
|
|
17
|
+
if (fs.existsSync(src)) try { fs.copyFileSync(src, path.join(tmp, "mailx.db" + s)); } catch {}
|
|
18
|
+
}
|
|
19
|
+
let fail = 0;
|
|
20
|
+
const ok = (m) => console.log(" ✓", m);
|
|
21
|
+
const bad = (m) => { console.error(" ✗", m); fail++; };
|
|
22
|
+
process.on("uncaughtException", (e) => bad(`uncaughtException: ${e.message}`));
|
|
23
|
+
process.on("unhandledRejection", (e) => bad(`unhandledRejection: ${e?.message || e}`));
|
|
24
|
+
|
|
25
|
+
console.log("\nservice-boot integration test");
|
|
26
|
+
|
|
27
|
+
// Mirror bin/mailx.ts: store on main, sync worker, MailxService(store, proxy).
|
|
28
|
+
const db = new MailxDB(tmp);
|
|
29
|
+
const store = new Store(db, new FileMessageStore(tmpStore));
|
|
30
|
+
const synced = await spawnSyncWorker({ configDir: tmp, storePath: tmpStore });
|
|
31
|
+
ok("sync worker spawned");
|
|
32
|
+
|
|
33
|
+
let svc;
|
|
34
|
+
try { svc = new MailxService(store, synced.proxy); ok("MailxService constructed with proxy (no throw)"); }
|
|
35
|
+
catch (e) { bad(`MailxService ctor threw: ${e.message}`); process.exit(1); }
|
|
36
|
+
|
|
37
|
+
// Give the read-worker a moment to come up, then exercise reads.
|
|
38
|
+
await new Promise(r => setTimeout(r, 400));
|
|
39
|
+
try {
|
|
40
|
+
const accts = await svc.getAccounts();
|
|
41
|
+
ok(`getAccounts() → ${accts.length} accounts`);
|
|
42
|
+
} catch (e) { bad(`getAccounts threw: ${e.message}`); }
|
|
43
|
+
try {
|
|
44
|
+
const inbox = await svc.getUnifiedInbox(1, 20, false);
|
|
45
|
+
ok(`getUnifiedInbox() → ${inbox.items.length} items / total ${inbox.total}`);
|
|
46
|
+
if (inbox.items[0] && !inbox.items[0].uuid) bad("envelope missing uuid (highlight regression)");
|
|
47
|
+
} catch (e) { bad(`getUnifiedInbox threw: ${e.message}`); }
|
|
48
|
+
try {
|
|
49
|
+
const ob = await svc.getOutboxStatus();
|
|
50
|
+
ok(`getOutboxStatus() (via worker) → total ${ob.total}`);
|
|
51
|
+
} catch (e) { bad(`getOutboxStatus threw: ${e.message}`); }
|
|
52
|
+
|
|
53
|
+
// Let the reconciler's startup timers fire — this is where a proxy/await
|
|
54
|
+
// mismatch would blow up. Watch for ~6s.
|
|
55
|
+
console.log(" letting reconciler timers run for 6s (watching for crashes)…");
|
|
56
|
+
await new Promise(r => setTimeout(r, 6000));
|
|
57
|
+
ok("survived 6s of reconciler activity without crashing");
|
|
58
|
+
|
|
59
|
+
await synced.close();
|
|
60
|
+
try { fs.rmSync(tmp, { recursive: true, force: true }); } catch {}
|
|
61
|
+
console.log(`\n${fail ? "RESULT: FAIL" : "RESULT: PASS"}\n`);
|
|
62
|
+
process.exit(fail);
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/** Sync-worker ISOLATION test: register real accounts over the bus, run a real
|
|
2
|
+
* syncAll INSIDE the worker, and prove (a) sync events forward back to main and
|
|
3
|
+
* (b) the MAIN event loop stays free the whole time. Network-resilient: a sync
|
|
4
|
+
* failure still validates that the worker didn't take main down with it. */
|
|
5
|
+
import fs from "node:fs";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import os from "node:os";
|
|
8
|
+
import { spawnSyncWorker } from "../packages/mailx-service/index.js";
|
|
9
|
+
import { MailxDB } from "../packages/mailx-store/index.js";
|
|
10
|
+
import { loadAccountsAsync } from "../packages/mailx-settings/index.js";
|
|
11
|
+
|
|
12
|
+
const liveDir = path.join(os.homedir(), ".rmfmail");
|
|
13
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "rmf-iso-"));
|
|
14
|
+
const tmpStore = path.join(tmp, "store");
|
|
15
|
+
fs.mkdirSync(tmpStore, { recursive: true });
|
|
16
|
+
for (const s of ["", "-wal", "-shm"]) {
|
|
17
|
+
const src = path.join(liveDir, "mailx.db" + s);
|
|
18
|
+
if (fs.existsSync(src)) try { fs.copyFileSync(src, path.join(tmp, "mailx.db" + s)); } catch {}
|
|
19
|
+
}
|
|
20
|
+
new MailxDB(tmp); // main builds schema first
|
|
21
|
+
console.log("\nsync-worker isolation test");
|
|
22
|
+
|
|
23
|
+
const synced = await spawnSyncWorker({ configDir: tmp, storePath: tmpStore });
|
|
24
|
+
console.log(" ✓ worker spawned");
|
|
25
|
+
|
|
26
|
+
// Register real accounts over the bus.
|
|
27
|
+
let accounts = [];
|
|
28
|
+
try { accounts = await loadAccountsAsync(); } catch (e) { console.log(` (loadAccounts failed: ${e.message})`); }
|
|
29
|
+
console.log(` accounts to register: ${accounts.length}`);
|
|
30
|
+
for (const a of accounts) {
|
|
31
|
+
try { await synced.proxy.addAccount(a); console.log(` ✓ addAccount ${a.id} crossed the bus`); }
|
|
32
|
+
catch (e) { console.log(` ✗ addAccount ${a.id}: ${e.message}`); }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Count sync events forwarded worker → main.
|
|
36
|
+
let events = 0;
|
|
37
|
+
const seen = new Set();
|
|
38
|
+
for (const ev of ["syncProgress", "folderSynced", "syncComplete", "folderCountsChanged", "syncError", "accountError"]) {
|
|
39
|
+
synced.proxy.on(ev, () => { events++; seen.add(ev); });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Main-loop lag monitor: a 25ms heartbeat. If sync in the worker froze main,
|
|
43
|
+
// this spikes; if the worker is truly isolated, it stays tiny.
|
|
44
|
+
let maxLag = 0, last = Date.now();
|
|
45
|
+
const beat = setInterval(() => { const now = Date.now(); maxLag = Math.max(maxLag, now - last - 25); last = now; }, 25);
|
|
46
|
+
|
|
47
|
+
// Fire a real syncAll in the worker (cap at 25s so a hung network ends the test).
|
|
48
|
+
const t0 = Date.now();
|
|
49
|
+
console.log(" running syncAll() in the worker (≤25s)…");
|
|
50
|
+
await Promise.race([
|
|
51
|
+
synced.proxy.syncAll().catch(e => console.log(` (syncAll returned error — fine, we're testing isolation: ${String(e.message).slice(0, 60)})`)),
|
|
52
|
+
new Promise(r => setTimeout(r, 25000)),
|
|
53
|
+
]);
|
|
54
|
+
const elapsed = Date.now() - t0;
|
|
55
|
+
clearInterval(beat);
|
|
56
|
+
|
|
57
|
+
console.log(`\n syncAll wall time: ${elapsed}ms`);
|
|
58
|
+
console.log(` sync events forwarded to main: ${events} (${[...seen].join(", ") || "none"})`);
|
|
59
|
+
console.log(` MAIN event-loop max lag during sync: ${maxLag}ms`);
|
|
60
|
+
|
|
61
|
+
let fail = 0;
|
|
62
|
+
if (maxLag < 500) console.log(` ✓ main loop stayed responsive (maxLag ${maxLag}ms < 500ms) — sync is isolated`);
|
|
63
|
+
else { console.log(` ✗ main loop lagged ${maxLag}ms — sync is NOT isolated`); fail++; }
|
|
64
|
+
if (accounts.length === 0) console.log(" ⚠ no accounts loaded — event-forwarding not exercised (network/cloud down)");
|
|
65
|
+
else if (events > 0) console.log(` ✓ real sync events forwarded worker→main`);
|
|
66
|
+
else console.log(" ⚠ no sync events seen (network may be down) — isolation still proven by lag");
|
|
67
|
+
|
|
68
|
+
await synced.close();
|
|
69
|
+
try { fs.rmSync(tmp, { recursive: true, force: true }); } catch {}
|
|
70
|
+
console.log(`\n${fail ? "RESULT: FAIL" : "RESULT: PASS"}\n`);
|
|
71
|
+
process.exit(fail);
|