globalThis.Wegwichtel ??= {}; (function (ns) { 'use strict'; const CONTROL = 'wegwichtel-offline-control-v1'; const PREFIX = 'wegwichtel-offline-data-'; const STATE_URL = '/__wegwichtel_offline_state__'; const ROUTES_URL = '/__wegwichtel_offline_routes__'; const DIRECTORY = 'wegwichtel-offline-v1'; function key(value) { const url = new URL(value, globalThis.location.origin); if (url.origin !== globalThis.location.origin) throw new TypeError(ns.I18n.t("offline-storage.error.foreign-origin")); url.searchParams.delete('v'); if (url.pathname.startsWith('/api/')) url.searchParams.delete('_'); url.hash = ''; return url.href; } async function state() { const cache = await caches.open(CONTROL); const response = await cache.match(STATE_URL); return response ? response.json() : null; } async function routes() { const response = await (await caches.open(CONTROL)).match(ROUTES_URL); return response ? response.json() : []; } async function writeRoutes(saved) { await (await caches.open(CONTROL)).put(ROUTES_URL, new Response(JSON.stringify(saved), { headers: { 'Content-Type': 'application/json' } })); } async function folder(id, create = false) { const root = await navigator.storage.getDirectory(); const directory = await root.getDirectoryHandle(DIRECTORY, { create }); return directory.getDirectoryHandle(id, { create }); } async function writeFile(directory, name, contents) { const handle = await directory.getFileHandle(name, { create: true }); const stream = await handle.createWritable(); try { await stream.write(contents); await stream.close(); } catch (error) { await stream.abort().catch(() => {}); throw error; } } async function create() { const id = `${PREFIX}${crypto.randomUUID()}`; let directory = null; if (typeof navigator.storage?.getDirectory === 'function') { try { directory = await folder(id, true); } catch { /* Use CacheStorage when OPFS is unavailable. */ } } return { id, mode: directory ? 'filesystem' : 'cache', directory, resources: {}, count: 0 }; } async function put(stage, url, response) { if (!response.ok || response.type === 'opaque') throw new Error(ns.I18n.t("offline-storage.error.response")); const normalized = key(url); if (stage.mode === 'cache') { const cache = await caches.open(stage.id); await cache.put(normalized, response.clone()); } else { const filename = `${stage.count}.bin`; const headers = [...response.headers].filter(([name]) => !['content-encoding', 'content-length', 'transfer-encoding'].includes(name)); await writeFile(stage.directory, filename, await response.blob()); stage.resources[normalized] = { filename, headers, status: response.status, statusText: response.statusText }; } stage.count += 1; } async function remove(id) { if (!id?.startsWith(PREFIX)) return; await caches.delete(id); if (typeof navigator.storage?.getDirectory === 'function') { try { const root = await navigator.storage.getDirectory(); const directory = await root.getDirectoryHandle(DIRECTORY); await directory.removeEntry(id, { recursive: true }); } catch { /* Missing directory or disabled OPFS needs no cleanup. */ } } } async function commit(stage, route = null) { if (stage.mode === 'filesystem') { await writeFile(stage.directory, 'index.json', JSON.stringify(stage.resources)); } const savedRoutes = route ? await routes() : []; const previous = route ? savedRoutes.find(item => String(item.route.id) === String(route.id)) : await state(); const current = { id: stage.id, mode: stage.mode, count: stage.count, savedAt: new Date().toISOString(), appOnly: !route }; if (route) { const summary = { ...route }; delete summary.pois; delete summary.points; current.route = summary; await writeRoutes([...savedRoutes.filter(item => String(item.route.id) !== String(route.id)), current]); } else { const cache = await caches.open(CONTROL); await cache.put(STATE_URL, new Response(JSON.stringify(current), { headers: { 'Content-Type': 'application/json' } })); } if (previous?.id !== current.id) await remove(previous?.id).catch(() => {}); return current; } async function matchSnapshot(current, normalized) { if (!current) return null; if (current.mode === 'cache') return (await caches.open(current.id)).match(normalized); try { const directory = await folder(current.id); const index = JSON.parse(await (await (await directory.getFileHandle('index.json')).getFile()).text()); const entry = index[normalized]; if (!entry) return null; const file = await (await directory.getFileHandle(entry.filename)).getFile(); return new Response(file, { status: entry.status, statusText: entry.statusText, headers: entry.headers }); } catch { return null; } } async function match(url) { const normalized = key(url); const snapshots = [await state(), ...await routes()]; for (const snapshot of snapshots) { const response = await matchSnapshot(snapshot, normalized); if (response) return response; } return null; } async function clearRoute(routeId) { const saved = await routes(); const previous = saved.find(item => String(item.route.id) === String(routeId)); await writeRoutes(saved.filter(item => String(item.route.id) !== String(routeId))); await remove(previous?.id); } async function clear() { // Disable serving first; remove only our offline resources, never route drafts/media. const cache = await caches.open(CONTROL); const previous = await state(); await cache.delete(STATE_URL); await remove(previous?.id); } ns.OfflineStore = Object.freeze({ key, state, routes, create, put, commit, match, remove, clear, clearRoute }); }(globalThis.Wegwichtel));