@glassly/cloud-client 0.3.1-beta.536 → 0.3.1-beta.551
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@glassly/cloud-client",
|
|
3
|
-
"version": "0.3.1-beta.
|
|
3
|
+
"version": "0.3.1-beta.551",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"exports": {
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"test": "bun test"
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"@glassly/cloud-protocol": "0.3.1-beta.
|
|
15
|
+
"@glassly/cloud-protocol": "0.3.1-beta.551",
|
|
16
16
|
"tweetnacl": "^1.0.3"
|
|
17
17
|
},
|
|
18
18
|
"devDependencies": {
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
*/
|
|
27
27
|
import {
|
|
28
28
|
PROTOCOL_MAJOR,
|
|
29
|
+
WS_CLOSE_CODES,
|
|
29
30
|
cloudToClientMessage,
|
|
30
31
|
type ConnectionInit,
|
|
31
32
|
type ConnectionAck,
|
|
@@ -150,10 +151,18 @@ function isUnauthorizedUpgrade(reason: string): boolean {
|
|
|
150
151
|
return /\b401\b|unauthorized/i.test(reason);
|
|
151
152
|
}
|
|
152
153
|
|
|
153
|
-
|
|
154
|
-
|
|
154
|
+
// The code is authoritative; the reason text is kept for runtimes that still
|
|
155
|
+
// close with 1012 (iOS's SocketRocket drops the reason for that code, which is
|
|
156
|
+
// why the server moved to the application range in the first place).
|
|
157
|
+
function isSupersededByNewerSession(code: number, reason: string): boolean {
|
|
158
|
+
return code === WS_CLOSE_CODES.SUPERSEDED || /superseded by newer session/i.test(reason);
|
|
155
159
|
}
|
|
156
160
|
|
|
161
|
+
// A socket that stayed open this long earned a backoff reset. Resetting on
|
|
162
|
+
// the ack itself let a server that closes us right after every handshake be
|
|
163
|
+
// retried at base delay forever.
|
|
164
|
+
const BACKOFF_RESET_AFTER_MS = 30_000;
|
|
165
|
+
|
|
157
166
|
export class Connection {
|
|
158
167
|
private readonly deps: ConnectionDeps;
|
|
159
168
|
private readonly timers: CloudClientTimers;
|
|
@@ -178,14 +187,15 @@ export class Connection {
|
|
|
178
187
|
// stop" from "the network dropped".
|
|
179
188
|
private closedByHost = false;
|
|
180
189
|
|
|
181
|
-
// True when the cloud closed us with
|
|
182
|
-
//
|
|
183
|
-
//
|
|
190
|
+
// True when the cloud closed us with WS_CLOSE_CODES.SUPERSEDED. Another live
|
|
191
|
+
// socket for this user won; reconnecting would just kill it and start a
|
|
192
|
+
// two-client fight. Stays down until the host calls open().
|
|
184
193
|
private replacedByNewerSession = false;
|
|
185
194
|
|
|
186
|
-
// How many consecutive
|
|
187
|
-
// Reset to zero
|
|
195
|
+
// How many consecutive (re)connect attempts, used to grow the backoff.
|
|
196
|
+
// Reset to zero once a handshaken socket has stayed open BACKOFF_RESET_AFTER_MS.
|
|
188
197
|
private reconnectAttempt = 0;
|
|
198
|
+
private backoffResetTimer: unknown | null = null;
|
|
189
199
|
|
|
190
200
|
// The pending reconnect timer, or null when no retry is queued. This single
|
|
191
201
|
// field makes `scheduleReconnect` idempotent: a second call while a timer is
|
|
@@ -285,6 +295,7 @@ export class Connection {
|
|
|
285
295
|
// Cancel any queued reconnect and stop the watchdog: the host wants us down,
|
|
286
296
|
// so neither the loop nor its backstop should bring the socket back up.
|
|
287
297
|
this.clearReconnectTimer();
|
|
298
|
+
this.clearBackoffReset();
|
|
288
299
|
this.stopWatchdog();
|
|
289
300
|
this.failPendingAck(new Error("Connection closed by host"));
|
|
290
301
|
this.teardownSocket();
|
|
@@ -502,7 +513,7 @@ export class Connection {
|
|
|
502
513
|
*/
|
|
503
514
|
private handleAck(ack: ConnectionAck): void {
|
|
504
515
|
this.currentAck = ack;
|
|
505
|
-
this.
|
|
516
|
+
this.armBackoffReset();
|
|
506
517
|
|
|
507
518
|
if (this.pendingAck) {
|
|
508
519
|
this.timers.clearTimeout(this.pendingAck.timer);
|
|
@@ -538,20 +549,22 @@ export class Connection {
|
|
|
538
549
|
new Error(`Socket closed during handshake: ${info.code}`),
|
|
539
550
|
);
|
|
540
551
|
|
|
552
|
+
this.clearBackoffReset();
|
|
553
|
+
|
|
541
554
|
if (this.closedByHost) {
|
|
542
555
|
this.setState("closed");
|
|
543
556
|
return;
|
|
544
557
|
}
|
|
545
558
|
|
|
546
|
-
this.setState("closed");
|
|
547
|
-
|
|
548
559
|
const reason = info.reason || `code ${info.code}`;
|
|
549
|
-
if (isSupersededByNewerSession(reason)) {
|
|
560
|
+
if (isSupersededByNewerSession(info.code, reason)) {
|
|
550
561
|
// Newest-wins on the server. Coming back up here would supersede the
|
|
551
562
|
// winner, which then reconnects, forever. Stay down until open().
|
|
563
|
+
// Flag first: "closed" subscribers read isReplacedByNewerSession.
|
|
552
564
|
this.replacedByNewerSession = true;
|
|
553
565
|
this.clearReconnectTimer();
|
|
554
566
|
this.stopWatchdog();
|
|
567
|
+
this.setState("closed");
|
|
555
568
|
this.deps.logger.info("ws-session-debug staying down after supersede", {
|
|
556
569
|
code: info.code,
|
|
557
570
|
reason,
|
|
@@ -560,6 +573,9 @@ export class Connection {
|
|
|
560
573
|
});
|
|
561
574
|
return;
|
|
562
575
|
}
|
|
576
|
+
|
|
577
|
+
this.setState("closed");
|
|
578
|
+
|
|
563
579
|
if (isUnauthorizedUpgrade(reason)) {
|
|
564
580
|
void Promise.resolve(this.deps.onAuthRejected?.())
|
|
565
581
|
.catch((err) => {
|
|
@@ -632,6 +648,22 @@ export class Connection {
|
|
|
632
648
|
}, delay);
|
|
633
649
|
}
|
|
634
650
|
|
|
651
|
+
/** Reset the backoff once this socket has proven it can stay up. */
|
|
652
|
+
private armBackoffReset(): void {
|
|
653
|
+
this.clearBackoffReset();
|
|
654
|
+
this.backoffResetTimer = this.timers.setTimeout(() => {
|
|
655
|
+
this.backoffResetTimer = null;
|
|
656
|
+
this.reconnectAttempt = 0;
|
|
657
|
+
}, BACKOFF_RESET_AFTER_MS);
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
private clearBackoffReset(): void {
|
|
661
|
+
if (this.backoffResetTimer !== null) {
|
|
662
|
+
this.timers.clearTimeout(this.backoffResetTimer);
|
|
663
|
+
this.backoffResetTimer = null;
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
635
667
|
/** Cancel a queued reconnect, if any. Safe to call when none is pending. */
|
|
636
668
|
private clearReconnectTimer(): void {
|
|
637
669
|
if (this.reconnectTimer !== null) {
|