v0.6.0 — A clock that only goes forward, and a cache that stops
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
👥 Ncounters.
💜 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.