@@ -24,10 +24,23 @@ export interface MBTAApiReady {
2424
2525export type MBTAApiResponse = MBTAApi | MBTAApiReady ;
2626
27+ // Helper to validate array responses from API
28+ const isValidArray = < T > ( data : unknown ) : data is T [ ] => {
29+ return Array . isArray ( data ) ;
30+ } ;
31+
2732// if isFirstRequest is true, get train positions from intial request data JSON
2833// if isFirstRequest is false, makes request for new train positions through backend server via chalice route defined in app.py
29- const getTrainPositions = ( routes : string [ ] ) : Promise < Train [ ] > => {
30- return fetch ( `${ APP_DATA_BASE_PATH } /trains/${ routes . join ( ',' ) } ` ) . then ( ( res ) => res . json ( ) ) ;
34+ const getTrainPositions = async ( routes : string [ ] ) : Promise < Train [ ] > => {
35+ const res = await fetch ( `${ APP_DATA_BASE_PATH } /trains/${ routes . join ( ',' ) } ` ) ;
36+ if ( ! res . ok ) {
37+ throw new Error ( `Failed to fetch trains: ${ res . status } ${ res . statusText } ` ) ;
38+ }
39+ const data = await res . json ( ) ;
40+ if ( ! isValidArray < Train > ( data ) ) {
41+ throw new Error ( `Invalid train data received: ${ JSON . stringify ( data ) . slice ( 0 , 100 ) } ` ) ;
42+ }
43+ return data ;
3144} ;
3245
3346const filterNew = ( trains : Train [ ] ) => {
@@ -59,12 +72,28 @@ const filterTrains = (trains: Train[], vehiclesAge: VehicleCategory) => {
5972 return trains ;
6073} ;
6174
62- const getStationsForRoute = ( route : string ) => {
63- return fetch ( `${ APP_DATA_BASE_PATH } /stops/${ route } ` ) . then ( ( res ) => res . json ( ) ) ;
75+ const getStationsForRoute = async ( route : string ) : Promise < Station [ ] > => {
76+ const res = await fetch ( `${ APP_DATA_BASE_PATH } /stops/${ route } ` ) ;
77+ if ( ! res . ok ) {
78+ throw new Error ( `Failed to fetch stations for ${ route } : ${ res . status } ${ res . statusText } ` ) ;
79+ }
80+ const data = await res . json ( ) ;
81+ if ( ! isValidArray < Station > ( data ) ) {
82+ throw new Error ( `Invalid station data for ${ route } : ${ JSON . stringify ( data ) . slice ( 0 , 100 ) } ` ) ;
83+ }
84+ return data ;
6485} ;
6586
66- const getRoutesInfo = ( routes : string [ ] ) => {
67- return fetch ( `${ APP_DATA_BASE_PATH } /routes/${ routes . join ( ',' ) } ` ) . then ( ( res ) => res . json ( ) ) ;
87+ const getRoutesInfo = async ( routes : string [ ] ) : Promise < Route [ ] > => {
88+ const res = await fetch ( `${ APP_DATA_BASE_PATH } /routes/${ routes . join ( ',' ) } ` ) ;
89+ if ( ! res . ok ) {
90+ throw new Error ( `Failed to fetch routes info: ${ res . status } ${ res . statusText } ` ) ;
91+ }
92+ const data = await res . json ( ) ;
93+ if ( ! isValidArray < Route > ( data ) ) {
94+ throw new Error ( `Invalid routes info received: ${ JSON . stringify ( data ) . slice ( 0 , 100 ) } ` ) ;
95+ }
96+ return data ;
6897} ;
6998
7099export const useMbtaApi = (
@@ -88,6 +117,8 @@ export const useMbtaApi = (
88117 enabled : ! ! routeNames ,
89118 staleTime : FIFTEEN_SECONDS ,
90119 refetchInterval : FIFTEEN_SECONDS ,
120+ retry : 3 ,
121+ retryDelay : ( attemptIndex ) => Math . min ( 1000 * 2 ** attemptIndex , 30000 ) ,
91122 } ) ;
92123
93124 const trainsByRoute = useMemo ( ( ) => {
@@ -108,40 +139,43 @@ export const useMbtaApi = (
108139
109140 useQuery ( {
110141 queryKey : [ 'getStations' , routeNamesKey ] ,
111- queryFn : ( ) => {
142+ queryFn : async ( ) => {
112143 const nextStopsByRoute : Record < string , Station [ ] > = { } ;
113- return Promise . all (
114- routeNames . map ( ( routeName ) =>
115- getStationsForRoute ( routeName ) . then ( ( data ) => {
116- nextStopsByRoute [ routeName ] = data ;
117- } )
118- )
119- ) . then ( ( ) => {
120- setStationsByRoute ( nextStopsByRoute ) ;
121- return nextStopsByRoute ;
122- } ) ;
144+ await Promise . all (
145+ routeNames . map ( async ( routeName ) => {
146+ const data = await getStationsForRoute ( routeName ) ;
147+ nextStopsByRoute [ routeName ] = data ;
148+ } )
149+ ) ;
150+ // Only update state on successful fetch
151+ setStationsByRoute ( nextStopsByRoute ) ;
152+ return nextStopsByRoute ;
123153 } ,
124154 // if routeNames is empty, don't make the request
125155 enabled : ! ! routeNames && routeNames . length > 0 ,
126156 staleTime : ONE_DAY ,
157+ retry : 3 ,
158+ retryDelay : ( attemptIndex ) => Math . min ( 1000 * 2 ** attemptIndex , 30000 ) ,
127159 } ) ;
128160
129161 useQuery ( {
130162 queryKey : [ 'getRoutesInfo' , routeNamesKey ] ,
131- queryFn : ( ) => {
163+ queryFn : async ( ) => {
132164 const nextRoutesInfo : Record < string , Route > = { } ;
133- return getRoutesInfo ( routeNames ) . then ( ( routes : Route [ ] ) => {
134- routes . forEach ( ( route : Route ) => {
135- if ( route . id ) {
136- nextRoutesInfo [ route . id ] = route ;
137- }
138- } ) ;
139- setRoutesInfoByRoute ( nextRoutesInfo ) ;
140- return nextRoutesInfo ;
165+ const routes = await getRoutesInfo ( routeNames ) ;
166+ routes . forEach ( ( route : Route ) => {
167+ if ( route . id ) {
168+ nextRoutesInfo [ route . id ] = route ;
169+ }
141170 } ) ;
171+ // Only update state on successful fetch
172+ setRoutesInfoByRoute ( nextRoutesInfo ) ;
173+ return nextRoutesInfo ;
142174 } ,
143175 enabled : ! ! routeNames && routeNames . length > 0 ,
144176 staleTime : ONE_DAY ,
177+ retry : 3 ,
178+ retryDelay : ( attemptIndex ) => Math . min ( 1000 * 2 ** attemptIndex , 30000 ) ,
145179 } ) ;
146180
147181 const isReady =
0 commit comments