import EventEmitter from "node:events"; import { ShuttleGetterSetterRepository } from "./ShuttleGetterSetterRepository"; import { IOrderedStop, IRoute, IShuttle, IStop, shuttleHasArrivedAtStop } from "../../entities/ShuttleRepositoryEntities"; import { IEntityWithId } from "../../entities/SharedEntities"; import { ShuttleRepositoryEvent, ShuttleRepositoryEventListener, ShuttleRepositoryEventName, ShuttleRepositoryEventPayloads, ShuttleStopArrival, ShuttleTravelTimeDataIdentifier, ShuttleTravelTimeDateFilterArguments, } from "./ShuttleGetterRepository"; /** * An unoptimized in memory repository. * (I would optimize it with actual data structures, but I'm * switching to another data store later anyways) */ export class UnoptimizedInMemoryShuttleRepository extends EventEmitter implements ShuttleGetterSetterRepository { constructor( readonly shuttleStopArrivalDegreeDelta: number = 0.001, ) { super() } public override on( event: T, listener: ShuttleRepositoryEventListener, ): this; public override on(event: string | symbol, listener: (...args: any[]) => void): this { return super.on(event, listener); } public override once( event: T, listener: ShuttleRepositoryEventListener, ): this; public override once(event: string | symbol, listener: (...args: any[]) => void): this { return super.once(event, listener); } public override off( event: T, listener: ShuttleRepositoryEventListener, ): this; public override off(event: string | symbol, listener: (...args: any[]) => void): this { return super.off(event, listener); } public override addListener( event: T, listener: ShuttleRepositoryEventListener, ): this; public override addListener(event: string | symbol, listener: (...args: any[]) => void): this { return super.addListener(event, listener); } public override removeListener( event: T, listener: ShuttleRepositoryEventListener, ): this; public override removeListener(event: string | symbol, listener: (...args: any[]) => void): this { return super.removeListener(event, listener); } public override emit( event: T, payload: ShuttleRepositoryEventPayloads[T], ): boolean; public override emit(event: string | symbol, ...args: any[]): boolean { return super.emit(event, ...args); } private stops: IStop[] = []; private routes: IRoute[] = []; private shuttles: IShuttle[] = []; private orderedStops: IOrderedStop[] = []; private shuttleLastStopArrivals: Map = new Map(); private travelTimeData: Map> = new Map(); private shuttlesAtStop: Set = new Set(); public async getStops(): Promise { return this.stops; } public async getStopById(stopId: string) { return this.findEntityById(stopId, this.stops); } public async getRoutes(): Promise { return this.routes; } public async getRouteById(routeId: string) { return this.findEntityById(routeId, this.routes); } public async getShuttles(): Promise { return this.shuttles; } public async getShuttlesByRouteId(routeId: string) { return this.shuttles.filter(shuttle => shuttle.routeId === routeId); } public async getShuttleById(shuttleId: string) { return this.findEntityById(shuttleId, this.shuttles); } public async getOrderedStopByRouteAndStopId(routeId: string, stopId: string) { return this.findEntityByMatcher((value) => value.routeId === routeId && value.stopId === stopId, this.orderedStops) } public async getOrderedStopsByStopId(stopId: string) { return this.orderedStops.filter((value) => value.stopId === stopId); } public async getOrderedStopsByRouteId(routeId: string) { return this.orderedStops.filter((value) => value.routeId === routeId); } private findEntityById(entityId: string, arrayToSearchIn: T[]) { return this.findEntityByMatcher((value) => value.id === entityId, arrayToSearchIn); } private findEntityByMatcher(callback: (value: T) => boolean, arrayToSearchIn: T[]): T | null { const entity = arrayToSearchIn.find(callback); if (!entity) { return null; } return entity; } public async addOrUpdateRoute(route: IRoute): Promise { const index = this.routes.findIndex((r) => r.id === route.id); if (index !== -1) { this.routes[index] = route; } else { this.routes.push(route); } } public async addOrUpdateShuttle( shuttle: IShuttle, travelTimeTimestamp = Date.now(), ): Promise { const index = this.shuttles.findIndex((s) => s.id === shuttle.id); if (index !== -1) { this.shuttles[index] = shuttle; } else { this.shuttles.push(shuttle); } this.emit(ShuttleRepositoryEvent.SHUTTLE_UPDATED, shuttle); await this.updateLastStopArrival(shuttle, travelTimeTimestamp); } public async addOrUpdateStop(stop: IStop): Promise { const index = this.stops.findIndex((s) => s.id === stop.id); if (index !== -1) { this.stops[index] = stop; } else { this.stops.push(stop); } } public async addOrUpdateOrderedStop(orderedStop: IOrderedStop): Promise { const index = this.orderedStops.findIndex((value) => value.stopId === orderedStop.stopId && value.routeId === orderedStop.routeId); if (index !== -1) { this.orderedStops[index] = orderedStop; } else { this.orderedStops.push(orderedStop); } } private async updateLastStopArrival( shuttle: IShuttle, travelTimeTimestamp = Date.now(), ) { const isAtStop = await this.checkIfShuttleIsAtStop(shuttle.id); let arrivedStop: IStop | undefined; if (isAtStop) { // Allow retrieval of the shuttle's current stop // Will still return undefined when the shuttle leaves the stop arrivedStop = await this.getArrivedStopIfNextStop(shuttle, true); } else { arrivedStop = await this.getArrivedStopIfNextStop(shuttle, false); } // Will not fire *any* events if the same stop const lastStop = await this.getShuttleLastStopArrival(shuttle.id); if (lastStop?.stopId === arrivedStop?.id) return; if (isAtStop) { if (lastStop) { this.emit(ShuttleRepositoryEvent.SHUTTLE_WILL_LEAVE_STOP, { stopArrivalThatShuttleIsLeaving: lastStop, }); } await this.markShuttleAsNotAtStop(shuttle.id); } if (arrivedStop) { // stop if same stop const shuttleArrival = { stopId: arrivedStop.id, timestamp: new Date(travelTimeTimestamp), shuttleId: shuttle.id, }; this.emit(ShuttleRepositoryEvent.SHUTTLE_WILL_ARRIVE_AT_STOP, { lastStopArrival: lastStop, willArriveAt: shuttleArrival, }); await this.markShuttleAsAtStop(shuttleArrival.shuttleId); await this.updateShuttleLastStopArrival(shuttleArrival); } } private async markShuttleAsAtStop(shuttleId: string) { this.shuttlesAtStop.add(shuttleId); } private async markShuttleAsNotAtStop(shuttleId: string) { this.shuttlesAtStop.delete(shuttleId); } public async checkIfShuttleIsAtStop(shuttleId: string) { return this.shuttlesAtStop.has(shuttleId); } private async updateShuttleLastStopArrival(lastStopArrival: ShuttleStopArrival) { this.shuttleLastStopArrivals.set(lastStopArrival.shuttleId, lastStopArrival); } public 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; } public async getArrivedStopIfNextStop( shuttle: IShuttle, canReturnShuttleCurrentStop: boolean = false, ): Promise { const degreeDelta = this.shuttleStopArrivalDegreeDelta; const lastStopArrival = await this.getShuttleLastStopArrival(shuttle.id); if (lastStopArrival) { // Return the shuttle's current stop depending on the flag if (canReturnShuttleCurrentStop) { const lastStop = await this.getStopById(lastStopArrival.stopId); if (lastStop && shuttleHasArrivedAtStop(shuttle, lastStop, degreeDelta)) { return lastStop; } } const lastOrderedStop = await this.getOrderedStopByRouteAndStopId(shuttle.routeId, lastStopArrival.stopId); const orderedStopAfter = lastOrderedStop?.nextStop; if (orderedStopAfter) { const stopAfter = await this.getStopById(orderedStopAfter.stopId); if (stopAfter && shuttleHasArrivedAtStop(shuttle, stopAfter, degreeDelta)) { return stopAfter; } } } else { const orderedStops = await this.getOrderedStopsByRouteId(shuttle.routeId); for (const orderedStop of orderedStops) { const stop = await this.getStopById(orderedStop.stopId); if (stop != null && shuttleHasArrivedAtStop(shuttle, stop, degreeDelta)) { return stop; } } } return undefined; } public async getShuttleLastStopArrival(shuttleId: string): Promise { return this.shuttleLastStopArrivals.get(shuttleId); } private async removeEntityByMatcherIfExists(callback: (value: T) => boolean, arrayToSearchIn: T[]) { const index = arrayToSearchIn.findIndex(callback); if (index > -1) { const entityToReturn = arrayToSearchIn[index]; arrayToSearchIn.splice(index, 1); return entityToReturn; } return null; } private async removeEntityByIdIfExists(entityId: string, arrayToSearchIn: T[]) { return await this.removeEntityByMatcherIfExists((value) => value.id === entityId, arrayToSearchIn); } public async removeRouteIfExists(routeId: string): Promise { return await this.removeEntityByIdIfExists(routeId, this.routes); } public async removeShuttleIfExists(shuttleId: string): Promise { const shuttle = await this.removeEntityByIdIfExists(shuttleId, this.shuttles); if (shuttle != null) { this.emit(ShuttleRepositoryEvent.SHUTTLE_REMOVED, shuttle); this.shuttlesAtStop.delete(shuttleId); await this.removeShuttleLastStopIfExists(shuttleId); } return shuttle; } public async removeStopIfExists(stopId: string): Promise { return await this.removeEntityByIdIfExists(stopId, this.stops); } public async removeOrderedStopIfExists(stopId: string, routeId: string): Promise { return await this.removeEntityByMatcherIfExists((orderedStop) => { return orderedStop.stopId === stopId && orderedStop.routeId === routeId }, this.orderedStops); } private async removeShuttleLastStopIfExists(shuttleId: string) { this.shuttleLastStopArrivals.delete(shuttleId); } public async clearShuttleData(): Promise { this.shuttles = []; this.shuttlesAtStop.clear(); await this.clearShuttleLastStopData(); } public async clearOrderedStopData(): Promise { this.orderedStops = []; } public async clearRouteData(): Promise { this.routes = []; } public async clearStopData(): Promise { this.stops = []; } private async clearShuttleLastStopData(): Promise { this.shuttleLastStopArrivals.clear(); } }