- GET /games/me/active and /games/me (Bearer token), /games/popular (live games, or the most spectated finished ones), and /game/:id now also returns saved records of finished games - Lists are cached in Valkey for 10 seconds, cleared when a game is saved, and empty instead of failing while the database is down - Live game previews over WebSocket: subscribers get the top 5 on subscribe and on change, at most once per second, without counting as spectators; ids are public ids - RTMT supports several connection listeners
93 lines
3.7 KiB
TypeScript
93 lines
3.7 KiB
TypeScript
import {
|
|
channel_game_move,
|
|
channel_listen_game_events,
|
|
channel_new_game,
|
|
channel_on_live_games,
|
|
channel_subscribe_live_games,
|
|
channel_unsubscribe_live_games,
|
|
} from "@mancala/core";
|
|
import { GameManager } from "../../src/game/GameManager";
|
|
import { GameRecorder } from "../../src/game/GameRecorder";
|
|
import { GameStoreImpl } from "../../src/game/gamestore/GameStoreImpl";
|
|
import { LiveGames } from "../../src/game/LiveGames";
|
|
import { MatchMakerImpl } from "../../src/matchmaker/MatchMakerImpl";
|
|
import { FakeRtmt } from "./FakeRtmt";
|
|
import { InMemoryGameHistoryRepository, InMemoryLiveGameStore } from "./fakes";
|
|
|
|
const ALICE = { id: "kc:alice", name: "alice", isGuest: false };
|
|
const BOB = { id: "kc:bob", name: "bob", isGuest: false };
|
|
const VIEWER = { id: "kc:viewer", name: "viewer", isGuest: false };
|
|
const SPECTATOR = { id: "kc:spectator", name: "spectator", isGuest: false };
|
|
|
|
describe("LiveGames", () => {
|
|
let rtmt: FakeRtmt;
|
|
let gameStore: GameStoreImpl;
|
|
let liveGames: LiveGames;
|
|
|
|
beforeEach(() => {
|
|
jest.useFakeTimers("modern");
|
|
rtmt = new FakeRtmt();
|
|
gameStore = new GameStoreImpl();
|
|
const recorder = new GameRecorder(new InMemoryGameHistoryRepository(), new InMemoryLiveGameStore());
|
|
const gameManager = new GameManager({
|
|
rtmt, gameStore, matchMaker: new MatchMakerImpl({ rtmt }), liveGameStore: new InMemoryLiveGameStore(), recorder,
|
|
});
|
|
liveGames = new LiveGames({ rtmt, gameStore, gameManager });
|
|
[ALICE, BOB, VIEWER, SPECTATOR].forEach((user) => rtmt.connect(user));
|
|
});
|
|
|
|
afterEach(() => {
|
|
liveGames.dispose();
|
|
jest.useRealTimers();
|
|
});
|
|
|
|
const pushes = () => rtmt.messagesTo(VIEWER.id, channel_on_live_games);
|
|
|
|
const startGame = () => {
|
|
rtmt.receive(ALICE.id, channel_new_game);
|
|
rtmt.receive(BOB.id, channel_new_game);
|
|
return gameStore.getGameByUser(ALICE.id)!;
|
|
};
|
|
|
|
it("sends the current games on subscribe", () => {
|
|
const game = startGame();
|
|
rtmt.receive(VIEWER.id, channel_subscribe_live_games);
|
|
expect(pushes()).toHaveLength(1);
|
|
expect(pushes()[0].games.map((g: any) => g.id)).toEqual([game.id]);
|
|
});
|
|
|
|
it("pushes changes at most once per second", () => {
|
|
rtmt.receive(VIEWER.id, channel_subscribe_live_games);
|
|
const game = startGame();
|
|
jest.advanceTimersByTime(0);
|
|
expect(pushes()).toHaveLength(2);
|
|
|
|
// Two changes right after a push are sent together, a second later.
|
|
rtmt.receive(game.mancalaGame.turnPlayerId, channel_game_move, { index: 0 });
|
|
rtmt.receive(SPECTATOR.id, channel_listen_game_events, game.id);
|
|
jest.advanceTimersByTime(999);
|
|
expect(pushes()).toHaveLength(2);
|
|
jest.advanceTimersByTime(1);
|
|
expect(pushes()).toHaveLength(3);
|
|
expect(pushes()[2].games[0]).toMatchObject({ moveCount: 1, spectatorCount: 1 });
|
|
});
|
|
|
|
it("does not count subscribers as spectators", () => {
|
|
const game = startGame();
|
|
rtmt.receive(VIEWER.id, channel_subscribe_live_games);
|
|
expect(game.spectatorIds).toEqual([]);
|
|
expect(pushes()[0].games[0].spectatorCount).toBe(0);
|
|
});
|
|
|
|
it("stops pushing to users who unsubscribe or go offline", () => {
|
|
rtmt.receive(VIEWER.id, channel_subscribe_live_games);
|
|
rtmt.receive(SPECTATOR.id, channel_subscribe_live_games);
|
|
rtmt.receive(VIEWER.id, channel_unsubscribe_live_games);
|
|
rtmt.disconnect(SPECTATOR.id);
|
|
const before = rtmt.sent.length;
|
|
startGame();
|
|
jest.advanceTimersByTime(1000);
|
|
expect(rtmt.sent.slice(before).filter((m) => m.channel === channel_on_live_games)).toEqual([]);
|
|
});
|
|
});
|