Files
noitu/web/src/lib/ws/connection.svelte.js
T
tiennm99 90cd679639 feat(web): state the rules, reach the chat from the top of the board, lint the tree
A /rules page says, in one place, what nothing in the app said before:
the chain rule, the clock, what a dead end costs, elimination and the last
player standing, how a word is scored and the reconnect window. Linked
from the landing page and from the board and lobby headers.

The chat control moves above the chain as a pill with the unread count,
where it can be reached on a phone mid-game, and the lobby's folded chat
now carries an unread badge too.

ESLint with the Svelte and JSDoc plugins, run in CI; the real findings
it turned up (missing each keys, untyped timer handles) are fixed.
2026-09-21 01:17:52 +07:00

91 lines
2.7 KiB
JavaScript

import { Status, createClient, hasStoredSession } from './client.js';
import { game } from '$lib/stores/game.svelte.js';
import { settings } from '$lib/stores/settings.svelte.js';
/**
* One socket for the whole app.
*
* The client itself is framework-agnostic and injectable, which is what makes
* it testable; this module is the small reactive shell that binds that one
* instance to the two stores and to the component tree.
*/
const state = $state({ status: Status.CLOSED });
/** @type {ReturnType<typeof createClient> | null} */
let client = null;
/** Opens the socket if it is not already open. Safe to call from any route. */
export function connect() {
if (client) return;
client = createClient({
nickname: () => settings.state.nickname,
onMessage: (msg) => game.apply(msg),
onStatus: (status) => {
state.status = status;
}
});
client.connect();
}
/**
* Sends a message, reporting whether it actually went out.
*
* Deliberately does not open the socket: a caller that has not connected yet
* has nothing queued to resume, and auto-connecting here would reopen the
* connection during teardown.
* @param {any} msg - a ClientMessage
* @returns {boolean}
*/
export function send(msg) {
return client?.send(msg) ?? false;
}
/**
* Retries the connection immediately instead of waiting out the backoff.
*
* For the player looking at a "mất kết nối" banner with a turn timer running:
* the schedule is tuned for a client nobody is watching, and this is the case
* where somebody is.
* @returns {boolean} whether an attempt was actually started
*/
export function reconnectNow() {
return client?.reconnectNow() ?? false;
}
/**
* The server's clock as this client estimates it. The countdown is drawn
* against this rather than Date.now(), so a device with a wrong clock still
* shows the right remaining time.
*/
export function serverNow() {
return client?.serverNow() ?? Date.now();
}
/**
* Drops the resume token without touching the connection.
*
* Used when a resume was refused: the token is spent, but the socket is fine
* and the player may well want to start something new on it.
*/
export function forgetSession() {
client?.forgetSession();
}
/**
* Closes the socket and forgets the session.
*
* Called when the player leaves the board. Keeping the socket open across
* routes would mean the next game is announced to the server under whatever
* nickname the previous `Hello` carried, and the resume token would offer the
* abandoned seat back to a connection that no longer wants it.
*/
export function disconnect() {
client?.forgetSession();
client?.close();
client = null;
state.status = Status.CLOSED;
}
export const connection = state;
export { Status, hasStoredSession };