import test from "node:test"; import assert from "node:assert/strict"; import { EventEmitter } from "node:events"; import { canonicalMatchId, validateOskiewarLiveState, OskiewarLiveManager, OSKIEWAR_LIVE_LIMITS } from "./oskiewar-live-manager.mjs"; class FakeSocket extends EventEmitter { constructor() { super(); this.readyState = 1; this.sent = []; this.closed = null; } send(value) { this.sent.push(JSON.parse(value)); } close(code, reason) { this.readyState = 3; this.closed = { code, reason }; this.emit("close"); } } const state = (seq = 1) => ({ format: "ac.oskiewar.live", version: 1, seq, at: 1000 + seq, phase: "fight", fighters: [ { name: "@JEFFREY", color: [190, 42, 58], x: 2000, y: 12000, z: 0, facing: 1, alive: true, grounded: true, ducking: false, blocking: false, score: 2, roundWins: 1, attack: "" }, { name: "@OSKIE", color: [38, 82, 176], x: 10000, y: 12000, z: 0, facing: -1, alive: true, grounded: true, ducking: false, blocking: true, score: 1, roundWins: 0, attack: "KICK" }, ], ball: { active: true, x: 6000, y: 11945, z: 0, radius: 55 }, camera: { x: 6000, y: 9140, width: 12000 }, round: { remainingMs: 24000, result: "" }, }); test("canonical match IDs accept the QR name or ow-prefixed ID", () => { const name = "bafegu-dorimi-kunapo"; assert.equal(canonicalMatchId(name), `ow-${name}`); assert.equal(canonicalMatchId(`ow-${name}`), `ow-${name}`); assert.equal(canonicalMatchId("sezzi7"), "ow-sezzi7"); assert.equal(canonicalMatchId("ow-shuppy652"), "ow-shuppy652"); assert.equal(canonicalMatchId("short-name"), null); }); test("live state validation rejects unbounded or malformed values", () => { assert.equal(validateOskiewarLiveState(state()), null); assert.equal(validateOskiewarLiveState({ ...state(), seriesId: "ow-zavoki-bemuru-ditale", roundId: "ow-bafegu-dorimi-kunapo", previousRoundId: "ow-dorimi-kunapo-lafegu", nextRoundId: "ow-fagori-buneta-kovisu", wind: { direction: -1, mph: 18 } }), null); assert.equal(validateOskiewarLiveState({ ...state(), fighters: [] }), "Invalid fighters"); assert.equal(validateOskiewarLiveState({ ...state(), nextRoundId: "bad" }), "Invalid next round ID"); assert.equal(validateOskiewarLiveState({ ...state(), wind: { direction: 0, mph: 8 } }), "Invalid wind"); assert.equal(validateOskiewarLiveState({ ...state(), replayUrl: "https://bad" }), "Invalid replay URL"); }); test("one publisher fans state only to viewers in its match", () => { let now = 100; const manager = new OskiewarLiveManager({ now: () => now }); const host = new FakeSocket(), viewer = new FakeSocket(), other = new FakeSocket(); manager.handleConnection(host, { url: "/oskiewar-live?match=bafegu-dorimi-kunapo&role=publisher" }); manager.handleConnection(viewer, { url: "/oskiewar-live?match=bafegu-dorimi-kunapo" }); manager.handleConnection(other, { url: "/oskiewar-live?match=lafegu-dorimi-kunapo" }); now += 30; host.emit("message", Buffer.from(JSON.stringify({ type: "oskiewar:state", content: state() }))); assert.equal(viewer.sent.at(-1).type, "oskiewar:state"); assert.equal(viewer.sent.at(-1).content.seq, 1); assert.notEqual(other.sent.at(-1)?.type, "oskiewar:state"); }); test("a fixed match broadcast fans out and caches state for late spectators", () => { let now = 100; const manager = new OskiewarLiveManager({ now: () => now }); const host = new FakeSocket(); const viewers = Array.from({ length: 12 }, () => new FakeSocket()); const url = "/oskiewar-live?match=bafegu-dorimi-kunapo"; manager.handleConnection(host, { url: `${url}&role=publisher` }); for (const viewer of viewers) manager.handleConnection(viewer, { url }); now += 30; host.emit("message", Buffer.from(JSON.stringify({ type: "oskiewar:state", content: state(), }))); assert.ok(viewers.every((viewer) => viewer.sent.at(-1)?.type === "oskiewar:state")); const late = new FakeSocket(); manager.handleConnection(late, { url }); assert.equal(late.sent.at(-1)?.type, "oskiewar:state"); assert.equal(late.sent.at(-1)?.content.seq, 1); }); // Coalescing is allowed to cost latency. It is not allowed to cost // information — and for a long time it cost both: the rate gate ran before // the store, so a burst of two frames in one instant (a host catch-up tick, // or a link stall batching them) delivered the OLDER one and lost the newer // for good. Against the real relay that was 25% of frames on a wired link // and 57% on a busy one. The newest frame now always survives the gate. test("over-rate publisher frames coalesce to the newest, and duplicates drop", () => { let now = 100; const manager = new OskiewarLiveManager({ now: () => now }); const host = new FakeSocket(), viewer = new FakeSocket(); const url = "/oskiewar-live?match=bafegu-dorimi-kunapo"; manager.handleConnection(host, { url: `${url}&role=publisher` }); manager.handleConnection(viewer, { url }); const publish = (seq) => host.emit("message", Buffer.from(JSON.stringify({ type: "oskiewar:state", content: state(seq) }))); now += 30; publish(1); const count = viewer.sent.length; assert.equal(viewer.sent.at(-1).content.seq, 1); // Three more inside the 25 ms floor: nothing goes out yet, and what is held // is the last of them, not the first. now += 1; publish(2); now += 1; publish(3); now += 1; publish(4); assert.equal(viewer.sent.length, count, "the burst is coalesced, not fanned out"); const room = manager.rooms.get("ow-bafegu-dorimi-kunapo"); assert.equal(room.state.seq, 4, "and the newest is what is held"); // A frame the room has already passed is a duplicate whenever it arrives. now += 30; publish(2); assert.equal(room.state.seq, 4); // The trailing flush hands the room whatever is newest when it fires. manager.flush(room); assert.equal(viewer.sent.length, count + 1); assert.equal(viewer.sent.at(-1).content.seq, 4); // And it fires once for that frame, however often it is asked. manager.flush(room); assert.equal(viewer.sent.length, count + 1); }); test("the seat hears its own fighter before the grandstand does", () => { const manager = new OskiewarLiveManager({ now: () => 1000 }); const host = new FakeSocket(), rival = new FakeSocket(), fan = new FakeSocket(); const url = "/oskiewar-live?match=sezzi7"; manager.handleConnection(host, { url: `${url}&role=publisher` }); manager.handleConnection(fan, { url }); manager.handleConnection(rival, { url: `${url}&role=challenger` }); const order = []; for (const [name, socket] of [["fan", fan], ["rival", rival]]) socket.send = ((inner) => (data) => { order.push(name); inner.call(socket, data); })(socket.send); const room = manager.rooms.get("ow-sezzi7"); room.state = state(9); manager.flush(room); assert.deepEqual(order, ["rival", "fan"]); }); test("a second live publisher cannot take over a match", () => { const manager = new OskiewarLiveManager(); const first = new FakeSocket(), second = new FakeSocket(); const url = "/oskiewar-live?match=bafegu-dorimi-kunapo&role=publisher"; manager.handleConnection(first, { url }); manager.handleConnection(second, { url }); assert.equal(second.closed?.code, 4409); }); test("a successor publisher counts from zero — no corpse seq gate, no stale frame", () => { let now = 100; const manager = new OskiewarLiveManager({ now: () => now }); const first = new FakeSocket(), viewer = new FakeSocket(); const url = "/oskiewar-live?match=bafegu-dorimi-kunapo"; manager.handleConnection(first, { url: `${url}&role=publisher` }); manager.handleConnection(viewer, { url }); now += 30; first.emit("message", Buffer.from(JSON.stringify( { type: "oskiewar:state", content: state(500) }))); assert.equal(viewer.sent.at(-1).content.seq, 500); first.close(); const second = new FakeSocket(); manager.handleConnection(second, { url: `${url}&role=publisher` }); const late = new FakeSocket(); manager.handleConnection(late, { url }); assert.ok(!late.sent.some((m) => m.type === "oskiewar:state"), "a mid-handoff spectator must not be handed the dead host's frame"); now += 30; second.emit("message", Buffer.from(JSON.stringify( { type: "oskiewar:state", content: state(1) }))); assert.equal(viewer.sent.at(-1).content.seq, 1, "the claimed room must fan the new host's first frame, not mute it"); assert.equal(late.sent.at(-1).content.seq, 1); }); test("a challenger is seated, watches state, and their presses reach the publisher", () => { let now = 100; const manager = new OskiewarLiveManager({ now: () => now }); const host = new FakeSocket(), rival = new FakeSocket(); const url = "/oskiewar-live?match=bafegu-dorimi-kunapo"; manager.handleConnection(host, { url: `${url}&role=publisher` }); manager.handleConnection(rival, { url: `${url}&role=challenger` }); assert.ok(rival.sent.some((message) => message.type === "oskiewar:seat" && message.content.seat === "challenger")); now += 30; host.emit("message", Buffer.from(JSON.stringify({ type: "oskiewar:state", content: state() }))); assert.equal(rival.sent.at(-1).type, "oskiewar:state"); now += 30; rival.emit("message", Buffer.from(JSON.stringify({ type: "oskiewar:input", content: { seq: 1, down: ["ArrowLeft", "A"], leftX: -0.4, leftY: 0, name: "@FRIEND", colors: [[10, 20, 30]] } }))); const input = host.sent.at(-1); assert.equal(input.type, "oskiewar:input"); assert.deepEqual(input.content.down, ["ArrowLeft", "A"]); assert.equal(input.content.name, "@FRIEND"); }); test("the second chair holds one challenger and frees on close", () => { const manager = new OskiewarLiveManager(); const host = new FakeSocket(), first = new FakeSocket(), second = new FakeSocket(); const url = "/oskiewar-live?match=bafegu-dorimi-kunapo"; manager.handleConnection(host, { url: `${url}&role=publisher` }); manager.handleConnection(first, { url: `${url}&role=challenger` }); manager.handleConnection(second, { url: `${url}&role=challenger` }); assert.equal(second.closed?.code, 4409); first.close(1000, "leaving"); const third = new FakeSocket(); manager.handleConnection(third, { url: `${url}&role=challenger` }); assert.ok(third.sent.some((message) => message.type === "oskiewar:seat")); }); // A host that leaves takes its last frame with it. Measured on ow-regga890: // the room still answered `hasState: true` with a seq 2857 frame of a `fight` // between NOBODY and NOBODY long after the host had gone, and it was the first // thing handed to anybody who opened that address — who then read a frozen // round as a fight they could take a seat in. test("a departed publisher's last frame does not outlive it", () => { let now = 100; const manager = new OskiewarLiveManager({ now: () => now }); const host = new FakeSocket(); const url = "/oskiewar-live?match=bafegu-dorimi-kunapo"; manager.handleConnection(host, { url: `${url}&role=publisher` }); now += 30; host.emit("message", Buffer.from(JSON.stringify({ type: "oskiewar:state", content: state(2857) }))); const early = new FakeSocket(); manager.handleConnection(early, { url }); assert.equal(early.sent.at(-1).content.seq, 2857); host.close(1006, "tab killed"); const late = new FakeSocket(); manager.handleConnection(late, { url }); const status = late.sent.find((message) => message.type === "oskiewar:status"); assert.equal(status.content.live, false); assert.equal(status.content.hasState, false); assert.ok(!late.sent.some((message) => message.type === "oskiewar:state")); }); // A chair is held by whoever is still speaking from it. A killed tab sends no // close frame, so its socket stays open until the shared ping sweep reaps it // two misses later — and for that half minute the person who just refreshed // was told the chair was taken and demoted to the grandstand, unable to rejoin // as themselves because the relay was holding their own dead socket against // them. test("a silent chair is reclaimed, a playing one is not", () => { let now = 100000; const manager = new OskiewarLiveManager({ now: () => now }); const host = new FakeSocket(), first = new FakeSocket(); const url = "/oskiewar-live?match=bafegu-dorimi-kunapo"; manager.handleConnection(host, { url: `${url}&role=publisher` }); manager.handleConnection(first, { url: `${url}&role=challenger` }); // Still playing: a second arrival is refused and stays to watch. now += 2000; first.emit("message", Buffer.from(JSON.stringify({ type: "oskiewar:input", content: { seq: 1, down: ["A"], leftX: 0, leftY: 0 } }))); const rival = new FakeSocket(); manager.handleConnection(rival, { url: `${url}&role=challenger` }); assert.equal(rival.closed?.code, 4409); assert.equal(first.readyState, 1); // Gone quiet past the ghost window: the same person comes back and the seat // is theirs, and the socket that was squatting on it is closed. now += OSKIEWAR_LIVE_LIMITS.CHALLENGER_GHOST_MS + 1; const rejoin = new FakeSocket(); manager.handleConnection(rejoin, { url: `${url}&role=challenger` }); assert.ok(rejoin.sent.some((message) => message.type === "oskiewar:seat")); assert.equal(rejoin.closed, null); assert.equal(first.closed?.code, 4410); // And the evicted socket's own close must not clear the seat it no longer // holds — the newcomer's pads still reach the host. first.emit("message", Buffer.from(JSON.stringify({ type: "oskiewar:input", content: { seq: 9, down: ["B"], leftX: 0, leftY: 0 } }))); now += 100; rejoin.emit("message", Buffer.from(JSON.stringify({ type: "oskiewar:input", content: { seq: 2, down: ["X"], leftX: 0, leftY: 0 } }))); const pads = host.sent.filter((message) => message.type === "oskiewar:input"); assert.deepEqual(pads.at(-1).content.down, ["X"]); }); // The bare front door asks for one open room. Only a live host with an empty // second chair and an untimed round qualifies — the timed rounds are the // recorded broadcast farm, television rather than an open chair. test("the matchmaker offers one live untimed room with an empty chair", () => { let now = 100; const manager = new OskiewarLiveManager({ now: () => now }); assert.equal(manager.openRoom(), null); const farm = new FakeSocket(); manager.handleConnection(farm, { url: "/oskiewar-live?match=lafegu-dorimi-kunapo&role=publisher" }); now += 30; farm.emit("message", Buffer.from(JSON.stringify({ type: "oskiewar:state", content: { ...state(), round: { remainingMs: 24000, timed: true, result: "" } } }))); assert.equal(manager.openRoom(), null); const host = new FakeSocket(); manager.handleConnection(host, { url: "/oskiewar-live?match=bafegu-dorimi-kunapo&role=publisher" }); now += 30; host.emit("message", Buffer.from(JSON.stringify({ type: "oskiewar:state", content: { ...state(), round: { remainingMs: 0, timed: false, result: "" } } }))); assert.deepEqual(manager.openRoom(), { matchId: "ow-bafegu-dorimi-kunapo", room: "bafegu-dorimi-kunapo" }); // A seated challenger closes the door; an emptied chair reopens it — // until the host's frames go stale. const rival = new FakeSocket(); manager.handleConnection(rival, { url: "/oskiewar-live?match=bafegu-dorimi-kunapo&role=challenger" }); assert.equal(manager.openRoom(), null); rival.close(1000, "leaving"); assert.equal(manager.openRoom()?.room, "bafegu-dorimi-kunapo"); now += 10001; assert.equal(manager.openRoom(), null); }); test("bent or over-rate challenger input is dropped in silence", () => { let now = 100; const manager = new OskiewarLiveManager({ now: () => now }); const host = new FakeSocket(), rival = new FakeSocket(); const url = "/oskiewar-live?match=bafegu-dorimi-kunapo"; manager.handleConnection(host, { url: `${url}&role=publisher` }); manager.handleConnection(rival, { url: `${url}&role=challenger` }); const count = host.sent.length; const press = (content) => rival.emit("message", Buffer.from(JSON.stringify({ type: "oskiewar:input", content }))); now += 30; press({ seq: 1, down: ["