Keep the full copy of an email the server says changed #106

Closed
opened 2026-08-27 19:42:35 +00:00 by jcoffey-dev · 0 comments
Owner

The rest of #100. The reading pane empties and refills when a thread is marked read — on an HTML message, a flash to the app's own background and out again. That's what remained after #104 stopped the message view rebuilding its body.

Cause

applyChanges dropped fullIds for every email the server reported as updated, so the next read would fetch it again:

// Drop cached versions of updated emails so they're refetched lazily.
for (const id of updated) {
  if (next[id] && nextFull[id]) delete nextFull[id];
}

But the reading pane renders only emails it holds in full (ThreadView.tsx, filter(e => Boolean(e && fullIds[e.id]))). Dropping one takes the message out of the open thread until the refetch at the end of the same function puts it back — and marking as read causes exactly that, because the server echoes our own change back as an update. The gap is a network round trip, which is why it's plainly visible against a real server and invisible locally.

Why keeping it is safe

RFC 8621 makes every property of an Email immutable except keywords and mailboxIds — the id is derived from the content, so a body cannot change beneath one. Both are in LIST_PROPS, which the refresh immediately below already merges over the cached copy. The eviction only ever cost the message its place in the thread.

On the evidence, plainly

I got this wrong once already by trusting a reproduction that didn't exist, so: this is reasoned from the code and matched against the reported symptom — "the pane empties and comes back", which is precisely what removing an email from the thread and refetching it looks like.

It is not backed by a local reproduction. The mock has never run this path: Email/set announced nothing and Email/changes always answered empty, so every check ever made here was against a server that never reported the change being made. #105 fixes that and should land alongside this, so the next person can reproduce it.

333 web and 77 server tests pass. npm run typecheck clean.

🤖 Generated with Claude Code

Merged 2026-08-27 as coffey-labs/ihasmail@31239ed9be

Rebuilt from: git history, session transcript.

The rest of #100. The reading pane **empties and refills** when a thread is marked read — on an HTML message, a flash to the app's own background and out again. That's what remained after #104 stopped the message view rebuilding its body. ### Cause `applyChanges` dropped `fullIds` for every email the server reported as updated, so the next read would fetch it again: ```js // Drop cached versions of updated emails so they're refetched lazily. for (const id of updated) { if (next[id] && nextFull[id]) delete nextFull[id]; } ``` But the reading pane renders only emails it holds in full (`ThreadView.tsx`, `filter(e => Boolean(e && fullIds[e.id]))`). Dropping one takes the message out of the open thread until the refetch at the end of the same function puts it back — and marking as read causes exactly that, because the server echoes our own change back as an update. The gap is a network round trip, which is why it's plainly visible against a real server and invisible locally. ### Why keeping it is safe RFC 8621 makes every property of an `Email` immutable except `keywords` and `mailboxIds` — the id is derived from the content, so a body cannot change beneath one. Both are in `LIST_PROPS`, which the refresh immediately below already merges over the cached copy. The eviction only ever cost the message its place in the thread. ### On the evidence, plainly I got this wrong once already by trusting a reproduction that didn't exist, so: **this is reasoned from the code and matched against the reported symptom** — "the pane empties and comes back", which is precisely what removing an email from the thread and refetching it looks like. It is **not** backed by a local reproduction. The mock has never run this path: `Email/set` announced nothing and `Email/changes` always answered empty, so every check ever made here was against a server that never reported the change being made. #105 fixes that and should land alongside this, so the next person can reproduce it. 333 web and 77 server tests pass. `npm run typecheck` clean. 🤖 Generated with [Claude Code](https://claude.com/claude-code) **Merged** 2026-08-27 as coffey-labs/ihasmail@31239ed9be4c <sub>Rebuilt from: git history, session transcript.</sub>
This repo is archived. You cannot comment on issues.