From 1c7f336e7f54e9e20331f57149e00d509c8c63af Mon Sep 17 00:00:00 2001 From: hgosalia Date: Sat, 15 Aug 2026 17:15:33 -0400 Subject: [PATCH 1/3] Bug: fix Chrome cache enumeration issue --- .gitignore | 1 + README.md | 22 ++++++++++++++++ docs/tile-caching.md | 2 +- sw.js | 60 +++++++++++++++++++++++++++++++++++--------- 4 files changed, 72 insertions(+), 13 deletions(-) diff --git a/.gitignore b/.gitignore index 083f0e8..8152537 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,4 @@ tests/test-results/ # OS .DS_Store Thumbs.db +opencode.json diff --git a/README.md b/README.md index 71a4b45..eb7da32 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,28 @@ --- +## Background Service (macOS) + +Run the server as a persistent background daemon via `launchctl`: + +```bash +# Install the LaunchAgent (one-time setup) +launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.matrix.travel.plist + +# Start it now (no login required) +launchctl kickstart -k gui/$(id -u)/com.matrix.travel + +# Check status +launchctl print gui/$(id -u)/com.matrix.travel + +# Stop the service +launchctl bootout gui/$(id -u)/com.matrix.travel +``` + +The LaunchAgent (`~/Library/LaunchAgents/com.matrix.travel.plist`) runs headless — no `RunAtLoad` so it won't start on login unless you explicitly `kickstart` it. Logs go to `matrix-out.log` / `matrix-err.log` in the project directory. + +--- + ## Features | Feature | Detail | diff --git a/docs/tile-caching.md b/docs/tile-caching.md index bafabe5..daa218e 100644 --- a/docs/tile-caching.md +++ b/docs/tile-caching.md @@ -42,7 +42,7 @@ See [data-storage.md](data-storage.md) for full details on IndexedDB, `matrix-da - **Disk cache limit:** 500 MB with LRU eviction (oldest tiles removed down to 80% when exceeded) - **Eviction runs:** at startup and after each new tile is cached - **Eviction logging:** written to `matrix-requests.log` -- **SW Cache API limit:** 10,000 entries with zoom-aware LRU (low-zoom tiles z≤8 protected) +- **SW Cache API limit:** 5,000 entries with zoom-aware LRU (low-zoom tiles z≤8 protected). Kept below Chrome's `cache.keys()` enumeration limit (~10k) which throws `AbortError: Operation too large`; if a tile cache somehow exceeds it, the service worker resets that cache automatically. ## URL-based versioning diff --git a/sw.js b/sw.js index 193cda0..fc1358b 100644 --- a/sw.js +++ b/sw.js @@ -1,5 +1,5 @@ // Matrix — Service Worker for offline support -const CACHE_VERSION = 'matrix-v20'; +const CACHE_VERSION = 'matrix-v21'; const APP_CACHE = `${CACHE_VERSION}-app`; const TILE_CACHE = `${CACHE_VERSION}-tiles`; @@ -35,8 +35,9 @@ const TILE_PATTERNS = [ /glyphs?\//, // map font glyphs ]; -// Max cached tiles (LRU eviction when exceeded) -const MAX_TILES = 10000; +// Max cached tiles (LRU eviction when exceeded). Kept well below Chrome's +// cache.keys() limit (~10k entries) which throws "Operation too large". +const MAX_TILES = 5000; console.log(`SW: ${CACHE_VERSION} loaded`); @@ -54,12 +55,26 @@ self.addEventListener('install', (event) => { self.addEventListener('activate', (event) => { event.waitUntil( - caches.keys().then((keys) => { - return Promise.all( - // Keep current app + tile caches, and preserve tile caches from older - // versions (tiles are map data — still valid across app updates) - keys.filter((k) => k !== APP_CACHE && k !== TILE_CACHE && !k.endsWith('-tiles')).map((k) => caches.delete(k)) - ); + caches.keys().then(async (keys) => { + const current = new Set([APP_CACHE, TILE_CACHE]); + const oldTileCaches = keys.filter((k) => !current.has(k) && k.endsWith('-tiles')); + const oldAppCaches = keys.filter((k) => !current.has(k) && !k.endsWith('-tiles')); + + // Old tile caches are preserved (tiles are map data — still valid across + // app updates). But if a cache grew too large for Chrome to enumerate + // (cache.keys() throws "Operation too large"), it can never be evicted + // selectively, so reset it entirely. + await Promise.all(oldTileCaches.map(async (name) => { + try { + const cache = await caches.open(name); + await cache.keys(); + } catch { + console.warn(`SW: cache ${name} too large to enumerate, resetting`); + await caches.delete(name); + } + })); + + await Promise.all(oldAppCaches.map((k) => caches.delete(k))); }) ); self.clients.claim(); @@ -149,7 +164,12 @@ const TRANSPARENT_PNG = Uint8Array.from(atob('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCA async function tileStrategy(request) { // L1: browser Cache API (instant) - const cached = await caches.match(request, { ignoreVary: true }); + let cached; + try { + cached = await caches.match(request, { ignoreVary: true }); + } catch (err) { + // Cache may be corrupted or too large to query — fall through to L2/L3 + } if (cached) return cached; const proxyUrl = `${self.location.origin}/api/tiles/proxy?url=${encodeURIComponent(request.url)}`; @@ -159,7 +179,7 @@ async function tileStrategy(request) { const cacheResp = new Response(body, { status: 200, headers: { 'Content-Type': ct } }); const cache = await caches.open(TILE_CACHE); cache.put(request, cacheResp.clone()); - evictOldTiles(cache); + evictOldTiles(cache).catch(() => {}); return cacheResp; } catch { return new Response(body, { status: 200, headers: { 'Content-Type': ct } }); @@ -201,8 +221,24 @@ function zoomFromUrl(url) { return m ? parseInt(m[1], 10) : 99; } +let lastEvictCheck = 0; + async function evictOldTiles(cache) { - const keys = await cache.keys(); + // Don't enumerate the whole cache on every tile put — Chrome throws + // "Operation too large" when a cache has too many entries to list. + const now = Date.now(); + if (now - lastEvictCheck < 5000) return; + lastEvictCheck = now; + + let keys; + try { + keys = await cache.keys(); + } catch (err) { + // Cache too large to enumerate — can't evict selectively, so reset it. + console.warn('SW: tile cache too large, resetting'); + await caches.delete(TILE_CACHE); + return; + } if (keys.length <= MAX_TILES) return; // Protect low-zoom tiles (z ≤ 8) — they cover the most area const protectedKeys = []; From 53fafa947d4ac1bce1058aad47710672179731b9 Mon Sep 17 00:00:00 2001 From: hgosalia Date: Sat, 15 Aug 2026 19:47:24 -0400 Subject: [PATCH 2/3] updated README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index eb7da32..537ad16 100644 --- a/README.md +++ b/README.md @@ -101,4 +101,4 @@ The LaunchAgent (`~/Library/LaunchAgents/com.matrix.travel.plist`) runs headless --- -Built with [Claude Code](https://claude.ai/claude-code) using Claude Opus 4.6 +Built with [Claude Code](https://claude.ai/claude-code) From ca45837034b63d1276577861286f536e75352639 Mon Sep 17 00:00:00 2001 From: hgosalia Date: Sat, 15 Aug 2026 19:57:06 -0400 Subject: [PATCH 3/3] updated README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 537ad16..ddf8862 100644 --- a/README.md +++ b/README.md @@ -101,4 +101,4 @@ The LaunchAgent (`~/Library/LaunchAgents/com.matrix.travel.plist`) runs headless --- -Built with [Claude Code](https://claude.ai/claude-code) + Built with [Claude Code](https://claude.ai/claude-code)