A stale bridge must not kill the heartbeat, and rc3 must invalidate the shell

Caught on hardware immediately after deploying rc3 to alpha: the player kept
playing content while reporting nothing at all, throwing every 15 seconds.

    Uncaught TypeError: BS.telemetrySnapshot is not a function

The page was rc3 and the bridge it ran was older. Two causes, both fixed.

CACHE_NAME stayed at rd-player-v19 across a release that changed both the
service worker's fetch strategy and the shipped /player assets. The activate
handler deletes every cache whose name does not match, so keeping the name kept
the previous shell cache alive — including a stale st-bridge.js. Bumped to v20.
Content lives in its own cache, so this costs a small shell re-download and never
re-fetches a playlist.

The deeper defect is that the call site treated an optional bridge method as
guaranteed. It was the ONLY unguarded BS.* call in the player; every other one
checks or wraps. The bridge and the page are halves of one contract but are
fetched separately, so version skew is a normal condition, not an anomaly — it
must degrade, not throw. Now guarded on typeof, so a skewed pair reports the
fields it can and keeps heartbeating.

Worth naming the failure shape: the display looked perfectly healthy. Content
played, the socket connected, the device showed online — and telemetry silently
stopped. Anything that reports health through the same path it is breaking will
fail this way.

1056 pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uaeo9MvzKoyXuN6ZsbhtkL
This commit is contained in:
ScreenTinker 2026-08-05 12:18:44 -05:00
parent 3059d8cd5c
commit 7233466030
2 changed files with 11 additions and 2 deletions

View file

@ -1718,7 +1718,11 @@
device_utc: Date.now(),
// Real values where the platform has them (temperature, storage quota). Spread LAST so
// it overrides the nulls above, and empty off-platform so nothing else changes.
...(BS ? BS.telemetrySnapshot() : {}),
// typeof, not truthiness: the bridge and this page are halves of one contract but are
// fetched separately, so a player can run a NEW page against a CACHED older bridge.
// Calling a method that version lacks threw here every 15s and took the whole heartbeat
// with it — the display kept playing while silently reporting nothing at all.
...(BS && typeof BS.telemetrySnapshot === 'function' ? BS.telemetrySnapshot() : {}),
}
});
}, HEARTBEAT_INTERVAL);

View file

@ -1,4 +1,9 @@
const CACHE_NAME = 'rd-player-v19';
// v20: rc3 changed the fetch strategy AND the shipped player assets. The activate handler deletes
// every cache whose name does not match, so leaving this at v19 kept the previous shell cache alive
// — a player then ran a new index.html against a stale st-bridge.js and threw on every heartbeat.
// Bump whenever a shipped /player asset changes shape; content lives in its own cache, so this
// costs a small re-download and never re-fetches the playlist.
const CACHE_NAME = 'rd-player-v20';
// Content lives in its own cache so the shell can be re-versioned (the activate handler deletes
// every cache that is not CACHE_NAME) WITHOUT throwing away megabytes of media that are still
// perfectly valid. Rolling the shell used to mean a player re-downloaded its entire playlist.