'use strict'; /* * THE BRICK: a `let` read during boot, before its declaration had executed. * * Found the hard way on 2026-08-07 — a BrightSign XT245 on shipped 1.9.32 went dark and STAYED * dark across reboots. The exit beacon said: * * crashed: Cannot access '_videoCompositingOk' before initialization @ player:3730:12 * * Boot restores the CACHED playlist and renders item 0 immediately, from a call site ~2300 lines * above where `_videoCompositingOk` was declared. When that item was a video carrying a transition, * `isVideoBufferable` read the binding while it was still in the temporal dead zone. A TDZ read is * a *throw*, not a `null` — so the player died during boot. * * And because the offending playlist came from the device's OWN localStorage cache, it never * stayed up long enough to receive a corrected one. Every boot re-read the same poisoned cache and * died the same way: a permanent brick, recoverable only by clearing device storage. Rebooting the * player — the one remedy an operator has — did nothing. * * Nothing about this is BrightSign-specific. Any web-based player could hit it. * * The fix is placement, so the test is about placement: anything boot can reach must be declared * before boot runs. A guard rather than a repro, because reproducing it needs a whole page * lifecycle — and a guard is what stops it coming back when someone tidies the declaration back * down next to its function, which is exactly where it looked like it belonged. */ const { test } = require('node:test'); const assert = require('node:assert/strict'); const fs = require('fs'); const path = require('path'); const PLAYER = fs.readFileSync(path.join(__dirname, '..', '..', 'server/player/index.html'), 'utf8'); const lineOf = (needle) => PLAYER.slice(0, PLAYER.indexOf(needle)).split('\n').length; test('the compositing cache is declared before the Boot section, not beside its function', () => { const decl = PLAYER.indexOf('let _videoCompositingOk = null;'); const boot = PLAYER.indexOf('==================== Boot ===================='); assert.ok(decl > 0, '_videoCompositingOk declaration not found'); assert.ok(boot > 0, 'Boot section marker not found'); assert.ok( decl < boot, `declared at line ${lineOf('let _videoCompositingOk = null;')} but Boot starts at line ` + `${lineOf('==================== Boot ====================')} — boot renders the cached playlist ` + 'and would read this binding in its temporal dead zone, bricking the player on every boot', ); }); test('it is declared exactly once — a second `let` would shadow nothing and throw again', () => { const n = (PLAYER.match(/let _videoCompositingOk\b/g) || []).length; assert.equal(n, 1, `expected one declaration, found ${n}`); }); test('every CODE read of it happens after the declaration', () => { // Comments must not count: this file documents the bug by name, both here and at the old // declaration site, and those mentions sit above the declaration by design. const stripped = PLAYER .replace(/\/\*[\s\S]*?\*\//g, (m) => ' '.repeat(m.length)) // block comments -> spaces, offsets preserved .replace(/(^|[^:])\/\/[^\n]*/g, (m) => ' '.repeat(m.length)); // line comments (not "://" in URLs) const decl = stripped.indexOf('let _videoCompositingOk = null;'); assert.ok(decl > 0, 'declaration not found in stripped source'); const early = []; const re = /_videoCompositingOk/g; let m; while ((m = re.exec(stripped))) if (m.index < decl) early.push(m.index); assert.equal(early.length, 0, `${early.length} code read(s) precede the declaration — each one is a temporal-dead-zone throw`); }); test('the boot path really does render a cached item before the old declaration site', () => { // Documents WHY the ordering matters, so a future reader can see the hazard is structural and // not a style preference. If this ever stops being true the guard above is merely harmless. const restore = PLAYER.indexOf('const cachedPlaylist = loadPlaylistCache();'); const consumer = PLAYER.indexOf('&& _videoCompositingOk !== false;'); assert.ok(restore > 0 && consumer > 0); assert.ok( restore < consumer, 'boot restores and renders the cached playlist before the consumer appears in source order — ' + 'which is the whole reason the declaration must be hoisted above boot', ); // And the consumer is reached only for video, which is why an image-first playlist survived it. const decl = PLAYER.slice(PLAYER.indexOf('const isVideoBufferable'), consumer + 40); assert.match(decl, /mime_type\.startsWith\('video\/'\)/, 'the short-circuit on video mime is what kept image-first playlists alive'); });