@@ -4,6 +4,50 @@ All notable changes to PlayolaPlayer are documented here. This project follows
44[ Semantic Versioning] ( https://semver.org/ ) . Versions correspond to git tags,
55which Swift Package Manager consumers pin to.
66
7+ ## 0.19.0
8+
9+ ### Added
10+
11+ - ** Bounded retry/backoff on the initial schedule fetch.** ` play(stationId:) `
12+ now retries a failed initial ` GET /v1/stations/{id}/schedule ` up to 3 times
13+ with exponential backoff (0.5s / 1s / 2s) before giving up, matching the
14+ recovery behavior the player already used for spin loading and the ongoing
15+ schedule poll. Only transient failures are retried — server ` 5xx ` responses
16+ and an explicit allow-list of connectivity ` URLError ` s (timeouts, host/DNS,
17+ connection-lost, etc.). Permanent failures (` 404 ` , decode errors, empty
18+ schedules, and non-connectivity ` URLError ` s such as ` .badURL ` ) still fail
19+ fast. This means a transient backend outage no longer instantly fails a
20+ station start with no automatic recovery.
21+
22+ ### Changed
23+
24+ - ** ` PlayolaStationPlayer.State ` gained a terminal ` .error(StationPlayerError) `
25+ case.** A failed ` play(...) ` now emits ` .loading(0) ` when the attempt begins
26+ and ` .error(_) ` when it fails terminally (instead of throwing without ever
27+ changing ` state ` , which could leave consumers stuck on a loading spinner with
28+ no signal). ` play(...) ` still ` throws ` as before — the new state is emitted
29+ * in addition to* the thrown error. Cancellation does not produce an ` .error `
30+ state.
31+
32+ ** Source-breaking for consumers** that ` switch ` exhaustively over ` State ` :
33+ add a ` case .error ` (or ` default ` ) arm. A typical handler treats ` .error ` as
34+ a recoverable, retry-able state (show the message, let the user tap play
35+ again). ` StationPlayerError ` is now ` Sendable ` .
36+
37+ - ** State transitions are now supersession-safe (last ` play() ` wins).** Each
38+ ` play(...) ` /` stop() ` takes a new internal generation; work from a prior
39+ attempt (its scheduling loop, or a spin whose audio starts later) can no
40+ longer publish state into a newer attempt. This closes a race where a
41+ lingering session could overwrite a freshly-emitted ` .error ` (or ` .idle ` )
42+ with ` .playing ` . Behavioral note: starting a new ` play() ` supersedes the
43+ previous session immediately, so a * failed* station switch lands on ` .error `
44+ rather than rolling back to the previously-playing station.
45+
46+ - ** ` playNow(from:to:) ` and ` schedulePlay(at:) ` are now ` async ` .** Both ` public `
47+ methods changed from synchronous to ` async ` so their audio work can run off
48+ the main thread. ** Source-breaking for direct callers** : add ` await ` at the
49+ call site (callers must already be in an ` async ` context, e.g. a ` Task ` ).
50+
751## 0.18.0
852
953### Added
0 commit comments