52 lines
2.7 KiB
TypeScript
52 lines
2.7 KiB
TypeScript
import { db } from './storage';
|
|
import { demoData } from './demo';
|
|
import type { NearbyStop, Snapshot } from '../types';
|
|
|
|
export type ResourceOptions = { routeId?: number; at?: string; tripKey?: string };
|
|
|
|
export class DataError extends Error {
|
|
constructor(public code: string) { super(code); }
|
|
}
|
|
|
|
export async function loadResource<T>(
|
|
resource: string, mode: 'live' | 'demo', cityId?: number, stopId?: number, date?: string, options: ResourceOptions = {},
|
|
): Promise<Snapshot<T>> {
|
|
const key = `${mode}:${resource}:${cityId ?? ''}:${stopId ?? ''}${date ? `:${date}` : ''}${options.routeId ? `:${options.routeId}:${options.at ?? ''}:${options.tripKey ?? ''}` : ''}`;
|
|
let data: T;
|
|
try {
|
|
if (!navigator.onLine) throw new DataError('offline');
|
|
if (mode === 'demo') data = demoData(resource, cityId, stopId, date, options) as T;
|
|
else {
|
|
const params = new URLSearchParams({ resource });
|
|
if (cityId) params.set('cityId', String(cityId));
|
|
if (stopId) params.set('stopId', String(stopId));
|
|
if (date) params.set('date', date);
|
|
if (options.routeId) params.set('routeId', String(options.routeId));
|
|
if (options.at) params.set('at', options.at);
|
|
if (options.tripKey) params.set('tripKey', options.tripKey);
|
|
const response = await fetch(`/api/data?${params}`, { signal: AbortSignal.timeout(15000), cache: 'no-store' });
|
|
const body = await response.json();
|
|
if (!response.ok) throw new DataError(body.error || 'data_unavailable');
|
|
data = body as T;
|
|
}
|
|
} catch (error) {
|
|
const snapshot = await db.snapshots.get(key).catch(() => undefined);
|
|
if (snapshot) return { data: snapshot.data as T, savedAt: snapshot.savedAt, saved: true };
|
|
throw error;
|
|
}
|
|
const savedAt = new Date().toISOString();
|
|
// Storage failure must not hide otherwise available API data.
|
|
await db.snapshots.put({ key, data, savedAt }).catch(() => undefined);
|
|
return { data, savedAt, saved: false };
|
|
}
|
|
|
|
export async function loadNearby(mode: 'live' | 'demo', cityId: number, lat: number, lon: number, signal: AbortSignal): Promise<NearbyStop[]> {
|
|
if (mode === 'demo') return (demoData('stops', cityId) as NearbyStop[]).slice(0, 4).map((stop, index) => ({ ...stop, distance: 120 + index * 90 }));
|
|
const params = new URLSearchParams({ resource: 'nearby', cityId: String(cityId), lat: lat.toFixed(5), lon: lon.toFixed(5), radius: '1000' });
|
|
const response = await fetch(`/api/data?${params}`, { cache: 'no-store', signal: AbortSignal.any([signal, AbortSignal.timeout(15000)]) });
|
|
const data = await response.json();
|
|
if (!response.ok) throw new DataError(data.error || 'data_unavailable');
|
|
// Nearby coordinates and results stay in memory, outside persistent snapshots.
|
|
return data as NearbyStop[];
|
|
}
|