From 1c2d2e91c1c6b790042b3dce6f004f50681f3443 Mon Sep 17 00:00:00 2001 From: Keith Smith Date: Sun, 16 Aug 2026 14:55:30 -0600 Subject: [PATCH] Fix stale room/message/member data after leaving and returning (#37) The service worker cached /api/rooms/mine, /api/rooms, .../messages, .../members, and /api/invites/mine with StaleWhileRevalidate: serve the previous cached response immediately, refresh the cache in the background for next time. That means every repeat visit showed content one visit behind -- reopening a room after someone messaged it, or checking a second device, both showed stale data until a manual reload (which finally picked up the now-revalidated cache). Only registers in a production build (vite dev never activates it), which is why this didn't surface during in-browser testing this session for #26/#27/#34. Switched all five routes to NetworkFirst: always prefer a live response, fall back to cache only when the network request itself fails or times out (genuinely offline), keeping the "readable while offline" behavior without the staleness. Verified against a real production build (`vite preview`, the only way the SW actually registers): sent a message from a second session against an already-cached room, and the very next fetch showed it immediately with no staleness. Co-Authored-By: Claude Sonnet 5 --- frontend/src/sw.ts | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/frontend/src/sw.ts b/frontend/src/sw.ts index 09bcf35..56b2e8a 100644 --- a/frontend/src/sw.ts +++ b/frontend/src/sw.ts @@ -3,7 +3,7 @@ import { CacheableResponsePlugin } from 'workbox-cacheable-response' import { ExpirationPlugin } from 'workbox-expiration' import { cleanupOutdatedCaches, createHandlerBoundToURL, precacheAndRoute } from 'workbox-precaching' import { NavigationRoute, registerRoute } from 'workbox-routing' -import { NetworkOnly, StaleWhileRevalidate } from 'workbox-strategies' +import { NetworkFirst, NetworkOnly } from 'workbox-strategies' declare let self: ServiceWorkerGlobalScope @@ -20,6 +20,18 @@ registerRoute( const READ_CACHE_EXPIRATION = { maxEntries: 50, maxAgeSeconds: 7 * 24 * 60 * 60 } const cacheableResponse = new CacheableResponsePlugin({ statuses: [0, 200] }) +// Always prefer a live network response over the cache -- these routes back +// an actively-updating chat, so "stale" isn't an acceptable default the way +// it can be for e.g. static assets. StaleWhileRevalidate was tried here +// first, but it serves the *previous* cached response immediately and only +// refreshes the cache in the background for next time, which means every +// repeat visit shows content that's one visit behind until a manual reload +// (confirmed as the cause of #37 -- messages/rooms/members looking stale +// after leaving and returning to a room, or after another device's update). +// NetworkFirst keeps the same "readable while offline" behavior (falls back +// to cache only when the network request itself fails or times out) without +// that staleness while online. +const NETWORK_TIMEOUT_SECONDS = 4 // Ported from Phase 3's vite.config.ts `workbox.runtimeCaching` -- that // option only applies to the generateSW strategy, so with a hand-written @@ -29,36 +41,41 @@ registerRoute(({ url }) => url.pathname.startsWith('/api/auth/'), new NetworkOnl registerRoute( ({ url }) => url.pathname === '/api/rooms/mine', - new StaleWhileRevalidate({ + new NetworkFirst({ cacheName: 'api-rooms-mine', + networkTimeoutSeconds: NETWORK_TIMEOUT_SECONDS, plugins: [cacheableResponse, new ExpirationPlugin(READ_CACHE_EXPIRATION)], }), ) registerRoute( ({ url }) => url.pathname === '/api/rooms', - new StaleWhileRevalidate({ + new NetworkFirst({ cacheName: 'api-rooms-open', + networkTimeoutSeconds: NETWORK_TIMEOUT_SECONDS, plugins: [cacheableResponse, new ExpirationPlugin(READ_CACHE_EXPIRATION)], }), ) registerRoute( ({ url }) => /^\/api\/rooms\/[^/]+\/messages$/.test(url.pathname), - new StaleWhileRevalidate({ + new NetworkFirst({ cacheName: 'api-room-messages', + networkTimeoutSeconds: NETWORK_TIMEOUT_SECONDS, plugins: [cacheableResponse, new ExpirationPlugin(READ_CACHE_EXPIRATION)], }), ) registerRoute( ({ url }) => /^\/api\/rooms\/[^/]+\/members$/.test(url.pathname), - new StaleWhileRevalidate({ + new NetworkFirst({ cacheName: 'api-room-members', + networkTimeoutSeconds: NETWORK_TIMEOUT_SECONDS, plugins: [cacheableResponse, new ExpirationPlugin(READ_CACHE_EXPIRATION)], }), ) registerRoute( ({ url }) => url.pathname === '/api/invites/mine', - new StaleWhileRevalidate({ + new NetworkFirst({ cacheName: 'api-invites-mine', + networkTimeoutSeconds: NETWORK_TIMEOUT_SECONDS, plugins: [cacheableResponse, new ExpirationPlugin(READ_CACHE_EXPIRATION)], }), )