From f744a3e34c1016c0095299797f12d659a231e1a4 Mon Sep 17 00:00:00 2001 From: Kacy Fortner Date: Sun, 5 Apr 2026 13:09:33 -0400 Subject: [PATCH 1/3] fix: tighten web chat status and shell updates --- web/public/sw.js | 59 +++++++++++++++++-------- web/src/App.tsx | 111 ++++++++++++++++++++++++++++++++++++----------- 2 files changed, 127 insertions(+), 43 deletions(-) diff --git a/web/public/sw.js b/web/public/sw.js index 3bebb08..a313fe1 100644 --- a/web/public/sw.js +++ b/web/public/sw.js @@ -1,4 +1,4 @@ -const cacheName = 'imsg-bridge-web-shell-v1'; +const cacheName = 'imsg-bridge-web-shell-v2'; const shellAssets = ['/', '/manifest.webmanifest', '/icon.svg']; self.addEventListener('install', (event) => { @@ -33,21 +33,44 @@ self.addEventListener('fetch', (event) => { return; } - event.respondWith( - caches.match(event.request).then((cached) => { - if (cached) { - return cached; - } - - return fetch(event.request).then((response) => { - if (!response.ok || response.type === 'opaque') { - return response; - } - - const clone = response.clone(); - caches.open(cacheName).then((cache) => cache.put(event.request, clone)); - return response; - }); - }), - ); + const isShellRequest = + event.request.mode === 'navigate' || shellAssets.includes(requestUrl.pathname); + + if (isShellRequest) { + event.respondWith(networkFirst(event.request)); + return; + } + + event.respondWith(cacheFirst(event.request)); }); + +async function networkFirst(request) { + try { + const response = await fetch(request); + if (response.ok && response.type !== 'opaque') { + const cache = await caches.open(cacheName); + await cache.put(request, response.clone()); + } + return response; + } catch (error) { + const cached = await caches.match(request); + if (cached) { + return cached; + } + throw error; + } +} + +async function cacheFirst(request) { + const cached = await caches.match(request); + if (cached) { + return cached; + } + + const response = await fetch(request); + if (response.ok && response.type !== 'opaque') { + const cache = await caches.open(cacheName); + await cache.put(request, response.clone()); + } + return response; +} diff --git a/web/src/App.tsx b/web/src/App.tsx index fcbd961..cf79464 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -670,6 +670,7 @@ function AppShell({ profile }: { profile: StoredServerProfile }) { () => chats.find((chat) => chat.id === activeChatId) ?? null, [activeChatId, chats], ); + const canConnectEvents = Boolean(token) && (status === 'ready' || status === 'refreshing'); useEffect(() => { chatsRef.current = chats; @@ -685,7 +686,7 @@ function AppShell({ profile }: { profile: StoredServerProfile }) { // websocket useEffect(() => { - if (!token || (status !== 'ready' && status !== 'refreshing')) { + if (!token || !canConnectEvents) { setEventsStatus('idle'); setEventsError(null); return; @@ -695,6 +696,7 @@ function AppShell({ profile }: { profile: StoredServerProfile }) { let socket: WebSocket | null = null; let reconnectTimer = 0; let reconnectDelay = 1000; + let lastCloseMessage: string | null = null; const connect = () => { if (cancelled) { @@ -714,6 +716,7 @@ function AppShell({ profile }: { profile: StoredServerProfile }) { return; } reconnectDelay = 1000; + lastCloseMessage = null; setEventsStatus('live'); setEventsError(null); }; @@ -742,20 +745,31 @@ function AppShell({ profile }: { profile: StoredServerProfile }) { if (cancelled) { return; } + lastCloseMessage = 'live updates could not connect. retrying…'; setEventsStatus('error'); - setEventsError('live event stream dropped. retrying…'); + setEventsError(lastCloseMessage); }; - socket.onclose = () => { + socket.onclose = (event) => { if (cancelled) { return; } - setEventsStatus('connecting'); + const closeMessage = describeEventStreamClose(event, lastCloseMessage); + const nextDelay = reconnectDelay; + + setEventsStatus('error'); + setEventsError(closeMessage); + reconnectTimer = window.setTimeout(() => { + if (cancelled) { + return; + } reconnectDelay = Math.min(reconnectDelay * 2, 15000); + setEventsStatus('connecting'); + setEventsError(null); connect(); - }, reconnectDelay); + }, nextDelay); }; }; @@ -766,7 +780,7 @@ function AppShell({ profile }: { profile: StoredServerProfile }) { window.clearTimeout(reconnectTimer); socket?.close(); }; - }, [profile.wsBaseUrl, status, token]); + }, [canConnectEvents, profile.wsBaseUrl, token]); // initial load useEffect(() => { @@ -1020,7 +1034,7 @@ function AppShell({ profile }: { profile: StoredServerProfile }) {

messages

- + @@ -1036,6 +1050,11 @@ function AppShell({ profile }: { profile: StoredServerProfile }) { status={status} onSelectChat={selectChat} /> + {eventsError ? ( +
+ {eventsError} +
+ ) : null}
{/* thread — hidden on mobile when chat list is shown */} @@ -1061,6 +1080,7 @@ function AppShell({ profile }: { profile: StoredServerProfile }) { onBack={() => setMobileShowThread(false)} status={threadStatus} threadError={threadError} + eventsError={eventsError} eventsStatus={eventsStatus} />
@@ -1143,6 +1163,7 @@ function ThreadView(props: { threadError: string | null; onReload: () => void; onBack: () => void; + eventsError: string | null; eventsStatus: 'idle' | 'connecting' | 'live' | 'error'; }) { const scrollRef = useRef(null); @@ -1172,7 +1193,7 @@ function ThreadView(props: { {props.activeChat ? displayChatName(props.activeChat) : 'select a chat'}

- +