From f52b968d85bb965a12ac142b507882e223941b12 Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Thu, 27 Mar 2025 09:14:58 -0700 Subject: [PATCH 01/33] add redis configuration with AOF --- redis.conf | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 redis.conf diff --git a/redis.conf b/redis.conf new file mode 100644 index 0000000..ccf37d4 --- /dev/null +++ b/redis.conf @@ -0,0 +1,28 @@ +# See https://raw.githubusercontent.com/redis/redis/unstable/redis.conf +# for a full example + +############################## APPEND ONLY MODE ############################### + +# By default Redis asynchronously dumps the dataset on disk. This mode is +# good enough in many applications, but an issue with the Redis process or +# a power outage may result into a few minutes of writes lost (depending on +# the configured save points). +# +# The Append Only File is an alternative persistence mode that provides +# much better durability. For instance using the default data fsync policy +# (see later in the config file) Redis can lose just one second of writes in a +# dramatic event like a server power outage, or a single write if something +# wrong with the Redis process itself happens, but the operating system is +# still running correctly. +# +# AOF and RDB persistence can be enabled at the same time without problems. +# If the AOF is enabled on startup Redis will load the AOF, that is the file +# with the better durability guarantees. +# +# Note that changing this value in a config file of an existing database and +# restarting the server can lead to data loss. A conversion needs to be done +# by setting it via CONFIG command on a live server first. +# +# Please check https://redis.io/docs/latest/operate/oss_and_stack/management/persistence/ for more information. + +appendonly no From 8ef161ae3beeaab7e5c779ff3b387326f2e40585 Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Thu, 27 Mar 2025 09:15:15 -0700 Subject: [PATCH 02/33] update AOF option --- redis.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/redis.conf b/redis.conf index ccf37d4..b916b95 100644 --- a/redis.conf +++ b/redis.conf @@ -25,4 +25,4 @@ # # Please check https://redis.io/docs/latest/operate/oss_and_stack/management/persistence/ for more information. -appendonly no +appendonly yes From 7761e09b0e004588e1e0043e85c463289e2a7942 Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Thu, 27 Mar 2025 09:21:17 -0700 Subject: [PATCH 03/33] add separate redis service without persistence --- docker-compose.yml | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 05a69c7..335f941 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -31,7 +31,7 @@ services: ports: - "4000:4000" depends_on: - - redis + - redis-no-persistence environment: <<: *common-server-environment @@ -42,7 +42,7 @@ services: build: . command: npm run test depends_on: - - redis + - redis-no-persistence environment: <<: *common-server-environment @@ -52,3 +52,12 @@ services: image: redis:alpine ports: - "6379:6379" + volumes: + - ./redis.conf:/usr/local/etc/redis/redis.conf + command: redis-server /usr/local/etc/redis/redis.conf + + redis-no-persistence: + image: redis:alpine + ports: + - "6379:6379" + From 617f4dc72bf8dd1883e684394816135c5796ba4e Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Thu, 27 Mar 2025 09:26:37 -0700 Subject: [PATCH 04/33] add redis and run npm audit fix --- package-lock.json | 135 ++++++++++++++++++++++++++++++++++++++-------- package.json | 3 +- 2 files changed, 115 insertions(+), 23 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4d485f2..2bbd249 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,8 @@ "dependencies": { "@apollo/server": "^4.11.2", "graphql": "^16.10.0", - "jsonwebtoken": "^9.0.2" + "jsonwebtoken": "^9.0.2", + "redis": "^4.7.0" }, "devDependencies": { "@graphql-codegen/cli": "5.0.3", @@ -628,25 +629,25 @@ } }, "node_modules/@babel/helpers": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.0.tgz", - "integrity": "sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.0.tgz", + "integrity": "sha512-U5eyP/CTFPuNE3qk+WZMxFkp/4zUzdceQlfzf7DdGdhp+Fezd7HD+i8Y24ZuTMKX3wQBld449jijbGq6OdGNQg==", "dev": true, "dependencies": { - "@babel/template": "^7.25.9", - "@babel/types": "^7.26.0" + "@babel/template": "^7.27.0", + "@babel/types": "^7.27.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.26.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.3.tgz", - "integrity": "sha512-WJ/CvmY8Mea8iDXo6a7RK2wbmJITT5fN3BEkRuFlxVyNx8jOKIIhmC4fSkTcPcf8JyavbBwIe6OpiCOBXt/IcA==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.0.tgz", + "integrity": "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==", "dev": true, "dependencies": { - "@babel/types": "^7.26.3" + "@babel/types": "^7.27.0" }, "bin": { "parser": "bin/babel-parser.js" @@ -1262,9 +1263,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.0.tgz", - "integrity": "sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.0.tgz", + "integrity": "sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==", "dev": true, "dependencies": { "regenerator-runtime": "^0.14.0" @@ -1274,14 +1275,14 @@ } }, "node_modules/@babel/template": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz", - "integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.0.tgz", + "integrity": "sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA==", "dev": true, "dependencies": { - "@babel/code-frame": "^7.25.9", - "@babel/parser": "^7.25.9", - "@babel/types": "^7.25.9" + "@babel/code-frame": "^7.26.2", + "@babel/parser": "^7.27.0", + "@babel/types": "^7.27.0" }, "engines": { "node": ">=6.9.0" @@ -1329,9 +1330,9 @@ "dev": true }, "node_modules/@babel/types": { - "version": "7.26.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.3.tgz", - "integrity": "sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.0.tgz", + "integrity": "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==", "dev": true, "dependencies": { "@babel/helper-string-parser": "^7.25.9", @@ -3413,6 +3414,64 @@ "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" }, + "node_modules/@redis/bloom": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-1.2.0.tgz", + "integrity": "sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg==", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/client": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@redis/client/-/client-1.6.0.tgz", + "integrity": "sha512-aR0uffYI700OEEH4gYnitAnv3vzVGXCFvYfdpu/CJKvk4pHfLPEy/JSZyrpQ+15WhXe1yJRXLtfQ84s4mEXnPg==", + "dependencies": { + "cluster-key-slot": "1.1.2", + "generic-pool": "3.9.0", + "yallist": "4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@redis/client/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "node_modules/@redis/graph": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@redis/graph/-/graph-1.1.1.tgz", + "integrity": "sha512-FEMTcTHZozZciLRl6GiiIB4zGm5z5F3F6a6FZCyrfxdKOhFlGkiAqlexWMBzCi4DcRoyiOsuLfW+cjlGWyExOw==", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/json": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@redis/json/-/json-1.0.7.tgz", + "integrity": "sha512-6UyXfjVaTBTJtKNG4/9Z8PSpKE6XgSyEb8iwaqDcy+uKrd/DGYHTWkUdnQDyzm727V7p21WUMhsqz5oy65kPcQ==", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/search": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@redis/search/-/search-1.2.0.tgz", + "integrity": "sha512-tYoDBbtqOVigEDMAcTGsRlMycIIjwMCgD8eR2t0NANeQmgK/lvxNAvYyb6bZDD4frHRhIHkJu2TBRvB0ERkOmw==", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/time-series": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-1.1.0.tgz", + "integrity": "sha512-c1Q99M5ljsIuc4YdaCwfUEXsofakb9c8+Zse2qxTadu8TalLXuAESzLvFAvNVbkmSlvlzIQOLpBCmWI9wTOt+g==", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, "node_modules/@repeaterjs/repeater": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/@repeaterjs/repeater/-/repeater-3.0.6.tgz", @@ -4496,6 +4555,14 @@ "node": ">=0.8" } }, + "node_modules/cluster-key-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", + "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -5365,6 +5432,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/generic-pool": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-3.9.0.tgz", + "integrity": "sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==", + "engines": { + "node": ">= 4" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -7993,6 +8068,22 @@ "node": ">= 6" } }, + "node_modules/redis": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/redis/-/redis-4.7.0.tgz", + "integrity": "sha512-zvmkHEAdGMn+hMRXuMBtu4Vo5P6rHQjLoHftu+lBqq8ZTA3RCVC/WzD790bkKKiNFp7d5/9PcSD19fJyyRvOdQ==", + "workspaces": [ + "./packages/*" + ], + "dependencies": { + "@redis/bloom": "1.2.0", + "@redis/client": "1.6.0", + "@redis/graph": "1.1.1", + "@redis/json": "1.0.7", + "@redis/search": "1.2.0", + "@redis/time-series": "1.1.0" + } + }, "node_modules/regenerator-runtime": { "version": "0.14.1", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", diff --git a/package.json b/package.json index d8ef7ca..7be6333 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "dependencies": { "@apollo/server": "^4.11.2", "graphql": "^16.10.0", - "jsonwebtoken": "^9.0.2" + "jsonwebtoken": "^9.0.2", + "redis": "^4.7.0" } } From 3a85f3da8bd78769bac2c433d9dc60df3da4f618 Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Thu, 27 Mar 2025 09:28:47 -0700 Subject: [PATCH 05/33] bind rest of notification scheduler methods to class --- src/notifications/schedulers/ETANotificationScheduler.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/notifications/schedulers/ETANotificationScheduler.ts b/src/notifications/schedulers/ETANotificationScheduler.ts index 8789cdd..50c92b8 100644 --- a/src/notifications/schedulers/ETANotificationScheduler.ts +++ b/src/notifications/schedulers/ETANotificationScheduler.ts @@ -32,6 +32,10 @@ export class ETANotificationScheduler { this.etaSubscriberCallback = this.etaSubscriberCallback.bind(this); this.sendEtaNotificationImmediatelyIfSecondsRemainingBelowThreshold = this.sendEtaNotificationImmediatelyIfSecondsRemainingBelowThreshold.bind(this); this.scheduleNotification = this.scheduleNotification.bind(this); + this.cancelNotificationIfExists = this.cancelNotificationIfExists.bind(this); + this.isNotificationScheduled = this.isNotificationScheduled.bind(this); + this.getSecondsThresholdForScheduledNotification = this.getSecondsThresholdForScheduledNotification.bind(this); + this.getAllScheduledNotificationsForDevice = this.getAllScheduledNotificationsForDevice.bind(this); } /** From fab99db755cb455749e6142dddedb49675ac8bb5 Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Thu, 27 Mar 2025 09:32:29 -0700 Subject: [PATCH 06/33] rename existing repository to shuttle repository --- src/ServerContext.ts | 4 ++-- src/index.ts | 6 +++--- src/loaders/ApiBasedRepositoryLoader.ts | 4 ++-- src/loaders/TimedApiBasedRepositoryLoader.ts | 7 +++---- src/loaders/loadTestData.ts | 4 ++-- .../schedulers/ETANotificationScheduler.ts | 14 +++++++------- ...terRepository.ts => ShuttleGetterRepository.ts} | 2 +- ...ository.ts => ShuttleGetterSetterRepository.ts} | 6 +++--- ....ts => UnoptimizedInMemoryShuttleRepository.ts} | 4 ++-- test/loaders/ApiBasedRepositoryLoaderTests.test.ts | 4 ++-- .../TimedApiBasedRepositoryLoaderTests.test.ts | 6 +++--- .../ETANotificationSchedulerTests.test.ts | 6 +++--- .../UnoptimizedInMemoryRepositoryTests.test.ts | 10 +++++----- test/testHelpers/apolloTestServerHelpers.ts | 4 ++-- test/testHelpers/repositorySetupHelpers.ts | 12 ++++++------ 15 files changed, 46 insertions(+), 47 deletions(-) rename src/repositories/{GetterRepository.ts => ShuttleGetterRepository.ts} (97%) rename src/repositories/{GetterSetterRepository.ts => ShuttleGetterSetterRepository.ts} (85%) rename src/repositories/{UnoptimizedInMemoryRepository.ts => UnoptimizedInMemoryShuttleRepository.ts} (97%) diff --git a/src/ServerContext.ts b/src/ServerContext.ts index 8dade11..95c0330 100644 --- a/src/ServerContext.ts +++ b/src/ServerContext.ts @@ -1,7 +1,7 @@ import { ETANotificationScheduler } from "./notifications/schedulers/ETANotificationScheduler"; -import { GetterSetterRepository } from "./repositories/GetterSetterRepository"; +import { ShuttleGetterSetterRepository } from "./repositories/ShuttleGetterSetterRepository"; export interface ServerContext { - repository: GetterSetterRepository; + repository: ShuttleGetterSetterRepository; notificationService: ETANotificationScheduler; } diff --git a/src/index.ts b/src/index.ts index 2bff64c..99e0f37 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,7 +3,7 @@ import { ApolloServer } from "@apollo/server"; import { startStandaloneServer } from "@apollo/server/standalone"; import { MergedResolvers } from "./MergedResolvers"; import { ServerContext } from "./ServerContext"; -import { UnoptimizedInMemoryRepository } from "./repositories/UnoptimizedInMemoryRepository"; +import { UnoptimizedInMemoryShuttleRepository } from "./repositories/UnoptimizedInMemoryShuttleRepository"; import { TimedApiBasedRepositoryLoader } from "./loaders/TimedApiBasedRepositoryLoader"; import { ETANotificationScheduler } from "./notifications/schedulers/ETANotificationScheduler"; import { loadTestData } from "./loaders/loadTestData"; @@ -18,7 +18,7 @@ async function main() { introspection: process.env.NODE_ENV !== "production", }); - const repository = new UnoptimizedInMemoryRepository(); + const repository = new UnoptimizedInMemoryShuttleRepository(); let notificationService: ETANotificationScheduler; if (process.argv.length > 2 && process.argv[2] == "integration-testing") { console.log("Using integration testing setup") @@ -37,7 +37,7 @@ async function main() { listen: { port: process.env.PORT ? parseInt(process.env.PORT) : 4000, }, - context: async ({ req, res }) => { + context: async () => { return { repository, notificationService, diff --git a/src/loaders/ApiBasedRepositoryLoader.ts b/src/loaders/ApiBasedRepositoryLoader.ts index 50003db..aa4b078 100644 --- a/src/loaders/ApiBasedRepositoryLoader.ts +++ b/src/loaders/ApiBasedRepositoryLoader.ts @@ -1,4 +1,4 @@ -import { GetterSetterRepository } from "../repositories/GetterSetterRepository"; +import { ShuttleGetterSetterRepository } from "../repositories/ShuttleGetterSetterRepository"; import { IEntityWithId, IEta, IRoute, IShuttle, IStop, ISystem } from "../entities/entities"; import { RepositoryLoader } from "./RepositoryLoader"; @@ -19,7 +19,7 @@ export class ApiBasedRepositoryLoader implements RepositoryLoader { baseUrl = "https://passiogo.com/mapGetData.php"; constructor( - public repository: GetterSetterRepository, + public repository: ShuttleGetterSetterRepository, ) { } diff --git a/src/loaders/TimedApiBasedRepositoryLoader.ts b/src/loaders/TimedApiBasedRepositoryLoader.ts index b747b0e..8c830ba 100644 --- a/src/loaders/TimedApiBasedRepositoryLoader.ts +++ b/src/loaders/TimedApiBasedRepositoryLoader.ts @@ -1,5 +1,4 @@ -import { GetterSetterRepository } from "../repositories/GetterSetterRepository"; -import { IEta, IRoute, IShuttle, IStop, ISystem } from "../entities/entities"; +import { ShuttleGetterSetterRepository } from "../repositories/ShuttleGetterSetterRepository"; import { ApiBasedRepositoryLoader } from "./ApiBasedRepositoryLoader"; // Ideas to break this into smaller pieces in the future: @@ -23,7 +22,7 @@ export class TimedApiBasedRepositoryLoader extends ApiBasedRepositoryLoader { readonly timeout = 10000; constructor( - repository: GetterSetterRepository, + repository: ShuttleGetterSetterRepository, ) { super(repository); this.startFetchDataAndUpdate = this.startFetchDataAndUpdate.bind(this); @@ -62,4 +61,4 @@ export class TimedApiBasedRepositoryLoader extends ApiBasedRepositoryLoader { this.timer = setTimeout(this.startFetchDataAndUpdate, this.timeout); } -} \ No newline at end of file +} diff --git a/src/loaders/loadTestData.ts b/src/loaders/loadTestData.ts index ad75af4..013379a 100644 --- a/src/loaders/loadTestData.ts +++ b/src/loaders/loadTestData.ts @@ -1,6 +1,6 @@ // Mock data import { IEta, IOrderedStop, IRoute, IShuttle, IStop, ISystem } from "../entities/entities"; -import { GetterSetterRepository } from "../repositories/GetterSetterRepository"; +import { ShuttleGetterSetterRepository } from "../repositories/ShuttleGetterSetterRepository"; const systems: ISystem[] = [ { @@ -4454,7 +4454,7 @@ const etas: IEta[] = [ } ]; -export async function loadTestData(repository: GetterSetterRepository) { +export async function loadTestData(repository: ShuttleGetterSetterRepository) { await Promise.all(systems.map(async (system) => { await repository.addOrUpdateSystem(system); })); diff --git a/src/notifications/schedulers/ETANotificationScheduler.ts b/src/notifications/schedulers/ETANotificationScheduler.ts index 50c92b8..3d270dd 100644 --- a/src/notifications/schedulers/ETANotificationScheduler.ts +++ b/src/notifications/schedulers/ETANotificationScheduler.ts @@ -1,4 +1,4 @@ -import { GetterRepository } from "../../repositories/GetterRepository"; +import { ShuttleGetterRepository } from "../../repositories/ShuttleGetterRepository"; import { TupleKey } from "../../types/TupleKey"; import { IEta } from "../../entities/entities"; import { AppleNotificationSender, NotificationAlertArguments } from "../senders/AppleNotificationSender"; @@ -24,7 +24,7 @@ type DeviceIdSecondsThresholdAssociation = { [key: string]: number }; export class ETANotificationScheduler { public static readonly defaultSecondsThresholdForNotificationToFire = 180; - constructor(private repository: GetterRepository, + constructor(private shuttleRepository: ShuttleGetterRepository, private appleNotificationSender = new AppleNotificationSender() ) { this.etaSubscriberCallback = this.etaSubscriberCallback.bind(this); @@ -50,9 +50,9 @@ export class ETANotificationScheduler { private async sendEtaNotificationImmediately(notificationData: NotificationSchedulingArguments): Promise { const { deviceId, shuttleId, stopId } = notificationData; - const shuttle = await this.repository.getShuttleById(shuttleId); - const stop = await this.repository.getStopById(stopId); - const eta = await this.repository.getEtaForShuttleAndStopId(shuttleId, stopId); + 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; @@ -127,8 +127,8 @@ export class ETANotificationScheduler { this.deviceIdsToDeliverTo[tuple.toString()][deviceId] = secondsThreshold; - this.repository.unsubscribeFromEtaUpdates(this.etaSubscriberCallback); - this.repository.subscribeToEtaUpdates(this.etaSubscriberCallback); + this.shuttleRepository.unsubscribeFromEtaUpdates(this.etaSubscriberCallback); + this.shuttleRepository.subscribeToEtaUpdates(this.etaSubscriberCallback); } /** diff --git a/src/repositories/GetterRepository.ts b/src/repositories/ShuttleGetterRepository.ts similarity index 97% rename from src/repositories/GetterRepository.ts rename to src/repositories/ShuttleGetterRepository.ts index 9c5f27f..664328f 100644 --- a/src/repositories/GetterRepository.ts +++ b/src/repositories/ShuttleGetterRepository.ts @@ -1,6 +1,6 @@ import { IEta, IOrderedStop, IRoute, IShuttle, IStop, ISystem } from "../entities/entities"; -export interface GetterRepository { +export interface ShuttleGetterRepository { getSystems(): Promise; getSystemById(systemId: string): Promise; diff --git a/src/repositories/GetterSetterRepository.ts b/src/repositories/ShuttleGetterSetterRepository.ts similarity index 85% rename from src/repositories/GetterSetterRepository.ts rename to src/repositories/ShuttleGetterSetterRepository.ts index 1c914c0..0c0bf90 100644 --- a/src/repositories/GetterSetterRepository.ts +++ b/src/repositories/ShuttleGetterSetterRepository.ts @@ -1,16 +1,16 @@ // If types match closely, we can use TypeScript "casting" // to convert from data repo to GraphQL schema -import { GetterRepository } from "./GetterRepository"; +import { ShuttleGetterRepository } from "./ShuttleGetterRepository"; import { IEta, IOrderedStop, IRoute, IShuttle, IStop, ISystem } from "../entities/entities"; /** - * GetterRepository interface for data derived from Passio API. + * ShuttleGetterRepository interface for data derived from Passio API. * The repository is not designed to have write locks in place. * Objects passed from/to the repository should be treated * as disposable. */ -export interface GetterSetterRepository extends GetterRepository { +export interface ShuttleGetterSetterRepository extends ShuttleGetterRepository { // Setter methods addOrUpdateSystem(system: ISystem): Promise; addOrUpdateRoute(route: IRoute): Promise; diff --git a/src/repositories/UnoptimizedInMemoryRepository.ts b/src/repositories/UnoptimizedInMemoryShuttleRepository.ts similarity index 97% rename from src/repositories/UnoptimizedInMemoryRepository.ts rename to src/repositories/UnoptimizedInMemoryShuttleRepository.ts index e347803..59afac9 100644 --- a/src/repositories/UnoptimizedInMemoryRepository.ts +++ b/src/repositories/UnoptimizedInMemoryShuttleRepository.ts @@ -1,4 +1,4 @@ -import { GetterSetterRepository } from "./GetterSetterRepository"; +import { ShuttleGetterSetterRepository } from "./ShuttleGetterSetterRepository"; import { IEntityWithId, IEta, IOrderedStop, IRoute, IShuttle, IStop, ISystem } from "../entities/entities"; /** @@ -6,7 +6,7 @@ import { IEntityWithId, IEta, IOrderedStop, IRoute, IShuttle, IStop, ISystem } f * (I would optimize it with actual data structures, but I'm * switching to another data store later anyways) */ -export class UnoptimizedInMemoryRepository implements GetterSetterRepository { +export class UnoptimizedInMemoryShuttleRepository implements ShuttleGetterSetterRepository { private systems: ISystem[] = []; private stops: IStop[] = []; private routes: IRoute[] = []; diff --git a/test/loaders/ApiBasedRepositoryLoaderTests.test.ts b/test/loaders/ApiBasedRepositoryLoaderTests.test.ts index 87cb896..8e857c8 100644 --- a/test/loaders/ApiBasedRepositoryLoaderTests.test.ts +++ b/test/loaders/ApiBasedRepositoryLoaderTests.test.ts @@ -1,6 +1,6 @@ import { beforeEach, describe, expect, it, jest, test } from "@jest/globals"; import { ApiBasedRepositoryLoader, ApiResponseError } from "../../src/loaders/ApiBasedRepositoryLoader"; -import { UnoptimizedInMemoryRepository } from "../../src/repositories/UnoptimizedInMemoryRepository"; +import { UnoptimizedInMemoryShuttleRepository } from "../../src/repositories/UnoptimizedInMemoryShuttleRepository"; import { fetchSystemDataSuccessfulResponse } from "../jsonSnapshots/fetchSystemData/fetchSystemDataSuccessfulResponse"; import { fetchSystemDataFailedResponse } from "../jsonSnapshots/fetchSystemData/fetchSystemDataFailedResponse"; import { fetchRouteDataSuccessfulResponse } from "../jsonSnapshots/fetchRouteData/fetchRouteDataSuccessfulResponse"; @@ -26,7 +26,7 @@ describe("ApiBasedRepositoryLoader", () => { let loader: ApiBasedRepositoryLoader; beforeEach(() => { - loader = new ApiBasedRepositoryLoader(new UnoptimizedInMemoryRepository()); + loader = new ApiBasedRepositoryLoader(new UnoptimizedInMemoryShuttleRepository()); resetGlobalFetchMockJson(); }); diff --git a/test/loaders/TimedApiBasedRepositoryLoaderTests.test.ts b/test/loaders/TimedApiBasedRepositoryLoaderTests.test.ts index 9979a06..602663e 100644 --- a/test/loaders/TimedApiBasedRepositoryLoaderTests.test.ts +++ b/test/loaders/TimedApiBasedRepositoryLoaderTests.test.ts @@ -1,7 +1,7 @@ import { afterEach, beforeAll, beforeEach, describe, expect, it, jest } from "@jest/globals"; import { TimedApiBasedRepositoryLoader } from "../../src/loaders/TimedApiBasedRepositoryLoader"; import { resetGlobalFetchMockJson } from "../testHelpers/fetchMockHelpers"; -import { UnoptimizedInMemoryRepository } from "../../src/repositories/UnoptimizedInMemoryRepository"; +import { UnoptimizedInMemoryShuttleRepository } from "../../src/repositories/UnoptimizedInMemoryShuttleRepository"; describe("TimedApiBasedRepositoryLoader", () => { let loader: TimedApiBasedRepositoryLoader; @@ -15,7 +15,7 @@ describe("TimedApiBasedRepositoryLoader", () => { beforeEach(() => { resetGlobalFetchMockJson(); - loader = new TimedApiBasedRepositoryLoader(new UnoptimizedInMemoryRepository()); + loader = new TimedApiBasedRepositoryLoader(new UnoptimizedInMemoryShuttleRepository()); spies = { fetchAndUpdateSystemData: jest.spyOn(loader, 'fetchAndUpdateSystemData'), @@ -64,4 +64,4 @@ describe("TimedApiBasedRepositoryLoader", () => { expect(loader['shouldBeRunning']).toBe(false); }); }); -}); \ No newline at end of file +}); diff --git a/test/notifications/schedulers/ETANotificationSchedulerTests.test.ts b/test/notifications/schedulers/ETANotificationSchedulerTests.test.ts index df528b9..afc6f85 100644 --- a/test/notifications/schedulers/ETANotificationSchedulerTests.test.ts +++ b/test/notifications/schedulers/ETANotificationSchedulerTests.test.ts @@ -1,6 +1,6 @@ import { beforeEach, describe, expect, it, jest } from "@jest/globals"; import { ETANotificationScheduler } from "../../../src/notifications/schedulers/ETANotificationScheduler"; -import { UnoptimizedInMemoryRepository } from "../../../src/repositories/UnoptimizedInMemoryRepository"; +import { UnoptimizedInMemoryShuttleRepository } from "../../../src/repositories/UnoptimizedInMemoryShuttleRepository"; import http2 from "http2"; import { IEta, IShuttle, IStop } from "../../../src/entities/entities"; import { addMockShuttleToRepository, addMockStopToRepository } from "../../testHelpers/repositorySetupHelpers"; @@ -42,11 +42,11 @@ async function waitForMilliseconds(ms: number): Promise { describe("ETANotificationScheduler", () => { - let repository: UnoptimizedInMemoryRepository + let repository: UnoptimizedInMemoryShuttleRepository let notificationService: ETANotificationScheduler; beforeEach(() => { - repository = new UnoptimizedInMemoryRepository(); + repository = new UnoptimizedInMemoryShuttleRepository(); mockNotificationSenderMethods(true); diff --git a/test/repositories/UnoptimizedInMemoryRepositoryTests.test.ts b/test/repositories/UnoptimizedInMemoryRepositoryTests.test.ts index 35956b3..003f71f 100644 --- a/test/repositories/UnoptimizedInMemoryRepositoryTests.test.ts +++ b/test/repositories/UnoptimizedInMemoryRepositoryTests.test.ts @@ -1,5 +1,5 @@ import { beforeEach, describe, expect, jest, test } from "@jest/globals"; -import { UnoptimizedInMemoryRepository } from "../../src/repositories/UnoptimizedInMemoryRepository"; +import { UnoptimizedInMemoryShuttleRepository } from "../../src/repositories/UnoptimizedInMemoryShuttleRepository"; import { generateMockEtas, generateMockOrderedStops, @@ -11,14 +11,14 @@ import { // For repositories created in the future, reuse core testing // logic from here and differentiate setup (e.g. creating mocks) -// Do this by creating a function which takes a GetterRepository -// or GetterSetterRepository instance +// Do this by creating a function which takes a ShuttleGetterRepository +// or ShuttleGetterSetterRepository instance describe("UnoptimizedInMemoryRepository", () => { - let repository: UnoptimizedInMemoryRepository; + let repository: UnoptimizedInMemoryShuttleRepository; beforeEach(() => { - repository = new UnoptimizedInMemoryRepository(); + repository = new UnoptimizedInMemoryShuttleRepository(); }); describe("getSystems", () => { diff --git a/test/testHelpers/apolloTestServerHelpers.ts b/test/testHelpers/apolloTestServerHelpers.ts index f9fa2d9..164bd35 100644 --- a/test/testHelpers/apolloTestServerHelpers.ts +++ b/test/testHelpers/apolloTestServerHelpers.ts @@ -1,7 +1,7 @@ import { readFileSync } from "fs"; import { ApolloServer } from "@apollo/server"; import { MergedResolvers } from "../../src/MergedResolvers"; -import { UnoptimizedInMemoryRepository } from "../../src/repositories/UnoptimizedInMemoryRepository"; +import { UnoptimizedInMemoryShuttleRepository } from "../../src/repositories/UnoptimizedInMemoryShuttleRepository"; import { beforeEach } from "@jest/globals"; import { ServerContext } from "../../src/ServerContext"; import { ETANotificationScheduler } from "../../src/notifications/schedulers/ETANotificationScheduler"; @@ -25,7 +25,7 @@ export function setupTestServerContext() { const context: { [key: string] : any } = {}; beforeEach(() => { - context.repository = new UnoptimizedInMemoryRepository(); + context.repository = new UnoptimizedInMemoryShuttleRepository(); context.notificationService = new ETANotificationScheduler(context.repository); }); diff --git a/test/testHelpers/repositorySetupHelpers.ts b/test/testHelpers/repositorySetupHelpers.ts index 4c95363..58caea3 100644 --- a/test/testHelpers/repositorySetupHelpers.ts +++ b/test/testHelpers/repositorySetupHelpers.ts @@ -5,9 +5,9 @@ import { generateMockStops, generateMockSystems } from "./mockDataGenerators"; -import { GetterSetterRepository } from "../../src/repositories/GetterSetterRepository"; +import { ShuttleGetterSetterRepository } from "../../src/repositories/ShuttleGetterSetterRepository"; -export async function addMockSystemToRepository(repository: GetterSetterRepository) { +export async function addMockSystemToRepository(repository: ShuttleGetterSetterRepository) { const mockSystems = generateMockSystems(); const mockSystem = mockSystems[0]; mockSystem.id = "1"; @@ -16,7 +16,7 @@ export async function addMockSystemToRepository(repository: GetterSetterReposito return mockSystem; } -export async function addMockRouteToRepository(repository: GetterSetterRepository, systemId: string) { +export async function addMockRouteToRepository(repository: ShuttleGetterSetterRepository, systemId: string) { const mockRoutes = generateMockRoutes(); const mockRoute = mockRoutes[0]; mockRoute.systemId = systemId; @@ -25,7 +25,7 @@ export async function addMockRouteToRepository(repository: GetterSetterRepositor return mockRoute; } -export async function addMockStopToRepository(repository: GetterSetterRepository, systemId: string) { +export async function addMockStopToRepository(repository: ShuttleGetterSetterRepository, systemId: string) { const mockStops = generateMockStops(); const mockStop = mockStops[0]; mockStop.systemId = systemId; @@ -34,7 +34,7 @@ export async function addMockStopToRepository(repository: GetterSetterRepository return mockStop; } -export async function addMockShuttleToRepository(repository: GetterSetterRepository, systemId: string) { +export async function addMockShuttleToRepository(repository: ShuttleGetterSetterRepository, systemId: string) { const mockShuttles = generateMockShuttles(); const mockShuttle = mockShuttles[0]; mockShuttle.systemId = systemId; @@ -42,7 +42,7 @@ export async function addMockShuttleToRepository(repository: GetterSetterReposit return mockShuttle; } -export async function addMockEtaToRepository(repository: GetterSetterRepository, stopId: string, shuttleId: string) { +export async function addMockEtaToRepository(repository: ShuttleGetterSetterRepository, stopId: string, shuttleId: string) { const etas = generateMockEtas(); const expectedEta = etas[0]; expectedEta.stopId = stopId; From 687fe0d826c8778fe114bd038f0361b4fb9950ce Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Thu, 27 Mar 2025 09:33:37 -0700 Subject: [PATCH 07/33] finish the rename for the data loaders --- src/index.ts | 8 ++++---- ...sitoryLoader.ts => ApiBasedShuttleRepositoryLoader.ts} | 4 ++-- .../{RepositoryLoader.ts => ShuttleRepositoryLoader.ts} | 2 +- ...yLoader.ts => TimedApiBasedShuttleRepositoryLoader.ts} | 4 ++-- src/loaders/{loadTestData.ts => loadShuttleTestData.ts} | 2 +- test/loaders/ApiBasedRepositoryLoaderTests.test.ts | 6 +++--- test/loaders/TimedApiBasedRepositoryLoaderTests.test.ts | 6 +++--- 7 files changed, 16 insertions(+), 16 deletions(-) rename src/loaders/{ApiBasedRepositoryLoader.ts => ApiBasedShuttleRepositoryLoader.ts} (98%) rename src/loaders/{RepositoryLoader.ts => ShuttleRepositoryLoader.ts} (94%) rename src/loaders/{TimedApiBasedRepositoryLoader.ts => TimedApiBasedShuttleRepositoryLoader.ts} (91%) rename src/loaders/{loadTestData.ts => loadShuttleTestData.ts} (99%) diff --git a/src/index.ts b/src/index.ts index 99e0f37..c2d04e4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,9 +4,9 @@ import { startStandaloneServer } from "@apollo/server/standalone"; import { MergedResolvers } from "./MergedResolvers"; import { ServerContext } from "./ServerContext"; import { UnoptimizedInMemoryShuttleRepository } from "./repositories/UnoptimizedInMemoryShuttleRepository"; -import { TimedApiBasedRepositoryLoader } from "./loaders/TimedApiBasedRepositoryLoader"; +import { TimedApiBasedShuttleRepositoryLoader } from "./loaders/TimedApiBasedShuttleRepositoryLoader"; import { ETANotificationScheduler } from "./notifications/schedulers/ETANotificationScheduler"; -import { loadTestData } from "./loaders/loadTestData"; +import { loadShuttleTestData } from "./loaders/loadShuttleTestData"; import { AppleNotificationSender } from "./notifications/senders/AppleNotificationSender"; const typeDefs = readFileSync("./schema.graphqls", "utf8"); @@ -22,11 +22,11 @@ async function main() { let notificationService: ETANotificationScheduler; if (process.argv.length > 2 && process.argv[2] == "integration-testing") { console.log("Using integration testing setup") - await loadTestData(repository); + await loadShuttleTestData(repository); const appleNotificationSender = new AppleNotificationSender(false); notificationService = new ETANotificationScheduler(repository, appleNotificationSender); } else { - const repositoryDataUpdater = new TimedApiBasedRepositoryLoader( + const repositoryDataUpdater = new TimedApiBasedShuttleRepositoryLoader( repository ); await repositoryDataUpdater.start(); diff --git a/src/loaders/ApiBasedRepositoryLoader.ts b/src/loaders/ApiBasedShuttleRepositoryLoader.ts similarity index 98% rename from src/loaders/ApiBasedRepositoryLoader.ts rename to src/loaders/ApiBasedShuttleRepositoryLoader.ts index aa4b078..b8a6285 100644 --- a/src/loaders/ApiBasedRepositoryLoader.ts +++ b/src/loaders/ApiBasedShuttleRepositoryLoader.ts @@ -1,6 +1,6 @@ import { ShuttleGetterSetterRepository } from "../repositories/ShuttleGetterSetterRepository"; import { IEntityWithId, IEta, IRoute, IShuttle, IStop, ISystem } from "../entities/entities"; -import { RepositoryLoader } from "./RepositoryLoader"; +import { ShuttleRepositoryLoader } from "./ShuttleRepositoryLoader"; export class ApiResponseError extends Error { constructor(message: string) { @@ -14,7 +14,7 @@ export class ApiResponseError extends Error { * Passio Go API. Supports automatic pruning of all data types * which inherit from `IEntityWithId`. */ -export class ApiBasedRepositoryLoader implements RepositoryLoader { +export class ApiBasedShuttleRepositoryLoader implements ShuttleRepositoryLoader { supportedSystemIds = ["263"]; baseUrl = "https://passiogo.com/mapGetData.php"; diff --git a/src/loaders/RepositoryLoader.ts b/src/loaders/ShuttleRepositoryLoader.ts similarity index 94% rename from src/loaders/RepositoryLoader.ts rename to src/loaders/ShuttleRepositoryLoader.ts index 5e34c49..9ffa137 100644 --- a/src/loaders/RepositoryLoader.ts +++ b/src/loaders/ShuttleRepositoryLoader.ts @@ -1,4 +1,4 @@ -export interface RepositoryLoader { +export interface ShuttleRepositoryLoader { fetchAndUpdateSystemData(): Promise; fetchAndUpdateRouteDataForExistingSystemsInRepository(): Promise; fetchAndUpdateRouteDataForSystemId(systemId: string): Promise; diff --git a/src/loaders/TimedApiBasedRepositoryLoader.ts b/src/loaders/TimedApiBasedShuttleRepositoryLoader.ts similarity index 91% rename from src/loaders/TimedApiBasedRepositoryLoader.ts rename to src/loaders/TimedApiBasedShuttleRepositoryLoader.ts index 8c830ba..1d15273 100644 --- a/src/loaders/TimedApiBasedRepositoryLoader.ts +++ b/src/loaders/TimedApiBasedShuttleRepositoryLoader.ts @@ -1,5 +1,5 @@ import { ShuttleGetterSetterRepository } from "../repositories/ShuttleGetterSetterRepository"; -import { ApiBasedRepositoryLoader } from "./ApiBasedRepositoryLoader"; +import { ApiBasedShuttleRepositoryLoader } from "./ApiBasedShuttleRepositoryLoader"; // Ideas to break this into smaller pieces in the future: // Have one repository data loader running for each supported system @@ -15,7 +15,7 @@ import { ApiBasedRepositoryLoader } from "./ApiBasedRepositoryLoader"; // - OrderedStops: reload every few minutes // - Systems: reload once a day -export class TimedApiBasedRepositoryLoader extends ApiBasedRepositoryLoader { +export class TimedApiBasedShuttleRepositoryLoader extends ApiBasedShuttleRepositoryLoader { private shouldBeRunning: boolean = false; private timer: any; diff --git a/src/loaders/loadTestData.ts b/src/loaders/loadShuttleTestData.ts similarity index 99% rename from src/loaders/loadTestData.ts rename to src/loaders/loadShuttleTestData.ts index 013379a..76cce74 100644 --- a/src/loaders/loadTestData.ts +++ b/src/loaders/loadShuttleTestData.ts @@ -4454,7 +4454,7 @@ const etas: IEta[] = [ } ]; -export async function loadTestData(repository: ShuttleGetterSetterRepository) { +export async function loadShuttleTestData(repository: ShuttleGetterSetterRepository) { await Promise.all(systems.map(async (system) => { await repository.addOrUpdateSystem(system); })); diff --git a/test/loaders/ApiBasedRepositoryLoaderTests.test.ts b/test/loaders/ApiBasedRepositoryLoaderTests.test.ts index 8e857c8..cead5ec 100644 --- a/test/loaders/ApiBasedRepositoryLoaderTests.test.ts +++ b/test/loaders/ApiBasedRepositoryLoaderTests.test.ts @@ -1,5 +1,5 @@ import { beforeEach, describe, expect, it, jest, test } from "@jest/globals"; -import { ApiBasedRepositoryLoader, ApiResponseError } from "../../src/loaders/ApiBasedRepositoryLoader"; +import { ApiBasedShuttleRepositoryLoader, ApiResponseError } from "../../src/loaders/ApiBasedShuttleRepositoryLoader"; import { UnoptimizedInMemoryShuttleRepository } from "../../src/repositories/UnoptimizedInMemoryShuttleRepository"; import { fetchSystemDataSuccessfulResponse } from "../jsonSnapshots/fetchSystemData/fetchSystemDataSuccessfulResponse"; import { fetchSystemDataFailedResponse } from "../jsonSnapshots/fetchSystemData/fetchSystemDataFailedResponse"; @@ -23,10 +23,10 @@ async function assertAsyncCallbackThrowsApiResponseError(callback: () => Promise } describe("ApiBasedRepositoryLoader", () => { - let loader: ApiBasedRepositoryLoader; + let loader: ApiBasedShuttleRepositoryLoader; beforeEach(() => { - loader = new ApiBasedRepositoryLoader(new UnoptimizedInMemoryShuttleRepository()); + loader = new ApiBasedShuttleRepositoryLoader(new UnoptimizedInMemoryShuttleRepository()); resetGlobalFetchMockJson(); }); diff --git a/test/loaders/TimedApiBasedRepositoryLoaderTests.test.ts b/test/loaders/TimedApiBasedRepositoryLoaderTests.test.ts index 602663e..3c81887 100644 --- a/test/loaders/TimedApiBasedRepositoryLoaderTests.test.ts +++ b/test/loaders/TimedApiBasedRepositoryLoaderTests.test.ts @@ -1,10 +1,10 @@ import { afterEach, beforeAll, beforeEach, describe, expect, it, jest } from "@jest/globals"; -import { TimedApiBasedRepositoryLoader } from "../../src/loaders/TimedApiBasedRepositoryLoader"; +import { TimedApiBasedShuttleRepositoryLoader } from "../../src/loaders/TimedApiBasedShuttleRepositoryLoader"; import { resetGlobalFetchMockJson } from "../testHelpers/fetchMockHelpers"; import { UnoptimizedInMemoryShuttleRepository } from "../../src/repositories/UnoptimizedInMemoryShuttleRepository"; describe("TimedApiBasedRepositoryLoader", () => { - let loader: TimedApiBasedRepositoryLoader; + let loader: TimedApiBasedShuttleRepositoryLoader; let spies: any; beforeAll(() => { @@ -15,7 +15,7 @@ describe("TimedApiBasedRepositoryLoader", () => { beforeEach(() => { resetGlobalFetchMockJson(); - loader = new TimedApiBasedRepositoryLoader(new UnoptimizedInMemoryShuttleRepository()); + loader = new TimedApiBasedShuttleRepositoryLoader(new UnoptimizedInMemoryShuttleRepository()); spies = { fetchAndUpdateSystemData: jest.spyOn(loader, 'fetchAndUpdateSystemData'), From 60b626b64f7fd9ab98bbca1c911d546db3ca2344 Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Thu, 27 Mar 2025 09:35:05 -0700 Subject: [PATCH 08/33] also rename the tests --- ...Tests.test.ts => ApiBasedShuttleRepositoryLoaderTests.test.ts} | 0 ....test.ts => TimedApiBasedShuttleRepositoryLoaderTests.test.ts} | 0 ....test.ts => UnoptimizedInMemoryShuttleRepositoryTests.test.ts} | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename test/loaders/{ApiBasedRepositoryLoaderTests.test.ts => ApiBasedShuttleRepositoryLoaderTests.test.ts} (100%) rename test/loaders/{TimedApiBasedRepositoryLoaderTests.test.ts => TimedApiBasedShuttleRepositoryLoaderTests.test.ts} (100%) rename test/repositories/{UnoptimizedInMemoryRepositoryTests.test.ts => UnoptimizedInMemoryShuttleRepositoryTests.test.ts} (100%) diff --git a/test/loaders/ApiBasedRepositoryLoaderTests.test.ts b/test/loaders/ApiBasedShuttleRepositoryLoaderTests.test.ts similarity index 100% rename from test/loaders/ApiBasedRepositoryLoaderTests.test.ts rename to test/loaders/ApiBasedShuttleRepositoryLoaderTests.test.ts diff --git a/test/loaders/TimedApiBasedRepositoryLoaderTests.test.ts b/test/loaders/TimedApiBasedShuttleRepositoryLoaderTests.test.ts similarity index 100% rename from test/loaders/TimedApiBasedRepositoryLoaderTests.test.ts rename to test/loaders/TimedApiBasedShuttleRepositoryLoaderTests.test.ts diff --git a/test/repositories/UnoptimizedInMemoryRepositoryTests.test.ts b/test/repositories/UnoptimizedInMemoryShuttleRepositoryTests.test.ts similarity index 100% rename from test/repositories/UnoptimizedInMemoryRepositoryTests.test.ts rename to test/repositories/UnoptimizedInMemoryShuttleRepositoryTests.test.ts From 09be37cedb9b5632633268b26d63967f438b6b7a Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Thu, 27 Mar 2025 09:57:49 -0700 Subject: [PATCH 09/33] move arguments and notification interfaces to notification repository file --- .../schedulers/ETANotificationScheduler.ts | 30 +++++------------ src/repositories/NotificationRepository.ts | 33 +++++++++++++++++++ src/resolvers/MutationResolvers.ts | 2 +- test/resolvers/QueryResolverTests.test.ts | 2 +- 4 files changed, 44 insertions(+), 23 deletions(-) create mode 100644 src/repositories/NotificationRepository.ts diff --git a/src/notifications/schedulers/ETANotificationScheduler.ts b/src/notifications/schedulers/ETANotificationScheduler.ts index 3d270dd..85e911f 100644 --- a/src/notifications/schedulers/ETANotificationScheduler.ts +++ b/src/notifications/schedulers/ETANotificationScheduler.ts @@ -2,22 +2,10 @@ import { ShuttleGetterRepository } from "../../repositories/ShuttleGetterReposit import { TupleKey } from "../../types/TupleKey"; import { IEta } from "../../entities/entities"; import { AppleNotificationSender, NotificationAlertArguments } from "../senders/AppleNotificationSender"; - -export interface NotificationLookupArguments { - deviceId: string; - shuttleId: string; - stopId: string; -} - -export interface NotificationSchedulingArguments extends NotificationLookupArguments { - /** - * Value which specifies the ETA of the shuttle for when - * the notification should fire. - * For example, a secondsThreshold of 180 would mean that the notification - * fires when the ETA drops below 3 minutes. - */ - secondsThreshold: number; -} +import { + NotificationLookupArguments, + ScheduledNotification +} from "../../repositories/NotificationRepository"; type DeviceIdSecondsThresholdAssociation = { [key: string]: number }; @@ -47,7 +35,7 @@ export class ETANotificationScheduler { */ private deviceIdsToDeliverTo: { [key: string]: DeviceIdSecondsThresholdAssociation } = {} - private async sendEtaNotificationImmediately(notificationData: NotificationSchedulingArguments): Promise { + private async sendEtaNotificationImmediately(notificationData: ScheduledNotification): Promise { const { deviceId, shuttleId, stopId } = notificationData; const shuttle = await this.shuttleRepository.getShuttleById(shuttleId); @@ -85,7 +73,7 @@ export class ETANotificationScheduler { const deviceIdsToRemove = new Set(); for (let deviceId of Object.keys(this.deviceIdsToDeliverTo[tupleKey])) { - const scheduledNotificationData: NotificationSchedulingArguments = { + const scheduledNotificationData: ScheduledNotification = { deviceId, secondsThreshold: this.deviceIdsToDeliverTo[tupleKey][deviceId], shuttleId: eta.shuttleId, @@ -103,7 +91,7 @@ export class ETANotificationScheduler { }); } - private async sendEtaNotificationImmediatelyIfSecondsRemainingBelowThreshold(notificationObject: NotificationSchedulingArguments, etaSecondsRemaining: number) { + private async sendEtaNotificationImmediatelyIfSecondsRemainingBelowThreshold(notificationObject: ScheduledNotification, etaSecondsRemaining: number) { if (etaSecondsRemaining > notificationObject.secondsThreshold) { return false; } @@ -119,7 +107,7 @@ export class ETANotificationScheduler { * @param secondsThreshold Value which specifies the ETA of the shuttle for when * the notification should fire. */ - public async scheduleNotification({ deviceId, shuttleId, stopId, secondsThreshold }: NotificationSchedulingArguments) { + public async scheduleNotification({ deviceId, shuttleId, stopId, secondsThreshold }: ScheduledNotification) { const tuple = new TupleKey(shuttleId, stopId); if (this.deviceIdsToDeliverTo[tuple.toString()] === undefined) { this.deviceIdsToDeliverTo[tuple.toString()] = {}; @@ -169,7 +157,7 @@ export class ETANotificationScheduler { * @param deviceId */ public async getAllScheduledNotificationsForDevice(deviceId: string): Promise { - const scheduledNotifications: NotificationSchedulingArguments[] = []; + const scheduledNotifications: ScheduledNotification[] = []; for (const key of Object.keys(this.deviceIdsToDeliverTo)) { if (deviceId in this.deviceIdsToDeliverTo[key]) { diff --git a/src/repositories/NotificationRepository.ts b/src/repositories/NotificationRepository.ts new file mode 100644 index 0000000..e37256e --- /dev/null +++ b/src/repositories/NotificationRepository.ts @@ -0,0 +1,33 @@ +export interface NotificationLookupArguments { + deviceId: string; + shuttleId: string; + stopId: string; +} + +export interface ScheduledNotification extends NotificationLookupArguments { + /** + * Value which specifies the ETA of the shuttle for when + * the notification should fire. + * For example, a secondsThreshold of 180 would mean that the notification + * fires when the ETA drops below 3 minutes. + */ + secondsThreshold: number; +} + +export class NotificationRepository { + public async getAllNotificationsForShuttleAndStopId(shuttleId: string, stopId: string) { + + } + + public async getSecondsThresholdForNotificationIfExists(lookupArguments: NotificationLookupArguments) { + + } + + public async addNotification(notification: ScheduledNotification) { + + } + + public async deleteNotification(lookupArguments: NotificationLookupArguments) { + + } +} diff --git a/src/resolvers/MutationResolvers.ts b/src/resolvers/MutationResolvers.ts index ac8e1ea..f512ed5 100644 --- a/src/resolvers/MutationResolvers.ts +++ b/src/resolvers/MutationResolvers.ts @@ -2,7 +2,7 @@ import { NotificationResponse, Resolvers } from "../generated/graphql"; import { ServerContext } from "../ServerContext"; import { ETANotificationScheduler, - NotificationSchedulingArguments + ScheduledNotification } from "../notifications/schedulers/ETANotificationScheduler"; export const MutationResolvers: Resolvers = { diff --git a/test/resolvers/QueryResolverTests.test.ts b/test/resolvers/QueryResolverTests.test.ts index 00007a7..dc36f79 100644 --- a/test/resolvers/QueryResolverTests.test.ts +++ b/test/resolvers/QueryResolverTests.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from "@jest/globals"; import { generateMockSystems } from "../testHelpers/mockDataGenerators"; import { setupTestServerContext, setupTestServerHolder } from "../testHelpers/apolloTestServerHelpers"; import assert = require("node:assert"); -import { NotificationSchedulingArguments } from "../../src/notifications/schedulers/ETANotificationScheduler"; +import { ScheduledNotification } from "../../src/notifications/schedulers/ETANotificationScheduler"; import { addMockShuttleToRepository, addMockStopToRepository } from "../testHelpers/repositorySetupHelpers"; // See Apollo documentation for integration test guide From c517d93e3a873febe27b689a7e91e54a2a8b1269 Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Thu, 27 Mar 2025 10:02:13 -0700 Subject: [PATCH 10/33] add test cases and rename some methods --- src/repositories/NotificationRepository.ts | 4 +- .../NotificationRepositoryTests.test.ts | 43 +++++++++++++++++++ 2 files changed, 45 insertions(+), 2 deletions(-) create mode 100644 test/repositories/NotificationRepositoryTests.test.ts diff --git a/src/repositories/NotificationRepository.ts b/src/repositories/NotificationRepository.ts index e37256e..7f74914 100644 --- a/src/repositories/NotificationRepository.ts +++ b/src/repositories/NotificationRepository.ts @@ -23,11 +23,11 @@ export class NotificationRepository { } - public async addNotification(notification: ScheduledNotification) { + public async addOrUpdateNotification(notification: ScheduledNotification) { } - public async deleteNotification(lookupArguments: NotificationLookupArguments) { + public async deleteNotificationIfExists(lookupArguments: NotificationLookupArguments) { } } diff --git a/test/repositories/NotificationRepositoryTests.test.ts b/test/repositories/NotificationRepositoryTests.test.ts new file mode 100644 index 0000000..9bbe38c --- /dev/null +++ b/test/repositories/NotificationRepositoryTests.test.ts @@ -0,0 +1,43 @@ +import { describe, it } from "@jest/globals"; + +describe("NotificationRepository", () => { + describe("getAllNotificationsForShuttleAndStopId", () => { + it("gets notifications correctly", async () => { + + }); + + it("returns empty array if no notifications", async () => { + + }); + }); + + describe("getSecondsThresholdForNotificationIfExists", () => { + it("gets the seconds threshold if exists", async () => { + + }); + + it("returns null if there is no seconds threshold", async () => { + + }); + }); + + describe("addOrUpdateNotification", () => { + it("adds the notification", async () => { + + }); + + it("updates the seconds threshold if the notification exists already", async () => { + + }); + }); + + describe("deleteNotificationIfExists", () => { + it("deletes the notification", async () => { + + }); + + it("does nothing if there's no notification", async () => { + + }); + }); +}); From 7379840070328f8cedcc461816c1c328e28dddb4 Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Thu, 27 Mar 2025 10:06:42 -0700 Subject: [PATCH 11/33] extract notification repository to interface --- .../schedulers/ETANotificationScheduler.ts | 5 +---- .../InMemoryNotificationRepository.ts | 19 +++++++++++++++++ src/repositories/NotificationRepository.ts | 21 +++++-------------- src/resolvers/MutationResolvers.ts | 4 ++-- .../NotificationRepositoryTests.test.ts | 2 +- 5 files changed, 28 insertions(+), 23 deletions(-) create mode 100644 src/repositories/InMemoryNotificationRepository.ts diff --git a/src/notifications/schedulers/ETANotificationScheduler.ts b/src/notifications/schedulers/ETANotificationScheduler.ts index 85e911f..0319cf7 100644 --- a/src/notifications/schedulers/ETANotificationScheduler.ts +++ b/src/notifications/schedulers/ETANotificationScheduler.ts @@ -2,10 +2,7 @@ import { ShuttleGetterRepository } from "../../repositories/ShuttleGetterReposit import { TupleKey } from "../../types/TupleKey"; import { IEta } from "../../entities/entities"; import { AppleNotificationSender, NotificationAlertArguments } from "../senders/AppleNotificationSender"; -import { - NotificationLookupArguments, - ScheduledNotification -} from "../../repositories/NotificationRepository"; +import { NotificationLookupArguments, ScheduledNotification } from "../../repositories/NotificationRepository"; type DeviceIdSecondsThresholdAssociation = { [key: string]: number }; diff --git a/src/repositories/InMemoryNotificationRepository.ts b/src/repositories/InMemoryNotificationRepository.ts new file mode 100644 index 0000000..c660ca7 --- /dev/null +++ b/src/repositories/InMemoryNotificationRepository.ts @@ -0,0 +1,19 @@ +import { NotificationLookupArguments, NotificationRepository, ScheduledNotification } from "./NotificationRepository"; + +export class InMemoryNotificationRepository implements NotificationRepository { + async getAllNotificationsForShuttleAndStopId(shuttleId: string, stopId: string) { + return []; + } + + async getSecondsThresholdForNotificationIfExists(lookupArguments: NotificationLookupArguments) { + return 0; + } + + async addOrUpdateNotification(notification: ScheduledNotification) { + + } + + async deleteNotificationIfExists(lookupArguments: NotificationLookupArguments) { + + } +} diff --git a/src/repositories/NotificationRepository.ts b/src/repositories/NotificationRepository.ts index 7f74914..3787f3d 100644 --- a/src/repositories/NotificationRepository.ts +++ b/src/repositories/NotificationRepository.ts @@ -14,20 +14,9 @@ export interface ScheduledNotification extends NotificationLookupArguments { secondsThreshold: number; } -export class NotificationRepository { - public async getAllNotificationsForShuttleAndStopId(shuttleId: string, stopId: string) { - - } - - public async getSecondsThresholdForNotificationIfExists(lookupArguments: NotificationLookupArguments) { - - } - - public async addOrUpdateNotification(notification: ScheduledNotification) { - - } - - public async deleteNotificationIfExists(lookupArguments: NotificationLookupArguments) { - - } +export interface NotificationRepository { + getAllNotificationsForShuttleAndStopId(shuttleId: string, stopId: string): Promise; + getSecondsThresholdForNotificationIfExists(lookupArguments: NotificationLookupArguments): Promise; + addOrUpdateNotification(notification: ScheduledNotification): Promise; + deleteNotificationIfExists(lookupArguments: NotificationLookupArguments): Promise; } diff --git a/src/resolvers/MutationResolvers.ts b/src/resolvers/MutationResolvers.ts index f512ed5..a840817 100644 --- a/src/resolvers/MutationResolvers.ts +++ b/src/resolvers/MutationResolvers.ts @@ -2,8 +2,8 @@ import { NotificationResponse, Resolvers } from "../generated/graphql"; import { ServerContext } from "../ServerContext"; import { ETANotificationScheduler, - ScheduledNotification } from "../notifications/schedulers/ETANotificationScheduler"; +import { ScheduledNotification } from "../repositories/NotificationRepository"; export const MutationResolvers: Resolvers = { Mutation: { @@ -23,7 +23,7 @@ export const MutationResolvers: Resolvers = { } } - const notificationData: NotificationSchedulingArguments = { + const notificationData: ScheduledNotification = { ...args.input, secondsThreshold: typeof args.input.secondsThreshold === 'number' ? args.input.secondsThreshold diff --git a/test/repositories/NotificationRepositoryTests.test.ts b/test/repositories/NotificationRepositoryTests.test.ts index 9bbe38c..3d4f18b 100644 --- a/test/repositories/NotificationRepositoryTests.test.ts +++ b/test/repositories/NotificationRepositoryTests.test.ts @@ -1,6 +1,6 @@ import { describe, it } from "@jest/globals"; -describe("NotificationRepository", () => { +describe("InMemoryNotificationRepository", () => { describe("getAllNotificationsForShuttleAndStopId", () => { it("gets notifications correctly", async () => { From 101c5ca6e0d8f39aebd1260b50f1822ecc78c7e7 Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Thu, 27 Mar 2025 10:07:11 -0700 Subject: [PATCH 12/33] change name of test for in-memory notification repository --- ...yTests.test.ts => InMemoryNotificationRepositoryTests.test.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename test/repositories/{NotificationRepositoryTests.test.ts => InMemoryNotificationRepositoryTests.test.ts} (100%) diff --git a/test/repositories/NotificationRepositoryTests.test.ts b/test/repositories/InMemoryNotificationRepositoryTests.test.ts similarity index 100% rename from test/repositories/NotificationRepositoryTests.test.ts rename to test/repositories/InMemoryNotificationRepositoryTests.test.ts From ead401a3b12f44218a2f0ce8422ec573bce8d1fc Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Thu, 27 Mar 2025 10:11:41 -0700 Subject: [PATCH 13/33] add tests for in-memory notification repository --- ...nMemoryNotificationRepositoryTests.test.ts | 63 ++++++++++++++++--- 1 file changed, 56 insertions(+), 7 deletions(-) diff --git a/test/repositories/InMemoryNotificationRepositoryTests.test.ts b/test/repositories/InMemoryNotificationRepositoryTests.test.ts index 3d4f18b..20ce245 100644 --- a/test/repositories/InMemoryNotificationRepositoryTests.test.ts +++ b/test/repositories/InMemoryNotificationRepositoryTests.test.ts @@ -1,43 +1,92 @@ -import { describe, it } from "@jest/globals"; +import { beforeEach, describe, expect, it } from "@jest/globals"; +import { InMemoryNotificationRepository } from "../../src/repositories/InMemoryNotificationRepository"; describe("InMemoryNotificationRepository", () => { + let repo: InMemoryNotificationRepository; + + beforeEach(() => { + repo = new InMemoryNotificationRepository(); + }) + + const notification = { + deviceId: "device1", + shuttleId: "shuttle1", + stopId: "stop1", + secondsThreshold: 180 + }; + describe("getAllNotificationsForShuttleAndStopId", () => { it("gets notifications correctly", async () => { + await repo.addOrUpdateNotification(notification); + const result = await repo.getAllNotificationsForShuttleAndStopId("shuttle1", "stop1"); + expect(result).toHaveLength(1); + expect(result[0]).toEqual(notification); }); it("returns empty array if no notifications", async () => { - + const result = await repo.getAllNotificationsForShuttleAndStopId("shuttle1", "stop1"); + expect(result).toEqual([]); }); }); describe("getSecondsThresholdForNotificationIfExists", () => { it("gets the seconds threshold if exists", async () => { + await repo.addOrUpdateNotification(notification); + const result = await repo.getSecondsThresholdForNotificationIfExists({ + deviceId: "device1", + shuttleId: "shuttle1", + stopId: "stop1" + }); + expect(result).toBe(180); }); it("returns null if there is no seconds threshold", async () => { - + const result = await repo.getSecondsThresholdForNotificationIfExists({ + deviceId: "device1", + shuttleId: "shuttle1", + stopId: "stop1" + }); + expect(result).toBeNull(); }); }); describe("addOrUpdateNotification", () => { - it("adds the notification", async () => { - - }); + // Add/get flow is covered in getAllNotificationsForShuttleAndStopId it("updates the seconds threshold if the notification exists already", async () => { + await repo.addOrUpdateNotification(notification); + await repo.addOrUpdateNotification({...notification, secondsThreshold: 300}); + const result = await repo.getSecondsThresholdForNotificationIfExists({ + deviceId: "device1", + shuttleId: "shuttle1", + stopId: "stop1" + }); + expect(result).toBe(300); }); }); describe("deleteNotificationIfExists", () => { it("deletes the notification", async () => { + await repo.addOrUpdateNotification(notification); + await repo.deleteNotificationIfExists({ + deviceId: "device1", + shuttleId: "shuttle1", + stopId: "stop1" + }); + const result = await repo.getAllNotificationsForShuttleAndStopId("shuttle1", "stop1"); + expect(result).toHaveLength(0); }); it("does nothing if there's no notification", async () => { - + await expect(repo.deleteNotificationIfExists({ + deviceId: "device1", + shuttleId: "shuttle1", + stopId: "stop1" + })).resolves.not.toThrow(); }); }); }); From f007b72d94563be1803c97cfcb717bfe65a8e5cf Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Thu, 27 Mar 2025 10:19:00 -0700 Subject: [PATCH 14/33] add code for in-memory notification repository --- .../schedulers/ETANotificationScheduler.ts | 5 -- .../InMemoryNotificationRepository.ts | 65 +++++++++++++++++-- src/repositories/NotificationRepository.ts | 2 +- 3 files changed, 61 insertions(+), 11 deletions(-) diff --git a/src/notifications/schedulers/ETANotificationScheduler.ts b/src/notifications/schedulers/ETANotificationScheduler.ts index 0319cf7..385d350 100644 --- a/src/notifications/schedulers/ETANotificationScheduler.ts +++ b/src/notifications/schedulers/ETANotificationScheduler.ts @@ -16,11 +16,6 @@ export class ETANotificationScheduler { this.sendEtaNotificationImmediately = this.sendEtaNotificationImmediately.bind(this); this.etaSubscriberCallback = this.etaSubscriberCallback.bind(this); this.sendEtaNotificationImmediatelyIfSecondsRemainingBelowThreshold = this.sendEtaNotificationImmediatelyIfSecondsRemainingBelowThreshold.bind(this); - this.scheduleNotification = this.scheduleNotification.bind(this); - this.cancelNotificationIfExists = this.cancelNotificationIfExists.bind(this); - this.isNotificationScheduled = this.isNotificationScheduled.bind(this); - this.getSecondsThresholdForScheduledNotification = this.getSecondsThresholdForScheduledNotification.bind(this); - this.getAllScheduledNotificationsForDevice = this.getAllScheduledNotificationsForDevice.bind(this); } /** diff --git a/src/repositories/InMemoryNotificationRepository.ts b/src/repositories/InMemoryNotificationRepository.ts index c660ca7..db6e9bc 100644 --- a/src/repositories/InMemoryNotificationRepository.ts +++ b/src/repositories/InMemoryNotificationRepository.ts @@ -1,19 +1,74 @@ import { NotificationLookupArguments, NotificationRepository, ScheduledNotification } from "./NotificationRepository"; +import { TupleKey } from "../types/TupleKey"; + +type DeviceIdSecondsThresholdAssociation = { [key: string]: number }; export class InMemoryNotificationRepository implements NotificationRepository { + /** + * 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`. + * The value is a dictionary of the device ID to the stored seconds threshold. + * @private + */ + private deviceIdsToDeliverTo: { [key: string]: DeviceIdSecondsThresholdAssociation } = {} + async getAllNotificationsForShuttleAndStopId(shuttleId: string, stopId: string) { - return []; + const tuple = new TupleKey(shuttleId, stopId); + if (this.deviceIdsToDeliverTo[tuple.toString()] === undefined) { + return []; + } + + return Object.keys(this.deviceIdsToDeliverTo[tuple.toString()]) + .map((deviceId) => { + return { + shuttleId, + stopId, + deviceId, + secondsThreshold: this.deviceIdsToDeliverTo[tuple.toString()][deviceId] + } + }); } - async getSecondsThresholdForNotificationIfExists(lookupArguments: NotificationLookupArguments) { - return 0; + async getSecondsThresholdForNotificationIfExists({ + shuttleId, + stopId, + deviceId + }: NotificationLookupArguments) { + const tuple = new TupleKey(shuttleId, stopId); + if (this.deviceIdsToDeliverTo[tuple.toString()] === undefined) { + return null; + } + return this.deviceIdsToDeliverTo[tuple.toString()][deviceId]; } - async addOrUpdateNotification(notification: ScheduledNotification) { + async addOrUpdateNotification({ + shuttleId, + stopId, + deviceId, + secondsThreshold + }: ScheduledNotification) { + const tuple = new TupleKey(shuttleId, stopId); + if (this.deviceIdsToDeliverTo[tuple.toString()] === undefined) { + this.deviceIdsToDeliverTo[tuple.toString()] = {}; + } + this.deviceIdsToDeliverTo[tuple.toString()][deviceId] = secondsThreshold; } - async deleteNotificationIfExists(lookupArguments: NotificationLookupArguments) { + async deleteNotificationIfExists({ + deviceId, + shuttleId, + stopId + }: NotificationLookupArguments) { + const tupleKey = new TupleKey(shuttleId, stopId); + if ( + this.deviceIdsToDeliverTo[tupleKey.toString()] === undefined + || !(deviceId in this.deviceIdsToDeliverTo[tupleKey.toString()]) + ) { + return; + } + delete this.deviceIdsToDeliverTo[tupleKey.toString()][deviceId]; } } diff --git a/src/repositories/NotificationRepository.ts b/src/repositories/NotificationRepository.ts index 3787f3d..343497e 100644 --- a/src/repositories/NotificationRepository.ts +++ b/src/repositories/NotificationRepository.ts @@ -16,7 +16,7 @@ export interface ScheduledNotification extends NotificationLookupArguments { export interface NotificationRepository { getAllNotificationsForShuttleAndStopId(shuttleId: string, stopId: string): Promise; - getSecondsThresholdForNotificationIfExists(lookupArguments: NotificationLookupArguments): Promise; + getSecondsThresholdForNotificationIfExists(lookupArguments: NotificationLookupArguments): Promise; addOrUpdateNotification(notification: ScheduledNotification): Promise; deleteNotificationIfExists(lookupArguments: NotificationLookupArguments): Promise; } From fb58414ba3955c33d47c1a13937d07e230e2a47e Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Thu, 27 Mar 2025 10:20:25 -0700 Subject: [PATCH 15/33] add notification repository as optional dependency --- .../schedulers/ETANotificationScheduler.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/notifications/schedulers/ETANotificationScheduler.ts b/src/notifications/schedulers/ETANotificationScheduler.ts index 385d350..0f875af 100644 --- a/src/notifications/schedulers/ETANotificationScheduler.ts +++ b/src/notifications/schedulers/ETANotificationScheduler.ts @@ -2,15 +2,22 @@ import { ShuttleGetterRepository } from "../../repositories/ShuttleGetterReposit import { TupleKey } from "../../types/TupleKey"; import { IEta } from "../../entities/entities"; import { AppleNotificationSender, NotificationAlertArguments } from "../senders/AppleNotificationSender"; -import { NotificationLookupArguments, ScheduledNotification } from "../../repositories/NotificationRepository"; +import { + NotificationLookupArguments, + NotificationRepository, + ScheduledNotification +} from "../../repositories/NotificationRepository"; +import { InMemoryNotificationRepository } from "../../repositories/InMemoryNotificationRepository"; type DeviceIdSecondsThresholdAssociation = { [key: string]: number }; export class ETANotificationScheduler { public static readonly defaultSecondsThresholdForNotificationToFire = 180; - constructor(private shuttleRepository: ShuttleGetterRepository, - private appleNotificationSender = new AppleNotificationSender() + constructor( + private shuttleRepository: ShuttleGetterRepository, + private notificationRepository: NotificationRepository = new InMemoryNotificationRepository(), + private appleNotificationSender = new AppleNotificationSender() ) { this.etaSubscriberCallback = this.etaSubscriberCallback.bind(this); this.sendEtaNotificationImmediately = this.sendEtaNotificationImmediately.bind(this); From ae30660095c76b7720e2d8db3200b321be6db151 Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Thu, 27 Mar 2025 10:23:30 -0700 Subject: [PATCH 16/33] fix constructors --- src/index.ts | 9 +++- .../ETANotificationSchedulerTests.test.ts | 42 ++++--------------- 2 files changed, 15 insertions(+), 36 deletions(-) diff --git a/src/index.ts b/src/index.ts index c2d04e4..3880fac 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,6 +8,7 @@ import { TimedApiBasedShuttleRepositoryLoader } from "./loaders/TimedApiBasedShu import { ETANotificationScheduler } from "./notifications/schedulers/ETANotificationScheduler"; import { loadShuttleTestData } from "./loaders/loadShuttleTestData"; import { AppleNotificationSender } from "./notifications/senders/AppleNotificationSender"; +import { InMemoryNotificationRepository } from "./repositories/InMemoryNotificationRepository"; const typeDefs = readFileSync("./schema.graphqls", "utf8"); @@ -23,8 +24,14 @@ async function main() { if (process.argv.length > 2 && process.argv[2] == "integration-testing") { console.log("Using integration testing setup") await loadShuttleTestData(repository); + const appleNotificationSender = new AppleNotificationSender(false); - notificationService = new ETANotificationScheduler(repository, appleNotificationSender); + const inMemoryNotificationRepository = new InMemoryNotificationRepository(); + notificationService = new ETANotificationScheduler( + repository, + inMemoryNotificationRepository, + appleNotificationSender + ); } else { const repositoryDataUpdater = new TimedApiBasedShuttleRepositoryLoader( repository diff --git a/test/notifications/schedulers/ETANotificationSchedulerTests.test.ts b/test/notifications/schedulers/ETANotificationSchedulerTests.test.ts index afc6f85..392844e 100644 --- a/test/notifications/schedulers/ETANotificationSchedulerTests.test.ts +++ b/test/notifications/schedulers/ETANotificationSchedulerTests.test.ts @@ -5,6 +5,7 @@ import http2 from "http2"; import { IEta, IShuttle, IStop } from "../../../src/entities/entities"; import { addMockShuttleToRepository, addMockStopToRepository } from "../../testHelpers/repositorySetupHelpers"; import { AppleNotificationSender } from "../../../src/notifications/senders/AppleNotificationSender"; +import { InMemoryNotificationRepository } from "../../../src/repositories/InMemoryNotificationRepository"; jest.mock("http2"); jest.mock("../../../src/notifications/senders/AppleNotificationSender"); @@ -51,7 +52,11 @@ describe("ETANotificationScheduler", () => { mockNotificationSenderMethods(true); const appleNotificationSender = new MockAppleNotificationSender(false); - notificationService = new ETANotificationScheduler(repository, appleNotificationSender); + notificationService = new ETANotificationScheduler( + repository, + new InMemoryNotificationRepository(), + appleNotificationSender + ); }); function generateNotificationDataAndEta(shuttle: IShuttle, stop: IStop) { @@ -141,6 +146,7 @@ describe("ETANotificationScheduler", () => { mockNotificationSenderMethods(false); notificationService = new ETANotificationScheduler( repository, + new InMemoryNotificationRepository(), new MockAppleNotificationSender(), ) @@ -176,38 +182,4 @@ describe("ETANotificationScheduler", () => { expect(http2.connect as jest.Mock).toHaveBeenCalledTimes(0); }); }); - - describe("getAllScheduledNotificationsForDevice", () => { - it("returns scheduled notifications for the device ID", async () => { - // Arrange - const shuttle1 = await addMockShuttleToRepository(repository, "1"); - const stop = await addMockStopToRepository(repository, "1"); - const { notificationData1 } = generateNotificationDataAndEta(shuttle1, stop); - await notificationService.scheduleNotification(notificationData1); - - const shuttle2 = { - ...shuttle1, - id: "2", - } - await repository.addOrUpdateShuttle(shuttle2); - - const notificationData2 = { - ...notificationData1, - shuttleId: shuttle2.id, - } - await notificationService.scheduleNotification(notificationData2); - - // Act - const notifications = await notificationService.getAllScheduledNotificationsForDevice(notificationData1.deviceId); - - // Assert - expect(notifications.length).toBe(2); - }); - - it("returns an empty array if there are no notifications", async () => { - // Act - const notifications = await notificationService.getAllScheduledNotificationsForDevice("1"); - expect(notifications.length).toBe(0); - }); - }); }); From bba00eb0672cc8b7b2d36fd81c8f7e57a5bb8f8a Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Thu, 27 Mar 2025 10:30:12 -0700 Subject: [PATCH 17/33] remove everything from the scheduler that's in the repository already --- .../schedulers/ETANotificationScheduler.ts | 120 ++---------------- 1 file changed, 12 insertions(+), 108 deletions(-) diff --git a/src/notifications/schedulers/ETANotificationScheduler.ts b/src/notifications/schedulers/ETANotificationScheduler.ts index 0f875af..bb820b9 100644 --- a/src/notifications/schedulers/ETANotificationScheduler.ts +++ b/src/notifications/schedulers/ETANotificationScheduler.ts @@ -1,16 +1,12 @@ import { ShuttleGetterRepository } from "../../repositories/ShuttleGetterRepository"; -import { TupleKey } from "../../types/TupleKey"; import { IEta } from "../../entities/entities"; import { AppleNotificationSender, NotificationAlertArguments } from "../senders/AppleNotificationSender"; import { - NotificationLookupArguments, NotificationRepository, ScheduledNotification } from "../../repositories/NotificationRepository"; import { InMemoryNotificationRepository } from "../../repositories/InMemoryNotificationRepository"; -type DeviceIdSecondsThresholdAssociation = { [key: string]: number }; - export class ETANotificationScheduler { public static readonly defaultSecondsThresholdForNotificationToFire = 180; @@ -25,15 +21,6 @@ export class ETANotificationScheduler { this.sendEtaNotificationImmediatelyIfSecondsRemainingBelowThreshold = this.sendEtaNotificationImmediatelyIfSecondsRemainingBelowThreshold.bind(this); } - /** - * 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`. - * The value is a dictionary of the device ID to the stored seconds threshold. - * @private - */ - private deviceIdsToDeliverTo: { [key: string]: DeviceIdSecondsThresholdAssociation } = {} - private async sendEtaNotificationImmediately(notificationData: ScheduledNotification): Promise { const { deviceId, shuttleId, stopId } = notificationData; @@ -64,29 +51,25 @@ export class ETANotificationScheduler { } private async etaSubscriberCallback(eta: IEta) { - const tuple = new TupleKey(eta.shuttleId, eta.stopId); - const tupleKey = tuple.toString(); - if (this.deviceIdsToDeliverTo[tupleKey] === undefined) { - return; - } - const deviceIdsToRemove = new Set(); - for (let deviceId of Object.keys(this.deviceIdsToDeliverTo[tupleKey])) { - const scheduledNotificationData: ScheduledNotification = { - deviceId, - secondsThreshold: this.deviceIdsToDeliverTo[tupleKey][deviceId], - shuttleId: eta.shuttleId, - stopId: eta.stopId, - } + const notifications = await this.notificationRepository.getAllNotificationsForShuttleAndStopId( + eta.shuttleId, + eta.stopId + ) - const deliveredSuccessfully = await this.sendEtaNotificationImmediatelyIfSecondsRemainingBelowThreshold(scheduledNotificationData, eta.secondsRemaining); + for (let notification of notifications) { + const deliveredSuccessfully = await this.sendEtaNotificationImmediatelyIfSecondsRemainingBelowThreshold(notification, eta.secondsRemaining); if (deliveredSuccessfully) { - deviceIdsToRemove.add(deviceId); + deviceIdsToRemove.add(notification.deviceId); } } deviceIdsToRemove.forEach((deviceId) => { - delete this.deviceIdsToDeliverTo[tupleKey][deviceId] + this.notificationRepository.deleteNotificationIfExists({ + shuttleId: eta.shuttleId, + stopId: eta.stopId, + deviceId, + }) }); } @@ -97,83 +80,4 @@ export class ETANotificationScheduler { return await this.sendEtaNotificationImmediately(notificationObject); } - - /** - * 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. - * @param secondsThreshold Value which specifies the ETA of the shuttle for when - * the notification should fire. - */ - public async scheduleNotification({ deviceId, shuttleId, stopId, secondsThreshold }: ScheduledNotification) { - const tuple = new TupleKey(shuttleId, stopId); - if (this.deviceIdsToDeliverTo[tuple.toString()] === undefined) { - this.deviceIdsToDeliverTo[tuple.toString()] = {}; - } - - this.deviceIdsToDeliverTo[tuple.toString()][deviceId] = secondsThreshold; - - this.shuttleRepository.unsubscribeFromEtaUpdates(this.etaSubscriberCallback); - this.shuttleRepository.subscribeToEtaUpdates(this.etaSubscriberCallback); - } - - /** - * 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 cancelNotificationIfExists({ deviceId, shuttleId, stopId }: NotificationLookupArguments) { - const tupleKey = new TupleKey(shuttleId, stopId); - if ( - this.deviceIdsToDeliverTo[tupleKey.toString()] === undefined - || !(deviceId in this.deviceIdsToDeliverTo[tupleKey.toString()]) - ) { - return; - } - - delete this.deviceIdsToDeliverTo[tupleKey.toString()][deviceId]; - } - - /** - * Check whether the notification is scheduled. - */ - public isNotificationScheduled(lookupArguments: NotificationLookupArguments): boolean { - return this.getSecondsThresholdForScheduledNotification(lookupArguments) != null; - } - - public getSecondsThresholdForScheduledNotification({ deviceId, shuttleId, stopId }: NotificationLookupArguments): number | null { - const tuple = new TupleKey(shuttleId, stopId); - if (this.deviceIdsToDeliverTo[tuple.toString()] === undefined) { - return null; - } - return this.deviceIdsToDeliverTo[tuple.toString()][deviceId]; - } - - /** - * Return all scheduled notification for the given device ID. - * @param deviceId - */ - public async getAllScheduledNotificationsForDevice(deviceId: string): Promise { - const scheduledNotifications: ScheduledNotification[] = []; - - for (const key of Object.keys(this.deviceIdsToDeliverTo)) { - if (deviceId in this.deviceIdsToDeliverTo[key]) { - const tupleKey = TupleKey.fromExistingStringKey(key); - const shuttleId = tupleKey.tuple[0] - const stopId = tupleKey.tuple[1]; - const secondsThreshold = this.deviceIdsToDeliverTo[key][deviceId]; - - scheduledNotifications.push({ - shuttleId, - stopId, - deviceId, - secondsThreshold, - }); - } - } - - return scheduledNotifications; - } } From bda46d680861d8266294dff64ce6ff47f1149d60 Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Thu, 27 Mar 2025 10:38:02 -0700 Subject: [PATCH 18/33] rename repository to server repository in server context --- src/ServerContext.ts | 2 +- src/resolvers/EtaResolvers.ts | 6 +-- src/resolvers/MutationResolvers.ts | 4 +- src/resolvers/OrderedStopResolvers.ts | 14 +++---- src/resolvers/QueryResolvers.ts | 4 +- src/resolvers/RouteResolvers.ts | 6 +-- src/resolvers/ShuttleResolvers.ts | 8 ++-- src/resolvers/StopResolvers.ts | 6 +-- src/resolvers/SystemResolvers.ts | 14 +++---- test/resolvers/EtaResolverTests.test.ts | 13 +++--- test/resolvers/MutationResolverTests.test.ts | 26 ++++++------ .../OrderedStopResolverTests.test.ts | 42 +++++++++---------- test/resolvers/QueryResolverTests.test.ts | 21 ++++------ test/resolvers/RouteResolverTests.test.ts | 24 +++++------ test/resolvers/ShuttleResolverTests.test.ts | 33 +++++++-------- test/resolvers/StopResolverTests.test.ts | 12 +++--- test/resolvers/SystemResolverTests.test.ts | 39 ++++++++--------- test/testHelpers/apolloTestServerHelpers.ts | 2 +- 18 files changed, 128 insertions(+), 148 deletions(-) diff --git a/src/ServerContext.ts b/src/ServerContext.ts index 95c0330..d6040ba 100644 --- a/src/ServerContext.ts +++ b/src/ServerContext.ts @@ -2,6 +2,6 @@ import { ETANotificationScheduler } from "./notifications/schedulers/ETANotifica import { ShuttleGetterSetterRepository } from "./repositories/ShuttleGetterSetterRepository"; export interface ServerContext { - repository: ShuttleGetterSetterRepository; + shuttleRepository: ShuttleGetterSetterRepository; notificationService: ETANotificationScheduler; } diff --git a/src/resolvers/EtaResolvers.ts b/src/resolvers/EtaResolvers.ts index 96bd426..b5e7c65 100644 --- a/src/resolvers/EtaResolvers.ts +++ b/src/resolvers/EtaResolvers.ts @@ -4,10 +4,10 @@ import { ServerContext } from "../ServerContext"; export const EtaResolvers: Resolvers = { ETA: { stop: async (parent, args, contextValue, info) => { - return await contextValue.repository.getStopById(parent.stopId); + return await contextValue.shuttleRepository.getStopById(parent.stopId); }, shuttle: async (parent, args, contextValue, info) => { - return await contextValue.repository.getShuttleById(parent.shuttleId); + return await contextValue.shuttleRepository.getShuttleById(parent.shuttleId); }, }, -} \ No newline at end of file +} diff --git a/src/resolvers/MutationResolvers.ts b/src/resolvers/MutationResolvers.ts index a840817..13c427e 100644 --- a/src/resolvers/MutationResolvers.ts +++ b/src/resolvers/MutationResolvers.ts @@ -8,14 +8,14 @@ import { ScheduledNotification } from "../repositories/NotificationRepository"; export const MutationResolvers: Resolvers = { Mutation: { scheduleNotification: async (_parent, args, context, _info) => { - const shuttle = await context.repository.getShuttleById(args.input.shuttleId); + const shuttle = await context.shuttleRepository.getShuttleById(args.input.shuttleId); if (!shuttle) { return { message: "Shuttle ID doesn't exist", success: false, } } - const stop = await context.repository.getStopById(args.input.stopId); + const stop = await context.shuttleRepository.getStopById(args.input.stopId); if (!stop) { return { message: "Stop ID doesn't exist", diff --git a/src/resolvers/OrderedStopResolvers.ts b/src/resolvers/OrderedStopResolvers.ts index ad6c8e6..fe047aa 100644 --- a/src/resolvers/OrderedStopResolvers.ts +++ b/src/resolvers/OrderedStopResolvers.ts @@ -7,13 +7,13 @@ export const OrderedStopResolvers: Resolvers = { const routeId = parent.routeId; const stopId = parent.stopId; - const currentOrderedStop = await contextValue.repository.getOrderedStopByRouteAndStopId(routeId, stopId); + const currentOrderedStop = await contextValue.shuttleRepository.getOrderedStopByRouteAndStopId(routeId, stopId); if (!currentOrderedStop) return null; const nextOrderedStop = currentOrderedStop.nextStop; if (!nextOrderedStop) return null; - const nextOrderedStopObject = await contextValue.repository.getStopById(nextOrderedStop.stopId); + const nextOrderedStopObject = await contextValue.shuttleRepository.getStopById(nextOrderedStop.stopId); if (!nextOrderedStopObject) return null; return { @@ -26,13 +26,13 @@ export const OrderedStopResolvers: Resolvers = { const routeId = parent.routeId; const stopId = parent.stopId; - const currentOrderedStop = await contextValue.repository.getOrderedStopByRouteAndStopId(routeId, stopId); + const currentOrderedStop = await contextValue.shuttleRepository.getOrderedStopByRouteAndStopId(routeId, stopId); if (!currentOrderedStop) return null; const previousOrderedStop = currentOrderedStop.previousStop; if (!previousOrderedStop) return null; - const previousOrderedStopObject = await contextValue.repository.getStopById(previousOrderedStop.stopId); + const previousOrderedStopObject = await contextValue.shuttleRepository.getStopById(previousOrderedStop.stopId); if (!previousOrderedStopObject) return null; return { @@ -42,11 +42,11 @@ export const OrderedStopResolvers: Resolvers = { } }, stop: async (parent, args, contextValue, info) => { - return await contextValue.repository.getStopById(parent.stopId); + return await contextValue.shuttleRepository.getStopById(parent.stopId); }, route: async (parent, args, contextValue, info) => { - return await contextValue.repository.getRouteById(parent.routeId); + return await contextValue.shuttleRepository.getRouteById(parent.routeId); }, }, -} \ No newline at end of file +} diff --git a/src/resolvers/QueryResolvers.ts b/src/resolvers/QueryResolvers.ts index 725ae8d..ccecf14 100644 --- a/src/resolvers/QueryResolvers.ts +++ b/src/resolvers/QueryResolvers.ts @@ -4,11 +4,11 @@ import { Resolvers } from "../generated/graphql"; export const QueryResolvers: Resolvers = { Query: { systems: async (_parent, args, contextValue, _info) => { - return await contextValue.repository.getSystems(); + return await contextValue.shuttleRepository.getSystems(); }, system: async (_parent, args, contextValue, _info) => { if (!args.id) return null; - const system = await contextValue.repository.getSystemById(args.id); + const system = await contextValue.shuttleRepository.getSystemById(args.id); if (system === null) return null; return { diff --git a/src/resolvers/RouteResolvers.ts b/src/resolvers/RouteResolvers.ts index d5a5810..a0adc01 100644 --- a/src/resolvers/RouteResolvers.ts +++ b/src/resolvers/RouteResolvers.ts @@ -4,7 +4,7 @@ import { ServerContext } from "../ServerContext"; export const RouteResolvers: Resolvers = { Route: { shuttles: async (parent, args, contextValue, info) => { - const shuttles = await contextValue.repository.getShuttlesByRouteId(parent.id); + const shuttles = await contextValue.shuttleRepository.getShuttlesByRouteId(parent.id); return shuttles.map(({ coordinates, @@ -22,10 +22,10 @@ export const RouteResolvers: Resolvers = { }, orderedStop: async (parent, args, contextValue, info) => { if (!args.forStopId) return null; - const orderedStop = await contextValue.repository.getOrderedStopByRouteAndStopId(parent.id, args.forStopId); + const orderedStop = await contextValue.shuttleRepository.getOrderedStopByRouteAndStopId(parent.id, args.forStopId); if (!orderedStop) return null; - const stop = await contextValue.repository.getStopById(orderedStop.stopId); + const stop = await contextValue.shuttleRepository.getStopById(orderedStop.stopId); if (!stop) return null; return { diff --git a/src/resolvers/ShuttleResolvers.ts b/src/resolvers/ShuttleResolvers.ts index 2350987..b264306 100644 --- a/src/resolvers/ShuttleResolvers.ts +++ b/src/resolvers/ShuttleResolvers.ts @@ -5,7 +5,7 @@ export const ShuttleResolvers: Resolvers = { Shuttle: { eta: async (parent, args, contextValue, info) => { if (!args.forStopId) return null; - const etaForStopId = await contextValue.repository.getEtaForShuttleAndStopId(parent.id, args.forStopId); + const etaForStopId = await contextValue.shuttleRepository.getEtaForShuttleAndStopId(parent.id, args.forStopId); if (etaForStopId === null) return null; return { @@ -16,7 +16,7 @@ export const ShuttleResolvers: Resolvers = { }; }, etas: async (parent, args, contextValue, info) => { - const etasForShuttle = await contextValue.repository.getEtasForShuttleId(parent.id); + const etasForShuttle = await contextValue.shuttleRepository.getEtasForShuttleId(parent.id); if (!etasForShuttle) return null; const computedEtas = await Promise.all(etasForShuttle.map(async ({ @@ -38,7 +38,7 @@ export const ShuttleResolvers: Resolvers = { return []; }, route: async (parent, args, contextValue, info) => { - const route = await contextValue.repository.getRouteById(parent.routeId); + const route = await contextValue.shuttleRepository.getRouteById(parent.routeId); if (route === null) return null; return { @@ -49,4 +49,4 @@ export const ShuttleResolvers: Resolvers = { } } }, -} \ No newline at end of file +} diff --git a/src/resolvers/StopResolvers.ts b/src/resolvers/StopResolvers.ts index 6ccbdf1..860e3be 100644 --- a/src/resolvers/StopResolvers.ts +++ b/src/resolvers/StopResolvers.ts @@ -4,10 +4,10 @@ import { ServerContext } from "../ServerContext"; export const StopResolvers: Resolvers = { Stop: { orderedStops: async (parent, args, contextValue, info) => { - return await contextValue.repository.getOrderedStopsByStopId(parent.id); + return await contextValue.shuttleRepository.getOrderedStopsByStopId(parent.id); }, etas: async (parent, args, contextValue, info) => { - return await contextValue.repository.getEtasForStopId(parent.id); + return await contextValue.shuttleRepository.getEtasForStopId(parent.id); }, }, -} \ No newline at end of file +} diff --git a/src/resolvers/SystemResolvers.ts b/src/resolvers/SystemResolvers.ts index dbb98ca..ae9903a 100644 --- a/src/resolvers/SystemResolvers.ts +++ b/src/resolvers/SystemResolvers.ts @@ -4,14 +4,14 @@ import { ServerContext } from "../ServerContext"; export const SystemResolvers: Resolvers = { System: { routes: async (parent, args, contextValue, info) => { - return await contextValue.repository.getRoutesBySystemId(parent.id); + return await contextValue.shuttleRepository.getRoutesBySystemId(parent.id); }, stops: async (parent, args, contextValue, info) => { - return await contextValue.repository.getStopsBySystemId(parent.id); + return await contextValue.shuttleRepository.getStopsBySystemId(parent.id); }, stop: async (parent, args, contextValue, info) => { if (!args.id) return null; - const stop = await contextValue.repository.getStopById(args.id); + const stop = await contextValue.shuttleRepository.getStopById(args.id); if (stop === null) return null; if (stop.systemId !== parent.id) return null; @@ -24,7 +24,7 @@ export const SystemResolvers: Resolvers = { }, route: async (parent, args, contextValue, info) => { if (!args.id) return null; - const route = await contextValue.repository.getRouteById(args.id); + const route = await contextValue.shuttleRepository.getRouteById(args.id); if (route === null) return null; if (route.systemId !== parent.id) return null; @@ -38,7 +38,7 @@ export const SystemResolvers: Resolvers = { }, shuttle: async (parent, args, contextValue, info) => { if (!args.id) return null; - const shuttle = await contextValue.repository.getShuttleById(args.id); + const shuttle = await contextValue.shuttleRepository.getShuttleById(args.id); if (shuttle === null) return null; if (shuttle.systemId !== parent.id) return null; @@ -46,7 +46,7 @@ export const SystemResolvers: Resolvers = { return shuttle; }, shuttles: async (parent, args, contextValue, info) => { - return await contextValue.repository.getShuttlesBySystemId(parent.id); + return await contextValue.shuttleRepository.getShuttlesBySystemId(parent.id); } }, -} \ No newline at end of file +} diff --git a/test/resolvers/EtaResolverTests.test.ts b/test/resolvers/EtaResolverTests.test.ts index 98c2e52..92a5600 100644 --- a/test/resolvers/EtaResolverTests.test.ts +++ b/test/resolvers/EtaResolverTests.test.ts @@ -18,10 +18,10 @@ describe("EtaResolvers", () => { let expectedEta: IEta; beforeEach(async () => { - mockSystem = await addMockSystemToRepository(context.repository); - mockShuttle = await addMockShuttleToRepository(context.repository, mockSystem.id); - mockStop = await addMockStopToRepository(context.repository, mockSystem.id); - expectedEta = await addMockEtaToRepository(context.repository, mockStop.id, mockShuttle.id); + mockSystem = await addMockSystemToRepository(context.shuttleRepository); + mockShuttle = await addMockShuttleToRepository(context.shuttleRepository, mockSystem.id); + mockStop = await addMockStopToRepository(context.shuttleRepository, mockSystem.id); + expectedEta = await addMockEtaToRepository(context.shuttleRepository, mockStop.id, mockShuttle.id); }); async function getResponseForEtaQuery(query: string) { @@ -32,9 +32,8 @@ describe("EtaResolvers", () => { shuttleId: mockShuttle.id, }, }, { - contextValue: { - repository: context.repository, - } + contextValue: context + }); return response; } diff --git a/test/resolvers/MutationResolverTests.test.ts b/test/resolvers/MutationResolverTests.test.ts index 73eaae5..448f958 100644 --- a/test/resolvers/MutationResolverTests.test.ts +++ b/test/resolvers/MutationResolverTests.test.ts @@ -49,9 +49,9 @@ describe("MutationResolvers", () => { it("adds a notification to the notification service", async () => { - const system = await addMockSystemToRepository(context.repository); - const shuttle = await addMockShuttleToRepository(context.repository, system.id); - const stop = await addMockStopToRepository(context.repository, system.id); + const system = await addMockSystemToRepository(context.shuttleRepository); + const shuttle = await addMockShuttleToRepository(context.shuttleRepository, system.id); + const stop = await addMockStopToRepository(context.shuttleRepository, system.id); const notificationInput = { deviceId: "1", @@ -76,9 +76,9 @@ describe("MutationResolvers", () => { }); it("adds a notification with the default seconds threshold if none is provided", async () => { - const system = await addMockSystemToRepository(context.repository); - const shuttle = await addMockShuttleToRepository(context.repository, system.id); - const stop = await addMockStopToRepository(context.repository, system.id); + const system = await addMockSystemToRepository(context.shuttleRepository); + const shuttle = await addMockShuttleToRepository(context.shuttleRepository, system.id); + const stop = await addMockStopToRepository(context.shuttleRepository, system.id); const notificationInput = { deviceId: "1", @@ -97,8 +97,8 @@ describe("MutationResolvers", () => { }); it("fails if the shuttle ID doesn't exist", async () => { - const system = await addMockSystemToRepository(context.repository); - const stop = await addMockStopToRepository(context.repository, system.id); + const system = await addMockSystemToRepository(context.shuttleRepository); + const stop = await addMockStopToRepository(context.shuttleRepository, system.id); const notificationInput = { deviceId: "1", @@ -110,8 +110,8 @@ describe("MutationResolvers", () => { }); it("fails if the stop ID doesn't exist", async () => { - const system = await addMockSystemToRepository(context.repository); - const shuttle = await addMockShuttleToRepository(context.repository, system.id); + const system = await addMockSystemToRepository(context.shuttleRepository); + const shuttle = await addMockShuttleToRepository(context.shuttleRepository, system.id); const notificationInput = { deviceId: "1", @@ -140,9 +140,9 @@ describe("MutationResolvers", () => { ` it("removes the notification from the notification service", async () => { - const system = await addMockSystemToRepository(context.repository); - const shuttle = await addMockShuttleToRepository(context.repository, system.id); - const stop = await addMockStopToRepository(context.repository, system.id); + const system = await addMockSystemToRepository(context.shuttleRepository); + const shuttle = await addMockShuttleToRepository(context.shuttleRepository, system.id); + const stop = await addMockStopToRepository(context.shuttleRepository, system.id); const notificationInput: any = { deviceId: "1", diff --git a/test/resolvers/OrderedStopResolverTests.test.ts b/test/resolvers/OrderedStopResolverTests.test.ts index 0d047ac..4813e2d 100644 --- a/test/resolvers/OrderedStopResolverTests.test.ts +++ b/test/resolvers/OrderedStopResolverTests.test.ts @@ -14,13 +14,13 @@ describe("OrderedStopResolvers", () => { let mockStops: IStop[]; beforeEach(async () => { - mockSystem = await addMockSystemToRepository(context.repository); - mockRoute = await addMockRouteToRepository(context.repository, mockSystem.id); + mockSystem = await addMockSystemToRepository(context.shuttleRepository); + mockRoute = await addMockRouteToRepository(context.shuttleRepository, mockSystem.id); mockStops = generateMockStops(); await Promise.all(mockStops.map(async (mockStop) => { mockStop.systemId = mockSystem.id; - await context.repository.addOrUpdateStop(mockStop); + await context.shuttleRepository.addOrUpdateStop(mockStop); })); }); @@ -38,8 +38,8 @@ describe("OrderedStopResolvers", () => { // Link the stops together orderedStops[0].nextStop = orderedStops[1]; orderedStops[1].previousStop = orderedStops[0]; - await context.repository.addOrUpdateOrderedStop(orderedStops[0]); - await context.repository.addOrUpdateOrderedStop(orderedStops[1]); + await context.shuttleRepository.addOrUpdateOrderedStop(orderedStops[0]); + await context.shuttleRepository.addOrUpdateOrderedStop(orderedStops[1]); return orderedStops; } @@ -68,9 +68,8 @@ describe("OrderedStopResolvers", () => { stopId, }, }, { - contextValue: { - repository: context.repository, - } + contextValue: context + }); } @@ -94,7 +93,7 @@ describe("OrderedStopResolvers", () => { it("returns null if there is no next stop in the repository", async () => { const orderedStops = await setUpOrderedStopsInRepository(); orderedStops[0].nextStop = undefined; - await context.repository.addOrUpdateOrderedStop(orderedStops[0]); + await context.shuttleRepository.addOrUpdateOrderedStop(orderedStops[0]); const response = await getResponseForNextStopQuery(orderedStops[0].stopId); @@ -105,7 +104,7 @@ describe("OrderedStopResolvers", () => { it("returns null if the next stop object no longer exists", async () => { const orderedStops = await setUpOrderedStopsInRepository(); - await context.repository.removeStopIfExists(orderedStops[1].stopId); + await context.shuttleRepository.removeStopIfExists(orderedStops[1].stopId); const response = await getResponseForNextStopQuery(orderedStops[0].stopId); @@ -140,9 +139,8 @@ describe("OrderedStopResolvers", () => { stopId, }, }, { - contextValue: { - repository: context.repository, - } + contextValue: context + }); } @@ -165,7 +163,7 @@ describe("OrderedStopResolvers", () => { it("returns null if there is no previous stop in the repository", async () => { const orderedStops = await setUpOrderedStopsInRepository(); orderedStops[1].previousStop = undefined; - await context.repository.addOrUpdateOrderedStop(orderedStops[1]); + await context.shuttleRepository.addOrUpdateOrderedStop(orderedStops[1]); const response = await getResponseForPreviousStopQuery(orderedStops[1].stopId); @@ -176,7 +174,7 @@ describe("OrderedStopResolvers", () => { it("returns null if the current stop no longer exists", async () => { const orderedStops = await setUpOrderedStopsInRepository(); - await context.repository.removeStopIfExists(orderedStops[0].stopId); + await context.shuttleRepository.removeStopIfExists(orderedStops[0].stopId); const response = await getResponseForPreviousStopQuery(orderedStops[1].stopId); @@ -214,9 +212,8 @@ describe("OrderedStopResolvers", () => { stopId, } }, { - contextValue: { - repository: context.repository, - } + contextValue: context + }); } @@ -226,7 +223,7 @@ describe("OrderedStopResolvers", () => { orderedStops[0].stopId = mockStops[0].id; // Add one stop only - await context.repository.addOrUpdateOrderedStop(orderedStops[0]); + await context.shuttleRepository.addOrUpdateOrderedStop(orderedStops[0]); const response = await getResponseForRouteQuery(orderedStops[1].stopId); @@ -265,16 +262,15 @@ describe("OrderedStopResolvers", () => { stopId, } }, { - contextValue: { - repository: context.repository, - } + contextValue: context + }); } it("returns the associated stop if it exists", async () => { const orderedStops = await setUpOrderedStopsInRepository(); orderedStops[0].stopId = mockStops[0].id; - await context.repository.addOrUpdateOrderedStop(orderedStops[0]); + await context.shuttleRepository.addOrUpdateOrderedStop(orderedStops[0]); const response = await getResponseForStopQuery(orderedStops[0].stopId); diff --git a/test/resolvers/QueryResolverTests.test.ts b/test/resolvers/QueryResolverTests.test.ts index dc36f79..6710d3a 100644 --- a/test/resolvers/QueryResolverTests.test.ts +++ b/test/resolvers/QueryResolverTests.test.ts @@ -15,7 +15,7 @@ describe("QueryResolvers", () => { async function addMockSystems() { const systems = generateMockSystems(); await Promise.all(systems.map(async (system) => { - await context.repository.addOrUpdateSystem(system); + await context.shuttleRepository.addOrUpdateSystem(system); })); return systems; } @@ -36,9 +36,8 @@ describe("QueryResolvers", () => { const response = await holder.testServer.executeOperation({ query, }, { - contextValue: { - repository: context.repository, - }, + contextValue: context + }); assert(response.body.kind === "single"); @@ -68,9 +67,8 @@ describe("QueryResolvers", () => { id: systemToGet.id, } }, { - contextValue: { - repository: context.repository, - } + contextValue: context + }); assert(response.body.kind === "single"); @@ -85,9 +83,8 @@ describe("QueryResolvers", () => { id: "nonexistent-id", } }, { - contextValue: { - repository: context.repository, - } + contextValue: context + }); assert(response.body.kind === "single"); @@ -106,8 +103,8 @@ describe("QueryResolvers", () => { it("returns correct data if the notification is scheduled", async () => { // Arrange - const shuttle = await addMockShuttleToRepository(context.repository, "1"); - const stop = await addMockStopToRepository(context.repository, "1") + const shuttle = await addMockShuttleToRepository(context.shuttleRepository, "1"); + const stop = await addMockStopToRepository(context.shuttleRepository, "1") const notification: NotificationSchedulingArguments = { shuttleId: shuttle.id, diff --git a/test/resolvers/RouteResolverTests.test.ts b/test/resolvers/RouteResolverTests.test.ts index 0279a6e..909c1d8 100644 --- a/test/resolvers/RouteResolverTests.test.ts +++ b/test/resolvers/RouteResolverTests.test.ts @@ -18,11 +18,11 @@ describe("RouteResolvers", () => { let mockStop: IStop; beforeEach(async () => { - mockSystem = await addMockSystemToRepository(context.repository); + mockSystem = await addMockSystemToRepository(context.shuttleRepository); const systemId = mockSystem.id; - mockRoute = await addMockRouteToRepository(context.repository, systemId); - mockStop = await addMockStopToRepository(context.repository, systemId); + mockRoute = await addMockRouteToRepository(context.shuttleRepository, systemId); + mockStop = await addMockStopToRepository(context.shuttleRepository, systemId); }); @@ -48,9 +48,8 @@ describe("RouteResolvers", () => { routeId: mockRoute.id, }, }, { - contextValue: { - repository: context.repository, - } + contextValue: context + }); } @@ -59,7 +58,7 @@ describe("RouteResolvers", () => { const expectedShuttle = expectedShuttles[0]; expectedShuttle.systemId = mockSystem.id; expectedShuttle.routeId = mockRoute.id; - await context.repository.addOrUpdateShuttle(expectedShuttle); + await context.shuttleRepository.addOrUpdateShuttle(expectedShuttle); const response = await getResponseForShuttlesQuery(); @@ -104,9 +103,8 @@ describe("RouteResolvers", () => { stopId: mockStop.id, } }, { - contextValue: { - repository: context.repository, - } + contextValue: context + }); } @@ -115,7 +113,7 @@ describe("RouteResolvers", () => { const expectedOrderedStop = orderedStops[0]; expectedOrderedStop.stopId = mockStop.id; expectedOrderedStop.routeId = mockRoute.id; - await context.repository.addOrUpdateOrderedStop(expectedOrderedStop); + await context.shuttleRepository.addOrUpdateOrderedStop(expectedOrderedStop); const response = await getResponseForOrderedStopQuery(); @@ -132,9 +130,9 @@ describe("RouteResolvers", () => { const expectedOrderedStop = orderedStops[0]; expectedOrderedStop.stopId = mockStop.id; expectedOrderedStop.routeId = mockRoute.id; - await context.repository.addOrUpdateOrderedStop(expectedOrderedStop); + await context.shuttleRepository.addOrUpdateOrderedStop(expectedOrderedStop); - await context.repository.removeStopIfExists(mockStop.id); + await context.shuttleRepository.removeStopIfExists(mockStop.id); const response = await getResponseForOrderedStopQuery(); diff --git a/test/resolvers/ShuttleResolverTests.test.ts b/test/resolvers/ShuttleResolverTests.test.ts index b2b3e81..800f34c 100644 --- a/test/resolvers/ShuttleResolverTests.test.ts +++ b/test/resolvers/ShuttleResolverTests.test.ts @@ -14,8 +14,8 @@ describe("ShuttleResolvers", () => { let mockShuttle: IShuttle; beforeEach(async () => { - mockSystem = await addMockSystemToRepository(context.repository); - mockShuttle = await addMockShuttleToRepository(context.repository, + mockSystem = await addMockSystemToRepository(context.shuttleRepository); + mockShuttle = await addMockShuttleToRepository(context.shuttleRepository, mockSystem.id); }); @@ -24,7 +24,7 @@ describe("ShuttleResolvers", () => { const etas = generateMockEtas(); await Promise.all(etas.map(async (eta) => { eta.shuttleId = shuttleId; - await context.repository.addOrUpdateEta(eta); + await context.shuttleRepository.addOrUpdateEta(eta); })); return etas; } @@ -56,9 +56,8 @@ describe("ShuttleResolvers", () => { stopId: mockEta.stopId, }, }, { - contextValue: { - repository: context.repository, - }, + contextValue: context + }); // Assert @@ -77,9 +76,8 @@ describe("ShuttleResolvers", () => { stopId: "nonexistent-stop", } }, { - contextValue: { - repository: context.repository, - } + contextValue: context + }); // Assert @@ -114,9 +112,8 @@ describe("ShuttleResolvers", () => { shuttleId: mockShuttle.id, }, }, { - contextValue: { - repository: context.repository, - } + contextValue: context + }); assert(response.body.kind === "single"); @@ -133,9 +130,8 @@ describe("ShuttleResolvers", () => { shuttleId: mockShuttle.id, }, }, { - contextValue: { - repository: context.repository, - } + contextValue: context + }); assert(response.body.kind === "single"); @@ -172,15 +168,14 @@ describe("ShuttleResolvers", () => { shuttleId: mockShuttle.id, } }, { - contextValue: { - repository: context.repository, - } + contextValue: context + }); } it("returns the route if it exists", async () => { const mockRoute = generateMockRoutes()[0]; - await context.repository.addOrUpdateRoute(mockRoute); + await context.shuttleRepository.addOrUpdateRoute(mockRoute); const response = await getResponseForQuery(); diff --git a/test/resolvers/StopResolverTests.test.ts b/test/resolvers/StopResolverTests.test.ts index fdda62b..d82ca78 100644 --- a/test/resolvers/StopResolverTests.test.ts +++ b/test/resolvers/StopResolverTests.test.ts @@ -13,8 +13,8 @@ describe("StopResolvers", () => { let mockSystem: ISystem; beforeEach(async () => { - mockSystem = await addMockSystemToRepository(context.repository); - mockStop = await addMockStopToRepository(context.repository, mockSystem.id); + mockSystem = await addMockSystemToRepository(context.shuttleRepository); + mockStop = await addMockStopToRepository(context.shuttleRepository, mockSystem.id); }) async function getResponseForQuery(query: string) { @@ -25,9 +25,7 @@ describe("StopResolvers", () => { stopId: mockStop.id, }, }, { - contextValue: { - repository: context.repository, - } + contextValue: context, }); } @@ -51,7 +49,7 @@ describe("StopResolvers", () => { mockOrderedStops = mockOrderedStops.filter((orderedStop) => orderedStop.stopId === mockOrderedStops[0].stopId); await Promise.all(mockOrderedStops.map(async orderedStop => { orderedStop.stopId = mockStop.id; - await context.repository.addOrUpdateOrderedStop(orderedStop); + await context.shuttleRepository.addOrUpdateOrderedStop(orderedStop); })); const response = await getResponseForQuery(query); @@ -88,7 +86,7 @@ describe("StopResolvers", () => { mockEtas = mockEtas.filter((eta) => eta.stopId === mockEtas[0].stopId); await Promise.all(mockEtas.map(async eta => { eta.stopId = mockStop.id; - await context.repository.addOrUpdateEta(eta); + await context.shuttleRepository.addOrUpdateEta(eta); })); const response = await getResponseForQuery(query); diff --git a/test/resolvers/SystemResolverTests.test.ts b/test/resolvers/SystemResolverTests.test.ts index b7f3f5d..1eb817c 100644 --- a/test/resolvers/SystemResolverTests.test.ts +++ b/test/resolvers/SystemResolverTests.test.ts @@ -17,7 +17,7 @@ describe("SystemResolvers", () => { let mockSystem: ISystem; beforeEach(async () => { - mockSystem = await addMockSystemToRepository(context.repository); + mockSystem = await addMockSystemToRepository(context.shuttleRepository); }); // TODO: Consolidate these into one single method taking an object @@ -29,7 +29,7 @@ describe("SystemResolvers", () => { }, }, { contextValue: { - repository: context.repository + shuttleRepository: context.shuttleRepository }, }); } @@ -50,7 +50,7 @@ describe("SystemResolvers", () => { const expectedRoutes = generateMockRoutes(); await Promise.all(expectedRoutes.map(async (route) => { route.systemId = mockSystem.id; - await context.repository.addOrUpdateRoute(route); + await context.shuttleRepository.addOrUpdateRoute(route); })); const response = await getResponseFromQueryNeedingSystemId(query); @@ -78,7 +78,7 @@ describe("SystemResolvers", () => { const expectedStops = generateMockStops(); await Promise.all(expectedStops.map(async (stop) => { stop.systemId = mockSystem.id; - await context.repository.addOrUpdateStop(stop); + await context.shuttleRepository.addOrUpdateStop(stop); })); const response = await getResponseFromQueryNeedingSystemId(query); @@ -111,13 +111,13 @@ describe("SystemResolvers", () => { }, }, { contextValue: { - repository: context.repository, + shuttleRepository: context.shuttleRepository, } }); } it("gets the stop with the correct id", async () => { - const mockStop = await addMockStopToRepository(context.repository, mockSystem.id); + const mockStop = await addMockStopToRepository(context.shuttleRepository, mockSystem.id); const response = await getResponseForStopQuery(mockStop.id); @@ -133,9 +133,9 @@ describe("SystemResolvers", () => { ...mockSystem, id: "2", } - await context.repository.addOrUpdateSystem(updatedSystem); + await context.shuttleRepository.addOrUpdateSystem(updatedSystem); - const mockStop = await addMockStopToRepository(context.repository, updatedSystem.id); + const mockStop = await addMockStopToRepository(context.shuttleRepository, updatedSystem.id); const response = await getResponseForStopQuery(mockStop.id); @@ -177,14 +177,12 @@ describe("SystemResolvers", () => { routeId, }, }, { - contextValue: { - repository: context.repository, - } + contextValue: context }); } it("gets the route with the correct id", async () => { - const mockRoute = await addMockRouteToRepository(context.repository, mockSystem.id); + const mockRoute = await addMockRouteToRepository(context.shuttleRepository, mockSystem.id); const response = await getResponseForRouteQuery(mockRoute.id); @@ -201,9 +199,9 @@ describe("SystemResolvers", () => { ...mockSystem, id: "2", } - await context.repository.addOrUpdateSystem(updatedSystem); + await context.shuttleRepository.addOrUpdateSystem(updatedSystem); - const mockRoute = await addMockRouteToRepository(context.repository, updatedSystem.id); + const mockRoute = await addMockRouteToRepository(context.shuttleRepository, updatedSystem.id); const response = await getResponseForRouteQuery(mockRoute.id); @@ -245,14 +243,13 @@ describe("SystemResolvers", () => { shuttleId: shuttleId, } }, { - contextValue: { - repository: context.repository, - } + contextValue: context + }); } it("gets the shuttle with the correct id", async () => { - const mockShuttle = await addMockShuttleToRepository(context.repository, mockSystem.id); + const mockShuttle = await addMockShuttleToRepository(context.shuttleRepository, mockSystem.id); const response = await getResponseForShuttleQuery(mockShuttle.id); @@ -268,9 +265,9 @@ describe("SystemResolvers", () => { ...mockSystem, id: "2", } - await context.repository.addOrUpdateSystem(updatedSystem); + await context.shuttleRepository.addOrUpdateSystem(updatedSystem); - const mockShuttle = await addMockShuttleToRepository(context.repository, updatedSystem.id); + const mockShuttle = await addMockShuttleToRepository(context.shuttleRepository, updatedSystem.id); const response = await getResponseForShuttleQuery(mockShuttle.id); @@ -308,7 +305,7 @@ describe("SystemResolvers", () => { const expectedShuttles = generateMockShuttles(); await Promise.all(expectedShuttles.map(async (shuttle) => { shuttle.systemId = mockSystem.id; - await context.repository.addOrUpdateShuttle(shuttle); + await context.shuttleRepository.addOrUpdateShuttle(shuttle); })); const response = await getResponseFromQueryNeedingSystemId(query); diff --git a/test/testHelpers/apolloTestServerHelpers.ts b/test/testHelpers/apolloTestServerHelpers.ts index 164bd35..b629ed8 100644 --- a/test/testHelpers/apolloTestServerHelpers.ts +++ b/test/testHelpers/apolloTestServerHelpers.ts @@ -25,7 +25,7 @@ export function setupTestServerContext() { const context: { [key: string] : any } = {}; beforeEach(() => { - context.repository = new UnoptimizedInMemoryShuttleRepository(); + context.shuttleRepository = new UnoptimizedInMemoryShuttleRepository(); context.notificationService = new ETANotificationScheduler(context.repository); }); From 3761f43909c20c3dcb8c7c45aeababe688f2528f Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Thu, 27 Mar 2025 10:42:43 -0700 Subject: [PATCH 19/33] update server context to only include the notification repository --- src/ServerContext.ts | 3 ++- src/index.ts | 26 +++++++++++++------- src/resolvers/MutationResolvers.ts | 6 ++--- src/resolvers/QueryResolvers.ts | 4 +-- test/resolvers/MutationResolverTests.test.ts | 10 ++++---- test/resolvers/QueryResolverTests.test.ts | 2 +- test/testHelpers/apolloTestServerHelpers.ts | 3 ++- 7 files changed, 32 insertions(+), 22 deletions(-) diff --git a/src/ServerContext.ts b/src/ServerContext.ts index d6040ba..732f7d8 100644 --- a/src/ServerContext.ts +++ b/src/ServerContext.ts @@ -1,7 +1,8 @@ import { ETANotificationScheduler } from "./notifications/schedulers/ETANotificationScheduler"; import { ShuttleGetterSetterRepository } from "./repositories/ShuttleGetterSetterRepository"; +import { NotificationRepository } from "./repositories/NotificationRepository"; export interface ServerContext { shuttleRepository: ShuttleGetterSetterRepository; - notificationService: ETANotificationScheduler; + notificationRepository: NotificationRepository; } diff --git a/src/index.ts b/src/index.ts index 3880fac..75b9d20 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,6 +9,7 @@ import { ETANotificationScheduler } from "./notifications/schedulers/ETANotifica import { loadShuttleTestData } from "./loaders/loadShuttleTestData"; import { AppleNotificationSender } from "./notifications/senders/AppleNotificationSender"; import { InMemoryNotificationRepository } from "./repositories/InMemoryNotificationRepository"; +import { NotificationRepository } from "./repositories/NotificationRepository"; const typeDefs = readFileSync("./schema.graphqls", "utf8"); @@ -19,25 +20,32 @@ async function main() { introspection: process.env.NODE_ENV !== "production", }); - const repository = new UnoptimizedInMemoryShuttleRepository(); + const shuttleRepository = new UnoptimizedInMemoryShuttleRepository(); + + let notificationRepository: NotificationRepository; let notificationService: ETANotificationScheduler; if (process.argv.length > 2 && process.argv[2] == "integration-testing") { console.log("Using integration testing setup") - await loadShuttleTestData(repository); + await loadShuttleTestData(shuttleRepository); const appleNotificationSender = new AppleNotificationSender(false); - const inMemoryNotificationRepository = new InMemoryNotificationRepository(); + notificationRepository = new InMemoryNotificationRepository(); notificationService = new ETANotificationScheduler( - repository, - inMemoryNotificationRepository, + shuttleRepository, + notificationRepository, appleNotificationSender ); } else { const repositoryDataUpdater = new TimedApiBasedShuttleRepositoryLoader( - repository + shuttleRepository, ); await repositoryDataUpdater.start(); - notificationService = new ETANotificationScheduler(repository); + + notificationRepository = new InMemoryNotificationRepository(); + notificationService = new ETANotificationScheduler( + shuttleRepository, + notificationRepository + ); } const { url } = await startStandaloneServer(server, { @@ -46,8 +54,8 @@ async function main() { }, context: async () => { return { - repository, - notificationService, + shuttleRepository, + notificationRepository, } }, }); diff --git a/src/resolvers/MutationResolvers.ts b/src/resolvers/MutationResolvers.ts index 13c427e..adcc20c 100644 --- a/src/resolvers/MutationResolvers.ts +++ b/src/resolvers/MutationResolvers.ts @@ -30,7 +30,7 @@ export const MutationResolvers: Resolvers = { : ETANotificationScheduler.defaultSecondsThresholdForNotificationToFire, } - await context.notificationService.scheduleNotification(notificationData); + await context.notificationRepository.scheduleNotification(notificationData); const response: NotificationResponse = { message: "Notification scheduled", @@ -40,8 +40,8 @@ export const MutationResolvers: Resolvers = { return response; }, cancelNotification: async (_parent, args, context, _info) => { - if (context.notificationService.isNotificationScheduled(args.input)) { - await context.notificationService.cancelNotificationIfExists(args.input); + if (context.notificationRepository.isNotificationScheduled(args.input)) { + await context.notificationRepository.cancelNotificationIfExists(args.input); return { success: true, message: "Notification cancelled", diff --git a/src/resolvers/QueryResolvers.ts b/src/resolvers/QueryResolvers.ts index ccecf14..996f9ac 100644 --- a/src/resolvers/QueryResolvers.ts +++ b/src/resolvers/QueryResolvers.ts @@ -18,11 +18,11 @@ export const QueryResolvers: Resolvers = { }, isNotificationScheduled: async (_parent, args, contextValue, _info) => { const notificationData = args.input; - return contextValue.notificationService.isNotificationScheduled(notificationData); + return contextValue.notificationRepository.isNotificationScheduled(notificationData); }, secondsThresholdForNotification: async (_parent, args, contextValue, _info) => { const notificationData = args.input; - return contextValue.notificationService.getSecondsThresholdForScheduledNotification(notificationData); + return contextValue.notificationRepository.getSecondsThresholdForScheduledNotification(notificationData); }, }, } diff --git a/test/resolvers/MutationResolverTests.test.ts b/test/resolvers/MutationResolverTests.test.ts index 448f958..0fe487a 100644 --- a/test/resolvers/MutationResolverTests.test.ts +++ b/test/resolvers/MutationResolverTests.test.ts @@ -44,7 +44,7 @@ describe("MutationResolvers", () => { const notificationResponse = response.body.singleResult.data?.scheduleNotification as any; expect(notificationResponse.success).toBe(false); - expect(context.notificationService.isNotificationScheduled(notificationInput)).toBe(false); + expect(context.notificationRepository.isNotificationScheduled(notificationInput)).toBe(false); } @@ -72,7 +72,7 @@ describe("MutationResolvers", () => { expect(notificationResponse?.success).toBe(true); expect(notificationResponse?.data).toEqual(expectedNotificationData); - expect(context.notificationService.getSecondsThresholdForScheduledNotification(expectedNotificationData)).toBe(240); + expect(context.notificationRepository.getSecondsThresholdForScheduledNotification(expectedNotificationData)).toBe(240); }); it("adds a notification with the default seconds threshold if none is provided", async () => { @@ -93,7 +93,7 @@ describe("MutationResolvers", () => { const notificationResponse = response.body.singleResult.data?.scheduleNotification as any; expect(notificationResponse?.success).toBe(true); - expect(context.notificationService.getSecondsThresholdForScheduledNotification(notificationInput)).toBe(180); + expect(context.notificationRepository.getSecondsThresholdForScheduledNotification(notificationInput)).toBe(180); }); it("fails if the shuttle ID doesn't exist", async () => { @@ -150,7 +150,7 @@ describe("MutationResolvers", () => { stopId: stop.id, secondsThreshold: 180, } - await context.notificationService.scheduleNotification(notificationInput); + await context.notificationRepository.scheduleNotification(notificationInput); const notificationLookup = { ...notificationInput @@ -166,7 +166,7 @@ describe("MutationResolvers", () => { expect(notificationResponse.success).toBe(true); expect(notificationResponse.data).toEqual(notificationLookup); - expect(context.notificationService.isNotificationScheduled(notificationLookup)).toBe(false); + expect(context.notificationRepository.isNotificationScheduled(notificationLookup)).toBe(false); }); it("fails if the notification doesn't exist", async () => { diff --git a/test/resolvers/QueryResolverTests.test.ts b/test/resolvers/QueryResolverTests.test.ts index 6710d3a..11a3282 100644 --- a/test/resolvers/QueryResolverTests.test.ts +++ b/test/resolvers/QueryResolverTests.test.ts @@ -112,7 +112,7 @@ describe("QueryResolvers", () => { deviceId: "1", secondsThreshold: 240, }; - await context.notificationService.scheduleNotification(notification); + await context.notificationRepository.scheduleNotification(notification); const notificationLookup: any = { ...notification, diff --git a/test/testHelpers/apolloTestServerHelpers.ts b/test/testHelpers/apolloTestServerHelpers.ts index b629ed8..a7512ec 100644 --- a/test/testHelpers/apolloTestServerHelpers.ts +++ b/test/testHelpers/apolloTestServerHelpers.ts @@ -5,6 +5,7 @@ import { UnoptimizedInMemoryShuttleRepository } from "../../src/repositories/Uno import { beforeEach } from "@jest/globals"; import { ServerContext } from "../../src/ServerContext"; import { ETANotificationScheduler } from "../../src/notifications/schedulers/ETANotificationScheduler"; +import { InMemoryNotificationRepository } from "../../src/repositories/InMemoryNotificationRepository"; function setUpTestServer() { @@ -26,7 +27,7 @@ export function setupTestServerContext() { beforeEach(() => { context.shuttleRepository = new UnoptimizedInMemoryShuttleRepository(); - context.notificationService = new ETANotificationScheduler(context.repository); + context.notificationRepository = new InMemoryNotificationRepository(); }); return context as ServerContext; From a665c29745567509ec108cedfaf9c40792eb885f Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Thu, 27 Mar 2025 10:46:38 -0700 Subject: [PATCH 20/33] add method to check if notification is scheduled --- src/repositories/InMemoryNotificationRepository.ts | 5 +++++ src/repositories/NotificationRepository.ts | 1 + 2 files changed, 6 insertions(+) diff --git a/src/repositories/InMemoryNotificationRepository.ts b/src/repositories/InMemoryNotificationRepository.ts index db6e9bc..ecd3e7b 100644 --- a/src/repositories/InMemoryNotificationRepository.ts +++ b/src/repositories/InMemoryNotificationRepository.ts @@ -42,6 +42,10 @@ export class InMemoryNotificationRepository implements NotificationRepository { return this.deviceIdsToDeliverTo[tuple.toString()][deviceId]; } + async isNotificationScheduled(lookupArguments: NotificationLookupArguments): Promise { + return await this.getSecondsThresholdForNotificationIfExists(lookupArguments) !== null; + } + async addOrUpdateNotification({ shuttleId, stopId, @@ -71,4 +75,5 @@ export class InMemoryNotificationRepository implements NotificationRepository { delete this.deviceIdsToDeliverTo[tupleKey.toString()][deviceId]; } + } diff --git a/src/repositories/NotificationRepository.ts b/src/repositories/NotificationRepository.ts index 343497e..6914bf8 100644 --- a/src/repositories/NotificationRepository.ts +++ b/src/repositories/NotificationRepository.ts @@ -17,6 +17,7 @@ export interface ScheduledNotification extends NotificationLookupArguments { export interface NotificationRepository { getAllNotificationsForShuttleAndStopId(shuttleId: string, stopId: string): Promise; getSecondsThresholdForNotificationIfExists(lookupArguments: NotificationLookupArguments): Promise; + isNotificationScheduled(lookupArguments: NotificationLookupArguments): Promise; addOrUpdateNotification(notification: ScheduledNotification): Promise; deleteNotificationIfExists(lookupArguments: NotificationLookupArguments): Promise; } From ef94a9aa7e7bc5063d673433373c6eafaedecc63 Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Thu, 27 Mar 2025 10:56:57 -0700 Subject: [PATCH 21/33] fix method calls and tests --- src/resolvers/MutationResolvers.ts | 7 ++++--- src/resolvers/QueryResolvers.ts | 4 ++-- test/resolvers/MutationResolverTests.test.ts | 16 ++++++++-------- test/resolvers/QueryResolverTests.test.ts | 6 +++--- 4 files changed, 17 insertions(+), 16 deletions(-) diff --git a/src/resolvers/MutationResolvers.ts b/src/resolvers/MutationResolvers.ts index adcc20c..9b48a7d 100644 --- a/src/resolvers/MutationResolvers.ts +++ b/src/resolvers/MutationResolvers.ts @@ -30,7 +30,7 @@ export const MutationResolvers: Resolvers = { : ETANotificationScheduler.defaultSecondsThresholdForNotificationToFire, } - await context.notificationRepository.scheduleNotification(notificationData); + await context.notificationRepository.addOrUpdateNotification(notificationData); const response: NotificationResponse = { message: "Notification scheduled", @@ -40,8 +40,9 @@ export const MutationResolvers: Resolvers = { return response; }, cancelNotification: async (_parent, args, context, _info) => { - if (context.notificationRepository.isNotificationScheduled(args.input)) { - await context.notificationRepository.cancelNotificationIfExists(args.input); + const isScheduled = await context.notificationRepository.isNotificationScheduled(args.input) + if (isScheduled) { + await context.notificationRepository.deleteNotificationIfExists(args.input); return { success: true, message: "Notification cancelled", diff --git a/src/resolvers/QueryResolvers.ts b/src/resolvers/QueryResolvers.ts index 996f9ac..06df1a9 100644 --- a/src/resolvers/QueryResolvers.ts +++ b/src/resolvers/QueryResolvers.ts @@ -18,11 +18,11 @@ export const QueryResolvers: Resolvers = { }, isNotificationScheduled: async (_parent, args, contextValue, _info) => { const notificationData = args.input; - return contextValue.notificationRepository.isNotificationScheduled(notificationData); + return await contextValue.notificationRepository.isNotificationScheduled(notificationData); }, secondsThresholdForNotification: async (_parent, args, contextValue, _info) => { const notificationData = args.input; - return contextValue.notificationRepository.getSecondsThresholdForScheduledNotification(notificationData); + return await contextValue.notificationRepository.getSecondsThresholdForNotificationIfExists(notificationData); }, }, } diff --git a/test/resolvers/MutationResolverTests.test.ts b/test/resolvers/MutationResolverTests.test.ts index 0fe487a..055195e 100644 --- a/test/resolvers/MutationResolverTests.test.ts +++ b/test/resolvers/MutationResolverTests.test.ts @@ -38,13 +38,13 @@ describe("MutationResolvers", () => { } ` - function assertFailedResponse(response: any, notificationInput: NotificationInput) { + async function assertFailedResponse(response: any, notificationInput: NotificationInput) { assert(response.body.kind === "single"); expect(response.body.singleResult.errors).toBeUndefined(); const notificationResponse = response.body.singleResult.data?.scheduleNotification as any; expect(notificationResponse.success).toBe(false); - expect(context.notificationRepository.isNotificationScheduled(notificationInput)).toBe(false); + expect(await context.notificationRepository.isNotificationScheduled(notificationInput)).toBe(false); } @@ -72,7 +72,7 @@ describe("MutationResolvers", () => { expect(notificationResponse?.success).toBe(true); expect(notificationResponse?.data).toEqual(expectedNotificationData); - expect(context.notificationRepository.getSecondsThresholdForScheduledNotification(expectedNotificationData)).toBe(240); + expect(await context.notificationRepository.getSecondsThresholdForNotificationIfExists(expectedNotificationData)).toBe(240); }); it("adds a notification with the default seconds threshold if none is provided", async () => { @@ -93,7 +93,7 @@ describe("MutationResolvers", () => { const notificationResponse = response.body.singleResult.data?.scheduleNotification as any; expect(notificationResponse?.success).toBe(true); - expect(context.notificationRepository.getSecondsThresholdForScheduledNotification(notificationInput)).toBe(180); + expect(await context.notificationRepository.getSecondsThresholdForNotificationIfExists(notificationInput)).toBe(180); }); it("fails if the shuttle ID doesn't exist", async () => { @@ -106,7 +106,7 @@ describe("MutationResolvers", () => { stopId: stop.id, } const response = await getServerResponse(query, notificationInput); - assertFailedResponse(response, notificationInput); + await assertFailedResponse(response, notificationInput); }); it("fails if the stop ID doesn't exist", async () => { @@ -120,7 +120,7 @@ describe("MutationResolvers", () => { } const response = await getServerResponse(query, notificationInput); - assertFailedResponse(response, notificationInput); + await assertFailedResponse(response, notificationInput); }); }); @@ -150,7 +150,7 @@ describe("MutationResolvers", () => { stopId: stop.id, secondsThreshold: 180, } - await context.notificationRepository.scheduleNotification(notificationInput); + await context.notificationRepository.addOrUpdateNotification(notificationInput); const notificationLookup = { ...notificationInput @@ -166,7 +166,7 @@ describe("MutationResolvers", () => { expect(notificationResponse.success).toBe(true); expect(notificationResponse.data).toEqual(notificationLookup); - expect(context.notificationRepository.isNotificationScheduled(notificationLookup)).toBe(false); + expect(await context.notificationRepository.isNotificationScheduled(notificationLookup)).toBe(false); }); it("fails if the notification doesn't exist", async () => { diff --git a/test/resolvers/QueryResolverTests.test.ts b/test/resolvers/QueryResolverTests.test.ts index 11a3282..52d6605 100644 --- a/test/resolvers/QueryResolverTests.test.ts +++ b/test/resolvers/QueryResolverTests.test.ts @@ -2,8 +2,8 @@ import { describe, expect, it } from "@jest/globals"; import { generateMockSystems } from "../testHelpers/mockDataGenerators"; import { setupTestServerContext, setupTestServerHolder } from "../testHelpers/apolloTestServerHelpers"; import assert = require("node:assert"); -import { ScheduledNotification } from "../../src/notifications/schedulers/ETANotificationScheduler"; import { addMockShuttleToRepository, addMockStopToRepository } from "../testHelpers/repositorySetupHelpers"; +import { ScheduledNotification } from "../../src/repositories/NotificationRepository"; // See Apollo documentation for integration test guide // https://www.apollographql.com/docs/apollo-server/testing/testing @@ -106,13 +106,13 @@ describe("QueryResolvers", () => { const shuttle = await addMockShuttleToRepository(context.shuttleRepository, "1"); const stop = await addMockStopToRepository(context.shuttleRepository, "1") - const notification: NotificationSchedulingArguments = { + const notification: ScheduledNotification = { shuttleId: shuttle.id, stopId: stop.id, deviceId: "1", secondsThreshold: 240, }; - await context.notificationRepository.scheduleNotification(notification); + await context.notificationRepository.addOrUpdateNotification(notification); const notificationLookup: any = { ...notification, From f2a2dd74f6efbff8f364b79a745ad51e507125a1 Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Thu, 27 Mar 2025 10:58:20 -0700 Subject: [PATCH 22/33] listen to shuttle ETA updates in the scheduler constructor --- src/notifications/schedulers/ETANotificationScheduler.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/notifications/schedulers/ETANotificationScheduler.ts b/src/notifications/schedulers/ETANotificationScheduler.ts index bb820b9..943b74d 100644 --- a/src/notifications/schedulers/ETANotificationScheduler.ts +++ b/src/notifications/schedulers/ETANotificationScheduler.ts @@ -17,8 +17,9 @@ export class ETANotificationScheduler { ) { this.etaSubscriberCallback = this.etaSubscriberCallback.bind(this); this.sendEtaNotificationImmediately = this.sendEtaNotificationImmediately.bind(this); - this.etaSubscriberCallback = this.etaSubscriberCallback.bind(this); this.sendEtaNotificationImmediatelyIfSecondsRemainingBelowThreshold = this.sendEtaNotificationImmediatelyIfSecondsRemainingBelowThreshold.bind(this); + + this.shuttleRepository.subscribeToEtaUpdates(this.etaSubscriberCallback); } private async sendEtaNotificationImmediately(notificationData: ScheduledNotification): Promise { From 2b28b94dbdcae082562a0827753dd8ac0678ef8a Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Thu, 27 Mar 2025 11:04:21 -0700 Subject: [PATCH 23/33] update eta notification scheduler test to use repository --- .../ETANotificationSchedulerTests.test.ts | 87 ++++++------------- 1 file changed, 28 insertions(+), 59 deletions(-) diff --git a/test/notifications/schedulers/ETANotificationSchedulerTests.test.ts b/test/notifications/schedulers/ETANotificationSchedulerTests.test.ts index 392844e..cadee33 100644 --- a/test/notifications/schedulers/ETANotificationSchedulerTests.test.ts +++ b/test/notifications/schedulers/ETANotificationSchedulerTests.test.ts @@ -6,6 +6,7 @@ import { IEta, IShuttle, IStop } from "../../../src/entities/entities"; import { addMockShuttleToRepository, addMockStopToRepository } from "../../testHelpers/repositorySetupHelpers"; import { AppleNotificationSender } from "../../../src/notifications/senders/AppleNotificationSender"; import { InMemoryNotificationRepository } from "../../../src/repositories/InMemoryNotificationRepository"; +import { NotificationRepository } from "../../../src/repositories/NotificationRepository"; jest.mock("http2"); jest.mock("../../../src/notifications/senders/AppleNotificationSender"); @@ -43,18 +44,20 @@ async function waitForMilliseconds(ms: number): Promise { describe("ETANotificationScheduler", () => { - let repository: UnoptimizedInMemoryShuttleRepository + let shuttleRepository: UnoptimizedInMemoryShuttleRepository let notificationService: ETANotificationScheduler; + let notificationRepository: NotificationRepository; beforeEach(() => { - repository = new UnoptimizedInMemoryShuttleRepository(); + shuttleRepository = new UnoptimizedInMemoryShuttleRepository(); + notificationRepository = new InMemoryNotificationRepository(); mockNotificationSenderMethods(true); const appleNotificationSender = new MockAppleNotificationSender(false); notificationService = new ETANotificationScheduler( - repository, - new InMemoryNotificationRepository(), + shuttleRepository, + notificationRepository, appleNotificationSender ); }); @@ -80,41 +83,27 @@ describe("ETANotificationScheduler", () => { return { eta, notificationData1, notificationData2 }; } - describe("scheduleNotification", () => { - it("schedules the notification", async () => { - // arrange - const notificationData = { - deviceId: "1", - shuttleId: "1", - stopId: "1", - secondsThreshold: 120, - }; - - await notificationService.scheduleNotification(notificationData); - - const isNotificationScheduled = notificationService.isNotificationScheduled(notificationData); - expect(isNotificationScheduled).toEqual(true); - }); - + describe("etaSubscriberCallback", () => { it("sends and clears correct notification after ETA changed", async () => { // Arrange - const shuttle = await addMockShuttleToRepository(repository, "1"); - const stop = await addMockStopToRepository(repository, "1"); + const shuttle = await addMockShuttleToRepository(shuttleRepository, "1"); + const stop = await addMockStopToRepository(shuttleRepository, "1"); const { eta, notificationData1, notificationData2 } = generateNotificationDataAndEta(shuttle, stop); // Act - await notificationService.scheduleNotification(notificationData1); - await notificationService.scheduleNotification(notificationData2); - await repository.addOrUpdateEta(eta); + await notificationRepository.addOrUpdateNotification(notificationData1); + await notificationRepository.addOrUpdateNotification(notificationData2); + await shuttleRepository.addOrUpdateEta(eta); // Assert // Because repository publisher calls subscriber without await // wait for the change to occur first - await waitForCondition(() => !notificationService.isNotificationScheduled(notificationData1)); + await waitForCondition(() => !notificationRepository.isNotificationScheduled(notificationData1)); + + const isFirstNotificationScheduled = await notificationRepository.isNotificationScheduled(notificationData1); + const isSecondNotificationScheduled = await notificationRepository.isNotificationScheduled(notificationData2); - const isFirstNotificationScheduled = notificationService.isNotificationScheduled(notificationData1); - const isSecondNotificationScheduled = notificationService.isNotificationScheduled(notificationData2); // No longer scheduled after being sent expect(isFirstNotificationScheduled).toBe(false); expect(isSecondNotificationScheduled).toBe(false); @@ -122,64 +111,44 @@ describe("ETANotificationScheduler", () => { 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 shuttle = await addMockShuttleToRepository(shuttleRepository, "1"); + const stop = await addMockStopToRepository(shuttleRepository, "1"); const { eta, notificationData1 } = generateNotificationDataAndEta(shuttle, stop); notificationData1.secondsThreshold = eta.secondsRemaining - 10; // Act - await notificationService.scheduleNotification(notificationData1); - await repository.addOrUpdateEta(eta); + await notificationRepository.addOrUpdateNotification(notificationData1); + await shuttleRepository.addOrUpdateEta(eta); // Assert await waitForMilliseconds(500); - const isNotificationScheduled = notificationService.isNotificationScheduled(notificationData1); + const isNotificationScheduled = await notificationRepository.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 shuttle = await addMockShuttleToRepository(shuttleRepository, "1"); + const stop = await addMockStopToRepository(shuttleRepository, "1"); const { eta, notificationData1 } = generateNotificationDataAndEta(shuttle, stop) mockNotificationSenderMethods(false); notificationService = new ETANotificationScheduler( - repository, + shuttleRepository, new InMemoryNotificationRepository(), new MockAppleNotificationSender(), ) // Act - await notificationService.scheduleNotification(notificationData1); - await repository.addOrUpdateEta(eta); + await notificationRepository.addOrUpdateNotification(notificationData1); + await shuttleRepository.addOrUpdateEta(eta); // Assert // The notification should stay scheduled to be retried once // the ETA updates again await waitForMilliseconds(500); - const isNotificationScheduled = notificationService.isNotificationScheduled(notificationData1); + const isNotificationScheduled = await notificationRepository.isNotificationScheduled(notificationData1); expect(isNotificationScheduled).toBe(true); }); }); - - - 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); - - await notificationService.scheduleNotification(notificationData1); - - // Act - await notificationService.cancelNotificationIfExists(notificationData1); - await repository.addOrUpdateEta(eta); - - // Assert - await waitForMilliseconds(500); - expect(http2.connect as jest.Mock).toHaveBeenCalledTimes(0); - }); - }); }); From b2fb430a388a19d58ef9f64d051b6db8b926fb77 Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Thu, 27 Mar 2025 11:08:40 -0700 Subject: [PATCH 24/33] add notification event subscriber/unsubscriber --- src/repositories/NotificationRepository.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/repositories/NotificationRepository.ts b/src/repositories/NotificationRepository.ts index 6914bf8..8e6fdef 100644 --- a/src/repositories/NotificationRepository.ts +++ b/src/repositories/NotificationRepository.ts @@ -14,10 +14,18 @@ export interface ScheduledNotification extends NotificationLookupArguments { secondsThreshold: number; } +export interface NotificationEvent { + notification: ScheduledNotification, + event: 'delete' | 'addOrUpdate' +} + export interface NotificationRepository { getAllNotificationsForShuttleAndStopId(shuttleId: string, stopId: string): Promise; getSecondsThresholdForNotificationIfExists(lookupArguments: NotificationLookupArguments): Promise; isNotificationScheduled(lookupArguments: NotificationLookupArguments): Promise; addOrUpdateNotification(notification: ScheduledNotification): Promise; deleteNotificationIfExists(lookupArguments: NotificationLookupArguments): Promise; + + subscribeToNotificationChanges(listener: (event: NotificationEvent) => any): void; + unsubscribeFromNotificationChanges(listener: (event: NotificationEvent) => any): void; } From b0f04a92561590da34a94eb32b8baaf64c359abd Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Thu, 27 Mar 2025 11:10:49 -0700 Subject: [PATCH 25/33] add stub methods for subscribe/unsubscribe --- .../InMemoryNotificationRepository.ts | 15 ++++++++++++++- src/repositories/NotificationRepository.ts | 6 ++++-- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/src/repositories/InMemoryNotificationRepository.ts b/src/repositories/InMemoryNotificationRepository.ts index ecd3e7b..78a4834 100644 --- a/src/repositories/InMemoryNotificationRepository.ts +++ b/src/repositories/InMemoryNotificationRepository.ts @@ -1,4 +1,10 @@ -import { NotificationLookupArguments, NotificationRepository, ScheduledNotification } from "./NotificationRepository"; +import { + Listener, + NotificationEvent, + NotificationLookupArguments, + NotificationRepository, + ScheduledNotification +} from "./NotificationRepository"; import { TupleKey } from "../types/TupleKey"; type DeviceIdSecondsThresholdAssociation = { [key: string]: number }; @@ -13,6 +19,8 @@ export class InMemoryNotificationRepository implements NotificationRepository { */ private deviceIdsToDeliverTo: { [key: string]: DeviceIdSecondsThresholdAssociation } = {} + private subscribers: Listener[] = []; + async getAllNotificationsForShuttleAndStopId(shuttleId: string, stopId: string) { const tuple = new TupleKey(shuttleId, stopId); if (this.deviceIdsToDeliverTo[tuple.toString()] === undefined) { @@ -76,4 +84,9 @@ export class InMemoryNotificationRepository implements NotificationRepository { delete this.deviceIdsToDeliverTo[tupleKey.toString()][deviceId]; } + public subscribeToNotificationChanges(listener: Listener): void { + } + + public unsubscribeFromNotificationChanges(listener: Listener): void { + } } diff --git a/src/repositories/NotificationRepository.ts b/src/repositories/NotificationRepository.ts index 8e6fdef..0f4ff86 100644 --- a/src/repositories/NotificationRepository.ts +++ b/src/repositories/NotificationRepository.ts @@ -14,6 +14,8 @@ export interface ScheduledNotification extends NotificationLookupArguments { secondsThreshold: number; } +export type Listener = ((event: NotificationEvent) => any); + export interface NotificationEvent { notification: ScheduledNotification, event: 'delete' | 'addOrUpdate' @@ -26,6 +28,6 @@ export interface NotificationRepository { addOrUpdateNotification(notification: ScheduledNotification): Promise; deleteNotificationIfExists(lookupArguments: NotificationLookupArguments): Promise; - subscribeToNotificationChanges(listener: (event: NotificationEvent) => any): void; - unsubscribeFromNotificationChanges(listener: (event: NotificationEvent) => any): void; + subscribeToNotificationChanges(listener: Listener): void; + unsubscribeFromNotificationChanges(listener: Listener): void; } From 51d66d88865ce8e4ccf70b4e4375c1c8ada9e239 Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Thu, 27 Mar 2025 11:18:43 -0700 Subject: [PATCH 26/33] add tests and implementation for notification deletes --- .../InMemoryNotificationRepository.ts | 24 ++++++++++++- ...nMemoryNotificationRepositoryTests.test.ts | 35 +++++++++++++++---- 2 files changed, 52 insertions(+), 7 deletions(-) diff --git a/src/repositories/InMemoryNotificationRepository.ts b/src/repositories/InMemoryNotificationRepository.ts index 78a4834..e543e8c 100644 --- a/src/repositories/InMemoryNotificationRepository.ts +++ b/src/repositories/InMemoryNotificationRepository.ts @@ -19,7 +19,7 @@ export class InMemoryNotificationRepository implements NotificationRepository { */ private deviceIdsToDeliverTo: { [key: string]: DeviceIdSecondsThresholdAssociation } = {} - private subscribers: Listener[] = []; + private listeners: Listener[] = []; async getAllNotificationsForShuttleAndStopId(shuttleId: string, stopId: string) { const tuple = new TupleKey(shuttleId, stopId); @@ -81,12 +81,34 @@ export class InMemoryNotificationRepository implements NotificationRepository { return; } + const secondsThreshold = this.deviceIdsToDeliverTo[tupleKey.toString()][deviceId]; delete this.deviceIdsToDeliverTo[tupleKey.toString()][deviceId]; + this.listeners.forEach((listener) => { + const event: NotificationEvent = { + event: 'delete', + notification: { + deviceId, + shuttleId, + stopId, + secondsThreshold + } + } + + listener(event); + }) } public subscribeToNotificationChanges(listener: Listener): void { + const index = this.listeners.findIndex((existingListener) => existingListener == listener); + if (index < 0) { + this.listeners.push(listener); + } } public unsubscribeFromNotificationChanges(listener: Listener): void { + const index = this.listeners.findIndex((existingListener) => existingListener == listener); + if (index >= 0) { + this.listeners.splice(index, 1); + } } } diff --git a/test/repositories/InMemoryNotificationRepositoryTests.test.ts b/test/repositories/InMemoryNotificationRepositoryTests.test.ts index 20ce245..1feb9c3 100644 --- a/test/repositories/InMemoryNotificationRepositoryTests.test.ts +++ b/test/repositories/InMemoryNotificationRepositoryTests.test.ts @@ -1,5 +1,6 @@ -import { beforeEach, describe, expect, it } from "@jest/globals"; +import { beforeEach, describe, expect, it, jest } from "@jest/globals"; import { InMemoryNotificationRepository } from "../../src/repositories/InMemoryNotificationRepository"; +import { NotificationEvent } from "../../src/repositories/NotificationRepository"; describe("InMemoryNotificationRepository", () => { let repo: InMemoryNotificationRepository; @@ -71,16 +72,13 @@ describe("InMemoryNotificationRepository", () => { describe("deleteNotificationIfExists", () => { it("deletes the notification", async () => { await repo.addOrUpdateNotification(notification); - await repo.deleteNotificationIfExists({ - deviceId: "device1", - shuttleId: "shuttle1", - stopId: "stop1" - }); + await repo.deleteNotificationIfExists(notification); const result = await repo.getAllNotificationsForShuttleAndStopId("shuttle1", "stop1"); expect(result).toHaveLength(0); }); + it("does nothing if there's no notification", async () => { await expect(repo.deleteNotificationIfExists({ deviceId: "device1", @@ -89,4 +87,29 @@ describe("InMemoryNotificationRepository", () => { })).resolves.not.toThrow(); }); }); + + describe("subscribeToNotificationChanges", () => { + it("calls subscribers when something is added", async () => { + + }); + + it("calls subscribers when something is deleted", async () => { + await repo.addOrUpdateNotification(notification); + + const mockCallback = jest.fn(); + repo.subscribeToNotificationChanges(mockCallback); + + await repo.deleteNotificationIfExists(notification); + + expect(mockCallback).toHaveBeenCalledTimes(1); + + const expectedEvent: NotificationEvent = { + event: 'delete', + notification: { + ...notification, + }, + }; + expect(mockCallback).toHaveBeenCalledWith(expectedEvent); + }); + }) }); From a84cedd05aba689da951f21ff4f318f857678ded Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Thu, 27 Mar 2025 11:23:07 -0700 Subject: [PATCH 27/33] add test and implementation for addOrUpdate listeners --- .../InMemoryNotificationRepository.ts | 13 +++++++ ...nMemoryNotificationRepositoryTests.test.ts | 35 +++++++++++++++++-- 2 files changed, 45 insertions(+), 3 deletions(-) diff --git a/src/repositories/InMemoryNotificationRepository.ts b/src/repositories/InMemoryNotificationRepository.ts index e543e8c..d93af1f 100644 --- a/src/repositories/InMemoryNotificationRepository.ts +++ b/src/repositories/InMemoryNotificationRepository.ts @@ -66,6 +66,19 @@ export class InMemoryNotificationRepository implements NotificationRepository { } this.deviceIdsToDeliverTo[tuple.toString()][deviceId] = secondsThreshold; + this.listeners.forEach((listener: Listener) => { + const event: NotificationEvent = { + event: 'addOrUpdate', + notification: { + shuttleId, + stopId, + deviceId, + secondsThreshold + }, + } + + listener(event); + }) } async deleteNotificationIfExists({ diff --git a/test/repositories/InMemoryNotificationRepositoryTests.test.ts b/test/repositories/InMemoryNotificationRepositoryTests.test.ts index 1feb9c3..3419d96 100644 --- a/test/repositories/InMemoryNotificationRepositoryTests.test.ts +++ b/test/repositories/InMemoryNotificationRepositoryTests.test.ts @@ -90,7 +90,38 @@ describe("InMemoryNotificationRepository", () => { describe("subscribeToNotificationChanges", () => { it("calls subscribers when something is added", async () => { + const mockCallback = jest.fn(); + repo.subscribeToNotificationChanges(mockCallback); + await repo.addOrUpdateNotification(notification); + + const expectedEvent: NotificationEvent = { + event: 'addOrUpdate', + notification, + } + expect(mockCallback).toHaveBeenCalledTimes(1); + expect(mockCallback).toHaveBeenCalledWith(expectedEvent); + }); + + it("calls subscribers when something is updated", async () => { + const mockCallback = jest.fn(); + repo.subscribeToNotificationChanges(mockCallback); + + await repo.addOrUpdateNotification(notification); + + const updatedNotification = { + ...notification, + secondsThreshold: notification.secondsThreshold + 60, + }; + + await repo.addOrUpdateNotification(updatedNotification); + + const expectedEvent: NotificationEvent = { + event: 'addOrUpdate', + notification, + } + expect(mockCallback).toHaveBeenCalledTimes(2); + expect(mockCallback).toHaveBeenCalledWith(expectedEvent); }); it("calls subscribers when something is deleted", async () => { @@ -105,9 +136,7 @@ describe("InMemoryNotificationRepository", () => { const expectedEvent: NotificationEvent = { event: 'delete', - notification: { - ...notification, - }, + notification, }; expect(mockCallback).toHaveBeenCalledWith(expectedEvent); }); From 9c22e154be54f49e53da77d7253f4be9142eec28 Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Thu, 27 Mar 2025 11:34:01 -0700 Subject: [PATCH 28/33] add bindings for the notifications repository --- src/repositories/InMemoryNotificationRepository.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/repositories/InMemoryNotificationRepository.ts b/src/repositories/InMemoryNotificationRepository.ts index d93af1f..bd1db3f 100644 --- a/src/repositories/InMemoryNotificationRepository.ts +++ b/src/repositories/InMemoryNotificationRepository.ts @@ -21,6 +21,16 @@ export class InMemoryNotificationRepository implements NotificationRepository { private listeners: Listener[] = []; + constructor() { + this.getAllNotificationsForShuttleAndStopId = this.getAllNotificationsForShuttleAndStopId.bind(this); + this.getSecondsThresholdForNotificationIfExists = this.getSecondsThresholdForNotificationIfExists.bind(this); + this.deleteNotificationIfExists = this.deleteNotificationIfExists.bind(this); + this.addOrUpdateNotification = this.addOrUpdateNotification.bind(this); + this.isNotificationScheduled = this.isNotificationScheduled.bind(this); + this.subscribeToNotificationChanges = this.subscribeToNotificationChanges.bind(this); + this.unsubscribeFromNotificationChanges = this.unsubscribeFromNotificationChanges.bind(this); + } + async getAllNotificationsForShuttleAndStopId(shuttleId: string, stopId: string) { const tuple = new TupleKey(shuttleId, stopId); if (this.deviceIdsToDeliverTo[tuple.toString()] === undefined) { @@ -51,7 +61,8 @@ export class InMemoryNotificationRepository implements NotificationRepository { } async isNotificationScheduled(lookupArguments: NotificationLookupArguments): Promise { - return await this.getSecondsThresholdForNotificationIfExists(lookupArguments) !== null; + const threshold = await this.getSecondsThresholdForNotificationIfExists(lookupArguments); + return threshold !== null; } async addOrUpdateNotification({ From 75a4e133eda212ff9afff7b841f66cadbfc4089a Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Thu, 27 Mar 2025 11:40:32 -0700 Subject: [PATCH 29/33] fix failing test for notification deletion --- src/repositories/InMemoryNotificationRepository.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/repositories/InMemoryNotificationRepository.ts b/src/repositories/InMemoryNotificationRepository.ts index bd1db3f..513f7b7 100644 --- a/src/repositories/InMemoryNotificationRepository.ts +++ b/src/repositories/InMemoryNotificationRepository.ts @@ -107,6 +107,12 @@ export class InMemoryNotificationRepository implements NotificationRepository { const secondsThreshold = this.deviceIdsToDeliverTo[tupleKey.toString()][deviceId]; delete this.deviceIdsToDeliverTo[tupleKey.toString()][deviceId]; + + if (Object.keys(this.deviceIdsToDeliverTo[tupleKey.toString()]).length === 0) { + // no more device IDs remaining for this key combination + delete this.deviceIdsToDeliverTo[tupleKey.toString()]; + } + this.listeners.forEach((listener) => { const event: NotificationEvent = { event: 'delete', From 02b3b77a61929273dd4738f377bd48e5274ab8b0 Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Mon, 31 Mar 2025 18:56:02 -0700 Subject: [PATCH 30/33] bind apple notification sender methods --- src/notifications/senders/AppleNotificationSender.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/notifications/senders/AppleNotificationSender.ts b/src/notifications/senders/AppleNotificationSender.ts index d64df98..8e5904a 100644 --- a/src/notifications/senders/AppleNotificationSender.ts +++ b/src/notifications/senders/AppleNotificationSender.ts @@ -17,6 +17,9 @@ export class AppleNotificationSender { private _lastRefreshedTimeMs: number | undefined = undefined; constructor(private shouldActuallySendNotifications = true) { + this.sendNotificationImmediately = this.sendNotificationImmediately.bind(this); + this.lastReloadedTimeForAPNsIsTooRecent = this.lastReloadedTimeForAPNsIsTooRecent.bind(this); + this.reloadAPNsTokenIfTimePassed = this.reloadAPNsTokenIfTimePassed.bind(this); } get lastRefreshedTimeMs(): number | undefined { From ef94055133825af8d767ca92402186c48751b1bf Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Mon, 31 Mar 2025 19:17:41 -0700 Subject: [PATCH 31/33] add start and stop methods, move subscribe out of constructor --- .../schedulers/ETANotificationScheduler.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/notifications/schedulers/ETANotificationScheduler.ts b/src/notifications/schedulers/ETANotificationScheduler.ts index 943b74d..98a16a2 100644 --- a/src/notifications/schedulers/ETANotificationScheduler.ts +++ b/src/notifications/schedulers/ETANotificationScheduler.ts @@ -18,8 +18,6 @@ export class ETANotificationScheduler { this.etaSubscriberCallback = this.etaSubscriberCallback.bind(this); this.sendEtaNotificationImmediately = this.sendEtaNotificationImmediately.bind(this); this.sendEtaNotificationImmediatelyIfSecondsRemainingBelowThreshold = this.sendEtaNotificationImmediatelyIfSecondsRemainingBelowThreshold.bind(this); - - this.shuttleRepository.subscribeToEtaUpdates(this.etaSubscriberCallback); } private async sendEtaNotificationImmediately(notificationData: ScheduledNotification): Promise { @@ -81,4 +79,13 @@ export class ETANotificationScheduler { 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); + } } From a95c89c15b42fba706c352e3cf7a984d4b6b49f7 Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Mon, 31 Mar 2025 19:21:10 -0700 Subject: [PATCH 32/33] update tests and index with updated scheduler interface --- src/index.ts | 3 ++ .../ETANotificationSchedulerTests.test.ts | 36 +++++++------------ 2 files changed, 15 insertions(+), 24 deletions(-) diff --git a/src/index.ts b/src/index.ts index 75b9d20..d1a1d14 100644 --- a/src/index.ts +++ b/src/index.ts @@ -30,11 +30,13 @@ async function main() { const appleNotificationSender = new AppleNotificationSender(false); notificationRepository = new InMemoryNotificationRepository(); + notificationService = new ETANotificationScheduler( shuttleRepository, notificationRepository, appleNotificationSender ); + notificationService.startListeningForUpdates(); } else { const repositoryDataUpdater = new TimedApiBasedShuttleRepositoryLoader( shuttleRepository, @@ -46,6 +48,7 @@ async function main() { shuttleRepository, notificationRepository ); + notificationService.startListeningForUpdates(); } const { url } = await startStandaloneServer(server, { diff --git a/test/notifications/schedulers/ETANotificationSchedulerTests.test.ts b/test/notifications/schedulers/ETANotificationSchedulerTests.test.ts index cadee33..974774d 100644 --- a/test/notifications/schedulers/ETANotificationSchedulerTests.test.ts +++ b/test/notifications/schedulers/ETANotificationSchedulerTests.test.ts @@ -1,7 +1,6 @@ import { beforeEach, describe, expect, it, jest } from "@jest/globals"; import { ETANotificationScheduler } from "../../../src/notifications/schedulers/ETANotificationScheduler"; import { UnoptimizedInMemoryShuttleRepository } from "../../../src/repositories/UnoptimizedInMemoryShuttleRepository"; -import http2 from "http2"; import { IEta, IShuttle, IStop } from "../../../src/entities/entities"; import { addMockShuttleToRepository, addMockStopToRepository } from "../../testHelpers/repositorySetupHelpers"; import { AppleNotificationSender } from "../../../src/notifications/senders/AppleNotificationSender"; @@ -17,23 +16,6 @@ function mockNotificationSenderMethods(shouldSimulateNotificationSend: boolean) MockAppleNotificationSender.prototype.sendNotificationImmediately = jest.fn(async () => shouldSimulateNotificationSend); } -/** - * Wait for a condition to become true until the timeout - * is hit. - * @param condition - * @param timeoutMilliseconds - * @param intervalMilliseconds - */ -async function waitForCondition(condition: () => boolean, timeoutMilliseconds = 5000, intervalMilliseconds = 500) { - const startTime = Date.now(); - while (!condition()) { - if (Date.now() - startTime > timeoutMilliseconds) { - throw new Error("Timeout waiting for condition"); - } - await new Promise((resolve) => setTimeout(resolve, intervalMilliseconds)); - } -} - /** * Wait for a specified number of milliseconds. * @param ms @@ -60,6 +42,7 @@ describe("ETANotificationScheduler", () => { notificationRepository, appleNotificationSender ); + notificationService.startListeningForUpdates(); }); function generateNotificationDataAndEta(shuttle: IShuttle, stop: IStop) { @@ -97,9 +80,8 @@ describe("ETANotificationScheduler", () => { await shuttleRepository.addOrUpdateEta(eta); // Assert - // Because repository publisher calls subscriber without await - // wait for the change to occur first - await waitForCondition(() => !notificationRepository.isNotificationScheduled(notificationData1)); + // Wait for the callback to actually be called + await waitForMilliseconds(1000); const isFirstNotificationScheduled = await notificationRepository.isNotificationScheduled(notificationData1); const isSecondNotificationScheduled = await notificationRepository.isNotificationScheduled(notificationData2); @@ -132,12 +114,18 @@ describe("ETANotificationScheduler", () => { const stop = await addMockStopToRepository(shuttleRepository, "1"); const { eta, notificationData1 } = generateNotificationDataAndEta(shuttle, stop) + // replace the old notification scheduler with a new one + // detach the old callback method from the shuttle repo + notificationService.stopListeningForUpdates(); + mockNotificationSenderMethods(false); + const updatedNotificationSender = new MockAppleNotificationSender(false); notificationService = new ETANotificationScheduler( shuttleRepository, - new InMemoryNotificationRepository(), - new MockAppleNotificationSender(), - ) + notificationRepository, + updatedNotificationSender + ); + notificationService.startListeningForUpdates(); // Act await notificationRepository.addOrUpdateNotification(notificationData1); From c59ccd7f1a2f42b9a1684297799e368d7425cf86 Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Mon, 31 Mar 2025 19:24:34 -0700 Subject: [PATCH 33/33] replace notifications repository with a fresh one in the test --- .../schedulers/ETANotificationSchedulerTests.test.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/notifications/schedulers/ETANotificationSchedulerTests.test.ts b/test/notifications/schedulers/ETANotificationSchedulerTests.test.ts index 974774d..7d9392f 100644 --- a/test/notifications/schedulers/ETANotificationSchedulerTests.test.ts +++ b/test/notifications/schedulers/ETANotificationSchedulerTests.test.ts @@ -118,6 +118,9 @@ describe("ETANotificationScheduler", () => { // detach the old callback method from the shuttle repo notificationService.stopListeningForUpdates(); + // replace the notification repository with a fresh one too + const notificationRepository = new InMemoryNotificationRepository(); + mockNotificationSenderMethods(false); const updatedNotificationSender = new MockAppleNotificationSender(false); notificationService = new ETANotificationScheduler(