From 1acd12d1135a1ec2261db2f9ff05b88ebaa0ba25 Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Mon, 31 Mar 2025 19:30:42 -0700 Subject: [PATCH 01/17] add stub methods for redis notification repository --- .../RedisNotificationRepository.ts | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 src/repositories/RedisNotificationRepository.ts diff --git a/src/repositories/RedisNotificationRepository.ts b/src/repositories/RedisNotificationRepository.ts new file mode 100644 index 0000000..ad31307 --- /dev/null +++ b/src/repositories/RedisNotificationRepository.ts @@ -0,0 +1,33 @@ +import { + Listener, + NotificationLookupArguments, + NotificationRepository, + ScheduledNotification +} from "./NotificationRepository"; + +class RedisNotificationRepository implements NotificationRepository { + addOrUpdateNotification(notification: ScheduledNotification): Promise { + return Promise.resolve(undefined); + } + + public async deleteNotificationIfExists(lookupArguments: NotificationLookupArguments): Promise { + } + + public async getAllNotificationsForShuttleAndStopId(shuttleId: string, stopId: string): Promise { + return []; + } + + public async getSecondsThresholdForNotificationIfExists(lookupArguments: NotificationLookupArguments): Promise { + return null; + } + + public async isNotificationScheduled(lookupArguments: NotificationLookupArguments): Promise { + return false; + } + + subscribeToNotificationChanges(listener: Listener): void { + } + + unsubscribeFromNotificationChanges(listener: Listener): void { + } +} From bef2ce18fb5dc193e21496435a573027bf384d0a Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Mon, 31 Mar 2025 19:35:28 -0700 Subject: [PATCH 02/17] add redis types --- package-lock.json | 11 +++++++++++ package.json | 1 + 2 files changed, 12 insertions(+) diff --git a/package-lock.json b/package-lock.json index 2bbd249..32b1839 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,6 +20,7 @@ "@jest/globals": "^29.7.0", "@types/jsonwebtoken": "^9.0.8", "@types/node": "^22.10.2", + "@types/redis": "^4.0.11", "jest": "^29.7.0", "ts-jest": "^29.2.5", "typescript": "^5.7.2" @@ -3679,6 +3680,16 @@ "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==" }, + "node_modules/@types/redis": { + "version": "4.0.11", + "resolved": "https://registry.npmjs.org/@types/redis/-/redis-4.0.11.tgz", + "integrity": "sha512-bI+gth8La8Wg/QCR1+V1fhrL9+LZUSWfcqpOj2Kc80ZQ4ffbdL173vQd5wovmoV9i071FU9oP2g6etLuEwb6Rg==", + "deprecated": "This is a stub types definition. redis provides its own type definitions, so you do not need this installed.", + "dev": true, + "dependencies": { + "redis": "*" + } + }, "node_modules/@types/send": { "version": "0.17.4", "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", diff --git a/package.json b/package.json index 7be6333..9ee704a 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "@jest/globals": "^29.7.0", "@types/jsonwebtoken": "^9.0.8", "@types/node": "^22.10.2", + "@types/redis": "^4.0.11", "jest": "^29.7.0", "ts-jest": "^29.2.5", "typescript": "^5.7.2" From c98367f12ead9b84b9ecc21bd03e5bfc1b1286f2 Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Mon, 31 Mar 2025 19:35:38 -0700 Subject: [PATCH 03/17] add bindings and redis client arg for constructor --- .../RedisNotificationRepository.ts | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/repositories/RedisNotificationRepository.ts b/src/repositories/RedisNotificationRepository.ts index ad31307..8480877 100644 --- a/src/repositories/RedisNotificationRepository.ts +++ b/src/repositories/RedisNotificationRepository.ts @@ -4,10 +4,24 @@ import { NotificationRepository, ScheduledNotification } from "./NotificationRepository"; +import { createClient, RedisClientType } from "redis"; class RedisNotificationRepository implements NotificationRepository { - addOrUpdateNotification(notification: ScheduledNotification): Promise { - return Promise.resolve(undefined); + constructor( + private redisClient = createClient({ + url: process.env.REDIS_URL, + }), + ) { + 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); + } + + public async addOrUpdateNotification(notification: ScheduledNotification): Promise { } public async deleteNotificationIfExists(lookupArguments: NotificationLookupArguments): Promise { From cc6623404c2a4081af9f6ffc9a285d983e724901 Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Mon, 31 Mar 2025 19:37:19 -0700 Subject: [PATCH 04/17] add connect/disconnect methods --- src/repositories/RedisNotificationRepository.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/repositories/RedisNotificationRepository.ts b/src/repositories/RedisNotificationRepository.ts index 8480877..6397140 100644 --- a/src/repositories/RedisNotificationRepository.ts +++ b/src/repositories/RedisNotificationRepository.ts @@ -21,6 +21,14 @@ class RedisNotificationRepository implements NotificationRepository { this.unsubscribeFromNotificationChanges = this.unsubscribeFromNotificationChanges.bind(this); } + public async connect() { + await this.redisClient.connect(); + } + + public async disconnect() { + await this.redisClient.disconnect(); + } + public async addOrUpdateNotification(notification: ScheduledNotification): Promise { } From 36359a4caaa8ff87611aa68ae60afc27af61f8b7 Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Mon, 31 Mar 2025 19:42:47 -0700 Subject: [PATCH 05/17] add export and getter to check connection status --- src/repositories/RedisNotificationRepository.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/repositories/RedisNotificationRepository.ts b/src/repositories/RedisNotificationRepository.ts index 6397140..a1522ed 100644 --- a/src/repositories/RedisNotificationRepository.ts +++ b/src/repositories/RedisNotificationRepository.ts @@ -4,9 +4,9 @@ import { NotificationRepository, ScheduledNotification } from "./NotificationRepository"; -import { createClient, RedisClientType } from "redis"; +import { createClient } from "redis"; -class RedisNotificationRepository implements NotificationRepository { +export class RedisNotificationRepository implements NotificationRepository { constructor( private redisClient = createClient({ url: process.env.REDIS_URL, @@ -21,6 +21,10 @@ class RedisNotificationRepository implements NotificationRepository { this.unsubscribeFromNotificationChanges = this.unsubscribeFromNotificationChanges.bind(this); } + get isReady() { + return this.redisClient.isReady + } + public async connect() { await this.redisClient.connect(); } From a7ac9888f68f28e871d56af22deb125a4d58514e Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Mon, 31 Mar 2025 19:43:17 -0700 Subject: [PATCH 06/17] add setup for repository tests --- .../RedisNotificationRepositoryTests.test.ts | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 test/repositories/RedisNotificationRepositoryTests.test.ts diff --git a/test/repositories/RedisNotificationRepositoryTests.test.ts b/test/repositories/RedisNotificationRepositoryTests.test.ts new file mode 100644 index 0000000..0eb4a92 --- /dev/null +++ b/test/repositories/RedisNotificationRepositoryTests.test.ts @@ -0,0 +1,22 @@ +import { afterEach, beforeEach, describe } from "@jest/globals"; +import { createClient, RedisClientType } from "redis"; +import { RedisNotificationRepository } from "../../src/repositories/RedisNotificationRepository"; + +describe("RedisNotificationRepository", () => { + let redisClient: RedisClientType; + let repository: RedisNotificationRepository; + + beforeEach(async () => { + redisClient = createClient({ + url: process.env.REDIS_URL, + }); + repository = new RedisNotificationRepository( + redisClient + ); + await repository.connect(); + }); + + afterEach(async () => { + await repository.disconnect(); + }) +}); From 7a5e1b8561b5ddcb6c0208f8814905615998489e Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Mon, 31 Mar 2025 19:50:37 -0700 Subject: [PATCH 07/17] use describe.each to test the multiple implementations --- ...test.ts => NotificationRepositoryTests.ts} | 22 ++++++++++++++----- .../RedisNotificationRepositoryTests.test.ts | 22 ------------------- 2 files changed, 17 insertions(+), 27 deletions(-) rename test/repositories/{InMemoryNotificationRepositoryTests.test.ts => NotificationRepositoryTests.ts} (88%) delete mode 100644 test/repositories/RedisNotificationRepositoryTests.test.ts diff --git a/test/repositories/InMemoryNotificationRepositoryTests.test.ts b/test/repositories/NotificationRepositoryTests.ts similarity index 88% rename from test/repositories/InMemoryNotificationRepositoryTests.test.ts rename to test/repositories/NotificationRepositoryTests.ts index 3419d96..5216224 100644 --- a/test/repositories/InMemoryNotificationRepositoryTests.test.ts +++ b/test/repositories/NotificationRepositoryTests.ts @@ -1,13 +1,25 @@ import { beforeEach, describe, expect, it, jest } from "@jest/globals"; import { InMemoryNotificationRepository } from "../../src/repositories/InMemoryNotificationRepository"; -import { NotificationEvent } from "../../src/repositories/NotificationRepository"; +import { NotificationEvent, NotificationRepository } from "../../src/repositories/NotificationRepository"; +import { RedisNotificationRepository } from "../../src/repositories/RedisNotificationRepository"; -describe("InMemoryNotificationRepository", () => { - let repo: InMemoryNotificationRepository; +const repositoryImplementations = [ + { + name: 'InMemoryNotificationRepository', + factory: () => new InMemoryNotificationRepository(), + }, + { + name: 'RedisNotificationRepository', + factory: () => new RedisNotificationRepository(), + }, +] + +describe.each(repositoryImplementations)('$name', ({ factory }) => { + let repo: NotificationRepository; beforeEach(() => { - repo = new InMemoryNotificationRepository(); - }) + repo = factory(); + }); const notification = { deviceId: "device1", diff --git a/test/repositories/RedisNotificationRepositoryTests.test.ts b/test/repositories/RedisNotificationRepositoryTests.test.ts deleted file mode 100644 index 0eb4a92..0000000 --- a/test/repositories/RedisNotificationRepositoryTests.test.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { afterEach, beforeEach, describe } from "@jest/globals"; -import { createClient, RedisClientType } from "redis"; -import { RedisNotificationRepository } from "../../src/repositories/RedisNotificationRepository"; - -describe("RedisNotificationRepository", () => { - let redisClient: RedisClientType; - let repository: RedisNotificationRepository; - - beforeEach(async () => { - redisClient = createClient({ - url: process.env.REDIS_URL, - }); - repository = new RedisNotificationRepository( - redisClient - ); - await repository.connect(); - }); - - afterEach(async () => { - await repository.disconnect(); - }) -}); From f34a2f27d74f5e0b62c4eb9f8ada0eff88ffd803 Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Mon, 31 Mar 2025 19:52:51 -0700 Subject: [PATCH 08/17] add redis notification repository tests back for edge cases --- ...ryTests.ts => NotificationRepositorySharedTests.test.ts} | 0 test/repositories/RedisNotificationRepositoryTests.test.ts | 6 ++++++ 2 files changed, 6 insertions(+) rename test/repositories/{NotificationRepositoryTests.ts => NotificationRepositorySharedTests.test.ts} (100%) create mode 100644 test/repositories/RedisNotificationRepositoryTests.test.ts diff --git a/test/repositories/NotificationRepositoryTests.ts b/test/repositories/NotificationRepositorySharedTests.test.ts similarity index 100% rename from test/repositories/NotificationRepositoryTests.ts rename to test/repositories/NotificationRepositorySharedTests.test.ts diff --git a/test/repositories/RedisNotificationRepositoryTests.test.ts b/test/repositories/RedisNotificationRepositoryTests.test.ts new file mode 100644 index 0000000..7a3dcaf --- /dev/null +++ b/test/repositories/RedisNotificationRepositoryTests.test.ts @@ -0,0 +1,6 @@ +// Test additional edge cases like Redis failing to connect, etc. + +import { describe, it } from "@jest/globals"; + +describe("RedisNotificationRepository", () => { +}); From 998643dc041db188a86376a8bed53be48093a63f Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Mon, 31 Mar 2025 19:55:56 -0700 Subject: [PATCH 09/17] add call to connect method in factory --- .../NotificationRepositorySharedTests.test.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/test/repositories/NotificationRepositorySharedTests.test.ts b/test/repositories/NotificationRepositorySharedTests.test.ts index 5216224..350ae53 100644 --- a/test/repositories/NotificationRepositorySharedTests.test.ts +++ b/test/repositories/NotificationRepositorySharedTests.test.ts @@ -6,19 +6,23 @@ import { RedisNotificationRepository } from "../../src/repositories/RedisNotific const repositoryImplementations = [ { name: 'InMemoryNotificationRepository', - factory: () => new InMemoryNotificationRepository(), + factory: async () => new InMemoryNotificationRepository(), }, { name: 'RedisNotificationRepository', - factory: () => new RedisNotificationRepository(), + factory: async () => { + const repo = new RedisNotificationRepository(); + await repo.connect(); + return repo; + }, }, ] describe.each(repositoryImplementations)('$name', ({ factory }) => { let repo: NotificationRepository; - beforeEach(() => { - repo = factory(); + beforeEach(async () => { + repo = await factory(); }); const notification = { From 3460f1becc88ceedff8f63b065f4c36d2a782660 Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Mon, 31 Mar 2025 20:07:40 -0700 Subject: [PATCH 10/17] update environment variables for testing and app-integration-testing --- docker-compose.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 335f941..e10842b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -33,7 +33,7 @@ services: depends_on: - redis-no-persistence environment: - <<: *common-server-environment + REDIS_URL: redis://redis-no-persistence:6379 volumes: - .:/usr/src/app @@ -44,10 +44,11 @@ services: depends_on: - redis-no-persistence environment: - <<: *common-server-environment + REDIS_URL: redis://redis-no-persistence:6379 volumes: - .:/usr/src/app + redis: image: redis:alpine ports: From 372ecba952448daa4d31265ebf40bef6efa4cacc Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Mon, 31 Mar 2025 20:07:55 -0700 Subject: [PATCH 11/17] restructure implementation holders into classes with teardown --- .../NotificationRepositorySharedTests.test.ts | 54 +++++++++++++------ 1 file changed, 39 insertions(+), 15 deletions(-) diff --git a/test/repositories/NotificationRepositorySharedTests.test.ts b/test/repositories/NotificationRepositorySharedTests.test.ts index 350ae53..282776a 100644 --- a/test/repositories/NotificationRepositorySharedTests.test.ts +++ b/test/repositories/NotificationRepositorySharedTests.test.ts @@ -1,30 +1,54 @@ -import { beforeEach, describe, expect, it, jest } from "@jest/globals"; +import { afterEach, beforeEach, describe, expect, it, jest } from "@jest/globals"; import { InMemoryNotificationRepository } from "../../src/repositories/InMemoryNotificationRepository"; import { NotificationEvent, NotificationRepository } from "../../src/repositories/NotificationRepository"; import { RedisNotificationRepository } from "../../src/repositories/RedisNotificationRepository"; +interface RepositoryHolder { + name: string; + factory(): Promise, + teardown(): Promise, +} + +class InMemoryRepositoryHolder implements RepositoryHolder { + name = 'InMemoryNotificationRepository'; + factory = async () => { + return new InMemoryNotificationRepository(); + } + teardown = async () => {} +} + +class RedisNotificationRepositoryHolder implements RepositoryHolder { + repo: RedisNotificationRepository | undefined; + + name = 'RedisNotificationRepository'; + factory = async () => { + this.repo = new RedisNotificationRepository(); + await this.repo.connect(); + return this.repo; + } + teardown = async () => { + if (this.repo) { + await this.repo.disconnect(); + } + } +} + const repositoryImplementations = [ - { - name: 'InMemoryNotificationRepository', - factory: async () => new InMemoryNotificationRepository(), - }, - { - name: 'RedisNotificationRepository', - factory: async () => { - const repo = new RedisNotificationRepository(); - await repo.connect(); - return repo; - }, - }, + new InMemoryRepositoryHolder(), + new RedisNotificationRepositoryHolder(), ] -describe.each(repositoryImplementations)('$name', ({ factory }) => { +describe.each(repositoryImplementations)('$name', (holder) => { let repo: NotificationRepository; beforeEach(async () => { - repo = await factory(); + repo = await holder.factory(); }); + afterEach(async () => { + await holder.teardown(); + }) + const notification = { deviceId: "device1", shuttleId: "shuttle1", From 9efd1d92891e72fc3277d966fbde44b541ace57e Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Mon, 31 Mar 2025 20:19:57 -0700 Subject: [PATCH 12/17] set tests to run sequentially --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9ee704a..c582671 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "start:dev": "npm run build:dev && node ./dist/index.js", "start": "npm run build && node ./dist/index.js", "generate": "graphql-codegen --config codegen.ts", - "test": "npm run build:dev && jest" + "test": "npm run build:dev && jest --runInBand" }, "devDependencies": { "@graphql-codegen/cli": "5.0.3", From 39066b88bc449d3274721146ba3e4d37a05d9762 Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Mon, 31 Mar 2025 20:21:09 -0700 Subject: [PATCH 13/17] have teardown clear all data in redis before starting next test --- src/repositories/RedisNotificationRepository.ts | 6 +++++- test/repositories/NotificationRepositorySharedTests.test.ts | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/repositories/RedisNotificationRepository.ts b/src/repositories/RedisNotificationRepository.ts index a1522ed..76d5765 100644 --- a/src/repositories/RedisNotificationRepository.ts +++ b/src/repositories/RedisNotificationRepository.ts @@ -22,7 +22,7 @@ export class RedisNotificationRepository implements NotificationRepository { } get isReady() { - return this.redisClient.isReady + return this.redisClient.isReady; } public async connect() { @@ -33,6 +33,10 @@ export class RedisNotificationRepository implements NotificationRepository { await this.redisClient.disconnect(); } + public async clearAllData() { + await this.redisClient.flushAll(); + } + public async addOrUpdateNotification(notification: ScheduledNotification): Promise { } diff --git a/test/repositories/NotificationRepositorySharedTests.test.ts b/test/repositories/NotificationRepositorySharedTests.test.ts index 282776a..6205833 100644 --- a/test/repositories/NotificationRepositorySharedTests.test.ts +++ b/test/repositories/NotificationRepositorySharedTests.test.ts @@ -28,6 +28,7 @@ class RedisNotificationRepositoryHolder implements RepositoryHolder { } teardown = async () => { if (this.repo) { + await this.repo.clearAllData(); await this.repo.disconnect(); } } From 50148cc2f4ab18e548236abbd9177a442ea67465 Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Mon, 31 Mar 2025 20:26:26 -0700 Subject: [PATCH 14/17] implement RedisNotificationRepository --- .../RedisNotificationRepository.ts | 96 +++++++++++++++++-- .../RedisNotificationRepositoryTests.test.ts | 6 -- 2 files changed, 88 insertions(+), 14 deletions(-) delete mode 100644 test/repositories/RedisNotificationRepositoryTests.test.ts diff --git a/src/repositories/RedisNotificationRepository.ts b/src/repositories/RedisNotificationRepository.ts index 76d5765..1690c50 100644 --- a/src/repositories/RedisNotificationRepository.ts +++ b/src/repositories/RedisNotificationRepository.ts @@ -1,5 +1,7 @@ +import { TupleKey } from '../types/TupleKey'; import { Listener, + NotificationEvent, NotificationLookupArguments, NotificationRepository, ScheduledNotification @@ -7,6 +9,9 @@ import { import { createClient } from "redis"; export class RedisNotificationRepository implements NotificationRepository { + private listeners: Listener[] = []; + private readonly NOTIFICATION_KEY_PREFIX = 'notification:'; + constructor( private redisClient = createClient({ url: process.env.REDIS_URL, @@ -37,27 +42,102 @@ export class RedisNotificationRepository implements NotificationRepository { await this.redisClient.flushAll(); } + private getNotificationKey(shuttleId: string, stopId: string): string { + const tuple = new TupleKey(shuttleId, stopId); + return `${this.NOTIFICATION_KEY_PREFIX}${tuple.toString()}`; + } + public async addOrUpdateNotification(notification: ScheduledNotification): Promise { + const { shuttleId, stopId, deviceId, secondsThreshold } = notification; + const key = this.getNotificationKey(shuttleId, stopId); + + await this.redisClient.hSet(key, deviceId, secondsThreshold.toString()); + + this.listeners.forEach((listener: Listener) => { + const event: NotificationEvent = { + event: 'addOrUpdate', + notification + }; + listener(event); + }); } public async deleteNotificationIfExists(lookupArguments: NotificationLookupArguments): Promise { + const { shuttleId, stopId, deviceId } = lookupArguments; + const key = this.getNotificationKey(shuttleId, stopId); + + const secondsThreshold = await this.redisClient.hGet(key, deviceId); + if (secondsThreshold) { + await this.redisClient.hDel(key, deviceId); + + // Check if hash is empty and delete it if so + const remainingFields = await this.redisClient.hLen(key); + if (remainingFields === 0) { + await this.redisClient.del(key); + } + + this.listeners.forEach((listener) => { + const event: NotificationEvent = { + event: 'delete', + notification: { + deviceId, + shuttleId, + stopId, + secondsThreshold: parseInt(secondsThreshold) + } + }; + listener(event); + }); + } } - public async getAllNotificationsForShuttleAndStopId(shuttleId: string, stopId: string): Promise { - return []; + public async getAllNotificationsForShuttleAndStopId( + shuttleId: string, + stopId: string + ): Promise { + const key = this.getNotificationKey(shuttleId, stopId); + const allNotifications = await this.redisClient.hGetAll(key); + + return Object.entries(allNotifications).map(([deviceId, secondsThreshold]) => ({ + shuttleId, + stopId, + deviceId, + secondsThreshold: parseInt(secondsThreshold) + })); } - public async getSecondsThresholdForNotificationIfExists(lookupArguments: NotificationLookupArguments): Promise { - return null; + public async getSecondsThresholdForNotificationIfExists( + lookupArguments: NotificationLookupArguments + ): Promise { + const { shuttleId, stopId, deviceId } = lookupArguments; + const key = this.getNotificationKey(shuttleId, stopId); + + const threshold = await this.redisClient.hGet(key, deviceId); + return threshold ? parseInt(threshold) : null; } - public async isNotificationScheduled(lookupArguments: NotificationLookupArguments): Promise { - return false; + public async isNotificationScheduled( + lookupArguments: NotificationLookupArguments + ): Promise { + const threshold = await this.getSecondsThresholdForNotificationIfExists(lookupArguments); + return threshold !== null; } - subscribeToNotificationChanges(listener: Listener): void { + public subscribeToNotificationChanges(listener: Listener): void { + const index = this.listeners.findIndex( + (existingListener) => existingListener === listener + ); + if (index < 0) { + this.listeners.push(listener); + } } - unsubscribeFromNotificationChanges(listener: Listener): void { + 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/RedisNotificationRepositoryTests.test.ts b/test/repositories/RedisNotificationRepositoryTests.test.ts deleted file mode 100644 index 7a3dcaf..0000000 --- a/test/repositories/RedisNotificationRepositoryTests.test.ts +++ /dev/null @@ -1,6 +0,0 @@ -// Test additional edge cases like Redis failing to connect, etc. - -import { describe, it } from "@jest/globals"; - -describe("RedisNotificationRepository", () => { -}); From c6f846d1099c4e7e5873e146fb7fbc83179a24b3 Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Mon, 31 Mar 2025 20:29:44 -0700 Subject: [PATCH 15/17] add tests for isNotificationScheduled --- .../NotificationRepositorySharedTests.test.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/test/repositories/NotificationRepositorySharedTests.test.ts b/test/repositories/NotificationRepositorySharedTests.test.ts index 6205833..6c7587f 100644 --- a/test/repositories/NotificationRepositorySharedTests.test.ts +++ b/test/repositories/NotificationRepositorySharedTests.test.ts @@ -181,5 +181,18 @@ describe.each(repositoryImplementations)('$name', (holder) => { }; expect(mockCallback).toHaveBeenCalledWith(expectedEvent); }); - }) + }); + + describe("isNotificationScheduled", () => { + it("returns true if the notification is in the repo", async () => { + await repo.addOrUpdateNotification(notification); + const result = await repo.isNotificationScheduled(notification); + expect(result).toBe(true); + }); + + it("returns false if the notification isn't in the repo", async () => { + const result = await repo.isNotificationScheduled(notification); + expect(result).toBe(false); + }) + }); }); From fbc08838df4fc5725cfbdf990e811fa9965b641a Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Mon, 31 Mar 2025 20:34:45 -0700 Subject: [PATCH 16/17] test unsubscribeFromNotificationChanges method --- .../NotificationRepositorySharedTests.test.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/test/repositories/NotificationRepositorySharedTests.test.ts b/test/repositories/NotificationRepositorySharedTests.test.ts index 6c7587f..d8df44e 100644 --- a/test/repositories/NotificationRepositorySharedTests.test.ts +++ b/test/repositories/NotificationRepositorySharedTests.test.ts @@ -183,6 +183,21 @@ describe.each(repositoryImplementations)('$name', (holder) => { }); }); + describe("unsubscribeFromNotificationChanges", () => { + it("stops calling subscribers when unsubscribed", async () => { + const mockCallback = jest.fn(); + repo.subscribeToNotificationChanges(mockCallback); + + await repo.addOrUpdateNotification(notification); + + repo.unsubscribeFromNotificationChanges(mockCallback); + + await repo.deleteNotificationIfExists(notification); + + expect(mockCallback).toHaveBeenCalledTimes(1); + }); + }); + describe("isNotificationScheduled", () => { it("returns true if the notification is in the repo", async () => { await repo.addOrUpdateNotification(notification); From 0e204af330ef1d108aa0a86b2797b183fb4f419c Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Mon, 31 Mar 2025 20:37:53 -0700 Subject: [PATCH 17/17] for development and production, swap out in memory repo for redis one --- src/index.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index d1a1d14..de49b21 100644 --- a/src/index.ts +++ b/src/index.ts @@ -10,6 +10,7 @@ import { loadShuttleTestData } from "./loaders/loadShuttleTestData"; import { AppleNotificationSender } from "./notifications/senders/AppleNotificationSender"; import { InMemoryNotificationRepository } from "./repositories/InMemoryNotificationRepository"; import { NotificationRepository } from "./repositories/NotificationRepository"; +import { RedisNotificationRepository } from "./repositories/RedisNotificationRepository"; const typeDefs = readFileSync("./schema.graphqls", "utf8"); @@ -24,6 +25,7 @@ async function main() { let notificationRepository: NotificationRepository; let notificationService: ETANotificationScheduler; + if (process.argv.length > 2 && process.argv[2] == "integration-testing") { console.log("Using integration testing setup") await loadShuttleTestData(shuttleRepository); @@ -43,7 +45,10 @@ async function main() { ); await repositoryDataUpdater.start(); - notificationRepository = new InMemoryNotificationRepository(); + const redisNotificationRepository = new RedisNotificationRepository(); + await redisNotificationRepository.connect(); + + notificationRepository = redisNotificationRepository; notificationService = new ETANotificationScheduler( shuttleRepository, notificationRepository