From 53ea44cb03ef6f8d941f6c8c9446551b0c5cd1ac Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Wed, 9 Sep 2026 14:00:22 +0200 Subject: feat: MNP 3.0 — a transfer needs a lease MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stage 4 of ~/next/improve-downloads.md, the flag day. Leases become compulsory and a 2.x peer is refused at the handshake. **The bound on leaseless reads (§3.4.1) did not exist, and it is what makes the rest mean anything.** Browsing a group is never subject to a transfer slot — that is an operator decision and a requirement: a member must be able to browse a group at capacity exactly as they browse an idle one. But "not leased" cannot mean "unbounded", or a client that simply omits `tr` transfers outside every cap and the caps are decoration. A session may now read two distinct files at once without a lease: one because a viewer looks at one file, two so that prefetching the next photo stays possible. A count of files and not a byte budget, because a RAW photo is 60-80 MB and is browsing while a 40 MB archive is a download, and no size threshold separates them. Thumbnails, posters and cover art never reach this check at all — they resolve out of the node's own cache. It is a fairness control among cooperating clients, in the company of `max_concurrent_streams`, and is not a defence against a member determined to saturate a node's disk. That member is a member, and the answer to them is `member revoke`. **MNP_VERSION and MNP_MIN_SUPPORTED both move to 3.0**, on both sides. The messages are additive; the requirement is not. An opt-in switch would leave a leaseless branch reachable on every node, which is finding C6's lesson — a transport that accepted a bare JWT — one feature later. **The desktop client now checks before it connects.** The SPA is served by the hub and picks up a new client on reload; the application ships its own interface, so an un-updated one would sign in, list groups, and fail every connection with `version_too_old` — a refusal in a protocol vocabulary with nothing anyone can act on. It asks `/v1/hub/version` for `client.minimum` and says so plainly instead. An unreachable hub is deliberately *not* "too old": a captive portal or a closed laptop must not make starting the application impossible. **Every package is aligned on 0.13.0.** `meshbay-client/package.json` had drifted to 1.0.0 while the Python packages were on 0.12.0 — invisible until something compared those numbers, and then load-bearing: an installed client announcing 1.0.0 sorts above a 0.13.0 minimum and walks through the gate meant to stop it. That is stated in the code rather than left to be rediscovered; it is acceptable exactly once, because the operator is updating every client, node and hub by hand for this flag day. A new test fails if two packages ever disagree again, and another fails if the hub would refuse the client the tree builds. Node suite 1209 passed, hub suite 861 passed. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01HCGdheDLxGReuKHga3BtST --- .../meshbay-node/tests/test_leaseless_reads.py | 85 ++++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 packages/meshbay-node/tests/test_leaseless_reads.py (limited to 'packages/meshbay-node/tests') diff --git a/packages/meshbay-node/tests/test_leaseless_reads.py b/packages/meshbay-node/tests/test_leaseless_reads.py new file mode 100644 index 0000000..70fd24f --- /dev/null +++ b/packages/meshbay-node/tests/test_leaseless_reads.py @@ -0,0 +1,85 @@ +""" +Browsing is never subject to a transfer slot — and is not unbounded either. + +**Operator decision, 2026-09-08:** a member must be able to browse a group that +is at capacity exactly as they browse an idle one. Not the poster grid, not the +covers, not opening a photo or a PDF to look at it. §3.4 of +~/next/improve-downloads.md satisfies that structurally: a transfer is what the +transfers widget shows, and nothing else takes a slot. + +But "not leased" cannot mean "unbounded". With MNP 3.0 making leases +compulsory, a client that simply omits `tr` would otherwise transfer outside +every cap, and the caps would be decoration — the leaseless branch left +reachable is finding C6's lesson (a transport that accepted a bare token) one +feature later. + +So a leaseless read is bounded by a small count of *files in flight*, not by +bytes: a RAW photo out of a camera is 60–80 MB and is browsing, a 40 MB archive +is a download, and no size threshold separates them. What separates them is +which function asked. +""" + +from meshbay_node.transfers import ( + LEASELESS_IDLE_SECS, MAX_LEASELESS_IN_FLIGHT, LeaselessReads, +) + + +def test_a_viewer_looking_at_one_file_is_never_refused(): + reads = LeaselessReads() + for chunk in range(20): + assert reads.admit("photo-1", now=float(chunk)) is True + + +def test_a_second_file_is_allowed_so_prefetching_stays_possible(): + """One is what a viewer needs; two is so the photo viewer can fetch the + next one while showing this one.""" + reads = LeaselessReads() + assert reads.admit("photo-1", now=0.0) is True + assert reads.admit("photo-2", now=0.0) is True + + +def test_a_third_file_is_refused(): + reads = LeaselessReads() + reads.admit("a", now=0.0) + reads.admit("b", now=0.0) + assert reads.admit("c", now=0.0) is False + + +def test_a_file_already_being_read_is_never_cut_off(): + """Even once the limit is reached. Refusing a chunk halfway through a photo + because the count moved would be worse than never having admitted it — the + viewer would show half an image and no error anyone can act on.""" + reads = LeaselessReads() + reads.admit("a", now=0.0) + reads.admit("b", now=0.0) + assert reads.admit("c", now=0.0) is False + assert reads.admit("a", now=1.0) is True + + +def test_finishing_one_frees_it_at_once(): + """The last chunk is the only "close" a leaseless read has. Waiting for the + idle timeout instead would mean somebody who looked at two photos cannot + look at a third for a minute.""" + reads = LeaselessReads() + reads.admit("a", now=0.0) + reads.admit("b", now=0.0) + reads.finish("a") + assert reads.admit("c", now=0.0) is True + + +def test_a_viewer_closed_mid_file_does_not_hold_its_place_for_ever(): + """It stops asking and says nothing — there is no message for "I closed the + tab". Without the idle expiry the session would carry two dead entries and + refuse every later preview, which is the bound turning into a bug.""" + reads = LeaselessReads() + reads.admit("a", now=0.0) + reads.admit("b", now=0.0) + assert reads.admit("c", now=1.0) is False + assert reads.admit("c", now=LEASELESS_IDLE_SECS + 2) is True + + +def test_the_bound_is_two(): + """Stated here so that changing it is a decision rather than a typo: it is + the number §3.4.1 argues for, and the argument is about viewers, not about + tuning.""" + assert MAX_LEASELESS_IN_FLIGHT == 2 -- cgit v1.2.3