diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-09-16 17:47:00 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-09-16 17:47:00 +0200 |
| commit | c35f3a73eb68cdd7c4179cded3d1ead63ec5a033 (patch) | |
| tree | eabf5919a19646d5410adfe0e184cc608e1a9c48 /packages/meshbay-hub/tests/harness/playlist_store_probe.py | |
| parent | 3d4080e9a1b79e99428a80391e224ea73cf4274d (diff) | |
| download | meshbay-c35f3a73eb68cdd7c4179cded3d1ead63ec5a033.tar.gz | |
playlists: refuse a body no frame can carry, and say which one
A DataChannel send() throws above the max-message-size the node advertises
(aiortc: 65536), so the 1 MB body cap was unreachable and each body push
swallowed the difference in a bare catch. Cap at 62 KB after sealing, name
the playlist in the sync result, keep syncing the rest.
Measured: 1000 tracks seal to 53 276 bytes, so the ceiling is ~1200.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-hub/tests/harness/playlist_store_probe.py')
| -rwxr-xr-x | packages/meshbay-hub/tests/harness/playlist_store_probe.py | 61 |
1 files changed, 61 insertions, 0 deletions
diff --git a/packages/meshbay-hub/tests/harness/playlist_store_probe.py b/packages/meshbay-hub/tests/harness/playlist_store_probe.py index f7573b7..83c00ad 100755 --- a/packages/meshbay-hub/tests/harness/playlist_store_probe.py +++ b/packages/meshbay-hub/tests/harness/playlist_store_probe.py @@ -362,6 +362,67 @@ function fakeNode() { names: readBack ? Object.values(readBack.playlists).map((p) => p.name).sort() : [], }); + // ── what a playlist costs, sealed ───────────────────────────────────── + // + // The cap below is in bytes, but the only number a reader can act on is a + // number of tracks — so it is measured here rather than quoted from the + // design doc. (Quoting it is how "225 tracks" got written down: §4's + // figure is ~270 bytes a track *raw* and ~60 *sealed*, and the raw one was + // read for the sealed one.) + // + // Varied metadata on purpose. A fixture where every track shares an artist + // and an album measures deflate's opinion of its own regularity: the first + // attempt at this, 400 tracks differing only by id, sealed to 41 bytes a + // track and would have let the case below pass while testing nothing. + const WORDS = ['aube', 'ciel', 'verre', 'nord', 'ombre', 'pluie', 'fer', + 'sel', 'onze', 'rive', 'brume', 'cendre', 'axe', 'lune']; + const hex = (n) => [...crypto.getRandomValues(new Uint8Array(n))] + .map((b) => b.toString(16).padStart(2, '0')).join(''); + const word = () => WORDS[Math.floor(Math.random() * WORDS.length)]; + const storedTrack = (i) => { + const artist = `${word()} ${word()}`; + const album = `${word()} ${word()} ${1970 + (i % 50)}`; + const title = `${word()} ${word()} ${word()}`; + 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 sizes = {}; + for (const n of [100, 500, 1000]) { + const tracks = []; + for (let i = 0; i < n; i++) tracks.push(storedTrack(i)); + const sealed = await seal({ v: 1, id: 'measure', rev: 1, device: 'aaa', tracks }, + bodyKind('measure'), USER, key); + sizes[n] = sealed.length; + } + steps.push({ step: 'what a playlist costs sealed', sizes }); + + // ── a playlist too large for one frame is named, not swallowed ──────── + // + // A DataChannel `send()` throws above the max-message-size the far end + // advertised, which for the node's aiortc is 65536 — so the node's 1 MB + // body cap is unreachable and the real limit is a count of tracks + // (docs/playlists.md §15.3). The push used to swallow every per-body + // failure in one bare `catch {}`: the playlist stopped leaving the browser + // and nothing anywhere said so, which is the silent loss this design + // exists to prevent. + const bigNode = fakeNode(); + const longId = await P.createPlaylist(USER, 'Trop longue'); + const many = []; + for (let i = 0; i < 1500; i++) { + const st = storedTrack(i); + many.push({ ...track(i), id: st.id, name: st.n, display_title: st.t, + path: st.p, artist: st.a, album: st.b, duration: st.d }); + } + await P.addTracks(USER, longId, many, 'g1'); + const bigResult = await P.syncWith(bigNode, USER); + steps.push({ step: 'a playlist too large for one frame', + result: bigResult, + bigKind: bodyKind(longId), + // Everything else must still have gone: one oversized + // playlist is not a broken sync. + stored: bigNode.stored.map((e) => ({ kind: e.kind, bytes: e.bytes })) }); + // ── a session with no HKDF handle degrades rather than failing ───────── P.setPlaylistTransport(null); P.forgetPlaylistKey(); |