service-worker.js (6230B)
1 import { build, files, prerendered, version } from "$service-worker"; 2 import { _env } from "$lib/_env"; 3 import { DEFAULT_SQL_WASM_PATH } from "@radroots/client/sql/constants"; 4 import { DEFAULT_GEOCODER_DATABASE_PATH } from "@radroots/geocoder/constants"; 5 import { RADROOTS_ASSET_CACHE_NAME, RADROOTS_ASSET_CACHE_PREFIX } from "@radroots/utils"; 6 7 const APP_SHELL_URL = new URL(self.registration.scope).pathname; 8 const normalize_env_path = (value) => 9 typeof value === "string" && value.trim().length ? value.trim() : undefined; 10 const parse_env_path = (route_path) => { 11 const path_end = route_path.search(/[?#]/u); 12 return path_end >= 0 ? route_path.slice(0, path_end) : route_path; 13 }; 14 const ensure_env_wasm_path = (value, env_name) => { 15 const path = parse_env_path(value); 16 const normalized = path.toLowerCase(); 17 if (!normalized || normalized.endsWith("/")) 18 throw new Error(`${env_name} must include a .wasm filename`); 19 if (!normalized.endsWith(".wasm")) 20 throw new Error(`${env_name} must end with .wasm`); 21 return value; 22 }; 23 const ensure_env_asset_path = (value, env_name) => { 24 const path = parse_env_path(value); 25 if (!path || path.endsWith("/")) 26 throw new Error(`${env_name} must include a file path`); 27 return value; 28 }; 29 const SQL_WASM_ENV = normalize_env_path(_env.SQL_WASM_URL); 30 const SQL_WASM_URL = SQL_WASM_ENV 31 ? ensure_env_wasm_path( 32 SQL_WASM_ENV, 33 "RADROOTS_WEB_SQL_WASM_URL", 34 ) 35 : DEFAULT_SQL_WASM_PATH; 36 const GEOCODER_DB_ENV = normalize_env_path(_env.GEOCODER_DB_URL); 37 const GEOCODER_DB_URL = GEOCODER_DB_ENV 38 ? ensure_env_asset_path(GEOCODER_DB_ENV, "RADROOTS_WEB_GEOCODER_DB_URL") 39 : DEFAULT_GEOCODER_DATABASE_PATH; 40 const ASSET_URLS = [...new Set([SQL_WASM_URL, GEOCODER_DB_URL])]; 41 const PRECACHE_URLS = [...new Set([...build, ...files, ...prerendered, APP_SHELL_URL])].filter( 42 (url) => !url.includes("/.") 43 ); 44 const PRECACHE_LIST = PRECACHE_URLS.map((url) => ({ 45 url, 46 revision: version 47 })); 48 const APP_CACHE = `cache-app-shell-v${version}`; 49 const APP_CACHE_PREFIX = "cache-app-shell-v"; 50 const ASSET_CACHE = RADROOTS_ASSET_CACHE_NAME; 51 const ASSET_CACHE_PREFIX = RADROOTS_ASSET_CACHE_PREFIX; 52 53 const normalize_asset_url = (url) => { 54 const resolved = new URL(url, self.location.origin); 55 resolved.search = ""; 56 resolved.hash = ""; 57 return resolved.href; 58 }; 59 const ASSET_URL_KEYS = new Set(ASSET_URLS.map((url) => normalize_asset_url(url))); 60 const ASSET_URLS_ABS = ASSET_URLS.map((url) => new URL(url, self.location.origin).href); 61 62 const cache_assets = async () => { 63 const cache = await caches.open(ASSET_CACHE); 64 await Promise.all( 65 ASSET_URLS_ABS.map(async (url) => { 66 const cached = await cache.match(url); 67 if (cached) return; 68 try { 69 const response = await fetch(url); 70 if (response.ok || response.type === "opaque") await cache.put(url, response.clone()); 71 } catch { } 72 }) 73 ); 74 }; 75 76 const is_asset_request = (request_url) => ASSET_URL_KEYS.has(normalize_asset_url(request_url)); 77 78 const precache = async () => { 79 const cache = await caches.open(APP_CACHE); 80 await cache.addAll(PRECACHE_LIST.map((entry) => entry.url)); 81 await cache_assets(); 82 }; 83 84 const cleanup_caches = async () => { 85 const keys = await caches.keys(); 86 for (const key of keys) { 87 const is_app_cache = key.startsWith(APP_CACHE_PREFIX) && key !== APP_CACHE; 88 const is_asset_cache = key.startsWith(ASSET_CACHE_PREFIX) && key !== ASSET_CACHE; 89 if (!is_app_cache && !is_asset_cache) continue; 90 await caches.delete(key); 91 } 92 }; 93 94 const range_response = async (request, response) => { 95 const range = request.headers.get("range"); 96 if (!range || !response || response.type === "opaque") return response; 97 const bytes = /bytes=(\d+)-(\d+)?/u.exec(range); 98 if (!bytes) return response; 99 const start = Number(bytes[1]); 100 const end_raw = bytes[2]; 101 const buffer = await response.arrayBuffer(); 102 const end = end_raw ? Number(end_raw) : buffer.byteLength - 1; 103 if (!Number.isFinite(start) || !Number.isFinite(end) || start > end) return response; 104 const sliced = buffer.slice(start, end + 1); 105 const headers = new Headers(response.headers); 106 headers.set("Content-Range", `bytes ${start}-${end}/${buffer.byteLength}`); 107 headers.set("Content-Length", `${sliced.byteLength}`); 108 return new Response(sliced, { status: 206, statusText: "Partial Content", headers }); 109 }; 110 111 const cache_first = async (request, cache_name = APP_CACHE) => { 112 const cache = await caches.open(cache_name); 113 const cached = await cache.match(request); 114 if (cached) return await range_response(request, cached); 115 const response = await fetch(request); 116 if (response.ok || response.type === "opaque") await cache.put(request, response.clone()); 117 return response; 118 }; 119 120 const network_first = async (request, cache_name = APP_CACHE) => { 121 const cache = await caches.open(cache_name); 122 try { 123 const response = await fetch(request); 124 if (response.ok || response.type === "opaque") await cache.put(request, response.clone()); 125 return response; 126 } catch { 127 const cached = await cache.match(request); 128 if (cached) return await range_response(request, cached); 129 const fallback = await cache.match(APP_SHELL_URL); 130 if (fallback) return fallback; 131 return new Response("offline", { status: 503 }); 132 } 133 }; 134 135 self.addEventListener("install", (event) => { 136 event.waitUntil(precache().then(() => self.skipWaiting())); 137 }); 138 139 self.addEventListener("activate", (event) => { 140 event.waitUntil(cleanup_caches().then(() => self.clients.claim())); 141 }); 142 143 self.addEventListener("fetch", (event) => { 144 const request = event.request; 145 if (request.method !== "GET") return; 146 const url = new URL(request.url); 147 if (is_asset_request(request.url)) { 148 event.respondWith(cache_first(request, ASSET_CACHE)); 149 return; 150 } 151 if (request.mode === "navigate") { 152 event.respondWith(network_first(request)); 153 return; 154 } 155 if (url.origin === self.location.origin) event.respondWith(cache_first(request)); 156 });