aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub/static
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/static')
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/group-page.js71
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/transport.js4
2 files changed, 56 insertions, 19 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/group-page.js b/packages/meshbay-hub/src/meshbay_hub/static/group-page.js
index c59d097..18d3a8f 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/group-page.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/group-page.js
@@ -311,7 +311,6 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs,
const sessionKeys = null;
setStatus('connecting');
- const nodeId = nodesData.nodes[0].node_id;
// Renewed here rather than taken from the prop. This effect no longer
// re-runs when the token rotates (see the dependency list below), so
// the captured one can be older than the session's — and it is used to
@@ -319,25 +318,59 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs,
// connection at all. Renewals are shared, so if one is already in
// flight this waits for it instead of starting a second.
const live = (await ensureFreshToken()) || token;
- // The same base the API calls use: signaling is a hub endpoint like
- // any other, and two sources for one address is how they drift.
- const transport = new window.MeshBayTransport(HUB, live);
- transportRef.current = transport;
- // Consulted only by the automatic reconnect after a WebRTC failure
- // (transport.js's _reconnectLoop) — the token captured by this
- // connect() call can be stale by then, since the whole point is that
- // some real time (screen lock, a dead NAT mapping) passed unnoticed.
- transport.onNeedToken = async () => (await ensureFreshToken()) || token;
- // Set before connect(), because connect() is where device_hello runs —
- // and again on every reconnect it makes, which is the case this exists
- // for: nothing else tells the page the answer changed.
- transport.onDeviceIdentity = (ok) => {
- if (!cancelled) setDeviceReady(ok);
- };
- const ack = await transport.connect(
- nodeId, live, groupId, null, sessionKeys, session.bundleKey, username,
- userId, session.pendingJoinCode, session.recoveryKey);
+ // Every node the hub lists, in turn — not `nodes[0]` and nothing else.
+ // The list is in hub registration order, and its head is not
+ // necessarily a node that can serve the group: one whose config does
+ // not list it refuses the handshake with "Group not hosted on this
+ // node". Stopping at the first made that refusal indistinguishable
+ // from the group being down, while a node that *could* serve it stood
+ // second in the same list — which is how `media` went dark on
+ // 2026-09-11 with its only real host online the whole time. The hub no
+ // longer registers a node for a group it does not claim; trying the
+ // rest is what keeps one bad entry from being fatal again.
+ // `transport` holds the attempt in progress, and keeps whichever one
+ // answers — so it is null after the loop exactly when none did.
+ let transport = null, ack = null, lastErr = null;
+ for (const n of nodesData.nodes) {
+ // The same base the API calls use: signaling is a hub endpoint like
+ // any other, and two sources for one address is how they drift.
+ transport = new window.MeshBayTransport(HUB, live);
+ transportRef.current = transport;
+ // Consulted only by the automatic reconnect after a WebRTC failure
+ // (transport.js's _reconnectLoop) — the token captured by this
+ // connect() call can be stale by then, since the whole point is that
+ // some real time (screen lock, a dead NAT mapping) passed unnoticed.
+ transport.onNeedToken = async () => (await ensureFreshToken()) || token;
+ // Set before connect(), because connect() is where device_hello runs —
+ // and again on every reconnect it makes, which is the case this exists
+ // for: nothing else tells the page the answer changed.
+ transport.onDeviceIdentity = (ok) => {
+ if (!cancelled) setDeviceReady(ok);
+ };
+ try {
+ ack = await transport.connect(
+ n.node_id, live, groupId, null, sessionKeys, session.bundleKey,
+ username, userId, session.pendingJoinCode, session.recoveryKey);
+ break;
+ } catch (e) {
+ lastErr = e;
+ // Closed and dropped before anything else looks at either: an
+ // attempt that failed must not be handed to `releaseWhenIdle` by
+ // the unmount cleanup, which exists to keep a *working* connection
+ // alive for a download still using it.
+ try { transport.close(); } catch { /* never opened */ }
+ transport = null;
+ transportRef.current = null;
+ if (cancelled) return;
+ // A refusal that names a state of *this browser* — a code to enter,
+ // a passphrase, a device to approve — is the same answer from every
+ // node, and the operator to act on is this one's. Trying the next
+ // node would only replace it with a less useful message.
+ if (e.reason && e.reason !== 'not_hosted') throw e;
+ }
+ }
+ if (!transport) throw (lastErr || new Error('no node served this group'));
session.pendingJoinCode = null;
if (cancelled) return;
setIsNodeAdmin(!!ack.is_node_admin);
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/transport.js b/packages/meshbay-hub/src/meshbay_hub/static/transport.js
index 32005b7..a21da19 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/transport.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/transport.js
@@ -283,6 +283,10 @@ const HANDSHAKE_REFUSALS = {
version_too_new: 'This node is running an older MeshBay than this page needs. '
+ 'Its operator has to update it.',
version_unreadable: 'The node could not read this page\'s protocol version.',
+ // Surfaced only when it was the *last* node the hub offered for the group —
+ // group-page.js moves on to the next one on this code rather than stopping.
+ not_hosted: 'No node the hub offered for this group is hosting it. Its '
+ + 'operator has to attach it on the node that holds its files.',
};
const JOIN_REFUSALS = {