viveworker 0.8.2 → 0.8.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -4
- package/package.json +1 -1
- package/scripts/com.viveworker.moltbook-scout.plist.sample +2 -2
- package/scripts/moltbook-api.mjs +178 -2
- package/scripts/moltbook-cli.mjs +134 -8
- package/scripts/moltbook-scout-auto.sh +100 -10
- package/scripts/moltbook-watcher.mjs +10 -0
- package/scripts/share-cli.mjs +127 -54
- package/scripts/viveworker-bridge.mjs +758 -46
- package/web/app.css +33 -0
- package/web/app.js +86 -19
- package/web/build-id.js +1 -1
- package/web/i18n.js +26 -2
- package/web/remote-pairing/api-router.js +168 -15
- package/web/remote-pairing/transport.js +27 -3
|
@@ -13,12 +13,13 @@
|
|
|
13
13
|
* 2. If LAN throws a `TypeError` (network failure, DNS, connect refused —
|
|
14
14
|
* i.e. the bridge isn't reachable), fall back to the relay's
|
|
15
15
|
* `RemotePairingRpcClient.fetch()`.
|
|
16
|
-
* 3. After a LAN failure, enter a
|
|
17
|
-
* requests skip LAN and go straight to relay.
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
16
|
+
* 3. After a LAN failure, enter a sticky relay window where subsequent
|
|
17
|
+
* requests skip LAN and go straight to relay. If LAN was healthy very
|
|
18
|
+
* recently, keep that window short so one transient WiFi wobble doesn't
|
|
19
|
+
* make an in-room phone behave like it is off-LAN for minutes.
|
|
20
|
+
* 4. While sticky relay is active, periodically run a tiny LAN probe. If
|
|
21
|
+
* LAN is back, clear sticky state and close the relay client. If LAN is
|
|
22
|
+
* still dead, the sticky window resets and relay stays the fast path.
|
|
22
23
|
* 5. AbortError (caller cancelled) is never treated as a LAN failure —
|
|
23
24
|
* we re-throw immediately rather than waste a relay attempt.
|
|
24
25
|
*
|
|
@@ -76,11 +77,9 @@ import { loadPairingState } from "./pairing-state.js";
|
|
|
76
77
|
// ---------------------------------------------------------------------------
|
|
77
78
|
|
|
78
79
|
/**
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
* outbound call, short enough that "I just walked back into wifi range"
|
|
83
|
-
* recovers without the user noticing.
|
|
80
|
+
* Hard upper bound after a LAN failure. We keep this for real off-LAN use so
|
|
81
|
+
* cellular/remote sessions do not pay LAN connect timeouts on every request.
|
|
82
|
+
* Recent successful LAN sessions use a much shorter transient window below.
|
|
84
83
|
*/
|
|
85
84
|
const STICKY_RELAY_MS = 5 * 60 * 1000;
|
|
86
85
|
|
|
@@ -99,6 +98,20 @@ const DEFAULT_RELAY_TIMEOUT_MS = 60_000;
|
|
|
99
98
|
*/
|
|
100
99
|
const DEFAULT_LAN_TIMEOUT_MS = 2_500;
|
|
101
100
|
const DEFAULT_STICKY_LAN_PROBE_TIMEOUT_MS = 350;
|
|
101
|
+
// Recover quickly from accidentally sticky relay while still avoiding LAN
|
|
102
|
+
// timeout spam during real off-LAN sessions.
|
|
103
|
+
const AUTO_STICKY_LAN_PROBE_INTERVAL_MS = 8_000;
|
|
104
|
+
const AUTO_STICKY_LAN_PROBE_TIMEOUT_MS = 900;
|
|
105
|
+
const LAN_REACHABILITY_PROBE_TIMEOUT_MS = 700;
|
|
106
|
+
const LAN_REACHABILITY_PROBE_PATH = "/health";
|
|
107
|
+
const RECENT_LAN_OK_GRACE_MS = 60_000;
|
|
108
|
+
const TRANSIENT_LAN_FAILURE_STICKY_MS = 8_000;
|
|
109
|
+
const TRANSIENT_LAN_FAILURE_THRESHOLD = 2;
|
|
110
|
+
// If LAN was healthy moments ago, a single timed-out local request is more
|
|
111
|
+
// likely a busy bridge/Safari wobble than a true off-LAN transition. Delay
|
|
112
|
+
// relay fallback for the first few consecutive failures to avoid burning
|
|
113
|
+
// Cloudflare Durable Object quota while the phone is still on trusted LAN.
|
|
114
|
+
const RECENT_LAN_RELAY_SUPPRESSION_FAILURES = 2;
|
|
102
115
|
const RELAY_FAILURE_WINDOW_MS = 60_000;
|
|
103
116
|
const RELAY_FAILURE_THRESHOLD = 6;
|
|
104
117
|
const RELAY_CIRCUIT_BREAKER_MS = 60_000;
|
|
@@ -132,6 +145,15 @@ let _lastPairingStateStatus = null;
|
|
|
132
145
|
/** Last transport route that completed successfully. */
|
|
133
146
|
let _lastSuccessfulRoute = null;
|
|
134
147
|
|
|
148
|
+
/** Last time a LAN request completed successfully. */
|
|
149
|
+
let _lastLanOkAtMs = 0;
|
|
150
|
+
|
|
151
|
+
/** Last time we tried a LAN probe while sticky relay was active. */
|
|
152
|
+
let _lastStickyLanProbeAtMs = 0;
|
|
153
|
+
|
|
154
|
+
/** Consecutive LAN transport failures since the last LAN success. */
|
|
155
|
+
let _consecutiveLanFailures = 0;
|
|
156
|
+
|
|
135
157
|
/** Recent relay-level failures used to avoid burning relay/DO quota in loops. */
|
|
136
158
|
let _relayFailureAtMs = [];
|
|
137
159
|
|
|
@@ -142,8 +164,10 @@ function newTelemetry() {
|
|
|
142
164
|
return {
|
|
143
165
|
lanOk: 0,
|
|
144
166
|
lanFail: 0,
|
|
167
|
+
lanReachableAfterFailure: 0,
|
|
145
168
|
relayOk: 0,
|
|
146
169
|
relayFail: 0,
|
|
170
|
+
relaySuppressed: 0,
|
|
147
171
|
lastLanFailAt: 0,
|
|
148
172
|
lastRelayFailAt: 0,
|
|
149
173
|
clientResets: 0,
|
|
@@ -440,6 +464,29 @@ function relayCircuitError(opts = {}) {
|
|
|
440
464
|
return err;
|
|
441
465
|
}
|
|
442
466
|
|
|
467
|
+
function shouldAutoProbeLanWhileSticky(now, opts = {}) {
|
|
468
|
+
if (opts.autoProbeLanWhileSticky === false) {
|
|
469
|
+
return false;
|
|
470
|
+
}
|
|
471
|
+
if (relayCircuitDelayMs(opts) > 0) {
|
|
472
|
+
return false;
|
|
473
|
+
}
|
|
474
|
+
return now - _lastStickyLanProbeAtMs >= AUTO_STICKY_LAN_PROBE_INTERVAL_MS;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function stickyLanProbeOpts(opts, autoProbe) {
|
|
478
|
+
if (!autoProbe) {
|
|
479
|
+
return opts;
|
|
480
|
+
}
|
|
481
|
+
if (opts.stickyLanProbeTimeoutMs != null) {
|
|
482
|
+
return opts;
|
|
483
|
+
}
|
|
484
|
+
return {
|
|
485
|
+
...opts,
|
|
486
|
+
stickyLanProbeTimeoutMs: AUTO_STICKY_LAN_PROBE_TIMEOUT_MS,
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
|
|
443
490
|
function recordRelayFailure(err, opts = {}) {
|
|
444
491
|
const now = nowMs(opts);
|
|
445
492
|
_relayFailureAtMs = _relayFailureAtMs.filter((at) => now - at <= RELAY_FAILURE_WINDOW_MS);
|
|
@@ -554,8 +601,11 @@ async function attemptLanFetch(url, init, opts) {
|
|
|
554
601
|
? await Promise.race([fetchPromise, timeoutPromise])
|
|
555
602
|
: await fetchPromise;
|
|
556
603
|
_telemetry.lanOk++;
|
|
604
|
+
_lastLanOkAtMs = nowMs(opts);
|
|
605
|
+
_consecutiveLanFailures = 0;
|
|
557
606
|
_lastSuccessfulRoute = "lan";
|
|
558
607
|
_stickyRelayUntilMs = 0;
|
|
608
|
+
_lastStickyLanProbeAtMs = 0;
|
|
559
609
|
resetRelayFailureCircuit();
|
|
560
610
|
closeRelayClient("lan connected");
|
|
561
611
|
emitRoutingStatus("lan-connected", opts, { url: String(url || "") });
|
|
@@ -566,12 +616,17 @@ async function attemptLanFetch(url, init, opts) {
|
|
|
566
616
|
throw err;
|
|
567
617
|
}
|
|
568
618
|
const lanErr = didTimeout ? new TypeError("LAN fetch timed out") : err;
|
|
619
|
+
const failedAt = nowMs(opts);
|
|
569
620
|
_telemetry.lanFail++;
|
|
570
|
-
|
|
571
|
-
|
|
621
|
+
_consecutiveLanFailures++;
|
|
622
|
+
_telemetry.lastLanFailAt = failedAt;
|
|
623
|
+
_lastStickyLanProbeAtMs = failedAt;
|
|
624
|
+
_stickyRelayUntilMs = failedAt + stickyRelayWindowAfterLanFailure(failedAt);
|
|
572
625
|
emitRoutingStatus("lan-failed", opts, {
|
|
573
626
|
url: String(url || ""),
|
|
574
627
|
reason: lanErr?.message || String(lanErr),
|
|
628
|
+
consecutiveLanFailures: _consecutiveLanFailures,
|
|
629
|
+
stickyRelayMs: Math.max(0, _stickyRelayUntilMs - failedAt),
|
|
575
630
|
});
|
|
576
631
|
return { ok: false, err: lanErr };
|
|
577
632
|
} finally {
|
|
@@ -582,8 +637,71 @@ async function attemptLanFetch(url, init, opts) {
|
|
|
582
637
|
}
|
|
583
638
|
}
|
|
584
639
|
|
|
640
|
+
function stickyRelayWindowAfterLanFailure(now) {
|
|
641
|
+
const recentLanOk = _lastLanOkAtMs > 0 && now - _lastLanOkAtMs <= RECENT_LAN_OK_GRACE_MS;
|
|
642
|
+
if (recentLanOk && _consecutiveLanFailures <= TRANSIENT_LAN_FAILURE_THRESHOLD) {
|
|
643
|
+
return TRANSIENT_LAN_FAILURE_STICKY_MS;
|
|
644
|
+
}
|
|
645
|
+
return STICKY_RELAY_MS;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
function shouldDelayRelayAfterRecentLanFailure(now, opts = {}) {
|
|
649
|
+
if (opts.delayRelayAfterRecentLanFailure === false) {
|
|
650
|
+
return false;
|
|
651
|
+
}
|
|
652
|
+
if (_lastLanOkAtMs <= 0 || now - _lastLanOkAtMs > RECENT_LAN_OK_GRACE_MS) {
|
|
653
|
+
return false;
|
|
654
|
+
}
|
|
655
|
+
return _consecutiveLanFailures > 0 &&
|
|
656
|
+
_consecutiveLanFailures <= RECENT_LAN_RELAY_SUPPRESSION_FAILURES;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
function suppressRelayAfterRecentLanFailure(url, err, opts = {}) {
|
|
660
|
+
_telemetry.relaySuppressed++;
|
|
661
|
+
emitRoutingStatus("remote-delayed", opts, {
|
|
662
|
+
url: String(url || ""),
|
|
663
|
+
reason: err?.message || String(err || "LAN failure"),
|
|
664
|
+
consecutiveLanFailures: _consecutiveLanFailures,
|
|
665
|
+
recentLanOkAtMs: _lastLanOkAtMs,
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
function shouldConfirmLanReachability(now, opts = {}) {
|
|
670
|
+
if (opts.confirmLanReachabilityBeforeRelay === false) {
|
|
671
|
+
return false;
|
|
672
|
+
}
|
|
673
|
+
return _lastLanOkAtMs > 0 && now - _lastLanOkAtMs <= RECENT_LAN_OK_GRACE_MS;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
async function confirmLanUnavailableBeforeRelay(url, err, opts = {}) {
|
|
677
|
+
if (!shouldConfirmLanReachability(nowMs(opts), opts)) {
|
|
678
|
+
return false;
|
|
679
|
+
}
|
|
680
|
+
const path = urlToRelayPath(url);
|
|
681
|
+
if (path === LAN_REACHABILITY_PROBE_PATH) {
|
|
682
|
+
return false;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
const probe = await attemptLanFetch(LAN_REACHABILITY_PROBE_PATH, {
|
|
686
|
+
cache: "no-store",
|
|
687
|
+
credentials: "same-origin",
|
|
688
|
+
headers: { Accept: "application/json" },
|
|
689
|
+
}, {
|
|
690
|
+
...opts,
|
|
691
|
+
lanTimeoutMs: opts.lanReachabilityProbeTimeoutMs ?? LAN_REACHABILITY_PROBE_TIMEOUT_MS,
|
|
692
|
+
});
|
|
693
|
+
if (!probe.ok) {
|
|
694
|
+
return false;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
_telemetry.lanReachableAfterFailure++;
|
|
698
|
+
suppressRelayAfterRecentLanFailure(url, err, opts);
|
|
699
|
+
return true;
|
|
700
|
+
}
|
|
701
|
+
|
|
585
702
|
async function attemptStickyLanProbe(url, init, opts) {
|
|
586
703
|
const probeTimeoutMs = opts.stickyLanProbeTimeoutMs ?? DEFAULT_STICKY_LAN_PROBE_TIMEOUT_MS;
|
|
704
|
+
_lastStickyLanProbeAtMs = nowMs(opts);
|
|
587
705
|
const lan = await attemptLanFetch(url, init, {
|
|
588
706
|
...opts,
|
|
589
707
|
lanTimeoutMs: probeTimeoutMs,
|
|
@@ -859,7 +977,11 @@ function nowMs(opts) {
|
|
|
859
977
|
* suppressRoutingStatus?: boolean,
|
|
860
978
|
* preferRelayError?: boolean,
|
|
861
979
|
* probeLanWhileSticky?: boolean,
|
|
980
|
+
* autoProbeLanWhileSticky?: boolean,
|
|
981
|
+
* delayRelayAfterRecentLanFailure?: boolean,
|
|
982
|
+
* confirmLanReachabilityBeforeRelay?: boolean,
|
|
862
983
|
* stickyLanProbeTimeoutMs?: number,
|
|
984
|
+
* lanReachabilityProbeTimeoutMs?: number,
|
|
863
985
|
* }} [opts]
|
|
864
986
|
* @returns {Promise<{
|
|
865
987
|
* ok: boolean,
|
|
@@ -875,9 +997,24 @@ export async function routedFetch(url, init = {}, opts = {}) {
|
|
|
875
997
|
|
|
876
998
|
// Sticky-relay path: LAN just failed, prefer relay for a while.
|
|
877
999
|
if (_stickyRelayUntilMs > t) {
|
|
878
|
-
if (opts
|
|
879
|
-
const lan = await
|
|
1000
|
+
if (shouldDelayRelayAfterRecentLanFailure(t, opts)) {
|
|
1001
|
+
const lan = await attemptLanFetch(url, init, opts);
|
|
1002
|
+
if (lan.ok) return lan.response;
|
|
1003
|
+
if (shouldDelayRelayAfterRecentLanFailure(nowMs(opts), opts)) {
|
|
1004
|
+
suppressRelayAfterRecentLanFailure(url, lan.err, opts);
|
|
1005
|
+
throw lan.err;
|
|
1006
|
+
}
|
|
1007
|
+
if (await confirmLanUnavailableBeforeRelay(url, lan.err, opts)) {
|
|
1008
|
+
throw lan.err;
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
const autoProbe = shouldAutoProbeLanWhileSticky(t, opts);
|
|
1012
|
+
if (opts.probeLanWhileSticky === true || autoProbe) {
|
|
1013
|
+
const lan = await attemptStickyLanProbe(url, init, stickyLanProbeOpts(opts, autoProbe));
|
|
880
1014
|
if (lan.ok) return lan.response;
|
|
1015
|
+
if (await confirmLanUnavailableBeforeRelay(url, lan.err, opts)) {
|
|
1016
|
+
throw lan.err;
|
|
1017
|
+
}
|
|
881
1018
|
}
|
|
882
1019
|
emitRoutingStatus("remote-switching", opts, { url: String(url || ""), sticky: true });
|
|
883
1020
|
const r = await attemptRelayFetch(url, init, opts);
|
|
@@ -897,6 +1034,13 @@ export async function routedFetch(url, init = {}, opts = {}) {
|
|
|
897
1034
|
// Happy path: LAN first.
|
|
898
1035
|
const lan = await attemptLanFetch(url, init, opts);
|
|
899
1036
|
if (lan.ok) return lan.response;
|
|
1037
|
+
if (shouldDelayRelayAfterRecentLanFailure(nowMs(opts), opts)) {
|
|
1038
|
+
suppressRelayAfterRecentLanFailure(url, lan.err, opts);
|
|
1039
|
+
throw lan.err;
|
|
1040
|
+
}
|
|
1041
|
+
if (await confirmLanUnavailableBeforeRelay(url, lan.err, opts)) {
|
|
1042
|
+
throw lan.err;
|
|
1043
|
+
}
|
|
900
1044
|
|
|
901
1045
|
// Try relay once before giving up.
|
|
902
1046
|
emitRoutingStatus("remote-switching", opts, { url: String(url || ""), sticky: false });
|
|
@@ -918,6 +1062,9 @@ export function __getTelemetry() {
|
|
|
918
1062
|
..._telemetry,
|
|
919
1063
|
stickyRelayUntilMs: _stickyRelayUntilMs,
|
|
920
1064
|
lastRoute: _lastSuccessfulRoute,
|
|
1065
|
+
lastLanOkAtMs: _lastLanOkAtMs,
|
|
1066
|
+
lastStickyLanProbeAtMs: _lastStickyLanProbeAtMs,
|
|
1067
|
+
consecutiveLanFailures: _consecutiveLanFailures,
|
|
921
1068
|
hasClient: Boolean(_client),
|
|
922
1069
|
clientPairingKey: _clientPairingKey,
|
|
923
1070
|
relayCircuitOpenUntilMs: _relayCircuitOpenUntilMs,
|
|
@@ -946,6 +1093,9 @@ export function __resetForTest() {
|
|
|
946
1093
|
_telemetry = newTelemetry();
|
|
947
1094
|
_lastPairingStateStatus = null;
|
|
948
1095
|
_lastSuccessfulRoute = null;
|
|
1096
|
+
_lastLanOkAtMs = 0;
|
|
1097
|
+
_lastStickyLanProbeAtMs = 0;
|
|
1098
|
+
_consecutiveLanFailures = 0;
|
|
949
1099
|
_relayFailureAtMs = [];
|
|
950
1100
|
_relayCircuitOpenUntilMs = 0;
|
|
951
1101
|
}
|
|
@@ -953,5 +1103,8 @@ export function __resetForTest() {
|
|
|
953
1103
|
// Test-visible constants for tests that want to assert behavior at the
|
|
954
1104
|
// sticky-window boundary without hard-coding 5 minutes in two places.
|
|
955
1105
|
export const __STICKY_RELAY_MS = STICKY_RELAY_MS;
|
|
1106
|
+
export const __AUTO_STICKY_LAN_PROBE_INTERVAL_MS = AUTO_STICKY_LAN_PROBE_INTERVAL_MS;
|
|
1107
|
+
export const __TRANSIENT_LAN_FAILURE_STICKY_MS = TRANSIENT_LAN_FAILURE_STICKY_MS;
|
|
956
1108
|
export const __DEFAULT_RELAY_TIMEOUT_MS = DEFAULT_RELAY_TIMEOUT_MS;
|
|
957
1109
|
export const __DEFAULT_LAN_TIMEOUT_MS = DEFAULT_LAN_TIMEOUT_MS;
|
|
1110
|
+
export const __RECENT_LAN_RELAY_SUPPRESSION_FAILURES = RECENT_LAN_RELAY_SUPPRESSION_FAILURES;
|
|
@@ -88,6 +88,7 @@ const DEFAULT_FAILURE_WINDOW_MS = 60_000;
|
|
|
88
88
|
const DEFAULT_FAILURE_THRESHOLD = 4;
|
|
89
89
|
const DEFAULT_CIRCUIT_BREAKER_MS = 60_000;
|
|
90
90
|
const DEFAULT_MAX_CIRCUIT_BREAKER_MS = 10 * 60_000;
|
|
91
|
+
const DEFAULT_STABLE_CONNECTION_MS = 15_000;
|
|
91
92
|
const DEFAULT_PROLOGUE = new TextEncoder().encode("viveworker/remote-pairing/v1");
|
|
92
93
|
|
|
93
94
|
// CloseEvent codes we emit. 1000 is normal; 4xxx is application-defined.
|
|
@@ -121,6 +122,7 @@ const CLOSE_RELAY_RESET_SESSION = 4004;
|
|
|
121
122
|
* @property {number} [failureThreshold]
|
|
122
123
|
* @property {number} [circuitBreakerMs]
|
|
123
124
|
* @property {number} [maxCircuitBreakerMs]
|
|
125
|
+
* @property {number} [stableConnectionMs]
|
|
124
126
|
* @property {typeof WebSocket} [WebSocketImpl] injectable for Node-side tests
|
|
125
127
|
* @property {{debug?: Function, warn?: Function}} [logger]
|
|
126
128
|
*/
|
|
@@ -166,6 +168,7 @@ export class RemotePairingTransport {
|
|
|
166
168
|
this._failureThreshold = opts.failureThreshold ?? DEFAULT_FAILURE_THRESHOLD;
|
|
167
169
|
this._circuitBreakerMs = opts.circuitBreakerMs ?? DEFAULT_CIRCUIT_BREAKER_MS;
|
|
168
170
|
this._maxCircuitBreakerMs = opts.maxCircuitBreakerMs ?? DEFAULT_MAX_CIRCUIT_BREAKER_MS;
|
|
171
|
+
this._stableConnectionMs = opts.stableConnectionMs ?? DEFAULT_STABLE_CONNECTION_MS;
|
|
169
172
|
|
|
170
173
|
this._WebSocketImpl = opts.WebSocketImpl ?? globalThis.WebSocket;
|
|
171
174
|
if (typeof this._WebSocketImpl !== "function") {
|
|
@@ -200,6 +203,8 @@ export class RemotePairingTransport {
|
|
|
200
203
|
this._pingTimer = null;
|
|
201
204
|
/** @type {ReturnType<typeof setTimeout> | null} */
|
|
202
205
|
this._handshakeTimer = null;
|
|
206
|
+
/** @type {ReturnType<typeof setTimeout> | null} */
|
|
207
|
+
this._stableConnectionTimer = null;
|
|
203
208
|
/** @type {number[]} recent reconnect-triggering failures. */
|
|
204
209
|
this._recentFailureAtMs = [];
|
|
205
210
|
/** If > Date.now(), reconnect attempts are intentionally delayed. */
|
|
@@ -317,6 +322,7 @@ export class RemotePairingTransport {
|
|
|
317
322
|
this._closed = true;
|
|
318
323
|
this._started = false;
|
|
319
324
|
this._cancelReconnectTimer();
|
|
325
|
+
this._cancelStableConnectionTimer();
|
|
320
326
|
this._stopPing();
|
|
321
327
|
this._stopHandshakeTimer();
|
|
322
328
|
if (this._ws) {
|
|
@@ -682,6 +688,24 @@ export class RemotePairingTransport {
|
|
|
682
688
|
}
|
|
683
689
|
}
|
|
684
690
|
|
|
691
|
+
_scheduleStableConnectionReset() {
|
|
692
|
+
const delay = Math.max(0, Number(this._stableConnectionMs) || 0);
|
|
693
|
+
this._stableConnectionTimer = setTimeout(() => {
|
|
694
|
+
this._stableConnectionTimer = null;
|
|
695
|
+
if (this._closed || this._state !== STATE.CONNECTED) return;
|
|
696
|
+
this._recentFailureAtMs = [];
|
|
697
|
+
this._circuitOpenUntilMs = 0;
|
|
698
|
+
this._circuitOpenCount = 0;
|
|
699
|
+
}, delay);
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
_cancelStableConnectionTimer() {
|
|
703
|
+
if (this._stableConnectionTimer != null) {
|
|
704
|
+
clearTimeout(this._stableConnectionTimer);
|
|
705
|
+
this._stableConnectionTimer = null;
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
|
|
685
709
|
_startPing() {
|
|
686
710
|
this._stopPing();
|
|
687
711
|
this._pingTimer = setInterval(() => {
|
|
@@ -742,11 +766,10 @@ export class RemotePairingTransport {
|
|
|
742
766
|
if (this._state === newState) return;
|
|
743
767
|
const prev = this._state;
|
|
744
768
|
this._state = newState;
|
|
769
|
+
this._cancelStableConnectionTimer();
|
|
745
770
|
if (newState === STATE.CONNECTED) {
|
|
746
771
|
this._reconnectAttempt = 0;
|
|
747
|
-
this.
|
|
748
|
-
this._circuitOpenUntilMs = 0;
|
|
749
|
-
this._circuitOpenCount = 0;
|
|
772
|
+
this._scheduleStableConnectionReset();
|
|
750
773
|
}
|
|
751
774
|
try {
|
|
752
775
|
this._onStateChange(newState, prev, info);
|
|
@@ -771,6 +794,7 @@ export class RemotePairingTransport {
|
|
|
771
794
|
this._stopPing();
|
|
772
795
|
this._stopHandshakeTimer();
|
|
773
796
|
this._cancelReconnectTimer();
|
|
797
|
+
this._cancelStableConnectionTimer();
|
|
774
798
|
if (this._ws) {
|
|
775
799
|
try { this._ws.close(CLOSE_FATAL, "fatal-error"); } catch {}
|
|
776
800
|
this._ws = null;
|