import { SelfUpdatingETARepository } from "./SelfUpdatingETARepository"; import { ShuttleGetterRepository, ShuttleRepositoryEvent, ShuttleStopArrival, ShuttleTravelTimeDataIdentifier, ShuttleTravelTimeDateFilterArguments } from "../ShuttleGetterRepository"; import { BaseInMemoryETARepository } from "./BaseInMemoryETARepository"; import { IShuttle } from "../../../entities/ShuttleRepositoryEntities"; export class InMemorySelfUpdatingETARepository extends BaseInMemoryETARepository implements SelfUpdatingETARepository { private referenceTime: Date | null = null; private travelTimeData: Map> = new Map(); constructor( readonly shuttleRepository: ShuttleGetterRepository ) { super(); this.setReferenceTime = this.setReferenceTime.bind(this); this.getAverageTravelTimeSeconds = this.getAverageTravelTimeSeconds.bind(this); this.startListeningForUpdates = this.startListeningForUpdates.bind(this); this.handleShuttleUpdate = this.handleShuttleUpdate.bind(this); this.handleShuttleWillArriveAtStop = this.handleShuttleWillArriveAtStop.bind(this); } setReferenceTime(referenceTime: Date): void { this.referenceTime = referenceTime; } async getAverageTravelTimeSeconds( { routeId, fromStopId, toStopId }: ShuttleTravelTimeDataIdentifier, { from, to }: ShuttleTravelTimeDateFilterArguments ): Promise { const key = `${routeId}:${fromStopId}:${toStopId}`; const dataPoints = this.travelTimeData.get(key); if (!dataPoints || dataPoints.length === 0) { return undefined; } const fromTimestamp = from.getTime(); const toTimestamp = to.getTime(); const filteredPoints = dataPoints.filter( (point) => point.timestamp >= fromTimestamp && point.timestamp <= toTimestamp ); if (filteredPoints.length === 0) { return undefined; } const sum = filteredPoints.reduce((acc, point) => acc + point.seconds, 0); return sum / filteredPoints.length; } startListeningForUpdates(): void { this.shuttleRepository.addListener(ShuttleRepositoryEvent.SHUTTLE_UPDATED, this.handleShuttleUpdate); this.shuttleRepository.addListener(ShuttleRepositoryEvent.SHUTTLE_WILL_ARRIVE_AT_STOP, this.handleShuttleWillArriveAtStop); } private async handleShuttleUpdate(shuttle: IShuttle): Promise { const lastStop = await this.shuttleRepository.getShuttleLastStopArrival(shuttle.id); if (!lastStop) return; const lastOrderedStop = await this.shuttleRepository.getOrderedStopByRouteAndStopId(shuttle.routeId, lastStop.stopId); const nextStop = lastOrderedStop?.nextStop; if (!nextStop) return; let referenceCurrentTime = new Date(); if (this.referenceTime != null) { referenceCurrentTime = this.referenceTime; } const oneWeekAgo = new Date(referenceCurrentTime.getTime() - (60 * 60 * 24 * 7 * 1000)); const travelTimeSeconds = await this.getAverageTravelTimeSeconds({ routeId: shuttle.routeId, fromStopId: lastStop.stopId, toStopId: nextStop.stopId, }, { from: oneWeekAgo, to: new Date(oneWeekAgo.getTime() + (60 * 60 * 1000)) }); if (travelTimeSeconds == undefined) return; const elapsedTimeMs = referenceCurrentTime.getTime() - lastStop.timestamp.getTime(); const predictedTimeSeconds = travelTimeSeconds - (elapsedTimeMs / 1000); await this.addOrUpdateEta({ secondsRemaining: predictedTimeSeconds, shuttleId: shuttle.id, stopId: nextStop.stopId, systemId: nextStop.systemId, updatedTime: new Date(), }); } private async handleShuttleWillArriveAtStop(shuttleArrival: ShuttleStopArrival): Promise { const lastStopTimestamp = await this.shuttleRepository.getShuttleLastStopArrival(shuttleArrival.shuttleId); if (lastStopTimestamp) { // disallow cases where this gets triggered multiple times if (lastStopTimestamp.stopId === shuttleArrival.stopId) return; const shuttle = await this.shuttleRepository.getShuttleById(lastStopTimestamp.shuttleId); if (!shuttle) return; const routeId = shuttle.routeId; const fromStopId = lastStopTimestamp.stopId; const toStopId = shuttleArrival.stopId; const travelTimeSeconds = (shuttleArrival.timestamp.getTime() - lastStopTimestamp.timestamp.getTime()) / 1000; await this.addTravelTimeDataPoint({ routeId, fromStopId, toStopId }, travelTimeSeconds, shuttleArrival.timestamp.getTime()); } } private async addTravelTimeDataPoint( { routeId, fromStopId, toStopId }: ShuttleTravelTimeDataIdentifier, travelTimeSeconds: number, timestamp = Date.now(), ): Promise { const key = `${routeId}:${fromStopId}:${toStopId}`; const dataPoints = this.travelTimeData.get(key) || []; dataPoints.push({ timestamp, seconds: travelTimeSeconds }); this.travelTimeData.set(key, dataPoints); } }