@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.536",
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.536",
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
- function isSupersededByNewerSession(reason: string): boolean {
154
- return /superseded by newer session/i.test(reason);
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 1012 "superseded by newer session".
182
- // Another live socket for this user won; reconnecting would just kill it
183
- // and start a two-client fight. Stays down until the host calls open().
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 failed (re)connect attempts, used to grow the backoff.
187
- // Reset to zero on a successful handshake.
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.reconnectAttempt = 0;
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) {