Skip to content

R4: stop the zombie stream on stall/error; fix the ensure() preload race#48

Merged
adyz merged 3 commits into
masterfrom
r4-fixuri-mici
Jul 4, 2026
Merged

R4: stop the zombie stream on stall/error; fix the ensure() preload race#48
adyz merged 3 commits into
masterfrom
r4-fixuri-mici

Conversation

@adyz

@adyz adyz commented Jul 4, 2026

Copy link
Copy Markdown
Owner

Fourth phase of the post-review refactor plan (plan.md, R4): two small behavior fixes, each with dedicated unit tests. Old e2e untouched: 37/37 green; 75/75 unit (6 new); clean typecheck.

Fix 1 — zombie stream under the loading tone (PLAUSIBLE finding, philosophy violation)

STALLED and PLAYER_ERROR from playing now run stopPlayer on the way to retrying, symmetric with the LOADING_TIMEOUT path. Before, the stalled stream stayed attached and un-paused: if the buffer refilled during the 3s RETRY_DELAY, the stream resumed audibly under the loading tone — overlapping sounds, which this project never allows. paused's PLAYER_ERROR keeps the old behavior (the element is already silent there).

Fix 2 — ensure() cancelling the sound it was supposed to protect (CONFIRMED finding)

On a slow first load, a supervisor tick landing while play() still waited for the blob preload would call element.play() on the empty src; the NotSupportedError flipped isPlaying to false and the pending start bailed — one extra ~2.5s of silence exactly when the feedback sound mattered most. ensure() now leaves the element alone while nothing has been started yet (no src attribute ⇒ startPlayback hasn't run; the pending start is still in flight). Minimal fix only — the full reconcile() redesign stays in 4b.

Also in this PR

  • First unit tests for soundEffects.ts (the review flagged it as the most delicate untested async code): fake <audio> element + controlled fetch, covering the race, OS-pause recovery, denied-play restart, and stop() semantics.
  • plan.md: R1–R3 marked done; 4b direction corrected after re-reading the PR Handoff real între sunete: cel vechi cântă până când cel nou chiar se aude #41 branch history — not a single feedback element: without a partner element there's no "never trade audible for silent" safety net (31d684a), and a0aec46 shows any real silence gap on a locked iPhone kills the session and gets the pending play() denied. 4b = the device-verified handoff/carry semantics (deferred stop + carry as last resort + reclaim), driven from the machine/reconciler instead of ad-hoc DOM coordination.

No lock-screen surface touched — no device smoke needed for this one (CI is enough).

🤖 Generated with Claude Code

… a pending sound start

From the post-review refactor plan (plan.md, R4). Two small behavior
fixes, each with dedicated unit tests; old e2e untouched and green (37/37).

- radioMachine: STALLED and PLAYER_ERROR from 'playing' now run stopPlayer
  on the way to 'retrying', symmetric with the LOADING_TIMEOUT path. The
  stalled stream stayed attached before, and a refilled buffer could
  resume audibly UNDER the loading tone during the 3s RETRY_DELAY —
  overlapping sounds, which the machine must never allow. (paused's
  PLAYER_ERROR keeps the old behavior: the element is already silent
  there, no overlap risk.)

- soundEffects: minimal fix for the ensure() vs pending-preload race.
  On a slow first load, a supervisor tick landing while play() still
  waited for the blob would call element.play() on the empty src, the
  NotSupportedError flipped isPlaying to false, and the pending start
  bailed — one extra ~2.5s of silence exactly when the feedback sound
  mattered. ensure() now leaves the element alone while nothing was
  started yet (no src attribute = startPlayback hasn't run). The full
  reconcile() redesign stays in 4b.

- soundEffects.test.ts: first unit tests for this module (fake <audio>
  element + controlled fetch): the race, OS-pause recovery, denied-play
  restart, and stop() semantics.

- plan.md: R1-R3 marked done; 4b direction corrected after re-reading
  the PR #41 history with Adrian — NOT a single feedback element (no
  "never trade audible for silent" safety net without a partner element,
  and a0aec46 shows any real silence gap on a locked iPhone kills the
  session). 4b = the device-verified handoff/carry semantics, driven
  from the machine/reconciler instead of ad-hoc DOM coordination.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 4, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
radio Ready Ready Preview, Comment Jul 4, 2026 6:11am

@github-actions

github-actions Bot commented Jul 4, 2026

Copy link
Copy Markdown

CI Summary

Check Status Result
Typecheck PASS tsc --noEmit clean
Unit tests PASS 75/75 passed; Coverage: Lines 100%, Branches 96.20%, Functions 97.14%, Statements 100%
Build PASS Build completed
Playwright browser PASS Chromium installed
E2E tests PASS 37/37 passed

…omes R4b, next after R4

Adrian's explicit requirement: the error sound must be audible on the
lock screen FROM THE FIRST TIME, not only after an error cycle with the
app open. The play-then-lock repro is a requirement bug, not an accepted
limitation. PR #41 proved the mechanism works on device; R4b rebuilds it
machine-driven, ordered before R5/R6.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adrian's device observations: after the silent-error state, unlocking
shows the error screen with no sound; next/prev do NOT bring it back
(the user's gesture is squandered — isPlaying intent is true while a
denied programmatic attempt is in flight, so warmUp/play early-return
and no element.play() runs inside the gesture stack); stop+play works
because stop() force-resets the intent. New reconciler rule: every user
gesture reconciles reality (element.paused → play() inside the gesture),
plus four device acceptance criteria for R4b.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@adyz adyz merged commit 4d59162 into master Jul 4, 2026
3 checks passed
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