Files
Live_RC/tests/browser_fake_decoder.js
larssand 8601954ea2 feat(practice): flag suspiciously short laps and remove any lap in one click
Practice laps under 85% of the driver's own median (a likely shortcut)
are flagged: counted, but never a best lap, record or training stat
until approved. Timing and Race Day get a one-click Approve/Remove
panel, overlays show a warning mark, and any lap can be removed or
restored from Timing and Judging with undo.

Also sends judge corrections straight to the server so its hot-results
protection can no longer hold back a correction made right after a
passing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 19:53:23 +02:00

57 lines
2.9 KiB
JavaScript

import { createHash } from "node:crypto";
import { createServer } from "node:http";
import { createServer as createNetServer } from "node:net";
export async function freePort() {
const server = createNetServer();
await new Promise((resolve, reject) => { server.once("error", reject); server.listen(0, "127.0.0.1", resolve); });
const port = server.address().port;
await new Promise(resolve => server.close(resolve));
return port;
}
// Accepts the WebSocket upgrade handshake so the frontend's real decoder-connect path
// (src/decoder_runtime.js) reaches `connected: true` without a physical AMMC decoder.
// sendPassing() pushes a PASSING frame for scenarios that need the app to see a transponder.
export async function startFakeDecoder() {
const port = await freePort();
const server = createServer();
// The upgraded sockets are intentionally left open (no decoder frames needed), so a
// plain server.close() would wait forever for them to end; track and destroy them
// explicitly instead. closeAllConnections() does not reach upgraded sockets.
const sockets = new Set();
let totalConnections = 0;
server.on("upgrade", (req, socket) => {
totalConnections += 1;
sockets.add(socket);
socket.on("close", () => sockets.delete(socket));
const key = req.headers["sec-websocket-key"];
if (!key) { socket.destroy(); return; }
const accept = createHash("sha1").update(key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11").digest("base64");
socket.write(["HTTP/1.1 101 Switching Protocols", "Upgrade: websocket", "Connection: Upgrade", `Sec-WebSocket-Accept: ${accept}`, "", ""].join("\r\n"));
});
await new Promise((resolve, reject) => { server.once("error", reject); server.listen(port, "127.0.0.1", resolve); });
return {
port,
activeConnections: () => sockets.size,
totalConnections: () => totalConnections,
// Drops every currently-open socket with a raw TCP reset (not a clean WS close
// handshake) to simulate a decoder/network interruption, while the server keeps
// listening so the frontend's own reconnect logic has somewhere to reconnect to.
dropConnections: () => { for (const socket of sockets) socket.destroy(); },
// Sends an AMMC-style PASSING message as an unmasked server->client WebSocket text frame.
// `at` (ms) sets the decoder's own passing time, for exact lap times in a test.
sendPassing: (transponder, at = Date.now()) => {
const payload = Buffer.from(JSON.stringify({ msg: "PASSING", transponder: String(transponder), rtc_time: new Date(at).toISOString() }));
const header = payload.length < 126
? Buffer.from([0x81, payload.length])
: Buffer.from([0x81, 126, payload.length >> 8, payload.length & 0xff]);
for (const socket of sockets) socket.write(Buffer.concat([header, payload]));
},
close: () => new Promise(resolve => {
for (const socket of sockets) socket.destroy();
server.close(() => resolve());
}),
};
}