57 lines
2 KiB
TypeScript
57 lines
2 KiB
TypeScript
export interface SportSession {
|
|
identity: string;
|
|
session_type: string;
|
|
label: string;
|
|
started_at: string;
|
|
ended_at: string | null;
|
|
}
|
|
|
|
interface SportPage {
|
|
api_version: 1;
|
|
offset: number;
|
|
more: boolean;
|
|
next_offset: number;
|
|
items: SportSession[];
|
|
}
|
|
|
|
function validSession(value: unknown): value is SportSession {
|
|
if (typeof value !== "object" || value === null) return false;
|
|
const item = value as Record<string, unknown>;
|
|
return typeof item.identity === "string" &&
|
|
typeof item.session_type === "string" &&
|
|
typeof item.label === "string" &&
|
|
typeof item.started_at === "string" &&
|
|
(item.ended_at === null || typeof item.ended_at === "string");
|
|
}
|
|
|
|
/** CONTRACT: each read is a fresh factual history projection. Pagination must
|
|
* complete before callers may present absence or permit a complete PDF. */
|
|
export async function fetchSportSessions(
|
|
start: string,
|
|
end: string,
|
|
signal?: AbortSignal,
|
|
): Promise<SportSession[]> {
|
|
const sessions: SportSession[] = [];
|
|
const ids = new Set<string>();
|
|
let offset = 0;
|
|
for (;;) {
|
|
const query = new URLSearchParams({ start, end, offset: String(offset), limit: "64" });
|
|
const response = await fetch(`/api/v1/sessions/sport?${query}`, { signal });
|
|
if (!response.ok) throw new Error(`Sport history HTTP ${response.status}`);
|
|
const page: SportPage = await response.json();
|
|
if (page.api_version !== 1 || page.offset !== offset ||
|
|
!Array.isArray(page.items) || !page.items.every(validSession) ||
|
|
typeof page.more !== "boolean" ||
|
|
!Number.isSafeInteger(page.next_offset) || page.next_offset <= offset) {
|
|
throw new TypeError("Invalid sport history page");
|
|
}
|
|
for (const item of page.items) {
|
|
if (ids.has(item.identity)) throw new TypeError("Duplicate sport session identity");
|
|
ids.add(item.identity);
|
|
sessions.push(item);
|
|
}
|
|
if (!page.more) return sessions;
|
|
if (page.items.length === 0) throw new TypeError("Empty sport history page");
|
|
offset = page.next_offset;
|
|
}
|
|
}
|