import { PLAYER_COUNT, type Match, type PreviewChunk, type Round } from '$lib/types'; import { listTree, resolveUrl, type FetchOpts } from '$lib/api/hub'; import { fetchParquetRows } from '$lib/api/parquet'; export type FetchOptions = FetchOpts; let matchesPromise: Promise | null = null; let roundsPromise: Promise | null = null; const matchPreviewsCache = new Map>(); // The dataset is sharded by upload batch. Index files are named like // `index/manifest-.parquet` / `index/rounds-.parquet`. // Discover them at runtime so new shards don't require a code change. async function listIndexShards( prefix: 'manifest' | 'rounds', opts: FetchOptions ): Promise { const entries = await listTree('index', opts); return entries .filter((e) => e.type === 'file') .map((e) => e.path) .filter((p) => { const name = p.split('/').pop() ?? ''; return name.startsWith(`${prefix}-`) && name.endsWith('.parquet'); }); } type IndexRow = { match_id: number; map_name: string; uploaded_at: string; round?: number }; async function loadShardedIndex( prefix: 'manifest' | 'rounds', opts: FetchOptions ): Promise { const paths = await listIndexShards(prefix, opts); if (!paths.length) return []; const shards = await Promise.all(paths.map((p) => fetchParquetRows(resolveUrl(p), opts))); // A re-rendered batch produces a second shard whose rows collide with the // original on (match,map[,round]). Keep the freshest copy per key. const keyOf = prefix === 'manifest' ? (r: T) => `${r.match_id}|${r.map_name}` : (r: T) => `${r.match_id}|${r.map_name}|${r.round}`; const latest = new Map(); for (const r of shards.flat()) { const k = keyOf(r); const cur = latest.get(k); if (!cur || r.uploaded_at > cur.uploaded_at) latest.set(k, r); } return Array.from(latest.values()); } export function listMatches(opts: FetchOptions = {}): Promise { if (matchesPromise) return matchesPromise; matchesPromise = loadShardedIndex('manifest', opts) .then((rows) => { rows.sort( (a, b) => new Date(b.match_date).getTime() - new Date(a.match_date).getTime() || (a.map_index ?? 0) - (b.map_index ?? 0) ); return rows; }) .catch((err) => { matchesPromise = null; throw err; }); return matchesPromise; } export async function listAllRounds(opts: FetchOptions = {}): Promise { if (!roundsPromise) { roundsPromise = loadShardedIndex('rounds', opts).catch((err) => { roundsPromise = null; throw err; }); } return roundsPromise; } export async function listRounds( matchId: number, mapName: string, opts: FetchOptions = {} ): Promise { const all = await listAllRounds(opts); return all .filter((r) => r.match_id === matchId && r.map_name === mapName) .sort((a, b) => a.round - b.round); } /** * Fetch all preview rows for every player on this (match, map) in parallel. * The shard_id from the match's manifest row is enough to construct each * player's parquet URL — no tree-API discovery needed. */ async function loadMatchPreviews( matchId: number, mapName: string, opts: FetchOptions ): Promise { const key = `${matchId}/${mapName}`; const cached = matchPreviewsCache.get(key); if (cached) return cached; const promise = (async () => { const matches = await listMatches(opts); const match = matches.find((m) => m.match_id === matchId && m.map_name === mapName); if (!match) return []; const players = Array.from({ length: PLAYER_COUNT }, (_, i) => i); const results = await Promise.all( players.map(async (player) => { const dir = `data/match_id=${matchId}/map_name=${mapName}/player=${player}`; try { const rows = await fetchParquetRows( resolveUrl(`${dir}/chunks-preview-${match.shard_id}.parquet`), opts ); for (const r of rows) r.preview_video = { src: resolveUrl(`${dir}/${r.preview_path}`) }; return rows; } catch { return [] as PreviewChunk[]; } }) ); return results.flat(); })().catch((err) => { matchPreviewsCache.delete(key); throw err; }); matchPreviewsCache.set(key, promise); return promise; } export async function listRoundPreviews( matchId: number, mapName: string, round: number, opts: FetchOptions = {} ): Promise { const all = await loadMatchPreviews(matchId, mapName, opts); return all .filter((p) => p.round === round) .sort((a, b) => a.player - b.player || a.chunk_index - b.chunk_index); }