diff --git a/src/client/Api.ts b/src/client/Api.ts index 9456f88db..2c85544c5 100644 --- a/src/client/Api.ts +++ b/src/client/Api.ts @@ -47,34 +47,42 @@ export async function fetchPlayerById( return false; } } -export async function getUserMe(): Promise { - try { - const userAuthResult = await userAuth(); - if (!userAuthResult) return false; - const { jwt } = userAuthResult; - // Get the user object - const response = await fetch(getApiBase() + "/users/@me", { - headers: { - authorization: `Bearer ${jwt}`, - }, - }); - if (response.status === 401) { - await logOut(); - return false; - } - if (response.status !== 200) return false; - const body = await response.json(); - const result = UserMeResponseSchema.safeParse(body); - if (!result.success) { - const error = z.prettifyError(result.error); - console.error("Invalid response", error); - return false; - } - return result.data; - } catch (e) { - return false; +let __userMe: Promise | null = null; +export async function getUserMe(): Promise { + if (__userMe !== null) { + return __userMe; } + __userMe = (async () => { + try { + const userAuthResult = await userAuth(); + if (!userAuthResult) return false; + const { jwt } = userAuthResult; + + // Get the user object + const response = await fetch(getApiBase() + "/users/@me", { + headers: { + authorization: `Bearer ${jwt}`, + }, + }); + if (response.status === 401) { + await logOut(); + return false; + } + if (response.status !== 200) return false; + const body = await response.json(); + const result = UserMeResponseSchema.safeParse(body); + if (!result.success) { + const error = z.prettifyError(result.error); + console.error("Invalid response", error); + return false; + } + return result.data; + } catch (e) { + return false; + } + })(); + return __userMe; } export async function createCheckoutSession( diff --git a/src/client/Matchmaking.ts b/src/client/Matchmaking.ts index e495ef131..c29931896 100644 --- a/src/client/Matchmaking.ts +++ b/src/client/Matchmaking.ts @@ -15,24 +15,15 @@ import { translateText } from "./Utils"; @customElement("matchmaking-modal") export class MatchmakingModal extends BaseModal { private gameCheckInterval: ReturnType | null = null; + private connectTimeout: ReturnType | null = null; @state() private connected = false; @state() private socket: WebSocket | null = null; @state() private gameID: string | null = null; - private elo = "unknown"; + private elo: number | "unknown" = "unknown"; constructor() { super(); this.id = "page-matchmaking"; - document.addEventListener("userMeResponse", (event: Event) => { - const customEvent = event as CustomEvent; - if (customEvent.detail) { - const userMeResponse = customEvent.detail as UserMeResponse; - this.elo = - userMeResponse.player?.leaderboard?.oneVone?.elo?.toString() ?? - "unknown"; - this.requestUpdate(); - } - }); } createRenderRoot() { @@ -125,18 +116,24 @@ export class MatchmakingModal extends BaseModal { ); this.socket.onopen = async () => { console.log("Connected to matchmaking server"); - setTimeout(() => { + this.connectTimeout = setTimeout(async () => { + if (this.socket?.readyState !== WebSocket.OPEN) { + console.warn("[Matchmaking] socket not ready"); + return; + } // Set a delay so the user can see the "connecting" message, // otherwise the "searching" message will be shown immediately. + // Also wait so people who back out immediately aren't added + // to the matchmaking queue. + this.socket.send( + JSON.stringify({ + type: "join", + jwt: await getPlayToken(), + }), + ); this.connected = true; this.requestUpdate(); - }, 1000); - this.socket?.send( - JSON.stringify({ - type: "join", - jwt: await getPlayToken(), - }), - ); + }, 2000); }; this.socket.onmessage = (event) => { console.log(event.data); @@ -145,6 +142,7 @@ export class MatchmakingModal extends BaseModal { this.socket?.close(); console.log(`matchmaking: got game ID: ${data.gameId}`); this.gameID = data.gameId; + this.gameCheckInterval = setInterval(() => this.checkGame(), 1000); } }; this.socket.onerror = (event: ErrorEvent) => { @@ -157,7 +155,6 @@ export class MatchmakingModal extends BaseModal { protected async onOpen(): Promise { const userMe = await getUserMe(); - // Early return if modal was closed during async operation if (!this.isModalOpen) { return; @@ -180,15 +177,21 @@ export class MatchmakingModal extends BaseModal { this.close(); return; } + + this.elo = userMe.player.leaderboard?.oneVone?.elo ?? "unknown"; + this.connected = false; this.gameID = null; this.connect(); - this.gameCheckInterval = setInterval(() => this.checkGame(), 1000); } protected onClose(): void { this.connected = false; this.socket?.close(); + if (this.connectTimeout) { + clearTimeout(this.connectTimeout); + this.connectTimeout = null; + } if (this.gameCheckInterval) { clearInterval(this.gameCheckInterval); this.gameCheckInterval = null; @@ -263,7 +266,7 @@ export class MatchmakingButton extends LitElement { } render() { - const button = this.isLoggedIn + return this.isLoggedIn ? html` + ` : html` `; - - return html` ${button} `; } private handleLoggedInClick() { diff --git a/src/client/components/BaseModal.ts b/src/client/components/BaseModal.ts index db44e5485..0f7d7b2e4 100644 --- a/src/client/components/BaseModal.ts +++ b/src/client/components/BaseModal.ts @@ -25,6 +25,16 @@ export abstract class BaseModal extends LitElement { return this; } + protected firstUpdated(): void { + if (this.modalEl) { + this.modalEl.onClose = () => { + if (this.isModalOpen) { + this.close(); + } + }; + } + } + disconnectedCallback() { this.unregisterEscapeHandler(); super.disconnectedCallback();