Fix matchmaking double join bug (#3065)

## Description:

There were several issues with the matchmaking modal:

1. It was defined twice (once before login, and once after login), so
players would sometimes join the matchmaking queue twice.
2. When clicking away from the modal (not clicking the back button), the
"onClose" callback was not triggered. So if a person closed & reopened
the modal, they would join twice'
3. Cache the userMe response so it can be called multiple times

## Please complete the following:

- [x] I have added screenshots for all UI updates
- [x] I process any text displayed to the user through translateText()
and I've added it to the en.json file
- [x] I have added relevant tests to the test directory
- [x] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced

## Please put your Discord username so you can be contacted if a bug or
regression is found:

evan
This commit is contained in:
Evan
2026-01-30 15:27:34 -08:00
committed by GitHub
parent 02dc5fc153
commit 6c2e0d1528
3 changed files with 70 additions and 50 deletions
+34 -26
View File
@@ -47,34 +47,42 @@ export async function fetchPlayerById(
return false;
}
}
export async function getUserMe(): Promise<UserMeResponse | false> {
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<UserMeResponse | false> | null = null;
export async function getUserMe(): Promise<UserMeResponse | false> {
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(
+26 -24
View File
@@ -15,24 +15,15 @@ import { translateText } from "./Utils";
@customElement("matchmaking-modal")
export class MatchmakingModal extends BaseModal {
private gameCheckInterval: ReturnType<typeof setInterval> | null = null;
private connectTimeout: ReturnType<typeof setTimeout> | 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<void> {
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`
<button
@click="${this.handleLoggedInClick}"
@@ -279,6 +282,7 @@ export class MatchmakingButton extends LitElement {
${translateText("matchmaking_button.description")}
</span>
</button>
<matchmaking-modal></matchmaking-modal>
`
: html`
<button
@@ -290,8 +294,6 @@ export class MatchmakingButton extends LitElement {
</span>
</button>
`;
return html` ${button} <matchmaking-modal></matchmaking-modal> `;
}
private handleLoggedInClick() {
+10
View File
@@ -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();