ws: decouple reconnection from auth internals, use model
This commit is contained in:
@@ -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]);
|
|
||||||
} else {
|
_wsConn = new WebSocket(_wsUrl(), ['Bearer ' + token]);
|
||||||
_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 => {
|
_wsFailCount = 0;
|
||||||
_wsRefreshing = false;
|
await refreshAuth(); // never rejects; failure path handled by model onSuccess
|
||||||
if (ok) {
|
if (getAuthToken()) {
|
||||||
_wsFailCount = 0;
|
_wsReconnectMs = 0;
|
||||||
_wsReconnectMs = 0;
|
_wsConn = null;
|
||||||
_wsConn = null;
|
if (oldConn) oldConn.close();
|
||||||
if (oldConn) oldConn.close();
|
setTimeout(_wsConnect, 100);
|
||||||
setTimeout(_wsConnect, 100);
|
}
|
||||||
} else {
|
// No token after the refresh: onSuccess already cleared storage
|
||||||
redirectLogin();
|
// and redirected to #/login; the no-token guard at the top of
|
||||||
}
|
// onclose stops further reconnect attempts.
|
||||||
}).catch(() => {
|
|
||||||
_wsRefreshing = false;
|
|
||||||
_wsReconnectMs = Math.min(_wsReconnectMs * 2 + 1000, 15000);
|
|
||||||
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user