import Dexie, { type Table } from 'dexie'; /** * On-device cache of map tiles, so the basemap survives a dead network. * * Why not a service worker? A SW only helps the web build; the Android app runs * the bundle inside Capacitor's WebView, where a SW never sees tile requests. An * IndexedDB-backed Leaflet layer caches tiles identically on web and native, and * keeps the failure contained to the map (no "stale app shell" class of bug). * * Growth is bounded: tiles are pruned oldest-used-first once the count exceeds * MAX_TILES, and oversized responses are skipped. */ interface CachedTile { /** `${z}/${x}/${y}` */ key: string; blob: Blob; cachedAt: number; lastUsed: number; } /** Skip caching anything bigger than this (a 256px PNG is normally 5–30 KB). */ export const MAX_TILE_BYTES = 500 * 1024; /** Roughly a large city's worth of tiles; pruning keeps us under this. */ const MAX_TILES = 3000; class TileDB extends Dexie { tiles!: Table; constructor() { super('SkateSpotsTilesDB'); this.version(1).stores({ tiles: 'key, lastUsed, cachedAt' }); } } let db: TileDB | null = null; function getDB(): TileDB { if (!db) db = new TileDB(); return db; } export async function getTile(key: string): Promise { try { const row = await getDB().tiles.get(key); if (!row) return null; // Touch lastUsed so pruning keeps tiles you actually look at. void getDB().tiles.update(key, { lastUsed: Date.now() }); return row.blob; } catch { return null; } } export async function putTile(key: string, blob: Blob): Promise { if (blob.size === 0 || blob.size > MAX_TILE_BYTES) return; try { const now = Date.now(); await getDB().tiles.put({ key, blob, cachedAt: now, lastUsed: now }); void prune(); } catch { // Quota / private mode — caching is best-effort, never fatal. } } let pruning = false; async function prune(): Promise { if (pruning) return; pruning = true; try { const count = await getDB().tiles.count(); if (count <= MAX_TILES) return; const oldest = await getDB() .tiles.orderBy('lastUsed') .limit(count - MAX_TILES) .primaryKeys(); await getDB().tiles.bulkDelete(oldest); } catch { // ignore } finally { pruning = false; } } export async function getTileCacheStats(): Promise<{ tiles: number; bytes: number }> { try { let bytes = 0; const tiles = await getDB().tiles.count(); await getDB().tiles.each((t) => { bytes += t.blob?.size ?? 0; }); return { tiles, bytes }; } catch { return { tiles: 0, bytes: 0 }; } } export async function clearTiles(): Promise { try { await getDB().tiles.clear(); } catch { // ignore } }