Notice a new build without being told #120

Closed
opened 2026-08-28 05:45:14 +00:00 by jcoffey-dev · 0 comments
Owner

Follow-on to #119. That made the reload possible; this makes it automatic. Checking only on a 401 was deferred, not automatic — it needs the tab to make a request, so an idle tab kept running the old build until someone touched it.

The obvious signal was the wrong one, and testing showed it

A deploy kills the EventSource behind /api/events, which looks like the perfect cue. It isn't:

  • The drop arrives while the container is still being replaced, so the /api/health check that follows cannot reach the server. It fails and is never retried.
  • Waiting for the stream to return doesn't work either — the session died with the old container, so the reconnect is answered with a 401 and never reaches connected at all.

I found this by watching a tab sit unchanged through a simulated deploy. The drop is still watched (free, occasionally lands late enough to be useful), but nothing depends on it.

What the guarantee actually rests on

A 60s poll while the tab is visible, plus a check on visibilitychange. Neither depends on the stream or on anyone being at the keyboard. /api/health touches nothing upstream, so the cost is one small request per minute per open tab; hidden tabs don't poll at all and check when they surface.

The reload is unconditional

An earlier revision of this branch held the reload back while a compose window had unsaved text. That protected the text, but it meant a tab could sit on a build the server no longer runs for as long as someone left a draft open — which is not automatic, and automatic is the point. The hold is gone.

The consequence is real and worth stating plainly: a deploy can take an unsent draft with it. Text that hasn't reached the server can't be saved after a deploy anyway, since the session went with the container. The trade is deliberate — a tab talking to a server it no longer matches is the worse failure, and it fails quietly.

Verified in a real browser, end to end

scenario result
tab open, idle, deploy lands, nothing touched reloaded on its own; marker gone, navType: "reload"
same, with a dirty compose open reloaded anyway, ~1s after the new server came up; draft gone, as intended
hidden tab no polling; checks on becoming visible
server unreachable / bad response no reload — staying stale beats losing the page
server keeps reporting a version the bundle lacks one reload, then stops; no loop

npm run typecheck, npm test (348 web + 80 server), npm run build all clean. 10 unit tests on this module, including fake-timer coverage of the poll and the hidden-tab case.

Merged 2026-08-27 as coffey-labs/ihasmail@f2e0cb6326

Rebuilt from: git history, session transcript.

Follow-on to #119. That made the reload possible; this makes it **automatic**. Checking only on a 401 was deferred, not automatic — it needs the tab to make a request, so an idle tab kept running the old build until someone touched it. ### The obvious signal was the wrong one, and testing showed it A deploy kills the EventSource behind `/api/events`, which looks like the perfect cue. It isn't: - The drop arrives **while the container is still being replaced**, so the `/api/health` check that follows cannot reach the server. It fails and is never retried. - Waiting for the stream to *return* doesn't work either — the session died with the old container, so the reconnect is answered with a 401 and never reaches `connected` at all. I found this by watching a tab sit unchanged through a simulated deploy. The drop is still watched (free, occasionally lands late enough to be useful), but nothing depends on it. ### What the guarantee actually rests on A 60s poll while the tab is visible, plus a check on `visibilitychange`. Neither depends on the stream or on anyone being at the keyboard. `/api/health` touches nothing upstream, so the cost is one small request per minute per open tab; hidden tabs don't poll at all and check when they surface. ### The reload is unconditional An earlier revision of this branch held the reload back while a compose window had unsaved text. That protected the text, but it meant a tab could sit on a build the server no longer runs for as long as someone left a draft open — which is not automatic, and automatic is the point. The hold is gone. The consequence is real and worth stating plainly: **a deploy can take an unsent draft with it.** Text that hasn't reached the server can't be saved after a deploy anyway, since the session went with the container. The trade is deliberate — a tab talking to a server it no longer matches is the worse failure, and it fails quietly. ### Verified in a real browser, end to end | scenario | result | |---|---| | tab open, idle, deploy lands, **nothing touched** | reloaded on its own; marker gone, `navType: "reload"` | | same, with a dirty compose open | **reloaded anyway**, ~1s after the new server came up; draft gone, as intended | | hidden tab | no polling; checks on becoming visible | | server unreachable / bad response | no reload — staying stale beats losing the page | | server keeps reporting a version the bundle lacks | one reload, then stops; no loop | `npm run typecheck`, `npm test` (348 web + 80 server), `npm run build` all clean. 10 unit tests on this module, including fake-timer coverage of the poll and the hidden-tab case. **Merged** 2026-08-27 as coffey-labs/ihasmail@f2e0cb632641 <sub>Rebuilt from: git history, session transcript.</sub>
This repo is archived. You cannot comment on issues.