From a2f76703eaff8dbb85a280ccfd965ee16a33144f Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Tue, 18 Nov 2025 19:07:06 -0800 Subject: [PATCH 01/33] Remove behavior of ETA clearing when shuttle is approaching stop --- .../shuttle/eta/RedisSelfUpdatingETARepository.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/repositories/shuttle/eta/RedisSelfUpdatingETARepository.ts b/src/repositories/shuttle/eta/RedisSelfUpdatingETARepository.ts index 76e723b..f030856 100644 --- a/src/repositories/shuttle/eta/RedisSelfUpdatingETARepository.ts +++ b/src/repositories/shuttle/eta/RedisSelfUpdatingETARepository.ts @@ -182,11 +182,6 @@ export class RedisSelfUpdatingETARepository extends BaseRedisETARepository imple lastArrival, currentArrival, }: WillArriveAtStopPayload) { - const etas = await this.getEtasForShuttleId(currentArrival.shuttleId); - for (const eta of etas) { - await this.removeEtaIfExists(eta.shuttleId, eta.stopId); - } - // only update time traveled if last arrival exists if (lastArrival) { // disallow cases where this gets triggered multiple times From 0798773bcb43a3c0693b79f69f03556f077774a7 Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Tue, 18 Nov 2025 19:07:44 -0800 Subject: [PATCH 02/33] Replicate the change in the in-memory version --- .../shuttle/eta/InMemorySelfUpdatingETARepository.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/repositories/shuttle/eta/InMemorySelfUpdatingETARepository.ts b/src/repositories/shuttle/eta/InMemorySelfUpdatingETARepository.ts index 89212ab..602e8ed 100644 --- a/src/repositories/shuttle/eta/InMemorySelfUpdatingETARepository.ts +++ b/src/repositories/shuttle/eta/InMemorySelfUpdatingETARepository.ts @@ -160,11 +160,6 @@ export class InMemorySelfUpdatingETARepository extends BaseInMemoryETARepository lastArrival, currentArrival, }: WillArriveAtStopPayload): Promise { - const etas = await this.getEtasForShuttleId(currentArrival.shuttleId); - for (const eta of etas) { - await this.removeEtaIfExists(eta.shuttleId, eta.stopId); - } - if (lastArrival) { // disallow cases where this gets triggered multiple times if (lastArrival.stopId === currentArrival.stopId) return; From 34b2ab05d40527c38fbe0eee1f002fc5d35e9dfb Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Tue, 18 Nov 2025 19:14:29 -0800 Subject: [PATCH 03/33] Rename properties in WillArriveAtStopPayload for more clarity --- .../shuttle/RedisShuttleRepository.ts | 4 ++-- .../shuttle/ShuttleGetterRepository.ts | 4 ++-- .../UnoptimizedInMemoryShuttleRepository.ts | 18 +++++++++--------- .../ShuttleRepositorySharedTests.test.ts | 6 +++--- .../eta/InMemorySelfUpdatingETARepository.ts | 4 ++-- .../eta/RedisSelfUpdatingETARepository.ts | 4 ++-- 6 files changed, 20 insertions(+), 20 deletions(-) diff --git a/src/repositories/shuttle/RedisShuttleRepository.ts b/src/repositories/shuttle/RedisShuttleRepository.ts index 763dde5..f8da040 100644 --- a/src/repositories/shuttle/RedisShuttleRepository.ts +++ b/src/repositories/shuttle/RedisShuttleRepository.ts @@ -406,8 +406,8 @@ export class RedisShuttleRepository extends BaseRedisRepository implements Shutt shuttleId: shuttle.id, }; this.emit(ShuttleRepositoryEvent.SHUTTLE_WILL_ARRIVE_AT_STOP, { - lastArrival: lastStop, - currentArrival: shuttleArrival, + lastStopArrival: lastStop, + willArriveAt: shuttleArrival, }); await this.updateShuttleLastStopArrival(shuttleArrival); } diff --git a/src/repositories/shuttle/ShuttleGetterRepository.ts b/src/repositories/shuttle/ShuttleGetterRepository.ts index 7fbab0c..f4e223f 100644 --- a/src/repositories/shuttle/ShuttleGetterRepository.ts +++ b/src/repositories/shuttle/ShuttleGetterRepository.ts @@ -13,8 +13,8 @@ export type EtaRemovedEventPayload = IEta; export type EtaDataClearedEventPayload = IEta[]; export interface WillArriveAtStopPayload { - lastArrival?: ShuttleStopArrival; - currentArrival: ShuttleStopArrival; + lastStopArrival?: ShuttleStopArrival; + willArriveAt: ShuttleStopArrival; }; export interface ShuttleRepositoryEventPayloads { diff --git a/src/repositories/shuttle/UnoptimizedInMemoryShuttleRepository.ts b/src/repositories/shuttle/UnoptimizedInMemoryShuttleRepository.ts index a3e7dac..8203170 100644 --- a/src/repositories/shuttle/UnoptimizedInMemoryShuttleRepository.ts +++ b/src/repositories/shuttle/UnoptimizedInMemoryShuttleRepository.ts @@ -3,13 +3,13 @@ import { ShuttleGetterSetterRepository } from "./ShuttleGetterSetterRepository"; import { IOrderedStop, IRoute, IShuttle, IStop, shuttleHasArrivedAtStop } from "../../entities/ShuttleRepositoryEntities"; import { IEntityWithId } from "../../entities/SharedEntities"; import { - ShuttleRepositoryEvent, - ShuttleRepositoryEventListener, - ShuttleRepositoryEventName, - ShuttleRepositoryEventPayloads, - ShuttleStopArrival, - ShuttleTravelTimeDataIdentifier, - ShuttleTravelTimeDateFilterArguments, + ShuttleRepositoryEvent, + ShuttleRepositoryEventListener, + ShuttleRepositoryEventName, + ShuttleRepositoryEventPayloads, + ShuttleStopArrival, + ShuttleTravelTimeDataIdentifier, + ShuttleTravelTimeDateFilterArguments, } from "./ShuttleGetterRepository"; /** @@ -187,8 +187,8 @@ export class UnoptimizedInMemoryShuttleRepository shuttleId: shuttle.id, }; this.emit(ShuttleRepositoryEvent.SHUTTLE_WILL_ARRIVE_AT_STOP, { - lastArrival: lastStop, - currentArrival: shuttleArrival, + lastStopArrival: lastStop, + willArriveAt: shuttleArrival, }); await this.updateShuttleLastStopArrival(shuttleArrival); } diff --git a/src/repositories/shuttle/__tests__/ShuttleRepositorySharedTests.test.ts b/src/repositories/shuttle/__tests__/ShuttleRepositorySharedTests.test.ts index f875f2e..4d5a059 100644 --- a/src/repositories/shuttle/__tests__/ShuttleRepositorySharedTests.test.ts +++ b/src/repositories/shuttle/__tests__/ShuttleRepositorySharedTests.test.ts @@ -770,7 +770,7 @@ describe.each(repositoryImplementations)('$name', (holder) => { expect(listener).toHaveBeenCalledTimes(1); const emittedPayload = listener.mock.calls[0][0] as any; - expect(emittedPayload.currentArrival).toEqual({ + expect(emittedPayload.willArriveAt).toEqual({ shuttleId: shuttle.id, stopId: stop1.id, timestamp: arrivalTime, @@ -824,14 +824,14 @@ describe.each(repositoryImplementations)('$name', (holder) => { expect(listener).toHaveBeenCalledTimes(2); const firstPayload = listener.mock.calls[0][0] as any; - expect(firstPayload.currentArrival).toEqual({ + expect(firstPayload.willArriveAt).toEqual({ shuttleId: shuttle.id, stopId: stop1.id, timestamp: firstArrivalTime, }); const secondPayload = listener.mock.calls[1][0] as any; - expect(secondPayload.currentArrival).toEqual({ + expect(secondPayload.willArriveAt).toEqual({ shuttleId: shuttle.id, stopId: stop2.id, timestamp: secondArrivalTime, diff --git a/src/repositories/shuttle/eta/InMemorySelfUpdatingETARepository.ts b/src/repositories/shuttle/eta/InMemorySelfUpdatingETARepository.ts index 602e8ed..073547f 100644 --- a/src/repositories/shuttle/eta/InMemorySelfUpdatingETARepository.ts +++ b/src/repositories/shuttle/eta/InMemorySelfUpdatingETARepository.ts @@ -157,8 +157,8 @@ export class InMemorySelfUpdatingETARepository extends BaseInMemoryETARepository } private async handleShuttleWillArriveAtStop({ - lastArrival, - currentArrival, + lastStopArrival: lastArrival, + willArriveAt: currentArrival, }: WillArriveAtStopPayload): Promise { if (lastArrival) { // disallow cases where this gets triggered multiple times diff --git a/src/repositories/shuttle/eta/RedisSelfUpdatingETARepository.ts b/src/repositories/shuttle/eta/RedisSelfUpdatingETARepository.ts index f030856..e9658b7 100644 --- a/src/repositories/shuttle/eta/RedisSelfUpdatingETARepository.ts +++ b/src/repositories/shuttle/eta/RedisSelfUpdatingETARepository.ts @@ -179,8 +179,8 @@ export class RedisSelfUpdatingETARepository extends BaseRedisETARepository imple private async handleShuttleWillArriveAtStop({ - lastArrival, - currentArrival, + lastStopArrival: lastArrival, + willArriveAt: currentArrival, }: WillArriveAtStopPayload) { // only update time traveled if last arrival exists if (lastArrival) { From f334054b5e83a0bd84d2aabec6ceb40509350b79 Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Tue, 18 Nov 2025 19:17:04 -0800 Subject: [PATCH 04/33] Add SHUTTLE_WILL_LEAVE_STOP event and upload payload names --- src/repositories/shuttle/ShuttleGetterRepository.ts | 10 ++++++++-- .../shuttle/eta/InMemorySelfUpdatingETARepository.ts | 4 ++-- .../shuttle/eta/RedisSelfUpdatingETARepository.ts | 4 ++-- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/repositories/shuttle/ShuttleGetterRepository.ts b/src/repositories/shuttle/ShuttleGetterRepository.ts index f4e223f..e281ddc 100644 --- a/src/repositories/shuttle/ShuttleGetterRepository.ts +++ b/src/repositories/shuttle/ShuttleGetterRepository.ts @@ -5,6 +5,7 @@ export const ShuttleRepositoryEvent = { SHUTTLE_UPDATED: "shuttleUpdated", SHUTTLE_REMOVED: "shuttleRemoved", SHUTTLE_WILL_ARRIVE_AT_STOP: "shuttleArrivedAtStop", + SHUTTLE_WILL_LEAVE_STOP: "shuttleWillLeaveStop", } as const; export type ShuttleRepositoryEventName = typeof ShuttleRepositoryEvent[keyof typeof ShuttleRepositoryEvent]; @@ -12,15 +13,20 @@ export type ShuttleRepositoryEventName = typeof ShuttleRepositoryEvent[keyof typ export type EtaRemovedEventPayload = IEta; export type EtaDataClearedEventPayload = IEta[]; -export interface WillArriveAtStopPayload { +export interface ShuttleWillArriveAtStopPayload { lastStopArrival?: ShuttleStopArrival; willArriveAt: ShuttleStopArrival; }; +export interface ShuttleWillLeaveStopPayload { + stopArrivalThatShuttleIsLeaving: ShuttleStopArrival; +} + export interface ShuttleRepositoryEventPayloads { [ShuttleRepositoryEvent.SHUTTLE_UPDATED]: IShuttle, [ShuttleRepositoryEvent.SHUTTLE_REMOVED]: IShuttle, - [ShuttleRepositoryEvent.SHUTTLE_WILL_ARRIVE_AT_STOP]: WillArriveAtStopPayload, + [ShuttleRepositoryEvent.SHUTTLE_WILL_ARRIVE_AT_STOP]: ShuttleWillArriveAtStopPayload, + [ShuttleRepositoryEvent.SHUTTLE_WILL_LEAVE_STOP]: ShuttleWillLeaveStopPayload, } export type ShuttleRepositoryEventListener = ( diff --git a/src/repositories/shuttle/eta/InMemorySelfUpdatingETARepository.ts b/src/repositories/shuttle/eta/InMemorySelfUpdatingETARepository.ts index 073547f..87945a4 100644 --- a/src/repositories/shuttle/eta/InMemorySelfUpdatingETARepository.ts +++ b/src/repositories/shuttle/eta/InMemorySelfUpdatingETARepository.ts @@ -1,5 +1,5 @@ import { SelfUpdatingETARepository } from "./SelfUpdatingETARepository"; -import { ShuttleGetterRepository, ShuttleRepositoryEvent, ShuttleStopArrival, ShuttleTravelTimeDataIdentifier, ShuttleTravelTimeDateFilterArguments, WillArriveAtStopPayload } from "../ShuttleGetterRepository"; +import { ShuttleGetterRepository, ShuttleRepositoryEvent, ShuttleStopArrival, ShuttleTravelTimeDataIdentifier, ShuttleTravelTimeDateFilterArguments, ShuttleWillArriveAtStopPayload } from "../ShuttleGetterRepository"; import { BaseInMemoryETARepository } from "./BaseInMemoryETARepository"; import { IOrderedStop, IShuttle } from "../../../entities/ShuttleRepositoryEntities"; import { ETARepositoryEvent } from "./ETAGetterRepository"; @@ -159,7 +159,7 @@ export class InMemorySelfUpdatingETARepository extends BaseInMemoryETARepository private async handleShuttleWillArriveAtStop({ lastStopArrival: lastArrival, willArriveAt: currentArrival, - }: WillArriveAtStopPayload): Promise { + }: ShuttleWillArriveAtStopPayload): Promise { if (lastArrival) { // disallow cases where this gets triggered multiple times if (lastArrival.stopId === currentArrival.stopId) return; diff --git a/src/repositories/shuttle/eta/RedisSelfUpdatingETARepository.ts b/src/repositories/shuttle/eta/RedisSelfUpdatingETARepository.ts index e9658b7..b9f292c 100644 --- a/src/repositories/shuttle/eta/RedisSelfUpdatingETARepository.ts +++ b/src/repositories/shuttle/eta/RedisSelfUpdatingETARepository.ts @@ -1,7 +1,7 @@ import { SelfUpdatingETARepository } from "./SelfUpdatingETARepository"; import { BaseRedisETARepository } from "./BaseRedisETARepository"; import { createClient, RedisClientType } from "redis"; -import { ShuttleGetterRepository, ShuttleRepositoryEvent, ShuttleStopArrival, ShuttleTravelTimeDataIdentifier, ShuttleTravelTimeDateFilterArguments, WillArriveAtStopPayload } from "../ShuttleGetterRepository"; +import { ShuttleGetterRepository, ShuttleRepositoryEvent, ShuttleStopArrival, ShuttleTravelTimeDataIdentifier, ShuttleTravelTimeDateFilterArguments, ShuttleWillArriveAtStopPayload } from "../ShuttleGetterRepository"; import { REDIS_RECONNECT_INTERVAL } from "../../../environment"; import { IEta, IOrderedStop, IShuttle } from "../../../entities/ShuttleRepositoryEntities"; import { ETARepositoryEvent } from "./ETAGetterRepository"; @@ -181,7 +181,7 @@ export class RedisSelfUpdatingETARepository extends BaseRedisETARepository imple private async handleShuttleWillArriveAtStop({ lastStopArrival: lastArrival, willArriveAt: currentArrival, - }: WillArriveAtStopPayload) { + }: ShuttleWillArriveAtStopPayload) { // only update time traveled if last arrival exists if (lastArrival) { // disallow cases where this gets triggered multiple times From 47708d050e79440282a1f94a6b9f281a5ebd13ec Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Tue, 18 Nov 2025 19:50:07 -0800 Subject: [PATCH 05/33] Move duplicate shuttle declarations in tests to a "sample" shuttle generated by the setup function --- .../ShuttleRepositorySharedTests.test.ts | 99 +++---------------- ...lfUpdatingETARepositorySharedTests.test.ts | 60 ++--------- ...outeAndOrderedStopsForShuttleRepository.ts | 13 ++- 3 files changed, 36 insertions(+), 136 deletions(-) diff --git a/src/repositories/shuttle/__tests__/ShuttleRepositorySharedTests.test.ts b/src/repositories/shuttle/__tests__/ShuttleRepositorySharedTests.test.ts index 4d5a059..94dd9e8 100644 --- a/src/repositories/shuttle/__tests__/ShuttleRepositorySharedTests.test.ts +++ b/src/repositories/shuttle/__tests__/ShuttleRepositorySharedTests.test.ts @@ -565,17 +565,7 @@ describe.each(repositoryImplementations)('$name', (holder) => { describe("addOrUpdateShuttle with shuttle tracking", () => { test("updates the shuttle's last stop arrival if shuttle is at a stop", async () => { - const { route, systemId, stop2 } = await setupRouteAndOrderedStops(); - - const shuttle = { - id: "sh1", - name: "Shuttle 1", - routeId: route.id, - systemId: systemId, - coordinates: stop2.coordinates, - orientationInDegrees: 0, - updatedTime: new Date(), - }; + const { stop2, sampleShuttleNotInRepository: shuttle } = await setupRouteAndOrderedStops(); await repository.addOrUpdateShuttle(shuttle); const lastStop = await repository.getShuttleLastStopArrival(shuttle.id); @@ -585,17 +575,7 @@ describe.each(repositoryImplementations)('$name', (holder) => { describe("getArrivedStopIfExists", () => { test("gets the stop that the shuttle is currently at, if exists", async () => { - const { route, systemId, stop2 } = await setupRouteAndOrderedStops(); - - const shuttle = { - id: "sh1", - name: "Shuttle 1", - routeId: route.id, - systemId: systemId, - coordinates: stop2.coordinates, - orientationInDegrees: 0, - updatedTime: new Date(), - }; + const { sampleShuttleNotInRepository: shuttle } = await setupRouteAndOrderedStops(); const result = await repository.getArrivedStopIfExists(shuttle); @@ -605,17 +585,8 @@ describe.each(repositoryImplementations)('$name', (holder) => { }); test("returns undefined if shuttle is not currently at a stop", async () => { - const { route, systemId } = await setupRouteAndOrderedStops(); - - const shuttle = { - id: "sh1", - name: "Shuttle 1", - routeId: route.id, - systemId: systemId, - coordinates: { latitude: 12.5, longitude: 22.5 }, - orientationInDegrees: 0, - updatedTime: new Date(), - }; + const { sampleShuttleNotInRepository } = await setupRouteAndOrderedStops(); + const shuttle = { ...sampleShuttleNotInRepository, coordinates: { latitude: 12.5, longitude: 22.5 } }; // Not at any stop const result = await repository.getArrivedStopIfExists(shuttle); @@ -625,17 +596,8 @@ describe.each(repositoryImplementations)('$name', (holder) => { describe("getShuttleLastStopArrival", () => { test("gets the shuttle's last stop if existing in the data", async () => { - const { route, systemId, stop1 } = await setupRouteAndOrderedStops(); - - const shuttle = { - id: "sh1", - name: "Shuttle 1", - routeId: route.id, - systemId: systemId, - coordinates: stop1.coordinates, - orientationInDegrees: 0, - updatedTime: new Date(), - }; + const { stop1, sampleShuttleNotInRepository: shuttle } = await setupRouteAndOrderedStops(); + shuttle.coordinates = stop1.coordinates; const stopArrivalTime = new Date("2024-01-15T10:30:00Z"); await repository.addOrUpdateShuttle(shuttle, stopArrivalTime.getTime()); @@ -657,17 +619,8 @@ describe.each(repositoryImplementations)('$name', (holder) => { }); test("returns the most recent stop arrival when updated multiple times", async () => { - const { route, systemId, stop1, stop2 } = await setupRouteAndOrderedStops(); - - const shuttle = { - id: "sh1", - name: "Shuttle 1", - routeId: route.id, - systemId: systemId, - coordinates: stop1.coordinates, - orientationInDegrees: 0, - updatedTime: new Date(), - }; + const { stop1, stop2, sampleShuttleNotInRepository: shuttle } = await setupRouteAndOrderedStops(); + shuttle.coordinates = stop1.coordinates; const firstArrivalTime = new Date("2024-01-15T10:30:00Z"); await repository.addOrUpdateShuttle(shuttle, firstArrivalTime.getTime()); @@ -750,20 +703,12 @@ describe.each(repositoryImplementations)('$name', (holder) => { describe("SHUTTLE_WILL_ARRIVE_AT_STOP event", () => { test("emits SHUTTLE_WILL_ARRIVE_AT_STOP event before shuttle arrives at a stop", async () => { - const { route, systemId, stop1 } = await setupRouteAndOrderedStops(); + const { stop1, sampleShuttleNotInRepository: shuttle } = await setupRouteAndOrderedStops(); const listener = jest.fn(); repository.on(ShuttleRepositoryEvent.SHUTTLE_WILL_ARRIVE_AT_STOP, listener); - const shuttle = { - id: "sh1", - name: "Shuttle 1", - routeId: route.id, - systemId: systemId, - coordinates: stop1.coordinates, - orientationInDegrees: 0, - updatedTime: new Date(), - }; + shuttle.coordinates = stop1.coordinates; const arrivalTime = new Date("2024-01-15T10:30:00Z"); await repository.addOrUpdateShuttle(shuttle, arrivalTime.getTime()); @@ -778,20 +723,12 @@ describe.each(repositoryImplementations)('$name', (holder) => { }); test("does not emit event when shuttle is not at a stop", async () => { - const { route, systemId } = await setupRouteAndOrderedStops(); + const { sampleShuttleNotInRepository } = await setupRouteAndOrderedStops(); const listener = jest.fn(); repository.on(ShuttleRepositoryEvent.SHUTTLE_WILL_ARRIVE_AT_STOP, listener); - const shuttle = { - id: "sh1", - name: "Shuttle 1", - routeId: route.id, - systemId: systemId, - coordinates: { latitude: 12.5, longitude: 22.5 }, // Not at any stop - orientationInDegrees: 0, - updatedTime: new Date(), - }; + const shuttle = { ...sampleShuttleNotInRepository, coordinates: { latitude: 12.5, longitude: 22.5 } }; // Not at any stop await repository.addOrUpdateShuttle(shuttle); @@ -799,20 +736,12 @@ describe.each(repositoryImplementations)('$name', (holder) => { }); test("emits multiple events as shuttle visits multiple stops", async () => { - const { route, systemId, stop1, stop2 } = await setupRouteAndOrderedStops(); + const { stop1, stop2, sampleShuttleNotInRepository: shuttle } = await setupRouteAndOrderedStops(); const listener = jest.fn(); repository.on(ShuttleRepositoryEvent.SHUTTLE_WILL_ARRIVE_AT_STOP, listener); - const shuttle = { - id: "sh1", - name: "Shuttle 1", - routeId: route.id, - systemId: systemId, - coordinates: stop1.coordinates, - orientationInDegrees: 0, - updatedTime: new Date(), - }; + shuttle.coordinates = stop1.coordinates; const firstArrivalTime = new Date("2024-01-15T10:30:00Z"); await repository.addOrUpdateShuttle(shuttle, firstArrivalTime.getTime()); diff --git a/src/repositories/shuttle/eta/__tests__/SelfUpdatingETARepositorySharedTests.test.ts b/src/repositories/shuttle/eta/__tests__/SelfUpdatingETARepositorySharedTests.test.ts index b260e3e..8460724 100644 --- a/src/repositories/shuttle/eta/__tests__/SelfUpdatingETARepositorySharedTests.test.ts +++ b/src/repositories/shuttle/eta/__tests__/SelfUpdatingETARepositorySharedTests.test.ts @@ -76,19 +76,11 @@ describe.each(repositoryImplementations)('$name', (holder) => { describe("handleShuttleWillArriveAtStop", () => { test("updates how long the shuttle took to get from one stop to another", async () => { - const { route, systemId, stop2, stop1 } = await setupRouteAndOrderedStops(); + const { route, stop2, stop1, sampleShuttleNotInRepository: shuttle } = await setupRouteAndOrderedStops(); repository.startListeningForUpdates(); - const shuttle = { - id: "sh1", - name: "Shuttle 1", - routeId: route.id, - systemId: systemId, - coordinates: stop1.coordinates, - orientationInDegrees: 0, - updatedTime: new Date(), - }; + shuttle.coordinates = stop1.coordinates; const firstStopArrivalTime = new Date(2025, 0, 1, 12, 0, 0); await shuttleRepository.addOrUpdateShuttle(shuttle, firstStopArrivalTime.getTime()); @@ -118,7 +110,7 @@ describe.each(repositoryImplementations)('$name', (holder) => { currentTime: Date, shuttleSecondArrivalTimeAtFirstStop: Date ) { - const { route, systemId, stop1, stop2, stop3 } = await setupRouteAndOrderedStops(); + const { stop1, stop2, stop3, sampleShuttleNotInRepository: shuttle } = await setupRouteAndOrderedStops(); // Populating travel time data const firstStopArrivalTime = new Date(2025, 0, 1, 11, 0, 0); @@ -128,15 +120,7 @@ describe.each(repositoryImplementations)('$name', (holder) => { repository.setReferenceTime(currentTime); repository.startListeningForUpdates(); - const shuttle = { - id: "sh1", - name: "Shuttle 1", - routeId: route.id, - systemId: systemId, - coordinates: stop1.coordinates, - orientationInDegrees: 0, - updatedTime: new Date(), - }; + shuttle.coordinates = stop1.coordinates; await shuttleRepository.addOrUpdateShuttle(shuttle, firstStopArrivalTime.getTime()); @@ -196,19 +180,11 @@ describe.each(repositoryImplementations)('$name', (holder) => { describe("getAverageTravelTimeSeconds", () => { test("returns the average travel time when historical data exists", async () => { - const { route, systemId, stop1, stop2 } = await setupRouteAndOrderedStops(); + const { route, stop1, stop2, sampleShuttleNotInRepository: shuttle } = await setupRouteAndOrderedStops(); repository.startListeningForUpdates(); - const shuttle = { - id: "sh1", - name: "Shuttle 1", - routeId: route.id, - systemId: systemId, - coordinates: stop1.coordinates, - orientationInDegrees: 0, - updatedTime: new Date(), - }; + shuttle.coordinates = stop1.coordinates; const firstStopTime = new Date(2025, 0, 1, 12, 0, 0); await shuttleRepository.addOrUpdateShuttle(shuttle, firstStopTime.getTime()); @@ -232,19 +208,11 @@ describe.each(repositoryImplementations)('$name', (holder) => { }); test("returns average of multiple data points", async () => { - const { route, systemId, stop1, stop2 } = await setupRouteAndOrderedStops(); + const { route, stop1, stop2, sampleShuttleNotInRepository: shuttle } = await setupRouteAndOrderedStops(); repository.startListeningForUpdates(); - const shuttle = { - id: "sh1", - name: "Shuttle 1", - routeId: route.id, - systemId: systemId, - coordinates: stop1.coordinates, - orientationInDegrees: 0, - updatedTime: new Date(), - }; + shuttle.coordinates = stop1.coordinates; // First trip: 10 minutes travel time await shuttleRepository.addOrUpdateShuttle(shuttle, new Date(2025, 0, 1, 12, 0, 0).getTime()); @@ -288,19 +256,11 @@ describe.each(repositoryImplementations)('$name', (holder) => { }); test("returns undefined when querying outside the time range of data", async () => { - const { route, systemId, stop1, stop2 } = await setupRouteAndOrderedStops(); + const { route, stop1, stop2, sampleShuttleNotInRepository: shuttle } = await setupRouteAndOrderedStops(); repository.startListeningForUpdates(); - const shuttle = { - id: "sh1", - name: "Shuttle 1", - routeId: route.id, - systemId: systemId, - coordinates: stop1.coordinates, - orientationInDegrees: 0, - updatedTime: new Date(), - }; + shuttle.coordinates = stop1.coordinates; await shuttleRepository.addOrUpdateShuttle(shuttle, new Date(2025, 0, 1, 12, 0, 0).getTime()); shuttle.coordinates = stop2.coordinates; diff --git a/testHelpers/setupRouteAndOrderedStopsForShuttleRepository.ts b/testHelpers/setupRouteAndOrderedStopsForShuttleRepository.ts index 7dd80e9..65e81ba 100644 --- a/testHelpers/setupRouteAndOrderedStopsForShuttleRepository.ts +++ b/testHelpers/setupRouteAndOrderedStopsForShuttleRepository.ts @@ -1,4 +1,4 @@ -import { IOrderedStop, IStop } from "../src/entities/ShuttleRepositoryEntities"; +import { IOrderedStop, IShuttle, IStop } from "../src/entities/ShuttleRepositoryEntities"; import { ShuttleGetterSetterRepository } from "../src/repositories/shuttle/ShuttleGetterSetterRepository"; export async function setupRouteAndOrderedStopsForShuttleRepository( @@ -71,11 +71,22 @@ export async function setupRouteAndOrderedStopsForShuttleRepository( await shuttleRepository.addOrUpdateOrderedStop(orderedStop2); await shuttleRepository.addOrUpdateOrderedStop(orderedStop3); + const sampleShuttleNotInRepository: IShuttle = { + id: "sh1", + name: "Shuttle 1", + routeId: route.id, + systemId: systemId, + coordinates: stop2.coordinates, + orientationInDegrees: 0, + updatedTime: new Date(), + }; + return { route, systemId, stop1, stop2, stop3, + sampleShuttleNotInRepository, }; } From 8d9ff3ac6219b6ec1da35dda922f23ebae0204f7 Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Tue, 18 Nov 2025 19:57:15 -0800 Subject: [PATCH 06/33] Add a test for emitting the event --- .../ShuttleRepositorySharedTests.test.ts | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/repositories/shuttle/__tests__/ShuttleRepositorySharedTests.test.ts b/src/repositories/shuttle/__tests__/ShuttleRepositorySharedTests.test.ts index 94dd9e8..f12f4d9 100644 --- a/src/repositories/shuttle/__tests__/ShuttleRepositorySharedTests.test.ts +++ b/src/repositories/shuttle/__tests__/ShuttleRepositorySharedTests.test.ts @@ -767,4 +767,30 @@ describe.each(repositoryImplementations)('$name', (holder) => { }); }); }); + + describe("SHUTTLE_WILL_LEAVE_STOP event", () => { + test("emits SHUTTLE_WILL_LEAVE_EVENT as a shuttle is leaving a stop", async () => { + const { stop1, sampleShuttleNotInRepository: shuttle } = await setupRouteAndOrderedStops(); + shuttle.coordinates = stop1.coordinates; + + const listener = jest.fn(); + repository.addListener(ShuttleRepositoryEvent.SHUTTLE_WILL_LEAVE_STOP, listener); + + // Simulate arrival at stop 1 + await repository.addOrUpdateShuttle(shuttle); + + shuttle.coordinates = { latitude: 12.5, longitude: 22.5 }; + + // Simulate leaving stop 1 + await repository.addOrUpdateShuttle(shuttle); + + expect(listener).toHaveBeenCalledWith({ + stopArrivalThatShuttleIsLeaving: { + shuttleId: shuttle.id, + stopId: stop1.id, + timestamp: expect.any(Date), + }, + }); + }); + }); }); From 4ffdd35b2107acb479f5d0e6fbd87e2e53c5cdbf Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Tue, 18 Nov 2025 20:02:06 -0800 Subject: [PATCH 07/33] Add other tests (one for multiple events, one for not emitting the event) --- .../ShuttleRepositorySharedTests.test.ts | 77 ++++++++++++++++--- 1 file changed, 67 insertions(+), 10 deletions(-) diff --git a/src/repositories/shuttle/__tests__/ShuttleRepositorySharedTests.test.ts b/src/repositories/shuttle/__tests__/ShuttleRepositorySharedTests.test.ts index f12f4d9..e8e4aa2 100644 --- a/src/repositories/shuttle/__tests__/ShuttleRepositorySharedTests.test.ts +++ b/src/repositories/shuttle/__tests__/ShuttleRepositorySharedTests.test.ts @@ -769,27 +769,84 @@ describe.each(repositoryImplementations)('$name', (holder) => { }); describe("SHUTTLE_WILL_LEAVE_STOP event", () => { - test("emits SHUTTLE_WILL_LEAVE_EVENT as a shuttle is leaving a stop", async () => { + test("emits SHUTTLE_WILL_LEAVE_STOP event when shuttle leaves a stop", async () => { const { stop1, sampleShuttleNotInRepository: shuttle } = await setupRouteAndOrderedStops(); shuttle.coordinates = stop1.coordinates; const listener = jest.fn(); - repository.addListener(ShuttleRepositoryEvent.SHUTTLE_WILL_LEAVE_STOP, listener); + repository.on(ShuttleRepositoryEvent.SHUTTLE_WILL_LEAVE_STOP, listener); // Simulate arrival at stop 1 - await repository.addOrUpdateShuttle(shuttle); + const arrivalTime = new Date("2024-01-15T10:30:00Z"); + await repository.addOrUpdateShuttle(shuttle, arrivalTime.getTime()); - shuttle.coordinates = { latitude: 12.5, longitude: 22.5 }; + shuttle.coordinates = { latitude: 12.5, longitude: 22.5 }; // Not at any stop // Simulate leaving stop 1 + const leaveTime = new Date("2024-01-15T10:32:00Z"); + await repository.addOrUpdateShuttle(shuttle, leaveTime.getTime()); + + expect(listener).toHaveBeenCalledTimes(1); + const emittedPayload = listener.mock.calls[0][0] as any; + expect(emittedPayload.stopArrivalThatShuttleIsLeaving).toEqual({ + shuttleId: shuttle.id, + stopId: stop1.id, + timestamp: arrivalTime, + }); + }); + + test("does not emit event when shuttle was not at a stop", async () => { + const { sampleShuttleNotInRepository } = await setupRouteAndOrderedStops(); + + const listener = jest.fn(); + repository.on(ShuttleRepositoryEvent.SHUTTLE_WILL_LEAVE_STOP, listener); + + // Start at coordinates not at any stop + const shuttle = { ...sampleShuttleNotInRepository, coordinates: { latitude: 12.5, longitude: 22.5 } }; await repository.addOrUpdateShuttle(shuttle); - expect(listener).toHaveBeenCalledWith({ - stopArrivalThatShuttleIsLeaving: { - shuttleId: shuttle.id, - stopId: stop1.id, - timestamp: expect.any(Date), - }, + // Move to different coordinates; still not at any stop + shuttle.coordinates = { latitude: 13.0, longitude: 23.0 }; + await repository.addOrUpdateShuttle(shuttle); + + expect(listener).toHaveBeenCalledTimes(0); + }); + + test("emits multiple events as shuttle leaves multiple stops", async () => { + const { stop1, stop2, sampleShuttleNotInRepository: shuttle } = await setupRouteAndOrderedStops(); + + const listener = jest.fn(); + repository.on(ShuttleRepositoryEvent.SHUTTLE_WILL_LEAVE_STOP, listener); + + // Arrive at stop1 + shuttle.coordinates = stop1.coordinates; + const firstArrivalTime = new Date("2024-01-15T10:30:00Z"); + await repository.addOrUpdateShuttle(shuttle, firstArrivalTime.getTime()); + + // Leave stop1 and arrive at stop2 + shuttle.coordinates = stop2.coordinates; + const secondArrivalTime = new Date("2024-01-15T10:35:00Z"); + await repository.addOrUpdateShuttle(shuttle, secondArrivalTime.getTime()); + + // Leave stop2 + shuttle.coordinates = { latitude: 12.5, longitude: 22.5 }; // Not at any stop + const secondLeaveTime = new Date("2024-01-15T10:40:00Z"); + await repository.addOrUpdateShuttle(shuttle, secondLeaveTime.getTime()); + + expect(listener).toHaveBeenCalledTimes(2); + + const firstPayload = listener.mock.calls[0][0] as any; + expect(firstPayload.stopArrivalThatShuttleIsLeaving).toEqual({ + shuttleId: shuttle.id, + stopId: stop1.id, + timestamp: firstArrivalTime, + }); + + const secondPayload = listener.mock.calls[1][0] as any; + expect(secondPayload.stopArrivalThatShuttleIsLeaving).toEqual({ + shuttleId: shuttle.id, + stopId: stop2.id, + timestamp: secondArrivalTime, }); }); }); From 78cf60af4a9957d4318d8286b0cf4ca9d2ed05bf Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Tue, 18 Nov 2025 20:27:00 -0800 Subject: [PATCH 08/33] Add code to mark shuttle at a stop (Redis set) --- src/repositories/shuttle/RedisShuttleRepository.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/repositories/shuttle/RedisShuttleRepository.ts b/src/repositories/shuttle/RedisShuttleRepository.ts index f8da040..15c45a5 100644 --- a/src/repositories/shuttle/RedisShuttleRepository.ts +++ b/src/repositories/shuttle/RedisShuttleRepository.ts @@ -84,6 +84,11 @@ export class RedisShuttleRepository extends BaseRedisRepository implements Shutt return `shuttle:eta:historical:${routeId}:${fromStopId}:${toStopId}`; } + /** + * Represents a set storing the shuttles that are currently at a stop. + */ + private readonly shuttleIsAtStopKey = "shuttle:atstop"; + // Helper methods for converting entities to Redis hashes private createRedisHashFromStop = (stop: IStop): Record => ({ id: stop.id, @@ -409,10 +414,19 @@ export class RedisShuttleRepository extends BaseRedisRepository implements Shutt lastStopArrival: lastStop, willArriveAt: shuttleArrival, }); + await this.markShuttleAsAtStop(shuttleArrival.shuttleId); await this.updateShuttleLastStopArrival(shuttleArrival); } } + private async markShuttleAsAtStop(shuttleId: string) { + await this.redisClient.sAdd(this.shuttleIsAtStopKey, shuttleId); + } + + private async checkIfShuttleIsAtStop(shuttleId: string) { + return await this.redisClient.sIsMember(this.shuttleIsAtStopKey, shuttleId); + } + public async getAverageTravelTimeSeconds( { routeId, fromStopId, toStopId }: ShuttleTravelTimeDataIdentifier, { from, to }: ShuttleTravelTimeDateFilterArguments, From c8c5aa28c61db9c75b0cb3ff4a39f2110957303c Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Tue, 18 Nov 2025 20:42:06 -0800 Subject: [PATCH 09/33] Add a method to mark a shuttle as not at a stop --- src/repositories/shuttle/RedisShuttleRepository.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/repositories/shuttle/RedisShuttleRepository.ts b/src/repositories/shuttle/RedisShuttleRepository.ts index 15c45a5..91c1f62 100644 --- a/src/repositories/shuttle/RedisShuttleRepository.ts +++ b/src/repositories/shuttle/RedisShuttleRepository.ts @@ -423,6 +423,10 @@ export class RedisShuttleRepository extends BaseRedisRepository implements Shutt await this.redisClient.sAdd(this.shuttleIsAtStopKey, shuttleId); } + private async markShuttleAsNotAtStop(shuttleId: string) { + await this.redisClient.sRem(this.shuttleIsAtStopKey, shuttleId); + } + private async checkIfShuttleIsAtStop(shuttleId: string) { return await this.redisClient.sIsMember(this.shuttleIsAtStopKey, shuttleId); } From 83671e2b22ba438cfb0a3302c1db255739fed454 Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Tue, 18 Nov 2025 21:06:10 -0800 Subject: [PATCH 10/33] Add logic to fire the SHUTTLE_WILL_LEAVE_STOP event --- .../shuttle/RedisShuttleRepository.ts | 17 +++++-- .../UnoptimizedInMemoryShuttleRepository.ts | 49 ++++++++++++++----- 2 files changed, 52 insertions(+), 14 deletions(-) diff --git a/src/repositories/shuttle/RedisShuttleRepository.ts b/src/repositories/shuttle/RedisShuttleRepository.ts index 91c1f62..86a8d5d 100644 --- a/src/repositories/shuttle/RedisShuttleRepository.ts +++ b/src/repositories/shuttle/RedisShuttleRepository.ts @@ -398,13 +398,24 @@ export class RedisShuttleRepository extends BaseRedisRepository implements Shutt shuttle: IShuttle, travelTimeTimestamp = Date.now(), ) { + const isAtStop = await this.checkIfShuttleIsAtStop(shuttle.id); const arrivedStop = await this.getArrivedStopIfExists(shuttle); + // Will not fire *any* events if the same stop + const lastStop = await this.getShuttleLastStopArrival(shuttle.id); + if (lastStop?.stopId === arrivedStop?.id) return; + + if (isAtStop) { + if (lastStop) { + this.emit(ShuttleRepositoryEvent.SHUTTLE_WILL_LEAVE_STOP, { + stopArrivalThatShuttleIsLeaving: lastStop, + }); + } + await this.markShuttleAsNotAtStop(shuttle.id); + } + if (arrivedStop) { // stop if same stop - const lastStop = await this.getShuttleLastStopArrival(shuttle.id); - if (lastStop?.stopId === arrivedStop.id) return; - const shuttleArrival = { stopId: arrivedStop.id, timestamp: new Date(travelTimeTimestamp), diff --git a/src/repositories/shuttle/UnoptimizedInMemoryShuttleRepository.ts b/src/repositories/shuttle/UnoptimizedInMemoryShuttleRepository.ts index 8203170..ab34df6 100644 --- a/src/repositories/shuttle/UnoptimizedInMemoryShuttleRepository.ts +++ b/src/repositories/shuttle/UnoptimizedInMemoryShuttleRepository.ts @@ -3,13 +3,13 @@ import { ShuttleGetterSetterRepository } from "./ShuttleGetterSetterRepository"; import { IOrderedStop, IRoute, IShuttle, IStop, shuttleHasArrivedAtStop } from "../../entities/ShuttleRepositoryEntities"; import { IEntityWithId } from "../../entities/SharedEntities"; import { - ShuttleRepositoryEvent, - ShuttleRepositoryEventListener, - ShuttleRepositoryEventName, - ShuttleRepositoryEventPayloads, - ShuttleStopArrival, - ShuttleTravelTimeDataIdentifier, - ShuttleTravelTimeDateFilterArguments, + ShuttleRepositoryEvent, + ShuttleRepositoryEventListener, + ShuttleRepositoryEventName, + ShuttleRepositoryEventPayloads, + ShuttleStopArrival, + ShuttleTravelTimeDataIdentifier, + ShuttleTravelTimeDateFilterArguments, } from "./ShuttleGetterRepository"; /** @@ -74,6 +74,7 @@ export class UnoptimizedInMemoryShuttleRepository private orderedStops: IOrderedStop[] = []; private shuttleLastStopArrivals: Map = new Map(); private travelTimeData: Map> = new Map(); + private shuttlesAtStop: Set = new Set(); public async getStops(): Promise { return this.stops; @@ -174,13 +175,24 @@ export class UnoptimizedInMemoryShuttleRepository shuttle: IShuttle, travelTimeTimestamp = Date.now(), ) { + const isAtStop = await this.checkIfShuttleIsAtStop(shuttle.id); const arrivedStop = await this.getArrivedStopIfExists(shuttle); - if (arrivedStop != undefined) { - // stop if same stop - const lastStop = await this.getShuttleLastStopArrival(shuttle.id); - if (lastStop?.stopId === arrivedStop.id) return; + // Will not fire *any* events if the same stop + const lastStop = await this.getShuttleLastStopArrival(shuttle.id); + if (lastStop?.stopId === arrivedStop?.id) return; + if (isAtStop) { + if (lastStop) { + this.emit(ShuttleRepositoryEvent.SHUTTLE_WILL_LEAVE_STOP, { + stopArrivalThatShuttleIsLeaving: lastStop, + }); + } + await this.markShuttleAsNotAtStop(shuttle.id); + } + + if (arrivedStop) { + // stop if same stop const shuttleArrival = { stopId: arrivedStop.id, timestamp: new Date(travelTimeTimestamp), @@ -190,10 +202,23 @@ export class UnoptimizedInMemoryShuttleRepository lastStopArrival: lastStop, willArriveAt: shuttleArrival, }); + await this.markShuttleAsAtStop(shuttleArrival.shuttleId); await this.updateShuttleLastStopArrival(shuttleArrival); } } + private async markShuttleAsAtStop(shuttleId: string) { + this.shuttlesAtStop.add(shuttleId); + } + + private async markShuttleAsNotAtStop(shuttleId: string) { + this.shuttlesAtStop.delete(shuttleId); + } + + private async checkIfShuttleIsAtStop(shuttleId: string) { + return this.shuttlesAtStop.has(shuttleId); + } + private async updateShuttleLastStopArrival(lastStopArrival: ShuttleStopArrival) { this.shuttleLastStopArrivals.set(lastStopArrival.shuttleId, lastStopArrival); @@ -267,6 +292,7 @@ export class UnoptimizedInMemoryShuttleRepository const shuttle = await this.removeEntityByIdIfExists(shuttleId, this.shuttles); if (shuttle != null) { this.emit(ShuttleRepositoryEvent.SHUTTLE_REMOVED, shuttle); + this.shuttlesAtStop.delete(shuttleId); await this.removeShuttleLastStopIfExists(shuttleId); } return shuttle; @@ -289,6 +315,7 @@ export class UnoptimizedInMemoryShuttleRepository public async clearShuttleData(): Promise { this.shuttles = []; + this.shuttlesAtStop.clear(); await this.clearShuttleLastStopData(); } From b8c3f17510b83cc4e8402f66d5037e7921683cab Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Wed, 19 Nov 2025 10:57:27 -0800 Subject: [PATCH 11/33] Add test for ETA clearing --- .../shuttle/RedisShuttleRepository.ts | 1 - ...lfUpdatingETARepositorySharedTests.test.ts | 82 +++++++++++++++---- 2 files changed, 66 insertions(+), 17 deletions(-) diff --git a/src/repositories/shuttle/RedisShuttleRepository.ts b/src/repositories/shuttle/RedisShuttleRepository.ts index 86a8d5d..3d8cf60 100644 --- a/src/repositories/shuttle/RedisShuttleRepository.ts +++ b/src/repositories/shuttle/RedisShuttleRepository.ts @@ -415,7 +415,6 @@ export class RedisShuttleRepository extends BaseRedisRepository implements Shutt } if (arrivedStop) { - // stop if same stop const shuttleArrival = { stopId: arrivedStop.id, timestamp: new Date(travelTimeTimestamp), diff --git a/src/repositories/shuttle/eta/__tests__/SelfUpdatingETARepositorySharedTests.test.ts b/src/repositories/shuttle/eta/__tests__/SelfUpdatingETARepositorySharedTests.test.ts index 8460724..6f7d266 100644 --- a/src/repositories/shuttle/eta/__tests__/SelfUpdatingETARepositorySharedTests.test.ts +++ b/src/repositories/shuttle/eta/__tests__/SelfUpdatingETARepositorySharedTests.test.ts @@ -8,6 +8,7 @@ import { RedisShuttleRepository } from "../../RedisShuttleRepository"; import { UnoptimizedInMemoryShuttleRepository } from "../../UnoptimizedInMemoryShuttleRepository"; import { setupRouteAndOrderedStopsForShuttleRepository } from "../../../../../testHelpers/setupRouteAndOrderedStopsForShuttleRepository"; import { ShuttleGetterSetterRepository } from "../../ShuttleGetterSetterRepository"; +import { IShuttle, IStop } from "../../../../entities/ShuttleRepositoryEntities"; class RedisSelfUpdatingETARepositoryHolder implements RepositoryHolder { repo: RedisSelfUpdatingETARepository | undefined; @@ -74,6 +75,38 @@ describe.each(repositoryImplementations)('$name', (holder) => { return await setupRouteAndOrderedStopsForShuttleRepository(shuttleRepository); } + async function populateTravelTimeDataForStops({ + currentTime, + shuttle, + stop1, + stop2, + stop3, + firstStopArrivalTime = new Date(2025, 0, 1, 11, 0, 0), + secondStopArrivalTime = new Date(2025, 0, 1, 11, 15, 0), + thirdStopArrivalTime = new Date(2025, 0, 1, 11, 20, 0), + }: { + currentTime: Date; + shuttle: IShuttle; + stop1: IStop; + stop2: IStop; + stop3: IStop; + firstStopArrivalTime?: Date; + secondStopArrivalTime?: Date; + thirdStopArrivalTime?: Date; + }) { + repository.setReferenceTime(currentTime); + repository.startListeningForUpdates(); + + shuttle.coordinates = stop1.coordinates; + await shuttleRepository.addOrUpdateShuttle(shuttle, firstStopArrivalTime.getTime()); + + shuttle.coordinates = stop2.coordinates; + await shuttleRepository.addOrUpdateShuttle(shuttle, secondStopArrivalTime.getTime()); + + shuttle.coordinates = stop3.coordinates; + await shuttleRepository.addOrUpdateShuttle(shuttle, thirdStopArrivalTime.getTime()); + } + describe("handleShuttleWillArriveAtStop", () => { test("updates how long the shuttle took to get from one stop to another", async () => { const { route, stop2, stop1, sampleShuttleNotInRepository: shuttle } = await setupRouteAndOrderedStops(); @@ -113,22 +146,7 @@ describe.each(repositoryImplementations)('$name', (holder) => { const { stop1, stop2, stop3, sampleShuttleNotInRepository: shuttle } = await setupRouteAndOrderedStops(); // Populating travel time data - const firstStopArrivalTime = new Date(2025, 0, 1, 11, 0, 0); - const secondStopArrivalTime = new Date(2025, 0, 1, 11, 15, 0); - const thirdStopArrivalTime = new Date(2025, 0, 1, 11, 20, 0); - - repository.setReferenceTime(currentTime); - repository.startListeningForUpdates(); - - shuttle.coordinates = stop1.coordinates; - - await shuttleRepository.addOrUpdateShuttle(shuttle, firstStopArrivalTime.getTime()); - - shuttle.coordinates = stop2.coordinates; - await shuttleRepository.addOrUpdateShuttle(shuttle, secondStopArrivalTime.getTime()); - - shuttle.coordinates = stop3.coordinates; - await shuttleRepository.addOrUpdateShuttle(shuttle, thirdStopArrivalTime.getTime()); + await populateTravelTimeDataForStops({ currentTime, shuttle, stop1, stop2, stop3 }); // Populating ETA data shuttle.coordinates = stop1.coordinates; @@ -178,6 +196,38 @@ describe.each(repositoryImplementations)('$name', (holder) => { }); }); + describe("handleShuttleWillLeaveStop", () => { + test("clears ETA of correct stop on leaving stop", async () => { + const { stop1, stop2, stop3, sampleShuttleNotInRepository: shuttle } = await setupRouteAndOrderedStops(); + + const shuttleSecondArrivalTimeAtFirstStop = new Date(2025, 0, 1, 12, 5, 0); + const shuttleSecondArrivalTimeAtSecondStop = new Date(2025, 0, 1, 12, 20, 0); + const currentTime = new Date(shuttleSecondArrivalTimeAtSecondStop.getTime() + 3 * 60 * 1000); + + repository.setReferenceTime(currentTime); + repository.startListeningForUpdates(); + + await populateTravelTimeDataForStops({ currentTime, shuttle, stop1, stop2, stop3 }); + + // Populating ETA data + shuttle.coordinates = stop1.coordinates; + await shuttleRepository.addOrUpdateShuttle(shuttle, shuttleSecondArrivalTimeAtFirstStop.getTime()); + + shuttle.coordinates = stop2.coordinates; + await shuttleRepository.addOrUpdateShuttle(shuttle, shuttleSecondArrivalTimeAtSecondStop.getTime()); + + shuttle.coordinates = { latitude: 12.5, longitude: 12.5 } + await shuttleRepository.addOrUpdateShuttle(shuttle, currentTime.getTime()); + + await new Promise((resolve) => setTimeout(resolve, 1000)); + + const etaForStop3 = await repository.getEtaForShuttleAndStopId(shuttle.id, stop3.id); + expect(etaForStop3).not.toBeNull(); + const etaForStop2 = await repository.getEtaForShuttleAndStopId(shuttle.id, stop2.id); + expect(etaForStop2).toBeNull(); + }, 60000); + }); + describe("getAverageTravelTimeSeconds", () => { test("returns the average travel time when historical data exists", async () => { const { route, stop1, stop2, sampleShuttleNotInRepository: shuttle } = await setupRouteAndOrderedStops(); From 545eaef74ac3560d3abccb4c04944c875e46448a Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Wed, 19 Nov 2025 11:31:13 -0800 Subject: [PATCH 12/33] Add implementation and test for leaving stop event --- .../eta/InMemorySelfUpdatingETARepository.ts | 28 ++++++++++++++++-- .../eta/RedisSelfUpdatingETARepository.ts | 29 ++++++++++++++++--- ...lfUpdatingETARepositorySharedTests.test.ts | 9 ++---- 3 files changed, 53 insertions(+), 13 deletions(-) diff --git a/src/repositories/shuttle/eta/InMemorySelfUpdatingETARepository.ts b/src/repositories/shuttle/eta/InMemorySelfUpdatingETARepository.ts index 87945a4..164b734 100644 --- a/src/repositories/shuttle/eta/InMemorySelfUpdatingETARepository.ts +++ b/src/repositories/shuttle/eta/InMemorySelfUpdatingETARepository.ts @@ -1,5 +1,5 @@ import { SelfUpdatingETARepository } from "./SelfUpdatingETARepository"; -import { ShuttleGetterRepository, ShuttleRepositoryEvent, ShuttleStopArrival, ShuttleTravelTimeDataIdentifier, ShuttleTravelTimeDateFilterArguments, ShuttleWillArriveAtStopPayload } from "../ShuttleGetterRepository"; +import { ShuttleGetterRepository, ShuttleRepositoryEvent, ShuttleStopArrival, ShuttleTravelTimeDataIdentifier, ShuttleTravelTimeDateFilterArguments, ShuttleWillArriveAtStopPayload, ShuttleWillLeaveStopPayload } from "../ShuttleGetterRepository"; import { BaseInMemoryETARepository } from "./BaseInMemoryETARepository"; import { IOrderedStop, IShuttle } from "../../../entities/ShuttleRepositoryEntities"; import { ETARepositoryEvent } from "./ETAGetterRepository"; @@ -8,6 +8,8 @@ export class InMemorySelfUpdatingETARepository extends BaseInMemoryETARepository private referenceTime: Date | null = null; private travelTimeData: Map> = new Map(); + private isListening = false; + constructor( readonly shuttleRepository: ShuttleGetterRepository ) { @@ -16,8 +18,12 @@ export class InMemorySelfUpdatingETARepository extends BaseInMemoryETARepository this.setReferenceTime = this.setReferenceTime.bind(this); this.getAverageTravelTimeSeconds = this.getAverageTravelTimeSeconds.bind(this); this.startListeningForUpdates = this.startListeningForUpdates.bind(this); - this.handleShuttleUpdate = this.handleShuttleUpdate.bind(this); this.handleShuttleWillArriveAtStop = this.handleShuttleWillArriveAtStop.bind(this); + this.handleShuttleUpdate = this.handleShuttleUpdate.bind(this); + this.updateCascadingEta = this.updateCascadingEta.bind(this); + this.getAverageTravelTimeSecondsWithFallbacks = this.getAverageTravelTimeSecondsWithFallbacks.bind(this); + this.removeEtaIfExists = this.removeEtaIfExists.bind(this); + this.handleShuttleWillLeaveStop = this.handleShuttleWillLeaveStop.bind(this); } setReferenceTime(referenceTime: Date): void { @@ -51,13 +57,23 @@ export class InMemorySelfUpdatingETARepository extends BaseInMemoryETARepository } startListeningForUpdates(): void { + if (this.isListening) { + console.warn("Already listening to updates; did you call startListeningForUpdates twice?"); + return; + } this.shuttleRepository.addListener(ShuttleRepositoryEvent.SHUTTLE_UPDATED, this.handleShuttleUpdate); this.shuttleRepository.addListener(ShuttleRepositoryEvent.SHUTTLE_WILL_ARRIVE_AT_STOP, this.handleShuttleWillArriveAtStop); + this.shuttleRepository.addListener(ShuttleRepositoryEvent.SHUTTLE_WILL_LEAVE_STOP, this.handleShuttleWillLeaveStop); + this.isListening = true; } - stopListeningForUpdates(): void { + if (!this.isListening) { + return; + } this.shuttleRepository.removeListener(ShuttleRepositoryEvent.SHUTTLE_UPDATED, this.handleShuttleUpdate); this.shuttleRepository.removeListener(ShuttleRepositoryEvent.SHUTTLE_WILL_ARRIVE_AT_STOP, this.handleShuttleWillArriveAtStop); + this.shuttleRepository.removeListener(ShuttleRepositoryEvent.SHUTTLE_WILL_LEAVE_STOP, this.handleShuttleWillLeaveStop); + this.isListening = false; } private async getAverageTravelTimeSecondsWithFallbacks( @@ -176,6 +192,12 @@ export class InMemorySelfUpdatingETARepository extends BaseInMemoryETARepository } } + private async handleShuttleWillLeaveStop({ + stopArrivalThatShuttleIsLeaving, + }: ShuttleWillLeaveStopPayload) { + await this.removeEtaIfExists(stopArrivalThatShuttleIsLeaving.shuttleId, stopArrivalThatShuttleIsLeaving.stopId); + } + private async addTravelTimeDataPoint( { routeId, fromStopId, toStopId }: ShuttleTravelTimeDataIdentifier, travelTimeSeconds: number, diff --git a/src/repositories/shuttle/eta/RedisSelfUpdatingETARepository.ts b/src/repositories/shuttle/eta/RedisSelfUpdatingETARepository.ts index b9f292c..230eca1 100644 --- a/src/repositories/shuttle/eta/RedisSelfUpdatingETARepository.ts +++ b/src/repositories/shuttle/eta/RedisSelfUpdatingETARepository.ts @@ -1,12 +1,14 @@ import { SelfUpdatingETARepository } from "./SelfUpdatingETARepository"; import { BaseRedisETARepository } from "./BaseRedisETARepository"; import { createClient, RedisClientType } from "redis"; -import { ShuttleGetterRepository, ShuttleRepositoryEvent, ShuttleStopArrival, ShuttleTravelTimeDataIdentifier, ShuttleTravelTimeDateFilterArguments, ShuttleWillArriveAtStopPayload } from "../ShuttleGetterRepository"; +import { ShuttleGetterRepository, ShuttleRepositoryEvent, ShuttleStopArrival, ShuttleTravelTimeDataIdentifier, ShuttleTravelTimeDateFilterArguments, ShuttleWillArriveAtStopPayload, ShuttleWillLeaveStopPayload } from "../ShuttleGetterRepository"; import { REDIS_RECONNECT_INTERVAL } from "../../../environment"; import { IEta, IOrderedStop, IShuttle } from "../../../entities/ShuttleRepositoryEntities"; import { ETARepositoryEvent } from "./ETAGetterRepository"; export class RedisSelfUpdatingETARepository extends BaseRedisETARepository implements SelfUpdatingETARepository { + private isListening = false; + constructor( readonly shuttleRepository: ShuttleGetterRepository, redisClient: RedisClientType = createClient({ @@ -29,6 +31,7 @@ export class RedisSelfUpdatingETARepository extends BaseRedisETARepository imple this.updateCascadingEta = this.updateCascadingEta.bind(this); this.getAverageTravelTimeSecondsWithFallbacks = this.getAverageTravelTimeSecondsWithFallbacks.bind(this); this.removeEtaIfExists = this.removeEtaIfExists.bind(this); + this.handleShuttleWillLeaveStop = this.handleShuttleWillLeaveStop.bind(this); } private createHistoricalEtaTimeSeriesKey = (routeId: string, fromStopId: string, toStopId: string) => { @@ -71,14 +74,25 @@ export class RedisSelfUpdatingETARepository extends BaseRedisETARepository imple } } - startListeningForUpdates() { + startListeningForUpdates(): void { + if (this.isListening) { + console.warn("Already listening to updates; did you call startListeningForUpdates twice?"); + return; + } this.shuttleRepository.addListener(ShuttleRepositoryEvent.SHUTTLE_UPDATED, this.handleShuttleUpdate); - this.shuttleRepository.addListener(ShuttleRepositoryEvent.SHUTTLE_WILL_ARRIVE_AT_STOP, this.handleShuttleWillArriveAtStop) + this.shuttleRepository.addListener(ShuttleRepositoryEvent.SHUTTLE_WILL_ARRIVE_AT_STOP, this.handleShuttleWillArriveAtStop); + this.shuttleRepository.addListener(ShuttleRepositoryEvent.SHUTTLE_WILL_LEAVE_STOP, this.handleShuttleWillLeaveStop); + this.isListening = true; } - stopListeningForUpdates() { + stopListeningForUpdates(): void { + if (!this.isListening) { + return; + } this.shuttleRepository.removeListener(ShuttleRepositoryEvent.SHUTTLE_UPDATED, this.handleShuttleUpdate); this.shuttleRepository.removeListener(ShuttleRepositoryEvent.SHUTTLE_WILL_ARRIVE_AT_STOP, this.handleShuttleWillArriveAtStop); + this.shuttleRepository.removeListener(ShuttleRepositoryEvent.SHUTTLE_WILL_LEAVE_STOP, this.handleShuttleWillLeaveStop); + this.isListening = false; } private async getAverageTravelTimeSecondsWithFallbacks( @@ -199,6 +213,13 @@ export class RedisSelfUpdatingETARepository extends BaseRedisETARepository imple } } + private async handleShuttleWillLeaveStop({ + stopArrivalThatShuttleIsLeaving, + }: ShuttleWillLeaveStopPayload) { + await this.removeEtaIfExists(stopArrivalThatShuttleIsLeaving.shuttleId, stopArrivalThatShuttleIsLeaving.stopId); + } + + public async addTravelTimeDataPoint( { routeId, fromStopId, toStopId }: ShuttleTravelTimeDataIdentifier, travelTimeSeconds: number, diff --git a/src/repositories/shuttle/eta/__tests__/SelfUpdatingETARepositorySharedTests.test.ts b/src/repositories/shuttle/eta/__tests__/SelfUpdatingETARepositorySharedTests.test.ts index 6f7d266..86ae52b 100644 --- a/src/repositories/shuttle/eta/__tests__/SelfUpdatingETARepositorySharedTests.test.ts +++ b/src/repositories/shuttle/eta/__tests__/SelfUpdatingETARepositorySharedTests.test.ts @@ -54,7 +54,7 @@ class InMemorySelfUpdatingETARepositoryHolder implements RepositoryHolder { @@ -200,13 +200,10 @@ describe.each(repositoryImplementations)('$name', (holder) => { test("clears ETA of correct stop on leaving stop", async () => { const { stop1, stop2, stop3, sampleShuttleNotInRepository: shuttle } = await setupRouteAndOrderedStops(); - const shuttleSecondArrivalTimeAtFirstStop = new Date(2025, 0, 1, 12, 5, 0); - const shuttleSecondArrivalTimeAtSecondStop = new Date(2025, 0, 1, 12, 20, 0); + const shuttleSecondArrivalTimeAtFirstStop = new Date(2025, 0, 8, 12, 0, 0); + const shuttleSecondArrivalTimeAtSecondStop = new Date(2025, 0, 8, 12, 15, 0); const currentTime = new Date(shuttleSecondArrivalTimeAtSecondStop.getTime() + 3 * 60 * 1000); - repository.setReferenceTime(currentTime); - repository.startListeningForUpdates(); - await populateTravelTimeDataForStops({ currentTime, shuttle, stop1, stop2, stop3 }); // Populating ETA data From dbcc882a18cfc6c644d38e59a197dca168ef656f Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Wed, 19 Nov 2025 11:43:04 -0800 Subject: [PATCH 13/33] Only update ETAs in shuttle repository loader if repository was injected --- src/loaders/shuttle/ApiBasedShuttleRepositoryLoader.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/loaders/shuttle/ApiBasedShuttleRepositoryLoader.ts b/src/loaders/shuttle/ApiBasedShuttleRepositoryLoader.ts index 19d4a14..bef753a 100644 --- a/src/loaders/shuttle/ApiBasedShuttleRepositoryLoader.ts +++ b/src/loaders/shuttle/ApiBasedShuttleRepositoryLoader.ts @@ -37,9 +37,9 @@ export class ApiBasedShuttleRepositoryLoader implements ShuttleRepositoryLoader await this.updateStopAndPolylineDataForRoutesInSystem(); await this.updateShuttleDataForSystemBasedOnProximityToRoutes(); - // Because ETA method doesn't support pruning yet, - // add a call to the clear method here - await this.updateEtaDataForExistingStopsForSystem(); + if (this.etaRepository) { + await this.updateEtaDataForExistingStopsForSystem(); + } } public async updateRouteDataForSystem() { From 2ff71b0dd19602fcd60e72a0baae2a8a6399ab52 Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Wed, 19 Nov 2025 11:48:22 -0800 Subject: [PATCH 14/33] Define a new event to indicate shuttle is near a stop --- src/repositories/shuttle/ShuttleGetterRepository.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/repositories/shuttle/ShuttleGetterRepository.ts b/src/repositories/shuttle/ShuttleGetterRepository.ts index e281ddc..78715a3 100644 --- a/src/repositories/shuttle/ShuttleGetterRepository.ts +++ b/src/repositories/shuttle/ShuttleGetterRepository.ts @@ -4,6 +4,7 @@ import type EventEmitter from "node:events"; export const ShuttleRepositoryEvent = { SHUTTLE_UPDATED: "shuttleUpdated", SHUTTLE_REMOVED: "shuttleRemoved", + SHUTTLE_IS_NEAR_NEXT_STOP: "shuttleIsNearNextStop", SHUTTLE_WILL_ARRIVE_AT_STOP: "shuttleArrivedAtStop", SHUTTLE_WILL_LEAVE_STOP: "shuttleWillLeaveStop", } as const; @@ -18,6 +19,8 @@ export interface ShuttleWillArriveAtStopPayload { willArriveAt: ShuttleStopArrival; }; +export type ShuttleIsNearStopPayload = ShuttleWillArriveAtStopPayload; + export interface ShuttleWillLeaveStopPayload { stopArrivalThatShuttleIsLeaving: ShuttleStopArrival; } @@ -25,6 +28,7 @@ export interface ShuttleWillLeaveStopPayload { export interface ShuttleRepositoryEventPayloads { [ShuttleRepositoryEvent.SHUTTLE_UPDATED]: IShuttle, [ShuttleRepositoryEvent.SHUTTLE_REMOVED]: IShuttle, + [ShuttleRepositoryEvent.SHUTTLE_IS_NEAR_NEXT_STOP]: ShuttleIsNearStopPayload, [ShuttleRepositoryEvent.SHUTTLE_WILL_ARRIVE_AT_STOP]: ShuttleWillArriveAtStopPayload, [ShuttleRepositoryEvent.SHUTTLE_WILL_LEAVE_STOP]: ShuttleWillLeaveStopPayload, } From fb01406a29d781f9bfeb579dd8693971607d085c Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Wed, 19 Nov 2025 11:51:58 -0800 Subject: [PATCH 15/33] Add placeholder test cases for SHUTTLE_IS_NEAR_NEXT_STOP event --- .../ShuttleRepositorySharedTests.test.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/repositories/shuttle/__tests__/ShuttleRepositorySharedTests.test.ts b/src/repositories/shuttle/__tests__/ShuttleRepositorySharedTests.test.ts index e8e4aa2..96afb73 100644 --- a/src/repositories/shuttle/__tests__/ShuttleRepositorySharedTests.test.ts +++ b/src/repositories/shuttle/__tests__/ShuttleRepositorySharedTests.test.ts @@ -701,6 +701,24 @@ describe.each(repositoryImplementations)('$name', (holder) => { }); }); + describe("SHUTTLE_IS_NEAR_NEXT_STOP event", () => { + test("emits SHUTTLE_IS_NEAR_NEXT_STOP when shuttle is within a set proximity of the next stop", async () => { + throw new Error("Not implemented"); + }); + + test("does not emit event when shuttle is near a stop, but it's not the shuttle's next stop", async () => { + throw new Error("Not implemented"); + }); + + test("only emits the event once", async () => { + throw new Error("Not implemented"); + }); + + test("does not emit event when shuttle is not near a stop", async () => { + throw new Error("Not implemented"); + }); + }); + describe("SHUTTLE_WILL_ARRIVE_AT_STOP event", () => { test("emits SHUTTLE_WILL_ARRIVE_AT_STOP event before shuttle arrives at a stop", async () => { const { stop1, sampleShuttleNotInRepository: shuttle } = await setupRouteAndOrderedStops(); From 1d98e8450e3963a9afffe11e2571bd15d2347ab0 Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Thu, 20 Nov 2025 16:26:25 -0800 Subject: [PATCH 16/33] Update InterchangeSystemBuilderArguments with new properties for controlling event emitter degree delta --- src/entities/InterchangeSystem.ts | 16 +++++++++++++++- src/index.ts | 2 ++ testHelpers/apolloTestServerHelpers.ts | 2 ++ 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/entities/InterchangeSystem.ts b/src/entities/InterchangeSystem.ts index 985a819..8ed86d3 100644 --- a/src/entities/InterchangeSystem.ts +++ b/src/entities/InterchangeSystem.ts @@ -46,7 +46,21 @@ export interface InterchangeSystemBuilderArguments { * Controls whether to self-calculate ETAs or use the external * shuttle provider for them. */ - useSelfUpdatingEtas: boolean + useSelfUpdatingEtas: boolean; + + /** + * The size of the threshold to detect when a shuttle has arrived + * at a stop, in latitude/longitude degrees. + */ + shuttleStopArrivalDegreeDelta: number; + + /** + * The size of the threshold to detect when a shuttle is "near" + * a stop, in latitude/longitude degrees. To determine this value, + * find the distance at which the shuttle would normally take + * ~1 minute to reach the next stop. + */ + shutleStopNearbyDegreeDelta: number; } export class InterchangeSystem { diff --git a/src/index.ts b/src/index.ts index a8fd797..03cd30f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -24,6 +24,8 @@ const supportedSystems: InterchangeSystemBuilderArguments[] = [ parkingSystemId: ChapmanApiBasedParkingRepositoryLoader.id, name: "Chapman University", useSelfUpdatingEtas: true, + shuttleStopArrivalDegreeDelta: 0.001, + shutleStopNearbyDegreeDelta: 0.003, } ] diff --git a/testHelpers/apolloTestServerHelpers.ts b/testHelpers/apolloTestServerHelpers.ts index 60a8355..83f3173 100644 --- a/testHelpers/apolloTestServerHelpers.ts +++ b/testHelpers/apolloTestServerHelpers.ts @@ -25,6 +25,8 @@ const systemInfoForTesting: InterchangeSystemBuilderArguments = { passioSystemId: "263", parkingSystemId: ChapmanApiBasedParkingRepositoryLoader.id, useSelfUpdatingEtas: false, + shuttleStopArrivalDegreeDelta: 0.001, + shutleStopNearbyDegreeDelta: 0.003, }; export function buildSystemForTesting() { From b202189ad6593aea0bdbd3f07115ab60569874d2 Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Thu, 20 Nov 2025 16:29:28 -0800 Subject: [PATCH 17/33] Fix typo in "shuttle" --- src/entities/InterchangeSystem.ts | 2 +- src/index.ts | 2 +- testHelpers/apolloTestServerHelpers.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/entities/InterchangeSystem.ts b/src/entities/InterchangeSystem.ts index 8ed86d3..b8cfbe1 100644 --- a/src/entities/InterchangeSystem.ts +++ b/src/entities/InterchangeSystem.ts @@ -60,7 +60,7 @@ export interface InterchangeSystemBuilderArguments { * find the distance at which the shuttle would normally take * ~1 minute to reach the next stop. */ - shutleStopNearbyDegreeDelta: number; + shuttleStopNearbyDegreeDelta: number; } export class InterchangeSystem { diff --git a/src/index.ts b/src/index.ts index 03cd30f..fb2d04d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -25,7 +25,7 @@ const supportedSystems: InterchangeSystemBuilderArguments[] = [ name: "Chapman University", useSelfUpdatingEtas: true, shuttleStopArrivalDegreeDelta: 0.001, - shutleStopNearbyDegreeDelta: 0.003, + shuttleStopNearbyDegreeDelta: 0.003, } ] diff --git a/testHelpers/apolloTestServerHelpers.ts b/testHelpers/apolloTestServerHelpers.ts index 83f3173..52cc048 100644 --- a/testHelpers/apolloTestServerHelpers.ts +++ b/testHelpers/apolloTestServerHelpers.ts @@ -26,7 +26,7 @@ const systemInfoForTesting: InterchangeSystemBuilderArguments = { parkingSystemId: ChapmanApiBasedParkingRepositoryLoader.id, useSelfUpdatingEtas: false, shuttleStopArrivalDegreeDelta: 0.001, - shutleStopNearbyDegreeDelta: 0.003, + shuttleStopNearbyDegreeDelta: 0.003, }; export function buildSystemForTesting() { From 645fe1055b018a0d7a28d9ab48860185344a54f3 Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Thu, 20 Nov 2025 16:41:14 -0800 Subject: [PATCH 18/33] Add createRedisClientForRepository method to deduplicate default Redis client in constructor --- src/helpers/createRedisClientForRepository.ts | 14 ++++++++++++++ src/repositories/BaseRedisRepository.ts | 13 +++---------- .../shuttle/eta/RedisSelfUpdatingETARepository.ts | 13 +++---------- 3 files changed, 20 insertions(+), 20 deletions(-) create mode 100644 src/helpers/createRedisClientForRepository.ts diff --git a/src/helpers/createRedisClientForRepository.ts b/src/helpers/createRedisClientForRepository.ts new file mode 100644 index 0000000..55312fd --- /dev/null +++ b/src/helpers/createRedisClientForRepository.ts @@ -0,0 +1,14 @@ +import { createClient, RedisClientType } from "redis"; +import { REDIS_RECONNECT_INTERVAL } from "../environment"; + +export default function createRedisClientForRepository() { + const client = createClient({ + url: process.env.REDIS_URL, + socket: { + tls: process.env.NODE_ENV === 'production', + rejectUnauthorized: false, + reconnectStrategy: REDIS_RECONNECT_INTERVAL, + }, + }); + return client as RedisClientType; +} diff --git a/src/repositories/BaseRedisRepository.ts b/src/repositories/BaseRedisRepository.ts index f722586..c2ec079 100644 --- a/src/repositories/BaseRedisRepository.ts +++ b/src/repositories/BaseRedisRepository.ts @@ -1,19 +1,12 @@ -import { createClient, RedisClientType } from 'redis'; -import { REDIS_RECONNECT_INTERVAL } from "../environment"; +import { RedisClientType } from 'redis'; import { EventEmitter } from 'stream'; +import createRedisClientForRepository from '../helpers/createRedisClientForRepository'; export abstract class BaseRedisRepository extends EventEmitter { protected redisClient; constructor( - redisClient: RedisClientType = createClient({ - url: process.env.REDIS_URL, - socket: { - tls: process.env.NODE_ENV === 'production', - rejectUnauthorized: false, - reconnectStrategy: REDIS_RECONNECT_INTERVAL, - }, - }), + redisClient: RedisClientType = createRedisClientForRepository(), ) { super(); this.redisClient = redisClient; diff --git a/src/repositories/shuttle/eta/RedisSelfUpdatingETARepository.ts b/src/repositories/shuttle/eta/RedisSelfUpdatingETARepository.ts index 230eca1..1755ec1 100644 --- a/src/repositories/shuttle/eta/RedisSelfUpdatingETARepository.ts +++ b/src/repositories/shuttle/eta/RedisSelfUpdatingETARepository.ts @@ -1,24 +1,17 @@ import { SelfUpdatingETARepository } from "./SelfUpdatingETARepository"; import { BaseRedisETARepository } from "./BaseRedisETARepository"; -import { createClient, RedisClientType } from "redis"; +import { RedisClientType } from "redis"; import { ShuttleGetterRepository, ShuttleRepositoryEvent, ShuttleStopArrival, ShuttleTravelTimeDataIdentifier, ShuttleTravelTimeDateFilterArguments, ShuttleWillArriveAtStopPayload, ShuttleWillLeaveStopPayload } from "../ShuttleGetterRepository"; -import { REDIS_RECONNECT_INTERVAL } from "../../../environment"; import { IEta, IOrderedStop, IShuttle } from "../../../entities/ShuttleRepositoryEntities"; import { ETARepositoryEvent } from "./ETAGetterRepository"; +import createRedisClientForRepository from "../../../helpers/createRedisClientForRepository"; export class RedisSelfUpdatingETARepository extends BaseRedisETARepository implements SelfUpdatingETARepository { private isListening = false; constructor( readonly shuttleRepository: ShuttleGetterRepository, - redisClient: RedisClientType = createClient({ - url: process.env.REDIS_URL, - socket: { - tls: process.env.NODE_ENV === 'production', - rejectUnauthorized: false, - reconnectStrategy: REDIS_RECONNECT_INTERVAL, - }, - }), + redisClient: RedisClientType = createRedisClientForRepository(), private referenceTime: Date | null = null, ) { super(redisClient); From a9db9b5d5c9334a7df83c704d2abf1cc93401954 Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Thu, 20 Nov 2025 16:47:50 -0800 Subject: [PATCH 19/33] Pass down the new arguments into the shuttle repositories --- src/entities/InterchangeSystem.ts | 12 ++++++++++-- .../shuttle/RedisShuttleRepository.ts | 19 +++++++++++++++---- .../UnoptimizedInMemoryShuttleRepository.ts | 10 +++++++++- 3 files changed, 34 insertions(+), 7 deletions(-) diff --git a/src/entities/InterchangeSystem.ts b/src/entities/InterchangeSystem.ts index b8cfbe1..398c619 100644 --- a/src/entities/InterchangeSystem.ts +++ b/src/entities/InterchangeSystem.ts @@ -23,6 +23,7 @@ import { RedisExternalSourceETARepository } from "../repositories/shuttle/eta/Re import { InMemorySelfUpdatingETARepository } from "../repositories/shuttle/eta/InMemorySelfUpdatingETARepository"; import { BaseRedisETARepository } from "../repositories/shuttle/eta/BaseRedisETARepository"; import { BaseInMemoryETARepository } from "../repositories/shuttle/eta/BaseInMemoryETARepository"; +import createRedisClientForRepository from "../helpers/createRedisClientForRepository"; export interface InterchangeSystemBuilderArguments { name: string; @@ -112,7 +113,11 @@ export class InterchangeSystem { } private static async buildRedisShuttleLoaderAndRepositories(args: InterchangeSystemBuilderArguments) { - const shuttleRepository = new RedisShuttleRepository(); + const shuttleRepository = new RedisShuttleRepository( + createRedisClientForRepository(), + args.shuttleStopArrivalDegreeDelta, + args.shuttleStopNearbyDegreeDelta, + ); await shuttleRepository.connect(); let etaRepository: BaseRedisETARepository; @@ -261,7 +266,10 @@ export class InterchangeSystem { } private static buildInMemoryShuttleLoaderAndRepositories(args: InterchangeSystemBuilderArguments) { - const shuttleRepository = new UnoptimizedInMemoryShuttleRepository(); + const shuttleRepository = new UnoptimizedInMemoryShuttleRepository( + args.shuttleStopArrivalDegreeDelta, + args.shuttleStopNearbyDegreeDelta, + ); let etaRepository: BaseInMemoryETARepository; let shuttleDataLoader: ApiBasedShuttleRepositoryLoader; diff --git a/src/repositories/shuttle/RedisShuttleRepository.ts b/src/repositories/shuttle/RedisShuttleRepository.ts index 3d8cf60..515f3cd 100644 --- a/src/repositories/shuttle/RedisShuttleRepository.ts +++ b/src/repositories/shuttle/RedisShuttleRepository.ts @@ -10,8 +10,18 @@ import { ShuttleTravelTimeDateFilterArguments } from "./ShuttleGetterRepository"; import { BaseRedisRepository } from "../BaseRedisRepository"; +import { RedisClientType } from "redis"; +import createRedisClientForRepository from "../../helpers/createRedisClientForRepository"; export class RedisShuttleRepository extends BaseRedisRepository implements ShuttleGetterSetterRepository { + constructor( + redisClient: RedisClientType = createRedisClientForRepository(), + readonly shuttleStopArrivalDegreeDelta: number = 0.001, + readonly shuttleStopNearbyDegreeDelta: number = 0.003, + ) { + super(redisClient); + } + get isReady() { return this.redisClient.isReady; } @@ -481,20 +491,21 @@ export class RedisShuttleRepository extends BaseRedisRepository implements Shutt * is on the shuttle's route. * * @param shuttle - * @param delta + * @param degreeDelta * @returns */ public async getArrivedStopIfExists( shuttle: IShuttle, - delta = 0.001, ): Promise { + const degreeDelta = this.shuttleStopArrivalDegreeDelta; + const lastStop = await this.getShuttleLastStopArrival(shuttle.id); if (lastStop) { const lastOrderedStop = await this.getOrderedStopByRouteAndStopId(shuttle.routeId, lastStop.stopId); const orderedStopAfter = lastOrderedStop?.nextStop; if (orderedStopAfter) { const stopAfter = await this.getStopById(orderedStopAfter.stopId); - if (stopAfter && shuttleHasArrivedAtStop(shuttle, stopAfter, delta)) { + if (stopAfter && shuttleHasArrivedAtStop(shuttle, stopAfter, degreeDelta)) { return stopAfter; } } @@ -503,7 +514,7 @@ export class RedisShuttleRepository extends BaseRedisRepository implements Shutt for (const orderedStop of orderedStops) { const stop = await this.getStopById(orderedStop.stopId); - if (stop != null && shuttleHasArrivedAtStop(shuttle, stop, delta)) { + if (stop != null && shuttleHasArrivedAtStop(shuttle, stop, degreeDelta)) { return stop; } } diff --git a/src/repositories/shuttle/UnoptimizedInMemoryShuttleRepository.ts b/src/repositories/shuttle/UnoptimizedInMemoryShuttleRepository.ts index ab34df6..8746bb0 100644 --- a/src/repositories/shuttle/UnoptimizedInMemoryShuttleRepository.ts +++ b/src/repositories/shuttle/UnoptimizedInMemoryShuttleRepository.ts @@ -20,6 +20,14 @@ import { export class UnoptimizedInMemoryShuttleRepository extends EventEmitter implements ShuttleGetterSetterRepository { + + constructor( + readonly shuttleStopArrivalDegreeDelta: number = 0.001, + readonly shuttleStopNearbyDegreeDelta: number = 0.003, + ) { + super() + } + public override on( event: T, listener: ShuttleRepositoryEventListener, @@ -252,8 +260,8 @@ export class UnoptimizedInMemoryShuttleRepository public async getArrivedStopIfExists( shuttle: IShuttle, - delta = 0.001, ): Promise { + const delta = this.shuttleStopArrivalDegreeDelta; const orderedStops = await this.getOrderedStopsByRouteId(shuttle.routeId); for (const orderedStop of orderedStops) { From 22322e5f0a3952339ac9cf01c5b9a3d5e9fe70c8 Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Thu, 20 Nov 2025 16:52:56 -0800 Subject: [PATCH 20/33] Add a separate tsconfig for excluding tests/mocks from builds, while leaving them for type checks --- package.json | 4 ++-- tsconfig.build.json | 15 +++++++++++++++ tsconfig.json | 2 +- 3 files changed, 18 insertions(+), 3 deletions(-) create mode 100644 tsconfig.build.json diff --git a/package.json b/package.json index f0a7798..bd558b4 100644 --- a/package.json +++ b/package.json @@ -4,8 +4,8 @@ "description": "", "main": "dist/index.js", "scripts": { - "build:dev": "npm install --include=dev && npm run generate && tsc", - "build": "npm install --include=dev && npm run generate && tsc && npm prune --omit=dev", + "build:dev": "npm install --include=dev && npm run generate && tsc --project tsconfig.build.json", + "build": "npm install --include=dev && npm run generate && tsc --project tsconfig.build.json && npm prune --omit=dev", "start:dev": "npm run build:dev && node ./dist/index.js", "start": "npm run build && node ./dist/index.js", "generate": "graphql-codegen --config codegen.ts", diff --git a/tsconfig.build.json b/tsconfig.build.json new file mode 100644 index 0000000..37ac446 --- /dev/null +++ b/tsconfig.build.json @@ -0,0 +1,15 @@ +// For builds, excludes tests and mocks +{ + "compilerOptions": { + "target": "es2016", + "module": "commonjs", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "skipLibCheck": true, + "outDir": "dist", + "sourceMap": true + }, + "include": ["src"], + "exclude": ["**/__tests__/*/**", "**/__mocks__/*/**"] +} diff --git a/tsconfig.json b/tsconfig.json index dfc1303..57e4d7f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,3 +1,4 @@ +// For type-checking, includes tests and mocks { "compilerOptions": { "target": "es2016", @@ -10,5 +11,4 @@ "sourceMap": true }, "include": ["src"], - "exclude": ["**/__tests__/*/**", "**/__mocks__/*/**"] } From ddab9b96ff58cfcd147e4040cf2251322223f907 Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Thu, 20 Nov 2025 17:47:13 -0800 Subject: [PATCH 21/33] Remove SHUTTLE_IS_NEAR_NEXT_STOP event and placeholder tests --- .../shuttle/ShuttleGetterRepository.ts | 2 -- .../ShuttleRepositorySharedTests.test.ts | 18 ------------------ 2 files changed, 20 deletions(-) diff --git a/src/repositories/shuttle/ShuttleGetterRepository.ts b/src/repositories/shuttle/ShuttleGetterRepository.ts index 78715a3..1518c93 100644 --- a/src/repositories/shuttle/ShuttleGetterRepository.ts +++ b/src/repositories/shuttle/ShuttleGetterRepository.ts @@ -4,7 +4,6 @@ import type EventEmitter from "node:events"; export const ShuttleRepositoryEvent = { SHUTTLE_UPDATED: "shuttleUpdated", SHUTTLE_REMOVED: "shuttleRemoved", - SHUTTLE_IS_NEAR_NEXT_STOP: "shuttleIsNearNextStop", SHUTTLE_WILL_ARRIVE_AT_STOP: "shuttleArrivedAtStop", SHUTTLE_WILL_LEAVE_STOP: "shuttleWillLeaveStop", } as const; @@ -28,7 +27,6 @@ export interface ShuttleWillLeaveStopPayload { export interface ShuttleRepositoryEventPayloads { [ShuttleRepositoryEvent.SHUTTLE_UPDATED]: IShuttle, [ShuttleRepositoryEvent.SHUTTLE_REMOVED]: IShuttle, - [ShuttleRepositoryEvent.SHUTTLE_IS_NEAR_NEXT_STOP]: ShuttleIsNearStopPayload, [ShuttleRepositoryEvent.SHUTTLE_WILL_ARRIVE_AT_STOP]: ShuttleWillArriveAtStopPayload, [ShuttleRepositoryEvent.SHUTTLE_WILL_LEAVE_STOP]: ShuttleWillLeaveStopPayload, } diff --git a/src/repositories/shuttle/__tests__/ShuttleRepositorySharedTests.test.ts b/src/repositories/shuttle/__tests__/ShuttleRepositorySharedTests.test.ts index 96afb73..e8e4aa2 100644 --- a/src/repositories/shuttle/__tests__/ShuttleRepositorySharedTests.test.ts +++ b/src/repositories/shuttle/__tests__/ShuttleRepositorySharedTests.test.ts @@ -701,24 +701,6 @@ describe.each(repositoryImplementations)('$name', (holder) => { }); }); - describe("SHUTTLE_IS_NEAR_NEXT_STOP event", () => { - test("emits SHUTTLE_IS_NEAR_NEXT_STOP when shuttle is within a set proximity of the next stop", async () => { - throw new Error("Not implemented"); - }); - - test("does not emit event when shuttle is near a stop, but it's not the shuttle's next stop", async () => { - throw new Error("Not implemented"); - }); - - test("only emits the event once", async () => { - throw new Error("Not implemented"); - }); - - test("does not emit event when shuttle is not near a stop", async () => { - throw new Error("Not implemented"); - }); - }); - describe("SHUTTLE_WILL_ARRIVE_AT_STOP event", () => { test("emits SHUTTLE_WILL_ARRIVE_AT_STOP event before shuttle arrives at a stop", async () => { const { stop1, sampleShuttleNotInRepository: shuttle } = await setupRouteAndOrderedStops(); From 37fbc3ef4596bc2edc345e1d000a5bfc9f7f96bb Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Thu, 20 Nov 2025 17:48:49 -0800 Subject: [PATCH 22/33] Make checkIfShuttleIsAtStop part of the ShuttleGetterRepository interface --- src/repositories/shuttle/RedisShuttleRepository.ts | 2 +- src/repositories/shuttle/ShuttleGetterRepository.ts | 8 ++++++++ .../shuttle/UnoptimizedInMemoryShuttleRepository.ts | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/repositories/shuttle/RedisShuttleRepository.ts b/src/repositories/shuttle/RedisShuttleRepository.ts index 515f3cd..9098f56 100644 --- a/src/repositories/shuttle/RedisShuttleRepository.ts +++ b/src/repositories/shuttle/RedisShuttleRepository.ts @@ -447,7 +447,7 @@ export class RedisShuttleRepository extends BaseRedisRepository implements Shutt await this.redisClient.sRem(this.shuttleIsAtStopKey, shuttleId); } - private async checkIfShuttleIsAtStop(shuttleId: string) { + public async checkIfShuttleIsAtStop(shuttleId: string) { return await this.redisClient.sIsMember(this.shuttleIsAtStopKey, shuttleId); } diff --git a/src/repositories/shuttle/ShuttleGetterRepository.ts b/src/repositories/shuttle/ShuttleGetterRepository.ts index 1518c93..6011a43 100644 --- a/src/repositories/shuttle/ShuttleGetterRepository.ts +++ b/src/repositories/shuttle/ShuttleGetterRepository.ts @@ -95,6 +95,14 @@ export interface ShuttleGetterRepository extends EventEmitter { */ getShuttleLastStopArrival(shuttleId: string): Promise; + /** + * Determine if the shuttle is currently at a stop. + * If `true`, then calling `getShuttleLastStopArrival` will get + * the stop the shuttle is currently at. + * @param shuttleId + */ + checkIfShuttleIsAtStop(shuttleId: string): Promise; + /** * Check if a shuttle has arrived at a stop within the given delta. * Returns the stop if the shuttle is at a stop, otherwise undefined. diff --git a/src/repositories/shuttle/UnoptimizedInMemoryShuttleRepository.ts b/src/repositories/shuttle/UnoptimizedInMemoryShuttleRepository.ts index 8746bb0..ec30b84 100644 --- a/src/repositories/shuttle/UnoptimizedInMemoryShuttleRepository.ts +++ b/src/repositories/shuttle/UnoptimizedInMemoryShuttleRepository.ts @@ -223,7 +223,7 @@ export class UnoptimizedInMemoryShuttleRepository this.shuttlesAtStop.delete(shuttleId); } - private async checkIfShuttleIsAtStop(shuttleId: string) { + public async checkIfShuttleIsAtStop(shuttleId: string) { return this.shuttlesAtStop.has(shuttleId); } From f34f78aaea7c08a78e4875f65afdd6d1e153e656 Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Thu, 20 Nov 2025 19:07:30 -0800 Subject: [PATCH 23/33] Remove the delta argument in ShuttleGetterRepository.getArrivedStopIfExists --- src/repositories/shuttle/ShuttleGetterRepository.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/repositories/shuttle/ShuttleGetterRepository.ts b/src/repositories/shuttle/ShuttleGetterRepository.ts index 6011a43..2678c2c 100644 --- a/src/repositories/shuttle/ShuttleGetterRepository.ts +++ b/src/repositories/shuttle/ShuttleGetterRepository.ts @@ -109,5 +109,5 @@ export interface ShuttleGetterRepository extends EventEmitter { * @param shuttle * @param delta - The coordinate delta tolerance (default 0.001) */ - getArrivedStopIfExists(shuttle: IShuttle, delta?: number): Promise; + getArrivedStopIfExists(shuttle: IShuttle): Promise; } From c2f1a67a7075ea488cc68dd2bc5fee23f1d16c44 Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Fri, 21 Nov 2025 10:39:08 -0800 Subject: [PATCH 24/33] Remove unused ETA code from RedisShuttleRepository --- .../shuttle/RedisShuttleRepository.ts | 40 ------------------- 1 file changed, 40 deletions(-) diff --git a/src/repositories/shuttle/RedisShuttleRepository.ts b/src/repositories/shuttle/RedisShuttleRepository.ts index 9098f56..ab41196 100644 --- a/src/repositories/shuttle/RedisShuttleRepository.ts +++ b/src/repositories/shuttle/RedisShuttleRepository.ts @@ -87,7 +87,6 @@ export class RedisShuttleRepository extends BaseRedisRepository implements Shutt private createStopKey = (stopId: string) => `shuttle:stop:${stopId}`; private createRouteKey = (routeId: string) => `shuttle:route:${routeId}`; private createShuttleKey = (shuttleId: string) => `shuttle:shuttle:${shuttleId}`; - private createEtaKey = (shuttleId: string, stopId: string) => `shuttle:eta:${shuttleId}:${stopId}`; private createOrderedStopKey = (routeId: string, stopId: string) => `shuttle:orderedstop:${routeId}:${stopId}`; private createShuttleLastStopKey = (shuttleId: string) => `shuttle:laststop:${shuttleId}`; private createHistoricalEtaTimeSeriesKey = (routeId: string, fromStopId: string, toStopId: string) => { @@ -308,45 +307,6 @@ export class RedisShuttleRepository extends BaseRedisRepository implements Shutt return this.createShuttleFromRedisData(data); } - public async getEtasForShuttleId(shuttleId: string): Promise { - const keys = await this.redisClient.keys(`shuttle:eta:${shuttleId}:*`); - const etas: IEta[] = []; - - for (const key of keys) { - const data = await this.redisClient.hGetAll(key); - if (Object.keys(data).length > 0) { - etas.push(this.createEtaFromRedisData(data)); - } - } - - return etas; - } - - public async getEtasForStopId(stopId: string): Promise { - const keys = await this.redisClient.keys('shuttle:eta:*'); - const etas: IEta[] = []; - - for (const key of keys) { - const data = await this.redisClient.hGetAll(key); - if (Object.keys(data).length > 0 && data.stopId === stopId) { - etas.push(this.createEtaFromRedisData(data)); - } - } - - return etas; - } - - public async getEtaForShuttleAndStopId(shuttleId: string, stopId: string): Promise { - const key = this.createEtaKey(shuttleId, stopId); - const data = await this.redisClient.hGetAll(key); - - if (Object.keys(data).length === 0) { - return null; - } - - return this.createEtaFromRedisData(data); - } - public async getOrderedStopByRouteAndStopId(routeId: string, stopId: string): Promise { const key = this.createOrderedStopKey(routeId, stopId); const data = await this.redisClient.hGetAll(key); From 712e31100632973bd0c5d7647ae5e29c03f15305 Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Fri, 21 Nov 2025 10:51:38 -0800 Subject: [PATCH 25/33] Update getArrivedStopIfExists method to take an argument returnNextStopOnly --- .../shuttle/RedisShuttleRepository.ts | 14 ++-------- .../shuttle/ShuttleGetterRepository.ts | 15 ++++++----- .../UnoptimizedInMemoryShuttleRepository.ts | 27 ++++++++++++++----- .../ShuttleRepositorySharedTests.test.ts | 26 ++++++++++++++++-- 4 files changed, 56 insertions(+), 26 deletions(-) diff --git a/src/repositories/shuttle/RedisShuttleRepository.ts b/src/repositories/shuttle/RedisShuttleRepository.ts index ab41196..6c0ccdc 100644 --- a/src/repositories/shuttle/RedisShuttleRepository.ts +++ b/src/repositories/shuttle/RedisShuttleRepository.ts @@ -443,24 +443,14 @@ export class RedisShuttleRepository extends BaseRedisRepository implements Shutt } } - /** - * Get the stop that the shuttle is currently at, if it exists. - * - * If the shuttle has a "last stop", it will only return the stop - * directly after the last stop. Otherwise, it may return any stop that - * is on the shuttle's route. - * - * @param shuttle - * @param degreeDelta - * @returns - */ public async getArrivedStopIfExists( shuttle: IShuttle, + returnNextStopOnly: boolean = false, ): Promise { const degreeDelta = this.shuttleStopArrivalDegreeDelta; const lastStop = await this.getShuttleLastStopArrival(shuttle.id); - if (lastStop) { + if (lastStop && returnNextStopOnly) { const lastOrderedStop = await this.getOrderedStopByRouteAndStopId(shuttle.routeId, lastStop.stopId); const orderedStopAfter = lastOrderedStop?.nextStop; if (orderedStopAfter) { diff --git a/src/repositories/shuttle/ShuttleGetterRepository.ts b/src/repositories/shuttle/ShuttleGetterRepository.ts index 2678c2c..5b7aea8 100644 --- a/src/repositories/shuttle/ShuttleGetterRepository.ts +++ b/src/repositories/shuttle/ShuttleGetterRepository.ts @@ -104,10 +104,13 @@ export interface ShuttleGetterRepository extends EventEmitter { checkIfShuttleIsAtStop(shuttleId: string): Promise; /** - * Check if a shuttle has arrived at a stop within the given delta. - * Returns the stop if the shuttle is at a stop, otherwise undefined. - * @param shuttle - * @param delta - The coordinate delta tolerance (default 0.001) - */ - getArrivedStopIfExists(shuttle: IShuttle): Promise; + * Get the stop that the shuttle is currently at, if it exists. + * + * @param shuttle + * @param returnNextStopOnly If set to true, and the shuttle has a "last stop", + * only return the stop directly after the last stop. + * Otherwise, it may return any stop that is on the shuttle's route. + * @returns + */ + getArrivedStopIfExists(shuttle: IShuttle, returnNextStopOnly: boolean): Promise; } diff --git a/src/repositories/shuttle/UnoptimizedInMemoryShuttleRepository.ts b/src/repositories/shuttle/UnoptimizedInMemoryShuttleRepository.ts index ec30b84..f6fbafa 100644 --- a/src/repositories/shuttle/UnoptimizedInMemoryShuttleRepository.ts +++ b/src/repositories/shuttle/UnoptimizedInMemoryShuttleRepository.ts @@ -260,16 +260,31 @@ export class UnoptimizedInMemoryShuttleRepository public async getArrivedStopIfExists( shuttle: IShuttle, + returnNextStopOnly: boolean = false, ): Promise { - const delta = this.shuttleStopArrivalDegreeDelta; - const orderedStops = await this.getOrderedStopsByRouteId(shuttle.routeId); + const degreeDelta = this.shuttleStopArrivalDegreeDelta; - for (const orderedStop of orderedStops) { - const stop = await this.getStopById(orderedStop.stopId); - if (stop != null && shuttleHasArrivedAtStop(shuttle, stop, delta)) { - return stop; + const lastStop = await this.getShuttleLastStopArrival(shuttle.id); + if (lastStop && returnNextStopOnly) { + const lastOrderedStop = await this.getOrderedStopByRouteAndStopId(shuttle.routeId, lastStop.stopId); + const orderedStopAfter = lastOrderedStop?.nextStop; + if (orderedStopAfter) { + const stopAfter = await this.getStopById(orderedStopAfter.stopId); + if (stopAfter && shuttleHasArrivedAtStop(shuttle, stopAfter, degreeDelta)) { + return stopAfter; + } + } + } else { + const orderedStops = await this.getOrderedStopsByRouteId(shuttle.routeId); + + for (const orderedStop of orderedStops) { + const stop = await this.getStopById(orderedStop.stopId); + if (stop != null && shuttleHasArrivedAtStop(shuttle, stop, degreeDelta)) { + return stop; + } } } + return undefined; } diff --git a/src/repositories/shuttle/__tests__/ShuttleRepositorySharedTests.test.ts b/src/repositories/shuttle/__tests__/ShuttleRepositorySharedTests.test.ts index e8e4aa2..55c6bb4 100644 --- a/src/repositories/shuttle/__tests__/ShuttleRepositorySharedTests.test.ts +++ b/src/repositories/shuttle/__tests__/ShuttleRepositorySharedTests.test.ts @@ -577,7 +577,7 @@ describe.each(repositoryImplementations)('$name', (holder) => { test("gets the stop that the shuttle is currently at, if exists", async () => { const { sampleShuttleNotInRepository: shuttle } = await setupRouteAndOrderedStops(); - const result = await repository.getArrivedStopIfExists(shuttle); + const result = await repository.getArrivedStopIfExists(shuttle, false); expect(result).toBeDefined(); expect(result?.id).toBe("st2"); @@ -588,10 +588,32 @@ describe.each(repositoryImplementations)('$name', (holder) => { const { sampleShuttleNotInRepository } = await setupRouteAndOrderedStops(); const shuttle = { ...sampleShuttleNotInRepository, coordinates: { latitude: 12.5, longitude: 22.5 } }; // Not at any stop - const result = await repository.getArrivedStopIfExists(shuttle); + const result = await repository.getArrivedStopIfExists(shuttle, false); expect(result).toBeUndefined(); }); + + test("only gets the shuttle's next stop if parameter passed and shuttle has arrived at stop", async () => { + const { sampleShuttleNotInRepository: shuttle, stop1, stop2 } = await setupRouteAndOrderedStops(); + + shuttle.coordinates = stop1.coordinates; + await repository.addOrUpdateShuttle(shuttle); + + let result = await repository.getArrivedStopIfExists(shuttle, true); + expect(result).toBeUndefined(); + + shuttle.coordinates = stop2.coordinates; + result = await repository.getArrivedStopIfExists(shuttle, true); + expect(result).not.toBeUndefined(); + }); + + test("still gets any stop for the shuttle if the shuttle has no last stop", async () => { + const { sampleShuttleNotInRepository: shuttle, stop1 } = await setupRouteAndOrderedStops(); + + shuttle.coordinates = stop1.coordinates; + const result = await repository.getArrivedStopIfExists(shuttle, true); + expect(result).not.toBeUndefined(); + }); }); describe("getShuttleLastStopArrival", () => { From 8c341e91e0275c2817f4cf94de16ffb43e46a946 Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Fri, 21 Nov 2025 10:56:48 -0800 Subject: [PATCH 26/33] Call getArrivedStopIfExists based on different parameter --- src/repositories/shuttle/RedisShuttleRepository.ts | 11 ++++++++++- .../shuttle/UnoptimizedInMemoryShuttleRepository.ts | 12 +++++++++++- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/src/repositories/shuttle/RedisShuttleRepository.ts b/src/repositories/shuttle/RedisShuttleRepository.ts index 6c0ccdc..1b4d952 100644 --- a/src/repositories/shuttle/RedisShuttleRepository.ts +++ b/src/repositories/shuttle/RedisShuttleRepository.ts @@ -369,7 +369,16 @@ export class RedisShuttleRepository extends BaseRedisRepository implements Shutt travelTimeTimestamp = Date.now(), ) { const isAtStop = await this.checkIfShuttleIsAtStop(shuttle.id); - const arrivedStop = await this.getArrivedStopIfExists(shuttle); + + let arrivedStop: IStop | undefined; + + if (isAtStop) { + // Allow retrieval of the same stop + // Will still return undefined when the shuttle leaves the stop + arrivedStop = await this.getArrivedStopIfExists(shuttle, false); + } else { + arrivedStop = await this.getArrivedStopIfExists(shuttle, true); + } // Will not fire *any* events if the same stop const lastStop = await this.getShuttleLastStopArrival(shuttle.id); diff --git a/src/repositories/shuttle/UnoptimizedInMemoryShuttleRepository.ts b/src/repositories/shuttle/UnoptimizedInMemoryShuttleRepository.ts index f6fbafa..5239099 100644 --- a/src/repositories/shuttle/UnoptimizedInMemoryShuttleRepository.ts +++ b/src/repositories/shuttle/UnoptimizedInMemoryShuttleRepository.ts @@ -184,7 +184,17 @@ export class UnoptimizedInMemoryShuttleRepository travelTimeTimestamp = Date.now(), ) { const isAtStop = await this.checkIfShuttleIsAtStop(shuttle.id); - const arrivedStop = await this.getArrivedStopIfExists(shuttle); + + let arrivedStop: IStop | undefined; + + if (isAtStop) { + // Allow retrieval of the same stop + // Will still return undefined when the shuttle leaves the stop + arrivedStop = await this.getArrivedStopIfExists(shuttle, false); + } else { + arrivedStop = await this.getArrivedStopIfExists(shuttle, true); + } + // Will not fire *any* events if the same stop const lastStop = await this.getShuttleLastStopArrival(shuttle.id); From 413443a162782f2ec7d5a80554634c9200d0264c Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Fri, 21 Nov 2025 11:07:47 -0800 Subject: [PATCH 27/33] Update the method to accept a canReturnShuttleCurrentStop flag instead --- .../shuttle/RedisShuttleRepository.ts | 22 +++++++++++++------ .../shuttle/ShuttleGetterRepository.ts | 9 ++++---- .../UnoptimizedInMemoryShuttleRepository.ts | 22 +++++++++++++------ .../ShuttleRepositorySharedTests.test.ts | 15 ++++++++----- 4 files changed, 44 insertions(+), 24 deletions(-) diff --git a/src/repositories/shuttle/RedisShuttleRepository.ts b/src/repositories/shuttle/RedisShuttleRepository.ts index 1b4d952..d4669bc 100644 --- a/src/repositories/shuttle/RedisShuttleRepository.ts +++ b/src/repositories/shuttle/RedisShuttleRepository.ts @@ -373,11 +373,11 @@ export class RedisShuttleRepository extends BaseRedisRepository implements Shutt let arrivedStop: IStop | undefined; if (isAtStop) { - // Allow retrieval of the same stop + // Allow retrieval of the shuttle's current stop // Will still return undefined when the shuttle leaves the stop - arrivedStop = await this.getArrivedStopIfExists(shuttle, false); - } else { arrivedStop = await this.getArrivedStopIfExists(shuttle, true); + } else { + arrivedStop = await this.getArrivedStopIfExists(shuttle, false); } // Will not fire *any* events if the same stop @@ -454,13 +454,21 @@ export class RedisShuttleRepository extends BaseRedisRepository implements Shutt public async getArrivedStopIfExists( shuttle: IShuttle, - returnNextStopOnly: boolean = false, + canReturnShuttleCurrentStop: boolean = false, ): Promise { const degreeDelta = this.shuttleStopArrivalDegreeDelta; - const lastStop = await this.getShuttleLastStopArrival(shuttle.id); - if (lastStop && returnNextStopOnly) { - const lastOrderedStop = await this.getOrderedStopByRouteAndStopId(shuttle.routeId, lastStop.stopId); + const lastStopArrival = await this.getShuttleLastStopArrival(shuttle.id); + if (lastStopArrival) { + // Return the shuttle's current stop depending on the flag + if (canReturnShuttleCurrentStop) { + const lastStop = await this.getStopById(lastStopArrival.stopId); + if (lastStop && shuttleHasArrivedAtStop(shuttle, lastStop, degreeDelta)) { + return lastStop; + } + } + + const lastOrderedStop = await this.getOrderedStopByRouteAndStopId(shuttle.routeId, lastStopArrival.stopId); const orderedStopAfter = lastOrderedStop?.nextStop; if (orderedStopAfter) { const stopAfter = await this.getStopById(orderedStopAfter.stopId); diff --git a/src/repositories/shuttle/ShuttleGetterRepository.ts b/src/repositories/shuttle/ShuttleGetterRepository.ts index 5b7aea8..44815ec 100644 --- a/src/repositories/shuttle/ShuttleGetterRepository.ts +++ b/src/repositories/shuttle/ShuttleGetterRepository.ts @@ -107,10 +107,11 @@ export interface ShuttleGetterRepository extends EventEmitter { * Get the stop that the shuttle is currently at, if it exists. * * @param shuttle - * @param returnNextStopOnly If set to true, and the shuttle has a "last stop", - * only return the stop directly after the last stop. - * Otherwise, it may return any stop that is on the shuttle's route. + * @param canReturnShuttleCurrentStop If set to true, and the shuttle's "last stop" + * matches the arrived stop, continue to return the arrived stop. + * Otherwise, only return the shuttle's next stop. + * This flag has no effect if the shuttle has not had a "last stop". * @returns */ - getArrivedStopIfExists(shuttle: IShuttle, returnNextStopOnly: boolean): Promise; + getArrivedStopIfExists(shuttle: IShuttle, canReturnShuttleCurrentStop: boolean): Promise; } diff --git a/src/repositories/shuttle/UnoptimizedInMemoryShuttleRepository.ts b/src/repositories/shuttle/UnoptimizedInMemoryShuttleRepository.ts index 5239099..38c7c02 100644 --- a/src/repositories/shuttle/UnoptimizedInMemoryShuttleRepository.ts +++ b/src/repositories/shuttle/UnoptimizedInMemoryShuttleRepository.ts @@ -188,11 +188,11 @@ export class UnoptimizedInMemoryShuttleRepository let arrivedStop: IStop | undefined; if (isAtStop) { - // Allow retrieval of the same stop + // Allow retrieval of the shuttle's current stop // Will still return undefined when the shuttle leaves the stop - arrivedStop = await this.getArrivedStopIfExists(shuttle, false); - } else { arrivedStop = await this.getArrivedStopIfExists(shuttle, true); + } else { + arrivedStop = await this.getArrivedStopIfExists(shuttle, false); } @@ -270,13 +270,21 @@ export class UnoptimizedInMemoryShuttleRepository public async getArrivedStopIfExists( shuttle: IShuttle, - returnNextStopOnly: boolean = false, + canReturnShuttleCurrentStop: boolean = false, ): Promise { const degreeDelta = this.shuttleStopArrivalDegreeDelta; - const lastStop = await this.getShuttleLastStopArrival(shuttle.id); - if (lastStop && returnNextStopOnly) { - const lastOrderedStop = await this.getOrderedStopByRouteAndStopId(shuttle.routeId, lastStop.stopId); + const lastStopArrival = await this.getShuttleLastStopArrival(shuttle.id); + if (lastStopArrival) { + // Return the shuttle's current stop depending on the flag + if (canReturnShuttleCurrentStop) { + const lastStop = await this.getStopById(lastStopArrival.stopId); + if (lastStop && shuttleHasArrivedAtStop(shuttle, lastStop, degreeDelta)) { + return lastStop; + } + } + + const lastOrderedStop = await this.getOrderedStopByRouteAndStopId(shuttle.routeId, lastStopArrival.stopId); const orderedStopAfter = lastOrderedStop?.nextStop; if (orderedStopAfter) { const stopAfter = await this.getStopById(orderedStopAfter.stopId); diff --git a/src/repositories/shuttle/__tests__/ShuttleRepositorySharedTests.test.ts b/src/repositories/shuttle/__tests__/ShuttleRepositorySharedTests.test.ts index 55c6bb4..08deaa2 100644 --- a/src/repositories/shuttle/__tests__/ShuttleRepositorySharedTests.test.ts +++ b/src/repositories/shuttle/__tests__/ShuttleRepositorySharedTests.test.ts @@ -574,7 +574,7 @@ describe.each(repositoryImplementations)('$name', (holder) => { }); describe("getArrivedStopIfExists", () => { - test("gets the stop that the shuttle is currently at, if exists", async () => { + test("gets any stop that the shuttle is currently at, if the shuttle has not had a last stop", async () => { const { sampleShuttleNotInRepository: shuttle } = await setupRouteAndOrderedStops(); const result = await repository.getArrivedStopIfExists(shuttle, false); @@ -593,27 +593,30 @@ describe.each(repositoryImplementations)('$name', (holder) => { expect(result).toBeUndefined(); }); - test("only gets the shuttle's next stop if parameter passed and shuttle has arrived at stop", async () => { + test("only gets the shuttle's next stop if shuttle has previously arrived at a stop", async () => { const { sampleShuttleNotInRepository: shuttle, stop1, stop2 } = await setupRouteAndOrderedStops(); shuttle.coordinates = stop1.coordinates; await repository.addOrUpdateShuttle(shuttle); - let result = await repository.getArrivedStopIfExists(shuttle, true); + let result = await repository.getArrivedStopIfExists(shuttle, false); expect(result).toBeUndefined(); shuttle.coordinates = stop2.coordinates; - result = await repository.getArrivedStopIfExists(shuttle, true); + result = await repository.getArrivedStopIfExists(shuttle, false); expect(result).not.toBeUndefined(); }); - test("still gets any stop for the shuttle if the shuttle has no last stop", async () => { + test("returns the shuttle's currently arrived stop if flag passed", async () => { const { sampleShuttleNotInRepository: shuttle, stop1 } = await setupRouteAndOrderedStops(); shuttle.coordinates = stop1.coordinates; + await repository.addOrUpdateShuttle(shuttle); + const result = await repository.getArrivedStopIfExists(shuttle, true); - expect(result).not.toBeUndefined(); + expect(result?.id === stop1.id) }); + }); describe("getShuttleLastStopArrival", () => { From 6c199e38a89c6846991b410926dc2a73c6cbde33 Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Fri, 21 Nov 2025 11:10:10 -0800 Subject: [PATCH 28/33] Test previously failing case for SHUTTLE_WILL_LEAVE_STOP --- .../shuttle/__tests__/ShuttleRepositorySharedTests.test.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/repositories/shuttle/__tests__/ShuttleRepositorySharedTests.test.ts b/src/repositories/shuttle/__tests__/ShuttleRepositorySharedTests.test.ts index 08deaa2..59745e7 100644 --- a/src/repositories/shuttle/__tests__/ShuttleRepositorySharedTests.test.ts +++ b/src/repositories/shuttle/__tests__/ShuttleRepositorySharedTests.test.ts @@ -804,6 +804,10 @@ describe.each(repositoryImplementations)('$name', (holder) => { // Simulate arrival at stop 1 const arrivalTime = new Date("2024-01-15T10:30:00Z"); await repository.addOrUpdateShuttle(shuttle, arrivalTime.getTime()); + + // Test that it actually emits the event correctly and not right after the shuttle arrives + await repository.addOrUpdateShuttle(shuttle, arrivalTime.getTime()); + expect(listener).not.toHaveBeenCalled(); shuttle.coordinates = { latitude: 12.5, longitude: 22.5 }; // Not at any stop From 715bef163c4d7d5277d936ae20729c717c9747d4 Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Fri, 21 Nov 2025 11:33:22 -0800 Subject: [PATCH 29/33] Add stopgap ETA of 1 second when the shuttle has arrived at a stop --- .../eta/InMemorySelfUpdatingETARepository.ts | 16 +++++++++ .../eta/RedisSelfUpdatingETARepository.ts | 16 +++++++++ ...lfUpdatingETARepositorySharedTests.test.ts | 35 +++++++++++++++++++ 3 files changed, 67 insertions(+) diff --git a/src/repositories/shuttle/eta/InMemorySelfUpdatingETARepository.ts b/src/repositories/shuttle/eta/InMemorySelfUpdatingETARepository.ts index 164b734..ef04186 100644 --- a/src/repositories/shuttle/eta/InMemorySelfUpdatingETARepository.ts +++ b/src/repositories/shuttle/eta/InMemorySelfUpdatingETARepository.ts @@ -90,9 +90,25 @@ export class InMemorySelfUpdatingETARepository extends BaseInMemoryETARepository } private async handleShuttleUpdate(shuttle: IShuttle): Promise { + const isAtStop = await this.shuttleRepository.checkIfShuttleIsAtStop(shuttle.id); const lastStop = await this.shuttleRepository.getShuttleLastStopArrival(shuttle.id); if (!lastStop) return; + if (isAtStop) { + // Update the ETA *to* the stop the shuttle is currently at, + // before starting from the current stop as normal. + // Account for cases where the shuttle arrived way earlier than + // expected based on the calculated ETA. + + await this.addOrUpdateEta({ + secondsRemaining: 1, + shuttleId: shuttle.id, + stopId: lastStop.stopId, + systemId: shuttle.systemId, + updatedTime: new Date(), + }); + } + const lastOrderedStop = await this.shuttleRepository.getOrderedStopByRouteAndStopId(shuttle.routeId, lastStop.stopId); await this.updateCascadingEta({ diff --git a/src/repositories/shuttle/eta/RedisSelfUpdatingETARepository.ts b/src/repositories/shuttle/eta/RedisSelfUpdatingETARepository.ts index 1755ec1..cabf5a3 100644 --- a/src/repositories/shuttle/eta/RedisSelfUpdatingETARepository.ts +++ b/src/repositories/shuttle/eta/RedisSelfUpdatingETARepository.ts @@ -102,9 +102,25 @@ export class RedisSelfUpdatingETARepository extends BaseRedisETARepository imple } private async handleShuttleUpdate(shuttle: IShuttle) { + const isAtStop = await this.shuttleRepository.checkIfShuttleIsAtStop(shuttle.id); const lastStop = await this.shuttleRepository.getShuttleLastStopArrival(shuttle.id); if (!lastStop) return; + if (isAtStop) { + // Update the ETA *to* the stop the shuttle is currently at, + // before starting from the current stop as normal. + // Account for cases where the shuttle arrived way earlier than + // expected based on the calculated ETA. + + await this.addOrUpdateEta({ + secondsRemaining: 1, + shuttleId: shuttle.id, + stopId: lastStop.stopId, + systemId: shuttle.systemId, + updatedTime: new Date(), + }); + } + const lastOrderedStop = await this.shuttleRepository.getOrderedStopByRouteAndStopId(shuttle.routeId, lastStop.stopId); await this.updateCascadingEta({ diff --git a/src/repositories/shuttle/eta/__tests__/SelfUpdatingETARepositorySharedTests.test.ts b/src/repositories/shuttle/eta/__tests__/SelfUpdatingETARepositorySharedTests.test.ts index 86ae52b..6d5d9b1 100644 --- a/src/repositories/shuttle/eta/__tests__/SelfUpdatingETARepositorySharedTests.test.ts +++ b/src/repositories/shuttle/eta/__tests__/SelfUpdatingETARepositorySharedTests.test.ts @@ -194,6 +194,41 @@ describe.each(repositoryImplementations)('$name', (holder) => { currentTime, shuttleSecondArrivalTimeAtFirstStop ); }); + + test("adds a 'stopgap' entry of 1 second when the shuttle arrives at a stop", async () => { + const { stop1, stop2, stop3, sampleShuttleNotInRepository: shuttle } = await setupRouteAndOrderedStops(); + + const shuttleSecondArrivalTimeAtFirstStop = new Date(2025, 0, 1, 12, 5, 0); + const currentTime = new Date(shuttleSecondArrivalTimeAtFirstStop.getTime() + 7 * 60 * 1000); + + // Populating travel time data + await populateTravelTimeDataForStops({ currentTime, shuttle, stop1, stop2, stop3 }); + + // Populate ETA data + // Simulate shuttle running early for second stop + shuttle.coordinates = stop1.coordinates; + await shuttleRepository.addOrUpdateShuttle( + shuttle, + shuttleSecondArrivalTimeAtFirstStop.getTime() + ); + + shuttle.coordinates = stop2.coordinates; + // Call twice to get the ETA repository to read the correct flag + await shuttleRepository.addOrUpdateShuttle( + shuttle, + currentTime.getTime(), + ); + await new Promise((resolve) => setTimeout(resolve, 500)); + + await shuttleRepository.addOrUpdateShuttle( + shuttle, + currentTime.getTime(), // ~8 minutes early + ); + await new Promise((resolve) => setTimeout(resolve, 500)); + + const eta = await repository.getEtaForShuttleAndStopId(shuttle.id, stop2.id); + expect(eta?.secondsRemaining).toEqual(1); + }); }); describe("handleShuttleWillLeaveStop", () => { From ee10daf957d19af24b8ac5e6477491b8339ddbe2 Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Fri, 21 Nov 2025 11:42:42 -0800 Subject: [PATCH 30/33] Rename getArrivedStopIfExists to getArrivedStopIfNextStop, and add more documentation --- .../shuttle/RedisShuttleRepository.ts | 20 +++++++++---------- .../shuttle/ShuttleGetterRepository.ts | 6 ++++-- .../UnoptimizedInMemoryShuttleRepository.ts | 20 +++++++++---------- .../ShuttleRepositorySharedTests.test.ts | 18 ++++++++--------- 4 files changed, 33 insertions(+), 31 deletions(-) diff --git a/src/repositories/shuttle/RedisShuttleRepository.ts b/src/repositories/shuttle/RedisShuttleRepository.ts index d4669bc..86af512 100644 --- a/src/repositories/shuttle/RedisShuttleRepository.ts +++ b/src/repositories/shuttle/RedisShuttleRepository.ts @@ -1,13 +1,13 @@ import { ShuttleGetterSetterRepository } from "./ShuttleGetterSetterRepository"; import { IEta, IOrderedStop, IRoute, IShuttle, IStop, shuttleHasArrivedAtStop } from "../../entities/ShuttleRepositoryEntities"; import { - ShuttleRepositoryEvent, - ShuttleRepositoryEventListener, - ShuttleRepositoryEventName, - ShuttleRepositoryEventPayloads, - ShuttleStopArrival, - ShuttleTravelTimeDataIdentifier, - ShuttleTravelTimeDateFilterArguments + ShuttleRepositoryEvent, + ShuttleRepositoryEventListener, + ShuttleRepositoryEventName, + ShuttleRepositoryEventPayloads, + ShuttleStopArrival, + ShuttleTravelTimeDataIdentifier, + ShuttleTravelTimeDateFilterArguments } from "./ShuttleGetterRepository"; import { BaseRedisRepository } from "../BaseRedisRepository"; import { RedisClientType } from "redis"; @@ -375,9 +375,9 @@ export class RedisShuttleRepository extends BaseRedisRepository implements Shutt if (isAtStop) { // Allow retrieval of the shuttle's current stop // Will still return undefined when the shuttle leaves the stop - arrivedStop = await this.getArrivedStopIfExists(shuttle, true); + arrivedStop = await this.getArrivedStopIfNextStop(shuttle, true); } else { - arrivedStop = await this.getArrivedStopIfExists(shuttle, false); + arrivedStop = await this.getArrivedStopIfNextStop(shuttle, false); } // Will not fire *any* events if the same stop @@ -452,7 +452,7 @@ export class RedisShuttleRepository extends BaseRedisRepository implements Shutt } } - public async getArrivedStopIfExists( + public async getArrivedStopIfNextStop( shuttle: IShuttle, canReturnShuttleCurrentStop: boolean = false, ): Promise { diff --git a/src/repositories/shuttle/ShuttleGetterRepository.ts b/src/repositories/shuttle/ShuttleGetterRepository.ts index 44815ec..91f51e3 100644 --- a/src/repositories/shuttle/ShuttleGetterRepository.ts +++ b/src/repositories/shuttle/ShuttleGetterRepository.ts @@ -104,7 +104,9 @@ export interface ShuttleGetterRepository extends EventEmitter { checkIfShuttleIsAtStop(shuttleId: string): Promise; /** - * Get the stop that the shuttle is currently at, if it exists. + * Get the stop that the shuttle is currently at, if it's the shuttle's + * next stop based on the "last stop" the shuttle was at. If there was no + * "last stop" for the shuttle, it may return any stop on the shuttle's route. * * @param shuttle * @param canReturnShuttleCurrentStop If set to true, and the shuttle's "last stop" @@ -113,5 +115,5 @@ export interface ShuttleGetterRepository extends EventEmitter { * This flag has no effect if the shuttle has not had a "last stop". * @returns */ - getArrivedStopIfExists(shuttle: IShuttle, canReturnShuttleCurrentStop: boolean): Promise; + getArrivedStopIfNextStop(shuttle: IShuttle, canReturnShuttleCurrentStop: boolean): Promise; } diff --git a/src/repositories/shuttle/UnoptimizedInMemoryShuttleRepository.ts b/src/repositories/shuttle/UnoptimizedInMemoryShuttleRepository.ts index 38c7c02..c3f192c 100644 --- a/src/repositories/shuttle/UnoptimizedInMemoryShuttleRepository.ts +++ b/src/repositories/shuttle/UnoptimizedInMemoryShuttleRepository.ts @@ -3,13 +3,13 @@ import { ShuttleGetterSetterRepository } from "./ShuttleGetterSetterRepository"; import { IOrderedStop, IRoute, IShuttle, IStop, shuttleHasArrivedAtStop } from "../../entities/ShuttleRepositoryEntities"; import { IEntityWithId } from "../../entities/SharedEntities"; import { - ShuttleRepositoryEvent, - ShuttleRepositoryEventListener, - ShuttleRepositoryEventName, - ShuttleRepositoryEventPayloads, - ShuttleStopArrival, - ShuttleTravelTimeDataIdentifier, - ShuttleTravelTimeDateFilterArguments, + ShuttleRepositoryEvent, + ShuttleRepositoryEventListener, + ShuttleRepositoryEventName, + ShuttleRepositoryEventPayloads, + ShuttleStopArrival, + ShuttleTravelTimeDataIdentifier, + ShuttleTravelTimeDateFilterArguments, } from "./ShuttleGetterRepository"; /** @@ -190,9 +190,9 @@ export class UnoptimizedInMemoryShuttleRepository if (isAtStop) { // Allow retrieval of the shuttle's current stop // Will still return undefined when the shuttle leaves the stop - arrivedStop = await this.getArrivedStopIfExists(shuttle, true); + arrivedStop = await this.getArrivedStopIfNextStop(shuttle, true); } else { - arrivedStop = await this.getArrivedStopIfExists(shuttle, false); + arrivedStop = await this.getArrivedStopIfNextStop(shuttle, false); } @@ -268,7 +268,7 @@ export class UnoptimizedInMemoryShuttleRepository return sum / filteredPoints.length; } - public async getArrivedStopIfExists( + public async getArrivedStopIfNextStop( shuttle: IShuttle, canReturnShuttleCurrentStop: boolean = false, ): Promise { diff --git a/src/repositories/shuttle/__tests__/ShuttleRepositorySharedTests.test.ts b/src/repositories/shuttle/__tests__/ShuttleRepositorySharedTests.test.ts index 59745e7..8520999 100644 --- a/src/repositories/shuttle/__tests__/ShuttleRepositorySharedTests.test.ts +++ b/src/repositories/shuttle/__tests__/ShuttleRepositorySharedTests.test.ts @@ -4,10 +4,10 @@ import { UnoptimizedInMemoryShuttleRepository } from "../UnoptimizedInMemoryShut import { ShuttleGetterSetterRepository } from "../ShuttleGetterSetterRepository"; import { RedisShuttleRepository } from "../RedisShuttleRepository"; import { - generateMockOrderedStops, - generateMockRoutes, - generateMockShuttles, - generateMockStops, + generateMockOrderedStops, + generateMockRoutes, + generateMockShuttles, + generateMockStops, } from "../../../../testHelpers/mockDataGenerators"; import { RepositoryHolder } from "../../../../testHelpers/RepositoryHolder"; import { setupRouteAndOrderedStopsForShuttleRepository } from "../../../../testHelpers/setupRouteAndOrderedStopsForShuttleRepository"; @@ -577,7 +577,7 @@ describe.each(repositoryImplementations)('$name', (holder) => { test("gets any stop that the shuttle is currently at, if the shuttle has not had a last stop", async () => { const { sampleShuttleNotInRepository: shuttle } = await setupRouteAndOrderedStops(); - const result = await repository.getArrivedStopIfExists(shuttle, false); + const result = await repository.getArrivedStopIfNextStop(shuttle, false); expect(result).toBeDefined(); expect(result?.id).toBe("st2"); @@ -588,7 +588,7 @@ describe.each(repositoryImplementations)('$name', (holder) => { const { sampleShuttleNotInRepository } = await setupRouteAndOrderedStops(); const shuttle = { ...sampleShuttleNotInRepository, coordinates: { latitude: 12.5, longitude: 22.5 } }; // Not at any stop - const result = await repository.getArrivedStopIfExists(shuttle, false); + const result = await repository.getArrivedStopIfNextStop(shuttle, false); expect(result).toBeUndefined(); }); @@ -599,11 +599,11 @@ describe.each(repositoryImplementations)('$name', (holder) => { shuttle.coordinates = stop1.coordinates; await repository.addOrUpdateShuttle(shuttle); - let result = await repository.getArrivedStopIfExists(shuttle, false); + let result = await repository.getArrivedStopIfNextStop(shuttle, false); expect(result).toBeUndefined(); shuttle.coordinates = stop2.coordinates; - result = await repository.getArrivedStopIfExists(shuttle, false); + result = await repository.getArrivedStopIfNextStop(shuttle, false); expect(result).not.toBeUndefined(); }); @@ -613,7 +613,7 @@ describe.each(repositoryImplementations)('$name', (holder) => { shuttle.coordinates = stop1.coordinates; await repository.addOrUpdateShuttle(shuttle); - const result = await repository.getArrivedStopIfExists(shuttle, true); + const result = await repository.getArrivedStopIfNextStop(shuttle, true); expect(result?.id === stop1.id) }); From 8d2dd60cfb3ef4f300e5464638308ddbef34af3e Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Sat, 22 Nov 2025 18:21:22 -0800 Subject: [PATCH 31/33] Remove the unsupported shuttleStopNearbyDegreeDelta option from InterchangeSystemBuilderArguments --- src/entities/InterchangeSystem.ts | 10 ---------- src/index.ts | 1 - .../shuttle/RedisShuttleRepository.ts | 15 +++++++-------- .../UnoptimizedInMemoryShuttleRepository.ts | 15 +++++++-------- testHelpers/apolloTestServerHelpers.ts | 1 - 5 files changed, 14 insertions(+), 28 deletions(-) diff --git a/src/entities/InterchangeSystem.ts b/src/entities/InterchangeSystem.ts index 398c619..c0f8383 100644 --- a/src/entities/InterchangeSystem.ts +++ b/src/entities/InterchangeSystem.ts @@ -54,14 +54,6 @@ export interface InterchangeSystemBuilderArguments { * at a stop, in latitude/longitude degrees. */ shuttleStopArrivalDegreeDelta: number; - - /** - * The size of the threshold to detect when a shuttle is "near" - * a stop, in latitude/longitude degrees. To determine this value, - * find the distance at which the shuttle would normally take - * ~1 minute to reach the next stop. - */ - shuttleStopNearbyDegreeDelta: number; } export class InterchangeSystem { @@ -116,7 +108,6 @@ export class InterchangeSystem { const shuttleRepository = new RedisShuttleRepository( createRedisClientForRepository(), args.shuttleStopArrivalDegreeDelta, - args.shuttleStopNearbyDegreeDelta, ); await shuttleRepository.connect(); @@ -268,7 +259,6 @@ export class InterchangeSystem { private static buildInMemoryShuttleLoaderAndRepositories(args: InterchangeSystemBuilderArguments) { const shuttleRepository = new UnoptimizedInMemoryShuttleRepository( args.shuttleStopArrivalDegreeDelta, - args.shuttleStopNearbyDegreeDelta, ); let etaRepository: BaseInMemoryETARepository; diff --git a/src/index.ts b/src/index.ts index fb2d04d..7340b04 100644 --- a/src/index.ts +++ b/src/index.ts @@ -25,7 +25,6 @@ const supportedSystems: InterchangeSystemBuilderArguments[] = [ name: "Chapman University", useSelfUpdatingEtas: true, shuttleStopArrivalDegreeDelta: 0.001, - shuttleStopNearbyDegreeDelta: 0.003, } ] diff --git a/src/repositories/shuttle/RedisShuttleRepository.ts b/src/repositories/shuttle/RedisShuttleRepository.ts index 86af512..b4751c6 100644 --- a/src/repositories/shuttle/RedisShuttleRepository.ts +++ b/src/repositories/shuttle/RedisShuttleRepository.ts @@ -1,13 +1,13 @@ import { ShuttleGetterSetterRepository } from "./ShuttleGetterSetterRepository"; import { IEta, IOrderedStop, IRoute, IShuttle, IStop, shuttleHasArrivedAtStop } from "../../entities/ShuttleRepositoryEntities"; import { - ShuttleRepositoryEvent, - ShuttleRepositoryEventListener, - ShuttleRepositoryEventName, - ShuttleRepositoryEventPayloads, - ShuttleStopArrival, - ShuttleTravelTimeDataIdentifier, - ShuttleTravelTimeDateFilterArguments + ShuttleRepositoryEvent, + ShuttleRepositoryEventListener, + ShuttleRepositoryEventName, + ShuttleRepositoryEventPayloads, + ShuttleStopArrival, + ShuttleTravelTimeDataIdentifier, + ShuttleTravelTimeDateFilterArguments } from "./ShuttleGetterRepository"; import { BaseRedisRepository } from "../BaseRedisRepository"; import { RedisClientType } from "redis"; @@ -17,7 +17,6 @@ export class RedisShuttleRepository extends BaseRedisRepository implements Shutt constructor( redisClient: RedisClientType = createRedisClientForRepository(), readonly shuttleStopArrivalDegreeDelta: number = 0.001, - readonly shuttleStopNearbyDegreeDelta: number = 0.003, ) { super(redisClient); } diff --git a/src/repositories/shuttle/UnoptimizedInMemoryShuttleRepository.ts b/src/repositories/shuttle/UnoptimizedInMemoryShuttleRepository.ts index c3f192c..5fe340e 100644 --- a/src/repositories/shuttle/UnoptimizedInMemoryShuttleRepository.ts +++ b/src/repositories/shuttle/UnoptimizedInMemoryShuttleRepository.ts @@ -3,13 +3,13 @@ import { ShuttleGetterSetterRepository } from "./ShuttleGetterSetterRepository"; import { IOrderedStop, IRoute, IShuttle, IStop, shuttleHasArrivedAtStop } from "../../entities/ShuttleRepositoryEntities"; import { IEntityWithId } from "../../entities/SharedEntities"; import { - ShuttleRepositoryEvent, - ShuttleRepositoryEventListener, - ShuttleRepositoryEventName, - ShuttleRepositoryEventPayloads, - ShuttleStopArrival, - ShuttleTravelTimeDataIdentifier, - ShuttleTravelTimeDateFilterArguments, + ShuttleRepositoryEvent, + ShuttleRepositoryEventListener, + ShuttleRepositoryEventName, + ShuttleRepositoryEventPayloads, + ShuttleStopArrival, + ShuttleTravelTimeDataIdentifier, + ShuttleTravelTimeDateFilterArguments, } from "./ShuttleGetterRepository"; /** @@ -23,7 +23,6 @@ export class UnoptimizedInMemoryShuttleRepository constructor( readonly shuttleStopArrivalDegreeDelta: number = 0.001, - readonly shuttleStopNearbyDegreeDelta: number = 0.003, ) { super() } diff --git a/testHelpers/apolloTestServerHelpers.ts b/testHelpers/apolloTestServerHelpers.ts index 52cc048..6f32883 100644 --- a/testHelpers/apolloTestServerHelpers.ts +++ b/testHelpers/apolloTestServerHelpers.ts @@ -26,7 +26,6 @@ const systemInfoForTesting: InterchangeSystemBuilderArguments = { parkingSystemId: ChapmanApiBasedParkingRepositoryLoader.id, useSelfUpdatingEtas: false, shuttleStopArrivalDegreeDelta: 0.001, - shuttleStopNearbyDegreeDelta: 0.003, }; export function buildSystemForTesting() { From 19cdbec42bc66744ce70a2a0d19ba0b15514dced Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Sun, 23 Nov 2025 20:29:06 -0800 Subject: [PATCH 32/33] Refactor all Redis keys to use variables, and add clearing of shuttle isAtStop set on shuttle clear --- .../shuttle/RedisShuttleRepository.ts | 78 +++++++++++-------- 1 file changed, 45 insertions(+), 33 deletions(-) diff --git a/src/repositories/shuttle/RedisShuttleRepository.ts b/src/repositories/shuttle/RedisShuttleRepository.ts index b4751c6..42ecbfd 100644 --- a/src/repositories/shuttle/RedisShuttleRepository.ts +++ b/src/repositories/shuttle/RedisShuttleRepository.ts @@ -82,20 +82,35 @@ export class RedisShuttleRepository extends BaseRedisRepository implements Shutt return super.emit(event, ...args); } - // Helper methods for Redis key generation - private createStopKey = (stopId: string) => `shuttle:stop:${stopId}`; - private createRouteKey = (routeId: string) => `shuttle:route:${routeId}`; - private createShuttleKey = (shuttleId: string) => `shuttle:shuttle:${shuttleId}`; - private createOrderedStopKey = (routeId: string, stopId: string) => `shuttle:orderedstop:${routeId}:${stopId}`; - private createShuttleLastStopKey = (shuttleId: string) => `shuttle:laststop:${shuttleId}`; - private createHistoricalEtaTimeSeriesKey = (routeId: string, fromStopId: string, toStopId: string) => { - return `shuttle:eta:historical:${routeId}:${fromStopId}:${toStopId}`; - } + // Key prefixes for individual entity keys + private readonly stopKeyPrefix = 'shuttle:stop:'; + private readonly routeKeyPrefix = 'shuttle:route:'; + private readonly shuttleKeyPrefix = 'shuttle:shuttle:'; + private readonly orderedStopKeyPrefix = 'shuttle:orderedstop:'; + private readonly lastStopKeyPrefix = 'shuttle:laststop:'; + private readonly historicalEtaKeyPrefix = 'shuttle:eta:historical:'; + + // Key patterns for bulk operations (e.g., getting all keys, clearing data) + private readonly stopKeyPattern = 'shuttle:stop:*'; + private readonly routeKeyPattern = 'shuttle:route:*'; + private readonly shuttleKeyPattern = 'shuttle:shuttle:*'; + private readonly orderedStopKeyPattern = 'shuttle:orderedstop:*'; + private readonly lastStopKeyPattern = 'shuttle:laststop:*'; /** * Represents a set storing the shuttles that are currently at a stop. */ - private readonly shuttleIsAtStopKey = "shuttle:atstop"; + private readonly shuttleIsAtStopKey = 'shuttle:atstop'; + + // Helper methods for Redis key generation + private readonly createStopKey = (stopId: string) => `${this.stopKeyPrefix}${stopId}`; + private readonly createRouteKey = (routeId: string) => `${this.routeKeyPrefix}${routeId}`; + private readonly createShuttleKey = (shuttleId: string) => `${this.shuttleKeyPrefix}${shuttleId}`; + private readonly createOrderedStopKey = (routeId: string, stopId: string) => `${this.orderedStopKeyPrefix}${routeId}:${stopId}`; + private readonly createShuttleLastStopKey = (shuttleId: string) => `${this.lastStopKeyPrefix}${shuttleId}`; + private readonly createHistoricalEtaTimeSeriesKey = (routeId: string, fromStopId: string, toStopId: string) => { + return `${this.historicalEtaKeyPrefix}${routeId}:${fromStopId}:${toStopId}`; + }; // Helper methods for converting entities to Redis hashes private createRedisHashFromStop = (stop: IStop): Record => ({ @@ -227,7 +242,7 @@ export class RedisShuttleRepository extends BaseRedisRepository implements Shutt // Getter methods public async getStops(): Promise { - const keys = await this.redisClient.keys('shuttle:stop:*'); + const keys = await this.redisClient.keys(this.stopKeyPattern); const stops: IStop[] = []; for (const key of keys) { @@ -252,7 +267,7 @@ export class RedisShuttleRepository extends BaseRedisRepository implements Shutt } public async getRoutes(): Promise { - const keys = await this.redisClient.keys('shuttle:route:*'); + const keys = await this.redisClient.keys(this.routeKeyPattern); const routes: IRoute[] = []; for (const key of keys) { @@ -277,7 +292,7 @@ export class RedisShuttleRepository extends BaseRedisRepository implements Shutt } public async getShuttles(): Promise { - const keys = await this.redisClient.keys('shuttle:shuttle:*'); + const keys = await this.redisClient.keys(this.shuttleKeyPattern); const shuttles: IShuttle[] = []; for (const key of keys) { @@ -318,7 +333,7 @@ export class RedisShuttleRepository extends BaseRedisRepository implements Shutt } public async getOrderedStopsByStopId(stopId: string): Promise { - const keys = await this.redisClient.keys('shuttle:orderedstop:*'); + const keys = await this.redisClient.keys(this.orderedStopKeyPattern); const orderedStops: IOrderedStop[] = []; for (const key of keys) { @@ -332,7 +347,7 @@ export class RedisShuttleRepository extends BaseRedisRepository implements Shutt } public async getOrderedStopsByRouteId(routeId: string): Promise { - const keys = await this.redisClient.keys(`shuttle:orderedstop:${routeId}:*`); + const keys = await this.redisClient.keys(`${this.orderedStopKeyPrefix}${routeId}:*`); const orderedStops: IOrderedStop[] = []; for (const key of keys) { @@ -578,39 +593,36 @@ export class RedisShuttleRepository extends BaseRedisRepository implements Shutt } // Clear methods - public async clearShuttleData(): Promise { - const keys = await this.redisClient.keys('shuttle:shuttle:*'); + private async clearRedisKeys(pattern: string): Promise { + const keys = await this.redisClient.keys(pattern); if (keys.length > 0) { await this.redisClient.del(keys); } + } + + public async clearShuttleData(): Promise { + await this.clearRedisKeys(this.shuttleKeyPattern); await this.clearShuttleLastStopData(); + await this.clearShuttleIsAtStopData(); } public async clearOrderedStopData(): Promise { - const keys = await this.redisClient.keys('shuttle:orderedstop:*'); - if (keys.length > 0) { - await this.redisClient.del(keys); - } + await this.clearRedisKeys(this.orderedStopKeyPattern); } public async clearRouteData(): Promise { - const keys = await this.redisClient.keys('shuttle:route:*'); - if (keys.length > 0) { - await this.redisClient.del(keys); - } + await this.clearRedisKeys(this.routeKeyPattern); } public async clearStopData(): Promise { - const keys = await this.redisClient.keys('shuttle:stop:*'); - if (keys.length > 0) { - await this.redisClient.del(keys); - } + await this.clearRedisKeys(this.stopKeyPattern); } private async clearShuttleLastStopData(): Promise { - const keys = await this.redisClient.keys('shuttle:laststop:*'); - if (keys.length > 0) { - await this.redisClient.del(keys); - } + await this.clearRedisKeys(this.lastStopKeyPattern); + } + + private async clearShuttleIsAtStopData(): Promise { + await this.clearRedisKeys(this.shuttleIsAtStopKey); } } From 298bf229f80bb6b6fc5b6e7be71424bfae4dd110 Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Sun, 23 Nov 2025 20:29:50 -0800 Subject: [PATCH 33/33] Mark shuttle as not at stop when removing a shuttle --- src/repositories/shuttle/RedisShuttleRepository.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/repositories/shuttle/RedisShuttleRepository.ts b/src/repositories/shuttle/RedisShuttleRepository.ts index 42ecbfd..3fb63c0 100644 --- a/src/repositories/shuttle/RedisShuttleRepository.ts +++ b/src/repositories/shuttle/RedisShuttleRepository.ts @@ -556,6 +556,7 @@ export class RedisShuttleRepository extends BaseRedisRepository implements Shutt this.emit(ShuttleRepositoryEvent.SHUTTLE_REMOVED, shuttle); await this.removeShuttleLastStopIfExists(shuttleId); + await this.markShuttleAsNotAtStop(shuttleId); return shuttle; }