diff --git a/src/components/DoomCanvas/DoomCanvas.tsx b/src/components/DoomCanvas/DoomCanvas.tsx index 9fc492c6..0d3176ab 100644 --- a/src/components/DoomCanvas/DoomCanvas.tsx +++ b/src/components/DoomCanvas/DoomCanvas.tsx @@ -3,9 +3,8 @@ import { EGameType, EmscriptenModule, NewGameResponse } from "../../types"; import { useAppContext } from "../../context/useAppContext"; import { HydraMultiplayer } from "../../utils/hydra-multiplayer"; import useKeys from "../../hooks/useKeys"; -import { getArgs } from "../../utils/game"; +import { getArgs, getBaseUrl } from "../../utils/game"; import { useMutation } from "@tanstack/react-query"; -import { SERVER_URL } from "../../constants"; import Card from "../Card"; import { FaRegCircleCheck } from "react-icons/fa6"; import { MdContentCopy } from "react-icons/md"; @@ -16,17 +15,19 @@ const DoomCanvas: React.FC = () => { const isEffectRan = useRef(false); const { gameData: { code, petName, type }, + region, } = useAppContext(); const keys = useKeys(); const urlClipboard = useClipboard({ copiedTimeout: 1500 }); + const baseUrl = getBaseUrl(region); const { mutate: fetchGameData, data } = useMutation({ mutationKey: ["fetchGameData", keys.address, code, type], mutationFn: async () => { const url = type === EGameType.HOST - ? `${SERVER_URL}new_game?address=${keys.address}` - : `${SERVER_URL}add_player?address=${keys.address}&id=${code}`; + ? `${baseUrl}/new_game?address=${keys.address}` + : `${baseUrl}/add_player?address=${keys.address}&id=${code}`; const response = await fetch(url); return response.json(); }, @@ -42,10 +43,10 @@ const DoomCanvas: React.FC = () => { ); useEffect(() => { - if (!keys.address) return; + if (!keys.address || !region) return; fetchGameData(); - }, [fetchGameData, keys.address]); + }, [fetchGameData, keys.address, region]); useEffect(() => { if (!keys.address || !data?.ip) return; diff --git a/src/components/InitialView/InitialView.tsx b/src/components/InitialView/InitialView.tsx index 39914e3f..bd4657bf 100644 --- a/src/components/InitialView/InitialView.tsx +++ b/src/components/InitialView/InitialView.tsx @@ -2,7 +2,6 @@ import { FC, useEffect, useState } from "react"; import Button from "../Button"; import hydraText from "../../assets/images/hydra-text.png"; import Modal from "../Modal"; -import SelectContinentDialog from "../SelectContinentDialog"; import Layout from "../Layout"; import SetNameModal from "../SetNameModal"; import { useAppContext } from "../../context/useAppContext"; @@ -25,8 +24,6 @@ const InitialView: FC = ({ startGame }) => { const [isNameModalOpen, setIsNameModalOpen] = useState( pathSegments[0] === EGameType.JOIN, ); - const [isSelectContinentModalOpen, setIsSelectContinentModalOpen] = - useState(false); const code = pathSegments[1]; useEffect(() => { @@ -111,11 +108,6 @@ const InitialView: FC = ({ startGame }) => {

- setIsSelectContinentModalOpen(false)} - isOpen={isSelectContinentModalOpen} - startGame={startGame} - /> setIsNameModalOpen(false)} isOpen={isNameModalOpen} diff --git a/src/components/SelectContinentDialog/SelectContinentDialog.tsx b/src/components/SelectContinentDialog/SelectContinentDialog.tsx deleted file mode 100644 index e4504937..00000000 --- a/src/components/SelectContinentDialog/SelectContinentDialog.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import { ChangeEventHandler, FC } from "react"; -import Modal from "../Modal"; -import Button from "../Button"; -import { REGIONS } from "../../constants"; -import { useAppContext } from "../../context/useAppContext"; - -interface SelectContinentDialogProps { - close: () => void; - isOpen: boolean; - startGame: () => void; -} - -const SelectContinentDialog: FC = ({ - close, - isOpen, - startGame, -}) => { - const { region, setRegion } = useAppContext(); - - const handleChange: ChangeEventHandler = (event) => { - setRegion(REGIONS.find((r) => r.value === event.target.value)!); - }; - - return ( - -

Select your continent

-
-
    - {REGIONS.map((continent) => ( -
  • - -
  • - ))} -
- -
-
- ); -}; - -export default SelectContinentDialog; diff --git a/src/components/SelectContinentDialog/index.ts b/src/components/SelectContinentDialog/index.ts deleted file mode 100644 index f83ca1c1..00000000 --- a/src/components/SelectContinentDialog/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from "./SelectContinentDialog"; diff --git a/src/constants.ts b/src/constants.ts index c91ec163..0230ff40 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -4,14 +4,5 @@ export const HANDLE_CACHE_KEY = "player-handle-cache"; export const HYDRA_DOOM_SESSION_KEY = "hydra-doom-session-key"; export const MAX_SPEED = 40; export const REGION = import.meta.env.VITE_REGION; -export const REGIONS = [ - { name: "Ohio, NA", value: "us-east-2" }, - { name: "Oregon, NA", value: "us-west-2" }, - { name: "Frankfurt, Europe", value: "eu-central-1" }, - { name: "Cape Town, Africa", value: "af-south-1" }, - { name: "Melbourne, Australia", value: "ap-southeast-4" }, - { name: "Seoul, Asia", value: "ap-northeast-2" }, - { name: "Sao Paulo, SA", value: "sa-east-1" }, -]; -export const SERVER_URL = import.meta.env.VITE_SERVER_URL; +export const REGIONS = ["us-east-1"]; export const TIC_RATE_MAGIC = 35; // 35 is the ticrate in DOOM WASM they use to calculate time. diff --git a/src/context/AppContextProvider.tsx b/src/context/AppContextProvider.tsx index 7ab42a5f..6a735dcf 100644 --- a/src/context/AppContextProvider.tsx +++ b/src/context/AppContextProvider.tsx @@ -1,25 +1,24 @@ import { FC, PropsWithChildren, useMemo, useState } from "react"; import { AppContext } from "./useAppContext"; +import { EGameType } from "../types"; +import useBestRegion from "../hooks/useBestRegion"; import { REGIONS } from "../constants"; -import { EGameType, Region } from "../types"; const AppContextProvider: FC = ({ children }) => { + const { bestRegion } = useBestRegion(REGIONS); const [gameData, setGameData] = useState({ code: "", petName: "", type: EGameType.SOLO, }); - const [region, setRegion] = useState(REGIONS[0]); - const value = useMemo( () => ({ gameData, - region, + region: bestRegion, setGameData, - setRegion, }), - [gameData, region], + [gameData, bestRegion], ); return {children}; diff --git a/src/context/useAppContext.ts b/src/context/useAppContext.ts index a37d30ee..de53e3fc 100644 --- a/src/context/useAppContext.ts +++ b/src/context/useAppContext.ts @@ -1,19 +1,16 @@ import { createContext, Dispatch, useContext } from "react"; -import { EGameType, GameData, Region } from "../types"; -import { REGIONS } from "../constants"; +import { EGameType, GameData } from "../types"; interface AppContextInterface { gameData: GameData; - region: Region; + region: string | null; setGameData: Dispatch>; - setRegion: Dispatch>; } export const AppContext = createContext({ gameData: { petName: "", code: "", type: EGameType.SOLO }, - region: REGIONS[0], + region: null, setGameData: () => {}, - setRegion: () => {}, }); export const useAppContext = () => { diff --git a/src/hooks/useBestRegion.ts b/src/hooks/useBestRegion.ts new file mode 100644 index 00000000..c8c07055 --- /dev/null +++ b/src/hooks/useBestRegion.ts @@ -0,0 +1,68 @@ +import { useState, useEffect } from "react"; +import { useQueries } from "@tanstack/react-query"; + +interface ServerHealth { + region: string; + latency: number; + error?: string; +} + +const useBestRegion = (regions: string[]) => { + const [bestRegion, setBestRegion] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [isError, setIsError] = useState(false); + + const checkServerHealth = async (region: string): Promise => { + const url = `http://api.${region}.hydra-doom.sundae.fi/health`; + + try { + const start = performance.now(); + const response = await fetch(url, { method: "HEAD" }); + const end = performance.now(); + + if (!response.ok) throw new Error("Server not healthy"); + + return { + region, + latency: end - start, + }; + } catch (error: any) { + return { + region, + latency: Infinity, + error: error.message, + }; + } + }; + + const results = useQueries({ + queries: regions.map((region) => ({ + queryKey: ["serverHealth", region], + queryFn: () => checkServerHealth(region), + staleTime: Infinity, + cacheTime: 0, + })), + }); + + useEffect(() => { + if (results.every((result) => result.isSuccess || result.isError)) { + const successfulResults = results + .filter((result) => result.isSuccess && result.data) + .map((result) => result.data as ServerHealth); + + if (successfulResults.length > 0) { + const sortedByLatency = successfulResults.sort( + (a, b) => a.latency - b.latency, + ); + setBestRegion(sortedByLatency[0].region); + } else { + setIsError(true); + } + setIsLoading(false); + } + }, [results]); + + return { bestRegion, isLoading, isError }; +}; + +export default useBestRegion; diff --git a/src/types.ts b/src/types.ts index a2bae381..36a808b6 100644 --- a/src/types.ts +++ b/src/types.ts @@ -34,11 +34,6 @@ export interface NewGameResponse { player_state: string; } -export interface Region { - name: string; - value: string; -} - interface FileSystem { createPreloadedFile( parent: string, diff --git a/src/utils/game.ts b/src/utils/game.ts index 39591637..c2901cd7 100644 --- a/src/utils/game.ts +++ b/src/utils/game.ts @@ -16,3 +16,8 @@ export const getArgs = ({ type, code, petName }: GameData) => { return args; }; + +export const getBaseUrl = (region: string | null, local = false) => { + if (local) return "http://localhost:3000"; + return `http://api.${region}.hydra-doom.sundae.fi`; +};