diff options
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/static/playlists.js')
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/static/playlists.js | 54 |
1 files changed, 50 insertions, 4 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/playlists.js b/packages/meshbay-hub/src/meshbay_hub/static/playlists.js index 5aa501a..af359f3 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/playlists.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/playlists.js @@ -30,6 +30,28 @@ import { * what §7 refuses. */ +/** + * The largest sealed body this browser can actually send. + * + * Not a policy — a wire limit. A DataChannel `send()` throws when the frame is + * larger than the value the far end advertised, and the node's aiortc fixes + * that at 65 536 (`docs/playlists.md` §15.3). The node's own 1 MB body cap is + * therefore unreachable from here, and the same constraint is why uploads chunk + * at 48 KB. + * + * 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). + * + * 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 + * that is wrong in the permissive direction is a thrown exception in the middle + * of a sync. + */ +const MAX_BODY_BYTES = 62 * 1024; + // ── local identity ─────────────────────────────────────────────────────────── const DEVICE_KEY = 'meshbay_playlist_device'; @@ -535,7 +557,12 @@ async function saveQueueAsPlaylist(userId, name, entries) { * be reconciled must never break whatever that connection was opened for. */ async function syncWith(transport, userId) { - const result = { ok: false, pulled: 0, pushed: 0, reason: null }; + const result = { + ok: false, pulled: 0, pushed: 0, reason: null, + // Named, not counted: "one playlist is too large" is only actionable if + // the reader is told which one. + tooLarge: [], failed: [], + }; if (!transport || !transport.connected) { result.reason = 'offline'; return result; @@ -634,11 +661,30 @@ async function syncWith(transport, userId) { result.unreadable = true; } } else if (localRev > nodeRev && localRev > 0) { + // Was: one `catch {}` covering both of the cases below. A node that went + // away mid-sweep and a playlist that can never be sent are not the same + // event, and swallowing the second is the silent loss this whole design + // exists to prevent — the reader keeps adding tracks to a playlist that + // stopped leaving the browser, and nothing anywhere says so. + let sealed; try { - await transport.storeUserBlob( - kind, localRev, await seal(local, kind, userId, key)); + sealed = await seal(local, kind, userId, key); + } catch { + result.failed.push(p.name); + continue; + } + if (sealed.byteLength > MAX_BODY_BYTES) { + result.tooLarge.push(p.name); + continue; + } + try { + await transport.storeUserBlob(kind, localRev, sealed); result.pushed += 1; - } catch { /* a cap, or a node that went away mid-sweep */ } + } catch { + // A node that went away mid-sweep: the next sync pushes this, because + // the node's revision is still behind the local one. + result.failed.push(p.name); + } } } |