Skip to content

R3: resume, toggle, and pause intent move into the machine#47

Merged
adyz merged 2 commits into
masterfrom
r3-resume-in-masina
Jul 4, 2026
Merged

R3: resume, toggle, and pause intent move into the machine#47
adyz merged 2 commits into
masterfrom
r3-resume-in-masina

Conversation

@adyz

@adyz adyz commented Jul 4, 2026

Copy link
Copy Markdown
Owner

Third phase of the post-review refactor plan (plan.md, R3) — the core one. The adapter is now pure forwarding: every user gesture is one send(), and what it means per state is the machine's decision. Old e2e suite untouched: 37/37 green; 69/69 unit (6 new); clean typecheck.

Bugs closed (all from the review)

  1. Lock-screen Play was a silent no-op in error — the 'play' action handler blindly called resumeRadio() (a bare play() on a cleared src), while the on-screen button routed through playRadio. Both are now the same RESUME event: in idle/error/recovering it raises PLAY with the selected station.
  2. Toggle during loading restarted playback the user had stoppedpauseRadio()'s AbortError was swallowed and the loading timeout later retried. TOGGLE in loading/retrying now raises a full STOP, consistent with the lock-screen pause policy. New unit test locks it.
  3. Restart-after-long-pause lost the offline fast-fail (regression from the migration) — the shared restartAfterLongPause ladder (used by PLAYER_PLAY, RESUME, TOGGLE) now honors isOnline: resuming on a dead network goes straight to error + recovery loop instead of ~9s of loading tone.

Design

  • RESUME in paused → invoked attemptPlay in a paused.resuming substate: presentation stays 'paused' (no new RadioState, no STATE_FX entry), a failed play() re-applies the paused fx exactly like the old RESUME_FAILED re-entry, and leaving the substate discards a stale resolve/reject like every other attemptPlay. getState()/the transition log normalize the compound value, so logs read the same.
  • PAUSE_REQUESTED replaces USER_PAUSE_INTENT: marks intent + pauses at root; the four feedback states override it (and TOGGLE) with a raised STOP — the stop-vs-pause policy left mediaSession.ts.
  • USER_PAUSE_INTENT and RESUME_FAILED events are gone; handleResumeError/resumePlayer deleted from the adapter; playerIsPaused left RadioDeps (nothing reads the element's paused flag anymore).

Deliberate behavior notes

  • resumeRadio() while already playing is now a no-op (was: a redundant play() on the playing element). One old test pinned that blind forward; it now asserts the no-op.
  • The known sub-100ms race where the element's paused flag disagrees with the machine state resolves by state now, not by element flag.

⚠️ Needs device smoke before merge

Per the plan, this touches the lock-screen paths: play/pause/prev/next from paused AND from error, resume after long pause, offline behavior — on iPhone (and ideally the macOS Now Playing widget).

🤖 Generated with Claude Code

From the post-review refactor plan (plan.md, R3). The adapter is now pure
forwarding — every user gesture is one send(); what it MEANS per state is
the machine's decision. Old e2e suite untouched and green (37/37).

New events, replacing USER_PAUSE_INTENT and RESUME_FAILED:
- RESUME — paused: in-machine play attempt (invoked attemptPlay in a
  paused.resuming substate; presentation stays 'paused', a failed play()
  re-applies the paused fx, leaving the substate discards stale
  resolve/reject). idle/error/recovering: raises PLAY with the selected
  station. Fixes the confirmed bug: lock-screen Play was a silent no-op in
  'error' (the on-screen button worked) — both are now the same event.
- TOGGLE — playing: pause with user intent marked; paused/idle/error/
  recovering: same as RESUME; loading/retrying: full STOP. Fixes the
  confirmed bug: toggle during loading swallowed the pause (AbortError)
  and the loading timeout then restarted playback the user had stopped.
- PAUSE_REQUESTED — marks intent + pauses; the feedback states override it
  (and TOGGLE) with a raised STOP, so the stop-vs-pause policy left
  mediaSession.ts and lives with the other policies.

Also fixed (confirmed regression): restart-after-long-pause now honors the
same offline fast-fail as PLAY — resuming on a dead network goes straight
to the error state + recovery loop instead of ~9s of loading tone. Shared
restartAfterLongPause ladder serves PLAYER_PLAY, RESUME and TOGGLE.

Fallout:
- playerIsPaused left RadioDeps — nothing reads the element's paused flag
  anymore; toggle is state-driven.
- radioCore: handleResumeError/resumePlayer deleted; getState/log
  normalize the compound 'paused' state to the flat RadioState.
- mediaSession 'pause' handler is a plain core.pauseRadio().
- 6 new unit tests spec the fixed behaviors; 2 resume tests adapted to the
  event API (one now asserts resume-while-playing is a no-op, previously
  it pinned the blind play() forward).

NEEDS DEVICE SMOKE before merge (lock screen: play/pause/prev/next from
paused AND from error; resume after long pause; offline).

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 5:52am

@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 69/69 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

Play-then-immediate-lock -> wifi off: loading tone plays but the error
tone never starts (fresh background start refused by iOS); once the app
has been through an error cycle with the screen open, later lock-screen
cycles work. The single tone channel (src swap on an already-audible
element) fixes this by construction.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@adyz adyz merged commit 39f5f4d 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