From 4a8b13b392b319a479afaf57c4cdd72b35354c7b Mon Sep 17 00:00:00 2001 From: anti Date: Fri, 24 Apr 2026 10:21:26 -0400 Subject: [PATCH] fix(web/session): instrument player lifecycle to catch async init failures MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The sync try/catch around AsciinemaPlayer.create() misses async failures in the player's internal init() promise — those land as unhandled rejections and are invisible from the component's POV. Subscribe to every lifecycle event (ready / play / pause / ended / error / errored / loading) and log the resolved duration. If the parser produces zero events despite a well-formed cast, duration resolves to 0 / NaN / rejected — one of those signals will point at whichever frame the render path is silently failing at. --- decnet_web/src/components/SessionDrawer.tsx | 23 +++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/decnet_web/src/components/SessionDrawer.tsx b/decnet_web/src/components/SessionDrawer.tsx index db739125..e7bdfe5e 100644 --- a/decnet_web/src/components/SessionDrawer.tsx +++ b/decnet_web/src/components/SessionDrawer.tsx @@ -145,13 +145,32 @@ const SessionDrawer: React.FC = ({ decky, sid, fields, onClo `| events=${playable.length} | cols=${header.width} rows=${header.height}`, ); try { - playerInstance.current = AsciinemaPlayer.create( + const p = AsciinemaPlayer.create( { data: cast }, playerContainer.current, { fit: 'width', terminalFontSize: '12px' }, ); + playerInstance.current = p; + // The player's init() is async; any failure there bypasses the + // sync try/catch above and lands as an unhandled rejection. + // Hook every lifecycle event so we can see which state it + // actually ends up in ("loading" / "ended" / "errored" / etc). + for (const evt of ['ready', 'play', 'pause', 'ended', 'error', 'errored', 'loading']) { + try { + p.addEventListener?.(evt, (...args: unknown[]) => + console.debug(`asciinema-player event: ${evt}`, ...args), + ); + } catch { /* addEventListener may not support this event name */ } + } + // getDuration() resolves once the recording is parsed. If it + // resolves to 0 or NaN we know the parser produced an empty + // events stream despite the cast looking well-formed. + p.getDuration?.().then( + (d: number) => console.debug('asciinema-player duration:', d), + (err: unknown) => console.error('asciinema-player getDuration failed:', err), + ); } catch (err) { - console.error('asciinema-player failed to mount', err); + console.error('asciinema-player failed to mount (sync):', err); } return () => { if (playerInstance.current) {