Skip to content

Faza 4a: XState v5 — mașina declarativă înlocuiește orchestrarea manuală#43

Merged
adyz merged 3 commits into
masterfrom
faza4-xstate
Jul 3, 2026
Merged

Faza 4a: XState v5 — mașina declarativă înlocuiește orchestrarea manuală#43
adyz merged 3 commits into
masterfrom
faza4-xstate

Conversation

@adyz

@adyz adyz commented Jul 3, 2026

Copy link
Copy Markdown
Owner

Ce face

Faza 4a din plan.md: toată orchestrarea manuală din radioCore (timeri, playId, contoare de retry/recovery) devine mașină XState v5 declarativă (radioMachine.ts), cu paritate 100% de comportament — dovedită de suita e2e neatinsă.

Ce dispare și cu ce se înlocuiește

Înainte (manual) Acum (declarativ)
3 timeri gestionați de mână (retry/loading/recovery) + clearTimeout peste tot after-delays — ieșirea din stare le anulează automat
playId incrementat manual contra race-urilor playerPlay() e promise actor invocat — un resolve întârziat după ieșirea din stare e aruncat prin construcție
watchdog + sound supervisor porniți/opriți imperativ callback actors invocați exact în stările care au nevoie de ei
setState() care putea fi chemat de oriunde, fără validare tranziții definite explicit — orice altceva e imposibil

STATE_FX rămâne tabel declarativ (entry action parametrizată per stare — exact filozofia "un state = efecte precise"). Backoff-ul de recovery e o expresie de delay peste context.recoveryCount; recheck-ul offline e self-transition cu cadență fixă, fără escaladare.

radioCore.ts devine adaptor subțire cu același API publicmain.ts și celelalte module neatinse (minus 2 deps dispărute: setTimeout/clearTimeout, care acum trăiesc în clock-ul actorului).

Capcana pe care doar e2e-ul a prins-o

Timerele globale de browser aruncă Illegal invocation când sunt apelate ca metode pe deps (cer this === window). Unit testele (mock-uri) nu aveau cum s-o vadă — suita e2e neatinsă a prins-o instant (18/37 roșii la prima rulare, 37/37 după wrapper-ul de o linie). Exact rolul ei de dovadă.

Verificare

  • tsc --noEmit curat • ✅ Unit 63/63 portate (SimulatedClock injectat, aserțiunile playId devenite comportamentale) • ✅ E2E 37/37 — NEATINSE • ✅ Coverage 99% pe mașină • ✅ Smoke complet pe vite preview
  • Cost: bundle 5.7 → 19.3KB gzip (xstate)
  • De verificat pe preview: smoke pe telefon (play, prev/next, offline, lock screen)

Rămâne 4b (separat): redesign audioInstance + canalul de feedback cu tone în STATE_FX — cere re-validare pe device.

🤖 Generated with Claude Code

radioMachine.ts expresses declaratively everything radioCore did with
manual timers and playId invalidation:

- after-delays replace the retry/loading/recovery timers — leaving a
  state cancels them; the recovery backoff is a delay expression over
  context.recoveryCount, with the offline recheck as a reentering
  self-transition at fixed cadence (no escalation);
- playerPlay() is an invoked promise actor — a late resolve/reject
  after leaving the state is discarded by construction (replaces the
  entire playId mechanism);
- the playback watchdog and sound supervisor are invoked callback
  actors scoped exactly to the states that need them;
- STATE_FX stays a declarative table, applied as a parametrized entry
  action (re-entries re-apply it, matching the old setState semantics).

radioCore.ts becomes a thin adapter with the same public API — main.ts
and the other modules are untouched except for two dropped deps
(setTimeout/clearTimeout now live on the actor clock) and a fix the e2e
suite caught that unit mocks could not: browser timer globals throw
"Illegal invocation" when called as methods on deps, so main.ts wraps
them. stateMachine.ts is deleted.

Unit tests ported to a wrapped SimulatedClock (with visibility into
scheduled delays); playId assertions became behavioral ones. 63/63
unit, 37/37 e2e untouched, machine coverage 99%, preview smoke green.
Bundle cost of xstate: 5.7 -> 19.3KB gzip.

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

vercel Bot commented Jul 3, 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 3, 2026 3:23pm

@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown

CI Summary

Check Status Result
Typecheck PASS tsc --noEmit clean
Unit tests PASS 63/63 passed; Coverage: Lines 99.73%, Branches 90.12%, Functions 93.54%, Statements 99.73%
Build PASS Build completed
Playwright browser PASS Chromium installed
E2E tests PASS 37/37 passed

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Refactors the radio player core from manually orchestrated timers/race-avoidance into a declarative XState v5 machine, while keeping the existing radioCore public API and updating unit tests/coverage settings accordingly.

Changes:

  • Introduces src/js/radioMachine.ts implementing the radio player as an XState v5 machine (delays, invoked actors, declarative state side effects).
  • Reworks src/js/radioCore.ts into a thin adapter that translates the existing API into machine events and starts an actor instance.
  • Updates tests and tooling: ports unit tests to use SimulatedClock, adjusts Vitest coverage includes, and wires browser timer deps accordingly.

Reviewed changes

Copilot reviewed 8 out of 9 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
vitest.config.js Updates coverage include list to track radioMachine.ts instead of the removed generic stateMachine.ts.
src/js/stateMachine.ts Removes the previous generic state-machine helper now superseded by XState.
src/js/radioMachine.ts Adds the XState v5 machine defining states, delays, actors (watchdog/supervisor), and declarative side effects.
src/js/radioCore.ts Converts the core into an adapter over an XState actor; removes manual timers/playId orchestration.
src/js/radioCore.test.ts Ports tests to advance XState “after” delays via a SimulatedClock wrapper and updates assertions accordingly.
src/js/main.ts Updates deps wiring for timer functions used by invoked callback actors (interval-based watchdog/supervisor).
plan.md Documents completion notes for phase 4a and implementation decisions.
package.json Adds xstate dependency.
package-lock.json Locks xstate dependency resolution.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/js/radioMachine.ts Outdated
Comment thread src/js/radioMachine.ts Outdated
Run `npm run dev` and open /?inspect — a stately.ai window renders the
live machine diagram with transitions and events in real time. Doubly
gated: import.meta.env.DEV (compile-time dead code in production —
verified zero stately bytes in dist) and an explicit URL param, so the
e2e runs against the dev server never open it.

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

adyz commented Jul 3, 2026

Copy link
Copy Markdown
Owner Author

Adăugat și Stately Inspector (dev-only, opt-in): npm run dev + http://localhost:5173/?inspect → diagrama live a mașinii cu tranzițiile/evenimentele în timp real, perfectă pentru debugging la scenariile offline și pentru designul lui 4b. Dublu păzit (DEV compile-time + param explicit în URL), verificat zero bytes în bundle-ul de producție, e2e neafectat (37/37). Pentru diagrama statică: lipește src/js/radioMachine.ts în https://stately.ai/editor

- Shared domain types (RadioState, RadioDeps, FeedbackSound) and the
  timing constants move to radioMachine.ts — their natural owner — and
  radioCore re-exports them, so every other module keeps its single
  import surface. The dependency is now one-directional
  (adapter -> machine); no more runtime value imports going both ways.
- Remove the unused recentUserPauseIntent guard (its logic already
  lives inline in unexpectedOfflinePause).

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

2 participants