diff --git a/server/internal/server/server.go b/server/internal/server/server.go index 5c7671b..0a33169 100644 --- a/server/internal/server/server.go +++ b/server/internal/server/server.go @@ -6,6 +6,7 @@ import ( "encoding/json" "log/slog" "net/http" + "strings" "time" "messenger/internal/api" @@ -72,13 +73,26 @@ func (h spaHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } defer index.Close() + w.Header().Set("Cache-Control", "no-cache") http.ServeContent(w, r, "index.html", time.Time{}, index) return } defer f.Close() + setStaticCache(w, path) http.ServeContent(w, r, path, time.Time{}, f) } +// setStaticCache lets hashed assets be cached forever while forcing the app +// shell, service worker and manifest to revalidate on every load. +func setStaticCache(w http.ResponseWriter, path string) { + switch { + case strings.HasPrefix(path, "/assets/"): + w.Header().Set("Cache-Control", "public, max-age=31536000, immutable") + case path == "/index.html" || path == "/sw.js" || path == "/manifest.webmanifest": + w.Header().Set("Cache-Control", "no-cache") + } +} + func writeJSON(w http.ResponseWriter, status int, body any) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) diff --git a/web/src/main.tsx b/web/src/main.tsx index 365663f..584874f 100644 --- a/web/src/main.tsx +++ b/web/src/main.tsx @@ -1,11 +1,11 @@ import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' -import { registerSW } from 'virtual:pwa-register' import './index.css' import App from './App' import { AuthProvider } from './auth/AuthContext' +import { setupPWA } from './pwa' -registerSW({ immediate: true }) +setupPWA() createRoot(document.getElementById('root')!).render( @@ -13,4 +13,4 @@ createRoot(document.getElementById('root')!).render( , -) +) \ No newline at end of file diff --git a/web/src/pwa.ts b/web/src/pwa.ts new file mode 100644 index 0000000..f2f5d9d --- /dev/null +++ b/web/src/pwa.ts @@ -0,0 +1,35 @@ +import { registerSW } from 'virtual:pwa-register' + +let refreshing = false + +const UPDATE_CHECK_INTERVAL_MS = 30 * 60 * 1000 + +// setupPWA registers the service worker and keeps the installed app fresh: +// it polls for updates and reloads once when a new worker takes control. +export function setupPWA(): void { + if (typeof navigator === 'undefined' || !('serviceWorker' in navigator)) return + + navigator.serviceWorker.addEventListener('controllerchange', () => { + if (refreshing) return + refreshing = true + window.location.reload() + }) + + registerSW({ + immediate: true, + onRegisteredSW(_swUrl, registration) { + if (!registration) return + const check = () => { + void registration.update().catch(() => {}) + } + // Installed PWAs stay open for days without navigation, so trigger + // update checks on a timer, on focus and when connectivity returns. + window.setInterval(check, UPDATE_CHECK_INTERVAL_MS) + document.addEventListener('visibilitychange', () => { + if (document.visibilityState === 'visible') check() + }) + window.addEventListener('online', check) + check() + }, + }) +} \ No newline at end of file