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
                Success             Token             Invalid
                                  expired             token
             Authenticated       Expired           Stop     
                                  Get new                
                Success             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.


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


Returns a specification to start this module under a supervisor.

Link to this section Types

Link to this type



start_opts() :: [{:timeouts, timeouts()}]


timeouts() :: %{refresh: timeout(), retry: timeout(), inactivity: timeout()}

Link to this section Functions

Link to this function


Returns a specification to start this module under a supervisor.

See Supervisor.

Link to this function



start_link({Tune.Spotify.Session.id(), Tune.Spotify.Session.credentials()}) ::
  {:ok, pid()} | {:error, term()}
  {Tune.Spotify.Session.id(), Tune.Spotify.Session.credentials(), start_opts()}
) :: {:ok, pid()} | {:error, term()}
Link to this function

start_link(session_id, credentials, start_opts)


) :: {:ok, pid()} | {:error, term()}