import { GetterRepository } from "./GetterRepository"; import { IEta, IOrderedStop, IRoute, IShuttle, IStop, ISystem } from "../entities/entities"; const baseUrl = "https://passiogo.com/mapGetData.php" // TODO: implement TTL functionality // TODO: add TTL values to everything // TODO: remove RepositoryDataLoader and UnoptimizedInMemoryRepository // TODO: make milliseconds (TTL) required on everything // TODO: extract cache into its own class export interface ApiBasedRepositoryCache { etasForShuttleId?: { [shuttleId: string]: IEta[], }, etasForStopId?: { [stopId: string]: IEta[], }, stopsBySystemId?: { [systemId: string]: IStop[], }, stopByStopId?: { [stopId: string]: IStop, }, shuttleByShuttleId?: { [shuttleId: string]: IShuttle, }, shuttlesBySystemId?: { [systemId: string]: IShuttle[], }, // To speed things up, implement caches for other data later } export interface ApiBasedRepositoryMillisecondTTLs { etasForShuttleId?: number, etasForStopId?: number, shuttleByShuttleId?: number, shuttlesBySystemId?: number, } const emptyCache: ApiBasedRepositoryCache = { etasForShuttleId: {}, etasForStopId: {}, shuttleByShuttleId: {}, shuttlesBySystemId: {}, } const defaultTtls: ApiBasedRepositoryMillisecondTTLs = { etasForShuttleId: 10000, etasForStopId: 10000, } export class ApiBasedRepository implements GetterRepository { private cache: ApiBasedRepositoryCache; constructor( initialCache: ApiBasedRepositoryCache | undefined = emptyCache, private ttls: ApiBasedRepositoryMillisecondTTLs = defaultTtls, ) { this.cache = initialCache; } /** * Seed the initial data in the cache, so data can be updated * given the correct ID. * Alternatively, pass in an `initialCache` with existing data * (useful for testing). * @param systemId */ public async seedCacheForSystemId(systemId: string): Promise { await this.getShuttlesBySystemId(systemId); await this.getShuttlesByRouteId(systemId); await this.getStopsBySystemId(systemId); await this.getSystemById(systemId); } public async getEtaForShuttleAndStopId(shuttleId: string, stopId: string): Promise { const shuttle = await this.getShuttleById(shuttleId); const systemId = shuttle?.systemId; if (!systemId) { return null; } await this.updateEtasForSystemIfTTL(systemId); if (this.cache?.etasForStopId && this.cache.etasForStopId[stopId]) { const etas = this.cache.etasForStopId[stopId]; const foundEta = etas.find((eta) => eta.stopId === stopId); if (foundEta) { return foundEta; } } return null; } public async getEtasForShuttleId(shuttleId: string): Promise { const shuttle = await this.getShuttleById(shuttleId); const systemId = shuttle?.systemId; if (!systemId) { return []; } await this.updateEtasForSystemIfTTL(systemId); if (!this.cache?.etasForShuttleId || !this.cache.etasForShuttleId[shuttleId]) { return []; } return this.cache.etasForShuttleId[shuttleId]; } public async getEtasForStopId(stopId: string): Promise { const stop = await this.getStopById(stopId); const systemId = stop?.systemId; if (!systemId) { return []; } await this.updateEtasForSystemIfTTL(systemId); if (!this.cache?.etasForStopId || !this.cache.etasForStopId[stopId]) { return []; } return this.cache.etasForStopId[stopId]; } public async updateEtasForSystemIfTTL(systemId: string) { // TODO: check if TTL try { const stops = await this.getStopsBySystemId(systemId); await Promise.all(stops.map(async (stop) => { const params = { eta: "3", stopIds: stop.id, }; const query = new URLSearchParams(params).toString(); const response = await fetch(`${baseUrl}?${query}`, { method: "GET", }); const json = await response.json(); if (json.ETAs && json.ETAs[stop.id]) { if (!this.cache.etasForStopId) { this.cache.etasForStopId = {}; } this.cache.etasForStopId[stop.id] = []; // This is technically incorrect, the entire shuttle cache // should not be reset like this // TODO: restore normal cache behavior this.cache.etasForShuttleId = {}; // Continue with the parsing json.ETAs[stop.id].forEach((jsonEta: any) => { // Update cache if (!this.cache.etasForStopId) { this.cache.etasForStopId = {}; } if (!this.cache.etasForShuttleId) { this.cache.etasForShuttleId = {}; } // TODO: create cache abstraction to deal with possibly undefined properties const shuttleId: string = jsonEta.busId; if (!this.cache.etasForShuttleId[shuttleId]) { this.cache.etasForShuttleId[shuttleId] = []; } const eta: IEta = { secondsRemaining: jsonEta.secondsSpent, shuttleId: `${shuttleId}`, stopId: stop.id, millisecondsSinceEpoch: Date.now(), }; this.cache.etasForStopId[stop.id].push(eta); this.cache.etasForShuttleId[shuttleId].push(eta); }); } })); } catch (e) { console.error(e); } } // TODO: migrate rest of logic over to this class public async getOrderedStopByRouteAndStopId(routeId: string, stopId: string): Promise<| null> { return null; } public async getOrderedStopsByRouteId(routeId: string): Promise<[]> { return Promise.resolve([]); } public async getOrderedStopsByStopId(stopId: string): Promise<[]> { return Promise.resolve([]); } public async getRouteById(routeId: string): Promise<| null> { return Promise.resolve(null); } public async getRoutesBySystemId(systemId: string): Promise<[]> { return Promise.resolve([]); } public async getShuttleById(shuttleId: string): Promise { if (!this.cache.shuttleByShuttleId) return null; let shuttle = this.cache.shuttleByShuttleId[shuttleId]; if (!shuttle) return null; // Call getShuttlesBySystemId to update the data if not TTL await this.updateShuttlesForSystemIfTTL(shuttle.systemId); shuttle = this.cache.shuttleByShuttleId[shuttleId]; if (!shuttle) return null; return shuttle; } public async getShuttlesByRouteId(routeId: string): Promise<[]> { return Promise.resolve([]); } public async getShuttlesBySystemId(systemId: string): Promise { return Promise.resolve([]); } public async updateShuttlesForSystemIfTTL(systemId: string) { // TODO: check if TTL try { // TODO: update shuttlesByRouteId // Update shuttleByShuttleId, shuttlesBySystemId const params = { getBuses: "2" }; const formDataJsonObject = { "s0": systemId, "sA": "1" }; const formData = new FormData(); formData.set("json", JSON.stringify(formDataJsonObject)); const query = new URLSearchParams(params).toString(); const response = await fetch(`${baseUrl}?${query}`, { method: "POST", body: formData, }); const json = await response.json(); if (json.buses && json.buses["-1"] === undefined) { const jsonBuses = Object.values(json.buses).map((busesArr: any) => { return busesArr[0]; }); // Store shuttles by system, with the additional side effect that // shuttleByShuttleId is updated const shuttles = await Promise.all(jsonBuses.map(async (jsonBus: any) => { const constructedShuttle: IShuttle = { name: jsonBus.bus, coordinates: { latitude: parseFloat(jsonBus.latitude), longitude: parseFloat(jsonBus.longitude), }, routeId: jsonBus.routeId, systemId: systemId, id: `${jsonBus.busId}` } if (this.cache.shuttleByShuttleId) { this.cache.shuttleByShuttleId[jsonBus.busId] = constructedShuttle; } return constructedShuttle; })); if (this.cache.shuttlesBySystemId) { this.cache.shuttlesBySystemId[systemId] = shuttles; } } else { console.warn(`No shuttle data available for system ID ${systemId} and JSON output ${json}`); } } catch (e) { console.error(e); } } public async getStopById(stopId: string): Promise { if (!this.cache.stopByStopId) return null; const oldStop = this.cache.stopByStopId[stopId]; if (!oldStop) return null; await this.updateStopsForSystemIdIfTTL(oldStop.systemId); const newStop = this.cache.stopByStopId[stopId]; if (!newStop) return null; return newStop; } public async getStopsBySystemId(systemId: string): Promise { await this.updateStopsForSystemIdIfTTL(systemId); if (!this.cache.stopsBySystemId || this.cache.stopsBySystemId[systemId]) { return []; } return this.cache.stopsBySystemId[systemId]; } public async updateStopsForSystemIdIfTTL(systemId: string) { // TODO: check if TTL try { const params = { getStops: "2", }; const formDataJsonObject = { "s0": systemId, "sA": 1 }; const formData = new FormData(); formData.set("json", JSON.stringify(formDataJsonObject)); const query = new URLSearchParams(params).toString(); const response = await fetch(`${baseUrl}?${query}`, { method: "POST", body: formData, }); const json = await response.json(); // TODO: update polyline data // TODO: update ordered stop data if (json.stops) { const jsonStops = Object.values(json.stops); await Promise.all(jsonStops.map(async (stop: any) => { const constructedStop: IStop = { name: stop.name, id: stop.id, systemId: systemId, coordinates: { latitude: parseFloat(stop.latitude), longitude: parseFloat(stop.longitude), }, }; if (this.cache.stopsBySystemId) { this.cache.stopsBySystemId[systemId]?.push(constructedStop); } if (this.cache.stopByStopId) { this.cache.stopByStopId[constructedStop.id] = constructedStop; } })); } } catch (e) { console.error(e); } } public async getSystemById(systemId: string): Promise { return null; } public async getSystems(): Promise<[]> { return Promise.resolve([]); } }