From 01ab7af630e7d61cd972ba86748fcdaf1d94cdb6 Mon Sep 17 00:00:00 2001 From: Devin Matte Date: Thu, 9 Oct 2025 11:57:09 -0400 Subject: [PATCH] Add caching where doable to reduce server load --- server/app.py | 12 ++++++++---- src/constants.ts | 2 ++ src/hooks/useMbtaApi.ts | 37 +++++++++++++++++++++---------------- src/hooks/usePrediction.ts | 4 ++-- 4 files changed, 33 insertions(+), 22 deletions(-) diff --git a/server/app.py b/server/app.py index 47879a1..cb7c0b7 100644 --- a/server/app.py +++ b/server/app.py @@ -28,7 +28,8 @@ def trains(route_ids_string): route_ids = route_ids_string.split(",") vehicle_data = asyncio.run(mbta_api.vehicle_data_for_routes(route_ids)) - return Response(json.dumps(vehicle_data), headers={"Cache-Control": "no-cache", "Access-Control-Allow-Origin": "*"}) + # Cache for 5 seconds to reduce API Gateway costs for multiple concurrent users + return Response(json.dumps(vehicle_data), headers={"Cache-Control": "public, max-age=5", "Access-Control-Allow-Origin": "*"}) # takes a single route id @@ -36,7 +37,8 @@ def trains(route_ids_string): @app.route("/stops/{route_id}", cors=cors_config) def stops(route_id): stop_data = asyncio.run(mbta_api.stops_for_route(route_id)) - return Response(json.dumps(stop_data), headers={"Content-Type": "application/json"}) + # Cache for 7 days - stops rarely change + return Response(json.dumps(stop_data), headers={"Content-Type": "application/json", "Cache-Control": "public, max-age=604800"}) # takes a comma-delimited string of route ids @@ -45,7 +47,8 @@ def stops(route_id): def routes(route_ids_string): route_ids = route_ids_string.split(",") route_data = asyncio.run(mbta_api.routes_info(route_ids)) - return Response(json.dumps(route_data), headers={"Content-Type": "application/json"}) + # Cache for 7 days - route info rarely changes + return Response(json.dumps(route_data), headers={"Content-Type": "application/json", "Cache-Control": "public, max-age=604800"}) # takes a single trip id @@ -53,7 +56,8 @@ def routes(route_ids_string): @app.route("/predictions/{trip_id}/{stop_id}", cors=cors_config) def vehicles(trip_id, stop_id): departure = asyncio.run(mbta_api.trip_departure_predictions(trip_id, stop_id)) - return Response(json.dumps(departure), headers={"Content-Type": "application/json"}) + # Cache for 10 seconds to reduce API Gateway costs for multiple concurrent users + return Response(json.dumps(departure), headers={"Content-Type": "application/json", "Cache-Control": "public, max-age=10"}) @app.schedule(Cron("0/10", "0-6,9-23", "*", "*", "?", "*")) diff --git a/src/constants.ts b/src/constants.ts index b31fd88..aeb34fb 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -15,6 +15,8 @@ if (typeof window !== 'undefined') { export const APP_DATA_BASE_PATH = FRONTEND_TO_BACKEND_MAP[domain] || ''; // Time in milliseconds +export const ONE_DAY = 24 * 60 * 60 * 1000; export const ONE_HOUR = 60 * 60 * 1000; export const FIVE_MINUTES = 5 * 60 * 1000; export const TEN_SECONDS = 10 * 1000; +export const FIFTEEN_SECONDS = 15 * 1000; diff --git a/src/hooks/useMbtaApi.ts b/src/hooks/useMbtaApi.ts index 2f2bf56..06eeabe 100644 --- a/src/hooks/useMbtaApi.ts +++ b/src/hooks/useMbtaApi.ts @@ -2,10 +2,10 @@ File that contains a React hook that provides data from the MBTA API */ -import { useEffect, useState, useMemo } from 'react'; +import { useState, useMemo } from 'react'; import { Line, Route, Station, Train, VehicleCategory } from '../types'; -import { APP_DATA_BASE_PATH, ONE_HOUR, TEN_SECONDS } from '../constants'; +import { APP_DATA_BASE_PATH, ONE_DAY, FIFTEEN_SECONDS } from '../constants'; import { useQuery } from '@tanstack/react-query'; export interface MBTAApi { @@ -80,8 +80,8 @@ export const useMbtaApi = ( queryFn: () => getTrainPositions(routeNames), // if routeNames is empty, don't make the request enabled: !!routeNames, - staleTime: TEN_SECONDS, - refetchInterval: TEN_SECONDS, + staleTime: FIFTEEN_SECONDS, + refetchInterval: FIFTEEN_SECONDS, }); const trainsByRoute = useMemo(() => { @@ -117,21 +117,26 @@ export const useMbtaApi = ( }, // if routeNames is empty, don't make the request enabled: !!routeNames && routeNames.length > 0, - staleTime: ONE_HOUR, + staleTime: ONE_DAY, }); - useEffect(() => { - const nextRoutesInfo: Record = {}; - getRoutesInfo(routeNames).then((routes: Route[]) => { - routes.forEach((route: Route) => { - if (route.id) { - nextRoutesInfo[route.id] = route; - } + useQuery({ + queryKey: ['getRoutesInfo', routeNamesKey], + queryFn: () => { + const nextRoutesInfo: Record = {}; + return getRoutesInfo(routeNames).then((routes: Route[]) => { + routes.forEach((route: Route) => { + if (route.id) { + nextRoutesInfo[route.id] = route; + } + }); + setRoutesInfoByRoute(nextRoutesInfo); + return nextRoutesInfo; }); - setRoutesInfoByRoute(nextRoutesInfo); - }); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [routeNamesKey]); + }, + enabled: !!routeNames && routeNames.length > 0, + staleTime: ONE_DAY, + }); const isReady = !!stationsByRoute && !!trainsByRoute && !!routesInfoByRoute && !isLoadingAllTrains; diff --git a/src/hooks/usePrediction.ts b/src/hooks/usePrediction.ts index 7e1698c..921d83e 100644 --- a/src/hooks/usePrediction.ts +++ b/src/hooks/usePrediction.ts @@ -1,4 +1,4 @@ -import { APP_DATA_BASE_PATH, TEN_SECONDS } from '../constants'; +import { APP_DATA_BASE_PATH, FIFTEEN_SECONDS } from '../constants'; import { useQuery } from '@tanstack/react-query'; const getPrediction = (tripId: string | null, stopId: string) => { @@ -16,7 +16,7 @@ export const usePrediction = (tripId: string | null, stopId: string) => { queryKey: ['getPrediction', tripId, stopId], queryFn: () => getPrediction(tripId, stopId), enabled: !!tripId, - staleTime: TEN_SECONDS, + staleTime: FIFTEEN_SECONDS, }); return prediction;