Fix #259: stale thread/label reads #266

Merged
crueber merged 1 commit from fix/issue-259 into main 2026-09-10 02:28:04 +00:00
Owner

Fixes #259 — kills the reaction flip-flop and the milestone/label id flash at the right layers.

Symptom 1 (reactions flip-flop): server cache class. The thread GET served private, max-age=0, stale-while-revalidate=60, so a refresh could paint a pre-mutation body from the browser cache while revalidating in the background. Now private, no-cache with the same version-keyed ETag: every read revalidates, 304-when-unchanged keeps it cheap (one conditional GET either way — no new round trips, law 6). The events tail stays no-store; neither class serves stale, so summary and tail cannot disagree. Mutation-triggered refetches already bypassed the HTTP cache (withNoStore + the #41 guarded invalidate), verified — no client invalidation change needed.

Symptom 2 (id flash): render gating, not denormalization. The thread carries ids; the 30s page-owned side-caches raced it. The sidebar chip (milestoneDisplay pending state) and milestone timeline rows (honest generic “changed the milestone” interim in issueEventText) now wait on milestones:{o}/{r} instead of flashing bare ids; deleted ids keep the bare-id self-heal. Label chips needed no gate — names ride the payload, only the color dot resolves late.

Tests: thread-GET cache-class + stale-ETag→200/new-version + fresh-ETag→304 assertions (TestGetIssueHTTP), events-tail no-store assertion; milestoneDisplay + pending-interim node --test cases (one existing test updated from bare-id-on-pending to the new interim contract). Gates: go test ./internal/issues/ -race ok, coverage 96.1% (≥95), full node --test web/test/unit/*.test.js 527/527, vite + esbuild builds pass. Doc updates in docs/features/02_issues.md §7 + Decisions, same change (law 12). No new deps.

Fixes #259 — kills the reaction flip-flop and the milestone/label id flash at the right layers. **Symptom 1 (reactions flip-flop): server cache class.** The thread GET served `private, max-age=0, stale-while-revalidate=60`, so a refresh could paint a pre-mutation body from the browser cache while revalidating in the background. Now `private, no-cache` with the same version-keyed ETag: every read revalidates, 304-when-unchanged keeps it cheap (one conditional GET either way — no new round trips, law 6). The events tail stays no-store; neither class serves stale, so summary and tail cannot disagree. Mutation-triggered refetches already bypassed the HTTP cache (`withNoStore` + the #41 guarded `invalidate`), verified — no client invalidation change needed. **Symptom 2 (id flash): render gating, not denormalization.** The thread carries ids; the 30s page-owned side-caches raced it. The sidebar chip (`milestoneDisplay` pending state) and milestone timeline rows (honest generic “changed the milestone” interim in `issueEventText`) now wait on `milestones:{o}/{r}` instead of flashing bare ids; deleted ids keep the bare-id self-heal. Label chips needed no gate — names ride the payload, only the color dot resolves late. **Tests:** thread-GET cache-class + stale-ETag→200/new-version + fresh-ETag→304 assertions (`TestGetIssueHTTP`), events-tail no-store assertion; `milestoneDisplay` + pending-interim `node --test` cases (one existing test updated from bare-id-on-pending to the new interim contract). Gates: `go test ./internal/issues/ -race` ok, coverage 96.1% (≥95), full `node --test web/test/unit/*.test.js` 527/527, vite + esbuild builds pass. Doc updates in `docs/features/02_issues.md` §7 + Decisions, same change (law 12). No new deps.
internal/issues (§7): thread GET cache class SWR → private, no-cache —
a refresh may never paint a pre-mutation body (kills the reaction
flip-flop); the version-keyed ETag + 304 path is unchanged so
revalidation stays cheap, and the no-store events tail agrees with it.
Mutation-triggered refetches already bypassed the HTTP cache
(withNoStore + the #41 guarded invalidate), so no client invalidation
change was needed.

web: new milestoneDisplay pending state (02 §3.2); the sidebar chip
renders a placeholder and milestone timeline rows the honest generic
interim until milestones:{o}/{r} settles — no bare-id flash on cold
loads; deleted ids keep the bare-id self-heal. Label chips needed no
gate (names ride the thread payload).

docs/features/02_issues.md §7 + Decisions entry in the same change.
Sign in to join this conversation.
No description provided.