mirror of
https://github.com/tiennm99/noitu.git
synced 2026-10-05 20:14:59 +00:00
A room used to be a wrapper around one game: joining started it, and the room died with it unless both players accepted a rematch inside thirty seconds. It is now a lobby that outlives its games. Whoever created the room owns it and the other seat is the guest. The guest readies and the owner starts; the owner has no readiness of their own, because starting is the same statement. A finished game returns both to the lobby, where the next one is agreed exactly as the last was — the readiness that started a game is spent with it. A guest takes their readiness back before leaving, which is deliberate friction: a player the owner is waiting on should have to say so before walking away. The owner can free the seat of a guest who is not ready, and not of one who is — readiness is a commitment, not an inconvenience. An owner who leaves hands the room to whoever is left, unreadied, because they are the one who starts now. Something has to bound a room that outlives its games: the last player out closes it, as does ten minutes in a lobby nobody started a game in. A dropped connection is still not a player leaving — the seat is held for the reconnect window in the lobby as well as mid-game, so a refresh no longer costs somebody their room, and a resume lands in the lobby it left. The rematch handshake is retired, and RoomCreated and RoomJoined go with it. All three described part of what RoomState now describes in full, and three messages for one lobby is three ways for a client to hold a view of it the server never had. One snapshot per recipient, broadcast from the one place that knows an input is finished, so no handler can forget to send it.
60 lines
2.2 KiB
JavaScript
60 lines
2.2 KiB
JavaScript
// ServerError.code is a UI key, so the server decides the vocabulary and this
|
|
// file has to speak all of it. The proto schema cannot help here — the codes
|
|
// are string literals in the Go transport, not an enum — so the guard reads
|
|
// them out of the source that emits them.
|
|
//
|
|
// Without this, adding a code on the server degrades silently to the generic
|
|
// fallback: the player is told "something went wrong" for a situation the
|
|
// server described precisely.
|
|
|
|
import { readFileSync, readdirSync } from 'node:fs';
|
|
import { join } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { describe, expect, it } from 'vitest';
|
|
import { errorMessages } from '../src/lib/i18n/vi.js';
|
|
|
|
const wsapiDir = fileURLToPath(new URL('../../server/internal/wsapi', import.meta.url));
|
|
|
|
/**
|
|
* Every error-code literal in the transport, excluding its own tests. Both the
|
|
* one-recipient and the broadcast call sites count: a code that only ever goes
|
|
* to both players is no less a code the client has to know.
|
|
*/
|
|
function serverErrorCodes() {
|
|
const codes = new Set();
|
|
const files = readdirSync(wsapiDir).filter((f) => f.endsWith('.go') && !f.endsWith('_test.go'));
|
|
|
|
for (const file of files) {
|
|
const source = readFileSync(join(wsapiDir, file), 'utf8');
|
|
for (const [, code] of source.matchAll(/(?:errorMsg|broadcastError)\("([a-z_]+)"\)/g)) {
|
|
codes.add(code);
|
|
}
|
|
}
|
|
return codes;
|
|
}
|
|
|
|
describe('server error codes', () => {
|
|
const codes = serverErrorCodes();
|
|
|
|
it('finds the call sites at all, so an empty match cannot pass as agreement', () => {
|
|
// A rename of errorMsg would otherwise turn this whole file green by
|
|
// finding nothing to check.
|
|
expect(codes.size).toBeGreaterThan(10);
|
|
expect(codes.has('room_not_found')).toBe(true);
|
|
});
|
|
|
|
it('has a Vietnamese message for every code the server can send', () => {
|
|
const missing = [...codes].filter((code) => !(code in errorMessages)).sort();
|
|
expect(missing).toEqual([]);
|
|
});
|
|
|
|
it('carries no message for a code the server cannot send', () => {
|
|
// Dead copy is a smaller problem than a missing message, but it is still
|
|
// a claim about the server that has stopped being true.
|
|
const stale = Object.keys(errorMessages)
|
|
.filter((code) => !codes.has(code))
|
|
.sort();
|
|
expect(stale).toEqual([]);
|
|
});
|
|
});
|