@bobfrankston/rmfmail 1.2.219 → 1.2.221

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 (33) hide show
  1. package/bin/mailx.js +93 -5
  2. package/bin/mailx.js.map +1 -1
  3. package/bin/mailx.ts +83 -5
  4. package/client/app.bundle.js +2 -2
  5. package/client/app.bundle.js.map +2 -2
  6. package/client/compose/compose.bundle.js +9 -3
  7. package/client/compose/compose.bundle.js.map +2 -2
  8. package/client/compose/compose.js +17 -1
  9. package/client/compose/compose.js.map +1 -1
  10. package/client/compose/compose.ts +15 -1
  11. package/client/lib/api-client.js +2 -2
  12. package/client/lib/api-client.js.map +1 -1
  13. package/client/lib/api-client.ts +2 -2
  14. package/client/lib/mailxapi.js +2 -2
  15. package/client/prewarm.html +16 -0
  16. package/package.json +5 -5
  17. package/packages/mailx-imap/index.d.ts +9 -0
  18. package/packages/mailx-imap/index.d.ts.map +1 -1
  19. package/packages/mailx-imap/index.js +55 -1
  20. package/packages/mailx-imap/index.js.map +1 -1
  21. package/packages/mailx-imap/index.ts +50 -1
  22. package/packages/mailx-imap/package-lock.json +2 -2
  23. package/packages/mailx-imap/package.json +1 -1
  24. package/packages/mailx-service/index.d.ts +1 -1
  25. package/packages/mailx-service/index.d.ts.map +1 -1
  26. package/packages/mailx-service/index.js +102 -26
  27. package/packages/mailx-service/index.js.map +1 -1
  28. package/packages/mailx-service/index.ts +89 -23
  29. package/packages/mailx-service/jsonrpc.js +1 -1
  30. package/packages/mailx-service/jsonrpc.js.map +1 -1
  31. package/packages/mailx-service/jsonrpc.ts +1 -1
  32. package/packages/mailx-service/package.json +1 -1
  33. /package/packages/mailx-imap/{node_modules.npmglobalize-stash-24032 → node_modules.npmglobalize-stash-39780}/.package-lock.json +0 -0
package/bin/mailx.ts CHANGED
@@ -2095,6 +2095,16 @@ RFC 5322 with CRLF line endings. Bodies are quoted-printable encoded (readable i
2095
2095
  // main-window shutdown log line to prove/refute the "closing a popout
2096
2096
  // closes the main window" coupling (Bob 2026-07-06, unreproduced).
2097
2097
  let lastPopoutClose: { at: number; key: string } | null = null;
2098
+ /** Hidden window that keeps the "popout" WebView2 profile's browser
2099
+ * process warm — see the prewarm block below showService. Deliberately
2100
+ * NOT in popoutWindows: it is infrastructure, not a user window, and must
2101
+ * not show up in the shutdown popout census. */
2102
+ let prewarmHandle: ReturnType<typeof showMessageBoxEx> | null = null;
2103
+ /** How long after boot to warm the popout profile. Late enough that it
2104
+ * isn't racing the MAIN window's own WebView creation for the same CPU,
2105
+ * early enough to beat a user reaching for ^N (the 2026-08-05 case was at
2106
+ * boot + 18s). */
2107
+ const PREWARM_DELAY_MS = 6000;
2098
2108
  const popoutKey = (a: string, f: number | undefined, u: number): string => `${a}|${f ?? ""}|${u}`;
2099
2109
  try {
2100
2110
  const { startPopoutServer } = await import("./popout-server.js");
@@ -2473,6 +2483,51 @@ RFC 5322 with CRLF line endings. Bodies are quoted-printable encoded (readable i
2473
2483
  return { ok: true };
2474
2484
  });
2475
2485
  }
2486
+ // Pre-warm the "popout" WebView2 profile.
2487
+ //
2488
+ // Every secondary window (compose, message popout, extra main window) runs
2489
+ // on profile "popout" — deliberately NOT the main window's profile, so a
2490
+ // crash there can't black out the main window (S67). The cost is that the
2491
+ // FIRST popout of a session pays for cold-starting an entire WebView2
2492
+ // browser process for that profile, and if the daemon is busy with initial
2493
+ // sync at the time, that cold start is not cheap: ^N 18 seconds after boot
2494
+ // on 2026-08-05 sat on a blank window for 59.4 SECONDS, all of it inside
2495
+ // WebViewBuilder::build() before a line of compose JS could run (the page
2496
+ // itself then rendered in 397ms). Nothing the page can do about it — there
2497
+ // is no WebView yet to draw a spinner into.
2498
+ //
2499
+ // So pay it up front, off the user's critical path: one hidden window on
2500
+ // that profile, opened a few seconds after boot (late enough not to
2501
+ // compete with the main window's own WebView creation), held open for the
2502
+ // life of the daemon so the browser process stays warm. Hidden windows
2503
+ // still create their WebView, which is the whole point.
2504
+ if (popoutInfo) {
2505
+ const pi3 = popoutInfo;
2506
+ setTimeout(() => {
2507
+ const t0 = Date.now();
2508
+ try {
2509
+ const h = showMessageBoxEx({
2510
+ title: "rmfmail (prewarm)",
2511
+ url: `http://127.0.0.1:${pi3.port}/app/${pi3.token}/client/prewarm.html`,
2512
+ size: { width: 200, height: 100 },
2513
+ hidden: true,
2514
+ focusOnCreate: false,
2515
+ aumid: "com.frankston.rmfmail",
2516
+ profile: "popout",
2517
+ });
2518
+ prewarmHandle = h;
2519
+ if (typeof h.pid === "number") addChildPid(h.pid);
2520
+ console.log(` [prewarm] popout WebView2 profile warming (pid=${h.pid ?? "?"})`);
2521
+ void h.result.catch(() => { /* closed with the daemon */ }).finally(() => {
2522
+ if (typeof h.pid === "number") removeChildPid(h.pid);
2523
+ console.log(` [prewarm] popout profile window closed after ${Date.now() - t0}ms`);
2524
+ });
2525
+ } catch (e: any) {
2526
+ // Never fatal — a cold first popout is slow, not broken.
2527
+ console.log(` [prewarm] skipped: ${e?.message || e}`);
2528
+ }
2529
+ }, PREWARM_DELAY_MS);
2530
+ }
2476
2531
  _tick("calling showService (WebView opens here)");
2477
2532
  const handle = showService({
2478
2533
  title: `rmfmail v${rootPkgVersion}`,
@@ -2549,6 +2604,9 @@ RFC 5322 with CRLF line endings. Bodies are quoted-printable encoded (readable i
2549
2604
  const inst = readInstanceFile();
2550
2605
  if (inst && inst.pid === process.pid) clearInstanceFile();
2551
2606
  try { handle.close(); } catch { /* already gone */ }
2607
+ // Hidden and therefore invisible to the user if orphaned — close it on
2608
+ // every exit path, not just gracefulShutdown.
2609
+ try { prewarmHandle?.close?.(); } catch { /* already gone */ }
2552
2610
  };
2553
2611
  process.once("exit", __cleanupInstance);
2554
2612
  process.once("SIGINT", () => { __cleanupInstance(); process.exit(0); });
@@ -2733,10 +2791,21 @@ RFC 5322 with CRLF line endings. Bodies are quoted-printable encoded (readable i
2733
2791
  // and emits this when Word writes; compose.ts listens and reloads Quill.
2734
2792
  imapManager.on("wordEditUpdated", (payload: { editId: string; html: string }) => {
2735
2793
  handle.send({ _event: "wordEditUpdated", type: "wordEditUpdated", ...payload });
2736
- // Bring compose back to the front after a Word save — the service's
2737
- // old mailx-host focusMainWindow call was a silent no-op (the helper
2738
- // never existed); this native msger command is the real mechanism.
2739
- handle.send({ _msgerWindow: "focus" });
2794
+ // Deliberately NO { _msgerWindow: "focus" } here. The autosave sidecar
2795
+ // saves every ~2s while the user types, and each save lands in this
2796
+ // handler so this line pulled the foreground off Word mid-sentence,
2797
+ // every couple of seconds, for the whole editing session (Bob
2798
+ // 2026-08-06: "keeps flipping back to rmfmail on any change"). The
2799
+ // focus now happens once, on wordEditClosed, below.
2800
+ });
2801
+ // Word (or the document) closed — the external edit is over. THIS is the
2802
+ // moment to bring rmfmail forward, replacing the old per-save focus grab.
2803
+ imapManager.on("wordEditClosed", (payload: { editId: string; fromPopout?: boolean }) => {
2804
+ handle.send({ _event: "wordEditClosed", type: "wordEditClosed", ...payload });
2805
+ // Only raise the MAIN window when the compose lives IN it. A popout
2806
+ // compose is a separate window with no service channel, so focusing
2807
+ // main would just cover it up.
2808
+ if (!payload.fromPopout) handle.send({ _msgerWindow: "focus" });
2740
2809
  });
2741
2810
  // Cloud-write/read failures from mailx-settings → push to UI as a banner so
2742
2811
  // silent fall-back-to-local can no longer swallow Drive errors.
@@ -3158,6 +3227,10 @@ RFC 5322 with CRLF line endings. Bodies are quoted-printable encoded (readable i
3158
3227
  if (shuttingDown) return;
3159
3228
  shuttingDown = true;
3160
3229
  console.log(`${reason} — shutting down`);
3230
+ // The prewarm window is hidden, so an orphaned one is invisible AND
3231
+ // unkillable by the user — close it explicitly rather than relying on
3232
+ // child-process cascade.
3233
+ try { prewarmHandle?.close?.(); } catch { /* already gone */ }
3161
3234
  imapManager.stopPeriodicSync();
3162
3235
  imapManager.stopOutboxWorker();
3163
3236
  // 3s hard timeout — don't hang on broken IMAP connections
@@ -3210,7 +3283,12 @@ RFC 5322 with CRLF line endings. Bodies are quoted-printable encoded (readable i
3210
3283
  const popoutDelta = lastPopoutClose
3211
3284
  ? `; last popout closed ${Date.now() - lastPopoutClose.at}ms ago (${lastPopoutClose.key})`
3212
3285
  : "; no popout closed this session";
3213
- await gracefulShutdown(`Window closed (main service child; ${popoutWindows.size} popout window(s) still tracked${popoutDelta})`);
3286
+ // Name the still-tracked popouts, not just the count. "2 popout window(s)
3287
+ // still tracked" (2026-08-05) was unactionable: two legitimately-open
3288
+ // compose windows and two entries whose close handler never fired look
3289
+ // identical from a bare count.
3290
+ const popoutKeys = popoutWindows.size > 0 ? ` [${[...popoutWindows.keys()].join(", ")}]` : "";
3291
+ await gracefulShutdown(`Window closed (main service child; ${popoutWindows.size} popout window(s) still tracked${popoutKeys}${popoutDelta})`);
3214
3292
  }
3215
3293
 
3216
3294
  main().catch(console.error);
@@ -614,8 +614,8 @@ function readConfigHelp(name) {
614
614
  function unsubscribeOneClick(url) {
615
615
  return ipc().unsubscribeOneClick?.(url);
616
616
  }
617
- function openInWord(editId, html) {
618
- return ipc().openInWord?.(editId, html) ?? Promise.resolve({ ok: false, path: "", opener: "none" });
617
+ function openInWord(editId, html, fromPopout = false) {
618
+ return ipc().openInWord?.(editId, html, fromPopout) ?? Promise.resolve({ ok: false, path: "", opener: "none" });
619
619
  }
620
620
  function closeWordEdit(editId) {
621
621
  return ipc().closeWordEdit?.(editId) ?? Promise.resolve();