From d0359e22ccad96d738409b4dde649b93461ef4d8 Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Sun, 2 Feb 2025 12:43:52 -0800 Subject: [PATCH 01/60] add NotificationService class with stubs, and test file --- src/services/NotificationService.ts | 17 +++++++++++++++++ test/services/NotificationServiceTests.test.ts | 0 2 files changed, 17 insertions(+) create mode 100644 src/services/NotificationService.ts create mode 100644 test/services/NotificationServiceTests.test.ts diff --git a/src/services/NotificationService.ts b/src/services/NotificationService.ts new file mode 100644 index 0000000..720161e --- /dev/null +++ b/src/services/NotificationService.ts @@ -0,0 +1,17 @@ +class NotificationService { + public startListeningForNotification() { + + } + + public stopListeningForNotification() { + + } + + public stopListeningForAllNotifications() { + + } + + public startReloadingNotificationTokens() { + + } +} diff --git a/test/services/NotificationServiceTests.test.ts b/test/services/NotificationServiceTests.test.ts new file mode 100644 index 0000000..e69de29 From 08d1f886936a40e9948e18ca22a8004999c11329 Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Sun, 2 Feb 2025 12:47:13 -0800 Subject: [PATCH 02/60] add constructor which takes in repository --- src/services/NotificationService.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/services/NotificationService.ts b/src/services/NotificationService.ts index 720161e..d125fad 100644 --- a/src/services/NotificationService.ts +++ b/src/services/NotificationService.ts @@ -1,4 +1,8 @@ +import { GetterRepository } from "../repositories/GetterRepository"; + class NotificationService { + constructor(private repository: GetterRepository) {} + public startListeningForNotification() { } From 62b040fccd5d0c48c27795e4512962ee4897bc81 Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Sun, 2 Feb 2025 13:49:00 -0800 Subject: [PATCH 03/60] add test cases for class --- src/services/NotificationService.ts | 2 +- .../services/NotificationServiceTests.test.ts | 35 +++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/src/services/NotificationService.ts b/src/services/NotificationService.ts index d125fad..9274d01 100644 --- a/src/services/NotificationService.ts +++ b/src/services/NotificationService.ts @@ -1,6 +1,6 @@ import { GetterRepository } from "../repositories/GetterRepository"; -class NotificationService { +export class NotificationService { constructor(private repository: GetterRepository) {} public startListeningForNotification() { diff --git a/test/services/NotificationServiceTests.test.ts b/test/services/NotificationServiceTests.test.ts index e69de29..c559ee3 100644 --- a/test/services/NotificationServiceTests.test.ts +++ b/test/services/NotificationServiceTests.test.ts @@ -0,0 +1,35 @@ +import { describe, it } from "@jest/globals"; + +describe("NotificationService", () => { + describe("startReloadingNotificationTokens", () => { + it("reloads keys when first called", async () => { + + }); + + it("sets a timer to reload keys", async () => { + + }); + }); + + describe("startListeningForNotification", () => { + it("sends a notification to given shuttle/stop ID when changed", async () => { + + }); + + it("clears the notification after delivering successfully", async () => { + + }); + }); + + describe("stopListeningForNotification", () => { + it("stops notification from sending to given shuttle/stop ID", async () => { + + }); + }); + + describe("stopListeningForAllNotifications", () => { + it("clears all notifications scheduled to be sent", async () => { + + }); + }) +}); From 88ab399c1190109c1a4d5b7fe1ef1d930b5ca71e Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Sun, 2 Feb 2025 13:49:59 -0800 Subject: [PATCH 04/60] rename methods for clarity --- src/services/NotificationService.ts | 6 +++--- test/services/NotificationServiceTests.test.ts | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/services/NotificationService.ts b/src/services/NotificationService.ts index 9274d01..fe816b2 100644 --- a/src/services/NotificationService.ts +++ b/src/services/NotificationService.ts @@ -3,15 +3,15 @@ import { GetterRepository } from "../repositories/GetterRepository"; export class NotificationService { constructor(private repository: GetterRepository) {} - public startListeningForNotification() { + public scheduleNotification() { } - public stopListeningForNotification() { + public cancelNotification() { } - public stopListeningForAllNotifications() { + public cancelAllNotifications() { } diff --git a/test/services/NotificationServiceTests.test.ts b/test/services/NotificationServiceTests.test.ts index c559ee3..f2b0461 100644 --- a/test/services/NotificationServiceTests.test.ts +++ b/test/services/NotificationServiceTests.test.ts @@ -11,7 +11,7 @@ describe("NotificationService", () => { }); }); - describe("startListeningForNotification", () => { + describe("scheduleNotification", () => { it("sends a notification to given shuttle/stop ID when changed", async () => { }); @@ -21,13 +21,13 @@ describe("NotificationService", () => { }); }); - describe("stopListeningForNotification", () => { + describe("cancelNotification", () => { it("stops notification from sending to given shuttle/stop ID", async () => { }); }); - describe("stopListeningForAllNotifications", () => { + describe("cancelAllNotifications", () => { it("clears all notifications scheduled to be sent", async () => { }); From c4771adef8e9a731a5b2ebd4cbe2cb0888374ad8 Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Sun, 2 Feb 2025 13:51:05 -0800 Subject: [PATCH 05/60] add ScheduledNotificationData and make schedule functions async --- src/services/NotificationService.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/services/NotificationService.ts b/src/services/NotificationService.ts index fe816b2..755b39c 100644 --- a/src/services/NotificationService.ts +++ b/src/services/NotificationService.ts @@ -1,13 +1,19 @@ import { GetterRepository } from "../repositories/GetterRepository"; +interface ScheduledNotificationData { + deviceId: string; + shuttleId: string; + stopId: string; +} + export class NotificationService { constructor(private repository: GetterRepository) {} - public scheduleNotification() { + public async scheduleNotification() { } - public cancelNotification() { + public async cancelNotification() { } From 483307f225b9bd7e9494b3f82e95bd0c1fac6ffc Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Sun, 2 Feb 2025 13:51:48 -0800 Subject: [PATCH 06/60] add arguments to scheduling methods --- src/services/NotificationService.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/services/NotificationService.ts b/src/services/NotificationService.ts index 755b39c..b47fff0 100644 --- a/src/services/NotificationService.ts +++ b/src/services/NotificationService.ts @@ -9,11 +9,11 @@ interface ScheduledNotificationData { export class NotificationService { constructor(private repository: GetterRepository) {} - public async scheduleNotification() { + public async scheduleNotification({ deviceId, shuttleId, stopId }: ScheduledNotificationData) { } - public async cancelNotification() { + public async cancelNotification({ deviceId, shuttleId, stopId }: ScheduledNotificationData) { } From 777388486225a030019e87b4e479129d1027ba3e Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Sun, 2 Feb 2025 13:59:00 -0800 Subject: [PATCH 07/60] add .env.example containing apns values --- .env.example | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .env.example diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..68d90ae --- /dev/null +++ b/.env.example @@ -0,0 +1,2 @@ +APNS_KEY_ID= +APNS_TEAM_ID= From 3bb7f3a030dcba0c3db3197077d91813a1c7b721 Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Sun, 2 Feb 2025 14:00:43 -0800 Subject: [PATCH 08/60] add jsonwebtoken to dependencies --- package-lock.json | 107 +++++++++++++++++++++++++++++++++++++++++++++- package.json | 3 +- 2 files changed, 108 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index acfc543..d36fb6b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,8 @@ "version": "1.0.0", "dependencies": { "@apollo/server": "^4.11.2", - "graphql": "^16.10.0" + "graphql": "^16.10.0", + "jsonwebtoken": "^9.0.2" }, "devDependencies": { "@graphql-codegen/cli": "5.0.3", @@ -4173,6 +4174,11 @@ "ieee754": "^1.1.13" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -4863,6 +4869,14 @@ "node": ">= 0.4" } }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -6895,6 +6909,62 @@ "node": ">=6" } }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/jsonwebtoken/node_modules/semver": { + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.0.tgz", + "integrity": "sha512-DrfFnPzblFmNrIZzg5RzHegbiRWg7KMR7btwi2yjHwx06zsUbO5g613sVwEV7FTwmzJu+Io0lJe2GJ3LxqpvBQ==", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, "node_modules/kleur": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", @@ -6981,12 +7051,47 @@ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "dev": true }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" + }, "node_modules/lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", "dev": true }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" + }, "node_modules/lodash.sortby": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", diff --git a/package.json b/package.json index 199a457..2595929 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "private": true, "dependencies": { "@apollo/server": "^4.11.2", - "graphql": "^16.10.0" + "graphql": "^16.10.0", + "jsonwebtoken": "^9.0.2" } } From fb981d4800705967ff73ce7b8c80bf8e3c3f5c3b Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Sun, 2 Feb 2025 14:09:35 -0800 Subject: [PATCH 09/60] add bundle id and key path as variables --- .env.example | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.env.example b/.env.example index 68d90ae..92650db 100644 --- a/.env.example +++ b/.env.example @@ -1,2 +1,4 @@ APNS_KEY_ID= APNS_TEAM_ID= +APNS_BUNDLE_ID= +APNS_KEY_PATH= From e2e47bafc53017869c8d617be1fc332472fd5cfe Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Sun, 2 Feb 2025 14:21:20 -0800 Subject: [PATCH 10/60] add jsonwebtoken types --- package-lock.json | 15 +++++++++++++++ package.json | 3 ++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index d36fb6b..f9544cc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0", "dependencies": { "@apollo/server": "^4.11.2", + "@types/jsonwebtoken": "^9.0.8", "graphql": "^16.10.0", "jsonwebtoken": "^9.0.2" }, @@ -3566,6 +3567,15 @@ "integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==", "dev": true }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.8", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.8.tgz", + "integrity": "sha512-7fx54m60nLFUVYlxAB1xpe9CBWX2vSrk50Y6ogRJ1v5xxtba7qXTg5BgYDN5dq+yuQQ9HaVlHJyAAt1/mxryFg==", + "dependencies": { + "@types/ms": "*", + "@types/node": "*" + } + }, "node_modules/@types/long": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", @@ -3576,6 +3586,11 @@ "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==" }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==" + }, "node_modules/@types/node": { "version": "22.10.2", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.2.tgz", diff --git a/package.json b/package.json index 2595929..d698ded 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,8 @@ "@types/node": "^22.10.2", "jest": "^29.7.0", "ts-jest": "^29.2.5", - "typescript": "^5.7.2" + "typescript": "^5.7.2", + "@types/jsonwebtoken": "^9.0.8" }, "private": true, "dependencies": { From a2386356f12397328fcbac9a401bbfe8b9a6d78b Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Sun, 2 Feb 2025 14:26:14 -0800 Subject: [PATCH 11/60] add experimental method to reload APNs token --- src/services/NotificationService.ts | 41 +++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/src/services/NotificationService.ts b/src/services/NotificationService.ts index b47fff0..982028b 100644 --- a/src/services/NotificationService.ts +++ b/src/services/NotificationService.ts @@ -1,4 +1,7 @@ import { GetterRepository } from "../repositories/GetterRepository"; +import * as crypto from "node:crypto"; +import jwt from "jsonwebtoken"; +import * as fs from "node:fs"; interface ScheduledNotificationData { deviceId: string; @@ -7,8 +10,46 @@ interface ScheduledNotificationData { } export class NotificationService { + private token: string | undefined = undefined; + private lastRefreshedTimeMs: number | undefined = undefined; + constructor(private repository: GetterRepository) {} + private encryptionKey = crypto.randomBytes(32); + private iv = crypto.randomBytes(16); + + public async reloadAPNsTokenIfTimePassed() { + if (this.lastReloadedTimeForAPNsIsTooRecent()) { + return; + } + + const keyId = process.env.APNS_KEY_ID; + const teamId = process.env.APNS_TEAM_ID; + const privateKeyPath = process.env.APNS_KEY_PATH; + if (!privateKeyPath) return; + const privateKey = fs.readFileSync(privateKeyPath); + + const tokenHeader = { + alg: "ES256", + "kid": keyId, + }; + + const claimsPayload = { + "iss": teamId, + "iat": Date.now(), + }; + + this.token = jwt.sign(claimsPayload, privateKey, { + algorithm: "ES256", + header: tokenHeader + }); + } + + private lastReloadedTimeForAPNsIsTooRecent() { + const thirtyMinutesMs = 1800000; + return this.lastRefreshedTimeMs && Date.now() - this.lastRefreshedTimeMs < thirtyMinutesMs; + } + public async scheduleNotification({ deviceId, shuttleId, stopId }: ScheduledNotificationData) { } From e30fafc9b74d9aaa56a98b8a12f1ce0db5beb1ff Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Sun, 2 Feb 2025 14:26:30 -0800 Subject: [PATCH 12/60] remove method to start reloading token over timed intervals --- src/services/NotificationService.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/services/NotificationService.ts b/src/services/NotificationService.ts index 982028b..1b341b5 100644 --- a/src/services/NotificationService.ts +++ b/src/services/NotificationService.ts @@ -61,8 +61,4 @@ export class NotificationService { public cancelAllNotifications() { } - - public startReloadingNotificationTokens() { - - } } From a69ab5853c945987b645c684008d104092643220 Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Sun, 2 Feb 2025 14:28:10 -0800 Subject: [PATCH 13/60] update test cases for notification service --- test/services/NotificationServiceTests.test.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/services/NotificationServiceTests.test.ts b/test/services/NotificationServiceTests.test.ts index f2b0461..741e155 100644 --- a/test/services/NotificationServiceTests.test.ts +++ b/test/services/NotificationServiceTests.test.ts @@ -1,15 +1,15 @@ import { describe, it } from "@jest/globals"; describe("NotificationService", () => { - describe("startReloadingNotificationTokens", () => { - it("reloads keys when first called", async () => { + describe("reloadAPNsTokenIfTimePassed", () => { + it("reloads the token if token hasn't been generated yet", async () => { }); - it("sets a timer to reload keys", async () => { + it("doesn't reload the token if last refreshed time is recent", async () => { }); - }); + }) describe("scheduleNotification", () => { it("sends a notification to given shuttle/stop ID when changed", async () => { From e0820cd17b628454a9d5be1331a2e6cb9f9059b5 Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Sun, 2 Feb 2025 14:42:32 -0800 Subject: [PATCH 14/60] add test setup --- test/services/NotificationServiceTests.test.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/test/services/NotificationServiceTests.test.ts b/test/services/NotificationServiceTests.test.ts index 741e155..0d25b1b 100644 --- a/test/services/NotificationServiceTests.test.ts +++ b/test/services/NotificationServiceTests.test.ts @@ -1,9 +1,18 @@ -import { describe, it } from "@jest/globals"; +import { beforeEach, describe, it } from "@jest/globals"; +import { NotificationService } from "../../src/services/NotificationService"; +import { UnoptimizedInMemoryRepository } from "../../src/repositories/UnoptimizedInMemoryRepository"; describe("NotificationService", () => { + let repository: UnoptimizedInMemoryRepository + let notificationService: NotificationService; + + beforeEach(() => { + repository = new UnoptimizedInMemoryRepository(); + notificationService = new NotificationService(repository); + }) + describe("reloadAPNsTokenIfTimePassed", () => { it("reloads the token if token hasn't been generated yet", async () => { - }); it("doesn't reload the token if last refreshed time is recent", async () => { From ddf008a0d61ab760d7592820692ccb9cd7c94373 Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Sun, 2 Feb 2025 14:43:07 -0800 Subject: [PATCH 15/60] remove async keyword from APNs reload --- src/services/NotificationService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/NotificationService.ts b/src/services/NotificationService.ts index 1b341b5..ea92c59 100644 --- a/src/services/NotificationService.ts +++ b/src/services/NotificationService.ts @@ -18,7 +18,7 @@ export class NotificationService { private encryptionKey = crypto.randomBytes(32); private iv = crypto.randomBytes(16); - public async reloadAPNsTokenIfTimePassed() { + public reloadAPNsTokenIfTimePassed() { if (this.lastReloadedTimeForAPNsIsTooRecent()) { return; } From 9129ff6e9101fcf8ac3813b5ee2370ab7b3a999e Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Sun, 2 Feb 2025 14:44:35 -0800 Subject: [PATCH 16/60] add getter to lastRefreshedTimeMs --- src/services/NotificationService.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/services/NotificationService.ts b/src/services/NotificationService.ts index ea92c59..8eacaa9 100644 --- a/src/services/NotificationService.ts +++ b/src/services/NotificationService.ts @@ -11,7 +11,11 @@ interface ScheduledNotificationData { export class NotificationService { private token: string | undefined = undefined; - private lastRefreshedTimeMs: number | undefined = undefined; + + private _lastRefreshedTimeMs: number | undefined = undefined; + get lastRefreshedTimeMs() { + return this._lastRefreshedTimeMs; + } constructor(private repository: GetterRepository) {} @@ -47,7 +51,7 @@ export class NotificationService { private lastReloadedTimeForAPNsIsTooRecent() { const thirtyMinutesMs = 1800000; - return this.lastRefreshedTimeMs && Date.now() - this.lastRefreshedTimeMs < thirtyMinutesMs; + return this._lastRefreshedTimeMs && Date.now() - this._lastRefreshedTimeMs < thirtyMinutesMs; } public async scheduleNotification({ deviceId, shuttleId, stopId }: ScheduledNotificationData) { From 3ff041dd4e75c0dbab126c70cb57824b649e719e Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Sun, 2 Feb 2025 14:46:21 -0800 Subject: [PATCH 17/60] add tests for reloadAPNsTokenIfTimePassed --- test/services/NotificationServiceTests.test.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/test/services/NotificationServiceTests.test.ts b/test/services/NotificationServiceTests.test.ts index 0d25b1b..29982b3 100644 --- a/test/services/NotificationServiceTests.test.ts +++ b/test/services/NotificationServiceTests.test.ts @@ -1,4 +1,4 @@ -import { beforeEach, describe, it } from "@jest/globals"; +import { beforeEach, describe, expect, it } from "@jest/globals"; import { NotificationService } from "../../src/services/NotificationService"; import { UnoptimizedInMemoryRepository } from "../../src/repositories/UnoptimizedInMemoryRepository"; @@ -13,10 +13,17 @@ describe("NotificationService", () => { describe("reloadAPNsTokenIfTimePassed", () => { it("reloads the token if token hasn't been generated yet", async () => { + notificationService.reloadAPNsTokenIfTimePassed(); + expect(notificationService.lastRefreshedTimeMs).toBeDefined(); }); it("doesn't reload the token if last refreshed time is recent", async () => { + notificationService.reloadAPNsTokenIfTimePassed(); + const lastRefreshedTimeMs = notificationService.lastRefreshedTimeMs; + notificationService.reloadAPNsTokenIfTimePassed(); + // Expect no change to have occurred + expect(lastRefreshedTimeMs).toEqual(notificationService.lastRefreshedTimeMs); }); }) From 998dcaa9c7c33caca83a9e10f8425f06d8da102e Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Sun, 2 Feb 2025 14:48:03 -0800 Subject: [PATCH 18/60] add process.env variables --- test/services/NotificationServiceTests.test.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test/services/NotificationServiceTests.test.ts b/test/services/NotificationServiceTests.test.ts index 29982b3..c4ab3a7 100644 --- a/test/services/NotificationServiceTests.test.ts +++ b/test/services/NotificationServiceTests.test.ts @@ -9,6 +9,14 @@ describe("NotificationService", () => { beforeEach(() => { repository = new UnoptimizedInMemoryRepository(); notificationService = new NotificationService(repository); + + // Ensure that tests don't hit the server + process.env = { + ...process.env, + APNS_KEY_ID: "1", + APNS_TEAM_ID: "1", + APNS_KEY_PATH: "./dummy-path.p8" + } }) describe("reloadAPNsTokenIfTimePassed", () => { From 8605b2087ae8af9e789df073723532fdc20f5283 Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Sun, 2 Feb 2025 14:52:07 -0800 Subject: [PATCH 19/60] add sample key and mock fs return value --- test/services/NotificationServiceTests.test.ts | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/test/services/NotificationServiceTests.test.ts b/test/services/NotificationServiceTests.test.ts index c4ab3a7..d34945e 100644 --- a/test/services/NotificationServiceTests.test.ts +++ b/test/services/NotificationServiceTests.test.ts @@ -1,6 +1,16 @@ -import { beforeEach, describe, expect, it } from "@jest/globals"; +import { beforeEach, describe, expect, it, jest } from "@jest/globals"; import { NotificationService } from "../../src/services/NotificationService"; import { UnoptimizedInMemoryRepository } from "../../src/repositories/UnoptimizedInMemoryRepository"; +import fs from "fs"; + +jest.mock("fs"); + +const sampleKey = `-----BEGIN PRIVATE KEY----- +9EKORECHU09 eouEUHKCREOFA12409kOHKOEU9125ADABOU098AOEBAOEI15bhao +XAOECR15bAAOECxrU91bAOEIAOERix145AOERDIIAOUX15997124xARODEARi119 +7k5AOEX151509KBRACOUDIAURCL1IAEO91245bAOECHDUAOE90kAOEB15139KAOA +19ax15qu +-----END PRIVATE KEY-----` describe("NotificationService", () => { let repository: UnoptimizedInMemoryRepository @@ -16,7 +26,9 @@ describe("NotificationService", () => { APNS_KEY_ID: "1", APNS_TEAM_ID: "1", APNS_KEY_PATH: "./dummy-path.p8" - } + }; + + (fs.readFileSync as jest.Mock).mockReturnValue(sampleKey); }) describe("reloadAPNsTokenIfTimePassed", () => { From 36909dd5ac1312e28541dcb3c9d9799ac80859f3 Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Sun, 2 Feb 2025 14:55:49 -0800 Subject: [PATCH 20/60] fix fs import --- src/services/NotificationService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/NotificationService.ts b/src/services/NotificationService.ts index 8eacaa9..1eca53c 100644 --- a/src/services/NotificationService.ts +++ b/src/services/NotificationService.ts @@ -1,7 +1,7 @@ import { GetterRepository } from "../repositories/GetterRepository"; import * as crypto from "node:crypto"; import jwt from "jsonwebtoken"; -import * as fs from "node:fs"; +import fs from "fs"; interface ScheduledNotificationData { deviceId: string; From 311906d88debcb2d832b745d002f9194300e7a5d Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Sun, 2 Feb 2025 14:55:59 -0800 Subject: [PATCH 21/60] update sample key --- test/services/NotificationServiceTests.test.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/services/NotificationServiceTests.test.ts b/test/services/NotificationServiceTests.test.ts index d34945e..4ef1953 100644 --- a/test/services/NotificationServiceTests.test.ts +++ b/test/services/NotificationServiceTests.test.ts @@ -6,10 +6,10 @@ import fs from "fs"; jest.mock("fs"); const sampleKey = `-----BEGIN PRIVATE KEY----- -9EKORECHU09 eouEUHKCREOFA12409kOHKOEU9125ADABOU098AOEBAOEI15bhao -XAOECR15bAAOECxrU91bAOEIAOERix145AOERDIIAOUX15997124xARODEARi119 -7k5AOEX151509KBRACOUDIAURCL1IAEO91245bAOECHDUAOE90kAOEB15139KAOA -19ax15qu +MIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQgsrmSAZIagOfCP8sB +Wi2CBXG1Oo7v1bispIZCwIr4RDegCgYIKoZIzj0DAQehRANCAATZHxV2wQJLMBq+ +ya+yfGi3g2ZUv6hrfe+j08ytekPHjXS0qzJoVELzKHa6EL9YAoZDXBtB6h+fGhXe +SOcONbaf -----END PRIVATE KEY-----` describe("NotificationService", () => { From 2c86d9189d6f3b09f7526437eacdce35c213874f Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Sun, 2 Feb 2025 14:57:06 -0800 Subject: [PATCH 22/60] update refreshed time on token generation --- src/services/NotificationService.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/services/NotificationService.ts b/src/services/NotificationService.ts index 1eca53c..33533ba 100644 --- a/src/services/NotificationService.ts +++ b/src/services/NotificationService.ts @@ -38,15 +38,17 @@ export class NotificationService { "kid": keyId, }; + const now = Date.now(); const claimsPayload = { "iss": teamId, - "iat": Date.now(), + "iat": now, }; this.token = jwt.sign(claimsPayload, privateKey, { algorithm: "ES256", header: tokenHeader }); + this._lastRefreshedTimeMs = now; } private lastReloadedTimeForAPNsIsTooRecent() { From f8ff7f9cc93db1521b73c7a4ef1c856d0764a4a5 Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Sun, 2 Feb 2025 15:13:13 -0800 Subject: [PATCH 23/60] add testing logic for scheduleNotification test --- test/services/NotificationServiceTests.test.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/test/services/NotificationServiceTests.test.ts b/test/services/NotificationServiceTests.test.ts index 4ef1953..a89dfa4 100644 --- a/test/services/NotificationServiceTests.test.ts +++ b/test/services/NotificationServiceTests.test.ts @@ -2,6 +2,7 @@ import { beforeEach, describe, expect, it, jest } from "@jest/globals"; import { NotificationService } from "../../src/services/NotificationService"; import { UnoptimizedInMemoryRepository } from "../../src/repositories/UnoptimizedInMemoryRepository"; import fs from "fs"; +import { IEta } from "../../src/entities/entities"; jest.mock("fs"); @@ -49,7 +50,23 @@ describe("NotificationService", () => { describe("scheduleNotification", () => { it("sends a notification to given shuttle/stop ID when changed", async () => { + // Arrange + const eta: IEta = { + shuttleId: "1", + stopId: "1", + secondsRemaining: 120, + }; + // Act + await notificationService.scheduleNotification({ + deviceId: "1", + shuttleId: eta.shuttleId, + stopId: eta.stopId, + }); + await repository.addOrUpdateEta(eta); + + // Assert + // ...that notification (fetch request) was sent }); it("clears the notification after delivering successfully", async () => { From 6172a044271dce1514dc851abdd30d0824eae742 Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Sun, 2 Feb 2025 21:09:07 -0800 Subject: [PATCH 24/60] add documentation and change interface for cancelAllNotifications --- src/services/NotificationService.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/services/NotificationService.ts b/src/services/NotificationService.ts index 33533ba..2d9651f 100644 --- a/src/services/NotificationService.ts +++ b/src/services/NotificationService.ts @@ -56,15 +56,27 @@ export class NotificationService { return this._lastRefreshedTimeMs && Date.now() - this._lastRefreshedTimeMs < thirtyMinutesMs; } + /** + * Queue a notification to be sent. + * @param deviceId The device ID to send the notification to. + * @param shuttleId Shuttle ID of ETA object to check. + * @param stopId Stop ID of ETA object to check. + */ public async scheduleNotification({ deviceId, shuttleId, stopId }: ScheduledNotificationData) { } + /** + * Cancel a pending notification. + * @param deviceId The device ID of the notification. + * @param shuttleId Shuttle ID of the ETA object. + * @param stopId Stop ID of the ETA object. + */ public async cancelNotification({ deviceId, shuttleId, stopId }: ScheduledNotificationData) { } - public cancelAllNotifications() { + public cancelAllNotifications(deviceId: string) { } } From fe233dfcbd8e71f60d0a4054ec7060a5f9bbdfad Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Sun, 2 Feb 2025 21:24:40 -0800 Subject: [PATCH 25/60] add pseudocode --- src/services/NotificationService.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/services/NotificationService.ts b/src/services/NotificationService.ts index 2d9651f..3ecaf58 100644 --- a/src/services/NotificationService.ts +++ b/src/services/NotificationService.ts @@ -56,6 +56,15 @@ export class NotificationService { return this._lastRefreshedTimeMs && Date.now() - this._lastRefreshedTimeMs < thirtyMinutesMs; } + private sendEtaNotificationImmediately({ deviceId, shuttleId, stopId }: ScheduledNotificationData) { + // Construct the fetch request + + // Send the fetch request + + // Check whether it was successful + // Return the result + } + /** * Queue a notification to be sent. * @param deviceId The device ID to send the notification to. @@ -63,7 +72,13 @@ export class NotificationService { * @param stopId Stop ID of ETA object to check. */ public async scheduleNotification({ deviceId, shuttleId, stopId }: ScheduledNotificationData) { + // Associate TupleKey(shuttleId, stopId) with array of device IDs + // Refresh the subscriber with the updated array if needed + // In the subscriber callback: + // If the ETA matches, call sendNotification with the necessary parameters + // If it was successful, unsubscribe the callback + // If it wasn't, leave it until the next ETA update } /** From 11fe22ceea2a11d11a2a9296ed3742959c63e560 Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Sun, 2 Feb 2025 21:31:24 -0800 Subject: [PATCH 26/60] add method to check if notification is scheduled (for testing --- src/services/NotificationService.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/services/NotificationService.ts b/src/services/NotificationService.ts index 3ecaf58..5b9105d 100644 --- a/src/services/NotificationService.ts +++ b/src/services/NotificationService.ts @@ -91,6 +91,15 @@ export class NotificationService { } + /** + * Check whether the notification is scheduled. + * @param deviceId + * @param shuttleId + * @param stopId + */ + public isNotificationScheduled({ deviceId, shuttleId, stopId }: ScheduledNotificationData) { + } + public cancelAllNotifications(deviceId: string) { } From 1e82ec26975bf5b7405035d324b920d89ab8e03b Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Sun, 2 Feb 2025 21:31:40 -0800 Subject: [PATCH 27/60] remove encryption key --- src/services/NotificationService.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/services/NotificationService.ts b/src/services/NotificationService.ts index 5b9105d..65a2f0a 100644 --- a/src/services/NotificationService.ts +++ b/src/services/NotificationService.ts @@ -19,9 +19,6 @@ export class NotificationService { constructor(private repository: GetterRepository) {} - private encryptionKey = crypto.randomBytes(32); - private iv = crypto.randomBytes(16); - public reloadAPNsTokenIfTimePassed() { if (this.lastReloadedTimeForAPNsIsTooRecent()) { return; From e9c2c8a7bc0920a2299bf4c16d3cb8e372ca1e35 Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Sun, 2 Feb 2025 21:33:23 -0800 Subject: [PATCH 28/60] add private object to store scheduled notifications --- src/services/NotificationService.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/services/NotificationService.ts b/src/services/NotificationService.ts index 65a2f0a..51e56af 100644 --- a/src/services/NotificationService.ts +++ b/src/services/NotificationService.ts @@ -19,6 +19,14 @@ export class NotificationService { constructor(private repository: GetterRepository) {} + /** + * An object of device ID arrays to deliver notifications to. + * The key should be a combination of the shuttle ID and + * stop ID, which can be generated using `TupleKey`. + * @private + */ + private deviceIdsToDeliverTo: { [key: string]: string[] } = {} + public reloadAPNsTokenIfTimePassed() { if (this.lastReloadedTimeForAPNsIsTooRecent()) { return; From 737a61fd416a96b5655b677c0de6abff5762af78 Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Mon, 3 Feb 2025 21:25:18 -0800 Subject: [PATCH 29/60] add signature and default return for isNotificationScheduled --- src/services/NotificationService.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/services/NotificationService.ts b/src/services/NotificationService.ts index 51e56af..2f6f6a5 100644 --- a/src/services/NotificationService.ts +++ b/src/services/NotificationService.ts @@ -102,7 +102,8 @@ export class NotificationService { * @param shuttleId * @param stopId */ - public isNotificationScheduled({ deviceId, shuttleId, stopId }: ScheduledNotificationData) { + public isNotificationScheduled({ deviceId, shuttleId, stopId }: ScheduledNotificationData): boolean { + return false; } public cancelAllNotifications(deviceId: string) { From 69525a643daefd445930b282b192431e0fb633df Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Mon, 3 Feb 2025 21:26:59 -0800 Subject: [PATCH 30/60] add another test to see if the data actually gets scheduled --- .../services/NotificationServiceTests.test.ts | 33 +++++++++++++++---- 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/test/services/NotificationServiceTests.test.ts b/test/services/NotificationServiceTests.test.ts index a89dfa4..53d7423 100644 --- a/test/services/NotificationServiceTests.test.ts +++ b/test/services/NotificationServiceTests.test.ts @@ -3,6 +3,7 @@ import { NotificationService } from "../../src/services/NotificationService"; import { UnoptimizedInMemoryRepository } from "../../src/repositories/UnoptimizedInMemoryRepository"; import fs from "fs"; import { IEta } from "../../src/entities/entities"; +import { updateGlobalFetchMockJson } from "../testHelpers/fetchMockHelpers"; jest.mock("fs"); @@ -49,7 +50,21 @@ describe("NotificationService", () => { }) describe("scheduleNotification", () => { - it("sends a notification to given shuttle/stop ID when changed", async () => { + it("schedules the notification", async () => { + // arrange + const notificationData = { + deviceId: "1", + shuttleId: "1", + stopId: "1" + }; + + await notificationService.scheduleNotification(notificationData); + + const isNotificationScheduled = notificationService.isNotificationScheduled(notificationData); + expect(isNotificationScheduled).toEqual(true); + }); + + it("sends and clears correct notification after ETA changed", async () => { // Arrange const eta: IEta = { shuttleId: "1", @@ -57,20 +72,26 @@ describe("NotificationService", () => { secondsRemaining: 120, }; + // Simulate 200 + empty object for successful push notification + updateGlobalFetchMockJson({}); + // Act - await notificationService.scheduleNotification({ + const notificationData = { deviceId: "1", shuttleId: eta.shuttleId, stopId: eta.stopId, - }); + } + await notificationService.scheduleNotification(notificationData); await repository.addOrUpdateEta(eta); // Assert - // ...that notification (fetch request) was sent + expect(fetch as jest.Mock).toHaveBeenCalledTimes(1); + const isNotificationScheduled = notificationService.isNotificationScheduled(notificationData); + // No longer scheduled after being sent + expect(isNotificationScheduled).toBe(false); }); - it("clears the notification after delivering successfully", async () => { - + it("leaves notification in array if delivery unsuccessful", async () => { }); }); From 3bdc730c95e5a4851187e017e0843a84e0e8d59e Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Mon, 3 Feb 2025 21:31:18 -0800 Subject: [PATCH 31/60] add the code to associate tuple key with array of device ids --- src/services/NotificationService.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/services/NotificationService.ts b/src/services/NotificationService.ts index 2f6f6a5..ec388c6 100644 --- a/src/services/NotificationService.ts +++ b/src/services/NotificationService.ts @@ -2,6 +2,7 @@ import { GetterRepository } from "../repositories/GetterRepository"; import * as crypto from "node:crypto"; import jwt from "jsonwebtoken"; import fs from "fs"; +import { TupleKey } from "../types/TupleKey"; interface ScheduledNotificationData { deviceId: string; @@ -78,6 +79,13 @@ export class NotificationService { */ public async scheduleNotification({ deviceId, shuttleId, stopId }: ScheduledNotificationData) { // Associate TupleKey(shuttleId, stopId) with array of device IDs + const tuple = new TupleKey(shuttleId, stopId); + if (this.deviceIdsToDeliverTo[tuple.toString()] === undefined) { + this.deviceIdsToDeliverTo[tuple.toString()] = [deviceId]; + } else { + this.deviceIdsToDeliverTo[tuple.toString()].push(deviceId); + } + // Refresh the subscriber with the updated array if needed // In the subscriber callback: @@ -103,7 +111,11 @@ export class NotificationService { * @param stopId */ public isNotificationScheduled({ deviceId, shuttleId, stopId }: ScheduledNotificationData): boolean { - return false; + const tuple = new TupleKey(shuttleId, stopId); + if (this.deviceIdsToDeliverTo[tuple.toString()] === undefined) { + return false; + } + return this.deviceIdsToDeliverTo[tuple.toString()].includes(deviceId); } public cancelAllNotifications(deviceId: string) { From e96316aa1e83824197eb2e28e4d2bcf8beec5908 Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Mon, 3 Feb 2025 21:37:20 -0800 Subject: [PATCH 32/60] add eta subscriber callback code --- src/services/NotificationService.ts | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/src/services/NotificationService.ts b/src/services/NotificationService.ts index ec388c6..3613803 100644 --- a/src/services/NotificationService.ts +++ b/src/services/NotificationService.ts @@ -3,6 +3,7 @@ import * as crypto from "node:crypto"; import jwt from "jsonwebtoken"; import fs from "fs"; import { TupleKey } from "../types/TupleKey"; +import { IEta } from "../entities/entities"; interface ScheduledNotificationData { deviceId: string; @@ -62,13 +63,29 @@ export class NotificationService { return this._lastRefreshedTimeMs && Date.now() - this._lastRefreshedTimeMs < thirtyMinutesMs; } - private sendEtaNotificationImmediately({ deviceId, shuttleId, stopId }: ScheduledNotificationData) { + private async sendEtaNotificationImmediately({ deviceId, shuttleId, stopId }: ScheduledNotificationData): Promise { // Construct the fetch request // Send the fetch request // Check whether it was successful // Return the result + return false; + } + + private async etaSubscriberCallback(eta: IEta) { + const tuple = new TupleKey(eta.shuttleId, eta.stopId); + if (this.deviceIdsToDeliverTo[tuple.toString()] === undefined) { + return; + } + + await Promise.all(this.deviceIdsToDeliverTo[tuple.toString()].map(async (deviceId) => { + await this.sendEtaNotificationImmediately({ + deviceId, + shuttleId: eta.shuttleId, + stopId: eta.stopId, + }); + })); } /** @@ -78,7 +95,6 @@ export class NotificationService { * @param stopId Stop ID of ETA object to check. */ public async scheduleNotification({ deviceId, shuttleId, stopId }: ScheduledNotificationData) { - // Associate TupleKey(shuttleId, stopId) with array of device IDs const tuple = new TupleKey(shuttleId, stopId); if (this.deviceIdsToDeliverTo[tuple.toString()] === undefined) { this.deviceIdsToDeliverTo[tuple.toString()] = [deviceId]; @@ -87,6 +103,8 @@ export class NotificationService { } // Refresh the subscriber with the updated array if needed + this.repository.unsubscribeFromEtaUpdates(this.etaSubscriberCallback); + this.repository.subscribeToEtaUpdates(this.etaSubscriberCallback); // In the subscriber callback: // If the ETA matches, call sendNotification with the necessary parameters From 8bc71d25fe75f488b2606890eb3f42f22da2b601 Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Mon, 3 Feb 2025 21:50:59 -0800 Subject: [PATCH 33/60] add code to update device ids object on successful delivery --- src/services/NotificationService.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/services/NotificationService.ts b/src/services/NotificationService.ts index 3613803..76651bd 100644 --- a/src/services/NotificationService.ts +++ b/src/services/NotificationService.ts @@ -19,7 +19,9 @@ export class NotificationService { return this._lastRefreshedTimeMs; } - constructor(private repository: GetterRepository) {} + constructor(private repository: GetterRepository) { + this.etaSubscriberCallback = this.etaSubscriberCallback.bind(this); + } /** * An object of device ID arrays to deliver notifications to. @@ -79,13 +81,19 @@ export class NotificationService { return; } - await Promise.all(this.deviceIdsToDeliverTo[tuple.toString()].map(async (deviceId) => { - await this.sendEtaNotificationImmediately({ + const indicesToRemove = new Set(); + await Promise.all(this.deviceIdsToDeliverTo[tuple.toString()].map(async (deviceId, index) => { + const deliveredSuccessfully = await this.sendEtaNotificationImmediately({ deviceId, shuttleId: eta.shuttleId, stopId: eta.stopId, }); + if (deliveredSuccessfully) { + indicesToRemove.add(index); + } })); + + this.deviceIdsToDeliverTo[tuple.toString()] = this.deviceIdsToDeliverTo[tuple.toString()].filter((_, index) => !indicesToRemove.has(index)); } /** @@ -102,7 +110,6 @@ export class NotificationService { this.deviceIdsToDeliverTo[tuple.toString()].push(deviceId); } - // Refresh the subscriber with the updated array if needed this.repository.unsubscribeFromEtaUpdates(this.etaSubscriberCallback); this.repository.subscribeToEtaUpdates(this.etaSubscriberCallback); From 90e5fa200e9a8844a24223b249632150ea9e1a01 Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Mon, 3 Feb 2025 21:51:52 -0800 Subject: [PATCH 34/60] add todo --- src/services/NotificationService.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/services/NotificationService.ts b/src/services/NotificationService.ts index 76651bd..a223e89 100644 --- a/src/services/NotificationService.ts +++ b/src/services/NotificationService.ts @@ -77,6 +77,7 @@ export class NotificationService { private async etaSubscriberCallback(eta: IEta) { const tuple = new TupleKey(eta.shuttleId, eta.stopId); + // TODO: move device IDs object (with TupleKey based string) to its own class if (this.deviceIdsToDeliverTo[tuple.toString()] === undefined) { return; } From 711caa1d15f35f8134a88c53ac2b5dca6914307f Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Mon, 3 Feb 2025 21:52:01 -0800 Subject: [PATCH 35/60] add a second notification to schedule for the test --- test/services/NotificationServiceTests.test.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/test/services/NotificationServiceTests.test.ts b/test/services/NotificationServiceTests.test.ts index 53d7423..3cd2978 100644 --- a/test/services/NotificationServiceTests.test.ts +++ b/test/services/NotificationServiceTests.test.ts @@ -76,19 +76,26 @@ describe("NotificationService", () => { updateGlobalFetchMockJson({}); // Act - const notificationData = { + const notificationData1 = { deviceId: "1", shuttleId: eta.shuttleId, stopId: eta.stopId, } - await notificationService.scheduleNotification(notificationData); + const notificationData2 = { + ...notificationData1, + deviceId: "2", + } + await notificationService.scheduleNotification(notificationData1); + await notificationService.scheduleNotification(notificationData2); await repository.addOrUpdateEta(eta); // Assert expect(fetch as jest.Mock).toHaveBeenCalledTimes(1); - const isNotificationScheduled = notificationService.isNotificationScheduled(notificationData); + const isFirstNotificationScheduled = notificationService.isNotificationScheduled(notificationData1); + const isSecondNotificationScheduled = notificationService.isNotificationScheduled(notificationData2); // No longer scheduled after being sent - expect(isNotificationScheduled).toBe(false); + expect(isFirstNotificationScheduled).toBe(false); + expect(isSecondNotificationScheduled).toBe(false); }); it("leaves notification in array if delivery unsuccessful", async () => { From 339148c890e950b4daade642bee189cd54f92225 Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Mon, 3 Feb 2025 21:55:26 -0800 Subject: [PATCH 36/60] implement test which checks if notification stays scheduled --- .../services/NotificationServiceTests.test.ts | 32 ++++++++++++++++--- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/test/services/NotificationServiceTests.test.ts b/test/services/NotificationServiceTests.test.ts index 3cd2978..b0aa767 100644 --- a/test/services/NotificationServiceTests.test.ts +++ b/test/services/NotificationServiceTests.test.ts @@ -71,11 +71,6 @@ describe("NotificationService", () => { stopId: "1", secondsRemaining: 120, }; - - // Simulate 200 + empty object for successful push notification - updateGlobalFetchMockJson({}); - - // Act const notificationData1 = { deviceId: "1", shuttleId: eta.shuttleId, @@ -85,6 +80,11 @@ describe("NotificationService", () => { ...notificationData1, deviceId: "2", } + + // Simulate 200 + empty object for successful push notification + updateGlobalFetchMockJson({}); + + // Act await notificationService.scheduleNotification(notificationData1); await notificationService.scheduleNotification(notificationData2); await repository.addOrUpdateEta(eta); @@ -99,6 +99,28 @@ describe("NotificationService", () => { }); it("leaves notification in array if delivery unsuccessful", async () => { + // Arrange + const eta: IEta = { + shuttleId: "1", + stopId: "1", + secondsRemaining: 120, + }; + const notificationData = { + deviceId: "1", + shuttleId: eta.shuttleId, + stopId: eta.stopId, + } + + updateGlobalFetchMockJson({}, 400); + + // Act + await notificationService.scheduleNotification(notificationData); + await repository.addOrUpdateEta(eta); + + // Assert + // The notification should stay scheduled to be retried once + // the ETA updates again + expect(notificationService.isNotificationScheduled(notificationData)).toBe(true); }); }); From 18402401e35666f5451c0544bbbd2a2d410d2799 Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Mon, 3 Feb 2025 21:57:42 -0800 Subject: [PATCH 37/60] rename token variable for clarity --- src/services/NotificationService.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/services/NotificationService.ts b/src/services/NotificationService.ts index a223e89..ffa0937 100644 --- a/src/services/NotificationService.ts +++ b/src/services/NotificationService.ts @@ -12,7 +12,7 @@ interface ScheduledNotificationData { } export class NotificationService { - private token: string | undefined = undefined; + private apnsToken: string | undefined = undefined; private _lastRefreshedTimeMs: number | undefined = undefined; get lastRefreshedTimeMs() { @@ -53,7 +53,7 @@ export class NotificationService { "iat": now, }; - this.token = jwt.sign(claimsPayload, privateKey, { + this.apnsToken = jwt.sign(claimsPayload, privateKey, { algorithm: "ES256", header: tokenHeader }); @@ -66,6 +66,8 @@ export class NotificationService { } private async sendEtaNotificationImmediately({ deviceId, shuttleId, stopId }: ScheduledNotificationData): Promise { + this.reloadAPNsTokenIfTimePassed(); + // Construct the fetch request // Send the fetch request From f602aa6c2ca9f0fe314fd308f836b10534e7948f Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Mon, 3 Feb 2025 22:03:28 -0800 Subject: [PATCH 38/60] add method to determine which url to use --- src/services/NotificationService.ts | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/services/NotificationService.ts b/src/services/NotificationService.ts index ffa0937..40e0dab 100644 --- a/src/services/NotificationService.ts +++ b/src/services/NotificationService.ts @@ -67,16 +67,29 @@ export class NotificationService { private async sendEtaNotificationImmediately({ deviceId, shuttleId, stopId }: ScheduledNotificationData): Promise { this.reloadAPNsTokenIfTimePassed(); - - // Construct the fetch request + const url = this.getAPNsFullUrlToUse(deviceId); // Send the fetch request + // Check whether it was successful // Return the result return false; } + private getAPNsFullUrlToUse(deviceId: string) { + // Construct the fetch request + const devBaseUrl = "https://api.sandbox.push.apple.com" + const prodBaseUrl = "https://api.push.apple.com" + const path = "/3/device/" + deviceId; + + let urlToUse = prodBaseUrl; + if (process.env.NODE_ENV !== "production") { + urlToUse = devBaseUrl + path; + } + return urlToUse; + } + private async etaSubscriberCallback(eta: IEta) { const tuple = new TupleKey(eta.shuttleId, eta.stopId); // TODO: move device IDs object (with TupleKey based string) to its own class From c5c0208371dac6994246e50d92c459c48da32297 Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Mon, 3 Feb 2025 22:16:53 -0800 Subject: [PATCH 39/60] add request code --- src/services/NotificationService.ts | 41 ++++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/src/services/NotificationService.ts b/src/services/NotificationService.ts index 40e0dab..7e6ec1e 100644 --- a/src/services/NotificationService.ts +++ b/src/services/NotificationService.ts @@ -69,8 +69,47 @@ export class NotificationService { this.reloadAPNsTokenIfTimePassed(); const url = this.getAPNsFullUrlToUse(deviceId); - // Send the fetch request + const shuttle = await this.repository.getShuttleById(shuttleId); + const stop = await this.repository.getStopById(stopId); + const eta = await this.repository.getEtaForShuttleAndStopId(shuttleId, stopId); + // TODO: add more specific errors + if (!shuttle) { + throw new Error("The shuttle given by the provided shuttleID doesn't exist."); + } + if (!stop) { + throw new Error("The shuttle given by the provided stopId doesn't exist."); + } + // TODO: account for cases where ETA may not exist due to "data race" with ApiBasedRepositoryLoader + if (!eta) { + throw new Error("There is no ETA for this shuttle/stop."); + } + // Send the fetch request + const bundleId = process.env.APNS_BUNDLE_ID; + if (typeof bundleId !== "string") { + throw new Error("APNS_BUNDLE_ID environment variable is not set correctly"); + } + + const headers = { + authorization: `bearer ${this.apnsToken}`, + "apns-push-type": "alert", + "apns-expiration": "0", + "apns-priority": "10", + "apns-topic": bundleId, + }; + const response = await fetch(url, { + method: "POST", + headers, + body: JSON.stringify({ + aps: { + alert: { + title: "Shuttle is arriving", + body: `Shuttle is approaching ${stop.name} in ${Math.ceil(eta.secondsRemaining / 60)} minutes.` + } + } + }), + }); + const json = await response.json(); // Check whether it was successful // Return the result From 103ddef523473e07457e9c44fca6890c7a9b07ad Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Mon, 3 Feb 2025 22:42:25 -0800 Subject: [PATCH 40/60] add handling for error and successful return --- src/services/NotificationService.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/services/NotificationService.ts b/src/services/NotificationService.ts index 7e6ec1e..defd0f0 100644 --- a/src/services/NotificationService.ts +++ b/src/services/NotificationService.ts @@ -111,9 +111,11 @@ export class NotificationService { }); const json = await response.json(); - // Check whether it was successful - // Return the result - return false; + if (response.status !== 200) { + console.error(`Notification failed for device ${deviceId}:`, json.reason); + return false; + } + return true; } private getAPNsFullUrlToUse(deviceId: string) { From d357af750917c96e946f1bdd7caa4ffbe4d5638f Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Mon, 3 Feb 2025 22:42:50 -0800 Subject: [PATCH 41/60] add shuttle and stop to repository within test --- test/services/NotificationServiceTests.test.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/test/services/NotificationServiceTests.test.ts b/test/services/NotificationServiceTests.test.ts index b0aa767..271a003 100644 --- a/test/services/NotificationServiceTests.test.ts +++ b/test/services/NotificationServiceTests.test.ts @@ -2,8 +2,9 @@ import { beforeEach, describe, expect, it, jest } from "@jest/globals"; import { NotificationService } from "../../src/services/NotificationService"; import { UnoptimizedInMemoryRepository } from "../../src/repositories/UnoptimizedInMemoryRepository"; import fs from "fs"; -import { IEta } from "../../src/entities/entities"; +import { IEta, IShuttle } from "../../src/entities/entities"; import { updateGlobalFetchMockJson } from "../testHelpers/fetchMockHelpers"; +import { addMockShuttleToRepository, addMockStopToRepository } from "../testHelpers/repositorySetupHelpers"; jest.mock("fs"); @@ -66,11 +67,15 @@ describe("NotificationService", () => { it("sends and clears correct notification after ETA changed", async () => { // Arrange + const shuttle = await addMockShuttleToRepository(repository, "1"); + const stop = await addMockStopToRepository(repository, "1"); + const eta: IEta = { - shuttleId: "1", - stopId: "1", + shuttleId: shuttle.id, + stopId: stop.id, secondsRemaining: 120, }; + const notificationData1 = { deviceId: "1", shuttleId: eta.shuttleId, @@ -90,7 +95,7 @@ describe("NotificationService", () => { await repository.addOrUpdateEta(eta); // Assert - expect(fetch as jest.Mock).toHaveBeenCalledTimes(1); + expect(fetch as jest.Mock).toHaveBeenCalledTimes(2); const isFirstNotificationScheduled = notificationService.isNotificationScheduled(notificationData1); const isSecondNotificationScheduled = notificationService.isNotificationScheduled(notificationData2); // No longer scheduled after being sent From 16d2d4b4ef9c35ccf83aebdb5241066c2fd98854 Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Mon, 3 Feb 2025 22:47:17 -0800 Subject: [PATCH 42/60] add seconds threshold for notification --- src/services/NotificationService.ts | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/src/services/NotificationService.ts b/src/services/NotificationService.ts index defd0f0..2ed9ae9 100644 --- a/src/services/NotificationService.ts +++ b/src/services/NotificationService.ts @@ -1,5 +1,4 @@ import { GetterRepository } from "../repositories/GetterRepository"; -import * as crypto from "node:crypto"; import jwt from "jsonwebtoken"; import fs from "fs"; import { TupleKey } from "../types/TupleKey"; @@ -140,11 +139,7 @@ export class NotificationService { const indicesToRemove = new Set(); await Promise.all(this.deviceIdsToDeliverTo[tuple.toString()].map(async (deviceId, index) => { - const deliveredSuccessfully = await this.sendEtaNotificationImmediately({ - deviceId, - shuttleId: eta.shuttleId, - stopId: eta.stopId, - }); + const deliveredSuccessfully = await this.sendEtaNotificationImmediatelyIfSecondsRemainingBelowThreshold(deviceId, eta); if (deliveredSuccessfully) { indicesToRemove.add(index); } @@ -153,6 +148,19 @@ export class NotificationService { this.deviceIdsToDeliverTo[tuple.toString()] = this.deviceIdsToDeliverTo[tuple.toString()].filter((_, index) => !indicesToRemove.has(index)); } + private async sendEtaNotificationImmediatelyIfSecondsRemainingBelowThreshold(deviceId: string, eta: IEta) { + const secondsThresholdForNotificationToFire = 300; + if (eta.secondsRemaining > secondsThresholdForNotificationToFire) { + return false; + } + + return await this.sendEtaNotificationImmediately({ + deviceId, + shuttleId: eta.shuttleId, + stopId: eta.stopId, + }); + } + /** * Queue a notification to be sent. * @param deviceId The device ID to send the notification to. From 4c8658a3d9a4443be690656ef1c8aa53dfe8280b Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Mon, 3 Feb 2025 22:52:35 -0800 Subject: [PATCH 43/60] bind all other methods to class --- src/services/NotificationService.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/services/NotificationService.ts b/src/services/NotificationService.ts index 2ed9ae9..ea9fbc8 100644 --- a/src/services/NotificationService.ts +++ b/src/services/NotificationService.ts @@ -20,6 +20,13 @@ export class NotificationService { constructor(private repository: GetterRepository) { this.etaSubscriberCallback = this.etaSubscriberCallback.bind(this); + this.reloadAPNsTokenIfTimePassed = this.reloadAPNsTokenIfTimePassed.bind(this); + this.lastReloadedTimeForAPNsIsTooRecent = this.lastReloadedTimeForAPNsIsTooRecent.bind(this); + this.sendEtaNotificationImmediately = this.sendEtaNotificationImmediately.bind(this); + this.getAPNsFullUrlToUse = this.getAPNsFullUrlToUse.bind(this); + this.etaSubscriberCallback = this.etaSubscriberCallback.bind(this); + this.sendEtaNotificationImmediatelyIfSecondsRemainingBelowThreshold = this.sendEtaNotificationImmediatelyIfSecondsRemainingBelowThreshold.bind(this); + this.scheduleNotification = this.scheduleNotification.bind(this); } /** From 64931e75465ea08a8f0c8788a2cddd878c569b3b Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Mon, 3 Feb 2025 23:00:18 -0800 Subject: [PATCH 44/60] remove redundant comments --- src/services/NotificationService.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/services/NotificationService.ts b/src/services/NotificationService.ts index ea9fbc8..0f28c92 100644 --- a/src/services/NotificationService.ts +++ b/src/services/NotificationService.ts @@ -184,11 +184,6 @@ export class NotificationService { this.repository.unsubscribeFromEtaUpdates(this.etaSubscriberCallback); this.repository.subscribeToEtaUpdates(this.etaSubscriberCallback); - - // In the subscriber callback: - // If the ETA matches, call sendNotification with the necessary parameters - // If it was successful, unsubscribe the callback - // If it wasn't, leave it until the next ETA update } /** From b252adb5d4a079adaff6d2c05b074f397b6525b9 Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Mon, 3 Feb 2025 23:06:28 -0800 Subject: [PATCH 45/60] use waitForCondition function to poll for data update --- test/services/NotificationServiceTests.test.ts | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/test/services/NotificationServiceTests.test.ts b/test/services/NotificationServiceTests.test.ts index 271a003..ba6b969 100644 --- a/test/services/NotificationServiceTests.test.ts +++ b/test/services/NotificationServiceTests.test.ts @@ -28,7 +28,8 @@ describe("NotificationService", () => { ...process.env, APNS_KEY_ID: "1", APNS_TEAM_ID: "1", - APNS_KEY_PATH: "./dummy-path.p8" + APNS_KEY_PATH: "./dummy-path.p8", + APNS_BUNDLE_ID: "dev.bchen.ProjectInter" }; (fs.readFileSync as jest.Mock).mockReturnValue(sampleKey); @@ -95,6 +96,20 @@ describe("NotificationService", () => { await repository.addOrUpdateEta(eta); // Assert + // Because repository publisher calls subscriber without await + // wait for the change to occur first + async function waitForCondition(condition: () => boolean, timeout = 5000, interval = 500) { + const startTime = Date.now(); + while (!condition()) { + if (Date.now() - startTime > timeout) { + throw new Error("Timeout waiting for condition"); + } + await new Promise((resolve) => setTimeout(resolve, interval)); + } + } + + await waitForCondition(() => !notificationService.isNotificationScheduled(notificationData1)); + expect(fetch as jest.Mock).toHaveBeenCalledTimes(2); const isFirstNotificationScheduled = notificationService.isNotificationScheduled(notificationData1); const isSecondNotificationScheduled = notificationService.isNotificationScheduled(notificationData2); From 179fa07d36f1858f6bee3032190d01621015c02e Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Mon, 3 Feb 2025 23:07:55 -0800 Subject: [PATCH 46/60] make polling function a global function --- .../services/NotificationServiceTests.test.ts | 26 ++++++++++++------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/test/services/NotificationServiceTests.test.ts b/test/services/NotificationServiceTests.test.ts index ba6b969..a42512f 100644 --- a/test/services/NotificationServiceTests.test.ts +++ b/test/services/NotificationServiceTests.test.ts @@ -15,6 +15,23 @@ ya+yfGi3g2ZUv6hrfe+j08ytekPHjXS0qzJoVELzKHa6EL9YAoZDXBtB6h+fGhXe SOcONbaf -----END PRIVATE KEY-----` +/** + * Wait for a condition to become true until the timeout + * is hit. + * @param condition + * @param timeout + * @param interval + */ +async function waitForCondition(condition: () => boolean, timeout = 5000, interval = 500) { + const startTime = Date.now(); + while (!condition()) { + if (Date.now() - startTime > timeout) { + throw new Error("Timeout waiting for condition"); + } + await new Promise((resolve) => setTimeout(resolve, interval)); + } +} + describe("NotificationService", () => { let repository: UnoptimizedInMemoryRepository let notificationService: NotificationService; @@ -98,15 +115,6 @@ describe("NotificationService", () => { // Assert // Because repository publisher calls subscriber without await // wait for the change to occur first - async function waitForCondition(condition: () => boolean, timeout = 5000, interval = 500) { - const startTime = Date.now(); - while (!condition()) { - if (Date.now() - startTime > timeout) { - throw new Error("Timeout waiting for condition"); - } - await new Promise((resolve) => setTimeout(resolve, interval)); - } - } await waitForCondition(() => !notificationService.isNotificationScheduled(notificationData1)); From 0cd7c2fe47768fb7ff069266185d4c0ed05efd52 Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Mon, 3 Feb 2025 23:13:13 -0800 Subject: [PATCH 47/60] add export for scheduled notification data --- src/services/NotificationService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/NotificationService.ts b/src/services/NotificationService.ts index 0f28c92..ee924a4 100644 --- a/src/services/NotificationService.ts +++ b/src/services/NotificationService.ts @@ -4,7 +4,7 @@ import fs from "fs"; import { TupleKey } from "../types/TupleKey"; import { IEta } from "../entities/entities"; -interface ScheduledNotificationData { +export interface ScheduledNotificationData { deviceId: string; shuttleId: string; stopId: string; From 11ea0518fe72502b625128effd38f44d6279cc84 Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Mon, 3 Feb 2025 23:13:27 -0800 Subject: [PATCH 48/60] extract notification/eta data into function --- .../services/NotificationServiceTests.test.ts | 55 +++++++++---------- 1 file changed, 26 insertions(+), 29 deletions(-) diff --git a/test/services/NotificationServiceTests.test.ts b/test/services/NotificationServiceTests.test.ts index a42512f..fb26785 100644 --- a/test/services/NotificationServiceTests.test.ts +++ b/test/services/NotificationServiceTests.test.ts @@ -2,7 +2,7 @@ import { beforeEach, describe, expect, it, jest } from "@jest/globals"; import { NotificationService } from "../../src/services/NotificationService"; import { UnoptimizedInMemoryRepository } from "../../src/repositories/UnoptimizedInMemoryRepository"; import fs from "fs"; -import { IEta, IShuttle } from "../../src/entities/entities"; +import { IEta, IShuttle, IStop } from "../../src/entities/entities"; import { updateGlobalFetchMockJson } from "../testHelpers/fetchMockHelpers"; import { addMockShuttleToRepository, addMockStopToRepository } from "../testHelpers/repositorySetupHelpers"; @@ -68,6 +68,25 @@ describe("NotificationService", () => { }); }) + function generateNotificationDataAndEta(shuttle: IShuttle, stop: IStop) { + const eta: IEta = { + shuttleId: shuttle.id, + stopId: stop.id, + secondsRemaining: 120, + }; + + const notificationData1 = { + deviceId: "1", + shuttleId: eta.shuttleId, + stopId: eta.stopId, + } + const notificationData2 = { + ...notificationData1, + deviceId: "2", + } + return { eta, notificationData1, notificationData2 }; + } + describe("scheduleNotification", () => { it("schedules the notification", async () => { // arrange @@ -88,21 +107,7 @@ describe("NotificationService", () => { const shuttle = await addMockShuttleToRepository(repository, "1"); const stop = await addMockStopToRepository(repository, "1"); - const eta: IEta = { - shuttleId: shuttle.id, - stopId: stop.id, - secondsRemaining: 120, - }; - - const notificationData1 = { - deviceId: "1", - shuttleId: eta.shuttleId, - stopId: eta.stopId, - } - const notificationData2 = { - ...notificationData1, - deviceId: "2", - } + const { eta, notificationData1, notificationData2 } = generateNotificationDataAndEta(shuttle, stop); // Simulate 200 + empty object for successful push notification updateGlobalFetchMockJson({}); @@ -115,7 +120,6 @@ describe("NotificationService", () => { // Assert // Because repository publisher calls subscriber without await // wait for the change to occur first - await waitForCondition(() => !notificationService.isNotificationScheduled(notificationData1)); expect(fetch as jest.Mock).toHaveBeenCalledTimes(2); @@ -128,27 +132,20 @@ describe("NotificationService", () => { it("leaves notification in array if delivery unsuccessful", async () => { // Arrange - const eta: IEta = { - shuttleId: "1", - stopId: "1", - secondsRemaining: 120, - }; - const notificationData = { - deviceId: "1", - shuttleId: eta.shuttleId, - stopId: eta.stopId, - } + const shuttle = await addMockShuttleToRepository(repository, "1"); + const stop = await addMockStopToRepository(repository, "1"); + const { eta, notificationData1, notificationData2 } = generateNotificationDataAndEta(shuttle, stop) updateGlobalFetchMockJson({}, 400); // Act - await notificationService.scheduleNotification(notificationData); + await notificationService.scheduleNotification(notificationData1); await repository.addOrUpdateEta(eta); // Assert // The notification should stay scheduled to be retried once // the ETA updates again - expect(notificationService.isNotificationScheduled(notificationData)).toBe(true); + expect(notificationService.isNotificationScheduled(notificationData1)).toBe(true); }); }); From b3b4b71e22c5db8f12df6fc024ba0e6023c4ce56 Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Mon, 3 Feb 2025 23:15:05 -0800 Subject: [PATCH 49/60] extract seconds number to public readonly class property --- src/services/NotificationService.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/services/NotificationService.ts b/src/services/NotificationService.ts index ee924a4..60983ca 100644 --- a/src/services/NotificationService.ts +++ b/src/services/NotificationService.ts @@ -11,6 +11,8 @@ export interface ScheduledNotificationData { } export class NotificationService { + public readonly secondsThresholdForNotificationToFire = 300; + private apnsToken: string | undefined = undefined; private _lastRefreshedTimeMs: number | undefined = undefined; @@ -156,8 +158,7 @@ export class NotificationService { } private async sendEtaNotificationImmediatelyIfSecondsRemainingBelowThreshold(deviceId: string, eta: IEta) { - const secondsThresholdForNotificationToFire = 300; - if (eta.secondsRemaining > secondsThresholdForNotificationToFire) { + if (eta.secondsRemaining > this.secondsThresholdForNotificationToFire) { return false; } From 602ccf81398058ffaca5b0326396132bd2d41384 Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Mon, 3 Feb 2025 23:20:42 -0800 Subject: [PATCH 50/60] update tests to wait for the publisher event to be sent --- .../services/NotificationServiceTests.test.ts | 43 ++++++++++++++++--- 1 file changed, 36 insertions(+), 7 deletions(-) diff --git a/test/services/NotificationServiceTests.test.ts b/test/services/NotificationServiceTests.test.ts index fb26785..980b0f7 100644 --- a/test/services/NotificationServiceTests.test.ts +++ b/test/services/NotificationServiceTests.test.ts @@ -19,19 +19,27 @@ SOcONbaf * Wait for a condition to become true until the timeout * is hit. * @param condition - * @param timeout - * @param interval + * @param timeoutMilliseconds + * @param intervalMilliseconds */ -async function waitForCondition(condition: () => boolean, timeout = 5000, interval = 500) { +async function waitForCondition(condition: () => boolean, timeoutMilliseconds = 5000, intervalMilliseconds = 500) { const startTime = Date.now(); while (!condition()) { - if (Date.now() - startTime > timeout) { + if (Date.now() - startTime > timeoutMilliseconds) { throw new Error("Timeout waiting for condition"); } - await new Promise((resolve) => setTimeout(resolve, interval)); + await new Promise((resolve) => setTimeout(resolve, intervalMilliseconds)); } } +/** + * Wait for a specified number of milliseconds. + * @param ms + */ +async function waitForMilliseconds(ms: number): Promise { + await new Promise((resolve) => setTimeout(resolve, ms)); +} + describe("NotificationService", () => { let repository: UnoptimizedInMemoryRepository let notificationService: NotificationService; @@ -130,11 +138,30 @@ describe("NotificationService", () => { expect(isSecondNotificationScheduled).toBe(false); }); + it("doesn't send notification if seconds threshold not exceeded", async () => { + // Arrange + const shuttle = await addMockShuttleToRepository(repository, "1"); + const stop = await addMockStopToRepository(repository, "1"); + const { eta, notificationData1 } = generateNotificationDataAndEta(shuttle, stop); + eta.secondsRemaining = notificationService.secondsThresholdForNotificationToFire + 100; + + updateGlobalFetchMockJson({}); + + // Act + await notificationService.scheduleNotification(notificationData1); + await repository.addOrUpdateEta(eta); + + // Assert + await waitForMilliseconds(500); + const isNotificationScheduled = notificationService.isNotificationScheduled(notificationData1); + expect(isNotificationScheduled).toBe(true); + }); + it("leaves notification in array if delivery unsuccessful", async () => { // Arrange const shuttle = await addMockShuttleToRepository(repository, "1"); const stop = await addMockStopToRepository(repository, "1"); - const { eta, notificationData1, notificationData2 } = generateNotificationDataAndEta(shuttle, stop) + const { eta, notificationData1 } = generateNotificationDataAndEta(shuttle, stop) updateGlobalFetchMockJson({}, 400); @@ -145,7 +172,9 @@ describe("NotificationService", () => { // Assert // The notification should stay scheduled to be retried once // the ETA updates again - expect(notificationService.isNotificationScheduled(notificationData1)).toBe(true); + await waitForMilliseconds(500); + const isNotificationScheduled = notificationService.isNotificationScheduled(notificationData1); + expect(isNotificationScheduled).toBe(true); }); }); From c593fe14e12affd2830dd89d1cb549b066c13f22 Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Mon, 3 Feb 2025 23:23:21 -0800 Subject: [PATCH 51/60] make getAPNsFullUrlToUse public and testable --- src/services/NotificationService.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/services/NotificationService.ts b/src/services/NotificationService.ts index 60983ca..9d637f0 100644 --- a/src/services/NotificationService.ts +++ b/src/services/NotificationService.ts @@ -126,13 +126,13 @@ export class NotificationService { return true; } - private getAPNsFullUrlToUse(deviceId: string) { + public static getAPNsFullUrlToUse(deviceId: string) { // Construct the fetch request const devBaseUrl = "https://api.sandbox.push.apple.com" const prodBaseUrl = "https://api.push.apple.com" const path = "/3/device/" + deviceId; - let urlToUse = prodBaseUrl; + let urlToUse = prodBaseUrl + path; if (process.env.NODE_ENV !== "production") { urlToUse = devBaseUrl + path; } From aff82a818596e35c0278fa67dd70243c48300102 Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Mon, 3 Feb 2025 23:25:53 -0800 Subject: [PATCH 52/60] fix binding errors with static method --- src/services/NotificationService.test.ts | 2 ++ src/services/NotificationService.ts | 3 +-- 2 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 src/services/NotificationService.test.ts diff --git a/src/services/NotificationService.test.ts b/src/services/NotificationService.test.ts new file mode 100644 index 0000000..fb3ff70 --- /dev/null +++ b/src/services/NotificationService.test.ts @@ -0,0 +1,2 @@ +import { NotificationService } from './NotificationService'; + diff --git a/src/services/NotificationService.ts b/src/services/NotificationService.ts index 9d637f0..1a66870 100644 --- a/src/services/NotificationService.ts +++ b/src/services/NotificationService.ts @@ -25,7 +25,6 @@ export class NotificationService { this.reloadAPNsTokenIfTimePassed = this.reloadAPNsTokenIfTimePassed.bind(this); this.lastReloadedTimeForAPNsIsTooRecent = this.lastReloadedTimeForAPNsIsTooRecent.bind(this); this.sendEtaNotificationImmediately = this.sendEtaNotificationImmediately.bind(this); - this.getAPNsFullUrlToUse = this.getAPNsFullUrlToUse.bind(this); this.etaSubscriberCallback = this.etaSubscriberCallback.bind(this); this.sendEtaNotificationImmediatelyIfSecondsRemainingBelowThreshold = this.sendEtaNotificationImmediatelyIfSecondsRemainingBelowThreshold.bind(this); this.scheduleNotification = this.scheduleNotification.bind(this); @@ -75,7 +74,7 @@ export class NotificationService { private async sendEtaNotificationImmediately({ deviceId, shuttleId, stopId }: ScheduledNotificationData): Promise { this.reloadAPNsTokenIfTimePassed(); - const url = this.getAPNsFullUrlToUse(deviceId); + const url = NotificationService.getAPNsFullUrlToUse(deviceId); const shuttle = await this.repository.getShuttleById(shuttleId); const stop = await this.repository.getStopById(stopId); From 6473661607f966ee78e87065d3920759c248c4fa Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Mon, 3 Feb 2025 23:26:16 -0800 Subject: [PATCH 53/60] add tests for getAPNsFullUrlToUse --- .../services/NotificationServiceTests.test.ts | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/test/services/NotificationServiceTests.test.ts b/test/services/NotificationServiceTests.test.ts index 980b0f7..f2a5dbc 100644 --- a/test/services/NotificationServiceTests.test.ts +++ b/test/services/NotificationServiceTests.test.ts @@ -178,6 +178,29 @@ describe("NotificationService", () => { }); }); + describe('getAPNsFullUrlToUse', () => { + it('should return the production URL when NODE_ENV is set to "production"', () => { + process.env.NODE_ENV = 'production'; + const deviceId = 'testDeviceId'; + const result = NotificationService.getAPNsFullUrlToUse(deviceId); + expect(result).toBe(`https://api.push.apple.com/3/device/${deviceId}`); + }); + + it('should return the sandbox URL when NODE_ENV is not set to "production"', () => { + process.env.NODE_ENV = 'development'; + const deviceId = 'testDeviceId'; + const result = NotificationService.getAPNsFullUrlToUse(deviceId); + expect(result).toBe(`https://api.sandbox.push.apple.com/3/device/${deviceId}`); + }); + + it('should append the correct device ID to the URL', () => { + process.env.NODE_ENV = 'production'; + const deviceId = 'device123'; + const result = NotificationService.getAPNsFullUrlToUse(deviceId); + expect(result).toBe(`https://api.push.apple.com/3/device/${deviceId}`); + }); + }); + describe("cancelNotification", () => { it("stops notification from sending to given shuttle/stop ID", async () => { From 80e976752b17ea39e060779909eb35ff2103db2b Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Mon, 3 Feb 2025 23:29:01 -0800 Subject: [PATCH 54/60] change behavior for missing shuttle/stop/ETA --- src/services/NotificationService.ts | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/services/NotificationService.ts b/src/services/NotificationService.ts index 1a66870..1c16c84 100644 --- a/src/services/NotificationService.ts +++ b/src/services/NotificationService.ts @@ -72,23 +72,28 @@ export class NotificationService { return this._lastRefreshedTimeMs && Date.now() - this._lastRefreshedTimeMs < thirtyMinutesMs; } - private async sendEtaNotificationImmediately({ deviceId, shuttleId, stopId }: ScheduledNotificationData): Promise { + private async sendEtaNotificationImmediately(notificationData: ScheduledNotificationData): Promise { + const { deviceId, shuttleId, stopId } = notificationData; this.reloadAPNsTokenIfTimePassed(); const url = NotificationService.getAPNsFullUrlToUse(deviceId); const shuttle = await this.repository.getShuttleById(shuttleId); const stop = await this.repository.getStopById(stopId); const eta = await this.repository.getEtaForShuttleAndStopId(shuttleId, stopId); - // TODO: add more specific errors if (!shuttle) { - throw new Error("The shuttle given by the provided shuttleID doesn't exist."); + console.warn(`Notification ${notificationData} fell through; no associated shuttle`); + return false; } if (!stop) { - throw new Error("The shuttle given by the provided stopId doesn't exist."); + console.warn(`Notification ${notificationData} fell through; no associated stop`); + return false; } - // TODO: account for cases where ETA may not exist due to "data race" with ApiBasedRepositoryLoader + + // 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) { - throw new Error("There is no ETA for this shuttle/stop."); + console.warn(`Notification ${notificationData} fell through; no associated ETA`); + return false; } // Send the fetch request From 3e4f6e544024476f5deb7b1d55fb343d55def9df Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Mon, 3 Feb 2025 23:29:45 -0800 Subject: [PATCH 55/60] remove empty test file --- src/services/NotificationService.test.ts | 2 -- 1 file changed, 2 deletions(-) delete mode 100644 src/services/NotificationService.test.ts diff --git a/src/services/NotificationService.test.ts b/src/services/NotificationService.test.ts deleted file mode 100644 index fb3ff70..0000000 --- a/src/services/NotificationService.test.ts +++ /dev/null @@ -1,2 +0,0 @@ -import { NotificationService } from './NotificationService'; - From d1db9b3742896a8f3d072b61513273ba0d3b74d8 Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Mon, 3 Feb 2025 23:45:30 -0800 Subject: [PATCH 56/60] add test for cancelNotification --- .../services/NotificationServiceTests.test.ts | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/test/services/NotificationServiceTests.test.ts b/test/services/NotificationServiceTests.test.ts index f2a5dbc..7c37abf 100644 --- a/test/services/NotificationServiceTests.test.ts +++ b/test/services/NotificationServiceTests.test.ts @@ -3,7 +3,7 @@ import { NotificationService } from "../../src/services/NotificationService"; import { UnoptimizedInMemoryRepository } from "../../src/repositories/UnoptimizedInMemoryRepository"; import fs from "fs"; import { IEta, IShuttle, IStop } from "../../src/entities/entities"; -import { updateGlobalFetchMockJson } from "../testHelpers/fetchMockHelpers"; +import { resetGlobalFetchMockJson, updateGlobalFetchMockJson } from "../testHelpers/fetchMockHelpers"; import { addMockShuttleToRepository, addMockStopToRepository } from "../testHelpers/repositorySetupHelpers"; jest.mock("fs"); @@ -58,6 +58,8 @@ describe("NotificationService", () => { }; (fs.readFileSync as jest.Mock).mockReturnValue(sampleKey); + + resetGlobalFetchMockJson(); }) describe("reloadAPNsTokenIfTimePassed", () => { @@ -203,7 +205,22 @@ describe("NotificationService", () => { describe("cancelNotification", () => { it("stops notification from sending to given shuttle/stop ID", async () => { + // Arrange + const shuttle = await addMockShuttleToRepository(repository, "1"); + const stop = await addMockStopToRepository(repository, "1"); + const { eta, notificationData1 } = generateNotificationDataAndEta(shuttle, stop); + updateGlobalFetchMockJson({}); + + await notificationService.scheduleNotification(notificationData1); + + // Act + await notificationService.cancelNotification(notificationData1); + await repository.addOrUpdateEta(eta); + + // Assert + await waitForMilliseconds(500); + expect(fetch as jest.Mock).toHaveBeenCalledTimes(0); }); }); From 403bec7c63dfdaa9c0f4556f9f81d4610fbc08de Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Mon, 3 Feb 2025 23:45:55 -0800 Subject: [PATCH 57/60] rename method to define what happens if notification doesn't exist --- src/services/NotificationService.ts | 2 +- test/services/NotificationServiceTests.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/services/NotificationService.ts b/src/services/NotificationService.ts index 1c16c84..1b49710 100644 --- a/src/services/NotificationService.ts +++ b/src/services/NotificationService.ts @@ -197,7 +197,7 @@ export class NotificationService { * @param shuttleId Shuttle ID of the ETA object. * @param stopId Stop ID of the ETA object. */ - public async cancelNotification({ deviceId, shuttleId, stopId }: ScheduledNotificationData) { + public async cancelNotificationIfExists({ deviceId, shuttleId, stopId }: ScheduledNotificationData) { } diff --git a/test/services/NotificationServiceTests.test.ts b/test/services/NotificationServiceTests.test.ts index 7c37abf..a4aa678 100644 --- a/test/services/NotificationServiceTests.test.ts +++ b/test/services/NotificationServiceTests.test.ts @@ -215,7 +215,7 @@ describe("NotificationService", () => { await notificationService.scheduleNotification(notificationData1); // Act - await notificationService.cancelNotification(notificationData1); + await notificationService.cancelNotificationIfExists(notificationData1); await repository.addOrUpdateEta(eta); // Assert From 629fe4dddd1f15ddde17e2ba6fa881abfd0131c9 Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Mon, 3 Feb 2025 23:48:53 -0800 Subject: [PATCH 58/60] add cancelNotificationIfExists implementation --- src/services/NotificationService.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/services/NotificationService.ts b/src/services/NotificationService.ts index 1b49710..a59d31e 100644 --- a/src/services/NotificationService.ts +++ b/src/services/NotificationService.ts @@ -198,7 +198,18 @@ export class NotificationService { * @param stopId Stop ID of the ETA object. */ public async cancelNotificationIfExists({ deviceId, shuttleId, stopId }: ScheduledNotificationData) { + const tupleKey = new TupleKey(shuttleId, stopId); + if ( + this.deviceIdsToDeliverTo[tupleKey.toString()] === undefined + || !this.deviceIdsToDeliverTo[tupleKey.toString()].includes(deviceId) + ) { + return; + } + const index = this.deviceIdsToDeliverTo[tupleKey.toString()].findIndex(id => id === deviceId); + if (index !== -1) { + this.deviceIdsToDeliverTo[tupleKey.toString()].splice(index, 1); + } } /** From 4a63929cc1a5aa6ce1bc3e7a75e030369a4143d0 Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Mon, 3 Feb 2025 23:50:00 -0800 Subject: [PATCH 59/60] remove cancelAllNotifications method (one less method to test --- src/services/NotificationService.ts | 4 ---- test/services/NotificationServiceTests.test.ts | 6 ------ 2 files changed, 10 deletions(-) diff --git a/src/services/NotificationService.ts b/src/services/NotificationService.ts index a59d31e..a45d177 100644 --- a/src/services/NotificationService.ts +++ b/src/services/NotificationService.ts @@ -225,8 +225,4 @@ export class NotificationService { } return this.deviceIdsToDeliverTo[tuple.toString()].includes(deviceId); } - - public cancelAllNotifications(deviceId: string) { - - } } diff --git a/test/services/NotificationServiceTests.test.ts b/test/services/NotificationServiceTests.test.ts index a4aa678..cf5308a 100644 --- a/test/services/NotificationServiceTests.test.ts +++ b/test/services/NotificationServiceTests.test.ts @@ -223,10 +223,4 @@ describe("NotificationService", () => { expect(fetch as jest.Mock).toHaveBeenCalledTimes(0); }); }); - - describe("cancelAllNotifications", () => { - it("clears all notifications scheduled to be sent", async () => { - - }); - }) }); From 9048d2f1bcc09a451aaa983e197f7bf2e205d393 Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Mon, 3 Feb 2025 23:54:37 -0800 Subject: [PATCH 60/60] remove todo for now --- src/services/NotificationService.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/services/NotificationService.ts b/src/services/NotificationService.ts index a45d177..66ec81d 100644 --- a/src/services/NotificationService.ts +++ b/src/services/NotificationService.ts @@ -145,7 +145,6 @@ export class NotificationService { private async etaSubscriberCallback(eta: IEta) { const tuple = new TupleKey(eta.shuttleId, eta.stopId); - // TODO: move device IDs object (with TupleKey based string) to its own class if (this.deviceIdsToDeliverTo[tuple.toString()] === undefined) { return; }