mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-17 00:10:54 +00:00
## What Implements the close-code contract from the matchmaking API handoff (API PR #419) in the matchmaking modal, plus two runnable integration harnesses. Previously `onclose` only logged, so an API deploy while queued left the player on the "searching" spinner forever (the queue is in-memory on the API worker). | Close | Behavior | | --- | --- | | 1008 `Invalid session` | Reconnect and re-send `join` — `getPlayToken()` refreshes an expired token internally, so the rejoin carries a fresh JWT | | 1000 `Replaced by newer connection` | Another tab/window took the queue slot: show a message (new `matchmaking_modal.replaced` string) and stop. No retry | | Any other close before assignment | Server restart/deploy: reconnect and re-send `join`, with exponential backoff (1s doubling to a 15s cap) | Intentional closes (user backs out of the modal, assignment received) don't reconnect. A pending 2s join timer from a previous socket is cleared before each reconnect so it can't fire on the new socket. ## Test harnesses (`tests/matchmaking/`) - **`npm run test:matchmaking`** (contained): drives the real modal in a headless browser against a fake in-process matchmaking server speaking the documented protocol; covers the whole close-code table over real WebSockets. Needs only `npm run dev`. - **`npm run test:matchmaking:e2e`**: real integration against the API worker on `localhost:8787` — two browser players join the real queue, the dev game server receives the checkin assignment, and both clients end up in the same created game. ## Why now This is required by the matchmaking API's client contract independent of the upcoming 2v2 work ("clients must already handle unexpected close → reconnect and rejoin"). The rest of the 2v2 integration is planned for the next version. ## Verification - Contained harness: 8/8 checks pass (join, 1006 reconnect+rejoin, 1008 rejoin with fresh token, assignment, replaced → message + no retry, intentional close → no retry/no message). - E2E harness against a local worker: 3/3 — both players matched into the same game, game created via checkin and joinable by both. - `npm test` (all 2,046 pass), `npx tsc --noEmit`, ESLint clean. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
45 lines
1.2 KiB
JavaScript
45 lines
1.2 KiB
JavaScript
// Shared helpers for the matchmaking harnesses (contained + e2e).
|
|
|
|
export async function waitFor(
|
|
fn,
|
|
{ timeoutMs = 10000, intervalMs = 200, label = "condition" } = {},
|
|
) {
|
|
const deadline = Date.now() + timeoutMs;
|
|
for (;;) {
|
|
const value = await fn();
|
|
if (value) return value;
|
|
if (Date.now() > deadline) {
|
|
throw new Error(`Timed out after ${timeoutMs}ms waiting for: ${label}`);
|
|
}
|
|
await new Promise((r) => setTimeout(r, intervalMs));
|
|
}
|
|
}
|
|
|
|
// Any HTTP response (including 404) counts as "up"; only a network error
|
|
// (nothing listening) counts as down.
|
|
export async function isUp(url) {
|
|
try {
|
|
await fetch(url, { signal: AbortSignal.timeout(2000) });
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
export function makeChecker() {
|
|
const results = [];
|
|
return {
|
|
check(name, cond) {
|
|
results.push({ name, pass: !!cond });
|
|
console.log(`${cond ? "PASS" : "FAIL"} ${name}`);
|
|
},
|
|
finish() {
|
|
const failed = results.filter((r) => !r.pass);
|
|
console.log(
|
|
`\n${results.length - failed.length}/${results.length} checks passed`,
|
|
);
|
|
process.exit(failed.length > 0 ? 1 : 0);
|
|
},
|
|
};
|
|
}
|