diff --git a/hasura/migrations/default/1875000000000_matches_share_code/down.sql b/hasura/migrations/default/1875000000000_matches_share_code/down.sql new file mode 100644 index 00000000..0d917395 --- /dev/null +++ b/hasura/migrations/default/1875000000000_matches_share_code/down.sql @@ -0,0 +1,4 @@ +DROP INDEX IF EXISTS public.idx_matches_share_code; + +ALTER TABLE public.matches + DROP COLUMN IF EXISTS share_code; diff --git a/hasura/migrations/default/1875000000000_matches_share_code/up.sql b/hasura/migrations/default/1875000000000_matches_share_code/up.sql new file mode 100644 index 00000000..d93c7786 --- /dev/null +++ b/hasura/migrations/default/1875000000000_matches_share_code/up.sql @@ -0,0 +1,10 @@ +-- Our one stable handle on a Valve match: demo URLs expire, share codes don't, +-- so this is what lets us ask the GC about a match again later. It otherwise +-- only lives on pending_match_imports, which ParseImportedDemo deletes on a +-- successful import. +ALTER TABLE public.matches + ADD COLUMN IF NOT EXISTS share_code text; + +CREATE INDEX IF NOT EXISTS idx_matches_share_code + ON public.matches (share_code) + WHERE share_code IS NOT NULL; diff --git a/hasura/migrations/default/1875000000100_drop_pending_import_parties/down.sql b/hasura/migrations/default/1875000000100_drop_pending_import_parties/down.sql new file mode 100644 index 00000000..2b1d3c7b --- /dev/null +++ b/hasura/migrations/default/1875000000100_drop_pending_import_parties/down.sql @@ -0,0 +1,2 @@ +ALTER TABLE public.pending_match_imports + ADD COLUMN IF NOT EXISTS parties jsonb; diff --git a/hasura/migrations/default/1875000000100_drop_pending_import_parties/up.sql b/hasura/migrations/default/1875000000100_drop_pending_import_parties/up.sql new file mode 100644 index 00000000..99e934f7 --- /dev/null +++ b/hasura/migrations/default/1875000000100_drop_pending_import_parties/up.sql @@ -0,0 +1,6 @@ +-- Valve does not expose queue parties after a match ends. Verified at the wire +-- level: the reservation returned by MatchListRequestFullGameInfo contains only +-- account_ids, and the CS2 demo carries no party data anywhere. Nothing ever +-- wrote a non-null value here. +ALTER TABLE public.pending_match_imports + DROP COLUMN IF EXISTS parties; diff --git a/src/steam-match-history/jobs/ParseImportedDemo.ts b/src/steam-match-history/jobs/ParseImportedDemo.ts index 3243b075..5f7ab33b 100644 --- a/src/steam-match-history/jobs/ParseImportedDemo.ts +++ b/src/steam-match-history/jobs/ParseImportedDemo.ts @@ -6,7 +6,6 @@ import { PostgresService } from "../../postgres/postgres.service"; import { DemoParserService } from "../../demos/demo-parser.service"; import { SteamMatchHistoryQueues } from "../enums/SteamMatchHistoryQueues"; import { MatchImportService } from "../match-import.service"; -import { MatchParty } from "../types/MatchParty"; export type ParseImportedDemoPayload = { valve_match_id: string; @@ -16,7 +15,6 @@ export type ParseImportedDemoPayload = { share_code?: string; demo_url?: string | null; match_start_time?: string | null; - parties?: MatchParty[] | null; }; @UseQueue("SteamMatchHistory", SteamMatchHistoryQueues.ParseImportedDemo, { @@ -45,6 +43,18 @@ export class ParseImportedDemo extends WorkerHost { this.logger.log( `parse-imported-demo skip valve_match_id=${valve_match_id}: match already imported (${existing}); admin must delete the match to re-import`, ); + // The pending row is about to go and it holds the only copy of the share + // code, which is our one stable handle on this match. Harvest it first. + const pending = await this.postgres.query>( + `SELECT share_code + FROM public.pending_match_imports + WHERE valve_match_id = $1::numeric`, + [valve_match_id], + ); + await this.matchImport.persistShareCode( + existing, + pending.at(0)?.share_code ?? job.data.share_code, + ); await this.postgres.query( `DELETE FROM public.pending_match_imports WHERE valve_match_id = $1::numeric`, [valve_match_id], @@ -57,10 +67,9 @@ export class ParseImportedDemo extends WorkerHost { share_code: string; demo_url: string | null; match_start_time: string | null; - parties: MatchParty[] | null; }> >( - `SELECT share_code, demo_url, match_start_time, parties + `SELECT share_code, demo_url, match_start_time FROM public.pending_match_imports WHERE valve_match_id = $1::numeric`, [valve_match_id], @@ -76,7 +85,6 @@ export class ParseImportedDemo extends WorkerHost { row?.match_start_time ?? job.data.match_start_time ?? null; const demoUrl = row?.demo_url ?? job.data.demo_url ?? null; - const parties = row?.parties ?? job.data.parties ?? null; if (!demoUrl) { this.logger.warn( @@ -91,13 +99,7 @@ export class ParseImportedDemo extends WorkerHost { [valve_match_id], ); - await this.runImport( - valve_match_id, - shareCode, - demoUrl, - matchStartTime, - parties, - ); + await this.runImport(valve_match_id, shareCode, demoUrl, matchStartTime); } catch (err) { const lastAttempt = (job.attemptsMade ?? 0) >= (job.opts.attempts ?? 1) - 1; @@ -116,7 +118,6 @@ export class ParseImportedDemo extends WorkerHost { shareCode: string | null, demoUrl: string, matchStartTime: string | null, - parties: MatchParty[] | null, ): Promise { const parsed = await this.demoParser.parseFromUrl(demoUrl); if (!parsed) { @@ -131,7 +132,7 @@ export class ParseImportedDemo extends WorkerHost { demoUrl, matchStartTime, externalId: valveMatchId, - parties, + shareCode, }, ); if (!result.matchId) { diff --git a/src/steam-match-history/jobs/ResolveMatchMetadata.ts b/src/steam-match-history/jobs/ResolveMatchMetadata.ts index 46ad45b4..916b84fa 100644 --- a/src/steam-match-history/jobs/ResolveMatchMetadata.ts +++ b/src/steam-match-history/jobs/ResolveMatchMetadata.ts @@ -6,7 +6,6 @@ import { UseQueue } from "src/utilities/QueueProcessors"; import { PostgresService } from "../../postgres/postgres.service"; import { SteamMatchHistoryQueues } from "../enums/SteamMatchHistoryQueues"; import { SteamGcService, ResolvedMatch } from "../steam-gc.service"; -import { MatchParty } from "../types/MatchParty"; import { MatchImportService } from "../match-import.service"; import { ParseImportedDemo } from "./ParseImportedDemo"; @@ -38,10 +37,9 @@ export class ResolveMatchMetadata extends WorkerHost { share_code: string; demo_url: string | null; match_start_time: string | null; - parties: MatchParty[] | null; }> >( - `SELECT share_code, demo_url, match_start_time, parties + `SELECT share_code, demo_url, match_start_time FROM public.pending_match_imports WHERE valve_match_id = $1::numeric`, [valve_match_id], @@ -57,7 +55,6 @@ export class ResolveMatchMetadata extends WorkerHost { share_code: row.share_code, demo_url: row.demo_url, match_start_time: row.match_start_time, - parties: row.parties, }); return; } @@ -79,42 +76,39 @@ export class ResolveMatchMetadata extends WorkerHost { throw error; } if (!resolved) { - await this.markFailed(valve_match_id, "gc returned no demo url"); + await this.markFailed(valve_match_id, "gc returned no match"); return; } + const demoUrl = resolved.demoUrl ?? row.demo_url; + const matchStartTime = resolved.matchStartTime ?? - (await this.matchImport.resolveDemoStartTime(resolved.demoUrl)); + row.match_start_time ?? + (demoUrl ? await this.matchImport.resolveDemoStartTime(demoUrl) : null); - const partyCount = new Set( - (resolved.parties ?? []).map((party) => party.party_key), - ).size; this.logger.log( - `resolved valve_match_id=${valve_match_id} map=${resolved.mapName ?? ""} matchStartTime=${matchStartTime ?? ""} [source=${resolved.matchStartTime ? "gc-matchtime" : matchStartTime ? "demo-cdn-last-modified" : "none"}] parties=${partyCount} demoUrl=${resolved.demoUrl}`, + `resolved valve_match_id=${valve_match_id} map=${resolved.mapName ?? ""} matchStartTime=${matchStartTime ?? ""} [source=${resolved.matchStartTime ? "gc-matchtime" : matchStartTime ? "demo-cdn-last-modified" : "none"}] demoUrl=${demoUrl ?? ""}`, ); await this.postgres.query( `UPDATE public.pending_match_imports - SET map_name = $2, + SET map_name = coalesce($2, map_name), match_start_time = $3, - demo_url = $4, - parties = $5::jsonb + demo_url = $4 WHERE valve_match_id = $1::numeric`, - [ - valve_match_id, - resolved.mapName, - matchStartTime, - resolved.demoUrl, - resolved.parties ? JSON.stringify(resolved.parties) : null, - ], + [valve_match_id, resolved.mapName, matchStartTime, demoUrl], ); + if (!demoUrl) { + await this.markFailed(valve_match_id, "gc returned no demo url"); + return; + } + await this.enqueueParse(valve_match_id, { share_code: row.share_code, - demo_url: resolved.demoUrl, + demo_url: demoUrl, match_start_time: matchStartTime, - parties: resolved.parties, }); } @@ -124,7 +118,6 @@ export class ResolveMatchMetadata extends WorkerHost { share_code: string; demo_url: string | null; match_start_time: string | null; - parties: MatchParty[] | null; }, ): Promise { const jobId = `parse-${valveMatchId}`; diff --git a/src/steam-match-history/match-import.service.spec.ts b/src/steam-match-history/match-import.service.spec.ts index 10c6195e..14b227bc 100644 --- a/src/steam-match-history/match-import.service.spec.ts +++ b/src/steam-match-history/match-import.service.spec.ts @@ -25,14 +25,6 @@ const computeStartingSides = ( } ).computeStartingSides; -const mapPartyIds = ( - MatchImportService as unknown as { - mapPartyIds: ( - parties: Array<{ steam_id: string; party_key: string }> | null, - ) => Map; - } -).mapPartyIds; - describe("MatchImportService.detectMatchType", () => { it("uses the majority rank_type, not the first player", () => { const players = [ @@ -228,41 +220,3 @@ describe("MatchImportService.computeStartingSides", () => { expect(sides.get("A")).toBe("T"); }); }); - -describe("MatchImportService.mapPartyIds", () => { - it("gives everyone in the same source party one shared uuid", () => { - const ids = mapPartyIds([ - { steam_id: "A", party_key: "9" }, - { steam_id: "B", party_key: "9" }, - { steam_id: "C", party_key: "4" }, - { steam_id: "D", party_key: "4" }, - ]); - - expect(ids.get("A")).toBe(ids.get("B")); - expect(ids.get("C")).toBe(ids.get("D")); - expect(ids.get("A")).not.toBe(ids.get("C")); - expect(ids.get("A")).toMatch( - /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/, - ); - }); - - it("does not reuse a source party key across imports", () => { - // Valve party ids are only unique within their own match, so two - // different matches that both saw party "9" must not look like the - // same group. - const first = mapPartyIds([ - { steam_id: "A", party_key: "9" }, - { steam_id: "B", party_key: "9" }, - ]); - const second = mapPartyIds([ - { steam_id: "C", party_key: "9" }, - { steam_id: "D", party_key: "9" }, - ]); - expect(first.get("A")).not.toBe(second.get("C")); - }); - - it("leaves players out when there is no party data", () => { - expect(mapPartyIds(null).size).toBe(0); - expect(mapPartyIds([]).size).toBe(0); - }); -}); diff --git a/src/steam-match-history/match-import.service.ts b/src/steam-match-history/match-import.service.ts index e35b689f..57c2a9ac 100644 --- a/src/steam-match-history/match-import.service.ts +++ b/src/steam-match-history/match-import.service.ts @@ -16,22 +16,19 @@ import { e_match_party_sources_enum, e_match_types_enum, } from "../../generated"; -import { MatchParty } from "./types/MatchParty"; type MatchType = e_match_types_enum; type Side = "T" | "CT"; -// "lobby" is written by the assign_lobby_parties trigger, never here. -function toPartySource(source: string): e_match_party_sources_enum | null { - return source === "valve" ? "valve" : null; -} - export type ImportExternalDemoOptions = { demoUrl?: string; matchStartTime?: string | null; externalId?: string | null; sourceObjectKey?: string; - parties?: MatchParty[] | null; + // Our one stable handle on a Valve match — demo URLs expire, share codes + // don't. The pending import row that carries it is deleted once the demo + // parses, so it has to be copied onto the match. + shareCode?: string | null; }; type SteamPlayerSummary = { @@ -68,7 +65,7 @@ export class MatchImportService { sourceKey: string, options: ImportExternalDemoOptions = {}, ): Promise<{ matchId: string | null; skipped?: string }> { - const { demoUrl, matchStartTime, externalId, sourceObjectKey, parties } = + const { demoUrl, matchStartTime, externalId, sourceObjectKey, shareCode } = options; if (MatchImportService.isFaceitServer(parsed.server_name)) { @@ -103,6 +100,7 @@ export class MatchImportService { faceitMatchId, ); } + await this.persistShareCode(existing, shareCode ?? sourceKey); return { matchId: existing, skipped: "already imported" }; } @@ -148,30 +146,8 @@ export class MatchImportService { const lineup1Id = await this.insertLineup(); const lineup2Id = await this.insertLineup(); - // source is re-derived from the server name above and can be "5stack", - // which the party_source FK would reject. - const partySource = toPartySource(source); - const partyIds = partySource - ? MatchImportService.mapPartyIds(parties) - : new Map(); - if (partyIds.size > 0) { - this.logger.log( - `parties for ${source}/${sourceKey}: ${new Set(partyIds.values()).size} group(s) covering ${partyIds.size} player(s)`, - ); - } - - await this.insertLineupPlayers( - lineup1Id, - lineup1Players, - partyIds, - partySource, - ); - await this.insertLineupPlayers( - lineup2Id, - lineup2Players, - partyIds, - partySource, - ); + await this.insertLineupPlayers(lineup1Id, lineup1Players); + await this.insertLineupPlayers(lineup2Id, lineup2Players); let startedAt = matchStartTime ?? null; let startSource = startedAt ? "gc-matchtime" : "none"; @@ -205,6 +181,8 @@ export class MatchImportService { startedAt, }); + await this.persistShareCode(matchId, shareCode ?? sourceKey); + if (externalId) { await this.postgres.query( `UPDATE public.matches SET external_id = $2 WHERE id = $1::uuid`, @@ -608,25 +586,27 @@ export class MatchImportService { return null; } - // Source party ids are only unique within their own match, so each gets a - // fresh uuid. - private static mapPartyIds( - parties: MatchParty[] | null | undefined, - ): Map { - const byKey = new Map(); - const bySteamId = new Map(); - for (const party of parties ?? []) { - if (!party.steam_id || !party.party_key) { - continue; - } - let partyId = byKey.get(party.party_key); - if (!partyId) { - partyId = randomUUID(); - byKey.set(party.party_key, partyId); - } - bySteamId.set(party.steam_id, partyId); + // sourceKey is the share code for share-code imports and the valve match id + // for everything else, so the shape is the only way to tell them apart. + private static asShareCode(value: string | null | undefined): string | null { + return value && /^CSGO(-[a-zA-Z0-9]{5}){5}$/.test(value) ? value : null; + } + + public async persistShareCode( + matchId: string, + value: string | null | undefined, + ): Promise { + const shareCode = MatchImportService.asShareCode(value); + if (!shareCode) { + return; } - return bySteamId; + await this.postgres.query( + `UPDATE public.matches + SET share_code = $2 + WHERE id = $1::uuid + AND share_code IS DISTINCT FROM $2`, + [matchId, shareCode], + ); } private static splitLineupsBySide( @@ -1015,14 +995,10 @@ export class MatchImportService { private async insertLineupPlayers( lineupId: string, players: ParsedPlayer[], - parties?: Map, - partySource?: e_match_party_sources_enum | null, ): Promise { const objects = players.map((p) => ({ match_lineup_id: lineupId, steam_id: p.steam_id, - party_id: parties?.get(p.steam_id) ?? null, - party_source: parties?.has(p.steam_id) ? partySource : null, })); if (objects.length === 0) { return; diff --git a/src/steam-match-history/steam-gc.service.spec.ts b/src/steam-match-history/steam-gc.service.spec.ts index c7dccc5d..f934d34f 100644 --- a/src/steam-match-history/steam-gc.service.spec.ts +++ b/src/steam-match-history/steam-gc.service.spec.ts @@ -1,130 +1,59 @@ import { SteamGcService } from "./steam-gc.service"; -import { MatchParty } from "./types/MatchParty"; -// extractMatchInfo / extractParties are pure private statics; reach them -// directly rather than standing up the GC client. +// extractMatchInfo is a pure private static; reach it directly rather than +// standing up the GC client. const extractMatchInfo = ( SteamGcService as unknown as { extractMatchInfo: (matches: unknown) => { - demoUrl: string; + demoUrl: string | null; mapName: string | null; matchStartTime: string | null; - parties: MatchParty[] | null; } | null; } ).extractMatchInfo; -const extractParties = ( - SteamGcService as unknown as { - extractParties: (roundStats?: unknown[]) => MatchParty[] | null; - } -).extractParties; - -// account id -> steam64 -const steamId = (accountId: number) => - (BigInt(accountId) + 76561197960265728n).toString(); - -const groupsOf = (parties: MatchParty[] | null) => { - const byKey = new Map(); - for (const party of parties ?? []) { - byKey.set(party.party_key, [ - ...(byKey.get(party.party_key) ?? []), - party.steam_id, - ]); - } - return byKey; -}; - -describe("SteamGcService.extractParties", () => { - it("zips the index-aligned account_ids and party_ids", () => { - const parties = extractParties([ - { - reservation: { - // duo duo solo solo trio trio trio - account_ids: [111, 222, 333, 444, 555, 666, 777], - party_ids: [9, 9, 0, 0, 4, 4, 4], - }, - }, - ]); - - const groups = groupsOf(parties); - expect(groups.size).toBe(2); - expect(groups.get("9").sort()).toEqual([steamId(111), steamId(222)].sort()); - expect(groups.get("4").sort()).toEqual( - [steamId(555), steamId(666), steamId(777)].sort(), - ); - // party_id 0 means "queued alone" - expect(parties.map((p) => p.steam_id)).not.toContain(steamId(333)); - }); - - it("drops a party of one", () => { - const parties = extractParties([ - { - reservation: { - account_ids: [111, 222], - party_ids: [7, 8], - }, - }, - ]); - expect(parties).toBeNull(); - }); - - it("returns null when the GC gave no reservation", () => { - expect(extractParties([{ map: "http://demo" }])).toBeNull(); - expect(extractParties([])).toBeNull(); - expect(extractParties(undefined)).toBeNull(); - expect(extractParties([{ reservation: {} }])).toBeNull(); - }); - - it("uses the last entry that carries both halves of the pairing", () => { - const parties = extractParties([ - { reservation: { account_ids: [111, 222] } }, - { - reservation: { - account_ids: [333, 444], - party_ids: [5, 5], - }, - }, - { map: "http://demo" }, - ]); - expect(groupsOf(parties).get("5").sort()).toEqual( - [steamId(333), steamId(444)].sort(), - ); - }); -}); - describe("SteamGcService.extractMatchInfo", () => { - it("carries parties alongside the demo url", () => { + it("reads the demo url and map from roundstatsall", () => { const resolved = extractMatchInfo([ { matchtime: 1700000000, watchablematchinfo: { game_map: "de_dust2" }, - roundstatsall: [ - { - map: "http://replay/demo.dem.bz2", - reservation: { - account_ids: [111, 222, 333], - party_ids: [3, 3, 0], - }, - }, - ], + roundstatsall: [{ map: "http://replay/demo.dem.bz2" }], }, ]); expect(resolved.demoUrl).toBe("http://replay/demo.dem.bz2"); expect(resolved.mapName).toBe("de_dust2"); - expect(groupsOf(resolved.parties).get("3").sort()).toEqual( - [steamId(111), steamId(222)].sort(), + expect(resolved.matchStartTime).toBe( + new Date(1700000000 * 1000).toISOString(), ); }); - it("resolves the demo with parties null when there is no reservation", () => { + it("prefers the legacy round stats url when present", () => { const resolved = extractMatchInfo([ { roundstats_legacy: { map: "http://replay/demo.dem.bz2" }, }, ]); expect(resolved.demoUrl).toBe("http://replay/demo.dem.bz2"); - expect(resolved.parties).toBeNull(); + }); + + // The demo link expires long before the rest of the match info does, so a + // missing url must not throw away the map/time we did get. + it("still resolves when the demo url is missing", () => { + const resolved = extractMatchInfo([ + { + watchablematchinfo: { game_mapgroup: "mg_de_inferno" }, + roundstatsall: [{}], + }, + ]); + + expect(resolved.demoUrl).toBeNull(); + expect(resolved.mapName).toBe("de_inferno"); + }); + + it("returns null when the gc gave us nothing at all", () => { + expect(extractMatchInfo([])).toBeNull(); + expect(extractMatchInfo(null)).toBeNull(); }); }); diff --git a/src/steam-match-history/steam-gc.service.ts b/src/steam-match-history/steam-gc.service.ts index 99326731..8828b887 100644 --- a/src/steam-match-history/steam-gc.service.ts +++ b/src/steam-match-history/steam-gc.service.ts @@ -8,7 +8,6 @@ import { ConfigService } from "@nestjs/config"; import SteamUser from "steam-user"; import GlobalOffensive from "globaloffensive"; import { CacheService } from "../cache/cache.service"; -import { MatchParty } from "./types/MatchParty"; const CS2_APP_ID = 730; const REFRESH_TOKEN_CACHE_KEY = "steam-gc:refresh-token"; @@ -17,15 +16,13 @@ const GC_READY_TIMEOUT_MS = 30_000; const IDLE_LOGOFF_MS = 5 * 60_000; export type ResolvedMatch = { - demoUrl: string; + // Null when the GC answered but the demo link has expired or is missing — + // the map and match time are still worth keeping in that case. + demoUrl: string | null; mapName: string | null; matchStartTime: string | null; - parties: MatchParty[] | null; }; -// account id -> steam64 -const STEAM_ID_BASE = 76561197960265728n; - export class SteamGcConnectionError extends Error {} @Injectable() @@ -265,13 +262,7 @@ export class SteamGcService matchtime?: number; watchablematchinfo?: { game_mapgroup?: string; game_map?: string }; roundstats_legacy?: { map?: string }; - roundstatsall?: Array<{ - map?: string; - // The reservation the GC handed the game server. account_ids and - // party_ids are index-aligned; players who queued together share a - // party id. This is the only place the queue grouping survives. - reservation?: { account_ids?: number[]; party_ids?: number[] }; - }>; + roundstatsall?: Array<{ map?: string }>; }; let demoUrl: string | null = null; @@ -291,9 +282,6 @@ export class SteamGcService } } } - if (!demoUrl) { - return null; - } const matchStartTime = typeof match.matchtime === "number" && match.matchtime > 0 @@ -311,65 +299,6 @@ export class SteamGcService demoUrl, mapName, matchStartTime, - parties: SteamGcService.extractParties(match.roundstatsall), }; } - - // Zips the reservation's index-aligned account_ids/party_ids into per-player - // party membership. Returns null when the GC gave us no reservation — party - // data is a bonus, never a reason to fail an import. - private static extractParties( - roundStats?: Array<{ - reservation?: { account_ids?: number[]; party_ids?: number[] }; - }>, - ): MatchParty[] | null { - if (!Array.isArray(roundStats)) { - return null; - } - - // Later entries are the fuller picture; take the last one that has both - // halves of the pairing. - let reservation: { account_ids?: number[]; party_ids?: number[] } | null = - null; - for (let i = roundStats.length - 1; i >= 0; i--) { - const candidate = roundStats[i]?.reservation; - if (candidate?.account_ids?.length && candidate?.party_ids?.length) { - reservation = candidate; - break; - } - } - if (!reservation) { - return null; - } - - const accountIds = reservation.account_ids ?? []; - const partyIds = reservation.party_ids ?? []; - - const byParty = new Map(); - for (let i = 0; i < accountIds.length; i++) { - const accountId = accountIds[i]; - const partyId = partyIds[i]; - // 0 / missing means "queued alone" — not a party. - if (!accountId || !partyId) { - continue; - } - const key = String(partyId); - const steamId = (BigInt(accountId) + STEAM_ID_BASE).toString(); - byParty.set(key, [...(byParty.get(key) ?? []), steamId]); - } - - const parties: MatchParty[] = []; - for (const [partyKey, steamIds] of byParty) { - // A "party" of one is just a solo queuer whose party id happened to be - // populated. - if (steamIds.length < 2) { - continue; - } - for (const steamId of steamIds) { - parties.push({ steam_id: steamId, party_key: partyKey }); - } - } - - return parties.length > 0 ? parties : null; - } } diff --git a/src/steam-match-history/types/MatchParty.ts b/src/steam-match-history/types/MatchParty.ts deleted file mode 100644 index 3a3d41e9..00000000 --- a/src/steam-match-history/types/MatchParty.ts +++ /dev/null @@ -1,6 +0,0 @@ -// party_key is the source's own id and is only unique within one match; it is -// remapped to a uuid before being stored. -export type MatchParty = { - steam_id: string; - party_key: string; -}; diff --git a/test/telemetry-perf.spec.ts b/test/telemetry-perf.spec.ts index f6cf86c0..954c3a0a 100644 --- a/test/telemetry-perf.spec.ts +++ b/test/telemetry-perf.spec.ts @@ -26,7 +26,6 @@ benchmark("telemetry perf", () => { scan: async (): Promise<[string, Array]> => ["0", []], }), } as never, - null as never, { get: () => ({ webDomain: "https://panel.test" }) } as never, postgres, { getPanelVersion: async () => "abc" } as never, diff --git a/test/telemetry.spec.ts b/test/telemetry.spec.ts index 43b1474b..8bee4c57 100644 --- a/test/telemetry.spec.ts +++ b/test/telemetry.spec.ts @@ -28,7 +28,6 @@ describe("telemetry (SQL-driven)", () => { service = new TelemetryService( new Logger("TelemetryTest"), { getConnection: () => redis } as never, - null as never, { get: () => ({ webDomain: "https://panel.test" }) } as never, postgres, { getPanelVersion: async () => "abcdef1234567890" } as never,