aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub')
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/group-page.js68
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/transport.js43
2 files changed, 94 insertions, 17 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 45049a16..304a28fb 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/group-page.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/group-page.js
@@ -247,6 +247,9 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs,
// In the Android application, when the local network was refused: whether
// Android will still ask ("prompt") or only its settings can change it.
const [lanBlocked, setLanBlocked] = useState('');
+ // Set while the message is a reconnect refused the local network: the
+ // reconnect that gets through takes it away.
+ const lanLostRef = useRef(false);
const transportRef = useRef(null);
const gekRef = useRef(null);
// One refresh per group: if a fresh token still says we are not a member, we
@@ -394,6 +397,7 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs,
// Dropped by the teardown below, so a transport handed on to a running
// download (`releaseWhenIdle`) stops driving a page that is gone.
let offReconnect = null;
+ let offRefused = null;
let deviceRetry = null;
// The cache is written here and read only by the search page. It used to
@@ -402,10 +406,27 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs,
// cache landing afterwards. Files shows what the node says, or says it
// cannot reach the node.
+ // This device dropped the node's local addresses (transport.js
+ // localNetworkRefused): a setting to change here, not a node to wait for.
+ // Android 17 gates it per application, Chrome from 155. In the application,
+ // whether Android will still ask or only its settings can change it.
+ const explainLanRefusal = async () => {
+ if (!platform.localNetwork.available) {
+ return t(/Android/i.test(navigator.userAgent)
+ ? 'group.lan_blocked_android' : 'group.lan_blocked_browser');
+ }
+ try {
+ const state = await platform.localNetwork.state();
+ if (!cancelled) setLanBlocked(state === 'granted' ? '' : state);
+ } catch { /* the message stands alone */ }
+ return t('group.lan_blocked_app');
+ };
+
const connect = async () => {
setStatus('discovering');
setError('');
setLanBlocked('');
+ lanLostRef.current = false;
// Belongs to the connection about to be made, not the group just left.
setDeviceReady(false);
gekRef.current = null;
@@ -632,8 +653,24 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs,
// full fetch rather than a delta: this session was never told what it
// missed, and a delta is computed against a snapshot only the node
// has.
+ // Connected on mobile data, then on the node's Wi-Fi without the
+ // permission: every attempt is refused and the reconnect goes on, so a
+ // permission granted or mobile data again reaches the group by itself.
+ // Said while it lasts, over the view the group had.
+ offRefused = transport.addReconnectFailureListener(async (e) => {
+ if (cancelled || e.reason !== 'local_network_blocked') return;
+ const message = await explainLanRefusal();
+ if (cancelled) return;
+ lanLostRef.current = true;
+ setError(message);
+ });
offReconnect = transport.addReconnectListener((reack) => {
if (cancelled) return;
+ if (lanLostRef.current) {
+ lanLostRef.current = false;
+ setError('');
+ setLanBlocked('');
+ }
(async () => {
try {
applyAck(reack);
@@ -721,19 +758,9 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs,
if (err.reason === 'no_keys') setNeedsPass(true);
// An identity sealed before the pepper: only the operator can clear it.
if (err.reason === 'bundle_format_retired') err.message = t('group.bundle_format_retired');
- // This device dropped the node's local addresses (transport.js
- // localNetworkRefused): a setting to change here, not a node to wait
- // for. Android 17 gates it per application, Chrome from 155.
- if (err.reason === 'local_network_blocked' && platform.localNetwork.available) {
- err.message = t('group.lan_blocked_app');
- try {
- const state = await platform.localNetwork.state();
- if (cancelled) return;
- setLanBlocked(state === 'granted' ? '' : state);
- } catch { /* the message stands alone */ }
- } else if (err.reason === 'local_network_blocked') {
- err.message = t(/Android/i.test(navigator.userAgent)
- ? 'group.lan_blocked_android' : 'group.lan_blocked_browser');
+ if (err.reason === 'local_network_blocked') {
+ err.message = await explainLanRefusal();
+ if (cancelled) return;
}
// A key this node has never pinned, for an account it knows. The way in
// is a device already trusted here, not an operator — which is the
@@ -787,6 +814,7 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs,
return () => {
cancelled = true;
if (offReconnect) { offReconnect(); offReconnect = null; }
+ if (offRefused) { offRefused(); offRefused = null; }
clearTimeout(deviceRetry);
// Nothing will update this group's dock row once the page lets go of it.
reportIndexPush(groupId, null);
@@ -814,6 +842,14 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs,
// it; nothing in between should disturb a working connection.
}, [groupId, Boolean(token), retryKey]);
+ // A connection still reconnecting is asked to try now rather than torn down:
+ // what it carries (a download, a film) goes on once it gets through.
+ const retry = useCallback(() => {
+ const transport = transportRef.current;
+ if (transport && transport.reconnecting) transport.retryNow();
+ else setRetryKey((k) => k + 1);
+ }, []);
+
// A group that could not be reached is tried again when the device joins
// another network: from the Wi-Fi to mobile data, or back, is what decides
// whether the node can be reached, and in the Android application there is
@@ -1061,12 +1097,12 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs,
<p class="settings-hint" style="margin-bottom:12px">${t('group.browser_access_off')}</p>`}
${error && html`<div class="error-msg" style="margin-bottom:12px">${error}${' '}
<button class="admin-btn" style="margin-left:8px;font-size:0.9em"
- onClick=${() => setRetryKey(k => k + 1)}>${t('group.retry')}</button>
+ onClick=${retry}>${t('group.retry')}</button>
</div>`}
${lanBlocked === 'prompt' && html`
<button class="admin-btn" style="margin-bottom:12px" onClick=${async () => {
try { await platform.localNetwork.request(); } catch { /* retried either way */ }
- setRetryKey(k => k + 1);
+ retry();
}}>${t('group.lan_allow')}</button>`}
${lanBlocked === 'denied' && html`
<p class="settings-hint">${t('group.lan_settings_hint')}</p>
@@ -1075,7 +1111,7 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs,
const back = () => {
if (document.visibilityState !== 'visible') return;
document.removeEventListener('visibilitychange', back);
- setRetryKey(k => k + 1);
+ retry();
};
document.addEventListener('visibilitychange', back);
platform.localNetwork.openSettings().catch(() => {
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/transport.js b/packages/meshbay-hub/src/meshbay_hub/static/transport.js
index 0b796cab..11088adc 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/transport.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/transport.js
@@ -747,6 +747,9 @@ class MeshBayTransport {
// and a single setter meant the second to arrive silently replaced
// the first, then cleared it on the way out.
this._reconnectListeners = new Set();
+ // Told of each attempt that failed, with its error: a reconnect refused
+ // the local network tries again for ever and says nothing otherwise.
+ this._reconnectFailureListeners = new Set();
this._onNeedToken = null;
// Which device key THIS connection has identified itself to the node with.
// Empty means "not identified": nothing can be sealed, so nothing can be
@@ -762,6 +765,9 @@ class MeshBayTransport {
// connect() like the diagnostic listener above it — this one has to
// survive every reconnect attempt, not restart with each one.
this._reconnectWakeResolve = null;
+ // Set by retryNow for the attempt it gave up: the next waits for nothing.
+ this._retryAtOnce = false;
+ this._abandonAttempt = null;
this._onVisibilityWake = () => {
if (document.visibilityState === 'visible') this._wakeReconnect();
};
@@ -823,6 +829,21 @@ class MeshBayTransport {
}
}
+ /**
+ * Tries again now: the person has just changed what made it fail. An
+ * attempt under way set up its connection before that change (a permission
+ * granted), and Chrome keeps refusing the addresses it dropped then until
+ * ICE gives up, half a minute: it is given up here, and the next starts at
+ * once.
+ */
+ retryNow() {
+ if (this._inReconnectAttempt && this._abandonAttempt) {
+ this._retryAtOnce = true;
+ this._abandonAttempt();
+ }
+ this._wakeReconnect();
+ }
+
get connected() { return this._connected; }
/** True while _reconnectLoop is running, backoff included. */
get reconnecting() { return !!this._reconnectPromise; }
@@ -864,6 +885,17 @@ class MeshBayTransport {
}
/**
+ * Told of each automatic reconnect attempt that failed, with its error, while
+ * the reconnect goes on. A phone that leaves mobile data for the node's Wi-Fi
+ * without the local network permission is refused on every attempt
+ * (`reason` local_network_blocked), and only the page can say why.
+ */
+ addReconnectFailureListener(fn) {
+ this._reconnectFailureListeners.add(fn);
+ return () => this._reconnectFailureListeners.delete(fn);
+ }
+
+ /**
* Told whenever this connection's device identity changes — including to
* *nothing*, which is the case that mattered.
*
@@ -1007,6 +1039,8 @@ class MeshBayTransport {
// DataChannel closed" on the console. Every failed reconnect attempt
// printed one, which is noise in exactly the log a freeze gets read from.
channelReady.catch(() => { /* the awaiter below reports it */ });
+ // retryNow; a no-op once the channel is open.
+ this._abandonAttempt = () => channelReject(new Error('Tried again at once'));
this._channel.onmessage = (event) => this._onMessage(event.data);
const channel = this._channel;
@@ -1698,7 +1732,9 @@ class MeshBayTransport {
this._reconnectAttempts = 0;
while (!this._closed) {
this._reconnectAttempts += 1;
- const delayMs = Math.min(30000, 1000 * 2 ** (this._reconnectAttempts - 1));
+ const delayMs = this._retryAtOnce ? 0
+ : Math.min(30000, 1000 * 2 ** (this._reconnectAttempts - 1));
+ this._retryAtOnce = false;
trace('reconnect_wait', { attempt: this._reconnectAttempts, delay_ms: delayMs });
// Interruptible: _wakeReconnect (fired on visibilitychange → visible)
// resolves this immediately instead of waiting out the rest of a
@@ -1752,6 +1788,11 @@ class MeshBayTransport {
});
console.warn('[MeshBay] Reconnect attempt', this._reconnectAttempts,
'failed:', e.message);
+ for (const fn of [...this._reconnectFailureListeners]) {
+ try { fn(e); } catch (err) {
+ console.error('[MeshBay] reconnect failure listener threw:', err);
+ }
+ }
// Loop again with a longer backoff — closing over `args`/`token`
// freshly next time, in case the token was the actual problem.
}