Not a member of GistPad yet?
Sign Up,
it unlocks many cool features!
- /// <reference types="@sveltejs/kit" />
- /// <reference no-default-lib="true"/>
- /// <reference lib="esnext" />
- /// <reference lib="webworker" />
- import { openDB } from "idb"
- declare const self: ServiceWorkerGlobalScope
- // ───────────────────────────────────────────────────────
- // CONFIG
- // ───────────────────────────────────────────────────────
- const CACHE_NAME = "map-h3-v2"
- const DB_NAME = "matatu-gps-db"
- const STORE_OUTBOX = "gps-outbox"
- const STATIC_ASSETS = [
- "/maps/bootstrap.parquet"
- ]
- // ───────────────────────────────────────────────────────
- // INSTALL / ACTIVATE
- // ───────────────────────────────────────────────────────
- self.addEventListener("install", (event) => {
- event.waitUntil(
- caches.open(CACHE_NAME).then((cache) => cache.addAll(STATIC_ASSETS))
- )
- self.skipWaiting()
- })
- self.addEventListener("activate", (event) => {
- event.waitUntil(
- caches.keys().then((keys) =>
- Promise.all(keys.filter(k => k !== CACHE_NAME).map(k => caches.delete(k)))
- )
- )
- self.clients.claim()
- })
- // ───────────────────────────────────────────────────────
- // FETCH (H3-native)
- // ───────────────────────────────────────────────────────
- self.addEventListener("fetch", (event) => {
- const req = event.request
- if (req.method !== "GET") return
- const url = new URL(req.url)
- // 1. MAP (H3-based)
- if (url.pathname.includes("/map") && hasHexParams(url)) {
- const normalized = normalizeHexRequest(req, "map")
- event.respondWith(cacheFirst(normalized))
- return
- }
- // 2. BUILDINGS (H3-based)
- if (url.pathname.includes("/buildings") && hasHexParams(url)) {
- const normalized = normalizeHexRequest(req, "buildings")
- event.respondWith(cacheFirst(normalized))
- return
- }
- // 3. Ignore streams
- if (url.pathname.includes("/api/gps/stream")) return
- // 4. Let app handle everything else
- })
- // ───────────────────────────────────────────────────────
- // MESSAGE HANDLERS
- // ───────────────────────────────────────────────────────
- self.addEventListener("message", (event) => {
- const data = event.data
- if (!data) return
- switch (data.type) {
- case "PREFETCH_HEXES":
- event.waitUntil(prefetchHexes(data.mapHexes || [], data.buildingHexes || []))
- break
- case "CACHE_CITY":
- event.waitUntil(cacheCity(data.mapHexes || [], data.buildingHexes || []))
- break
- }
- })
- // ───────────────────────────────────────────────────────
- // PREFETCH (UNIFIED H3)
- // ───────────────────────────────────────────────────────
- async function prefetchHexes(
- mapHexes: string[],
- buildingHexes: string[]
- ): Promise<void> {
- const cache = await caches.open(CACHE_NAME)
- const requests = [
- ...chunkHexes(mapHexes).map(h => `/map?hexes=${h.join(",")}`),
- ...chunkHexes(buildingHexes).map(h => `/buildings?hexes=${h.join(",")}`)
- ]
- await Promise.all(
- requests.map(async (url) => {
- const req = normalizeHexRequest(new Request(url), url.includes("/map") ? "map" : "buildings")
- if (!(await cache.match(req))) {
- try {
- const res = await fetch(req)
- if (res.ok) await cache.put(req, res.clone())
- } catch {}
- }
- })
- )
- notifyClients({ type: "PREFETCH_COMPLETE" })
- }
- // ───────────────────────────────────────────────────────
- // OFFLINE CACHE (H3-native)
- // ───────────────────────────────────────────────────────
- async function cacheCity(
- mapHexes: string[],
- buildingHexes: string[]
- ): Promise<void> {
- const total = mapHexes.length + buildingHexes.length
- let completed = 0
- // map data
- for (const chunk of chunkHexes(mapHexes)) {
- await fetchAndCache(`/map?hexes=${chunk.join(",")}`)
- completed += chunk.length
- notifyClients({
- type: "CACHE_PROGRESS",
- progress: completed / total,
- phase: "map"
- })
- }
- // buildings
- for (const chunk of chunkHexes(buildingHexes)) {
- await fetchAndCache(`/buildings?hexes=${chunk.join(",")}`)
- completed += chunk.length
- notifyClients({
- type: "CACHE_PROGRESS",
- progress: completed / total,
- phase: "buildings"
- })
- }
- notifyClients({ type: "CITY_CACHED" })
- }
- // ───────────────────────────────────────────────────────
- // BACKGROUND SYNC
- // ───────────────────────────────────────────────────────
- self.addEventListener("sync", (event: SyncEvent) => {
- if (event.tag === "sync-gps-data") {
- event.waitUntil(flushGpsOutbox())
- }
- })
- async function flushGpsOutbox(): Promise<void> {
- const db = await openDB(DB_NAME, 1)
- const items = await db.getAll(STORE_OUTBOX)
- for (const item of items) {
- try {
- const res = await fetch("/api/map/gps-update", {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify(item),
- })
- if (res.ok) {
- await db.delete(STORE_OUTBOX, item.id)
- } else {
- throw new Error()
- }
- } catch (err) {
- console.warn("[sw] Sync retry failed:", err)
- return
- }
- }
- notifyClients({ type: "SYNC_COMPLETE" })
- }
- // ───────────────────────────────────────────────────────
- // CACHE HELPERS
- // ───────────────────────────────────────────────────────
- async function cacheFirst(req: Request): Promise<Response> {
- const cache = await caches.open(CACHE_NAME)
- const cached = await cache.match(req)
- if (cached) return cached
- const res = await fetch(req)
- if (res.ok) {
- cache.put(req, res.clone())
- notifyClients({ type: "CACHE_UPDATED", url: req.url })
- }
- return res
- }
- async function fetchAndCache(url: string) {
- const cache = await caches.open(CACHE_NAME)
- const req = normalizeHexRequest(new Request(url), url.includes("/map") ? "map" : "buildings")
- if (!(await cache.match(req))) {
- try {
- const res = await fetch(req)
- if (res.ok) await cache.put(req, res.clone())
- } catch {}
- }
- }
- // ───────────────────────────────────────────────────────
- // NORMALIZATION (CRITICAL)
- // ───────────────────────────────────────────────────────
- function normalizeHexRequest(req: Request, type: "map" | "buildings"): Request {
- const url = new URL(req.url)
- let hexes: string[] = []
- if (url.searchParams.has("hex")) {
- hexes = [url.searchParams.get("hex")!]
- }
- if (url.searchParams.has("hexes")) {
- hexes = url.searchParams.get("hexes")!.split(",")
- }
- // 🔥 canonical form
- hexes = hexes.map(h => h.trim()).filter(Boolean).sort()
- const normalized = `/${type}?hexes=${hexes.join(",")}`
- return new Request(normalized)
- }
- function hasHexParams(url: URL): boolean {
- return url.searchParams.has("hex") || url.searchParams.has("hexes")
- }
- // chunking prevents huge requests
- function chunkHexes(hexes: string[], size = 30): string[][] {
- const chunks: string[][] = []
- for (let i = 0; i < hexes.length; i += size) {
- chunks.push(hexes.slice(i, i + size))
- }
- return chunks
- }
- // ───────────────────────────────────────────────────────
- // CLIENT MESSAGING
- // ───────────────────────────────────────────────────────
- async function notifyClients(message: Record<string, unknown>) {
- const clients = await self.clients.matchAll({ includeUncontrolled: true })
- for (const client of clients) {
- client.postMessage(message)
- }
- }
RAW Paste Data
Copied
