Paint saved playlists before the launch sync and swap a background-downloaded update instantly

This commit is contained in:
Jonathan Sykes
2026-09-22 13:27:12 +08:00
parent 691e204d61
commit 987697e759
7 changed files with 300 additions and 18 deletions

View File

@@ -309,6 +309,20 @@ Local DB file: `server/data/ytplayer.db` (gitignored). `BUILD_TAG` is computed f
worker and the SW_UPDATE_AVAILABLE broadcast are only *prompts to re-check*.
This loop was "fixed" three times by chasing individual triggers — don't add
a trigger that calls `showUpdateBanner()` directly.
- **Background download + instant swap (added 2026-09-22).** A new worker
precaches the whole shell during `install`, so by the time the user presses
"Refresh UI" the build is usually already on disk. `applyUpdate` asks the
**waiting** worker `CACHE_STATUS` over a `MessageChannel`; when it answers
`ready: true` (it re-checks every SHELL url, since a cache can be evicted)
the download is skipped entirely and the swap is immediate.
Two rules keep this safe: the probe is **opt-in** (`askStatus`) — never
implicit, because a probe awaiting a reply hangs forever if the caller
injects a `setTimeout` that never fires (the existing tests do exactly
that) — and *any* doubt (no reply, timeout, thrown error, `ready: false`)
falls back to the all-or-nothing download below, which is still what
guarantees correctness. `maybeShowUpdateBanner` also calls `prefetchUpdate()`
→ `registration.update()` so the download starts the moment a new build is
seen rather than when the user clicks.
- **Refresh UI** (`frontend/sw-update.js`): `refreshShellInPlace()` re-downloads
every file the shell caches hold — cache-busted (`?__ytpfresh=`) so even an
OLD worker's cache-first handler can't answer from its cache, 3 tries per file,
@@ -361,6 +375,19 @@ Local DB file: `server/data/ytplayer.db` (gitignored). `BUILD_TAG` is computed f
the fetch resolves to `null` offline and `respondWith(null)` throws. It ends
with `|| Response.error()`.
## First paint vs. the network (launch)
`boot()` used to `await` the profile pull and the shared-playlist inbox before
the first `render()`, so on a slow link the sidebar stayed empty for as long as
the network took. Everything the device knows is already in localStorage, so it
now paints immediately and reconciles afterwards:
- `?list=` / `?profile=` share links still run **before** the first paint — they
*replace* the synced slice, so painting first would flash the old playlists
and swap them out. They are skipped entirely when the parameter is absent.
- `syncOnLaunch()` then runs the network pass with a spinner (`#syncSpinner`,
beside the Playlists header, `setSyncing()` is counted so the last finisher
clears it) and re-renders **only if** `playlistFingerprint()` changed — a
needless render would drop the sidebar's scroll position.
## Data model quirks
- Client state persists in localStorage key **`_ytpdata`** and syncs (debounced
400 ms) to `POST /api/user/sync`, keyed by a browser fingerprint.