fix: cache backend API data so beekeepers can view it with no signal
The service worker only ever cached same-origin static assets/pages - every real data call (colonies, apiaries, tasks, honey batches, ...) was excluded, both because it targets a different origin (API_URL points at a separate backend service, per .env.example) and because of an explicit /api path exclusion. In practice this meant a beekeeper opening the app at an apiary with no signal saw nothing at all: not even data fetched minutes earlier on the same device, despite the offline write-queue built earlier assuming the app itself stays usable while offline. The backend can't be matched by origin the way static assets are (the service worker has no static knowledge of the runtime-configured API_URL), so it's matched by path instead - every backend call goes through /api/v1/. /api/runtime-config is explicitly excluded and always fetched live, since a stale cached value would point the app at the wrong backend. Error responses (4xx/5xx) are now never cached either, closing a latent bug that could otherwise serve a stale 401 back after a token refresh. Since the cache now holds real business data, it's cleared on logout so it can't leak into the next person's session on a shared device. Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
1 parent
3678a01319
commit
fb4713e320
2 files changed
+35
-3
No files matched your search
@@ -1,4 +1,4 @@
|
||||
const CACHE_NAME = "apicore-shell-v2";
|
||||
const CACHE_NAME = "apicore-shell-v3";
|
||||
|
||||
self.addEventListener("install", () => {
|
||||
self.skipWaiting();
|
||||
@@ -23,14 +23,33 @@ self.addEventListener("fetch", (event) => {
|
||||
}
|
||||
|
||||
const url = new URL(request.url);
|
||||
if (url.origin !== self.location.origin || url.pathname.startsWith("/api")) {
|
||||
|
||||
// /api/runtime-config tells the app which backend origin to use - it
|
||||
// must always be fetched live. A stale cached value would point the app
|
||||
// at the wrong (or no longer running) backend, which is worse than no
|
||||
// cache at all.
|
||||
if (url.pathname === "/api/runtime-config") {
|
||||
return;
|
||||
}
|
||||
|
||||
// Same-origin static assets/pages, or the real backend API. The backend
|
||||
// runs on its own origin (API_URL, injected at runtime via
|
||||
// /api/runtime-config) rather than this app's own origin, so it can't be
|
||||
// matched by origin the way static assets are - matched by path instead,
|
||||
// since every backend call goes through /api/v1/. Without this, colonies,
|
||||
// apiaries, tasks etc. were never cached at all: a beekeeper opening the
|
||||
// app with no signal at an apiary saw nothing, not even data they had
|
||||
// already loaded minutes earlier on the same device.
|
||||
const isCacheable =
|
||||
url.origin === self.location.origin || url.pathname.includes("/api/v1/");
|
||||
if (!isCacheable) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Page navigations must never be answered with a synthetic Response.error() -
|
||||
// Chrome/Edge render that as a native "This page couldn't load" interstitial
|
||||
// instead of the app. Let the browser handle navigations directly; only
|
||||
// static assets (JS/CSS/images) go through the offline-cache fallback below.
|
||||
// static assets/API data go through the offline-cache fallback below.
|
||||
if (request.mode === "navigate") {
|
||||
return;
|
||||
}
|
||||
@@ -38,6 +57,12 @@ self.addEventListener("fetch", (event) => {
|
||||
event.respondWith(
|
||||
fetch(request)
|
||||
.then((response) => {
|
||||
// Never cache an error response (e.g. a 401 hit right as an access
|
||||
// token expired, before refresh could kick in) - it would otherwise
|
||||
// get served back later as if it were still the current state.
|
||||
if (!response.ok) {
|
||||
return response;
|
||||
}
|
||||
const responseClone = response.clone();
|
||||
return caches
|
||||
.open(CACHE_NAME)
|
||||
|
||||
@@ -91,6 +91,13 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
await logoutRequest();
|
||||
storeAccessToken(null);
|
||||
setUser(null);
|
||||
// The service worker cache now holds real business data (colonies,
|
||||
// tasks, etc.) for offline viewing - on a shared/handed-off device
|
||||
// that must not survive into the next person's session.
|
||||
if ("caches" in window) {
|
||||
const keys = await caches.keys();
|
||||
await Promise.all(keys.map((key) => caches.delete(key)));
|
||||
}
|
||||
}, []);
|
||||
|
||||
const hasPermission = useCallback(
|
||||
|
||||
Reference in new issue
Block a user