import { ETANotificationScheduler } from "../notifications/schedulers/ETANotificationScheduler"; import { TimedApiBasedRepositoryLoader } from "../loaders/TimedApiBasedRepositoryLoader"; import { UnoptimizedInMemoryShuttleRepository } from "../repositories/shuttle/UnoptimizedInMemoryShuttleRepository"; import { RedisNotificationRepository } from "../repositories/notifications/RedisNotificationRepository"; import { NotificationRepository } from "../repositories/notifications/NotificationRepository"; import { ShuttleGetterSetterRepository } from "../repositories/shuttle/ShuttleGetterSetterRepository"; import { InMemoryNotificationRepository } from "../repositories/notifications/InMemoryNotificationRepository"; import { AppleNotificationSender } from "../notifications/senders/AppleNotificationSender"; import { ApiBasedShuttleRepositoryLoader } from "../loaders/shuttle/ApiBasedShuttleRepositoryLoader"; import { ParkingGetterSetterRepository } from "../repositories/parking/ParkingGetterSetterRepository"; import { InMemoryParkingRepository } from "../repositories/parking/InMemoryParkingRepository"; import { buildParkingRepositoryLoaderIfExists, ParkingRepositoryLoaderBuilderArguments } from "../loaders/parking/buildParkingRepositoryLoaderIfExists"; import { RedisParkingRepository } from "../repositories/parking/RedisParkingRepository"; import { RedisShuttleRepository } from "../repositories/shuttle/RedisShuttleRepository"; import { ShuttleGetterRepository } from "../repositories/shuttle/ShuttleGetterRepository"; import { InMemoryExternalSourceETARepository } from "../repositories/shuttle/eta/InMemoryExternalSourceETARepository"; import { ETAGetterRepository } from "../repositories/shuttle/eta/ETAGetterRepository"; import { RedisSelfUpdatingETARepository } from "../repositories/shuttle/eta/RedisSelfUpdatingETARepository"; import { RedisExternalSourceETARepository } from "../repositories/shuttle/eta/RedisExternalSourceETARepository"; import { InMemorySelfUpdatingETARepository } from "../repositories/shuttle/eta/InMemorySelfUpdatingETARepository"; import { BaseRedisETARepository } from "../repositories/shuttle/eta/BaseRedisETARepository"; import { BaseInMemoryETARepository } from "../repositories/shuttle/eta/BaseInMemoryETARepository"; import createRedisClientForRepository from "../helpers/createRedisClientForRepository"; export interface InterchangeSystemBuilderArguments { name: string; /** * ID to identify the system internally and in the API. */ id: string; /** * ID for fetching shuttle data from the Passio GO! system. */ passioSystemId: string; /** * ID for the parking repository ID in the codebase. */ parkingSystemId?: string; /** * Controls whether to self-calculate ETAs or use the external * shuttle provider for them. */ useSelfUpdatingEtas: boolean; /** * The size of the threshold to detect when a shuttle has arrived * at a stop, in latitude/longitude degrees. */ shuttleStopArrivalDegreeDelta: number; } export class InterchangeSystem { private constructor( public name: string, public id: string, public shuttleTimedDataLoader: TimedApiBasedRepositoryLoader, public shuttleRepository: ShuttleGetterSetterRepository, public etaRepository: ETAGetterRepository, public notificationScheduler: ETANotificationScheduler, public notificationRepository: NotificationRepository, public parkingTimedDataLoader: TimedApiBasedRepositoryLoader | null, public parkingRepository: ParkingGetterSetterRepository | null, ) { } /** * Construct an instance of the class where all composited * classes are correctly linked, meant for use in development and production. * @param args */ static async build( args: InterchangeSystemBuilderArguments, ) { const { shuttleRepository, timedShuttleDataLoader, etaRepository } = await InterchangeSystem.buildRedisShuttleLoaderAndRepositories(args); timedShuttleDataLoader.start(); const { notificationScheduler, notificationRepository } = await InterchangeSystem.buildNotificationSchedulerAndRepository( etaRepository, shuttleRepository, args ); notificationScheduler.startListeningForUpdates(); let { parkingRepository, timedParkingLoader } = await InterchangeSystem.buildRedisParkingLoaderAndRepository(args.parkingSystemId); timedParkingLoader?.start(); return new InterchangeSystem( args.name, args.id, timedShuttleDataLoader, shuttleRepository, etaRepository, notificationScheduler, notificationRepository, timedParkingLoader, parkingRepository, ); } private static async buildRedisShuttleLoaderAndRepositories(args: InterchangeSystemBuilderArguments) { const shuttleRepository = new RedisShuttleRepository( createRedisClientForRepository(), args.shuttleStopArrivalDegreeDelta, ); await shuttleRepository.connect(); let etaRepository: BaseRedisETARepository; let shuttleDataLoader: ApiBasedShuttleRepositoryLoader; if (args.useSelfUpdatingEtas) { etaRepository = new RedisSelfUpdatingETARepository(shuttleRepository); (etaRepository as RedisSelfUpdatingETARepository).startListeningForUpdates(); shuttleDataLoader = new ApiBasedShuttleRepositoryLoader( args.passioSystemId, args.id, shuttleRepository, ); } else { etaRepository = new RedisExternalSourceETARepository(); shuttleDataLoader = new ApiBasedShuttleRepositoryLoader( args.passioSystemId, args.id, shuttleRepository, etaRepository as RedisExternalSourceETARepository, ); } await etaRepository.connect(); const timedShuttleDataLoader = new TimedApiBasedRepositoryLoader( shuttleDataLoader, ); return { shuttleRepository, etaRepository, timedShuttleDataLoader }; } private static async buildNotificationSchedulerAndRepository( etaRepository: ETAGetterRepository, shuttleRepository: ShuttleGetterRepository, args: InterchangeSystemBuilderArguments ) { const notificationRepository = new RedisNotificationRepository(); await notificationRepository.connect(); const notificationScheduler = new ETANotificationScheduler( etaRepository, shuttleRepository, notificationRepository, new AppleNotificationSender(), args.id ); return { notificationScheduler, notificationRepository }; } private static async buildRedisParkingLoaderAndRepository(id?: string) { if (id === undefined) { return { parkingRepository: null, timedParkingLoader: null }; } let parkingRepository: RedisParkingRepository | null = new RedisParkingRepository(); await parkingRepository.connect(); const loaderBuilderArguments: ParkingRepositoryLoaderBuilderArguments = { id, repository: parkingRepository, }; let parkingLoader = buildParkingRepositoryLoaderIfExists( loaderBuilderArguments, ); let timedParkingLoader = null; if (parkingLoader == null) { parkingRepository = null; } else { timedParkingLoader = new TimedApiBasedRepositoryLoader(parkingLoader); } return { parkingRepository, timedParkingLoader }; } /** * Construct an instance of the class where all composited * classes are correctly linked, meant for unit tests, and server/app * integration tests. * @param args */ static buildForTesting( args: InterchangeSystemBuilderArguments, ) { const { shuttleRepository, timedShuttleLoader, etaRepository } = InterchangeSystem.buildInMemoryShuttleLoaderAndRepositories(args); // Timed shuttle loader is not started here const { notificationScheduler, notificationRepository } = InterchangeSystem.buildInMemoryNotificationSchedulerAndRepository( etaRepository, shuttleRepository, args ); notificationScheduler.startListeningForUpdates(); let { parkingRepository, timedParkingLoader } = this.buildInMemoryParkingLoaderAndRepository(args.parkingSystemId); // Timed parking loader is not started here return new InterchangeSystem( args.name, args.id, timedShuttleLoader, shuttleRepository, etaRepository, notificationScheduler, notificationRepository, timedParkingLoader, parkingRepository, ); } private static buildInMemoryNotificationSchedulerAndRepository( etaRepository: ETAGetterRepository, shuttleRepository: UnoptimizedInMemoryShuttleRepository, args: InterchangeSystemBuilderArguments ) { const notificationRepository = new InMemoryNotificationRepository(); const notificationScheduler = new ETANotificationScheduler( etaRepository, shuttleRepository, notificationRepository, new AppleNotificationSender(false), args.id ); return { notificationScheduler, notificationRepository }; } private static buildInMemoryParkingLoaderAndRepository(id?: string) { if (id === undefined) { return { parkingRepository: null, timedParkingLoader: null }; } let parkingRepository: ParkingGetterSetterRepository | null = new InMemoryParkingRepository(); const loaderBuilderArguments: ParkingRepositoryLoaderBuilderArguments = { id, repository: parkingRepository, }; let parkingLoader = buildParkingRepositoryLoaderIfExists( loaderBuilderArguments, ); let timedParkingLoader = null; if (parkingLoader == null) { parkingRepository = null; } else { timedParkingLoader = new TimedApiBasedRepositoryLoader(parkingLoader); } return { parkingRepository, timedParkingLoader }; } private static buildInMemoryShuttleLoaderAndRepositories(args: InterchangeSystemBuilderArguments) { const shuttleRepository = new UnoptimizedInMemoryShuttleRepository( args.shuttleStopArrivalDegreeDelta, ); let etaRepository: BaseInMemoryETARepository; let shuttleDataLoader: ApiBasedShuttleRepositoryLoader; if (args.useSelfUpdatingEtas) { etaRepository = new InMemorySelfUpdatingETARepository(shuttleRepository); (etaRepository as InMemorySelfUpdatingETARepository).startListeningForUpdates(); shuttleDataLoader = new ApiBasedShuttleRepositoryLoader( args.passioSystemId, args.id, shuttleRepository, ); } else { etaRepository = new InMemoryExternalSourceETARepository(); shuttleDataLoader = new ApiBasedShuttleRepositoryLoader( args.passioSystemId, args.id, shuttleRepository, etaRepository as InMemoryExternalSourceETARepository, ); } // Note that this loader should not be started, // so the test data doesn't get overwritten const timedShuttleLoader = new TimedApiBasedRepositoryLoader( shuttleDataLoader ); return { shuttleRepository, etaRepository, timedShuttleLoader }; } }