From 9a2b2f65b90b42cca961181b37895ab76bb156f1 Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Fri, 26 Sep 2025 14:31:20 -0700 Subject: [PATCH 01/11] Rename methods to indicate a single responsibility --- src/loaders/RepositoryLoader.ts | 2 +- src/loaders/TimedApiBasedRepositoryLoader.ts | 2 +- .../ChapmanApiBasedParkingRepositoryLoader.ts | 2 +- ...iBasedParkingRepositoryLoaderTests.test.ts | 2 +- .../ApiBasedShuttleRepositoryLoader.ts | 22 +++++++++---------- .../shuttle/ShuttleRepositoryLoader.ts | 10 ++++----- ...iBasedShuttleRepositoryLoaderTests.test.ts | 20 ++++++++--------- 7 files changed, 30 insertions(+), 30 deletions(-) diff --git a/src/loaders/RepositoryLoader.ts b/src/loaders/RepositoryLoader.ts index 668aa26..95c96a6 100644 --- a/src/loaders/RepositoryLoader.ts +++ b/src/loaders/RepositoryLoader.ts @@ -1,3 +1,3 @@ export interface RepositoryLoader { - fetchAndUpdateAll(): Promise; + updateAll(): Promise; } diff --git a/src/loaders/TimedApiBasedRepositoryLoader.ts b/src/loaders/TimedApiBasedRepositoryLoader.ts index 8daf3be..3bd8a52 100644 --- a/src/loaders/TimedApiBasedRepositoryLoader.ts +++ b/src/loaders/TimedApiBasedRepositoryLoader.ts @@ -33,7 +33,7 @@ export class TimedApiBasedRepositoryLoader { if (!this.shouldBeRunning) return; try { - await this.loader.fetchAndUpdateAll(); + await this.loader.updateAll(); } catch (e) { console.error(e); } diff --git a/src/loaders/parking/ChapmanApiBasedParkingRepositoryLoader.ts b/src/loaders/parking/ChapmanApiBasedParkingRepositoryLoader.ts index 5377c4c..18881cc 100644 --- a/src/loaders/parking/ChapmanApiBasedParkingRepositoryLoader.ts +++ b/src/loaders/parking/ChapmanApiBasedParkingRepositoryLoader.ts @@ -21,7 +21,7 @@ export class ChapmanApiBasedParkingRepositoryLoader implements ParkingRepository this.fetchAndUpdateParkingStructures = this.fetchAndUpdateParkingStructures.bind(this); } - async fetchAndUpdateAll() { + async updateAll() { await this.fetchAndUpdateParkingStructures(); } diff --git a/src/loaders/parking/__tests__/ChapmanApiBasedParkingRepositoryLoaderTests.test.ts b/src/loaders/parking/__tests__/ChapmanApiBasedParkingRepositoryLoaderTests.test.ts index ebc33a8..580d77c 100644 --- a/src/loaders/parking/__tests__/ChapmanApiBasedParkingRepositoryLoaderTests.test.ts +++ b/src/loaders/parking/__tests__/ChapmanApiBasedParkingRepositoryLoaderTests.test.ts @@ -34,7 +34,7 @@ describe("ChapmanApiBasedParkingRepositoryLoader", () => { spy.mockResolvedValue(undefined); }); - await loader.fetchAndUpdateAll(); + await loader.updateAll(); Object.values(spies).forEach((spy: any) => { expect(spy).toHaveBeenCalled(); diff --git a/src/loaders/shuttle/ApiBasedShuttleRepositoryLoader.ts b/src/loaders/shuttle/ApiBasedShuttleRepositoryLoader.ts index 72e3d95..eec4f0f 100644 --- a/src/loaders/shuttle/ApiBasedShuttleRepositoryLoader.ts +++ b/src/loaders/shuttle/ApiBasedShuttleRepositoryLoader.ts @@ -28,18 +28,18 @@ export class ApiBasedShuttleRepositoryLoader implements ShuttleRepositoryLoader return ids; } - public async fetchAndUpdateAll() { - await this.fetchAndUpdateRouteDataForSystem(); - await this.fetchAndUpdateStopAndPolylineDataForRoutesInSystem(); - await this.fetchAndUpdateShuttleDataForSystem(); + public async updateAll() { + await this.updateRouteDataForSystem(); + await this.updateStopAndPolylineDataForRoutesInSystem(); + await this.updateShuttleDataForSystem(); // Because ETA method doesn't support pruning yet, // add a call to the clear method here await this.repository.clearEtaData(); - await this.fetchAndUpdateEtaDataForExistingStopsForSystem(); + await this.updateEtaDataForExistingStopsForSystem(); } - public async fetchAndUpdateRouteDataForSystem() { + public async updateRouteDataForSystem() { const systemId = this.passioSystemId; const routeIdsToPrune = await this.constructExistingEntityIdSet(async () => { return await this.repository.getRoutes(); @@ -90,7 +90,7 @@ export class ApiBasedShuttleRepositoryLoader implements ShuttleRepositoryLoader } } - public async fetchAndUpdateStopAndPolylineDataForRoutesInSystem() { + public async updateStopAndPolylineDataForRoutesInSystem() { const passioSystemId = this.passioSystemId; // Fetch from the API @@ -131,7 +131,7 @@ export class ApiBasedShuttleRepositoryLoader implements ShuttleRepositoryLoader } } - public async fetchAndUpdateShuttleDataForSystem() { + public async updateShuttleDataForSystem() { const systemId = this.passioSystemId; const shuttleIdsToPrune = await this.constructExistingEntityIdSet(async () => { return await this.repository.getShuttles(); @@ -191,15 +191,15 @@ export class ApiBasedShuttleRepositoryLoader implements ShuttleRepositoryLoader } } - public async fetchAndUpdateEtaDataForExistingStopsForSystem() { + public async updateEtaDataForExistingStopsForSystem() { const stops = await this.repository.getStops(); await Promise.all(stops.map(async (stop) => { let stopId = stop.id; - await this.fetchAndUpdateEtaDataForStopId(stopId); + await this.updateEtaDataForStopId(stopId); })); } - public async fetchAndUpdateEtaDataForStopId(stopId: string) { + public async updateEtaDataForStopId(stopId: string) { const params = { eta: "3", stopIds: stopId, diff --git a/src/loaders/shuttle/ShuttleRepositoryLoader.ts b/src/loaders/shuttle/ShuttleRepositoryLoader.ts index 382c97e..6c4eb3e 100644 --- a/src/loaders/shuttle/ShuttleRepositoryLoader.ts +++ b/src/loaders/shuttle/ShuttleRepositoryLoader.ts @@ -1,9 +1,9 @@ import { RepositoryLoader } from "../RepositoryLoader"; export interface ShuttleRepositoryLoader extends RepositoryLoader { - fetchAndUpdateRouteDataForSystem(): Promise; - fetchAndUpdateStopAndPolylineDataForRoutesInSystem(): Promise; - fetchAndUpdateShuttleDataForSystem(): Promise; - fetchAndUpdateEtaDataForExistingStopsForSystem(): Promise; - fetchAndUpdateEtaDataForStopId(stopId: string): Promise; + updateRouteDataForSystem(): Promise; + updateStopAndPolylineDataForRoutesInSystem(): Promise; + updateShuttleDataForSystem(): Promise; + updateEtaDataForExistingStopsForSystem(): Promise; + updateEtaDataForStopId(stopId: string): Promise; } diff --git a/src/loaders/shuttle/__tests__/ApiBasedShuttleRepositoryLoaderTests.test.ts b/src/loaders/shuttle/__tests__/ApiBasedShuttleRepositoryLoaderTests.test.ts index 7770064..ddcf095 100644 --- a/src/loaders/shuttle/__tests__/ApiBasedShuttleRepositoryLoaderTests.test.ts +++ b/src/loaders/shuttle/__tests__/ApiBasedShuttleRepositoryLoaderTests.test.ts @@ -44,7 +44,7 @@ describe("ApiBasedShuttleRepositoryLoader", () => { spy.mockResolvedValue(undefined); }); - await loader.fetchAndUpdateAll(); + await loader.updateAll(); Object.values(spies).forEach((spy: any) => { expect(spy).toHaveBeenCalled(); @@ -65,7 +65,7 @@ describe("ApiBasedShuttleRepositoryLoader", () => { updateGlobalFetchMockJson(fetchRouteDataSuccessfulResponse); // Act - await loader.fetchAndUpdateRouteDataForSystem(); + await loader.updateRouteDataForSystem(); // Assert const routes = await loader.repository.getRoutes(); @@ -80,7 +80,7 @@ describe("ApiBasedShuttleRepositoryLoader", () => { updateGlobalFetchMockJsonToThrowSyntaxError(); await assertAsyncCallbackThrowsApiResponseError(async () => { - await loader.fetchAndUpdateRouteDataForSystem(); + await loader.updateRouteDataForSystem(); }); }); }); @@ -99,7 +99,7 @@ describe("ApiBasedShuttleRepositoryLoader", () => { const stopsArray = Object.values(fetchStopAndPolylineDataSuccessfulResponse.stops); - await loader.fetchAndUpdateStopAndPolylineDataForRoutesInSystem(); + await loader.updateStopAndPolylineDataForRoutesInSystem(); const stops = await loader.repository.getStops(); expect(stops.length).toEqual(stopsArray.length); @@ -119,7 +119,7 @@ describe("ApiBasedShuttleRepositoryLoader", () => { updateGlobalFetchMockJsonToThrowSyntaxError(); await assertAsyncCallbackThrowsApiResponseError(async () => { - await loader.fetchAndUpdateStopAndPolylineDataForRoutesInSystem(); + await loader.updateStopAndPolylineDataForRoutesInSystem(); }); }) }); @@ -135,7 +135,7 @@ describe("ApiBasedShuttleRepositoryLoader", () => { updateGlobalFetchMockJson(fetchShuttleDataSuccessfulResponse); const busesInResponse = Object.values(fetchShuttleDataSuccessfulResponse.buses); - await loader.fetchAndUpdateShuttleDataForSystem(); + await loader.updateShuttleDataForSystem(); const shuttles = await loader.repository.getShuttles(); @@ -146,7 +146,7 @@ describe("ApiBasedShuttleRepositoryLoader", () => { updateGlobalFetchMockJsonToThrowSyntaxError(); await assertAsyncCallbackThrowsApiResponseError(async () => { - await loader.fetchAndUpdateShuttleDataForSystem(); + await loader.updateShuttleDataForSystem(); }); }); }); @@ -164,7 +164,7 @@ describe("ApiBasedShuttleRepositoryLoader", () => { await loader.repository.addOrUpdateStop(stop); })); - await loader.fetchAndUpdateEtaDataForExistingStopsForSystem(); + await loader.updateEtaDataForExistingStopsForSystem(); expect(spy.mock.calls.length).toEqual(stops.length); }); @@ -177,7 +177,7 @@ describe("ApiBasedShuttleRepositoryLoader", () => { // @ts-ignore const etasFromResponse = fetchEtaDataSuccessfulResponse.ETAs[stopId] - await loader.fetchAndUpdateEtaDataForStopId(stopId); + await loader.updateEtaDataForStopId(stopId); const etas = await loader.repository.getEtasForStopId(stopId); expect(etas.length).toEqual(etasFromResponse.length); @@ -187,7 +187,7 @@ describe("ApiBasedShuttleRepositoryLoader", () => { updateGlobalFetchMockJsonToThrowSyntaxError(); await assertAsyncCallbackThrowsApiResponseError(async () => { - await loader.fetchAndUpdateEtaDataForStopId("263"); + await loader.updateEtaDataForStopId("263"); }); }); }); From 74a55b3f573dd0c04635c514acea2a6b994ec824 Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Fri, 26 Sep 2025 14:45:11 -0700 Subject: [PATCH 02/11] Refactor shuttle update method into clear sub-procedures --- .../ApiBasedShuttleRepositoryLoader.ts | 92 +++++++++++-------- ...iBasedShuttleRepositoryLoaderTests.test.ts | 10 +- 2 files changed, 61 insertions(+), 41 deletions(-) diff --git a/src/loaders/shuttle/ApiBasedShuttleRepositoryLoader.ts b/src/loaders/shuttle/ApiBasedShuttleRepositoryLoader.ts index eec4f0f..29ad295 100644 --- a/src/loaders/shuttle/ApiBasedShuttleRepositoryLoader.ts +++ b/src/loaders/shuttle/ApiBasedShuttleRepositoryLoader.ts @@ -132,11 +132,37 @@ export class ApiBasedShuttleRepositoryLoader implements ShuttleRepositoryLoader } public async updateShuttleDataForSystem() { - const systemId = this.passioSystemId; + try { + const json = await this.fetchShuttleDataJson(); + const shuttles = this.constructShuttlesFromJson(json); + if (shuttles !== null) { + await this.updateShuttleDataInRepository(shuttles); + } else { + console.warn(`Shuttle update failed for the following JSON: ${JSON.stringify(json)}`) + } + } catch(e: any) { + throw new ApiResponseError(e.message); + } + } + + private async updateShuttleDataInRepository(shuttles: IShuttle[]) { const shuttleIdsToPrune = await this.constructExistingEntityIdSet(async () => { return await this.repository.getShuttles(); }); + await Promise.all(shuttles.map(async (shuttle) => { + await this.repository.addOrUpdateShuttle(shuttle); + shuttleIdsToPrune.delete(shuttle.id); + })); + + await Promise.all(Array.from(shuttleIdsToPrune).map(async (shuttleId) => { + await this.repository.removeShuttleIfExists(shuttleId); + })); + } + + private async fetchShuttleDataJson() { + const systemId = this.passioSystemId; + const params = { getBuses: "2" }; @@ -151,44 +177,37 @@ export class ApiBasedShuttleRepositoryLoader implements ShuttleRepositoryLoader const query = new URLSearchParams(params).toString(); - try { - const response = await fetch(`${this.baseUrl}?${query}`, { - method: "POST", - body: formData, + const response = await fetch(`${this.baseUrl}?${query}`, { + method: "POST", + body: formData, + }); + return await response.json(); + } + + private constructShuttlesFromJson(json: any): IShuttle[] | null { + if (json.buses && json.buses["-1"] === undefined) { + const jsonBuses = Object.values(json.buses).map((busesArr: any) => { + return busesArr[0]; }); - const json = await response.json(); - if (json.buses && json.buses["-1"] === undefined) { - const jsonBuses = Object.values(json.buses).map((busesArr: any) => { - return busesArr[0]; - }); - - await Promise.all(jsonBuses.map(async (jsonBus: any) => { - const constructedShuttle: IShuttle = { - name: jsonBus.bus, - coordinates: { - latitude: parseFloat(jsonBus.latitude), - longitude: parseFloat(jsonBus.longitude), - }, - routeId: jsonBus.routeId, - systemId: this.systemIdForConstructedData, - id: `${jsonBus.busId}`, - orientationInDegrees: parseFloat(jsonBus.calculatedCourse), - updatedTime: new Date(), - } - - await this.repository.addOrUpdateShuttle(constructedShuttle); - - shuttleIdsToPrune.delete(constructedShuttle.id); - })); - } - - await Promise.all(Array.from(shuttleIdsToPrune).map(async (shuttleId) => { - await this.repository.removeShuttleIfExists(shuttleId); - })); - } catch(e: any) { - throw new ApiResponseError(e.message); + return jsonBuses.map((jsonBus: any) => { + const constructedShuttle: IShuttle = { + name: jsonBus.bus, + coordinates: { + latitude: parseFloat(jsonBus.latitude), + longitude: parseFloat(jsonBus.longitude), + }, + routeId: jsonBus.routeId, + systemId: this.systemIdForConstructedData, + id: `${jsonBus.busId}`, + orientationInDegrees: parseFloat(jsonBus.calculatedCourse), + updatedTime: new Date(), + } + return constructedShuttle; + }); } + + return null; } public async updateEtaDataForExistingStopsForSystem() { @@ -331,4 +350,5 @@ export class ApiBasedShuttleRepositoryLoader implements ShuttleRepositoryLoader })) } } + } diff --git a/src/loaders/shuttle/__tests__/ApiBasedShuttleRepositoryLoaderTests.test.ts b/src/loaders/shuttle/__tests__/ApiBasedShuttleRepositoryLoaderTests.test.ts index ddcf095..d62f91b 100644 --- a/src/loaders/shuttle/__tests__/ApiBasedShuttleRepositoryLoaderTests.test.ts +++ b/src/loaders/shuttle/__tests__/ApiBasedShuttleRepositoryLoaderTests.test.ts @@ -34,10 +34,10 @@ describe("ApiBasedShuttleRepositoryLoader", () => { describe("fetchAndUpdateAll", () => { it("calls all the correct methods", async () => { const spies = { - fetchAndUpdateRouteDataForSystem: jest.spyOn(loader, "fetchAndUpdateRouteDataForSystem"), - fetchAndUpdateStopAndPolylineDataForRoutesInSystem: jest.spyOn(loader, "fetchAndUpdateStopAndPolylineDataForRoutesInSystem"), - fetchAndUpdateShuttleDataForSystem: jest.spyOn(loader, "fetchAndUpdateShuttleDataForSystem"), - fetchAndUpdateEtaDataForExistingStopsForSystem: jest.spyOn(loader, "fetchAndUpdateEtaDataForExistingStopsForSystem"), + updateRouteDataForSystem: jest.spyOn(loader, "updateRouteDataForSystem"), + updateStopAndPolylineDataForRoutesInSystem: jest.spyOn(loader, "updateStopAndPolylineDataForRoutesInSystem"), + updateShuttleDataForSystem: jest.spyOn(loader, "updateShuttleDataForSystem"), + updateEtaDataForExistingStopsForSystem: jest.spyOn(loader, "updateEtaDataForExistingStopsForSystem"), }; Object.values(spies).forEach((spy: any) => { @@ -153,7 +153,7 @@ describe("ApiBasedShuttleRepositoryLoader", () => { describe("fetchAndUpdateEtaDataForExistingStopsForSystem", () => { it("calls fetchAndUpdateEtaDataForStopId for every stop in repository", async () => { - const spy = jest.spyOn(loader, "fetchAndUpdateEtaDataForStopId"); + const spy = jest.spyOn(loader, "updateEtaDataForStopId"); const stops = generateMockStops(); stops.forEach((stop) => { From db263122a1b63e02d056630cd574f59b4819715e Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Fri, 26 Sep 2025 14:56:34 -0700 Subject: [PATCH 03/11] Refactor remaining methods in ApiBasedShuttleRepositoryLoader.ts to follow the same sub-procedure pattern --- .../ApiBasedShuttleRepositoryLoader.ts | 182 +++++++++++------- 1 file changed, 110 insertions(+), 72 deletions(-) diff --git a/src/loaders/shuttle/ApiBasedShuttleRepositoryLoader.ts b/src/loaders/shuttle/ApiBasedShuttleRepositoryLoader.ts index 29ad295..74d6c87 100644 --- a/src/loaders/shuttle/ApiBasedShuttleRepositoryLoader.ts +++ b/src/loaders/shuttle/ApiBasedShuttleRepositoryLoader.ts @@ -40,11 +40,37 @@ export class ApiBasedShuttleRepositoryLoader implements ShuttleRepositoryLoader } public async updateRouteDataForSystem() { - const systemId = this.passioSystemId; + try { + const json = await this.fetchRouteDataJson(); + const routes = this.constructRoutesFromJson(json); + if (routes !== null) { + await this.updateRouteDataInRepository(routes); + } else { + console.warn(`Route update failed for the following JSON: ${JSON.stringify(json)}`); + } + } catch(e: any) { + throw new ApiResponseError(e.message); + } + } + + private async updateRouteDataInRepository(routes: IRoute[]) { const routeIdsToPrune = await this.constructExistingEntityIdSet(async () => { return await this.repository.getRoutes(); }); + await Promise.all(routes.map(async (route) => { + await this.repository.addOrUpdateRoute(route); + routeIdsToPrune.delete(route.id); + })); + + await Promise.all(Array.from(routeIdsToPrune).map(async (routeId) => { + await this.repository.removeRouteIfExists(routeId); + })); + } + + private async fetchRouteDataJson() { + const systemId = this.passioSystemId; + const params = { getRoutes: "2", }; @@ -52,53 +78,63 @@ export class ApiBasedShuttleRepositoryLoader implements ShuttleRepositoryLoader const formDataJsonObject = { "systemSelected0": systemId, "amount": "1", - } + }; const formData = new FormData(); formData.set("json", JSON.stringify(formDataJsonObject)); const query = new URLSearchParams(params).toString(); - try { - const response = await fetch(`${this.baseUrl}?${query}`, { - method: "POST", - body: formData, + const response = await fetch(`${this.baseUrl}?${query}`, { + method: "POST", + body: formData, + }); + return await response.json(); + } + + private constructRoutesFromJson(json: any): IRoute[] | null { + if (typeof json.all === "object") { + return json.all.map((jsonRoute: any) => { + const constructedRoute: IRoute = { + name: jsonRoute.name, + color: jsonRoute.color, + id: jsonRoute.myid, + polylineCoordinates: [], + systemId: this.systemIdForConstructedData, + updatedTime: new Date(), + }; + return constructedRoute; }); - const json = await response.json(); + } - if (typeof json.all === "object") { - await Promise.all(json.all.map(async (jsonRoute: any) => { - const constructedRoute: IRoute = { - name: jsonRoute.name, - color: jsonRoute.color, - id: jsonRoute.myid, - polylineCoordinates: [], - systemId: this.systemIdForConstructedData, - updatedTime: new Date(), - }; + return null; + } - await this.repository.addOrUpdateRoute(constructedRoute); - - routeIdsToPrune.delete(constructedRoute.id); - })) - } - - await Promise.all(Array.from(routeIdsToPrune).map(async (routeId) => { - await this.repository.removeRouteIfExists(routeId); - })); + public async updateStopAndPolylineDataForRoutesInSystem() { + try { + const json = await this.fetchStopAndPolylineDataJson(); + await this.updateStopAndPolylineDataInRepository(json); } catch(e: any) { throw new ApiResponseError(e.message); } } - public async updateStopAndPolylineDataForRoutesInSystem() { - const passioSystemId = this.passioSystemId; - - // Fetch from the API - // Pass JSON output into two different methods to update repository + private async updateStopAndPolylineDataInRepository(json: any) { const stopIdsToPrune = await this.constructExistingEntityIdSet(async () => { return await this.repository.getStops(); }); + await this.updateStopDataForSystemAndApiResponse(json, stopIdsToPrune); + await this.updateOrderedStopDataForExistingStops(json); + await this.updatePolylineDataForExistingRoutesAndApiResponse(json); + + await Promise.all(Array.from(stopIdsToPrune).map(async (stopId) => { + await this.repository.removeStopIfExists(stopId); + })); + } + + private async fetchStopAndPolylineDataJson() { + const passioSystemId = this.passioSystemId; + const params = { getStops: "2", }; @@ -112,23 +148,11 @@ export class ApiBasedShuttleRepositoryLoader implements ShuttleRepositoryLoader const query = new URLSearchParams(params).toString(); - try { - const response = await fetch(`${this.baseUrl}?${query}`, { - method: "POST", - body: formData, - }); - const json = await response.json(); - - await this.updateStopDataForSystemAndApiResponse(json, stopIdsToPrune); - await this.updateOrderedStopDataForExistingStops(json); - await this.updatePolylineDataForExistingRoutesAndApiResponse(json); - - await Promise.all(Array.from(stopIdsToPrune).map(async (stopId) => { - await this.repository.removeStopIfExists(stopId); - })); - } catch(e: any) { - throw new ApiResponseError(e.message); - } + const response = await fetch(`${this.baseUrl}?${query}`, { + method: "POST", + body: formData, + }); + return await response.json(); } public async updateShuttleDataForSystem() { @@ -219,6 +243,24 @@ export class ApiBasedShuttleRepositoryLoader implements ShuttleRepositoryLoader } public async updateEtaDataForStopId(stopId: string) { + try { + const json = await this.fetchEtaDataJson(stopId); + const etas = this.constructEtasFromJson(json, stopId); + if (etas !== null) { + await this.updateEtaDataInRepository(etas); + } + } catch(e: any) { + throw new ApiResponseError(e.message); + } + } + + private async updateEtaDataInRepository(etas: IEta[]) { + await Promise.all(etas.map(async (eta) => { + await this.repository.addOrUpdateEta(eta); + })); + } + + private async fetchEtaDataJson(stopId: string) { const params = { eta: "3", stopIds: stopId, @@ -226,32 +268,28 @@ export class ApiBasedShuttleRepositoryLoader implements ShuttleRepositoryLoader const query = new URLSearchParams(params).toString(); - try { - const response = await fetch(`${this.baseUrl}?${query}`, { - method: "GET", + const response = await fetch(`${this.baseUrl}?${query}`, { + method: "GET", + }); + return await response.json(); + } + + private constructEtasFromJson(json: any, stopId: string): IEta[] | null { + if (json.ETAs && json.ETAs[stopId]) { + return json.ETAs[stopId].map((jsonEta: any) => { + const shuttleId: string = jsonEta.busId; + const eta: IEta = { + secondsRemaining: jsonEta.secondsSpent, + shuttleId: `${shuttleId}`, + stopId: stopId, + updatedTime: new Date(), + systemId: this.systemIdForConstructedData, + }; + return eta; }); - const json = await response.json(); - - if (json.ETAs && json.ETAs[stopId]) { - // Continue with the parsing - json.ETAs[stopId].forEach((jsonEta: any) => { - // Update cache - const shuttleId: string = jsonEta.busId; - - const eta: IEta = { - secondsRemaining: jsonEta.secondsSpent, - shuttleId: `${shuttleId}`, - stopId: stopId, - updatedTime: new Date(), - systemId: this.systemIdForConstructedData, - }; - - this.repository.addOrUpdateEta(eta); - }); - } - } catch(e: any) { - throw new ApiResponseError(e.message); } + + return null; } protected async updateStopDataForSystemAndApiResponse( From 7d886975b9a9d306fd5dffb47b9eabbcfdc23df7 Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Fri, 26 Sep 2025 14:56:49 -0700 Subject: [PATCH 04/11] Update mocked methods in tests, and test method names to match --- .../TimedApiBasedRepositoryLoaderTests.test.ts | 4 ++-- .../ApiBasedShuttleRepositoryLoaderTests.test.ts | 14 +++++++------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/loaders/__tests__/TimedApiBasedRepositoryLoaderTests.test.ts b/src/loaders/__tests__/TimedApiBasedRepositoryLoaderTests.test.ts index fc94ad3..02e6d1f 100644 --- a/src/loaders/__tests__/TimedApiBasedRepositoryLoaderTests.test.ts +++ b/src/loaders/__tests__/TimedApiBasedRepositoryLoaderTests.test.ts @@ -1,4 +1,4 @@ -import { afterEach, beforeAll, beforeEach, describe, expect, it, jest } from "@jest/globals"; +import { afterEach, beforeEach, describe, expect, it, jest } from "@jest/globals"; import { TimedApiBasedRepositoryLoader } from "../TimedApiBasedRepositoryLoader"; import { resetGlobalFetchMockJson } from "../../../testHelpers/fetchMockHelpers"; import { UnoptimizedInMemoryShuttleRepository } from "../../repositories/shuttle/UnoptimizedInMemoryShuttleRepository"; @@ -23,7 +23,7 @@ describe("TimedApiBasedRepositoryLoader", () => { ); spies = { - fetchAndUpdateAll: jest.spyOn(mockLoader, 'fetchAndUpdateAll'), + updateAll: jest.spyOn(mockLoader, 'updateAll'), }; Object.values(spies).forEach((spy: any) => { diff --git a/src/loaders/shuttle/__tests__/ApiBasedShuttleRepositoryLoaderTests.test.ts b/src/loaders/shuttle/__tests__/ApiBasedShuttleRepositoryLoaderTests.test.ts index d62f91b..fa636e4 100644 --- a/src/loaders/shuttle/__tests__/ApiBasedShuttleRepositoryLoaderTests.test.ts +++ b/src/loaders/shuttle/__tests__/ApiBasedShuttleRepositoryLoaderTests.test.ts @@ -31,7 +31,7 @@ describe("ApiBasedShuttleRepositoryLoader", () => { const systemId = "1"; - describe("fetchAndUpdateAll", () => { + describe("updateAll", () => { it("calls all the correct methods", async () => { const spies = { updateRouteDataForSystem: jest.spyOn(loader, "updateRouteDataForSystem"), @@ -52,7 +52,7 @@ describe("ApiBasedShuttleRepositoryLoader", () => { }); }); - describe("fetchAndUpdateRouteDataForSystem", () => { + describe("updateRouteDataForSystem", () => { it("updates route data in repository if response received", async () => { // Arrange // Test pruning @@ -85,7 +85,7 @@ describe("ApiBasedShuttleRepositoryLoader", () => { }); }); - describe("fetchAndUpdateStopAndPolylineDataForRoutesWithSystemId", () => { + describe("updateStopAndPolylineDataForRoutesInSystem", () => { it("updates stop and polyline data if response received", async () => { // Arrange // Test pruning of stops only @@ -124,7 +124,7 @@ describe("ApiBasedShuttleRepositoryLoader", () => { }) }); - describe("fetchAndUpdateShuttleDataForSystem", () => { + describe("updateShuttleDataForSystem", () => { it("updates shuttle data in repository if response received", async () => { const shuttlesToPrune = generateMockShuttles(); await Promise.all(shuttlesToPrune.map(async (shuttle) => { @@ -151,8 +151,8 @@ describe("ApiBasedShuttleRepositoryLoader", () => { }); }); - describe("fetchAndUpdateEtaDataForExistingStopsForSystem", () => { - it("calls fetchAndUpdateEtaDataForStopId for every stop in repository", async () => { + describe("updateEtaDataForExistingStopsForSystem", () => { + it("calls updateEtaDataForStopId for every stop in repository", async () => { const spy = jest.spyOn(loader, "updateEtaDataForStopId"); const stops = generateMockStops(); @@ -170,7 +170,7 @@ describe("ApiBasedShuttleRepositoryLoader", () => { }); }); - describe("fetchAndUpdateEtaDataForStopId", () => { + describe("updateEtaDataForStopId", () => { const stopId = "177666"; it("updates ETA data for stop id if response received", async () => { updateGlobalFetchMockJson(fetchEtaDataSuccessfulResponse); From 9268ad518125b2b8379d76ef69824c347bb4a9f1 Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Fri, 26 Sep 2025 15:00:04 -0700 Subject: [PATCH 05/11] Repeat warning message for ETA data --- src/loaders/shuttle/ApiBasedShuttleRepositoryLoader.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/loaders/shuttle/ApiBasedShuttleRepositoryLoader.ts b/src/loaders/shuttle/ApiBasedShuttleRepositoryLoader.ts index 74d6c87..e79dd45 100644 --- a/src/loaders/shuttle/ApiBasedShuttleRepositoryLoader.ts +++ b/src/loaders/shuttle/ApiBasedShuttleRepositoryLoader.ts @@ -248,6 +248,8 @@ export class ApiBasedShuttleRepositoryLoader implements ShuttleRepositoryLoader const etas = this.constructEtasFromJson(json, stopId); if (etas !== null) { await this.updateEtaDataInRepository(etas); + } else { + console.warn(`ETA update failed for stop ${stopId} with the following JSON: ${JSON.stringify(json)}`); } } catch(e: any) { throw new ApiResponseError(e.message); From 7ed780a45974ff77237f84b48c4732ea33ec245e Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Fri, 26 Sep 2025 15:40:58 -0700 Subject: [PATCH 06/11] Implement filtering of shuttles by proximity to the closest route polyline coordinate --- .../ApiBasedShuttleRepositoryLoader.ts | 54 +++++++++++++++++-- .../shuttle/ShuttleRepositoryLoader.ts | 2 +- 2 files changed, 50 insertions(+), 6 deletions(-) diff --git a/src/loaders/shuttle/ApiBasedShuttleRepositoryLoader.ts b/src/loaders/shuttle/ApiBasedShuttleRepositoryLoader.ts index e79dd45..fb7352a 100644 --- a/src/loaders/shuttle/ApiBasedShuttleRepositoryLoader.ts +++ b/src/loaders/shuttle/ApiBasedShuttleRepositoryLoader.ts @@ -1,7 +1,7 @@ import { ShuttleGetterSetterRepository } from "../../repositories/shuttle/ShuttleGetterSetterRepository"; import { IEta, IRoute, IShuttle, IStop } from "../../entities/ShuttleRepositoryEntities"; import { ShuttleRepositoryLoader } from "./ShuttleRepositoryLoader"; -import { IEntityWithId } from "../../entities/SharedEntities"; +import { ICoordinates, IEntityWithId } from "../../entities/SharedEntities"; import { ApiResponseError } from "../ApiResponseError"; /** @@ -10,12 +10,13 @@ import { ApiResponseError } from "../ApiResponseError"; * which inherit from `IEntityWithId`. */ export class ApiBasedShuttleRepositoryLoader implements ShuttleRepositoryLoader { - baseUrl = "https://passiogo.com/mapGetData.php"; + readonly baseUrl = "https://passiogo.com/mapGetData.php"; constructor( public passioSystemId: string, public systemIdForConstructedData: string, public repository: ShuttleGetterSetterRepository, + readonly shuttleToRouteCoordinateMaximumDistanceMiles = 10, ) { } @@ -31,7 +32,7 @@ export class ApiBasedShuttleRepositoryLoader implements ShuttleRepositoryLoader public async updateAll() { await this.updateRouteDataForSystem(); await this.updateStopAndPolylineDataForRoutesInSystem(); - await this.updateShuttleDataForSystem(); + await this.updateShuttleDataForSystemBasedOnProximityToRoutes(); // Because ETA method doesn't support pruning yet, // add a call to the clear method here @@ -155,11 +156,12 @@ export class ApiBasedShuttleRepositoryLoader implements ShuttleRepositoryLoader return await response.json(); } - public async updateShuttleDataForSystem() { + public async updateShuttleDataForSystemBasedOnProximityToRoutes() { try { const json = await this.fetchShuttleDataJson(); - const shuttles = this.constructShuttlesFromJson(json); + let shuttles = this.constructShuttlesFromJson(json); if (shuttles !== null) { + shuttles = await this.filterShuttlesByDistanceFromCorrespondingRoute(shuttles); await this.updateShuttleDataInRepository(shuttles); } else { console.warn(`Shuttle update failed for the following JSON: ${JSON.stringify(json)}`) @@ -391,4 +393,46 @@ export class ApiBasedShuttleRepositoryLoader implements ShuttleRepositoryLoader } } + private filterShuttlesByDistanceFromCorrespondingRoute = async (shuttles: IShuttle[]) => { + let filteredShuttles: IShuttle[] = []; + + await Promise.all(shuttles.map(async (shuttle) => { + const route = await this.repository.getRouteById(shuttle.routeId); + if (route != null) { + let closestDistanceMiles = Number.MAX_VALUE; + + route.polylineCoordinates.forEach((coordinate) => { + const calculatedDistance = ApiBasedShuttleRepositoryLoader.convertDistanceBetweenCoordinatesToMiles(coordinate, shuttle.coordinates) + if (closestDistanceMiles > calculatedDistance) { + closestDistanceMiles = calculatedDistance; + } + }); + + if (closestDistanceMiles <= this.shuttleToRouteCoordinateMaximumDistanceMiles) { + filteredShuttles.push(shuttle); + } + } else { + console.warn(`No corresponding route found for ID ${shuttle.routeId} of shuttle ${shuttle.id}`) + } + })); + + return filteredShuttles; + }; + + public static convertDistanceBetweenCoordinatesToMiles = (fromCoordinate: ICoordinates, toCoordinate: ICoordinates): number => { + const earthRadiusMiles = 3959; + + const lat1Rad = fromCoordinate.latitude * Math.PI / 180; + const lat2Rad = toCoordinate.latitude * Math.PI / 180; + const deltaLatRad = (toCoordinate.latitude - fromCoordinate.latitude) * Math.PI / 180; + const deltaLonRad = (toCoordinate.longitude - fromCoordinate.longitude) * Math.PI / 180; + + const a = Math.sin(deltaLatRad / 2) * Math.sin(deltaLatRad / 2) + + Math.cos(lat1Rad) * Math.cos(lat2Rad) * + Math.sin(deltaLonRad / 2) * Math.sin(deltaLonRad / 2); + + const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); + + return earthRadiusMiles * c; + }; } diff --git a/src/loaders/shuttle/ShuttleRepositoryLoader.ts b/src/loaders/shuttle/ShuttleRepositoryLoader.ts index 6c4eb3e..d556455 100644 --- a/src/loaders/shuttle/ShuttleRepositoryLoader.ts +++ b/src/loaders/shuttle/ShuttleRepositoryLoader.ts @@ -3,7 +3,7 @@ import { RepositoryLoader } from "../RepositoryLoader"; export interface ShuttleRepositoryLoader extends RepositoryLoader { updateRouteDataForSystem(): Promise; updateStopAndPolylineDataForRoutesInSystem(): Promise; - updateShuttleDataForSystem(): Promise; + updateShuttleDataForSystemBasedOnProximityToRoutes(): Promise; updateEtaDataForExistingStopsForSystem(): Promise; updateEtaDataForStopId(stopId: string): Promise; } From 5ed15ee5340984f9570dca01ba28ae7d3acb808c Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Fri, 26 Sep 2025 15:56:27 -0700 Subject: [PATCH 07/11] Update mock data arrangement of shuttle data test to match updated requirements --- ...iBasedShuttleRepositoryLoaderTests.test.ts | 50 ++++++++++++++----- 1 file changed, 37 insertions(+), 13 deletions(-) diff --git a/src/loaders/shuttle/__tests__/ApiBasedShuttleRepositoryLoaderTests.test.ts b/src/loaders/shuttle/__tests__/ApiBasedShuttleRepositoryLoaderTests.test.ts index fa636e4..7aeb9ff 100644 --- a/src/loaders/shuttle/__tests__/ApiBasedShuttleRepositoryLoaderTests.test.ts +++ b/src/loaders/shuttle/__tests__/ApiBasedShuttleRepositoryLoaderTests.test.ts @@ -5,7 +5,7 @@ import { fetchRouteDataSuccessfulResponse } from "../../../../testHelpers/jsonSn import { fetchStopAndPolylineDataSuccessfulResponse } from "../../../../testHelpers/jsonSnapshots/fetchStopAndPolylineData/fetchStopAndPolylineDataSuccessfulResponse"; -import { generateMockRoutes, generateMockShuttles, generateMockStops } from "../../../../testHelpers/mockDataGenerators"; +import { generateMockRoutes, generateMockStops } from "../../../../testHelpers/mockDataGenerators"; import { fetchShuttleDataSuccessfulResponse } from "../../../../testHelpers/jsonSnapshots/fetchShuttleData/fetchShuttleDataSuccessfulResponse"; @@ -36,7 +36,7 @@ describe("ApiBasedShuttleRepositoryLoader", () => { const spies = { updateRouteDataForSystem: jest.spyOn(loader, "updateRouteDataForSystem"), updateStopAndPolylineDataForRoutesInSystem: jest.spyOn(loader, "updateStopAndPolylineDataForRoutesInSystem"), - updateShuttleDataForSystem: jest.spyOn(loader, "updateShuttleDataForSystem"), + updateShuttleDataForSystem: jest.spyOn(loader, "updateShuttleDataForSystemBasedOnProximityToRoutes"), updateEtaDataForExistingStopsForSystem: jest.spyOn(loader, "updateEtaDataForExistingStopsForSystem"), }; @@ -124,18 +124,42 @@ describe("ApiBasedShuttleRepositoryLoader", () => { }) }); - describe("updateShuttleDataForSystem", () => { - it("updates shuttle data in repository if response received", async () => { - const shuttlesToPrune = generateMockShuttles(); - await Promise.all(shuttlesToPrune.map(async (shuttle) => { - shuttle.systemId = systemId; - await loader.repository.addOrUpdateShuttle(shuttle); - })) + describe("updateShuttleDataForSystemBasedOnProximityToRoutes", () => { + it("updates shuttle data in repository from API if shuttles close enough to route", async () => { + const distance = 1; + loader = new ApiBasedShuttleRepositoryLoader( + "263", + "1", + new UnoptimizedInMemoryShuttleRepository(), + distance, + ); - updateGlobalFetchMockJson(fetchShuttleDataSuccessfulResponse); - const busesInResponse = Object.values(fetchShuttleDataSuccessfulResponse.buses); + const routes = generateMockRoutes(); + routes[0].polylineCoordinates = [ + { latitude: 33.78792, longitude: -117.86187 }, + { latitude: 33.78792, longitude: -117.86200 }, + { latitude: 33.78792, longitude: -117.86245 } + ]; - await loader.updateShuttleDataForSystem(); + await Promise.all(routes.map(async (route) => { + await loader.repository.addOrUpdateRoute(route); + })); + + const modifiedSuccessfulResponse = { + ...fetchShuttleDataSuccessfulResponse, + }; + + Object.keys(modifiedSuccessfulResponse.buses).forEach((busId) => { + const bus = (modifiedSuccessfulResponse.buses as any)[busId][0]; + bus.latitude = "33.78792"; + bus.longitude = "-117.86187"; + bus.routeId = routes[0].id; + }); + + updateGlobalFetchMockJson(modifiedSuccessfulResponse); + const busesInResponse = Object.values(modifiedSuccessfulResponse.buses); + + await loader.updateShuttleDataForSystemBasedOnProximityToRoutes(); const shuttles = await loader.repository.getShuttles(); @@ -146,7 +170,7 @@ describe("ApiBasedShuttleRepositoryLoader", () => { updateGlobalFetchMockJsonToThrowSyntaxError(); await assertAsyncCallbackThrowsApiResponseError(async () => { - await loader.updateShuttleDataForSystem(); + await loader.updateShuttleDataForSystemBasedOnProximityToRoutes(); }); }); }); From 155ab2b4d248639ea6cab67f2bbf7755d4e4f30a Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Fri, 26 Sep 2025 16:01:32 -0700 Subject: [PATCH 08/11] Filter out out-of-service shuttles --- src/loaders/shuttle/ApiBasedShuttleRepositoryLoader.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/loaders/shuttle/ApiBasedShuttleRepositoryLoader.ts b/src/loaders/shuttle/ApiBasedShuttleRepositoryLoader.ts index fb7352a..9d8126d 100644 --- a/src/loaders/shuttle/ApiBasedShuttleRepositoryLoader.ts +++ b/src/loaders/shuttle/ApiBasedShuttleRepositoryLoader.ts @@ -159,7 +159,7 @@ export class ApiBasedShuttleRepositoryLoader implements ShuttleRepositoryLoader public async updateShuttleDataForSystemBasedOnProximityToRoutes() { try { const json = await this.fetchShuttleDataJson(); - let shuttles = this.constructShuttlesFromJson(json); + let shuttles = this.constructInServiceShuttlesFromJson(json); if (shuttles !== null) { shuttles = await this.filterShuttlesByDistanceFromCorrespondingRoute(shuttles); await this.updateShuttleDataInRepository(shuttles); @@ -210,11 +210,12 @@ export class ApiBasedShuttleRepositoryLoader implements ShuttleRepositoryLoader return await response.json(); } - private constructShuttlesFromJson(json: any): IShuttle[] | null { + private constructInServiceShuttlesFromJson(json: any): IShuttle[] | null { if (json.buses && json.buses["-1"] === undefined) { const jsonBuses = Object.values(json.buses).map((busesArr: any) => { return busesArr[0]; }); + jsonBuses.filter((bus) => bus.outOfService != 0) return jsonBuses.map((jsonBus: any) => { const constructedShuttle: IShuttle = { From 67a4b3fc8eaa9bc4b7520e497dd783f82fab8b1a Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Fri, 26 Sep 2025 16:12:01 -0700 Subject: [PATCH 09/11] Update test cases to test updating shuttle data depending on proximity --- ...iBasedShuttleRepositoryLoaderTests.test.ts | 80 ++++++++++++++----- 1 file changed, 60 insertions(+), 20 deletions(-) diff --git a/src/loaders/shuttle/__tests__/ApiBasedShuttleRepositoryLoaderTests.test.ts b/src/loaders/shuttle/__tests__/ApiBasedShuttleRepositoryLoaderTests.test.ts index 7aeb9ff..3f0ff94 100644 --- a/src/loaders/shuttle/__tests__/ApiBasedShuttleRepositoryLoaderTests.test.ts +++ b/src/loaders/shuttle/__tests__/ApiBasedShuttleRepositoryLoaderTests.test.ts @@ -16,6 +16,7 @@ import { updateGlobalFetchMockJsonToThrowSyntaxError } from "../../../../testHelpers/fetchMockHelpers"; import { assertAsyncCallbackThrowsApiResponseError } from "../../../../testHelpers/assertAsyncCallbackThrowsApiResponseError"; +import { IRoute } from "../../../entities/ShuttleRepositoryEntities"; describe("ApiBasedShuttleRepositoryLoader", () => { let loader: ApiBasedShuttleRepositoryLoader; @@ -125,47 +126,86 @@ describe("ApiBasedShuttleRepositoryLoader", () => { }); describe("updateShuttleDataForSystemBasedOnProximityToRoutes", () => { - it("updates shuttle data in repository from API if shuttles close enough to route", async () => { - const distance = 1; - loader = new ApiBasedShuttleRepositoryLoader( - "263", - "1", - new UnoptimizedInMemoryShuttleRepository(), - distance, - ); - + function generateMockRoutesWithPolylineCoordinates() { const routes = generateMockRoutes(); routes[0].polylineCoordinates = [ - { latitude: 33.78792, longitude: -117.86187 }, - { latitude: 33.78792, longitude: -117.86200 }, - { latitude: 33.78792, longitude: -117.86245 } + {latitude: 33.78792, longitude: -117.86187}, + {latitude: 33.78792, longitude: -117.86200}, + {latitude: 33.78792, longitude: -117.86245} ]; + return routes; + } - await Promise.all(routes.map(async (route) => { - await loader.repository.addOrUpdateRoute(route); - })); - + function getMockJsonResponseMatchingRouteAndCoordinates(route: IRoute, longitude: string, latitude: string) { const modifiedSuccessfulResponse = { ...fetchShuttleDataSuccessfulResponse, }; Object.keys(modifiedSuccessfulResponse.buses).forEach((busId) => { const bus = (modifiedSuccessfulResponse.buses as any)[busId][0]; - bus.latitude = "33.78792"; - bus.longitude = "-117.86187"; - bus.routeId = routes[0].id; + bus.latitude = latitude; + bus.longitude = longitude; + bus.routeId = route.id; }); + return modifiedSuccessfulResponse; + } + async function addMockRoutes(routes: IRoute[]) { + await Promise.all(routes.map(async (route) => { + await loader.repository.addOrUpdateRoute(route); + })); + } + + it("updates shuttle data in repository from API if shuttles close enough to route", async () => { + const distanceMiles = 1; + loader = new ApiBasedShuttleRepositoryLoader( + "263", + "1", + new UnoptimizedInMemoryShuttleRepository(), + distanceMiles, + ); + + const routes = generateMockRoutesWithPolylineCoordinates(); + await addMockRoutes(routes); + const modifiedSuccessfulResponse = getMockJsonResponseMatchingRouteAndCoordinates( + routes[0], + "-117.86187", + "33.78792" + ); updateGlobalFetchMockJson(modifiedSuccessfulResponse); const busesInResponse = Object.values(modifiedSuccessfulResponse.buses); await loader.updateShuttleDataForSystemBasedOnProximityToRoutes(); const shuttles = await loader.repository.getShuttles(); - expect(shuttles.length).toEqual(busesInResponse.length); }); + it("does not update shuttle data in repository from API if shuttles are not close enough to route", async () => { + const distanceMiles = 1; + loader = new ApiBasedShuttleRepositoryLoader( + "263", + "1", + new UnoptimizedInMemoryShuttleRepository(), + distanceMiles, + ); + + const routes = generateMockRoutesWithPolylineCoordinates(); + await addMockRoutes(routes); + + const modifiedSuccessfulResponse = getMockJsonResponseMatchingRouteAndCoordinates( + routes[0], + "-116.86187", + "32.78792" + ); + updateGlobalFetchMockJson(modifiedSuccessfulResponse); + + await loader.updateShuttleDataForSystemBasedOnProximityToRoutes(); + + const shuttles = await loader.repository.getShuttles(); + expect(shuttles.length).toEqual(0); + }); + it("throws the correct error if the API response contains no data", async () => { updateGlobalFetchMockJsonToThrowSyntaxError(); From dc50f98a6db932a35c46776923212de42e4f226e Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Fri, 26 Sep 2025 16:19:39 -0700 Subject: [PATCH 10/11] Restore the pruning test for shuttles --- ...iBasedShuttleRepositoryLoaderTests.test.ts | 45 ++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/src/loaders/shuttle/__tests__/ApiBasedShuttleRepositoryLoaderTests.test.ts b/src/loaders/shuttle/__tests__/ApiBasedShuttleRepositoryLoaderTests.test.ts index 3f0ff94..799b2bd 100644 --- a/src/loaders/shuttle/__tests__/ApiBasedShuttleRepositoryLoaderTests.test.ts +++ b/src/loaders/shuttle/__tests__/ApiBasedShuttleRepositoryLoaderTests.test.ts @@ -5,7 +5,7 @@ import { fetchRouteDataSuccessfulResponse } from "../../../../testHelpers/jsonSn import { fetchStopAndPolylineDataSuccessfulResponse } from "../../../../testHelpers/jsonSnapshots/fetchStopAndPolylineData/fetchStopAndPolylineDataSuccessfulResponse"; -import { generateMockRoutes, generateMockStops } from "../../../../testHelpers/mockDataGenerators"; +import { generateMockRoutes, generateMockShuttles, generateMockStops } from "../../../../testHelpers/mockDataGenerators"; import { fetchShuttleDataSuccessfulResponse } from "../../../../testHelpers/jsonSnapshots/fetchShuttleData/fetchShuttleDataSuccessfulResponse"; @@ -206,6 +206,49 @@ describe("ApiBasedShuttleRepositoryLoader", () => { expect(shuttles.length).toEqual(0); }); + it("prunes shuttles correctly", async () => { + const distanceMiles = 1; + loader = new ApiBasedShuttleRepositoryLoader( + "263", + "1", + new UnoptimizedInMemoryShuttleRepository(), + distanceMiles, + ); + + // Add mock shuttles to repository (these should be pruned) + const shuttlesToPrune = generateMockShuttles(); + await Promise.all(shuttlesToPrune.map(async (shuttle) => { + shuttle.systemId = systemId; + await loader.repository.addOrUpdateShuttle(shuttle); + })); + + const routes = generateMockRoutesWithPolylineCoordinates(); + await addMockRoutes(routes); + + const modifiedSuccessfulResponse = getMockJsonResponseMatchingRouteAndCoordinates( + routes[0], + "-117.86187", + "33.78792" + ); + + updateGlobalFetchMockJson(modifiedSuccessfulResponse); + + // Update shuttles from API + await loader.updateShuttleDataForSystemBasedOnProximityToRoutes(); + + // Old shuttles should be pruned, only API shuttles should remain + const shuttles = await loader.repository.getShuttles(); + const busesInResponse = Object.values(modifiedSuccessfulResponse.buses); + + expect(shuttles.length).toEqual(busesInResponse.length); + + // Verify none of the original mock shuttles remain + shuttlesToPrune.forEach((originalShuttle) => { + const foundShuttle = shuttles.find(s => s.id === originalShuttle.id); + expect(foundShuttle).toBeUndefined(); + }); + }); + it("throws the correct error if the API response contains no data", async () => { updateGlobalFetchMockJsonToThrowSyntaxError(); From 513c8bb12745603d0296f9e97bbe9a4385f2d10d Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Fri, 26 Sep 2025 16:24:20 -0700 Subject: [PATCH 11/11] Change the default maximum distance, and move it to environment.ts --- src/environment.ts | 2 ++ src/loaders/shuttle/ApiBasedShuttleRepositoryLoader.ts | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/environment.ts b/src/environment.ts index b47673a..b99c149 100644 --- a/src/environment.ts +++ b/src/environment.ts @@ -22,3 +22,5 @@ export const RATE_LIMIT_DELAY_MULTIPLIER_MS = process.env.RATE_LIMIT_DELAY_MULTI : 1000; export const REDIS_RECONNECT_INTERVAL = 30000; + +export const SHUTTLE_TO_ROUTE_COORDINATE_MAXIMUM_DISTANCE_MILES = 5; diff --git a/src/loaders/shuttle/ApiBasedShuttleRepositoryLoader.ts b/src/loaders/shuttle/ApiBasedShuttleRepositoryLoader.ts index 9d8126d..7e6f64e 100644 --- a/src/loaders/shuttle/ApiBasedShuttleRepositoryLoader.ts +++ b/src/loaders/shuttle/ApiBasedShuttleRepositoryLoader.ts @@ -3,6 +3,7 @@ import { IEta, IRoute, IShuttle, IStop } from "../../entities/ShuttleRepositoryE import { ShuttleRepositoryLoader } from "./ShuttleRepositoryLoader"; import { ICoordinates, IEntityWithId } from "../../entities/SharedEntities"; import { ApiResponseError } from "../ApiResponseError"; +import { SHUTTLE_TO_ROUTE_COORDINATE_MAXIMUM_DISTANCE_MILES } from "../../environment"; /** * Class which can load data into a repository from the @@ -16,7 +17,7 @@ export class ApiBasedShuttleRepositoryLoader implements ShuttleRepositoryLoader public passioSystemId: string, public systemIdForConstructedData: string, public repository: ShuttleGetterSetterRepository, - readonly shuttleToRouteCoordinateMaximumDistanceMiles = 10, + readonly shuttleToRouteCoordinateMaximumDistanceMiles = SHUTTLE_TO_ROUTE_COORDINATE_MAXIMUM_DISTANCE_MILES, ) { }