ws: decouple reconnection from auth internals, use model

This commit is contained in:
2026-08-15 01:41:09 +00:00
parent 382bbd989b
commit 11a398ce89
+29 -22
View File
@@ -4,15 +4,21 @@
* WebSocket connection manager with auto-reconnect. WS messages are routed * WebSocket connection manager with auto-reconnect. WS messages are routed
* to model-based refresh and direct onMessage handlers. * to model-based refresh and direct onMessage handlers.
* Page-level subscribe/unsubscribe is replaced by the model layer. * Page-level subscribe/unsubscribe is replaced by the model layer.
*
* The JWT is read from the auth model (single source of truth). After 3
* failed close attempts a token refresh is triggered through the auth
* model; the reconnect decision branches on the model's token state —
* never on the refresh promise. Terminal (no-token) transitions are
* handled by the auth model's onSuccess (clears storage, redirects,
* dispatches auth:logout).
*/ */
import { refreshByTopic } from './model.js'; import { refreshByTopic } from './model.js';
import { tryRefreshToken, redirectLogin } from './api.js'; import { refreshAuth, getAuthToken } from './auth_model.js';
let _wsConn = null; let _wsConn = null;
let _wsReconnectMs = 0; let _wsReconnectMs = 0;
let _wsFailCount = 0; let _wsFailCount = 0;
let _wsRefreshing = false;
let _wsClosingHandled = false; let _wsClosingHandled = false;
/** Direct onMessage handlers — { topics, handler, unsubscribed }[] */ /** Direct onMessage handlers — { topics, handler, unsubscribed }[] */
@@ -32,50 +38,43 @@ function _wsUrl() {
/** Attempt a WebSocket connection. /** Attempt a WebSocket connection.
* Passes the JWT in the WebSocket subprotocol header (Sec-WebSocket-Protocol) * Passes the JWT in the WebSocket subprotocol header (Sec-WebSocket-Protocol)
* instead of a query parameter, keeping it out of logs and browser history. * instead of a query parameter, keeping it out of logs and browser history.
* No token: no socket is created — the daemon 401s unauthenticated WS
* connections and connect() only runs while authenticated.
*/ */
function _wsConnect() { function _wsConnect() {
if (_wsConn && _wsConn.readyState <= 1) return; if (_wsConn && _wsConn.readyState <= 1) return;
_wsClosingHandled = false; _wsClosingHandled = false;
const token = window.__auth_token__; const token = getAuthToken();
if (token) { if (!token) return;
_wsConn = new WebSocket(_wsUrl(), ['Bearer ' + token]); _wsConn = new WebSocket(_wsUrl(), ['Bearer ' + token]);
} else {
_wsConn = new WebSocket(_wsUrl());
}
_wsConn.onopen = () => { _wsConn.onopen = () => {
_wsReconnectMs = 0; _wsReconnectMs = 0;
_wsFailCount = 0; _wsFailCount = 0;
_wsRefreshing = false;
_wsClosingHandled = false; _wsClosingHandled = false;
}; };
_wsConn.onclose = () => { _wsConn.onclose = async () => {
if (_wsClosingHandled) return; if (_wsClosingHandled) return;
_wsClosingHandled = true; _wsClosingHandled = true;
if (!window.__auth_token__) return; if (!getAuthToken()) return;
_wsFailCount++; _wsFailCount++;
if (_wsFailCount >= 3 && !_wsRefreshing) { if (_wsFailCount >= 3) {
_wsRefreshing = true;
const oldConn = _wsConn; const oldConn = _wsConn;
tryRefreshToken().then(ok => {
_wsRefreshing = false;
if (ok) {
_wsFailCount = 0; _wsFailCount = 0;
await refreshAuth(); // never rejects; failure path handled by model onSuccess
if (getAuthToken()) {
_wsReconnectMs = 0; _wsReconnectMs = 0;
_wsConn = null; _wsConn = null;
if (oldConn) oldConn.close(); if (oldConn) oldConn.close();
setTimeout(_wsConnect, 100); setTimeout(_wsConnect, 100);
} else {
redirectLogin();
} }
}).catch(() => { // No token after the refresh: onSuccess already cleared storage
_wsRefreshing = false; // and redirected to #/login; the no-token guard at the top of
_wsReconnectMs = Math.min(_wsReconnectMs * 2 + 1000, 15000); // onclose stops further reconnect attempts.
setTimeout(_wsConnect, _wsReconnectMs);
});
return; return;
} }
@@ -153,3 +152,11 @@ export function onMessage(topics, handler) {
export function connect() { export function connect() {
_wsConnect(); _wsConnect();
} }
/** Close the WS socket (terminal auth transition). */
export function disconnect() {
if (_wsConn) {
_wsConn.close();
_wsConn = null;
}
}