aboutsummaryrefslogtreecommitdiffstats
path: root/docs/MESHBAY_DESIGN.md
diff options
context:
space:
mode:
Diffstat (limited to 'docs/MESHBAY_DESIGN.md')
-rw-r--r--docs/MESHBAY_DESIGN.md57
1 files changed, 46 insertions, 11 deletions
diff --git a/docs/MESHBAY_DESIGN.md b/docs/MESHBAY_DESIGN.md
index 2b6d1ac..aaa232c 100644
--- a/docs/MESHBAY_DESIGN.md
+++ b/docs/MESHBAY_DESIGN.md
@@ -32,7 +32,7 @@
| **this document** | the architecture, the trust model, and the reason each decision is what it is |
| `MESHBAY_NODE_PROTOCOL.md` | the MNP wire format, message by message |
| `transfers-v1.md` | the transfer system's failure-mode analysis, kept because a synthesis cannot carry "every way a slot can be lost" |
-| `playlists.md` | the playlist design in full — decided, not built (§9.10) |
+| `playlists.md` | the playlist design and its interface in full — decided, not built (§9.10) |
| `cast-smart-tv.md` | the DLNA/UPnP device backend — designed, not built (§11.4) |
| `WINDOWS-PORT.md` | the Windows port's audit and packaging detail (§11.2) |
| `PACKAGING-GUIDE.md`, `HTTPS.md`, `MAIL-SERVER.md`, `windows-build.md` | installation and server operations |
@@ -2327,10 +2327,23 @@ removed from the hub, and it is the same rule that keeps resume positions local:
*nothing new learns what you watch.* An encrypted blob on the hub is technically
trivial and is still refused.
-It lives on the node instead, as **an opaque per-account blob in `bundles.db`** —
-the same shape as the keypair bundle, which the node already stores and cannot
-read. **No new trust boundary**: the node is not asked to hold a kind of thing it
-does not already hold for that same account.
+It lives on the node instead, as **opaque per-account blobs in `bundles.db`** — the
+same shape as the keypair bundle, which the node already stores and cannot read.
+**No new trust boundary**: the node is not asked to hold a kind of thing it does
+not already hold for that same account.
+
+**One blob per playlist, plus a small manifest** — not one blob for the collection,
+and the reason is write amplification rather than size. Under a single blob,
+starring one track rewrites and re-uploads the whole collection to every node
+reached; split, it rewrites that one playlist. The manifest — names, revisions,
+tombstones, counts, a few KB — is also the only thing every menu needs, so "add to
+playlist" draws instantly with every node offline, and a body is fetched only when
+its playlist is opened or played. Blobs are **compressed before sealing and padded
+after**: the payload is repetitive enough to be worth a factor of three, and the
+padding is what stops a ciphertext length from counting somebody's tracks. The node
+caps each blob and the account's total, and **refuses rather than truncates** — a
+truncating cap silently loses tracks, which is the failure the whole design exists
+to prevent.
**The key is the one thing that must not be got wrong.** Identity keys are per node
(§3.2), so a blob encrypted under one is unreadable from every other node — the
@@ -2340,14 +2353,36 @@ is the bundle key, so `playlist_key = HKDF(bundle_key, info =
Argon2 run, and a purpose-separated subkey rather than the bundle key reused with a
different AAD (§4.4's rule). The nonce is 96 random bits and never a counter, for
exactly the reason chat's is (§4.5): two devices of one account derive the *same*
-key, which is the point.
+key, which is the point. The AAD names the blob's *kind*, so one playlist's body
+cannot be served in place of another's.
**Merge is the hard third, and the granularity is what makes it tractable.** The
-unit is **one playlist, not the collection**; revision counters order writes, never
-the wall clock; and **a deletion is a tombstone, never an absence** — an absence is
-indistinguishable from a device that has not seen the addition yet. A node that is
-offline for a month therefore cannot corrupt anything: it holds an older revision of
-some playlists and is overwritten per playlist, not wholesale.
+unit is **one playlist, not the collection** — which the per-playlist blob now
+makes true of the storage as well, so two devices editing two playlists do not even
+write the same row. Revision counters order writes, never the wall clock; and **a
+deletion is a tombstone, never an absence** — an absence is indistinguishable from a
+device that has not seen the addition yet. A node that is offline for a month
+therefore cannot corrupt anything: it holds an older revision of some playlists and
+is overwritten per playlist, not wholesale.
+
+**The interface is a menu inside Music, not a page.** Music's player is already
+persistent at shell level and already holds a queue that crosses groups; *loading* a
+playlist replaces that queue, and below that call a playlist and an album are
+indistinguishable — so auto-advance, shuffle and prefetch are unchanged by
+construction. The one genuine code change is that the queue can today only be
+*replaced*: "play next" and "add to queue" require it to become appendable, which
+makes it a small reducer rather than three pieces of component state. Everything
+else is a context menu on a cover or a track row, and one button in Music's sticky
+toolbar. Because Music is mounted by both the group page and the Search page
+(§9.11), a playlist built inside a group is managed from the consolidated view with
+no second surface and no application-registry entry.
+
+**Unavailability is answered at play time, not at add time.** Whether a group is
+reachable is only knowable by dialing, and refusing to add a track because its node
+is off tonight loses the user's intent permanently to a condition that lasts an
+evening. So adding never dials; playback skips, distinguishing a file that will not
+decode (a property of that file) from a group that does not answer (a property of
+that group, whose tracks are then skipped together).
**It adds no dialing and no new streaming path.** Sync rides connections the client
already makes, and playback is unchanged (§9.8).