diff options
7 files changed, 36 insertions, 40 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/downloads.js b/packages/meshbay-hub/src/meshbay_hub/static/downloads.js index c790394..9c23d3c 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/downloads.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/downloads.js @@ -649,8 +649,9 @@ async function _attemptStreamedDownload(filename, size, attempt, // writing to the stream stalls that download where we cannot see it or // resume it, and an idle worker is terminated within seconds, taking the // stream with it. A pause button here would restart from zero, which is - // worse than not offering one. §6.5 of ~/next/improve-downloads.md records - // what giving Firefox and Safari a resumable target would cost. + // worse than not offering one. Offering Firefox and Safari a resumable + // target means a resumable sink of our own, which is a larger piece of work + // than the button it would enable. pausable: false, writable: { write: (bytes) => { lastWrite = Date.now(); return writer.write(bytes); }, diff --git a/packages/meshbay-hub/src/meshbay_hub/static/file-utils.js b/packages/meshbay-hub/src/meshbay_hub/static/file-utils.js index 99d9f9a..b732734 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/file-utils.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/file-utils.js @@ -451,9 +451,9 @@ async function downloadEntry(transfers, transport, gek, entry) { }, // After the target, never before: a granted slot has to be taken up within - // the node's deadline, and opening a target can outlast it. See §8.1 of - // ~/next/improve-downloads.md — the other order was tried and cost two of - // three downloads. + // the node's deadline, and opening a target can outlast it — a Save As + // dialog waits for a person. Asking for the slot first loses it to the + // grant deadline while the dialog is open. makeLease: () => transport.openTransfer({ kind: 'download', bytes: entry.size, chunks: totalChunks }), diff --git a/packages/meshbay-hub/tests/test_client_version_gate.py b/packages/meshbay-hub/tests/test_client_version_gate.py index 69ca062..4dc9b98 100644 --- a/packages/meshbay-hub/tests/test_client_version_gate.py +++ b/packages/meshbay-hub/tests/test_client_version_gate.py @@ -5,9 +5,9 @@ The SPA is served by the hub, so a browser picks up a new client on reload. The desktop application **ships its own interface**, so on a flag day an un-updated one can still sign in, still list groups, and then fail every connection with `version_too_old` — a refusal in a protocol vocabulary, surfacing as a node that -will not talk, with nothing anyone can act on. §12.3 of -~/next/improve-downloads.md named this as the thing that had to exist before -MNP 3.0 could ship. +will not talk, with nothing anyone can act on. Refusing at the handshake is only +honest if something tells the person *before* they get there, and this is that +something. `compareVersions` and `refuseIfTooOld` are lifted out of `main.js` **as text** and executed against a modelled environment, on the rule this repo follows diff --git a/packages/meshbay-hub/tests/test_transfers.py b/packages/meshbay-hub/tests/test_transfers.py index d93cf80..a776b50 100644 --- a/packages/meshbay-hub/tests/test_transfers.py +++ b/packages/meshbay-hub/tests/test_transfers.py @@ -560,7 +560,7 @@ def test_a_transport_is_not_closed_under_a_preparing_transfer(tmp_path): # The rule the whole design turns on: **a paused transfer holds nothing.** Its # slot goes back to the node the moment it stops, and resuming rejoins the queue # at the tail. Anything else lets one member close a node by pausing four -# downloads and going to lunch (§6.2 of ~/next/improve-downloads.md). +# downloads and going to lunch. def _pausable_run(): diff --git a/packages/meshbay-node/src/meshbay_node/transfers.py b/packages/meshbay-node/src/meshbay_node/transfers.py index dd382b4..cef817a 100644 --- a/packages/meshbay-node/src/meshbay_node/transfers.py +++ b/packages/meshbay-node/src/meshbay_node/transfers.py @@ -28,9 +28,8 @@ properties are load-bearing, and each one is a decision: This module is deliberately free of asyncio and of the transport: it decides, and the caller does the I/O. `sweep()` is called on a clock the caller owns, and every method returns what changed so the caller can push it. That is what makes -the failure modes in §5 of ~/next/improve-downloads.md testable at all — a -queue that only reveals itself through a DataChannel is a queue nobody can -prove things about. +the failure modes testable at all — a queue that only reveals itself through a +DataChannel is a queue nobody can prove things about. """ from __future__ import annotations @@ -412,28 +411,24 @@ class TransferSlots: # Browsing a group is never subject to a transfer slot — not the poster grid, # not the covers, not opening a photo or a PDF to look at it. A member must be # able to browse a group that is at capacity exactly as they browse an idle one. -# That is a requirement, and §3.4 of ~/next/improve-downloads.md satisfies it -# structurally: a transfer is what the transfers widget shows, and nothing else -# takes a slot. +# That requirement is met structurally rather than by judgement: a transfer is +# what the transfers widget shows, and nothing else takes a slot. # # But "not leased" cannot mean "unbounded", or a client that simply omits `tr` # transfers outside every cap and the caps are decoration. # -# **The number comes from what the shipped client legitimately does, and it was -# wrong once already.** §3.4.1 of ~/next/improve-downloads.md argued for two, -# reasoning about *viewers*: a photo viewer shows one photo, a preview modal one -# document, and the second is for prefetching the next photo. That reasoning -# forgot the music player, which warms a read-ahead window — `prefetchDepth()` -# in music-player.js returns 5 on Wi-Fi, 3 otherwise — so playing an album has -# six files in flight and the fourth was refused with `transfer_required`. -# Reported the day MNP 3.0 shipped, as "I try to play a track and it tells me to -# download it instead". +# **The number comes from what the client legitimately does**, and reasoning +# about *viewers* alone gets it wrong: a photo viewer shows one photo and a +# preview modal one document, but the music player warms a read-ahead window — +# `prefetchDepth()` in music-player.js returns 5 on Wi-Fi, 3 otherwise — so +# playing an album has six files in flight before anyone has done anything +# unusual, and a bound of two refuses a member trying to play a track. # -# Twelve: six for the music read-ahead at its widest, two for a photo viewer -# and its own prefetch running at the same time in the same session, and the -# rest as headroom for the next app that reads ahead. `test_leaseless_reads.py` -# derives the floor from music-player.js itself, so raising the client's -# prefetch without raising this fails rather than reaching a person. +# Twelve: six for that read-ahead at its widest, two for a photo viewer and its +# own prefetch running at the same time in the same session, and the rest as +# headroom for the next app that reads ahead. `test_leaseless_reads.py` derives +# the floor from music-player.js itself, so raising the client's prefetch +# without raising this fails rather than reaching a person. # # Generosity is cheap here and refusal is not. This is a fairness control among # cooperating clients, not a security boundary — a client that lies gets twelve @@ -445,11 +440,11 @@ class TransferSlots: # size threshold separates them. What separates them is which function asked. # # What it costs, stated plainly: a client that lies — labelling a bulk download -# as a view — gets two files at a time instead of its member cap. That is the -# residual, it is bounded, it is audited, and it is the same kind of statement -# as the cap itself. **This is a fairness control among cooperating clients**, -# not a defence against a member determined to saturate a node's disk. The -# answer to that member is `member revoke`. +# as a view — gets this many files at a time instead of its member cap. That is +# the residual, it is bounded, it is audited, and it is the same kind of +# statement as the cap itself. **This is a fairness control among cooperating +# clients**, not a defence against a member determined to saturate a node's +# disk. The answer to that member is `member revoke`. MAX_LEASELESS_IN_FLIGHT = 12 # A leaseless read has no "close" message, so it ends when the last chunk goes diff --git a/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py b/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py index 46fbba3..b3618b5 100644 --- a/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py +++ b/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py @@ -4822,9 +4822,9 @@ class WebRTCPeerSession: # 11:54:19 reclaimed 919ebf54 (abandoned) # 11:55:48 Upload complete: ... (3 522 297 517 bytes) # - # The download twin of this was fixed on 2026-09-08 (§12.1 of - # ~/next/improve-downloads.md); the same omission was still here, - # invisible until uploads started taking a real lease. + # `_do_file_request` marks a lease alive for exactly the same reason; + # both sides of a transfer have to say they are still moving, or the + # sweeper reclaims whichever one forgot. tr = msg.get("tr") if tr: slots = self._ctx.get("_transfer_slots") diff --git a/packages/meshbay-node/tests/test_leaseless_reads.py b/packages/meshbay-node/tests/test_leaseless_reads.py index 64aed3d..6bd7f1f 100644 --- a/packages/meshbay-node/tests/test_leaseless_reads.py +++ b/packages/meshbay-node/tests/test_leaseless_reads.py @@ -3,9 +3,9 @@ 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. +covers, not opening a photo or a PDF to look at it. That is met structurally +rather than by judgement: 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 |