Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 36 additions & 23 deletions stores/MatchmakingStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,11 @@ export const useMatchmakingStore = defineStore("matchmaking", () => {
// change while its player sits in the list, so keep the ones already
// fetched and only ask the API for steam ids never seen before.
const profiles = new Map<string, any>();
// Every steam id already asked for, found or not: the api broadcasts the
// roster to every tab whenever anyone connects, so asking again for ids the
// api didn't return re-ran these queries in every tab on every broadcast.
const askedSteamIds = new Set<string>();
const MAX_ASKED_STEAM_IDS = 5000;

const rebuildPlayersOnline = () => {
playersOnline.value = onlinePlayerSteamIds.value
Expand All @@ -82,18 +87,14 @@ export const useMatchmakingStore = defineStore("matchmaking", () => {

const queryPlayers = async () => {
const steamIds = onlinePlayerSteamIds.value;
const online = new Set(steamIds.map(String));

// Bound the cache to who is actually online; otherwise a long-lived tab
// accumulates every player who has ever connected during its session.
for (const steamId of profiles.keys()) {
if (!online.has(steamId)) {
profiles.delete(steamId);
}
if (askedSteamIds.size > MAX_ASKED_STEAM_IDS) {
askedSteamIds.clear();
profiles.clear();
}

const missing = steamIds.filter(
(steamId) => !profiles.has(String(steamId)),
(steamId) => !askedSteamIds.has(String(steamId)),
);

// Players only left, or they are all already known -- no round trip, but
Expand All @@ -103,23 +104,35 @@ export const useMatchmakingStore = defineStore("matchmaking", () => {
return;
}

const { data } = await getGraphqlClient().query({
query: generateQuery({
players: [
{
where: {
steam_id: {
_in: $("steam_ids", "[bigint]!"),
for (const steamId of missing) {
askedSteamIds.add(String(steamId));
}

let data: any;
try {
({ data } = await getGraphqlClient().query({
query: generateQuery({
players: [
{
where: {
steam_id: {
_in: $("steam_ids", "[bigint]!"),
},
},
},
},
playerFields,
],
}),
variables: {
steam_ids: missing,
},
});
playerFields,
],
}),
variables: {
steam_ids: missing,
},
}));
} catch (error) {
for (const steamId of missing) {
askedSteamIds.delete(String(steamId));
}
throw error;
}

for (const player of data.players) {
profiles.set(String(player.steam_id), player);
Expand Down
108 changes: 108 additions & 0 deletions tests/stores/onlineRosterAsksOnce.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { print } from "graphql";
import { flushPromises } from "@vue/test-utils";
import { useMatchmakingStore } from "~/stores/MatchmakingStore";

vi.mock("~/web-sockets/Socket", async (importOriginal) => ({
...(await importOriginal<typeof import("~/web-sockets/Socket")>()),
default: {
connect() {},
rejoinAll() {},
listen: () => ({ stop() {} }),
joinLobby: () => ({ messages: [], on() {}, leave() {} }),
markLobbyRead() {},
},
}));

const ALPHA = "76561198000000101";
const BRAVO = "76561198000000102";
const CHARLIE = "76561198000000103";
const UNKNOWN = "76561198000000199";
const DELTA = "76561198000000104";
const ECHO = "76561198000000105";
const KNOWN = new Set([ALPHA, BRAVO, CHARLIE, DELTA, ECHO]);
let failNextPlayers = false;

let asked: Array<{ kind: "players" | "message_requests"; ids: string[] }> = [];

beforeEach(() => {
const client = useApollo().clients!.default;
vi.spyOn(client, "query").mockImplementation((async (options: any) => {
const document = print(options.query);
if (/allow_message_requests/.test(document)) {
asked.push({
kind: "message_requests",
ids: options.variables.steamIds.map(String),
});
return { data: { players: [] } };
}
if (options.variables?.steam_ids) {
const ids = options.variables.steam_ids.map(String);
if (failNextPlayers) {
failNextPlayers = false;
throw new Error("offline");
}
asked.push({ kind: "players", ids });
return {
data: {
players: ids
.filter((id: string) => KNOWN.has(id))
.map((steam_id: string) => ({ steam_id, name: steam_id })),
},
};
}
return { data: { __type: { fields: [] } } };
}) as any);
});

afterEach(() => {
asked = [];
vi.restoreAllMocks();
});

async function roster(
store: ReturnType<typeof useMatchmakingStore>,
ids: string[],
) {
store.onlinePlayerSteamIds = ids;
await new Promise((resolve) => setTimeout(resolve, 300));
await flushPromises();
}

describe("the online roster", () => {
it("asks about each player once, however often the roster is broadcast", async () => {
const store = useMatchmakingStore();

await roster(store, [ALPHA, BRAVO, UNKNOWN]);
await roster(store, [UNKNOWN, BRAVO, ALPHA]);
await roster(store, [ALPHA, UNKNOWN]);
await roster(store, [ALPHA, BRAVO, UNKNOWN]);
await roster(store, [ALPHA, BRAVO, UNKNOWN, CHARLIE]);

expect(asked.filter((a) => a.kind === "players")).toEqual([
{ kind: "players", ids: [ALPHA, BRAVO, UNKNOWN] },
{ kind: "players", ids: [CHARLIE] },
]);
expect(asked.filter((a) => a.kind === "message_requests")).toHaveLength(2);
expect(store.playersOnline.map((p: any) => p.steam_id)).toEqual([
ALPHA,
BRAVO,
CHARLIE,
]);
});

it("asks again for players whose lookup failed", async () => {
const store = useMatchmakingStore();
failNextPlayers = true;
const unhandled = vi.fn();
process.on("unhandledRejection", unhandled);

await roster(store, [DELTA]);
await roster(store, [DELTA, ECHO]);
process.off("unhandledRejection", unhandled);

expect(asked.filter((a) => a.kind === "players")).toEqual([
{ kind: "players", ids: [DELTA, ECHO] },
]);
});
});
Loading