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([]); }); });