Skip to content

v0.6.0 — A clock that only goes forward, and a cache that stops

Choose a tag to compare

@SAKMZ SAKMZ released this 01 Aug 07:17

Most of this release came out of one review comment on r/coolgithubprojects. Two of the four points raised were already handled; two were real, and both are fixed here.

⏱️ Playback no longer trusts the wall clock

Every client renders its audio position from the server's timestamps, so a listener whose laptop clock is twenty seconds off still hears the track in the right place. That part was fine. The problem was one level down: the server's own epoch was Date.now().

Wall clock time is not monotonic. An NTP correction, a VM resuming from suspend, a container host stepping its clock — any of them move Date.now() sideways, and because every position in the room is measured from that epoch, the whole room jumps together mid-track.

The playback clock is now anchored to performance.now(), which only ever moves forward at one second per second:

const WALL_AT_BOOT = Date.now();
const MONO_AT_BOOT = performance.now();
const monoNow = () => WALL_AT_BOOT + (performance.now() - MONO_AT_BOOT);

Seven timing sites moved onto it: the serverNow in both state broadcasts, the start epoch for repeat-one and for advancing the queue, the pause and seek arithmetic, and the sync handshake. Room creation timestamps and chat timestamps stay on the wall clock deliberately — those are dates, and a date that drifts from the world is the wrong answer.

Measured on the live instance afterwards: two clients in the same room, sampled 10.681s apart in wall time, were 10.681s apart in track position. No correction jumps, position advancing at exactly 1.0×.

💾 The cache has a ceiling now, and you can see it

Tracks were dropped 72 hours after their last play. Age is not a bound. A busy room pulls far more inside that window than a small disk holds, and Syncwave is usually running on a machine somebody also lives on — filling their disk is a worse failure than re-downloading a track.

CACHE_MAX_MB caps it, defaulting to 4096. The sweep runs at boot and hourly: the age pass first, then, if the total is still over, least-recently-played files go until it's back under. 0 disables the cap and restores the old behaviour.

The Storage panel in /admin shows what it's actually costing you — usage against the limit, track count, and buttons to sweep or clear. Clearing is safe: rooms and queues are untouched, tracks just download again next time someone plays them.

"Why is my disk full" is a bad way to discover a listening room has been busy.

📱 The room fits a phone properly

  • Reactions are one tap. They were behind a popover — tap to open, tap to send. Now the six emoji sit directly above the player controls, and one tap sends a burst of floating emoji rather than a single one.
  • The listener list no longer flows behind the player. With more than one name it ran under the bottom controls; with one name it still did, after the first attempt at fixing it. The actual cause was a flex child without min-h-0, so it never shrank to its container.
  • On phones the panel doesn't list listeners at all. The top bar already shows them, and showing the same thing twice cost the artwork the room it needed.
  • The panel is centred and no longer double-counts. It was carrying 122px of dead space and two identical 👥 N counters.

💜 A note at the bottom

The landing page and the join screen now say what this was actually built for. Improvise your own; it's one component.

🔍 Findable

README and repo description now say self-hosted Spotify Jam alternative in the words people search for, rather than leaving it to be inferred.

🐛 The Storage panel shipped broken, briefly

Worth writing down because it was invisible. .gitignore had cache/ with no leading slash, which matches a directory called cache at any depth — including app/api/admin/cache/. The route behind the Storage panel was never committed, git status showed clean because ignored files don't appear in it, and every build from a clone got a panel stuck on "Checking disk usage…" forever.

cache/, data/, out/ and build/ are now anchored to the repo root. If you cloned between the two commits, pull again.


Upgrading: git pull && docker compose up -d --build.

If your cache is already over 4 GB, the first sweep after restart will trim it back to the cap, oldest first. Set CACHE_MAX_MB higher, or 0, if you'd rather it didn't.