@pylonsync/sync 0.3.177 → 0.3.178

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/index.ts +30 -0
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "0.3.177",
6
+ "version": "0.3.178",
7
7
  "type": "module",
8
8
  "main": "src/index.ts",
9
9
  "types": "src/index.ts",
package/src/index.ts CHANGED
@@ -714,6 +714,7 @@ export class SyncEngine {
714
714
  * the client can't hammer the server on auth failures.
715
715
  */
716
716
  private wsStableTimer: ReturnType<typeof setTimeout> | null = null;
717
+ private pingTimer: ReturnType<typeof setInterval> | null = null;
717
718
 
718
719
  /**
719
720
  * Registered consumers for binary WebSocket frames. SyncEngine itself
@@ -986,6 +987,10 @@ export class SyncEngine {
986
987
  clearInterval(this.pollTimer);
987
988
  this.pollTimer = null;
988
989
  }
990
+ if (this.pingTimer) {
991
+ clearInterval(this.pingTimer);
992
+ this.pingTimer = null;
993
+ }
989
994
  if (this.visibilityHandler && typeof document !== "undefined") {
990
995
  document.removeEventListener("visibilitychange", this.visibilityHandler);
991
996
  this.visibilityHandler = null;
@@ -1033,6 +1038,27 @@ export class SyncEngine {
1033
1038
  this.reconnectAttempts = 0;
1034
1039
  this.wsStableTimer = null;
1035
1040
  }, 5_000);
1041
+ // Client-side keepalive ping. The server's per-client reader
1042
+ // thread blocks on a synchronous WS read for as long as no client
1043
+ // message arrives. On the HTTP-multiplexed `/api/sync/ws` path
1044
+ // tiny_http doesn't expose stream-level read timeouts, so the
1045
+ // reader's mutex hold is bounded only by client activity. Without
1046
+ // these pings the broadcaster contends for the same mutex and
1047
+ // wedges — Insert events never reach the tab. A 1s cadence makes
1048
+ // worst-case broadcast latency ~1s even when the user is idle.
1049
+ // Browsers don't expose WebSocket-level PING frames; a JSON
1050
+ // payload with type:"ping" achieves the same effect (the server
1051
+ // reads, looks up an unknown "ping" type, loops back releasing
1052
+ // the mutex; broadcaster grabs it during the gap).
1053
+ if (this.pingTimer) clearInterval(this.pingTimer);
1054
+ this.pingTimer = setInterval(() => {
1055
+ if (this.ws?.readyState !== WebSocket.OPEN) return;
1056
+ try {
1057
+ this.ws.send('{"type":"ping"}');
1058
+ } catch {
1059
+ // ignore — onclose will trigger reconnect
1060
+ }
1061
+ }, 1_000);
1036
1062
  // Re-send any active CRDT subscriptions across the new socket.
1037
1063
  // The server purged them on disconnect (`unsubscribe_all`), so
1038
1064
  // without this resync a tab that was subscribed before a network
@@ -1162,6 +1188,10 @@ export class SyncEngine {
1162
1188
  clearTimeout(this.wsStableTimer);
1163
1189
  this.wsStableTimer = null;
1164
1190
  }
1191
+ if (this.pingTimer) {
1192
+ clearInterval(this.pingTimer);
1193
+ this.pingTimer = null;
1194
+ }
1165
1195
  // Surface the disconnect to UI consumers immediately. If
1166
1196
  // `running` flipped to false (engine stopped), `stop()` already
1167
1197
  // set "offline" — don't override that.