feat(frontend): polish web (logout, 401→pairing, reconnexion globale) (#13)
Flow logout : POST /api/logout puis retour à l'écran de pairing et déconnexion du live. 401 renvoie au pairing sans boucle. Bannière de reconnexion couvrant le cas live-only et le serveur indisponible. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -78,6 +78,14 @@ export interface WsLiveClientConfig {
|
||||
setTimeoutImpl?: SetTimeoutLike;
|
||||
/** Injected `clearTimeout`. */
|
||||
clearTimeoutImpl?: ClearTimeoutLike;
|
||||
/**
|
||||
* Called after a reconnection attempt fails. The browser cannot read the HTTP
|
||||
* status of a rejected WS upgrade, so an auth-revoked socket looks like any
|
||||
* other outage from here. F6 wires this to a cheap HTTP probe (`health`): a
|
||||
* `401` there routes back to pairing (via the invoker's `onUnauthorized`),
|
||||
* while a network error just lets the backoff keep retrying.
|
||||
*/
|
||||
onReconnectFailed?: () => void;
|
||||
}
|
||||
|
||||
/** A live terminal subscription tracked for output routing + reconnection replay. */
|
||||
@ -137,6 +145,7 @@ export class WsLiveClient {
|
||||
private readonly reconnectMaxMs: number;
|
||||
private readonly setTimeoutImpl: SetTimeoutLike;
|
||||
private readonly clearTimeoutImpl: ClearTimeoutLike;
|
||||
private readonly onReconnectFailed?: () => void;
|
||||
|
||||
private socket: WebSocketLike | null = null;
|
||||
private opening: Promise<void> | null = null;
|
||||
@ -172,6 +181,7 @@ export class WsLiveClient {
|
||||
config.setTimeoutImpl ?? ((fn, ms) => setTimeout(fn, ms));
|
||||
this.clearTimeoutImpl =
|
||||
config.clearTimeoutImpl ?? ((h) => clearTimeout(h as ReturnType<typeof setTimeout>));
|
||||
this.onReconnectFailed = config.onReconnectFailed;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
@ -274,7 +284,7 @@ export class WsLiveClient {
|
||||
}
|
||||
|
||||
/** Whether the socket should auto-reconnect (a terminal or a live subscription). */
|
||||
private needsReconnect(): boolean {
|
||||
needsReconnect(): boolean {
|
||||
return this.terminals.size > 0 || this.domainEventHandler !== null;
|
||||
}
|
||||
|
||||
@ -297,7 +307,10 @@ export class WsLiveClient {
|
||||
await this.connect();
|
||||
await this.reattachAll();
|
||||
} catch {
|
||||
// Still down: back off and retry (unless nothing needs the socket anymore).
|
||||
// Still down: probe (an auth-revoked upgrade is indistinguishable from an
|
||||
// outage here — the probe surfaces a 401 as a pairing bounce), then back
|
||||
// off and retry (unless nothing needs the socket anymore).
|
||||
this.onReconnectFailed?.();
|
||||
if (this.needsReconnect()) this.scheduleReconnect();
|
||||
}
|
||||
}
|
||||
@ -479,6 +492,37 @@ export class WsLiveClient {
|
||||
return bytesToBase64(bytes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Soft teardown for a sign-out / auth bounce (F6): closes the socket, drops all
|
||||
* live state and stops the reconnect loop, but — unlike {@link dispose} — leaves
|
||||
* the client reusable. A later `ensureConnected` (after re-pairing) reconnects
|
||||
* as a fresh `connecting`. The socket's own `onclose` is detached first so it
|
||||
* cannot flip the state back to `reconnecting`.
|
||||
*/
|
||||
disconnect(): void {
|
||||
if (this.reconnectHandle) {
|
||||
this.clearTimeoutImpl(this.reconnectHandle);
|
||||
this.reconnectHandle = null;
|
||||
}
|
||||
this.rejectAllPending({ code: "WS_CLOSED", message: "client disconnected" });
|
||||
this.outputSinks.clear();
|
||||
this.terminals.clear();
|
||||
this.domainEventHandler = null;
|
||||
const socket = this.socket;
|
||||
if (socket) {
|
||||
socket.onopen = null;
|
||||
socket.onmessage = null;
|
||||
socket.onerror = null;
|
||||
socket.onclose = null;
|
||||
socket.close();
|
||||
}
|
||||
this.socket = null;
|
||||
this.opening = null;
|
||||
this.everConnected = false;
|
||||
this.reconnectAttempt = 0;
|
||||
this.setConnState("closed");
|
||||
}
|
||||
|
||||
/** Closes the socket and rejects everything pending. */
|
||||
dispose(): void {
|
||||
this.disposed = true;
|
||||
|
||||
Reference in New Issue
Block a user