From acfc91d3c1ebbde7f5ceeaa0471950627755829a Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Sun, 2 Feb 2025 12:49:41 -0800 Subject: [PATCH 01/12] add method stubs for subscribing/unsubscribing to changes --- src/repositories/GetterRepository.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/repositories/GetterRepository.ts b/src/repositories/GetterRepository.ts index 4c6a3b0..c9f473c 100644 --- a/src/repositories/GetterRepository.ts +++ b/src/repositories/GetterRepository.ts @@ -16,8 +16,9 @@ export interface GetterRepository { getEtasForShuttleId(shuttleId: string): Promise; getEtasForStopId(stopId: string): Promise; - getEtaForShuttleAndStopId(shuttleId: string, stopId: string): Promise; + subscribeToEtaChanges(listener: (eta: IEta) => void): Promise; + unsubscribeFromEtaChanges(listener: (eta: IEta) => void): Promise; getOrderedStopByRouteAndStopId(routeId: string, stopId: string): Promise; @@ -34,4 +35,4 @@ export interface GetterRepository { * @param routeId */ getOrderedStopsByRouteId(routeId: string): Promise; -} \ No newline at end of file +} From 345d4905fed16831eb1bd4acebe9acc7f4d532e1 Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Sun, 2 Feb 2025 12:51:25 -0800 Subject: [PATCH 02/12] add method stubs in basic in-memory repository --- src/repositories/UnoptimizedInMemoryRepository.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/repositories/UnoptimizedInMemoryRepository.ts b/src/repositories/UnoptimizedInMemoryRepository.ts index 938f4ea..0d6deff 100644 --- a/src/repositories/UnoptimizedInMemoryRepository.ts +++ b/src/repositories/UnoptimizedInMemoryRepository.ts @@ -49,7 +49,7 @@ export class UnoptimizedInMemoryRepository implements GetterSetterRepository { public async getShuttleById(shuttleId: string) { return this.findEntityById(shuttleId, this.shuttles); } - + public async getEtasForShuttleId(shuttleId: string) { return this.etas.filter(eta => eta.shuttleId === shuttleId); } @@ -58,6 +58,12 @@ export class UnoptimizedInMemoryRepository implements GetterSetterRepository { return this.etas.filter(eta => eta.stopId === stopId); } + public async subscribeToEtaChanges(listener: (eta: IEta) => void): Promise { + } + + public async unsubscribeFromEtaChanges(listener: (eta: IEta) => void): Promise { + } + public async getEtaForShuttleAndStopId(shuttleId: string, stopId: string) { return this.findEntityByMatcher((value) => value.stopId === stopId && value.shuttleId === shuttleId, this.etas); } @@ -209,4 +215,4 @@ export class UnoptimizedInMemoryRepository implements GetterSetterRepository { this.stops = []; } -} \ No newline at end of file +} From 5957010a7f444f96d6324229d8016cda7f30ba37 Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Sun, 2 Feb 2025 13:04:03 -0800 Subject: [PATCH 03/12] add test cases and documentation for functions --- src/repositories/GetterRepository.ts | 18 +++++++++++++++++- .../UnoptimizedInMemoryRepositoryTests.test.ts | 10 +++++++++- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/src/repositories/GetterRepository.ts b/src/repositories/GetterRepository.ts index c9f473c..8ab048d 100644 --- a/src/repositories/GetterRepository.ts +++ b/src/repositories/GetterRepository.ts @@ -17,7 +17,23 @@ export interface GetterRepository { getEtasForShuttleId(shuttleId: string): Promise; getEtasForStopId(stopId: string): Promise; getEtaForShuttleAndStopId(shuttleId: string, stopId: string): Promise; - subscribeToEtaChanges(listener: (eta: IEta) => void): Promise; + + /** + * Subscribe to all updates in ETA data. + * The subscriber persists even if the ETA data does not + * exist within the repository, and may fire again + * if ETA data is restored. + * @param listener + */ + subscribeToEtaChanges( + listener: (eta: IEta) => void, + ): Promise; + + /** + * Unsubscribe from all ETA updates for the given callback. + * Callback must be passed by reference. + * @param listener + */ unsubscribeFromEtaChanges(listener: (eta: IEta) => void): Promise; getOrderedStopByRouteAndStopId(routeId: string, stopId: string): Promise; diff --git a/test/repositories/UnoptimizedInMemoryRepositoryTests.test.ts b/test/repositories/UnoptimizedInMemoryRepositoryTests.test.ts index f130ba5..5f1beca 100644 --- a/test/repositories/UnoptimizedInMemoryRepositoryTests.test.ts +++ b/test/repositories/UnoptimizedInMemoryRepositoryTests.test.ts @@ -225,6 +225,14 @@ describe("UnoptimizedInMemoryRepository", () => { }); }); + describe("subscribeToEtaChanges", () => { + test("notifies listener if the eta has changed") + }); + + describe("unsubscribeFromEtaChanges", () => { + + }); + describe("getOrderedStopByRouteAndStopId", () => { test("gets an ordered stop by route ID and stop ID", async () => { const mockOrderedStops = generateMockOrderedStops(); @@ -711,4 +719,4 @@ describe("UnoptimizedInMemoryRepository", () => { expect(result).toEqual([]); }); }); -}); \ No newline at end of file +}); From 899b953be5dd2f8470a2c7433bdd6a14cd40520e Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Sun, 2 Feb 2025 13:05:13 -0800 Subject: [PATCH 04/12] add test cases --- .../repositories/UnoptimizedInMemoryRepositoryTests.test.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/test/repositories/UnoptimizedInMemoryRepositoryTests.test.ts b/test/repositories/UnoptimizedInMemoryRepositoryTests.test.ts index 5f1beca..5983dfb 100644 --- a/test/repositories/UnoptimizedInMemoryRepositoryTests.test.ts +++ b/test/repositories/UnoptimizedInMemoryRepositoryTests.test.ts @@ -226,11 +226,15 @@ describe("UnoptimizedInMemoryRepository", () => { }); describe("subscribeToEtaChanges", () => { - test("notifies listener if the eta has changed") + test("notifies listeners if etas has changed", async () => { + + }); }); describe("unsubscribeFromEtaChanges", () => { + test("stops notifying listeners after etas have stopped changing", async () => { + }); }); describe("getOrderedStopByRouteAndStopId", () => { From ec62b0c8bf048a1d09fd36ddb72a0b3257aee0ba Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Sun, 2 Feb 2025 13:07:42 -0800 Subject: [PATCH 05/12] update interface to not be async --- src/repositories/GetterRepository.ts | 4 ++-- src/repositories/UnoptimizedInMemoryRepository.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/repositories/GetterRepository.ts b/src/repositories/GetterRepository.ts index 8ab048d..b2f9f92 100644 --- a/src/repositories/GetterRepository.ts +++ b/src/repositories/GetterRepository.ts @@ -27,14 +27,14 @@ export interface GetterRepository { */ subscribeToEtaChanges( listener: (eta: IEta) => void, - ): Promise; + ): void; /** * Unsubscribe from all ETA updates for the given callback. * Callback must be passed by reference. * @param listener */ - unsubscribeFromEtaChanges(listener: (eta: IEta) => void): Promise; + unsubscribeFromEtaChanges(listener: (eta: IEta) => void): void; getOrderedStopByRouteAndStopId(routeId: string, stopId: string): Promise; diff --git a/src/repositories/UnoptimizedInMemoryRepository.ts b/src/repositories/UnoptimizedInMemoryRepository.ts index 0d6deff..c5569e9 100644 --- a/src/repositories/UnoptimizedInMemoryRepository.ts +++ b/src/repositories/UnoptimizedInMemoryRepository.ts @@ -58,10 +58,10 @@ export class UnoptimizedInMemoryRepository implements GetterSetterRepository { return this.etas.filter(eta => eta.stopId === stopId); } - public async subscribeToEtaChanges(listener: (eta: IEta) => void): Promise { + public subscribeToEtaChanges(listener: (eta: IEta) => void) { } - public async unsubscribeFromEtaChanges(listener: (eta: IEta) => void): Promise { + public unsubscribeFromEtaChanges(listener: (eta: IEta) => void) { } public async getEtaForShuttleAndStopId(shuttleId: string, stopId: string) { From 047ff3a56e5a85f9b00332d114c9b9598f2bc522 Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Sun, 2 Feb 2025 13:09:54 -0800 Subject: [PATCH 06/12] add test cases for the new functions --- ...UnoptimizedInMemoryRepositoryTests.test.ts | 26 +++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/test/repositories/UnoptimizedInMemoryRepositoryTests.test.ts b/test/repositories/UnoptimizedInMemoryRepositoryTests.test.ts index 5983dfb..1ce76b1 100644 --- a/test/repositories/UnoptimizedInMemoryRepositoryTests.test.ts +++ b/test/repositories/UnoptimizedInMemoryRepositoryTests.test.ts @@ -1,4 +1,4 @@ -import { beforeEach, describe, expect, test } from "@jest/globals"; +import { beforeEach, describe, expect, jest, test } from "@jest/globals"; import { UnoptimizedInMemoryRepository } from "../../src/repositories/UnoptimizedInMemoryRepository"; import { generateMockEtas, @@ -226,14 +226,36 @@ describe("UnoptimizedInMemoryRepository", () => { }); describe("subscribeToEtaChanges", () => { - test("notifies listeners if etas has changed", async () => { + test("notifies listeners if etas have been added or changed", async () => { + const mockCallback = jest.fn(); // Jest mock function to simulate a listener + repository.subscribeToEtaChanges(mockCallback); + const mockEtas = generateMockEtas(); + for (const eta of mockEtas) { + await repository.addOrUpdateEta(eta); // Trigger changes in ETAs + } + + expect(mockCallback).toHaveBeenCalledTimes(mockEtas.length); + expect(mockCallback).toHaveBeenCalledWith(mockEtas[0]); // First notification + expect(mockCallback).toHaveBeenCalledWith(mockEtas[mockEtas.length - 1]); // Last notification }); }); describe("unsubscribeFromEtaChanges", () => { test("stops notifying listeners after etas have stopped changing", async () => { + const mockCallback = jest.fn(); // Jest mock function to simulate a listener + repository.subscribeToEtaChanges(mockCallback); + const mockEtas = generateMockEtas(); + await repository.addOrUpdateEta(mockEtas[0]); + + repository.unsubscribeFromEtaChanges(mockCallback); + + await repository.addOrUpdateEta(mockEtas[mockEtas.length - 1]); + + expect(mockCallback).toHaveBeenCalledTimes(1); + expect(mockCallback).toHaveBeenCalledWith(mockEtas[0]); // First notification + expect(mockCallback).not.toHaveBeenCalledWith(mockEtas[mockEtas.length - 1]); // Last notification }); }); From 01cbad3a730b34d7262480c7ee91f5920f2ae38a Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Sun, 2 Feb 2025 13:12:20 -0800 Subject: [PATCH 07/12] add code to add/remove from subscriber array --- src/repositories/UnoptimizedInMemoryRepository.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/repositories/UnoptimizedInMemoryRepository.ts b/src/repositories/UnoptimizedInMemoryRepository.ts index c5569e9..203dde2 100644 --- a/src/repositories/UnoptimizedInMemoryRepository.ts +++ b/src/repositories/UnoptimizedInMemoryRepository.ts @@ -1,5 +1,6 @@ import { GetterSetterRepository } from "./GetterSetterRepository"; import { IEntityWithId, IEta, IOrderedStop, IRoute, IShuttle, IStop, ISystem } from "../entities/entities"; +import { TupleKey } from "../types/TupleKey"; /** * An unoptimized in memory repository. @@ -14,6 +15,8 @@ export class UnoptimizedInMemoryRepository implements GetterSetterRepository { private etas: IEta[] = []; private orderedStops: IOrderedStop[] = []; + private subscribers: ((eta: IEta) => void)[] = []; + public async getSystems() { return this.systems; } @@ -59,9 +62,14 @@ export class UnoptimizedInMemoryRepository implements GetterSetterRepository { } public subscribeToEtaChanges(listener: (eta: IEta) => void) { + this.subscribers.push(listener); } public unsubscribeFromEtaChanges(listener: (eta: IEta) => void) { + const index = this.subscribers.findIndex((existingListener) => existingListener == listener); + if (index >= 0) { + this.subscribers.splice(index, 1); + } } public async getEtaForShuttleAndStopId(shuttleId: string, stopId: string) { From a810a2e78fd08964663af1ea7f55be909577bf12 Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Sun, 2 Feb 2025 13:13:32 -0800 Subject: [PATCH 08/12] add 'publish' code to addOrUpdateEta --- src/repositories/UnoptimizedInMemoryRepository.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/repositories/UnoptimizedInMemoryRepository.ts b/src/repositories/UnoptimizedInMemoryRepository.ts index 203dde2..a888dab 100644 --- a/src/repositories/UnoptimizedInMemoryRepository.ts +++ b/src/repositories/UnoptimizedInMemoryRepository.ts @@ -152,6 +152,10 @@ export class UnoptimizedInMemoryRepository implements GetterSetterRepository { } else { this.etas.push(eta); } + + this.subscribers.forEach(subscriber => { + subscriber(eta); + }); } private async removeEntityByMatcherIfExists(callback: (value: T) => boolean, arrayToSearchIn: T[]) { From 9dd6e945c2822b4ebe8f51c96f8112b196419587 Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Sun, 2 Feb 2025 13:14:04 -0800 Subject: [PATCH 09/12] extract publishing code to private method --- src/repositories/UnoptimizedInMemoryRepository.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/repositories/UnoptimizedInMemoryRepository.ts b/src/repositories/UnoptimizedInMemoryRepository.ts index a888dab..e4addc1 100644 --- a/src/repositories/UnoptimizedInMemoryRepository.ts +++ b/src/repositories/UnoptimizedInMemoryRepository.ts @@ -152,7 +152,10 @@ export class UnoptimizedInMemoryRepository implements GetterSetterRepository { } else { this.etas.push(eta); } + this.publishEtaUpdateToSubscribers(eta); + } + private publishEtaUpdateToSubscribers(eta: IEta) { this.subscribers.forEach(subscriber => { subscriber(eta); }); From b30e4be89504221ae6c18c0a32f5d3b0cb3e97d2 Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Sun, 2 Feb 2025 13:14:45 -0800 Subject: [PATCH 10/12] rename methods for clarity on method events --- src/repositories/GetterRepository.ts | 4 ++-- src/repositories/UnoptimizedInMemoryRepository.ts | 4 ++-- .../repositories/UnoptimizedInMemoryRepositoryTests.test.ts | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/repositories/GetterRepository.ts b/src/repositories/GetterRepository.ts index b2f9f92..9c5f27f 100644 --- a/src/repositories/GetterRepository.ts +++ b/src/repositories/GetterRepository.ts @@ -25,7 +25,7 @@ export interface GetterRepository { * if ETA data is restored. * @param listener */ - subscribeToEtaChanges( + subscribeToEtaUpdates( listener: (eta: IEta) => void, ): void; @@ -34,7 +34,7 @@ export interface GetterRepository { * Callback must be passed by reference. * @param listener */ - unsubscribeFromEtaChanges(listener: (eta: IEta) => void): void; + unsubscribeFromEtaUpdates(listener: (eta: IEta) => void): void; getOrderedStopByRouteAndStopId(routeId: string, stopId: string): Promise; diff --git a/src/repositories/UnoptimizedInMemoryRepository.ts b/src/repositories/UnoptimizedInMemoryRepository.ts index e4addc1..273aab6 100644 --- a/src/repositories/UnoptimizedInMemoryRepository.ts +++ b/src/repositories/UnoptimizedInMemoryRepository.ts @@ -61,11 +61,11 @@ export class UnoptimizedInMemoryRepository implements GetterSetterRepository { return this.etas.filter(eta => eta.stopId === stopId); } - public subscribeToEtaChanges(listener: (eta: IEta) => void) { + public subscribeToEtaUpdates(listener: (eta: IEta) => void) { this.subscribers.push(listener); } - public unsubscribeFromEtaChanges(listener: (eta: IEta) => void) { + public unsubscribeFromEtaUpdates(listener: (eta: IEta) => void) { const index = this.subscribers.findIndex((existingListener) => existingListener == listener); if (index >= 0) { this.subscribers.splice(index, 1); diff --git a/test/repositories/UnoptimizedInMemoryRepositoryTests.test.ts b/test/repositories/UnoptimizedInMemoryRepositoryTests.test.ts index 1ce76b1..dc22f15 100644 --- a/test/repositories/UnoptimizedInMemoryRepositoryTests.test.ts +++ b/test/repositories/UnoptimizedInMemoryRepositoryTests.test.ts @@ -228,7 +228,7 @@ describe("UnoptimizedInMemoryRepository", () => { describe("subscribeToEtaChanges", () => { test("notifies listeners if etas have been added or changed", async () => { const mockCallback = jest.fn(); // Jest mock function to simulate a listener - repository.subscribeToEtaChanges(mockCallback); + repository.subscribeToEtaUpdates(mockCallback); const mockEtas = generateMockEtas(); for (const eta of mockEtas) { @@ -244,12 +244,12 @@ describe("UnoptimizedInMemoryRepository", () => { describe("unsubscribeFromEtaChanges", () => { test("stops notifying listeners after etas have stopped changing", async () => { const mockCallback = jest.fn(); // Jest mock function to simulate a listener - repository.subscribeToEtaChanges(mockCallback); + repository.subscribeToEtaUpdates(mockCallback); const mockEtas = generateMockEtas(); await repository.addOrUpdateEta(mockEtas[0]); - repository.unsubscribeFromEtaChanges(mockCallback); + repository.unsubscribeFromEtaUpdates(mockCallback); await repository.addOrUpdateEta(mockEtas[mockEtas.length - 1]); From fe16bc71246857c8798def8c15cb053751baa996 Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Sun, 2 Feb 2025 13:16:28 -0800 Subject: [PATCH 11/12] add test case for nonexistent callback --- .../UnoptimizedInMemoryRepositoryTests.test.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/test/repositories/UnoptimizedInMemoryRepositoryTests.test.ts b/test/repositories/UnoptimizedInMemoryRepositoryTests.test.ts index dc22f15..35956b3 100644 --- a/test/repositories/UnoptimizedInMemoryRepositoryTests.test.ts +++ b/test/repositories/UnoptimizedInMemoryRepositoryTests.test.ts @@ -257,6 +257,17 @@ describe("UnoptimizedInMemoryRepository", () => { expect(mockCallback).toHaveBeenCalledWith(mockEtas[0]); // First notification expect(mockCallback).not.toHaveBeenCalledWith(mockEtas[mockEtas.length - 1]); // Last notification }); + + test("does nothing if the listener doesn't exist", async () => { + const mockCallback = jest.fn(); + repository.subscribeToEtaUpdates(mockCallback); + + const mockEtas = generateMockEtas(); + + repository.unsubscribeFromEtaUpdates(() => {}); + await repository.addOrUpdateEta(mockEtas[0]); + expect(mockCallback).toHaveBeenCalledTimes(1); + }); }); describe("getOrderedStopByRouteAndStopId", () => { From 7d762f17dc515e24e93f40e24ba7ccf3d6f56c4d Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Sun, 2 Feb 2025 13:18:33 -0800 Subject: [PATCH 12/12] optimize imports --- src/repositories/UnoptimizedInMemoryRepository.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/repositories/UnoptimizedInMemoryRepository.ts b/src/repositories/UnoptimizedInMemoryRepository.ts index 273aab6..e347803 100644 --- a/src/repositories/UnoptimizedInMemoryRepository.ts +++ b/src/repositories/UnoptimizedInMemoryRepository.ts @@ -1,6 +1,5 @@ import { GetterSetterRepository } from "./GetterSetterRepository"; import { IEntityWithId, IEta, IOrderedStop, IRoute, IShuttle, IStop, ISystem } from "../entities/entities"; -import { TupleKey } from "../types/TupleKey"; /** * An unoptimized in memory repository.