Switch sending/state changes to reducer to prep for optimizations
This commit is contained in:
parent
c74e4f8ce0
commit
6a43b5c0dc
|
@ -5,41 +5,38 @@ import { v4 } from 'uuid';
|
||||||
|
|
||||||
import { assertIsDefined, assertNever, noop, reloadOutdatedPage, websocketUrl } from '../common';
|
import { assertIsDefined, assertNever, noop, reloadOutdatedPage, websocketUrl } from '../common';
|
||||||
import { useServerTime } from '../hooks/useServerTime';
|
import { useServerTime } from '../hooks/useServerTime';
|
||||||
import { version } from '../metadata.json';
|
import { version as codiesVersion } from '../metadata.json';
|
||||||
import { ClientNote, ServerNote, State, StatePlayer, TimeResponse, WordPack } from '../protocol';
|
import { ClientNote, PartialClientNote, ServerNote, State, StatePlayer, TimeResponse, WordPack } from '../protocol';
|
||||||
import { GameView, Sender } from './gameView';
|
import { GameView, Sender } from './gameView';
|
||||||
import { Loading } from './loading';
|
import { Loading } from './loading';
|
||||||
|
|
||||||
const socketUrl = websocketUrl('/api/ws');
|
const socketUrl = websocketUrl('/api/ws');
|
||||||
|
|
||||||
function useSender(sendNote: (r: ClientNote) => void, version: number): Sender {
|
function useSender(dispatch: (action: PartialClientNote) => void): Sender {
|
||||||
return React.useMemo<Sender>(() => {
|
return React.useMemo<Sender>(() => {
|
||||||
return {
|
return {
|
||||||
reveal: (row: number, col: number) =>
|
reveal: (row: number, col: number) =>
|
||||||
sendNote({
|
dispatch({
|
||||||
method: 'reveal',
|
method: 'reveal',
|
||||||
version,
|
|
||||||
params: {
|
params: {
|
||||||
row,
|
row,
|
||||||
col,
|
col,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
newGame: () => sendNote({ method: 'newGame', version, params: {} }),
|
newGame: () => dispatch({ method: 'newGame', params: {} }),
|
||||||
endTurn: () => sendNote({ method: 'endTurn', version, params: {} }),
|
endTurn: () => dispatch({ method: 'endTurn', params: {} }),
|
||||||
changeNickname: (nickname: string) => sendNote({ method: 'changeNickname', version, params: { nickname } }),
|
changeNickname: (nickname: string) => dispatch({ method: 'changeNickname', params: { nickname } }),
|
||||||
changeRole: (spymaster: boolean) => sendNote({ method: 'changeRole', version, params: { spymaster } }),
|
changeRole: (spymaster: boolean) => dispatch({ method: 'changeRole', params: { spymaster } }),
|
||||||
changeTeam: (team: number) => sendNote({ method: 'changeTeam', version, params: { team } }),
|
changeTeam: (team: number) => dispatch({ method: 'changeTeam', params: { team } }),
|
||||||
randomizeTeams: () => sendNote({ method: 'randomizeTeams', version, params: {} }),
|
randomizeTeams: () => dispatch({ method: 'randomizeTeams', params: {} }),
|
||||||
changePack: (num: number, enable: boolean) =>
|
changePack: (num: number, enable: boolean) => dispatch({ method: 'changePack', params: { num, enable } }),
|
||||||
sendNote({ method: 'changePack', version, params: { num, enable } }),
|
changeTurnMode: (timed: boolean) => dispatch({ method: 'changeTurnMode', params: { timed } }),
|
||||||
changeTurnMode: (timed: boolean) => sendNote({ method: 'changeTurnMode', version, params: { timed } }),
|
changeTurnTime: (seconds: number) => dispatch({ method: 'changeTurnTime', params: { seconds } }),
|
||||||
changeTurnTime: (seconds: number) => sendNote({ method: 'changeTurnTime', version, params: { seconds } }),
|
addPacks: (packs: WordPack[]) => dispatch({ method: 'addPacks', params: { packs } }),
|
||||||
addPacks: (packs: WordPack[]) => sendNote({ method: 'addPacks', version, params: { packs } }),
|
removePack: (num: number) => dispatch({ method: 'removePack', params: { num } }),
|
||||||
removePack: (num: number) => sendNote({ method: 'removePack', version, params: { num } }),
|
changeHideBomb: (hideBomb: boolean) => dispatch({ method: 'changeHideBomb', params: { hideBomb } }),
|
||||||
changeHideBomb: (hideBomb: boolean) =>
|
|
||||||
sendNote({ method: 'changeHideBomb', version, params: { hideBomb } }),
|
|
||||||
};
|
};
|
||||||
}, [sendNote, version]);
|
}, [dispatch]);
|
||||||
}
|
}
|
||||||
|
|
||||||
function usePlayer(playerID: string, state?: State): { pState: StatePlayer; pTeam: number } | undefined {
|
function usePlayer(playerID: string, state?: State): { pState: StatePlayer; pTeam: number } | undefined {
|
||||||
|
@ -71,7 +68,7 @@ function useWS(roomID: string, playerID: string, nickname: string, dead: () => v
|
||||||
//
|
//
|
||||||
// X-CODIES-VERSION would be cleaner, but the WS hook doesn't
|
// X-CODIES-VERSION would be cleaner, but the WS hook doesn't
|
||||||
// support anything but query params.
|
// support anything but query params.
|
||||||
queryParams: { roomID: roomID, playerID: playerID, nickname: nickname, codiesVersion: version },
|
queryParams: { roomID: roomID, playerID: playerID, nickname: nickname, codiesVersion: codiesVersion },
|
||||||
reconnectAttempts,
|
reconnectAttempts,
|
||||||
onMessage: () => {
|
onMessage: () => {
|
||||||
retry.current = 0;
|
retry.current = 0;
|
||||||
|
@ -99,35 +96,77 @@ function useWS(roomID: string, playerID: string, nickname: string, dead: () => v
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function syncTime(setOffset: (offset: number) => void) {
|
function useSyncedServerTime() {
|
||||||
const fn = async () => {
|
const { setOffset } = useServerTime();
|
||||||
let bestRTT: number | undefined;
|
|
||||||
let offset = 0;
|
|
||||||
|
|
||||||
for (let i = 0; i < 3; i++) {
|
const syncTime = React.useCallback(() => {
|
||||||
const before = Date.now();
|
const fn = async () => {
|
||||||
const resp = await fetch('/api/time');
|
let bestRTT: number | undefined;
|
||||||
const after = Date.now();
|
let offset = 0;
|
||||||
|
|
||||||
const body = await resp.json();
|
for (let i = 0; i < 3; i++) {
|
||||||
if (resp.ok) {
|
const before = Date.now();
|
||||||
const rtt = (after - before) / 2;
|
const resp = await fetch('/api/time');
|
||||||
|
const after = Date.now();
|
||||||
|
|
||||||
if (bestRTT !== undefined && rtt > bestRTT) {
|
const body = await resp.json();
|
||||||
continue;
|
if (resp.ok) {
|
||||||
|
const rtt = (after - before) / 2;
|
||||||
|
|
||||||
|
if (bestRTT !== undefined && rtt > bestRTT) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
bestRTT = rtt;
|
||||||
|
|
||||||
|
const t = TimeResponse.parse(body);
|
||||||
|
const serverTime = t.time.getTime() + rtt;
|
||||||
|
offset = serverTime - Date.now();
|
||||||
}
|
}
|
||||||
|
|
||||||
bestRTT = rtt;
|
|
||||||
|
|
||||||
const t = TimeResponse.parse(body);
|
|
||||||
const serverTime = t.time.getTime() + rtt;
|
|
||||||
offset = serverTime - Date.now();
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
setOffset(offset);
|
setOffset(offset);
|
||||||
};
|
};
|
||||||
fn().catch(noop);
|
fn().catch(noop);
|
||||||
|
}, [setOffset]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const interval = window.setInterval(() => {
|
||||||
|
syncTime();
|
||||||
|
}, 10 * 60 * 1000);
|
||||||
|
return () => window.clearInterval(interval);
|
||||||
|
}, [syncTime]);
|
||||||
|
|
||||||
|
return syncTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
type StateAction = { method: 'setState'; state: State } | PartialClientNote;
|
||||||
|
|
||||||
|
function useStateReducer(sendNote: (r: ClientNote) => void) {
|
||||||
|
// TODO: Create a new state which contains the server state.
|
||||||
|
// TODO: Put sendNote in the state instead of reffing it?
|
||||||
|
const sendNoteRef = React.useRef(sendNote);
|
||||||
|
sendNoteRef.current = sendNote;
|
||||||
|
|
||||||
|
return React.useCallback(
|
||||||
|
(state: State | undefined, action: StateAction): State | undefined => {
|
||||||
|
if (state === undefined) {
|
||||||
|
if (action.method === 'setState') {
|
||||||
|
return action.state;
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (action.method) {
|
||||||
|
case 'setState':
|
||||||
|
return action.state;
|
||||||
|
default:
|
||||||
|
sendNoteRef.current({ ...action, version: state.version });
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[sendNoteRef]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GameProps {
|
export interface GameProps {
|
||||||
|
@ -139,21 +178,13 @@ export interface GameProps {
|
||||||
export const Game = (props: GameProps) => {
|
export const Game = (props: GameProps) => {
|
||||||
const [playerID] = React.useState(v4);
|
const [playerID] = React.useState(v4);
|
||||||
const nickname = React.useRef(props.nickname); // Preserve a nickname for use in reconnects.
|
const nickname = React.useRef(props.nickname); // Preserve a nickname for use in reconnects.
|
||||||
const [state, setState] = React.useState<State | undefined>();
|
|
||||||
const { setOffset } = useServerTime();
|
|
||||||
|
|
||||||
const { sendJsonMessage, lastJsonMessage } = useWS(props.roomID, playerID, nickname.current, props.leave, () =>
|
const syncTime = useSyncedServerTime();
|
||||||
syncTime(setOffset)
|
const { sendJsonMessage, lastJsonMessage } = useWS(props.roomID, playerID, nickname.current, props.leave, syncTime);
|
||||||
);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
const reducer = useStateReducer(sendJsonMessage);
|
||||||
const interval = window.setInterval(() => {
|
const [state, dispatch] = React.useReducer(reducer, undefined);
|
||||||
syncTime(setOffset);
|
const send = useSender(dispatch);
|
||||||
}, 10 * 60 * 1000);
|
|
||||||
return () => window.clearInterval(interval);
|
|
||||||
}, [setOffset]);
|
|
||||||
|
|
||||||
const send = useSender(sendJsonMessage, state?.version ?? 0);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (!lastJsonMessage) {
|
if (!lastJsonMessage) {
|
||||||
|
@ -164,7 +195,7 @@ export const Game = (props: GameProps) => {
|
||||||
|
|
||||||
switch (note.method) {
|
switch (note.method) {
|
||||||
case 'state':
|
case 'state':
|
||||||
setState(note.params);
|
dispatch({ method: 'setState', state: note.params });
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
assertNever(note.method);
|
assertNever(note.method);
|
||||||
|
|
|
@ -11,69 +11,71 @@ const WordPack = myzod.object({
|
||||||
words: myzod.array(myzod.string()),
|
words: myzod.array(myzod.string()),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export type PartialClientNote = Infer<typeof PartialClientNote>;
|
||||||
|
export type PartialClientNoteSender = (r: PartialClientNote) => void;
|
||||||
|
const PartialClientNote = myzod.union([
|
||||||
|
myzod.object({
|
||||||
|
method: myzod.literal('newGame'),
|
||||||
|
params: myzod.object({}),
|
||||||
|
}),
|
||||||
|
myzod.object({
|
||||||
|
method: myzod.literal('endTurn'),
|
||||||
|
params: myzod.object({}),
|
||||||
|
}),
|
||||||
|
myzod.object({
|
||||||
|
method: myzod.literal('randomizeTeams'),
|
||||||
|
params: myzod.object({}),
|
||||||
|
}),
|
||||||
|
myzod.object({
|
||||||
|
method: myzod.literal('reveal'),
|
||||||
|
params: myzod.object({ row: myzod.number(), col: myzod.number() }),
|
||||||
|
}),
|
||||||
|
myzod.object({
|
||||||
|
method: myzod.literal('changeTeam'),
|
||||||
|
params: myzod.object({ team: myzod.number() }),
|
||||||
|
}),
|
||||||
|
myzod.object({
|
||||||
|
method: myzod.literal('changeNickname'),
|
||||||
|
params: myzod.object({ nickname: myzod.string() }),
|
||||||
|
}),
|
||||||
|
myzod.object({
|
||||||
|
method: myzod.literal('changeRole'),
|
||||||
|
params: myzod.object({ spymaster: myzod.boolean() }),
|
||||||
|
}),
|
||||||
|
myzod.object({
|
||||||
|
method: myzod.literal('changePack'),
|
||||||
|
params: myzod.object({ num: myzod.number(), enable: myzod.boolean() }),
|
||||||
|
}),
|
||||||
|
myzod.object({
|
||||||
|
method: myzod.literal('changeTurnMode'),
|
||||||
|
params: myzod.object({ timed: myzod.boolean() }),
|
||||||
|
}),
|
||||||
|
myzod.object({
|
||||||
|
method: myzod.literal('changeTurnTime'),
|
||||||
|
params: myzod.object({ seconds: myzod.number() }),
|
||||||
|
}),
|
||||||
|
myzod.object({
|
||||||
|
method: myzod.literal('addPacks'),
|
||||||
|
params: myzod.object({
|
||||||
|
packs: myzod.array(WordPack),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
myzod.object({
|
||||||
|
method: myzod.literal('removePack'),
|
||||||
|
params: myzod.object({ num: myzod.number() }),
|
||||||
|
}),
|
||||||
|
myzod.object({
|
||||||
|
method: myzod.literal('changeHideBomb'),
|
||||||
|
params: myzod.object({ hideBomb: myzod.boolean() }),
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
export type ClientNote = Infer<typeof ClientNote>;
|
export type ClientNote = Infer<typeof ClientNote>;
|
||||||
export const ClientNote = myzod
|
export const ClientNote = myzod
|
||||||
.object({
|
.object({
|
||||||
version: myzod.number(),
|
version: myzod.number(),
|
||||||
})
|
})
|
||||||
.and(
|
.and(PartialClientNote);
|
||||||
myzod.union([
|
|
||||||
myzod.object({
|
|
||||||
method: myzod.literal('newGame'),
|
|
||||||
params: myzod.object({}),
|
|
||||||
}),
|
|
||||||
myzod.object({
|
|
||||||
method: myzod.literal('endTurn'),
|
|
||||||
params: myzod.object({}),
|
|
||||||
}),
|
|
||||||
myzod.object({
|
|
||||||
method: myzod.literal('randomizeTeams'),
|
|
||||||
params: myzod.object({}),
|
|
||||||
}),
|
|
||||||
myzod.object({
|
|
||||||
method: myzod.literal('reveal'),
|
|
||||||
params: myzod.object({ row: myzod.number(), col: myzod.number() }),
|
|
||||||
}),
|
|
||||||
myzod.object({
|
|
||||||
method: myzod.literal('changeTeam'),
|
|
||||||
params: myzod.object({ team: myzod.number() }),
|
|
||||||
}),
|
|
||||||
myzod.object({
|
|
||||||
method: myzod.literal('changeNickname'),
|
|
||||||
params: myzod.object({ nickname: myzod.string() }),
|
|
||||||
}),
|
|
||||||
myzod.object({
|
|
||||||
method: myzod.literal('changeRole'),
|
|
||||||
params: myzod.object({ spymaster: myzod.boolean() }),
|
|
||||||
}),
|
|
||||||
myzod.object({
|
|
||||||
method: myzod.literal('changePack'),
|
|
||||||
params: myzod.object({ num: myzod.number(), enable: myzod.boolean() }),
|
|
||||||
}),
|
|
||||||
myzod.object({
|
|
||||||
method: myzod.literal('changeTurnMode'),
|
|
||||||
params: myzod.object({ timed: myzod.boolean() }),
|
|
||||||
}),
|
|
||||||
myzod.object({
|
|
||||||
method: myzod.literal('changeTurnTime'),
|
|
||||||
params: myzod.object({ seconds: myzod.number() }),
|
|
||||||
}),
|
|
||||||
myzod.object({
|
|
||||||
method: myzod.literal('addPacks'),
|
|
||||||
params: myzod.object({
|
|
||||||
packs: myzod.array(WordPack),
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
myzod.object({
|
|
||||||
method: myzod.literal('removePack'),
|
|
||||||
params: myzod.object({ num: myzod.number() }),
|
|
||||||
}),
|
|
||||||
myzod.object({
|
|
||||||
method: myzod.literal('changeHideBomb'),
|
|
||||||
params: myzod.object({ hideBomb: myzod.boolean() }),
|
|
||||||
}),
|
|
||||||
])
|
|
||||||
);
|
|
||||||
|
|
||||||
// Messages sent from server to client.
|
// Messages sent from server to client.
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue