aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub/static/transport.js
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/static/transport.js')
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/transport.js109
1 files changed, 102 insertions, 7 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/transport.js b/packages/meshbay-hub/src/meshbay_hub/static/transport.js
index 0d987d9a..0b796cab 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/transport.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/transport.js
@@ -168,6 +168,53 @@ async function _localNetworkRefusedBy(pc, offerSdp, answerSdp) {
}
}
+// ── Another network ──────────────────────────────────────────────────────────
+//
+// A phone moving between mobile data and the Wi-Fi the node is on keeps a
+// connection whose candidate pair runs over the network it left. ICE says so
+// only once its consent checks lapse, about thirty seconds, and on Android not
+// before the old network has lingered; a connection that had failed was not
+// tried again at all, and the application had to be killed to reach the group.
+
+// How long after the change before anything is tried: the new network's route
+// and addresses take a moment, and a burst of events is one change.
+const NETWORK_SETTLE_MS = 1500;
+// How long a connection that may be on the old network has to answer a ping.
+const NETWORK_PROBE_MS = 5000;
+
+/**
+ * Calls `fn` once the device has moved to another network, and returns the way
+ * to stop. `online` is the end of no network at all; the connection's `type`
+ * (Chrome on Android, and its WebView) is Wi-Fi against mobile data, and its
+ * other changes, a bandwidth estimate, are not news; `meshbay-default-network`
+ * is the Android application's own word for it (MainActivity.watchNetwork).
+ */
+function onNetworkChange(fn) {
+ const connection = (typeof navigator !== 'undefined' && navigator.connection) || null;
+ let type = connection ? connection.type : undefined;
+ let timer = null;
+ const changed = () => {
+ clearTimeout(timer);
+ timer = setTimeout(fn, NETWORK_SETTLE_MS);
+ };
+ const onConnection = () => {
+ if (connection.type === undefined || connection.type === type) return;
+ type = connection.type;
+ changed();
+ };
+ window.addEventListener('online', changed);
+ window.addEventListener('meshbay-default-network', changed);
+ if (connection && connection.addEventListener) connection.addEventListener('change', onConnection);
+ return () => {
+ clearTimeout(timer);
+ window.removeEventListener('online', changed);
+ window.removeEventListener('meshbay-default-network', changed);
+ if (connection && connection.removeEventListener) {
+ connection.removeEventListener('change', onConnection);
+ }
+ };
+}
+
// Segments of 256 KB: 24 in flight is 6 MB, enough to keep playback fed over a
// slow link and small enough that nothing accumulates.
// How long to collect ICE candidates before sending the offer anyway. Long
@@ -719,6 +766,52 @@ class MeshBayTransport {
if (document.visibilityState === 'visible') this._wakeReconnect();
};
document.addEventListener('visibilitychange', this._onVisibilityWake);
+ this._networkProbe = null;
+ this._offNetwork = onNetworkChange(() => this._networkChanged());
+ }
+
+ /**
+ * The device moved to another network. A reconnect that is backing off tries
+ * again at once, on the network just joined; an open session is asked
+ * whether it still answers.
+ */
+ _networkChanged() {
+ if (this._closed) return;
+ trace('network_change', {
+ pc: this._pc?.connectionState, ice: this._pc?.iceConnectionState,
+ session: this._sessionUp, reconnecting: !!this._reconnectPromise,
+ });
+ if (this._reconnectPromise) { this._wakeReconnect(); return; }
+ this._probeSoon('network_change', 0);
+ }
+
+ /** A ping a moment from now; no answer is a lost connection, reconnected. */
+ _probeSoon(why, delayMs = NETWORK_SETTLE_MS) {
+ if (this._closed || !this._sessionUp) return;
+ clearTimeout(this._networkProbe);
+ this._networkProbe = setTimeout(async () => {
+ const pc = this._pc;
+ if (this._closed || !this._sessionUp || this._reconnectPromise) return;
+ try {
+ await this.ping(NETWORK_PROBE_MS);
+ } catch {
+ if (pc !== this._pc || this._closed || this._reconnectPromise) return;
+ trace('probe_lost', { why });
+ console.warn('[MeshBay] No answer after', why, '- reconnecting');
+ this._connectionLost();
+ }
+ }, delayMs);
+ }
+
+ /** The connection is gone: reconnect, and fail what was waiting on it. */
+ _connectionLost() {
+ this._connected = false;
+ this._sessionUp = false;
+ this._reconnect();
+ const err = new Error('WebRTC connection lost');
+ err.name = 'TransportLostError';
+ for (const [, p] of this._pending) p.reject(err);
+ this._pending.clear();
}
/** Cuts short a reconnect currently backing off (see _reconnectLoop). A
@@ -966,13 +1059,7 @@ class MeshBayTransport {
// staying "open" throughout, so every send from here on would simply
// sit out its own timeout instead of failing fast.
if (pc.connectionState === 'failed' && pc === this._pc && !this._closed) {
- this._connected = false;
- this._sessionUp = false;
- this._reconnect();
- const err = new Error('WebRTC connection lost');
- err.name = 'TransportLostError';
- for (const [, p] of this._pending) p.reject(err);
- this._pending.clear();
+ this._connectionLost();
}
};
pc.oniceconnectionstatechange = () => {
@@ -985,6 +1072,11 @@ class MeshBayTransport {
iceReached = true;
if (pc === this._pc) this._noteConnectProgress('ice_connected');
}
+ // Often a moment's loss that clears itself, and as often the network
+ // under the pair gone: a ping tells which long before "failed" does.
+ if (pc.iceConnectionState === 'disconnected' && pc === this._pc) {
+ this._probeSoon('ice_disconnected');
+ }
};
// Diagnostic-only: a periodic health ping and a resume-triggered one, so
@@ -1855,6 +1947,8 @@ class MeshBayTransport {
this._closed = true;
this._closeLanes();
document.removeEventListener('visibilitychange', this._onVisibilityWake);
+ this._offNetwork();
+ clearTimeout(this._networkProbe);
this._wakeReconnect();
if (this._diagCleanup) { this._diagCleanup(); this._diagCleanup = null; }
if (this._channel) this._channel.close();
@@ -2660,4 +2754,5 @@ function extendTransport(part) {
// Export
MeshBayTransport.localNetworkRefused = localNetworkRefused;
+MeshBayTransport.onNetworkChange = onNetworkChange;
window.MeshBayTransport = MeshBayTransport;