Skip to content

Make service worker network-first instead of cache-first - #1022

Open
FlatterAtMainz wants to merge 9 commits into
jaedb:masterfrom
FlatterAtMainz:fix/sw-network-first-caching
Open

Make service worker network-first instead of cache-first#1022
FlatterAtMainz wants to merge 9 commits into
jaedb:masterfrom
FlatterAtMainz:fix/sw-network-first-caching

Conversation

@FlatterAtMainz

Copy link
Copy Markdown

Summary

  • The PWA service worker's fetch handler was cache-first with no real invalidation path: activate only purges old caches when the service worker script's own bytes change, which a normal webpack rebuild of app.min.js/app.min.css never does (fixed filenames, no content hash).
  • In practice, once a browser cached the app bundle on its first visit, it kept serving that exact bundle indefinitely — silently masking every subsequent deploy, including bug fixes, until a user manually cleared the service worker/cache storage.
  • Flipped the strategy to network-first: always attempt a live fetch first (still caching successful GET responses under the same blacklist rules as before), and only fall back to the cache when the network request itself fails. The cache now serves its intended purpose — an offline fallback — without being able to hide live updates from users who are online.

Test plan

  • Load Iris in a browser with the old service worker already cached; confirm a subsequent deploy is picked up on next reload without manually clearing site data.
  • Go offline after an initial successful load; confirm previously-fetched resources (app shell, art, etc.) still serve from cache.
  • Confirm blacklisted endpoints (me/tracks, me/albums, following/contains, etc.) are still never cached.

FlatterAtMainz and others added 9 commits July 20, 2026 17:32
.output-control__inner capped its height with max-height on mobile but
used overflow-y: visible, so a snapserver device/group list taller than
the popup just overflowed off-screen with no way to scroll to it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
max-height: calc(100vh - 140px) sizes the popup using the full
layout viewport, which on mobile includes space the browser's
address bar can occupy. When the address bar is visible, the
actually-visible viewport is shorter than 100vh, so the popup
(anchored to the bottom, growing upward) extends above what's
visible, hiding its top row. Add a 100dvh override, which tracks
the real visible viewport, after the 100vh fallback for browsers
that don't support dvh.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
On mobile the popup only auto-opens via PlaybackControls expanding
(force_expanded=true), which also hides the popup's own close button
(display:none in CSS) and suppressed its own click-outside overlay.
That left no way to dismiss just the output-control popup - only
tapping in the empty space above it (collapsing the whole playback
bar) worked. Always render the click-outside overlay regardless of
force_expanded so the popup can close independently.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Errors surfaced through handleException often ended up either blank
or as a bare technical fragment (e.g. "Could not load your profile"
with no explanation, or a raw "SyntaxError: JSON.parse: unexpected
end of input"). Add a humanizeError util that recognises common
failure shapes (invalid/empty JSON responses, network failures,
expired/revoked OAuth grants, known HTTP status codes) and prefixes
the description with a plain-language explanation, applied centrally
in the HANDLE_EXCEPTION handler so every existing call site benefits.

Also:
- handleException now falls back to a plain Error object's .message
  for the description, and no longer throws when data.error is
  undefined (message derivation had the same unguarded access).
- Wrap the JSON.parse of xhr.responseText in HANDLE_EXCEPTION in a
  try/catch - a non-JSON error response would otherwise throw inside
  the exception handler itself.
- Spotify getMe() and refreshToken() failures now get specific
  messages pointing at expired/revoked authorization, since that was
  the actual root cause behind the vague errors we hit in practice.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… optional chaining

- Humanized error explanations now live under errors.* in en.yaml
  (via i18n()) instead of being hardcoded in JS, so they're
  translatable like the rest of the UI.
- Replace `x && x.y` null checks with optional chaining per review
  feedback.
…ailures

getSearchResults never resolved its process notification on request
failure or on cancel-button click, leaving it stuck in the UI forever.
The five Spotify library-fetch actions had the same underlying gap
(no rejection handler at all), silently swallowing errors and leaking
stuck process state on any failed request.
The fetch handler served every cached response indefinitely with no
invalidation path: the cache is only purged in 'activate', which only
fires when the service worker script's own bytes change - something a
plain webpack rebuild of app.min.js/app.min.css never does. In
practice, once a browser cached the app bundle on first visit, it kept
serving that exact bundle forever, silently masking every subsequent
deploy (bug fixes included) until a user manually cleared the service
worker/cache storage.

Flip to network-first: always attempt a live fetch first (still
caching successful GET responses under the same blacklist rules as
before), and only fall back to the cache when the network request
itself fails. The cache now serves its intended purpose - an offline
fallback - without being able to hide live updates from users who are
online.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant