aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-09-16 11:17:46 +0200
committerChristophe Besson <cbesson@gmail.com>2026-09-16 11:17:46 +0200
commitcde57423e04812fe2c939fdf983e3b45a77fd82d (patch)
tree7fe44bc9d83cf72382394ebf9b5177c207618be2 /packages/meshbay-hub
parent20706fb9a4ec646816b44a10842aa8f58ea0fd75 (diff)
downloadmeshbay-cde57423e04812fe2c939fdf983e3b45a77fd82d.tar.gz
mnp 3.1: per-account blobs the node cannot read
One row per playlist plus a manifest, so starring a track rewrites that playlist rather than the whole collection. blob_enc is a BLOB, not base64 TEXT: these run to hundreds of kilobytes. user_id comes from the session and never from the message; kind is validated against a pattern; every cap refuses with a stated reason rather than truncating. Additive, so MNP_MIN_SUPPORTED does not move — a 3.0 node answers "unknown message type" and the client writes to the next one it reaches. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-hub')
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/transport.js74
1 files changed, 69 insertions, 5 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/transport.js b/packages/meshbay-hub/src/meshbay_hub/static/transport.js
index 77bf522..a6b3f52 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/transport.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/transport.js
@@ -267,11 +267,15 @@ window.addEventListener('hashchange', () => {
// The `v: '0.1'` on every other message in this file is the historical value
// and is read by nothing; it is left alone deliberately. The range is
// negotiated once, at the start, not restated per message.
-const MNP_V = '3.0';
-// Raised with it on the 3.0 flag day. A node older than 3.0 cannot grant the
-// lease this client opens for every download and upload, so talking to one
-// would mean every transfer failing for a reason the person cannot act on.
-// Refusing it at the handshake says so once, in a sentence.
+const MNP_V = '3.1';
+// **Not** raised with it, and that is the whole difference between 3.0 and 3.1.
+// 3.0 was a flag day because a node older than it cannot grant the lease this
+// client opens for every download and upload, so talking to one would mean
+// every transfer failing for a reason the person cannot act on. 3.1 only adds
+// `user_blob_*`: a 3.0 node answers "unknown message type" and the client
+// stores its playlists on the next node it reaches, keeping its own copy
+// meanwhile (docs/playlists.md §6.4). Refusing every 3.0 node over a feature
+// that degrades this quietly would be the flag day nobody needed.
const MNP_V_MIN = '3.0';
// Codes a NODE sends us, in its own vocabulary (meshbay_common/handshake.py's
@@ -2737,6 +2741,66 @@ class MeshBayTransport {
return msg;
}
+ // ── Per-account blobs (playlists) ──────────────────────────────────────
+ //
+ // MNP 3.1, docs/playlists.md §8.1. The node stores bytes it cannot read and
+ // hands them back; `user_id` is never sent, because the node takes it from
+ // the authenticated session and would be wrong to take it from here.
+ //
+ // `blob_enc` is a Uint8Array and goes on the wire as msgpack `bin`, not
+ // base64: a playlist runs to hundreds of kilobytes and base64 is a third of
+ // every write. None of these needs an entry in the `ack` fallback at the
+ // bottom of _handleMessage — that branch exists for nodes too old to stamp
+ // `req_id`, and no node old enough to skip it knows these messages at all.
+
+ async storeUserBlob(kind, rev, blobEnc) {
+ const msg = await this._sendAndWait({
+ type: 'user_blob_store', v: '0.1',
+ kind, rev, blob_enc: blobEnc,
+ });
+ // A refused store is the size cap or the account quota, and the node says
+ // which. Thrown rather than swallowed: the caller has to be able to tell
+ // the reader that this playlist did not save.
+ if (msg.type === 'error') throw new Error(msg.detail);
+ return msg;
+ }
+
+ /**
+ * One blob, or `{ rev: null, blob_enc: null }` when this account has never
+ * written that kind here — which is the ordinary state of a node the reader
+ * has just joined, and must not read as a failure.
+ */
+ async fetchUserBlob(kind) {
+ const msg = await this._sendAndWait({
+ type: 'user_blob_fetch', v: '0.1', kind,
+ });
+ if (msg.type === 'error') throw new Error(msg.detail);
+ return { rev: msg.rev ?? null, blob_enc: msg.blob_enc ?? null };
+ }
+
+ /**
+ * Which kinds this node holds, and at what revision — never a payload.
+ *
+ * What a client that has lost its local state needs: playlist ids are
+ * client-generated, so there is nothing to fetch by name until this says
+ * what the names are.
+ */
+ async listUserBlobs() {
+ const msg = await this._sendAndWait({
+ type: 'user_blob_list', v: '0.1',
+ });
+ if (msg.type === 'error') throw new Error(msg.detail);
+ return msg.blobs || [];
+ }
+
+ async deleteUserBlob(kind) {
+ const msg = await this._sendAndWait({
+ type: 'user_blob_delete', v: '0.1', kind,
+ });
+ if (msg.type === 'error') throw new Error(msg.detail);
+ return msg;
+ }
+
get gekRaw() { return this._gekRaw; }
/**