Spaces:
Running
Running
File size: 4,598 Bytes
95e3d2a 31d3580 a79c533 31d3580 95e3d2a a79c533 31d3580 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 | 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<Match[]> | null = null;
let roundsPromise: Promise<Round[]> | null = null;
const matchPreviewsCache = new Map<string, Promise<PreviewChunk[]>>();
// The dataset is sharded by upload batch. Index files are named like
// `index/manifest-<shard_id>.parquet` / `index/rounds-<shard_id>.parquet`.
// Discover them at runtime so new shards don't require a code change.
async function listIndexShards(
prefix: 'manifest' | 'rounds',
opts: FetchOptions
): Promise<string[]> {
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<T extends IndexRow>(
prefix: 'manifest' | 'rounds',
opts: FetchOptions
): Promise<T[]> {
const paths = await listIndexShards(prefix, opts);
if (!paths.length) return [];
const shards = await Promise.all(paths.map((p) => fetchParquetRows<T>(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<string, T>();
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<Match[]> {
if (matchesPromise) return matchesPromise;
matchesPromise = loadShardedIndex<Match>('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<Round[]> {
if (!roundsPromise) {
roundsPromise = loadShardedIndex<Round>('rounds', opts).catch((err) => {
roundsPromise = null;
throw err;
});
}
return roundsPromise;
}
export async function listRounds(
matchId: number,
mapName: string,
opts: FetchOptions = {}
): Promise<Round[]> {
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<PreviewChunk[]> {
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<PreviewChunk>(
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<PreviewChunk[]> {
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);
}
|