Tune.Spotify.Session.HTTP (tune v0.1.0)
This module implements a state machine mapped to a user session, wrapping interaction with the Spotify API.
General structure
The state machine implements the Tune.Spotify.Session
behaviour for its public API
and uses GenStateMachine
to model its lifecycle.
If you're not familiar with the gen_statem
behaviour (which powers
GenStateMachine
), it's beneficial to read
http://erlang.org/doc/design_principles/statem.html before proceeding
further.
The state machine uses the handle_event_function
callback mode and has 3
states: :not_authenticated
, :authenticated
and :expired
.
┌─────────────────┐
│Not authenticated│
└─────────────────┘
│
│
▼
┌ ─ ─ ─ ─ ─ ─ ─ ─ ─
┌──────── Authenticate │─────────┐
│ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ │
│ │ │
Success Token Invalid
│ expired token
│ │ │
│ │ │
▼ ▼ ▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│Authenticated│ │ Expired │ │ Stop │
└─────────────┘ └─────────────┘ └─────────────┘
▲ │ ▲
│ Get new │
Success token │
│ ┌ ─ ─ ─ ─ │ │
└──── Refresh │◀───┘ │
└ ─ ─ ─ ─ │
│ │
│ Invalid │
└──────────refresh ─────────┘
token
When the process starts, it tries to authenticate against the Spotify API using the provided credentials. If successful, it enters the authenticated state, where all API functions can be executed correctly.
If authentication fails because the authentication token has expired, the process tries to get a new token using the refresh token supplied by the Spotify API. This process effectively extends the duration of the session.
Any error that indicates that credentials are invalid causes the process to stop. Any transient network error automatically triggers a delayed retry, which guarantees that eventually the state machine reaches the authenticated state.
Data lifecycle
Aside from acting as an API client for on-demand operations (e.g. search, play/pause, etc.), the state machine also regularly polls the Spotify API for current player status and connected devices. Both pieces of information are kept in the state machine data for fast read and corresponding events are broadcasted when they change.
Automatic data fetch is performed after successful authentication (via an
internal
event) and then scheduled via a state_timeout
event. Once
handled, the scheduled event requeues itself via the same state_timeout
events.
Usage of state_timeout
events complies with the general state machine: if
at any point the machine enters the expired state, any queued state_timeout
event is automatically expired.
Automatic fetching will resume once the machine enters the authenticated state.
Subscriptions
Multiple processes are able to subscribe to the events keyed by the session id.
Broadcast and subscribe are implemented via Phoenix.PubSub
, however the
state machine maintains its own set of monitored processes subscribed to the session
id.
Subscription tracking is necessary to implementing automatic termination of a state machine after a period of inactivity. Without that, the state machine would indefinitely poll the Spotify API, even when no client is interested into the topic, until a crash error or a node reboot.
Every 30 seconds, the state machine fires a named timeout
event, checking if
there's any subscribed process. If not, it terminates. Subscribed processes
are monitored, so when they terminate, their exit is handled by the state machine,
which removes them from its data.
Usage of named timeout
events is necessary, as they're guaranteed to fire
irrespectively of state changes.
Link to this section Summary
Functions
Returns a specification to start this module under a supervisor.
Link to this section Types
start_opts()
Specs
start_opts() :: [{:timeouts, timeouts()}]
timeouts()
Specs
Link to this section Functions
child_spec(init_arg)
Returns a specification to start this module under a supervisor.
See Supervisor
.
start_link(arg)
Specs
start_link({Tune.Spotify.Session.id(), Tune.Spotify.Session.credentials()}) :: {:ok, pid()} | {:error, term()}
start_link( {Tune.Spotify.Session.id(), Tune.Spotify.Session.credentials(), start_opts()} ) :: {:ok, pid()} | {:error, term()}
start_link(session_id, credentials, start_opts)
Specs
start_link( Tune.Spotify.Session.id(), Tune.Spotify.Session.credentials(), start_opts() ) :: {:ok, pid()} | {:error, term()}