summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-09-16 17:47:00 +0200
committerChristophe Besson <cbesson@gmail.com>2026-09-16 17:47:00 +0200
commitc35f3a73eb68cdd7c4179cded3d1ead63ec5a033 (patch)
treeeabf5919a19646d5410adfe0e184cc608e1a9c48 /packages/meshbay-hub/src
parent3d4080e9a1b79e99428a80391e224ea73cf4274d (diff)
downloadmeshbay-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/src')
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/de.js1
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/en.js1
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/es.js1
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js1
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/it.js1
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js1
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js1
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js1
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js1
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js1
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/playlist-menu.js17
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/playlists.js54
12 files changed, 72 insertions, 9 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js
index 4cfa0a7..cb92b18 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js
@@ -336,6 +336,7 @@ export default {
'playlists.synced': 'Playlists synchronisiert',
'playlists.sync_failed': 'Synchronisierung fehlgeschlagen',
'playlists.err_no_key': 'Melde dich einmal ab und mit deinem Passwort wieder an, damit Playlists zwischen Geräten synchronisiert werden.',
+ 'playlists.err_too_large': '„{name}“ ist zu groß, um mit deinen anderen Geräten synchronisiert zu werden. Teile sie in Playlists von etwa {n} Titeln auf.',
'playlists.track_removed': 'Titel entfernt',
'playlists.save_queue': 'Warteschlange als Playlist speichern…',
'playlists.cancel': 'Abbrechen',
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js
index 1616eb5..3b84c3c 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js
@@ -334,6 +334,7 @@ export default {
'playlists.synced': 'Playlists synced',
'playlists.sync_failed': 'Could not sync playlists',
'playlists.err_no_key': 'Sign out and sign in with your password once to sync playlists across devices.',
+ 'playlists.err_too_large': '"{name}" is too large to sync to your other devices. Split it into playlists of about {n} tracks.',
'playlists.track_removed': 'Track removed',
'playlists.save_queue': 'Save the queue as a playlist…',
'playlists.cancel': 'Cancel',
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js
index ff74857..145f464 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js
@@ -334,6 +334,7 @@ export default {
'playlists.synced': 'Listas sincronizadas',
'playlists.sync_failed': 'No se pudieron sincronizar',
'playlists.err_no_key': 'Cierra sesión y vuelve a entrar con tu contraseña una vez para sincronizar las listas entre dispositivos.',
+ 'playlists.err_too_large': '«{name}» es demasiado grande para sincronizarse con tus otros dispositivos. Divídela en listas de unas {n} canciones.',
'playlists.track_removed': 'Pista quitada',
'playlists.save_queue': 'Guardar la cola como lista…',
'playlists.cancel': 'Cancelar',
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js
index 52af974..a4f32de 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js
@@ -335,6 +335,7 @@ export default {
'playlists.synced': 'Playlists synchronisées',
'playlists.sync_failed': 'Synchronisation impossible',
'playlists.err_no_key': 'Déconnecte-toi puis reconnecte-toi avec ton mot de passe une fois, pour synchroniser les playlists entre appareils.',
+ 'playlists.err_too_large': "« {name} » est trop volumineuse pour être synchronisée vers tes autres appareils. Divise-la en playlists d'environ {n} morceaux.",
'playlists.track_removed': 'Morceau retiré',
'playlists.save_queue': 'Enregistrer la file comme playlist…',
'playlists.cancel': 'Annuler',
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js
index 75b420d..1da0793 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js
@@ -335,6 +335,7 @@ export default {
'playlists.synced': 'Playlist sincronizzate',
'playlists.sync_failed': 'Sincronizzazione non riuscita',
'playlists.err_no_key': 'Esci e rientra con la tua password una volta per sincronizzare le playlist tra dispositivi.',
+ 'playlists.err_too_large': '«{name}» è troppo grande per sincronizzarsi con i tuoi altri dispositivi. Dividila in playlist di circa {n} brani.',
'playlists.track_removed': 'Brano rimosso',
'playlists.save_queue': 'Salva la coda come playlist…',
'playlists.cancel': 'Annulla',
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js
index c87d795..971ecb2 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js
@@ -332,6 +332,7 @@ export default {
'playlists.synced': 'プレイリストを同期しました',
'playlists.sync_failed': '同期できませんでした',
'playlists.err_no_key': 'プレイリストを端末間で同期するには、一度サインアウトしてパスワードでサインインしてください。',
+ 'playlists.err_too_large': '「{name}」は大きすぎて他のデバイスと同期できません。{n} 曲程度のプレイリストに分けてください。',
'playlists.track_removed': '曲を削除しました',
'playlists.save_queue': 'キューをプレイリストとして保存…',
'playlists.cancel': 'キャンセル',
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js
index 4fa3d01..dae63d2 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js
@@ -336,6 +336,7 @@ export default {
'playlists.synced': 'Afspeellijsten gesynchroniseerd',
'playlists.sync_failed': 'Synchroniseren mislukt',
'playlists.err_no_key': 'Meld je één keer af en met je wachtwoord weer aan om afspeellijsten tussen apparaten te synchroniseren.',
+ 'playlists.err_too_large': '"{name}" is te groot om met je andere apparaten te synchroniseren. Splits hem op in playlists van ongeveer {n} nummers.',
'playlists.track_removed': 'Nummer verwijderd',
'playlists.save_queue': 'Wachtrij opslaan als afspeellijst…',
'playlists.cancel': 'Annuleren',
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js
index d0fd387..639d518 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js
@@ -345,6 +345,7 @@ export default {
'playlists.synced': 'Playlisty zsynchronizowane',
'playlists.sync_failed': 'Nie udało się zsynchronizować',
'playlists.err_no_key': 'Wyloguj się i zaloguj raz hasłem, aby synchronizować playlisty między urządzeniami.',
+ 'playlists.err_too_large': '„{name}” jest zbyt duża, aby zsynchronizować ją z innymi urządzeniami. Podziel ją na playlisty po około {n} utworów.',
'playlists.track_removed': 'Utwór usunięty',
'playlists.save_queue': 'Zapisz kolejkę jako playlistę…',
'playlists.cancel': 'Anuluj',
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js
index 94c56a6..371b84f 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js
@@ -336,6 +336,7 @@ export default {
'playlists.synced': 'Playlists sincronizadas',
'playlists.sync_failed': 'Não foi possível sincronizar',
'playlists.err_no_key': 'Saia e entre com sua senha uma vez para sincronizar as playlists entre dispositivos.',
+ 'playlists.err_too_large': '"{name}" é grande demais para sincronizar com seus outros dispositivos. Divida-a em playlists de cerca de {n} faixas.',
'playlists.track_removed': 'Faixa removida',
'playlists.save_queue': 'Salvar a fila como playlist…',
'playlists.cancel': 'Cancelar',
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js
index bc77d5f..96ba4fe 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js
@@ -329,6 +329,7 @@ export default {
'playlists.synced': '播放列表已同步',
'playlists.sync_failed': '无法同步',
'playlists.err_no_key': '请退出并用密码重新登录一次,以便在设备间同步播放列表。',
+ 'playlists.err_too_large': '「{name}」太大,无法同步到你的其他设备。请拆分为约 {n} 首歌曲的播放列表。',
'playlists.track_removed': '已移除曲目',
'playlists.save_queue': '将队列保存为播放列表…',
'playlists.cancel': '取消',
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 e2fb3f9..70c105d 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/playlist-menu.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/playlist-menu.js
@@ -197,11 +197,18 @@ function PlaylistMenuButton({ userId, lists, reload, onPlayQueue, onSync, cached
// playlist subkey existed — and an AES handle is non-extractable, so
// there is nothing to derive it from. One sign-in with the passphrase
// fixes it for good; a code on screen does not say that.
- say(r && r.ok
- ? `${t('playlists.synced')} (${r.pushed || 0}↑ ${r.pulled || 0}↓)`
- : (why === 'no_key'
- ? t('playlists.err_no_key')
- : `${t('playlists.sync_failed')}: ${why}`));
+ // A playlist too large to send is reported ahead of the totals. The
+ // sync itself worked — everything else moved — so `ok` is true, and
+ // saying "synced" while one playlist silently stayed behind would be
+ // 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 })
+ : (r && r.ok
+ ? `${t('playlists.synced')} (${r.pushed || 0}↑ ${r.pulled || 0}↓)`
+ : (why === 'no_key'
+ ? t('playlists.err_no_key')
+ : `${t('playlists.sync_failed')}: ${why}`)));
},
},
]);
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);
+ }
}
}