@bobfrankston/rmfmail 1.2.4 → 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.
Files changed (53) hide show
  1. package/bin/mailx.js +22 -2
  2. package/bin/mailx.js.map +1 -1
  3. package/bin/mailx.ts +21 -2
  4. package/client/app.bundle.js +15 -3
  5. package/client/app.bundle.js.map +2 -2
  6. package/client/app.js +13 -1
  7. package/client/app.js.map +1 -1
  8. package/client/app.ts +11 -1
  9. package/client/components/message-viewer.js +7 -2
  10. package/client/components/message-viewer.js.map +1 -1
  11. package/client/components/message-viewer.ts +7 -2
  12. package/client/compose/compose.bundle.js +41 -5
  13. package/client/compose/compose.bundle.js.map +2 -2
  14. package/client/compose/compose.js +3 -1
  15. package/client/compose/compose.js.map +1 -1
  16. package/client/compose/compose.ts +3 -1
  17. package/client/compose/editor.js +4 -4
  18. package/client/compose/editor.js.map +1 -1
  19. package/client/compose/editor.ts +10 -5
  20. package/client/lib/rmf-tiny.js +47 -0
  21. package/package.json +3 -3
  22. package/packages/mailx-imap/index.d.ts +15 -0
  23. package/packages/mailx-imap/index.d.ts.map +1 -1
  24. package/packages/mailx-imap/index.js +161 -0
  25. package/packages/mailx-imap/index.js.map +1 -1
  26. package/packages/mailx-imap/index.ts +122 -0
  27. package/packages/mailx-imap/package-lock.json +2 -2
  28. package/packages/mailx-imap/package.json +1 -1
  29. package/packages/mailx-service/index.d.ts +1 -5
  30. package/packages/mailx-service/index.d.ts.map +1 -1
  31. package/packages/mailx-service/index.js +18 -171
  32. package/packages/mailx-service/index.js.map +1 -1
  33. package/packages/mailx-service/index.ts +19 -128
  34. package/packages/mailx-service/package.json +1 -0
  35. package/packages/mailx-service/sync-worker-client.d.ts +33 -0
  36. package/packages/mailx-service/sync-worker-client.d.ts.map +1 -0
  37. package/packages/mailx-service/sync-worker-client.js +89 -0
  38. package/packages/mailx-service/sync-worker-client.js.map +1 -0
  39. package/packages/mailx-service/sync-worker-client.ts +92 -0
  40. package/packages/mailx-service/sync-worker.d.ts +33 -0
  41. package/packages/mailx-service/sync-worker.d.ts.map +1 -0
  42. package/packages/mailx-service/sync-worker.js +92 -0
  43. package/packages/mailx-service/sync-worker.js.map +1 -0
  44. package/packages/mailx-service/sync-worker.ts +101 -0
  45. package/packages/mailx-store/db.d.ts +1 -0
  46. package/packages/mailx-store/db.d.ts.map +1 -1
  47. package/packages/mailx-store/db.js +19 -0
  48. package/packages/mailx-store/db.js.map +1 -1
  49. package/packages/mailx-store/db.ts +20 -1
  50. package/packages/mailx-store/package.json +1 -1
  51. package/test/service-boot.mjs +62 -0
  52. package/test/sync-worker-isolation.mjs +71 -0
  53. package/test/sync-worker-smoke.mjs +60 -0
@@ -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);
@@ -0,0 +1,60 @@
1
+ /** Sync-worker plumbing smoke test: spawn → init handshake → generic RPC
2
+ * round-trip → event-proxy sanity. Does NOT exercise live IMAP. */
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+ import os from "node:os";
6
+ import { spawnSyncWorker } from "../packages/mailx-service/index.js";
7
+ import { MailxDB } from "../packages/mailx-store/index.js";
8
+
9
+ const liveDir = path.join(os.homedir(), ".rmfmail");
10
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "rmf-syncw-"));
11
+ const tmpStore = path.join(tmp, "store");
12
+ fs.mkdirSync(tmpStore, { recursive: true });
13
+ for (const s of ["", "-wal", "-shm"]) {
14
+ const src = path.join(liveDir, "mailx.db" + s);
15
+ if (fs.existsSync(src)) try { fs.copyFileSync(src, path.join(tmp, "mailx.db" + s)); } catch {}
16
+ }
17
+ let fail = 0;
18
+ const ok = (m) => console.log(" ✓", m);
19
+ const bad = (m) => { console.error(" ✗", m); fail++; };
20
+
21
+ // Main builds the schema first (worker opens with skipMigrations).
22
+ new MailxDB(tmp);
23
+ console.log("\nsync-worker smoke test");
24
+
25
+ let synced;
26
+ try {
27
+ synced = await spawnSyncWorker({ configDir: tmp, storePath: tmpStore });
28
+ ok("spawned + init-ok handshake");
29
+ } catch (e) { bad(`spawn failed: ${e.message}`); process.exit(1); }
30
+
31
+ // Event proxy: local on/emit must work (bin's forwarders rely on this).
32
+ let got = null;
33
+ synced.proxy.on("smokeTest", (a) => { got = a; });
34
+ synced.proxy.emit("smokeTest", 42);
35
+ await new Promise(r => setTimeout(r, 20));
36
+ if (got === 42) ok("proxy is a working EventEmitter (local on/emit)");
37
+ else bad(`local event did not fire (got ${got})`);
38
+
39
+ // Generic RPC round-trip: getDiagnosticsSnapshot needs no network.
40
+ try {
41
+ const snap = await synced.proxy.getDiagnosticsSnapshot();
42
+ if (snap && typeof snap === "object") ok(`RPC round-trip: getDiagnosticsSnapshot → ${Object.keys(snap).length} keys`);
43
+ else bad(`getDiagnosticsSnapshot returned ${typeof snap}`);
44
+ } catch (e) { bad(`getDiagnosticsSnapshot RPC threw: ${e.message}`); }
45
+
46
+ // getOutboxStatus round-trip (outbox lives in the worker now).
47
+ try {
48
+ const ob = await synced.proxy.getOutboxStatus();
49
+ ok(`RPC round-trip: getOutboxStatus → ${JSON.stringify(ob).slice(0, 80)}`);
50
+ } catch (e) { bad(`getOutboxStatus RPC threw: ${e.message}`); }
51
+
52
+ // Unknown method → clean error, not a hang.
53
+ try {
54
+ await synced.proxy.__definitelyNotARealMethod__();
55
+ bad("unknown method should have thrown");
56
+ } catch (e) { ok(`unknown method rejected cleanly: ${e.message.slice(0, 60)}`); }
57
+
58
+ await synced.close();
59
+ console.log(`\n${fail ? "RESULT: FAIL" : "RESULT: PASS"}\n`);
60
+ process.exit(fail ? 1 : 0);