summaryrefslogtreecommitdiffstats
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.js87
1 files changed, 71 insertions, 16 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/transport.js b/packages/meshbay-hub/src/meshbay_hub/static/transport.js
index f6adaac..79cd8e1 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/transport.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/transport.js
@@ -59,6 +59,57 @@ const UPLOAD_BUFFER_HIGH = 1024 * 1024;
// that never answers costs a pause rather than the whole attempt.
const ICE_GATHER_TIMEOUT_MS = 4000;
+// Answers to an offer that are about the hub's load, never about the node: 429
+// is one of the hub's per-account ceilings (docs/MESHBAY_DESIGN.md §7.2), and
+// 502/503 is the hub restarting behind its proxy. An offer refused for either is
+// sent again, so it is not reported as a node that cannot be reached — which is
+// what a single 429 used to become in Search, for a group whose node was
+// answering every other offer in under a second. Anything else — 404 for a node
+// that is not connected, 403, 504 for a node that did not answer — fails at once:
+// retrying it would make a dead node cost time instead of costing nothing.
+const OFFER_RETRY_STATUSES = new Set([429, 502, 503]);
+// Worst case about twenty seconds of waiting, all of it on a hub that is
+// answering. Search's own deadline treats each retry as progress and is still
+// bounded by its ceiling.
+const OFFER_RETRY_DELAYS_MS = [500, 1000, 2000, 4000, 8000];
+const OFFER_RETRY_AFTER_MAX_MS = 10000;
+
+/**
+ * POST an offer, sending it again while the hub refuses it for load.
+ *
+ * `Retry-After` is honoured when the hub gives one, and every delay is jittered:
+ * the refusals this exists for come in bursts — several groups dialled at once,
+ * or every connection of a phone reconnecting as it wakes — and retries that
+ * all land on the same millisecond would be refused together again.
+ *
+ * `isClosed` is asked after each wait, so a caller that gave up (Search's
+ * deadline, a page that unmounted) sends no further offer on its behalf.
+ */
+async function postOffer(call, url, init, { isClosed, onRetry } = {}) {
+ for (let attempt = 0; ; attempt += 1) {
+ const resp = await call(url, init);
+ if (resp.ok) return resp;
+ if (!OFFER_RETRY_STATUSES.has(resp.status) || attempt >= OFFER_RETRY_DELAYS_MS.length) {
+ const detail = await resp.json().catch(() => ({}));
+ const err = new Error(`Signaling failed: ${resp.status} ${detail.detail || ''}`);
+ err.status = resp.status;
+ throw err;
+ }
+ const after = Number(resp.headers && resp.headers.get && resp.headers.get('Retry-After'));
+ const base = after > 0
+ ? Math.min(after * 1000, OFFER_RETRY_AFTER_MAX_MS)
+ : OFFER_RETRY_DELAYS_MS[attempt];
+ const delay = Math.round(base * (0.75 + Math.random() * 0.5));
+ if (onRetry) onRetry(resp.status, delay, attempt + 1);
+ await new Promise((resolve) => setTimeout(resolve, delay));
+ if (isClosed && isClosed()) {
+ const err = new Error('Transport closed while the hub was busy');
+ err.status = resp.status;
+ throw err;
+ }
+ }
+}
+
const STREAM_CREDITS = 24;
function _aborted() {
@@ -823,23 +874,27 @@ class MeshBayTransport {
// app:// origin is refused by CORS.
const call = (window.MeshBayPlatform && window.MeshBayPlatform.apiFetch)
|| fetch;
- const resp = await call(
+ const resp = await postOffer(call,
`${this._hubUrl}/v1/nodes/${nodeId}/webrtc/offer`, {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- 'Authorization': `Bearer ${this._accessToken}`,
- },
- body: JSON.stringify({
- sdp: this._pc.localDescription.sdp,
- ice_candidates: [],
- }),
- });
-
- if (!resp.ok) {
- const detail = await resp.json().catch(() => ({}));
- throw new Error(`Signaling failed: ${resp.status} ${detail.detail || ''}`);
- }
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': `Bearer ${this._accessToken}`,
+ },
+ body: JSON.stringify({
+ sdp: this._pc.localDescription.sdp,
+ ice_candidates: [],
+ }),
+ }, {
+ isClosed: () => this._closed,
+ onRetry: (status, delayMs, attempt) => {
+ console.warn('[MeshBay] Hub refused the offer with', status,
+ '— sending it again in', delayMs, 'ms (attempt', attempt, ')');
+ trace('offer_retry', { status, delay_ms: delayMs, attempt });
+ // The hub answered: a busy hub, not a silent node.
+ this._noteConnectProgress('offer_retry');
+ },
+ });
const answer = await resp.json();
this._rawAnswerSdp = answer.sdp;