Files
project-inter-server/src/notifications/schedulers/ETANotificationScheduler.ts

98 lines
3.9 KiB
TypeScript

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<boolean> {
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<string>();
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);
}
}