aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--docs/MESHBAY_NODE_PROTOCOL.md11
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/transport.js9
-rw-r--r--packages/meshbay-node/src/meshbay_node/transport/webrtc/handshake.py14
-rwxr-xr-xpackages/meshbay-node/tests/transfer_probe.py29
4 files changed, 22 insertions, 41 deletions
diff --git a/docs/MESHBAY_NODE_PROTOCOL.md b/docs/MESHBAY_NODE_PROTOCOL.md
index 0886a7e..2e314f2 100644
--- a/docs/MESHBAY_NODE_PROTOCOL.md
+++ b/docs/MESHBAY_NODE_PROTOCOL.md
@@ -579,7 +579,6 @@ at all — including a decryption.
| `chat_link_preview` | whether the node unfurls links posted here. Absent means on |
| `search_listed` | whether the reader's cross-group Search lists this group. Presentation only — the index is served identically either way. Absent means listed |
| `chat_epoch` | the chat epoch a client must seal under right now (§11.7). There is no `chat_encrypted` beside it, because there is no switch |
-| `transfer_limits` | `{download, upload}` — this member's own caps in this group, so the interface can say "2 of your 2 slots are busy" instead of drawing a bare spinner. Absent reads as "no limit known" and the hint is not drawn; never as "unlimited", which would have the interface contradicting the node (§11.2) |
| `tmdb_enabled`, `musicbrainz_enabled` | per-group metadata lookups |
| `tmdb_token_customized`, `tmdb_language` | node-wide TMDB config; the token itself is never sent |
| `indexing` | the `index_progress` counters (§11.1) so a client connecting mid-scan shows progress immediately. Never a path, a filename or a root name |
@@ -1412,8 +1411,8 @@ an absent setting as no limit would leave the node-wide cap as the only control,
is the situation leases exist to end. The per-group value is a signed operator
operation (`transfer_limits`, §10.4, bounded to 1–32; zero is refused, because a member
who may not transfer at all is a member the operator revokes). The node-wide values are
-daemon settings. A member's own caps ride on the handshake ack so the interface can say
-"2 of your 2 slots are busy" rather than draw a spinner that explains nothing.
+daemon settings. A member's own cap rides on every `transfer_state`, so the interface can
+say "2 of your 2 slots are busy" rather than draw a spinner that explains nothing.
**Queueing.** One FIFO per kind. `_pump` walks it in arrival order and **skips** a
member who is at their own cap rather than stopping at them — granting strictly in
@@ -1422,9 +1421,9 @@ told how many are `ahead` of it. Beyond 32 queued per member the answer is
`too_many_queued`, because an unbounded queue is how a node runs out of memory politely.
`used` and `cap` on `transfer_state` are this member's own count and this member's own
-limit **in this group** — the same value `_has_room` enforces and the same one the
-handshake ack announces. Three readings of one number, and an interface that draws a
-different one from the node's is an interface that offers a slot the node will queue.
+limit **in this group** — the same value `_has_room` enforces. The interface reads it
+from here and from nowhere else: one number with two sources is an interface that can
+offer a slot the node will queue.
**Reclaim.** The session teardown is the primary path and it is immediate. A sweeper
runs every 15 s for whatever the teardown cannot see, and tells two failures apart:
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/transport.js b/packages/meshbay-hub/src/meshbay_hub/static/transport.js
index 7f1b232..98d010e 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/transport.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/transport.js
@@ -500,11 +500,6 @@ class MeshBayTransport {
this._inFlightUploads = new Set();
// tr → Lease. A transfer's slot on the node, from the client's side.
this._leases = new Map();
- // Set from the handshake ack: a node that answers with `transfer_limits`
- // speaks transfer slots. Used instead of a timeout, because "no answer
- // yet" and "this node will never answer" are indistinguishable in time and
- // guessing wrong either stalls every download or defeats the cap.
- this._transferLimits = null;
// Set once close() runs — stops the automatic reconnect from firing on a
// connection the caller tore down on purpose (leaving the group, page
// unload), which would otherwise race back in right as everything else
@@ -573,9 +568,6 @@ class MeshBayTransport {
set onIndexDelta(fn) { this._onIndexDelta = fn; }
set onRootsChanged(fn) { this._onRootsChanged = fn; }
- /** This member's own caps in this group, or null when the node said nothing. */
- get transferLimits() { return this._transferLimits; }
-
set onAppsEnabled(fn) { this._onAppsEnabled = fn; }
set onAppDirectories(fn) { this._onAppDirectories = fn; }
set onChatDirectory(fn) { this._onChatDirectory = fn; }
@@ -1157,7 +1149,6 @@ class MeshBayTransport {
delete ack.nonce;
delete ack.ct;
Object.assign(ack, config);
- this._transferLimits = ack.transfer_limits || null;
// From the *sealed* part of the ack: a forged epoch would have this
// client sealing under a key the group has retired.
diff --git a/packages/meshbay-node/src/meshbay_node/transport/webrtc/handshake.py b/packages/meshbay-node/src/meshbay_node/transport/webrtc/handshake.py
index eceb2b4..f701072 100644
--- a/packages/meshbay-node/src/meshbay_node/transport/webrtc/handshake.py
+++ b/packages/meshbay-node/src/meshbay_node/transport/webrtc/handshake.py
@@ -24,7 +24,6 @@ from meshbay_common.handshake import (
)
from meshbay_common.protocol import MNP
-from meshbay_node import transfers as transfers_mod
from meshbay_node.indexer.indexer import DirectoryIndexer
from meshbay_node.transport.webrtc.channel import _extract_dtls_fingerprint, _get_remote_ip
from meshbay_node.transport.webrtc.limits import MAX_MSG
@@ -266,19 +265,6 @@ class HandshakeMixin:
# No `chat_encrypted` beside it: there is no switch. A peer that
# reached this point speaks MNP 2.0, and 2.0 has no plaintext chat.
"chat_epoch": int(self._group_ctx().get("chat_epoch", 0) or 0),
- # This member's own transfer caps in this group, so the interface
- # can say "2 of 2 of your slots are busy" rather than draw a bare
- # spinner. Absent reads as "no limit known" and the hint is simply
- # not drawn — never as "unlimited", which would have the interface
- # contradicting the node.
- "transfer_limits": {
- "download": self._slots().member_cap(
- transfers_mod.DOWNLOAD,
- (self._group_id or "", self._user_id or "")),
- "upload": self._slots().member_cap(
- transfers_mod.UPLOAD,
- (self._group_id or "", self._user_id or "")),
- },
# So a client that connects mid-scan shows the indexing state
# immediately, instead of waiting for the next periodic
# INDEX_PROGRESS push. Never a path or filename — see
diff --git a/packages/meshbay-node/tests/transfer_probe.py b/packages/meshbay-node/tests/transfer_probe.py
index 7aa4902..6666960 100755
--- a/packages/meshbay-node/tests/transfer_probe.py
+++ b/packages/meshbay-node/tests/transfer_probe.py
@@ -274,7 +274,14 @@ async def operator_checks(client, ack, group, node_id) -> int:
import re as _re
failures = 0
- member_cap = int((ack.get("transfer_limits") or {}).get("download") or 0)
+ # This member's own cap, as the node states it on every `transfer_state`.
+ probe_tr, probe_state = await _open_transfer(client)
+ member_cap = int(probe_state.get("cap") or 0)
+ client.send({"type": "transfer_close", "v": "0.1", "tr": probe_tr, "reason": "done"})
+ while True: # closed before anything below counts what is in use
+ reply = await client.recv_type("transfer_state", timeout=15)
+ if reply.get("tr") == probe_tr and reply.get("state") == "closed":
+ break
# The queue has to be held by the *node* cap, not by this member's own.
#
@@ -446,16 +453,6 @@ async def probe(args) -> int:
ack = await alice.connect(http, group["id"], node_id)
- limits = ack.get("transfer_limits")
- if limits is None:
- print("This node does not hand out transfer slots — it predates "
- "them, or the handshake ack lost the field. Nothing below "
- "can be measured.")
- await alice.close()
- return 1
- cap = int(limits.get("download") or 0)
- print(f"node reports this member may run {cap} download(s) at once\n")
-
if args.operator:
return await operator_checks(alice, ack, group, node_id)
@@ -472,9 +469,17 @@ async def probe(args) -> int:
# held. So the first reply is read for what the node says is already
# in use, and the run stops rather than measuring against a moving
# floor.
- want = args.want or (cap + 2)
opened = [await _open_transfer(alice)]
first = opened[0][1]
+ # This member's cap, as the node states it on every `transfer_state`.
+ cap = int(first.get("cap") or 0)
+ if not cap:
+ print("This node does not state a transfer cap — nothing below can "
+ "be measured.")
+ await alice.close()
+ return 1
+ print(f"node reports this member may run {cap} download(s) at once\n")
+ want = args.want or (cap + 2)
if first.get("state") != "granted" or first.get("used", 1) != 1:
print(f"this node is not idle: it reports {first.get('used')} of "
f"{first.get('cap')} slots already used by this member, and "