mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 12:00:44 +00:00
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:
+34
-26
@@ -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
@@ -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() {
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user