@@ -5,6 +5,7 @@ import * as Sentry from 'sentry-expo';
55import { ApiError } from '../types/api-error' ;
66import { offlineQueue } from '../utils/offlineQueue' ;
77import { trackApiLatency } from '../utils/perf' ;
8+ import { generateIdempotencyKey } from './idempotency' ;
89
910type ExpoConstantsDevHostShape = {
1011 manifest2 ?: {
@@ -28,13 +29,66 @@ function readIdempotencyHeader(headers: unknown): string | undefined {
2829 return typeof value === 'string' ? value : undefined ;
2930}
3031
32+ function buildQueueableRequest (
33+ method : string ,
34+ url : string ,
35+ data : unknown ,
36+ idempotencyKey ?: string ,
37+ ) : {
38+ method : 'POST' | 'PATCH' | 'DELETE' ;
39+ url : string ;
40+ data ?: unknown ;
41+ headers ?: Record < string , string > ;
42+ } | null {
43+ const normalizedMethod = method . toUpperCase ( ) ;
44+
45+ if (
46+ normalizedMethod === 'POST' &&
47+ ( ( url . includes ( '/groups/' ) && url . includes ( '/expenses' ) ) ||
48+ url . includes ( '/settlements' ) ||
49+ url . includes ( '/invite' ) )
50+ ) {
51+ return {
52+ method : 'POST' ,
53+ url,
54+ data,
55+ headers : { 'x-idempotency-key' : idempotencyKey ?? generateIdempotencyKey ( 'mutation' ) } ,
56+ } ;
57+ }
58+
59+ if (
60+ normalizedMethod === 'PATCH' &&
61+ ( url . includes ( '/expenses/' ) || url . includes ( '/recurring-expenses/' ) )
62+ ) {
63+ return {
64+ method : 'PATCH' ,
65+ url,
66+ data,
67+ } ;
68+ }
69+
70+ if (
71+ normalizedMethod === 'DELETE' &&
72+ ( url . includes ( '/expenses/' ) || url . includes ( '/recurring-expenses/' ) )
73+ ) {
74+ return {
75+ method : 'DELETE' ,
76+ url,
77+ headers : idempotencyKey ? { 'x-idempotency-key' : idempotencyKey } : undefined ,
78+ } ;
79+ }
80+
81+ return null ;
82+ }
83+
3184function resolveApiBaseUrl ( ) : string {
3285 if ( process . env . EXPO_PUBLIC_API_URL ) {
3386 return process . env . EXPO_PUBLIC_API_URL ;
3487 }
3588
3689 const constants = Constants as unknown as ExpoConstantsDevHostShape ;
37- const hostUri = constants . manifest2 ?. extra ?. expoClient ?. hostUri ?? constants . manifest ?. debuggerHost ;
90+ const hostUri =
91+ constants . manifest2 ?. extra ?. expoClient ?. hostUri ?? constants . manifest ?. debuggerHost ;
3892 const host = hostUri ?. split ( ':' ) [ 0 ] ;
3993
4094 if ( host && ! host . endsWith ( '.exp.direct' ) ) {
@@ -90,7 +144,8 @@ api.interceptors.request.use(async (config) => {
90144
91145api . interceptors . response . use (
92146 ( response ) => {
93- const startTs = ( response . config as typeof response . config & { metadata ?: { startTs : number } } ) . metadata ?. startTs ;
147+ const startTs = ( response . config as typeof response . config & { metadata ?: { startTs : number } } )
148+ . metadata ?. startTs ;
94149 if ( startTs ) {
95150 trackApiLatency ( response . config . url , response . config . method , Date . now ( ) - startTs ) ;
96151 }
@@ -100,21 +155,16 @@ api.interceptors.response.use(
100155 return response ;
101156 } ,
102157 ( error ) => {
103- const startTs = ( error ?. config as { metadata ?: { startTs : number } } | undefined ) ?. metadata ?. startTs ;
158+ const startTs = ( error ?. config as { metadata ?: { startTs : number } } | undefined ) ?. metadata
159+ ?. startTs ;
104160 if ( startTs ) {
105161 trackApiLatency ( error ?. config ?. url , error ?. config ?. method , Date . now ( ) - startTs ) ;
106162 }
107163
108164 const method = String ( error ?. config ?. method ?? '' ) . toUpperCase ( ) ;
109165 const url = String ( error ?. config ?. url ?? '' ) ;
110166 const retryMarked = error ?. config ?. headers ?. [ 'x-offline-retry' ] === '1' ;
111- const shouldQueue =
112- ! retryMarked &&
113- method === 'POST' &&
114- ( url . includes ( '/groups/' ) && url . includes ( '/expenses' ) ||
115- url . includes ( '/settlements' ) ||
116- url . includes ( '/invite' ) ) &&
117- ! error ?. response ;
167+ const shouldQueue = ! retryMarked && ! error ?. response ;
118168
119169 if ( shouldQueue ) {
120170 let data : unknown = error ?. config ?. data ;
@@ -127,21 +177,21 @@ api.interceptors.response.use(
127177 }
128178 }
129179
130- void offlineQueue . enqueue ( {
131- id : ` ${ Date . now ( ) } _ ${ Math . random ( ) . toString ( 36 ) . slice ( 2 ) } ` ,
132- method : 'POST' ,
133- url ,
134- data ,
135- headers : idempotencyKey ? { 'x-idempotency-key' : idempotencyKey } : undefined ,
136- } ) ;
180+ const queuedRequest = buildQueueableRequest ( method , url , data , idempotencyKey ) ;
181+ if ( queuedRequest ) {
182+ void offlineQueue . enqueue ( {
183+ id : ` ${ Date . now ( ) } _ ${ Math . random ( ) . toString ( 36 ) . slice ( 2 ) } ` ,
184+ ... queuedRequest ,
185+ } ) ;
186+ }
137187 }
138188
139189 const wrapped : ApiError = {
140190 code : String ( error ?. response ?. status ?? 'NETWORK_ERROR' ) ,
141191 message :
142192 ( error ?. response ?. data ?. message as string | undefined ) ??
143193 ( typeof error ?. message === 'string' ? error . message : 'Request failed' ) +
144- ( shouldQueue ? ' (queued for retry)' : '' ) ,
194+ ( shouldQueue ? ' (queued for retry when supported )' : '' ) ,
145195 context : {
146196 status : error ?. response ?. status ,
147197 url : error ?. config ?. url ,
0 commit comments