summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-09-16 17:51:19 +0200
committerChristophe Besson <cbesson@gmail.com>2026-09-16 17:51:19 +0200
commit929bfcccf8928a30d86d48a8b0137db0dd4a1ee0 (patch)
treea22966059d22cccd60238752406f98a58304ec16
parentc35f3a73eb68cdd7c4179cded3d1ead63ec5a033 (diff)
downloadmeshbay-929bfcccf8928a30d86d48a8b0137db0dd4a1ee0.tar.gz
playlists: promise the count that holds when nothing compresses
The ceiling is 1200 tracks on a library that repeats and 660 on one that never does; the message said 1000, which would send a reader back into the wall at 660. It says 500, measured at both ends. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
-rw-r--r--docs/playlists.md27
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/playlist-menu.js2
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/playlists.js11
-rwxr-xr-xpackages/meshbay-hub/tests/harness/playlist_store_probe.py26
-rw-r--r--packages/meshbay-hub/tests/test_playlist_store.py31
5 files changed, 78 insertions, 19 deletions
diff --git a/docs/playlists.md b/docs/playlists.md
index 45e36fe..232eae5 100644
--- a/docs/playlists.md
+++ b/docs/playlists.md
@@ -1229,14 +1229,25 @@ reads §15.1 as more than it is.
`UPLOAD_CHUNK_SIZE` is 48 KB going up while `CHUNK_SIZE` is 1 MB coming down,
which is the same constraint seen from both ends.
- So `USER_BLOB_BODY_MAX = 1 MB` is a cap the client can never reach. Measured
- rather than derived: a thousand tracks with varied metadata seal to **53 276
- bytes**, and each further track costs about 50, so one playlist tops out near
- **1200 tracks**. (An earlier draft of this paragraph said 225, by reading §4's
- ~270 bytes a track *raw* as the *sealed* figure. `test_playlist_store.py`
- measures it now, and fails if a thousand tracks stop fitting.) Reading is
- unaffected — a node answers with a whole body, and 256 KB is measured coming
- back.
+ So `USER_BLOB_BODY_MAX = 1 MB` is a cap the client can never reach. How many
+ tracks 62 KB is depends entirely on how much the metadata repeats, because
+ repetition is all deflate has to work with — so it was measured at both ends
+ rather than derived:
+
+ | a library where… | per track | one playlist tops out at |
+ |---|---|---|
+ | artists, albums and folders recur | ~50 B | ~1200 tracks |
+ | nothing repeats at all | ~91 B | ~660 tracks |
+
+ Real libraries sit near the first row — an album contributes a dozen tracks
+ sharing three fields — but the sentence shown to a reader has to hold in the
+ second, so it says **500**. (Two earlier drafts of this paragraph were wrong
+ in both directions: 225, from reading §4's ~270 bytes a track *raw* as the
+ *sealed* figure; then 1200, from a fixture with fourteen words of vocabulary,
+ which measured deflate's opinion of its own regularity. Both numbers are now
+ measured by `test_playlist_store.py`, which fails if either stops holding.)
+ Reading is unaffected — a node answers with a whole body, and 256 KB is
+ measured coming back.
**Taken:** the client caps itself at 62 KB of sealed body, checked after
sealing because compression makes a track count a poor guide to a blob length.
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/playlist-menu.js b/packages/meshbay-hub/src/meshbay_hub/static/playlist-menu.js
index 70c105d..64d1c62 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/playlist-menu.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/playlist-menu.js
@@ -203,7 +203,7 @@ function PlaylistMenuButton({ userId, lists, reload, onPlayQueue, onSync, cached
// the lie this whole report exists to stop.
const big = (r && r.tooLarge) || [];
say(big.length
- ? t('playlists.err_too_large', { name: big[0], n: 1000 })
+ ? t('playlists.err_too_large', { name: big[0], n: 500 })
: (r && r.ok
? `${t('playlists.synced')} (${r.pushed || 0}↑ ${r.pulled || 0}↓)`
: (why === 'no_key'
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/playlists.js b/packages/meshbay-hub/src/meshbay_hub/static/playlists.js
index af359f3..bac9b3d 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/playlists.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/playlists.js
@@ -41,9 +41,14 @@ import {
*
* 62 KB leaves room for the frame around the bytes — the four-byte length
* prefix and the msgpack map that carries the kind, the revision and a request
- * id. Measured, a thousand tracks seal to 53 276 bytes and each further track
- * costs about 50, so the ceiling is near **1200 tracks in one playlist**
- * (`test_playlist_store.py`, which fails if that stops being true).
+ * id.
+ *
+ * How many tracks that is depends entirely on how much the metadata repeats,
+ * because repetition is all deflate has to work with. Measured both ways: a
+ * library where artists and albums recur costs ~50 bytes a track and tops out
+ * near **1200**; one with no repetition at all costs ~91 and tops out near
+ * **660**. The sentence shown to a reader says 500, which holds in both worlds
+ * — `test_playlist_store.py` fails if it stops holding in the harsher one.
*
* Checked *after* sealing rather than estimated before it: compression makes
* the length of a playlist a poor guide to the length of its blob, and a guess
diff --git a/packages/meshbay-hub/tests/harness/playlist_store_probe.py b/packages/meshbay-hub/tests/harness/playlist_store_probe.py
index 83c00ad..6421426 100755
--- a/packages/meshbay-hub/tests/harness/playlist_store_probe.py
+++ b/packages/meshbay-hub/tests/harness/playlist_store_probe.py
@@ -395,7 +395,31 @@ function fakeNode() {
bodyKind('measure'), USER, key);
sizes[n] = sealed.length;
}
- steps.push({ step: 'what a playlist costs sealed', sizes });
+
+ // The same, with nothing to compress. Fourteen words of vocabulary is a
+ // library that repeats itself; a real one does not, and deflate's whole
+ // saving here comes from repetition. This is the pessimistic bound, and the
+ // number a reader is told has to sit under *it*, not under the friendly one
+ // — the two differ by enough to matter.
+ const noise = (n) => [...crypto.getRandomValues(new Uint8Array(n))]
+ .map((b) => 'abcdefghijklmnopqrstuvwxyz '[b % 27]).join('');
+ const diverseTrack = (i) => {
+ const artist = noise(18);
+ const album = noise(22);
+ const title = noise(26);
+ return { id: hex(16), g: 'g1', hv: 1, n: `${i % 20} - ${title}.flac`,
+ s: 30000000 + i, p: `${artist}/${album}`, t: title, a: artist,
+ b: album, d: 180 + (i % 300), tn: (i % 20) + 1 };
+ };
+ const diverse = {};
+ for (const n of [100, 500, 1000]) {
+ const tracks = [];
+ for (let i = 0; i < n; i++) tracks.push(diverseTrack(i));
+ const sealed = await seal({ v: 1, id: 'measure', rev: 1, device: 'aaa', tracks },
+ bodyKind('measure'), USER, key);
+ diverse[n] = sealed.length;
+ }
+ steps.push({ step: 'what a playlist costs sealed', sizes, diverse });
// ── a playlist too large for one frame is named, not swallowed ────────
//
diff --git a/packages/meshbay-hub/tests/test_playlist_store.py b/packages/meshbay-hub/tests/test_playlist_store.py
index ce094c4..701de41 100644
--- a/packages/meshbay-hub/tests/test_playlist_store.py
+++ b/packages/meshbay-hub/tests/test_playlist_store.py
@@ -286,17 +286,36 @@ def test_what_a_playlist_costs_sealed_is_measured_not_quoted(steps):
"""The ceiling the UI promises comes from here, not from the design doc.
§4 measured ~270 bytes a track *raw* and ~60 *sealed*; the raw figure was
- then read for the sealed one and "about 225 tracks" written down, which is
- five times too strict. So the number is measured where it is used.
+ read for the sealed one and "about 225 tracks" written down, which is five
+ times too strict. So it is measured where it is used.
"""
sizes = steps["what a playlist costs sealed"]["sizes"]
per_track = (sizes["1000"] - sizes["100"]) / 900
assert 30 < per_track < 90, f"a track now costs {per_track:.0f} sealed bytes"
- # The sentence shown to a reader is "playlists of about 1000 tracks". If
- # compression regresses, or a field is added to what is stored, that
- # sentence becomes false — and this is what says so.
assert sizes["1000"] < 62 * 1024, (
- f"a thousand tracks no longer fit in one frame: {sizes['1000']} bytes")
+ f"a thousand ordinary tracks no longer fit in one frame: {sizes['1000']}")
+
+
+def test_the_promised_count_holds_for_a_library_that_never_repeats(steps):
+ """The number in the message has to survive the worst compression, not the
+ friendly one.
+
+ Everything deflate saves here comes from repetition — the same artist, the
+ same album, the same folder, over and over. A library that never repeats
+ costs nearly twice as much a track, and the two ceilings are 1200 and 660.
+ A reader told to split at 1000 would hit the wall again at 660; the message
+ says 500, and this is what keeps it true.
+ """
+ diverse = steps["what a playlist costs sealed"]["diverse"]
+ per_track = (diverse["1000"] - diverse["100"]) / 900
+ assert per_track > (
+ (steps["what a playlist costs sealed"]["sizes"]["1000"]
+ - steps["what a playlist costs sealed"]["sizes"]["100"]) / 900), (
+ "the harsh fixture compresses as well as the ordinary one — "
+ "it is not measuring the worst case any more")
+ assert diverse["500"] < 62 * 1024, (
+ f"500 tracks of wholly unrepeating metadata no longer fit: "
+ f"{diverse['500']} bytes, at {per_track:.0f} a track")
def test_a_playlist_too_large_for_a_frame_is_named(steps):