import { apiClient } from './client'; import { parseMatchRoundsSafe, parseMatchWeapons, parseMatchChat, parseMatchParseResponse } from '$lib/schemas'; import { transformMatchesListResponse, transformMatchDetail, type LegacyMatchListItem, type LegacyMatchDetail } from './transformers'; import type { Match, MatchesListResponse, MatchesQueryParams, MatchParseResponse, MatchRoundsResponse, MatchWeaponsResponse, MatchChatResponse } from '$lib/types'; /** * Match API endpoints */ export const matchesAPI = { /** * Parse match from share code * @param shareCode - CS:GO/CS2 match share code * @returns Parse status response */ async parseMatch(shareCode: string): Promise { const url = `/match/parse/${shareCode}`; const data = await apiClient.get(url); // Validate with Zod schema return parseMatchParseResponse(data); }, /** * Get match details with player statistics * @param matchId - Match ID (uint64 as string) * @returns Complete match data */ async getMatch(matchId: string): Promise { const url = `/match/${matchId}`; // API returns legacy format const data = await apiClient.get(url); // Transform legacy API response to new format return transformMatchDetail(data); }, /** * Get match weapons statistics * @param matchId - Match ID * @returns Weapon statistics for all players */ async getMatchWeapons(matchId: string | number): Promise { const url = `/match/${matchId}/weapons`; const data = await apiClient.get(url); // Validate with Zod schema return parseMatchWeapons(data); }, /** * Get match round-by-round statistics * @param matchId - Match ID * @returns Round statistics and economy data * @throws Error if data is invalid or demo not parsed yet */ async getMatchRounds(matchId: string | number): Promise { const url = `/match/${matchId}/rounds`; const data = await apiClient.get(url); // Validate with Zod schema using safe parse // This handles cases where the demo hasn't been parsed yet const result = parseMatchRoundsSafe(data); if (!result.success) { // If validation fails, it's likely the demo hasn't been parsed yet throw new Error('Demo not parsed yet or invalid response format'); } return result.data; }, /** * Get match chat messages * @param matchId - Match ID * @returns Chat messages from the match */ async getMatchChat(matchId: string | number): Promise { const url = `/match/${matchId}/chat`; const data = await apiClient.get(url); // Validate with Zod schema return parseMatchChat(data); }, /** * Get paginated list of matches * * IMPORTANT: The API returns a plain array, not an object with properties. * We must manually implement pagination by: * 1. Requesting limit + 1 matches * 2. Checking if we got more than limit (means there are more pages) * 3. Extracting timestamp from last match for next page * * Pagination flow: * - First call: GET /matches?limit=20 → returns array of up to 20 matches * - Next call: GET /matches/next/{timestamp}?limit=20 → returns next 20 matches * - Continue until response.length < limit (reached the end) * * @param params - Query parameters (filters, pagination) * @param params.limit - Number of matches to return (default: 50) * @param params.before_time - Unix timestamp for pagination (get matches before this time) * @param params.map - Filter by map name (e.g., "de_inferno") * @param params.player_id - Filter by player Steam ID * @returns List of matches with pagination metadata */ async getMatches(params?: MatchesQueryParams): Promise { const url = params?.before_time ? `/matches/next/${params.before_time}` : '/matches'; const limit = params?.limit || 50; // CRITICAL: API returns a plain array, not a wrapped object // NOTE: Backend has a hard limit of 20 matches per request // We assume hasMore = true if we get exactly the limit we requested const data = await apiClient.get(url, { params: { limit: limit, map: params?.map, player_id: params?.player_id } }); // Handle null or empty response if (!data || !Array.isArray(data)) { console.warn('[API] getMatches received null or invalid data'); return transformMatchesListResponse([], false, undefined); } // If we got exactly the limit, assume there might be more // If we got less, we've reached the end const hasMore = data.length === limit; // Get the timestamp from the LAST match BEFORE transformation // The legacy API format has `date` as a Unix timestamp (number) const lastLegacyMatch = data.length > 0 ? data[data.length - 1] : undefined; const nextPageTime = hasMore && lastLegacyMatch ? lastLegacyMatch.date : undefined; // Transform legacy API response to new format return transformMatchesListResponse(data, hasMore, nextPageTime); }, /** * Search matches (cancelable for live search) * @param params - Search parameters * @returns List of matching matches */ async searchMatches(params?: MatchesQueryParams): Promise { const url = params?.before_time ? `/matches/next/${params.before_time}` : '/matches'; const limit = params?.limit || 20; // API returns a plain array, not a wrapped object // Backend has a hard limit of 20 matches per request const data = await apiClient.getCancelable(url, 'match-search', { params: { limit: limit, map: params?.map, player_id: params?.player_id } }); // If we got exactly the limit, assume there might be more const hasMore = data.length === limit; // Get the timestamp from the LAST match BEFORE transformation // The legacy API format has `date` as a Unix timestamp (number) const lastLegacyMatch = data.length > 0 ? data[data.length - 1] : undefined; const nextPageTime = hasMore && lastLegacyMatch ? lastLegacyMatch.date : undefined; // Transform legacy API response to new format return transformMatchesListResponse(data, hasMore, nextPageTime); }, /** * Get match by share code * Convenience method that extracts match ID from share code if needed * @param shareCodeOrId - Share code or match ID * @returns Match data */ async getMatchByShareCode(shareCodeOrId: string): Promise { // If it looks like a share code, parse it first if (shareCodeOrId.startsWith('CSGO-')) { const parseResult = await this.parseMatch(shareCodeOrId); return this.getMatch(parseResult.match_id); } // Otherwise treat as match ID return this.getMatch(shareCodeOrId); } }; /** * Match API with default export */ export default matchesAPI;