Files
History/src/lib/sse.ts
T
kamilkrzywda 17d6cecab9
CI / build + unit + e2e (pull_request) Successful in 1m14s
feat: remove a saved game & redirect on missing/deleted game (#20)
- DELETE /api/games/:id route + Db.delete() (404 when absent)
- PresetPicker: per-saved-game trash button + confirm dialog (Cancel/Delete)
- App: runDeleteGame; useSse onError -> goHome on 'deleted'/'not found'
  (a deleted/missing game now returns the live tab and any deep link home)
- DeleteGameResponse in shared/protocol.ts; api.deleteGame()
- unit test (Db.delete) + 2 e2e tests (home-screen delete; in-flight delete
  returns the active tab home and a fresh deep link lands on home)
- version 0.7.0 -> 0.8.0 + changelog

Fixes #20
2026-10-11 20:32:55 +00:00

62 lines
2.0 KiB
TypeScript

import { useEffect, useRef } from 'react';
import type { GameState } from '@shared/types';
import type { TurnResult } from '@shared/protocol';
interface SseHandlers {
onState?: (state: GameState) => void;
onTurn?: (state: GameState, result: TurnResult) => void;
/**
* Fired for a server-sent NAMED `event: error` (a MessageEvent with a JSON
* body `{ message }`) — e.g. the game was deleted or no longer exists. Pure
* connection errors (no data) are ignored and EventSource auto-reconnects.
*/
onError?: (message: string) => void;
}
/**
* Subscribe to a game's SSE stream. The server sends a `state` event on connect
* and a `turn` event after every turn; EventSource auto-reconnects on drop, so a
* reload always resyncs to the persisted server state.
*
* Handlers are kept in a ref so the (id-keyed) effect always invokes the latest
* callbacks without re-subscribing on every render.
*/
export function useSse(id: string | null, handlers: SseHandlers): void {
const handlersRef = useRef(handlers);
handlersRef.current = handlers;
useEffect(() => {
if (!id) return;
const es = new EventSource(`/sse/${id}`);
const onState = (e: MessageEvent) => {
try {
handlersRef.current.onState?.(JSON.parse(e.data).state as GameState);
} catch {
/* ignore malformed */
}
};
const onTurn = (e: MessageEvent) => {
try {
const d = JSON.parse(e.data) as { state: GameState; result: TurnResult };
handlersRef.current.onTurn?.(d.state, d.result);
} catch {
/* ignore malformed */
}
};
const onError = (e: Event) => {
if (e instanceof MessageEvent && e.data) {
try {
const d = JSON.parse(e.data) as { message?: string };
if (d.message) handlersRef.current.onError?.(d.message);
} catch {
/* ignore malformed */
}
}
};
es.addEventListener('state', onState);
es.addEventListener('turn', onTurn);
es.addEventListener('error', onError);
return () => es.close();
}, [id]);
}