import { ShuttleGetterRepository } from "../../repositories/ShuttleGetterRepository"; import { IEta } from "../../entities/ShuttleRepositoryEntities"; import { AppleNotificationSender, NotificationAlertArguments } from "../senders/AppleNotificationSender"; import { NotificationRepository, ScheduledNotification } from "../../repositories/NotificationRepository"; import { InMemoryNotificationRepository } from "../../repositories/InMemoryNotificationRepository"; export class ETANotificationScheduler { public static readonly defaultSecondsThresholdForNotificationToFire = 180; constructor( private shuttleRepository: ShuttleGetterRepository, private notificationRepository: NotificationRepository = new InMemoryNotificationRepository(), private appleNotificationSender = new AppleNotificationSender(), private interchangeSystemId: string, ) { this.etaSubscriberCallback = this.etaSubscriberCallback.bind(this); this.sendEtaNotificationImmediately = this.sendEtaNotificationImmediately.bind(this); this.sendEtaNotificationImmediatelyIfSecondsRemainingBelowThreshold = this.sendEtaNotificationImmediatelyIfSecondsRemainingBelowThreshold.bind(this); } private async sendEtaNotificationImmediately(notificationData: ScheduledNotification): Promise { const { deviceId, shuttleId, stopId } = notificationData; const shuttle = await this.shuttleRepository.getShuttleById(shuttleId); const stop = await this.shuttleRepository.getStopById(stopId); const eta = await this.shuttleRepository.getEtaForShuttleAndStopId(shuttleId, stopId); if (!shuttle) { console.warn(`Notification ${notificationData} fell through; no associated shuttle`); return false; } if (!stop) { console.warn(`Notification ${notificationData} fell through; no associated stop`); return false; } // Notification may not be sent if ETA is unavailable at the moment; // this is fine because it will be sent again when ETA becomes available if (!eta) { console.warn(`Notification ${notificationData} fell through; no associated ETA`); return false; } const notificationAlertArguments: NotificationAlertArguments = { title: "Shuttle is arriving", body: `Shuttle is approaching ${stop.name} in ${Math.ceil(eta.secondsRemaining / 60)} minutes.`, customKeys: { shuttleId, stopId, systemId: this.interchangeSystemId, }, } return this.appleNotificationSender.sendNotificationImmediately(deviceId, notificationAlertArguments); } private async etaSubscriberCallback(eta: IEta) { const deviceIdsToRemove = new Set(); const notifications = await this.notificationRepository.getAllNotificationsForShuttleAndStopId( eta.shuttleId, eta.stopId ) for (let notification of notifications) { const deliveredSuccessfully = await this.sendEtaNotificationImmediatelyIfSecondsRemainingBelowThreshold(notification, eta.secondsRemaining); if (deliveredSuccessfully) { deviceIdsToRemove.add(notification.deviceId); } } deviceIdsToRemove.forEach((deviceId) => { this.notificationRepository.deleteNotificationIfExists({ shuttleId: eta.shuttleId, stopId: eta.stopId, deviceId, }) }); } private async sendEtaNotificationImmediatelyIfSecondsRemainingBelowThreshold(notificationObject: ScheduledNotification, etaSecondsRemaining: number) { if (etaSecondsRemaining > notificationObject.secondsThreshold) { return false; } return await this.sendEtaNotificationImmediately(notificationObject); } // The following is a workaround for the constructor being called twice public startListeningForUpdates() { this.shuttleRepository.subscribeToEtaUpdates(this.etaSubscriberCallback); } public stopListeningForUpdates() { this.shuttleRepository.subscribeToEtaUpdates(this.etaSubscriberCallback); } }