feat: complete Phase 3 - Domain Modeling & Data Layer

Implements comprehensive type system, runtime validation, API client,
and testing infrastructure for CS2.WTF.

TypeScript Interfaces (src/lib/types/):
- Match.ts: Match, MatchPlayer, MatchListItem types
- Player.ts: Player, PlayerMeta, PlayerProfile types
- RoundStats.ts: Round economy and performance data
- Weapon.ts: Weapon statistics with hit groups (HitGroup, WeaponType enums)
- Message.ts: Chat messages with filtering support
- api.ts: API responses, errors, and APIException class
- Complete type safety with strict null checks

Zod Schemas (src/lib/schemas/):
- Runtime validation for all data models
- Schema parsers with safe/unsafe variants
- Special handling for backend typo (looses → losses)
- Share code validation regex
- CS2-specific validations (rank 0-30000, MR12 rounds)

API Client (src/lib/api/):
- client.ts: Axios-based HTTP client with error handling
  - Request cancellation support (AbortController)
  - Automatic retry logic for transient failures
  - Timeout handling (10s default)
  - Typed APIException errors
- players.ts: Player endpoints (profile, meta, track/untrack, search)
- matches.ts: Match endpoints (parse, details, weapons, rounds, chat, search)
- Zod validation on all API responses

MSW Mock Handlers (src/mocks/):
- fixtures.ts: Comprehensive mock data for testing
- handlers/players.ts: Mock player API endpoints
- handlers/matches.ts: Mock match API endpoints
- browser.ts: Browser MSW worker for development
- server.ts: Node MSW server for Vitest tests
- Realistic responses with delays and pagination
- Safe integer IDs to avoid precision loss

Configuration:
- .env.example: Complete environment variable documentation
- src/vite-env.d.ts: Vite environment type definitions
- All strict TypeScript checks passing (0 errors, 0 warnings)

Features:
- Cancellable requests for search (prevent race conditions)
- Data normalization (backend typo handling)
- Comprehensive error types (NetworkError, Timeout, etc.)
- Share code parsing and validation
- Pagination support for players and matches
- Mock data for offline development and testing

Build Status: ✓ Production build successful
Type Check: ✓ 0 errors, 0 warnings
Lint: ✓ All checks passed
Phase 3 Completion: 100%

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-04 20:31:20 +01:00
parent 66aea51c39
commit d811efc394
26 changed files with 2444 additions and 6 deletions

150
src/lib/api/matches.ts Normal file
View File

@@ -0,0 +1,150 @@
import { apiClient } from './client';
import {
parseMatch,
parseMatchesList,
parseMatchRounds,
parseMatchWeapons,
parseMatchChat,
parseMatchParseResponse
} from '$lib/schemas';
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<MatchParseResponse> {
const url = `/match/parse/${shareCode}`;
const data = await apiClient.get<MatchParseResponse>(url);
// Validate with Zod schema
return parseMatchParseResponse(data);
},
/**
* Get match details with player statistics
* @param matchId - Match ID (uint64)
* @returns Complete match data
*/
async getMatch(matchId: string | number): Promise<Match> {
const url = `/match/${matchId}`;
const data = await apiClient.get<Match>(url);
// Validate with Zod schema
return parseMatch(data);
},
/**
* Get match weapons statistics
* @param matchId - Match ID
* @returns Weapon statistics for all players
*/
async getMatchWeapons(matchId: string | number): Promise<MatchWeaponsResponse> {
const url = `/match/${matchId}/weapons`;
const data = await apiClient.get<MatchWeaponsResponse>(url);
// Validate with Zod schema
return parseMatchWeapons(data);
},
/**
* Get match round-by-round statistics
* @param matchId - Match ID
* @returns Round statistics and economy data
*/
async getMatchRounds(matchId: string | number): Promise<MatchRoundsResponse> {
const url = `/match/${matchId}/rounds`;
const data = await apiClient.get<MatchRoundsResponse>(url);
// Validate with Zod schema
return parseMatchRounds(data);
},
/**
* Get match chat messages
* @param matchId - Match ID
* @returns Chat messages from the match
*/
async getMatchChat(matchId: string | number): Promise<MatchChatResponse> {
const url = `/match/${matchId}/chat`;
const data = await apiClient.get<MatchChatResponse>(url);
// Validate with Zod schema
return parseMatchChat(data);
},
/**
* Get paginated list of matches
* @param params - Query parameters (filters, pagination)
* @returns List of matches with pagination
*/
async getMatches(params?: MatchesQueryParams): Promise<MatchesListResponse> {
const url = params?.before_time ? `/matches/next/${params.before_time}` : '/matches';
const data = await apiClient.get<MatchesListResponse>(url, {
params: {
limit: params?.limit,
map: params?.map,
player_id: params?.player_id
}
});
// Validate with Zod schema
return parseMatchesList(data);
},
/**
* Search matches (cancelable for live search)
* @param params - Search parameters
* @returns List of matching matches
*/
async searchMatches(params?: MatchesQueryParams): Promise<MatchesListResponse> {
const url = '/matches';
const data = await apiClient.getCancelable<MatchesListResponse>(url, 'match-search', {
params: {
limit: params?.limit || 20,
map: params?.map,
player_id: params?.player_id,
before_time: params?.before_time
}
});
// Validate with Zod schema
return parseMatchesList(data);
},
/**
* 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<Match> {
// 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;