/* * Routing rules of the service worker (public/sw.js), kept free of the Cache * and Fetch APIs so they can be unit-tested (src/lib/offline/sw-routes.spec.ts). * A plain script on purpose: the worker loads it with importScripts and gets * `self.ApiCoreSwRoutes`, Vitest loads it with require. No syntax newer than * the oldest phone browser that runs the worker (no lookbehind, no `?.`). * * Changing this file changes its content version: update SW_ROUTES_VERSION * in sw.js (a test says which value), or phones keep the old rules. */ (function (root) { "use strict"; /** Pages whose HTML is kept for a cold start without network. */ var SHELL_ROUTES = [ "/", "/login", "/dashboard", "/offline", "/offline/apiary", "/offline/colony", "/inspections/new", "/varroa/new", "/treatments/new", "/feedings/new", "/swarm-control/new", ]; var OFFLINE_PAGE = "/offline"; /** Read only this device's storage: answered from the cache at once. */ var OFFLINE_PAGES = ["/offline", "/offline/apiary", "/offline/colony"]; var SHELL_TIMEOUT_MS = 4000; var DEEP_LINK_TIMEOUT_MS = 10000; var OTHER_PAGE_TIMEOUT_MS = 15000; /** Build assets of old builds stay this long after they were last fetched. */ var BUILD_ASSET_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000; // Deep links (QR codes, NFC tags) open the offline view of the same record. // Static pages under the same path are not ids. var DETAIL_ROUTES = [ { pattern: /^\/colonies\/([^/]+)$/, offlinePage: "/offline/colony", notIds: ["new"] }, { pattern: /^\/apiaries\/([^/]+)$/, offlinePage: "/offline/apiary", notIds: ["new", "map"] }, ]; // Every backend call goes through /api/v1/. The backend matches paths // case-insensitively, so these rules do too. var API_PATH = /\/api\/v1\//; // Never kept, each for its own reason: var NEVER_CACHED_API_PATHS = [ // tokens and session state /\/api\/v1\/auth(\/|$)/i, // apiaries themselves: exact coordinates and the address (theft risk) /\/api\/v1\/apiaries\/?$/i, /\/api\/v1\/apiaries\/[^/]+\/?$/i, // exports and backups: full records incl. coordinates and addresses, // names (Bestandsbuch), megabytes /\/api\/v1\/export\//i, /\/api\/v1\/backup(\/|$)/i, // Telegram link and settings (one-time link, chat state), and the System // page (queue and delivery details): current state or nothing /\/api\/v1\/profile\/telegram(\/|$)/i, /\/api\/v1\/profile\/notification-settings(\/|$)/i, /\/api\/v1\/system(\/|$)/i, // what the offline packs answer: without network they must fail fast, // so the pack - with its age shown - answers instead of an old copy /\/api\/v1\/colonies\/?$/i, /\/api\/v1\/colonies\/[^/]+\/inspections\/?$/i, /\/api\/v1\/apiaries\/[^/]+\/(colony-overview|offline-pack)\/?$/i, ]; // A root-relative build asset, in an attribute or in the inline RSC data // (where quotes are escaped). The leading character keeps absolute URLs of // other hosts out. var NEXT_STATIC_ASSET = /(^|["'(\s,=])(\/_next\/static\/[^"'\s<>()\\]+)/g; var NEXT_STATIC_PREFIX = "/_next/static/"; function normalizePath(pathname) { return pathname.length > 1 && pathname.charAt(pathname.length - 1) === "/" ? pathname.slice(0, -1) : pathname; } function isShellRoute(pathname) { return SHELL_ROUTES.indexOf(normalizePath(pathname)) !== -1; } function decodeSegment(segment) { try { return decodeURIComponent(segment); } catch { return null; } } /** "from" parameter: where the user wanted to go, for the way back. */ function fromParam(url) { return "from=" + encodeURIComponent(url.pathname + url.search); } function offlineHomeFor(url) { return OFFLINE_PAGE + "?" + fromParam(url); } function detailRouteOf(pathname) { for (var index = 0; index < DETAIL_ROUTES.length; index += 1) { var route = DETAIL_ROUTES[index]; var match = route.pattern.exec(pathname); if (!match) { continue; } var id = decodeSegment(match[1]); if (id === null || route.notIds.indexOf(id) !== -1) { return null; } return { offlinePage: route.offlinePage, id: id }; } return null; } /** * What a page navigation gets when neither the network nor the cache has * that exact URL: a shell page from the cache (its query stays in the * address bar, the page reads it), the offline view of a record, or the * offline start page. Redirects carry `from` for the way back. */ function offlineFallbackFor(url) { var pathname = normalizePath(url.pathname); if (isShellRoute(pathname)) { return { kind: "cached-path", path: pathname }; } var detail = detailRouteOf(pathname); if (detail) { return { kind: "redirect", to: detail.offlinePage + "?id=" + encodeURIComponent(detail.id) + "&" + fromParam(url), }; } return { kind: "redirect", to: offlineHomeFor(url) }; } /** * How long a page navigation waits for the network. On a timeout the * cached copy answers; without one, `redirectOnTimeout` decides between the * offline fallback and waiting on (a shell page has no better fallback). */ function navigationPlanFor(url) { var pathname = normalizePath(url.pathname); if (pathname === "/api" || pathname.indexOf("/api/") === 0) { return { kind: "browser" }; } if (OFFLINE_PAGES.indexOf(pathname) !== -1) { return { kind: "cache-first" }; } if (isShellRoute(pathname)) { return { kind: "network-first", timeoutMs: SHELL_TIMEOUT_MS, redirectOnTimeout: false }; } if (detailRouteOf(pathname)) { return { kind: "network-first", timeoutMs: DEEP_LINK_TIMEOUT_MS, redirectOnTimeout: true }; } return { kind: "network-first", timeoutMs: OTHER_PAGE_TIMEOUT_MS, redirectOnTimeout: true }; } function isCacheableApiPath(pathname) { if (!API_PATH.test(pathname)) { return false; } for (var index = 0; index < NEVER_CACHED_API_PATHS.length; index += 1) { if (NEVER_CACHED_API_PATHS[index].test(pathname)) { return false; } } return true; } /** Only answers of the backend named by the runtime config are kept. */ function shouldCacheApi(url, apiOrigin) { return Boolean(apiOrigin) && url.origin === apiOrigin && isCacheableApiPath(url.pathname); } /** The backend's origin from /api/runtime-config ({ apiUrl }), or null. */ function apiOriginFrom(config) { if (!config || typeof config.apiUrl !== "string") { return null; } try { var origin = new URL(config.apiUrl).origin; return origin === "null" ? null : origin; } catch { return null; } } /** Next.js route data (soft navigations): never kept, the page HTML is. */ function isRoutePayload(url) { return url.searchParams.has("_rsc"); } /** The build assets (scripts, styles, fonts) a page's HTML refers to. */ function extractAssetUrls(html) { var found = []; var match; NEXT_STATIC_ASSET.lastIndex = 0; while ((match = NEXT_STATIC_ASSET.exec(html)) !== null) { var asset = match[2].replace(/&/g, "&"); if (found.indexOf(asset) === -1) { found.push(asset); } } return found; } /** * Cached build assets no shell page refers to any more and not fetched * within a week (`date`: the answer's Date header, null when missing) - * leftovers of old builds. Lazily loaded chunks of the current build are * kept while they are in use. */ function buildAssetsToPrune(cached, current, now) { return cached .filter(function (entry) { var url = new URL(entry.url); if (url.pathname.indexOf(NEXT_STATIC_PREFIX) !== 0) { return false; } if (current.indexOf(url.pathname + url.search) !== -1) { return false; } return entry.date === null || now - entry.date > BUILD_ASSET_MAX_AGE_MS; }) .map(function (entry) { return entry.url; }); } var routes = { SHELL_ROUTES: SHELL_ROUTES, offlineFallbackFor: offlineFallbackFor, offlineHomeFor: offlineHomeFor, navigationPlanFor: navigationPlanFor, isShellRoute: isShellRoute, extractAssetUrls: extractAssetUrls, isCacheableApiPath: isCacheableApiPath, shouldCacheApi: shouldCacheApi, apiOriginFrom: apiOriginFrom, isRoutePayload: isRoutePayload, buildAssetsToPrune: buildAssetsToPrune, }; root.ApiCoreSwRoutes = routes; if (typeof module !== "undefined" && module.exports) { module.exports = routes; } })(typeof self !== "undefined" ? self : globalThis);