fix(web): stop authenticating WebSocket reconnect storm
Console evidence: on the authenticated layout, api.me() opened wss://.../api/ws?token=<JWT> and the browser logged "WebSocket is closed before the connection is established", then an uncaught connection_closed rejection — a self-inflicted reconnect storm. Root cause (two halves): 1. connectedToken was only assigned inside onopen, so during the CONNECTING window every other caller (RealtimeProvider's 2s poll, rpc's whenOpen) saw connectedToken !== token and called openWith again, tearing down the socket it had just dialled. 2. Replacing a socket left its late onclose to run onSocketClosed, which nulled this.socket and connectedToken — clobbering the successor and firing a spurious reconnect + rejected openPromise. Fix: - Set connectedToken the moment we commit to a dial (in openWith), so concurrent callers wait on the same openPromise instead of re-dialling. - Guard onopen/onmessage/onclose with `this.socket !== socket` so a replaced socket's late callbacks are ignored and cannot clobber its successor. Anonymous login escaped this (both tokens null); an authenticated connection (a real JWT) triggered it, matching the reported failure.
This commit is contained in:
+20
-2
@@ -142,12 +142,21 @@ export class RealtimeClient {
|
||||
: websocketUrl();
|
||||
const socket = new WebSocket(url);
|
||||
this.socket = socket;
|
||||
// Record the token of THIS attempt immediately, not only on open. While the
|
||||
// socket is CONNECTING, ``connectedToken`` must already equal the token we
|
||||
// are dialling, so concurrent callers (the provider's poll, another RPC)
|
||||
// wait on ``openPromise`` instead of tearing the socket down and dialling a
|
||||
// second one — which is what produced "closed before established" + a
|
||||
// reconnect storm.
|
||||
this.connectedToken = token;
|
||||
|
||||
this.openPromise = new Promise<void>((resolve, reject) => {
|
||||
let settled = false;
|
||||
socket.onopen = () => {
|
||||
// This socket may have been replaced (token change) before its handshake
|
||||
// finished; if so, its late "open" belongs to a link we already dropped.
|
||||
if (this.socket !== socket) return;
|
||||
settled = true;
|
||||
this.connectedToken = token;
|
||||
this.lastActivityAt = Date.now();
|
||||
this.reconnectAttempts = 0;
|
||||
this.setStatus("live");
|
||||
@@ -160,8 +169,17 @@ export class RealtimeClient {
|
||||
}
|
||||
resolve();
|
||||
};
|
||||
socket.onmessage = (message) => this.handleFrame(message.data);
|
||||
socket.onmessage = (message) => {
|
||||
// Ignore frames from a socket that was already replaced (e.g. a token
|
||||
// change opened a new one): its late frames belong to a dead link.
|
||||
if (this.socket !== socket) return;
|
||||
this.handleFrame(message.data);
|
||||
};
|
||||
socket.onclose = () => {
|
||||
// A replaced socket's late close must not clobber its successor: without
|
||||
// this guard, closing the old socket nulls ``this.socket``/token and
|
||||
// fires a spurious reconnect -- the "closed before established" storm.
|
||||
if (this.socket !== socket) return;
|
||||
if (!settled) {
|
||||
reject(
|
||||
new ApiError(0, "connection_closed", "اتصال پیش از برقراری قطع شد."),
|
||||
|
||||
Reference in New Issue
Block a user