Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,9 @@ node --version # should be v20.19.0 or newer

```bash
npm run dev # vite dev server (include tailwind)
# + http://localhost:5173/?inspect → Stately Inspector:
# diagrama LIVE a masinii de stari, cu tranzitii/events
# in timp real (doar dev; zero bytes in productie)
npm test # unit tests (vitest)
npm run test:coverage # unit coverage + raport HTML in coverage/index.html
npm run test:e2e # e2e tests (playwright, pornește singur serverul)
Expand Down
146 changes: 145 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,12 @@
"license": "ISC",
"description": "",
"dependencies": {
"tailwindcss": "^4.0.14"
"tailwindcss": "^4.0.14",
"xstate": "^5.32.4"
},
"devDependencies": {
"@playwright/test": "^1.58.2",
"@statelyai/inspect": "^0.7.2",
"@tailwindcss/vite": "^4.3.2",
"@vitest/coverage-v8": "^3.2.4",
"typescript": "^6.0.3",
Expand Down
30 changes: 30 additions & 0 deletions plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,36 @@ manual pe MediaSession (iOS/macOS daca se poate — e zona cea mai fragila).
Obiectiv: tranzitii declarative si timere/race-uri rezolvate din constructie,
cu paritate 100% de comportament.

### 4a: masina + adaptorul [gata — branch `faza4-xstate`]

Implementat conform planului de mai jos. Note:
- `applyFx` e entry action parametrizata per stare (nu subscribe) — reenter
(RESUME_FAILED, recheck offline) re-aplica fx exact ca vechiul setState.
- `beginErrorCycle` (increment recoveryCount + clear offlineRecheck) ruleaza
pe TRANZITIILE spre error, nu pe entry — garanteaza ca RECOVERY_DELAY vede
contextul incrementat. Recheck-ul offline e self-transition cu reenter +
`offlineRecheck: true` (cadenta fixa, fara escaladare).
- pauseRadio/resumePlayer/toggle/onPlayButtonClick raman in adaptor (citesc
playerIsPaused/getState live, ca inainte); restul devine events.
- deps.setTimeout/clearTimeout au DISPARUT din contract (after-delays traiesc
in clock-ul actorului); testele injecteaza SimulatedClock impachetat cu
vizibilitate pe delay-urile programate (clock.hasScheduled).
- CAPCANA gasita de e2e (nu de unit): browserul arunca "Illegal invocation"
cand masina apeleaza deps.setInterval ca metoda — global timer functions
cer this=window; fix: wrapper arrow in main.ts. Unit testele n-o puteau
prinde (mock-uri); inca un argument pentru e2e-ul neatins ca dovada.
- Cost bundle: 16KB -> 60KB raw (5.7 -> 19.3KB gzip) — pretul xstate.
- Verificat: typecheck curat, 63/63 unit portate (SimulatedClock, playId ->
asertiuni comportamentale), 37/37 e2e NEATINSE, coverage 99% pe masina,
smoke complet pe vite preview.

### 4b: redesign audioInstance + canal de feedback [de facut]

Ramane separat (vezi sectiunea de mai jos "Redesign audioInstance") — cere
re-validare pe device (lock screen, prev/next, offline).

### Planul initial (referinta)

- Instalam `xstate` (fara `@xstate/react`).
- `src/js/radioMachine.ts` cu `setup()`:
- Stari: `idle`, `loading`, `playing`, `paused`, `retrying`, `error`,
Expand Down
18 changes: 14 additions & 4 deletions src/js/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,16 @@ const showButton = (which: 'play' | 'pause' | 'stop') => {

// --- Core ---

// Dev-only: live machine diagram via the Stately Inspector.
// Run `npm run dev` and open http://localhost:5173/?inspect — a stately.ai
// window shows the running machine with transitions/events in real time.
// Compile-time dead code in production (import.meta.env.DEV is false), and
// opt-in via the URL param so the e2e runs (dev server!) never open it.
const inspector =
import.meta.env.DEV && new URLSearchParams(window.location.search).has('inspect')
? (await import('@statelyai/inspect')).createBrowserInspector()
: undefined;

const core = createRadioCore({
getStationUrl: (i) => radioSelect.options[i].value,
getStationCount: () => radioSelect.options.length,
Expand All @@ -109,13 +119,13 @@ const core = createRadioCore({
maybeReloadForPendingServiceWorkerUpdate(s);
},
saveLastIndex,
setTimeout,
clearTimeout: (id) => clearTimeout(id ?? undefined),
setInterval,
// Wrapped: the machine calls these as methods on deps, and browser timer
// functions throw "Illegal invocation" when invoked with a foreign `this`.
setInterval: (fn, ms) => setInterval(fn, ms),
clearInterval: (id) => clearInterval(id ?? undefined),
performanceNow: () => performance.now(),
isOnline: () => navigator.onLine,
});
}, inspector ? { inspect: inspector.inspect } : {});
connectMediaSessionCore(core);
focusInitialPlaybackControl();

Expand Down
Loading