Hello,
Over the last couple of months I started writing a browser-based Spotify interface that fits the way I use the platform.
The UI is written in LiveView with a sprinkle of custom Javascript for specific widgets, while CSS is completely custom on top of the default milligram.css included with a generated Phoenix application.
I’m sharing it here to gather some feedback around structure and patterns.
LiveView structure
So far I have one main view (the explorer) which mounts a component (the bottom mini player) which mounts another component (the song progress bar). This hierarchy is the result of experimenting with the aim to minimize the amount of data sent over the network.
ExplorerLive
|_ MiniPlayerComponent
|_ ProgressBarComponent
Specifically: the “currently playing status” is updated every second according to these rules:
- A song change concerns the entire UI, as the currently playing song is highlighted in a few different places (e.g. if you’re browsing the album that contains the song).
- Playing, pausing, volume or device changes should update the entire miniplayer
- Progress in the same song only updates the progress bar
Implementation-wise, this translates to:
- and 2. Update relevant assigns in ExplorerLive and re-render the entire central portion of the UI and miniplayer
- Send updates to ProgressBarComponent and re-render the progress bar
Components are stateful, but not isolated process-wise.
Things I’m unsure about:
- For case 3. the live view directly addresses the progress bar, bypassing the mini player component. This works, but I do wonder if it’s a case of implementation leak.
- All event handling is in the live view. Again, seems like an implementation leak.
- I’m finding directly managing assigns error-prone: I’d like to use less keys and have specialized functions to update the assigns state, but that makes it more difficult to control diffing during updates (i.e. more UI is re-rendered).
Session state machine
I’ve implemented the main connected user session as a separate state machine.
The implementation takes care of managing initial authentication, refreshing credentials, periodically polling new information and auto-termination in case a user has disconnected.
It’s implemented outside the live view session so that a user can have multiple browser windows open with only one session (and only one polling lifecycle).
I’m fairly happy about the structure so far - once you know the rules behind events in gen_statem
, it looks reasonable to me but I’d like a second opinion. The code documentation has a diagram/description on how it’s implemented.
Here’s where I’m doubtful (lifted from the module docs)
Broadcast and subscribe are implemented via
Phoenix.PubSub
, however the
Worker maintains its own set of monitored processes subscribed to the session
id.
Subscription tracking is necessary to implementing automatic termination of a
worker after a period of inactivity. Without that, the worker would
indefinitely poll the Spotify API, even when no client is interested into the
topic, until a crash error or a node reboot.
In practice, a live view session subscribes to the worker process. The worker monitors the live view session, so that it can decide if it’s safe to terminate. Two browser windows for the same user equal two monitors.
Having a custom monitoring logic makes it that Phoenix.PubSub
is almost redundant. I could message those processes directly, plus in a distributed scenario I would still need to update the state machine work correctly.
Testing
I’m experimenting with tests seeded with stream_data and expressed as properties. This suits well specific sections of the UI, e.g. search, where having randomized search results already helped me find issues I hadn’t thought about.
Properties, however, slow down (non-linearly) when I increase the number of runs. I still have to investigate and see where the problem is (e.g. shrinking gets slower or interaction with Mox is bottlenecked) - I’m wondering if by looking at the tests structure anyone with more experience in combining stream_data and Mox can shed some light. If not, I’ll dig and report back what I find.
General information
- You can use the application at tune.fullyforged.com (with no uptime guarantees, but it’s my production instance).
- Source at GitHub - fully-forged/tune: A streamlined Spotify client and browser with a focus on performance and integrations.
- Code documentation at https://tune-docs.fullyforged.com
Thanks for reading so far!