@bobfrankston/rmfmail 1.1.212 → 1.1.214

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.ts CHANGED
@@ -4750,9 +4750,24 @@ optAutomarkDelay?.addEventListener("change", () => {
4750
4750
  declare const mailxapi: { isApp: boolean; platform: string; ensureServer: () => Promise<boolean>; getVersion: () => Promise<any> } | undefined;
4751
4751
  const isApp = typeof mailxapi !== "undefined" && mailxapi?.isApp;
4752
4752
 
4753
- // Wait for server ready signal, then fetch version
4754
- const versionPromise = getVersion();
4753
+ // Wait for server ready signal, then fetch version. getVersion is idempotent
4754
+ // and trivial; msger occasionally drops the startup IPC (see api-client note),
4755
+ // so retry with backoff rather than leaving "(??)" + a "service error" banner
4756
+ // up forever for a cosmetic version read (Bob 2026-06-02). A short per-action
4757
+ // timeout (mailxapi.js) makes each failed attempt give up in 5s.
4758
+ async function getVersionWithRetry(attempts = 5): Promise<any> {
4759
+ for (let i = 0; i < attempts; i++) {
4760
+ try { return await getVersion(); }
4761
+ catch (e) {
4762
+ if (i === attempts - 1) { console.warn("getVersion failed after retries:", e); return {}; }
4763
+ await new Promise(r => setTimeout(r, 1500));
4764
+ }
4765
+ }
4766
+ return {};
4767
+ }
4768
+ const versionPromise = getVersionWithRetry();
4755
4769
  versionPromise.then((d: any) => {
4770
+ if (!d || !d.version) return; // all retries dropped — leave the prior text, no scary banner
4756
4771
  const els = document.querySelectorAll<HTMLElement>(".app-version");
4757
4772
  const storage = d.storage || {};
4758
4773
  const storageLabel = storage.provider && storage.provider !== "local"
@@ -53,27 +53,67 @@
53
53
  // turned a slow-but-fine search into "mailxapi timeout:
54
54
  // searchMessages". Local search stays fast and well under any limit.
55
55
  searchMessages: 120000,
56
+ // getVersion reads package.json — instant. With the startup-race
57
+ // re-send above (it's in RESENDABLE) a dropped post is re-tried every
58
+ // 2s, so a 5s ceiling normally allows ~2 re-sends before giving up —
59
+ // enough to ride out the brief window before the Rust→daemon pipe is
60
+ // wired. Short ceiling so a genuinely wedged daemon surfaces fast
61
+ // rather than hanging the version display for 30s.
62
+ getVersion: 5000,
56
63
  };
64
+ // Actions that are PURE READS and therefore safe to auto-re-send if the
65
+ // first post is lost. Why a post gets lost: at startup the WebView can
66
+ // call `window.ipc.postMessage` after `window.ipc` is injected but BEFORE
67
+ // the Rust host has wired its message handler to the daemon's stdin — the
68
+ // post is accepted by WebView2 and silently dropped downstream, so no
69
+ // response ever comes and the callback times out. That's the real cause of
70
+ // "getVersion got dropped" (Bob 2026-06-03) — a startup race in our own
71
+ // bridge, not a mysterious msger behavior. Re-sending a read is harmless
72
+ // (the daemon ignores a duplicate response — _msgapiServiceResolve no-ops
73
+ // when the callback is already gone). WRITES are NOT listed: re-sending a
74
+ // send/saveDraft/delete/move/flag could double-apply.
75
+ var RESENDABLE = {
76
+ getVersion: 1, getAccounts: 1, getFolders: 1, getUnifiedInbox: 1, getMessages: 1,
77
+ getSettings: 1, getAutocompleteSettings: 1, getDiagnostics: 1, getOutboxStatus: 1,
78
+ getSyncPending: 1, getCalendars: 1, getCalendarEvents: 1, getTasks: 1,
79
+ getPrimaryAccount: 1, getThreadMessages: 1, getPriorityLists: 1
80
+ };
81
+ var RESEND_INTERVAL_MS = 2000;
57
82
  function callNode(action, params) {
58
83
  var id = String(++_callbackId);
84
+ var msgStr = JSON.stringify(Object.assign({ _action: action, _cbid: id }, params || {}));
59
85
  return new Promise(function(resolve, reject) {
60
- var timer = setTimeout(function() {
86
+ function send() {
87
+ if (window.ipc && window.ipc.postMessage) {
88
+ // Trace BEFORE the call so a slow daemon-receive shows up as
89
+ // ipc-send + N s before the daemon logs `[ipc] ←`. Skip the
90
+ // trace event itself to avoid recursion.
91
+ if (action !== "logClientEvent") _traceIpcSend(action, id);
92
+ window.ipc.postMessage(msgStr);
93
+ return true;
94
+ }
95
+ return false;
96
+ }
97
+ var entry = { resolve: resolve, reject: reject, timer: null, resend: null };
98
+ _callbacks[id] = entry;
99
+ entry.timer = setTimeout(function() {
61
100
  delete _callbacks[id];
101
+ if (entry.resend) clearInterval(entry.resend);
62
102
  reject(new Error("mailxapi timeout: " + action));
63
103
  }, ACTION_TIMEOUT_MS[action] || 30000);
64
- _callbacks[id] = { resolve: resolve, reject: reject, timer: timer };
65
- var msg = Object.assign({ _action: action, _cbid: id }, params || {});
66
- if (window.ipc && window.ipc.postMessage) {
67
- // Trace BEFORE the actual call so a slow daemon-receive shows
68
- // up as ipc-send wall-clock + N seconds before the daemon
69
- // logs `[ipc] ←`. Skip tracing on the trace event itself so
70
- // we don't recurse.
71
- if (action !== "logClientEvent") _traceIpcSend(action, id);
72
- window.ipc.postMessage(JSON.stringify(msg));
73
- } else {
74
- clearTimeout(timer);
104
+ var sent = send();
105
+ if (!sent && !RESENDABLE[action]) {
106
+ // Channel object not injected yet AND not safe to wait/re-send
107
+ // (a write) fail now rather than risk it.
108
+ clearTimeout(entry.timer);
75
109
  delete _callbacks[id];
76
110
  reject(new Error("No IPC channel available"));
111
+ return;
112
+ }
113
+ // Re-send pure reads until a response lands or the ceiling fires —
114
+ // heals a startup-race drop (and a not-yet-injected window.ipc).
115
+ if (RESENDABLE[action]) {
116
+ entry.resend = setInterval(send, RESEND_INTERVAL_MS);
77
117
  }
78
118
  });
79
119
  }
@@ -84,6 +124,7 @@
84
124
  if (!cb) return;
85
125
  delete _callbacks[id];
86
126
  clearTimeout(cb.timer);
127
+ if (cb.resend) clearInterval(cb.resend);
87
128
  cb.resolve(value);
88
129
  };
89
130
 
@@ -93,6 +134,7 @@
93
134
  if (!cb) return;
94
135
  delete _callbacks[id];
95
136
  clearTimeout(cb.timer);
137
+ if (cb.resend) clearInterval(cb.resend);
96
138
  cb.reject(new Error(error));
97
139
  };
98
140
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/rmfmail",
3
- "version": "1.1.212",
3
+ "version": "1.1.214",
4
4
  "description": "Local-first email client with IMAP sync and standalone native app",
5
5
  "type": "module",
6
6
  "main": "bin/mailx.js",