From 1dbe6b7ec033048895619034b03bb4be1b819ecd Mon Sep 17 00:00:00 2001 From: Gavin McDonald Date: Sat, 28 Jun 2025 20:16:52 -0400 Subject: [PATCH] throttle 'tilts' from the server --- app/AppContext.tsx | 20 ++++++++++---------- components/Card.tsx | 6 +++--- components/CardSelect.tsx | 4 ++-- components/Settings.tsx | 6 +++--- constants/time.ts | 2 ++ hooks/useSocket.ts | 19 ++++++++++--------- server.ts | 25 ++++++++++++++++++++++++- tools/throttle.ts | 12 ++++++++++++ 8 files changed, 66 insertions(+), 28 deletions(-) create mode 100644 tools/throttle.ts diff --git a/app/AppContext.tsx b/app/AppContext.tsx index 44a62e8..c1f30dc 100644 --- a/app/AppContext.tsx +++ b/app/AppContext.tsx @@ -25,10 +25,10 @@ export interface AppContext { noGame: boolean; tilt: Tilt[]; selectCardIndex: number; - flipCard: (cardIndex: number) => void; - handleSettings: (gameData: GameUpdate) => void; - redraw: (cardIndex: number) => void; - select: (cardID: string) => void; + emitFlip: (cardIndex: number) => void; + emitSettings: (gameData: GameUpdate) => void; + emitRedraw: (cardIndex: number) => void; + emitSelect: (cardID: string) => void; setGameID: (gameID: string) => void; setSelectCardIndex: (cardIndex: number) => void; setTilt: (tilt: Tilt[]) => void; @@ -41,7 +41,7 @@ export function AppProvider({ children }: { children: ReactNode }) { const [selectCardIndex, setSelectCardIndex] = useState(-1); const [tilt, setTilt] = useState([]); - const { flipCard, redraw, select, handleSettings, emitTilt } = useSocket({ + const { emitFlip, emitRedraw, emitSelect, emitSettings, emitTilt } = useSocket({ gameID, setGameData, setNoGame, @@ -58,7 +58,7 @@ export function AppProvider({ children }: { children: ReactNode }) { const handleSelect = (cardID: string) => { setSelectCardIndex(-1); - select(selectCardIndex, cardID); + emitSelect(selectCardIndex, cardID); }; const appInterface = { @@ -66,10 +66,10 @@ export function AppProvider({ children }: { children: ReactNode }) { noGame, tilt, selectCardIndex, - flipCard, - handleSettings, - redraw, - select: handleSelect, + emitFlip, + emitSettings, + emitRedraw, + emitSelect: handleSelect, setGameID, setSelectCardIndex, setTilt, diff --git a/components/Card.tsx b/components/Card.tsx index 4b12f86..b27ba9b 100644 --- a/components/Card.tsx +++ b/components/Card.tsx @@ -22,7 +22,7 @@ type CardProps = { export default function Card({ card, cardIndex }: CardProps) { const [tooltip, setTooltip] = useState(null); - const { flipCard, gameData, redraw, setSelectCardIndex } = useAppContext(); + const { emitFlip, gameData, emitRedraw, setSelectCardIndex } = useAppContext(); const { dmID, settings } = gameData; const isDM = !!dmID; @@ -32,7 +32,7 @@ export default function Card({ card, cardIndex }: CardProps) { const handleClick = () => { if (isDM) { - flipCard(cardIndex); + emitFlip(cardIndex); } }; @@ -79,7 +79,7 @@ export default function Card({ card, cardIndex }: CardProps) { /> {isDM && !flipped && ( redraw(cardIndex)} + onRedraw={() => emitRedraw(cardIndex)} onSelect={() => setSelectCardIndex(cardIndex)} onHover={setTooltip} /> diff --git a/components/CardSelect.tsx b/components/CardSelect.tsx index fcddc6b..482ec0c 100644 --- a/components/CardSelect.tsx +++ b/components/CardSelect.tsx @@ -14,7 +14,7 @@ type CardSelectProps = { }; export default function CardSelect({ className = '' }: CardSelectProps) { - const { gameData, select, selectCardIndex, setSelectCardIndex } = useAppContext(); + const { gameData, emitSelect, selectCardIndex, setSelectCardIndex } = useAppContext(); const { cards: hand, settings } = gameData; const handIDs = hand.map(({ id }) => id); @@ -53,7 +53,7 @@ export default function CardSelect({ className = '' }: CardSelectProps) {
select(card.id)} + onClick={() => emitSelect(card.id)} > { - handleSettings({ + emitSettings({ ...gameData, settings: { ...gameData.settings, @@ -38,7 +38,7 @@ export default function Settings() { }; const tuneRadio = (cardStyle: CardStyle) => { - handleSettings({ + emitSettings({ ...gameData, settings: { ...gameData.settings, diff --git a/constants/time.ts b/constants/time.ts index 013a0b4..e422df4 100644 --- a/constants/time.ts +++ b/constants/time.ts @@ -2,3 +2,5 @@ export const SECOND = 1000; export const MINUTE = 60 * SECOND; export const HOUR = 60 * MINUTE; export const DAY = 24 * HOUR; + +export const thirtyFPS = SECOND / 30; diff --git a/hooks/useSocket.ts b/hooks/useSocket.ts index 8af6b3e..6bdb3b4 100644 --- a/hooks/useSocket.ts +++ b/hooks/useSocket.ts @@ -2,6 +2,7 @@ import { useEffect } from 'react'; import { socket } from '@/socket'; import throttle from '@/tools/throttle'; +import { thirtyFPS } from '@/constants/time'; import type { GameUpdate, Tilt } from '@/types'; interface UseSocketProps { @@ -38,28 +39,28 @@ export default function useSocket({ gameID, setGameData, setNoGame }: UseSocketP }; }, [gameID]); - const flipCard = (cardIndex: number) => { + const emitFlip = (cardIndex: number) => { socket.emit('flip-card', { gameID, cardIndex, }); }; - const handleSettings = (gameData: GameUpdate) => { + const emitSettings = (gameData: GameUpdate) => { socket.emit('settings', { gameID, gameData, }); }; - const redraw = (cardIndex: number) => { + const emitRedraw = (cardIndex: number) => { socket.emit('redraw', { gameID, cardIndex, }); }; - const select = (cardIndex: number, cardID: string) => { + const emitSelect = (cardIndex: number, cardID: string) => { socket.emit('select', { gameID, cardIndex, @@ -72,13 +73,13 @@ export default function useSocket({ gameID, setGameData, setNoGame }: UseSocketP cardIndex, tilt, }); - }, 33.3); + }, thirtyFPS); return { - flipCard, - handleSettings, - redraw, - select, + emitFlip, + emitSettings, + emitRedraw, + emitSelect, emitTilt, }; } diff --git a/server.ts b/server.ts index eb8e062..8678e74 100644 --- a/server.ts +++ b/server.ts @@ -4,6 +4,8 @@ import { Server as SocketIOServer, type Socket } from 'socket.io'; import GameStore from '@/lib/GameStore'; import omit from '@/tools/omit'; + +import { thirtyFPS } from '@/constants/time'; import type { ClientUpdate, GameUpdate, Tilt } from '@/types'; const dev = process.env.NODE_ENV !== 'production'; @@ -15,6 +17,8 @@ const handler = app.getRequestHandler(); const gameStore = new GameStore(); +const timedReleases = {}; + app.prepare().then(() => { const httpServer = createServer(handler); @@ -25,6 +29,25 @@ app.prepare().then(() => { io.to(gameUpdate.spectatorID).emit(event, omit(gameUpdate, 'dmID')); }; + const timedRelease = (event: string, gameUpdate: GameUpdate, threshold: number) => { + const now = Date.now(); + const lastEvent = timedReleases[event]; + + if (lastEvent?.embargo >= now) { + clearTimeout(lastEvent.to); + const embargo = lastEvent.embargo - now; + + const to = setTimeout(() => { + broadcast(event, gameUpdate); + }, embargo); + + timedReleases[event] = { embargo, to }; + } else { + broadcast(event, gameUpdate); + timedReleases[event] = { embargo: now + threshold }; + } + }; + io.on('connection', (socket: Socket) => { //console.log(Date.now(), `Client connected: ${socket.id}`); @@ -122,7 +145,7 @@ app.prepare().then(() => { socket.on('tilt', ({ cardIndex, tilt }: { cardIndex: number; tilt: Tilt }) => { try { const gameState = gameStore.tilt(socket.id, cardIndex, tilt); - broadcast('game-update', gameState); + timedRelease('game-update', gameState, thirtyFPS); } catch (e) { const error = e instanceof Error ? e.message : e; console.error(Date.now(), 'Error[tilt]', error); diff --git a/tools/throttle.ts b/tools/throttle.ts new file mode 100644 index 0000000..db1f15a --- /dev/null +++ b/tools/throttle.ts @@ -0,0 +1,12 @@ +export default function throttle(func: Function, threshold: number) { + let lastCall = 0; + + return (...args: any[]) => { + const now = Date.now(); + + if (now - lastCall >= threshold) { + lastCall = now; + func(...args); + } + }; +}