@bobfrankston/rmfmail 1.2.125 → 1.2.126
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/TODO.md +1 -1
- package/package.json +3 -3
- package/packages/mailx-service/sync-queue.d.ts +1 -0
- package/packages/mailx-service/sync-queue.d.ts.map +1 -1
- package/packages/mailx-service/sync-queue.js +20 -0
- package/packages/mailx-service/sync-queue.js.map +1 -1
- package/packages/mailx-service/sync-queue.ts +28 -0
package/TODO.md
CHANGED
|
@@ -254,7 +254,7 @@ Small, self-contained items. Pick them up between higher-priority blocks without
|
|
|
254
254
|
|
|
255
255
|
Alternative source worth evaluating before generalizing: **Hebcal** (`https://www.hebcal.com/`) publishes a more rigorously curated Jewish-only feed (iCal + JSON API), and similar projects exist for other traditions. A `JsonHolidayProvider` interface that abstracts Google vs Hebcal vs ICS-by-URL behind a common shape would let users plug in whatever feed they trust.
|
|
256
256
|
- **Q150 — `discovered` contacts don't belong in the cloud file — split it.** Bob 2026-05-16 hit a 1.5 MB `contacts.jsonc` (11,034 `discovered` rows) and asked about CSV. CSV is the wrong fix (saves ~half the bytes, loses the preferred/denylist/groups structure). Real issue: `discovered` is a *derived cache* — name/email/useCount/lastUsed rebuildable per-device from the message corpus (`seedContactsFromMessages`) — so it shouldn't be in the cloud-synced JSONC at all. Split: cloud `contacts.jsonc` keeps only user-curated data (`preferred`, `denylist`, `groups`) ≈ a few KB; `discovered` lives in the local DB only, rebuilt from each device's mail. Shrinks the cloud file ~700×, kills the 1.5 MB-blob fragility, and matches the "local store is a cache" rule.
|
|
257
|
-
-
|
|
257
|
+
- ~~**Q154 — Prefetch "0 FETCH responses" on new arrivals: verify-then-classify instead of ERROR noise.**~~ **DONE (2026-07-12, v1.2.125).** Zero-response chunks now UID-verify against the server: absent → one info line ("no longer in folder — reconcile owns the rows"); still present → ERROR marked "real fetch failure". Watch the log for the STILL-PRESENT variant — that would be a genuine prefetch bug. [S] Diagnosed 2026-07-12: 60–120×/day, bobma INBOX. Sequence: envelope arrives → server-side filtering (Sieve/screener) moves it to _Spam within seconds → body prefetch FETCHes the now-gone UID in INBOX → 0 responses → logged as ERROR twice; move-detect later rebinds the row (self-healing). Fix: on a 0-response batch, UID SEARCH the missing UIDs — absent from folder = expected-move (log one info line, let reconcile own it); still present = REAL fetch failure (keep the ERROR). Also answers the broader "deleted on server — find out for certain" principle (see v1.2.124 bodyError classifier: deletion is only claimed on positive server evidence). Watch: whether messages that STAY in INBOX ever hit this path — if so that is a real prefetch bug, not noise.
|
|
258
258
|
|
|
259
259
|
- **Q151 — AI config window (natural-language config edits).** Bob 2026-05-16: an AI window that understands the config files so he can say "don't show outlook_…@outlook.com". Feasible — mailx already has AI plumbing (aiTransform, provider+key in autocomplete settings) so it reuses the existing key. Design constraint: the AI must NOT free-form-rewrite a JSONC file (a malformed contacts.jsonc wipes everything — just saw it). It proposes a structured operation (`denylist add <email>`, `preferred add …`), shows a diff, the user confirms, and mailx applies it via the existing typed methods (`addToDenylist`, `addPreferredContact`, …). Worth it for open-ended requests ("stop showing anything from that marketing company", "merge these contacts") — for a single denylist it's overkill vs. a direct affordance (Q149).
|
|
260
260
|
- **Q149 — Visible prefer/ignore affordances.** (1) ✅ **Done v1.1.55** — compose autocomplete rows now have visible per-row ★ (prefer → `contacts.jsonc#preferred[]`) and ⊘ (ignore → `denylist[]`) buttons, shown on hover; plain click, no dependence on the flaky right-click. (2) STILL OPEN — "Never use this address" in the message-viewer From/To/Cc right-click menu (currently Copy / Add to contacts / Reply only). (3) Related: route ★ to a Google-Contacts star instead of `preferred[]` once that's decided (see Q151 discussion).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bobfrankston/rmfmail",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.126",
|
|
4
4
|
"description": "Local-first email client with IMAP sync and standalone native app",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "bin/mailx.js",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"dependencies": {
|
|
36
36
|
"@bobfrankston/iflow-direct": "^0.1.56",
|
|
37
37
|
"@bobfrankston/mailx-host": "^0.1.13",
|
|
38
|
-
"@bobfrankston/mailx-imap": "^0.1.
|
|
38
|
+
"@bobfrankston/mailx-imap": "^0.1.116",
|
|
39
39
|
"@bobfrankston/mailx-store-web": "^0.1.38",
|
|
40
40
|
"@bobfrankston/mailx-sync": "^0.1.27",
|
|
41
41
|
"@bobfrankston/miscinfo": "^1.0.13",
|
|
@@ -113,7 +113,7 @@
|
|
|
113
113
|
"dependencies": {
|
|
114
114
|
"@bobfrankston/iflow-direct": "^0.1.56",
|
|
115
115
|
"@bobfrankston/mailx-host": "^0.1.13",
|
|
116
|
-
"@bobfrankston/mailx-imap": "^0.1.
|
|
116
|
+
"@bobfrankston/mailx-imap": "^0.1.116",
|
|
117
117
|
"@bobfrankston/mailx-store-web": "^0.1.38",
|
|
118
118
|
"@bobfrankston/mailx-sync": "^0.1.27",
|
|
119
119
|
"@bobfrankston/miscinfo": "^1.0.13",
|
|
@@ -45,6 +45,7 @@ export declare class SyncQueue {
|
|
|
45
45
|
* One push in flight per draft; while it runs, only the LATEST queued
|
|
46
46
|
* raw message is kept (intermediate autosaves are superseded). */
|
|
47
47
|
private draftPushes;
|
|
48
|
+
private static readonly DRAFT_PUSH_MIN_INTERVAL_MS;
|
|
48
49
|
/** Per-account debounce timers. Multiple rapid enqueues (3 flag toggles,
|
|
49
50
|
* bulk move/delete) coalesce into one processSyncActions drain after
|
|
50
51
|
* DRAIN_DEBOUNCE_MS. Was previously in ImapManager.debounceSyncActions;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sync-queue.d.ts","sourceRoot":"","sources":["sync-queue.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,2BAA2B,CAAC;AACzD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAE5D,MAAM,MAAM,IAAI,GAAG,aAAa,GAAG,MAAM,GAAG,UAAU,GAAG,UAAU,CAAC;AAEpE;;;sDAGsD;AACtD,UAAU,YAAY;IAAG,SAAS,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,IAAI,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE;AAEzG,qBAAa,SAAS;
|
|
1
|
+
{"version":3,"file":"sync-queue.d.ts","sourceRoot":"","sources":["sync-queue.ts"],"names":[],"mappings":"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"}
|
|
@@ -31,6 +31,7 @@ export class SyncQueue {
|
|
|
31
31
|
* One push in flight per draft; while it runs, only the LATEST queued
|
|
32
32
|
* raw message is kept (intermediate autosaves are superseded). */
|
|
33
33
|
draftPushes = new Map();
|
|
34
|
+
static DRAFT_PUSH_MIN_INTERVAL_MS = 30_000;
|
|
34
35
|
/** Per-account debounce timers. Multiple rapid enqueues (3 flag toggles,
|
|
35
36
|
* bulk move/delete) coalesce into one processSyncActions drain after
|
|
36
37
|
* DRAIN_DEBOUNCE_MS. Was previously in ImapManager.debounceSyncActions;
|
|
@@ -113,6 +114,22 @@ export class SyncQueue {
|
|
|
113
114
|
slot.pending = { rawMessage };
|
|
114
115
|
return;
|
|
115
116
|
}
|
|
117
|
+
// Trailing-edge throttle (see slot.lastPushAt comment).
|
|
118
|
+
const sincePush = Date.now() - (slot.lastPushAt || 0);
|
|
119
|
+
if (slot.lastPushAt && sincePush < SyncQueue.DRAFT_PUSH_MIN_INTERVAL_MS) {
|
|
120
|
+
slot.pending = { rawMessage };
|
|
121
|
+
if (!slot.throttleTimer) {
|
|
122
|
+
slot.throttleTimer = setTimeout(() => {
|
|
123
|
+
slot.throttleTimer = null;
|
|
124
|
+
const held = slot.pending;
|
|
125
|
+
slot.pending = null;
|
|
126
|
+
if (held)
|
|
127
|
+
this.enqueueDraftPush(accountId, held.rawMessage, undefined, draftId);
|
|
128
|
+
}, SyncQueue.DRAFT_PUSH_MIN_INTERVAL_MS - sincePush);
|
|
129
|
+
}
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
slot.lastPushAt = Date.now();
|
|
116
133
|
slot.inFlight = true;
|
|
117
134
|
const run = (raw) => {
|
|
118
135
|
// Delete the copy WE last appended (delete-by-UID is reliable) rather
|
|
@@ -147,6 +164,9 @@ export class SyncQueue {
|
|
|
147
164
|
/** Drop the retained draft-push slot once a draft is sent or discarded, so
|
|
148
165
|
* its tracked UID doesn't linger for the compose session's lifetime. */
|
|
149
166
|
forgetDraft(accountId, draftId) {
|
|
167
|
+
const slot = this.draftPushes.get(`${accountId}:${draftId || ""}`);
|
|
168
|
+
if (slot?.throttleTimer)
|
|
169
|
+
clearTimeout(slot.throttleTimer);
|
|
150
170
|
this.draftPushes.delete(`${accountId}:${draftId || ""}`);
|
|
151
171
|
}
|
|
152
172
|
/** The AUTHORITATIVE UID of the draft's current local/server row. The
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sync-queue.js","sourceRoot":"","sources":["sync-queue.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAaH,MAAM,OAAO,SAAS;
|
|
1
|
+
{"version":3,"file":"sync-queue.js","sourceRoot":"","sources":["sync-queue.ts"],"names":[],"mappings":"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"}
|
|
@@ -50,7 +50,18 @@ export class SyncQueue {
|
|
|
50
50
|
* so header-only dedup let every ~2-5s checkpoint append a NEW draft
|
|
51
51
|
* (Bob 2026-07-01: Trash flooded with 76 copies of one 19-min reply). */
|
|
52
52
|
lastUid?: number;
|
|
53
|
+
/** Wall-clock of the last push we actually started. Server pushes are
|
|
54
|
+
* THROTTLED (trailing edge) — every IMAP checkpoint is an APPEND +
|
|
55
|
+
* delete-previous, so a 2s cadence churns the Drafts row UID every
|
|
56
|
+
* save: the list rebinds, the user's selection/preview evaporate
|
|
57
|
+
* (Bob 2026-07-12 "even if I select it it is not showing"), and
|
|
58
|
+
* Gmail-style dup floods get more chances. The LOCAL editing/.eml
|
|
59
|
+
* checkpoint (crash safety) still happens on every save — only the
|
|
60
|
+
* server mirror is throttled. */
|
|
61
|
+
lastPushAt?: number;
|
|
62
|
+
throttleTimer?: ReturnType<typeof setTimeout> | null;
|
|
53
63
|
}>();
|
|
64
|
+
private static readonly DRAFT_PUSH_MIN_INTERVAL_MS = 30_000;
|
|
54
65
|
/** Per-account debounce timers. Multiple rapid enqueues (3 flag toggles,
|
|
55
66
|
* bulk move/delete) coalesce into one processSyncActions drain after
|
|
56
67
|
* DRAIN_DEBOUNCE_MS. Was previously in ImapManager.debounceSyncActions;
|
|
@@ -136,6 +147,21 @@ export class SyncQueue {
|
|
|
136
147
|
slot.pending = { rawMessage };
|
|
137
148
|
return;
|
|
138
149
|
}
|
|
150
|
+
// Trailing-edge throttle (see slot.lastPushAt comment).
|
|
151
|
+
const sincePush = Date.now() - (slot.lastPushAt || 0);
|
|
152
|
+
if (slot.lastPushAt && sincePush < SyncQueue.DRAFT_PUSH_MIN_INTERVAL_MS) {
|
|
153
|
+
slot.pending = { rawMessage };
|
|
154
|
+
if (!slot.throttleTimer) {
|
|
155
|
+
slot.throttleTimer = setTimeout(() => {
|
|
156
|
+
slot.throttleTimer = null;
|
|
157
|
+
const held = slot.pending;
|
|
158
|
+
slot.pending = null;
|
|
159
|
+
if (held) this.enqueueDraftPush(accountId, held.rawMessage, undefined, draftId);
|
|
160
|
+
}, SyncQueue.DRAFT_PUSH_MIN_INTERVAL_MS - sincePush);
|
|
161
|
+
}
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
slot.lastPushAt = Date.now();
|
|
139
165
|
slot.inFlight = true;
|
|
140
166
|
const run = (raw: string): void => {
|
|
141
167
|
// Delete the copy WE last appended (delete-by-UID is reliable) rather
|
|
@@ -169,6 +195,8 @@ export class SyncQueue {
|
|
|
169
195
|
/** Drop the retained draft-push slot once a draft is sent or discarded, so
|
|
170
196
|
* its tracked UID doesn't linger for the compose session's lifetime. */
|
|
171
197
|
forgetDraft(accountId: string, draftId?: string): void {
|
|
198
|
+
const slot = this.draftPushes.get(`${accountId}:${draftId || ""}`);
|
|
199
|
+
if (slot?.throttleTimer) clearTimeout(slot.throttleTimer);
|
|
172
200
|
this.draftPushes.delete(`${accountId}:${draftId || ""}`);
|
|
173
201
|
}
|
|
174
202
|
|