@@ -136,12 +136,17 @@ function invalidateTTLCache(prefix: string): void {
136136
137137// ==================== Fetch with ETag/Cache ====================
138138
139- async function fetchSourceWithEtag ( url : string ) : Promise < EcosystemSource | null > {
139+ type FetchSourceResult = {
140+ payload : EcosystemSource | null ;
141+ errorMessage ?: string ;
142+ } ;
143+
144+ async function fetchSourceWithEtag ( url : string ) : Promise < FetchSourceResult > {
140145 const fetchUrl = normalizeFetchUrl ( url ) ;
141146 const cacheKey = fetchUrl ;
142147 // Check TTL cache first
143148 const ttlCached = getTTLCached < EcosystemSource > ( `source:${ cacheKey } ` ) ;
144- if ( ttlCached ) return ttlCached ;
149+ if ( ttlCached ) return { payload : ttlCached } ;
145150
146151 // Get cached entry for ETag
147152 const cached = await getCachedSource ( cacheKey ) ;
@@ -158,42 +163,45 @@ async function fetchSourceWithEtag(url: string): Promise<EcosystemSource | null>
158163 if ( response . status === 304 && cached ) {
159164 debugLog ( 'fetch source not modified' , { url, fetchUrl } ) ;
160165 setTTLCached ( `source:${ cacheKey } ` , cached . data , TTL_MS ) ;
161- return cached . data ;
166+ return { payload : cached . data } ;
162167 }
163168
164169 if ( ! response . ok ) {
170+ const message = `HTTP ${ response . status } ` ;
165171 debugLog ( 'fetch source failed' , { url, fetchUrl, status : response . status } ) ;
166172 // Fall back to cache on error
167173 if ( cached ) {
168174 setTTLCached ( `source:${ cacheKey } ` , cached . data , TTL_MS ) ;
169- return cached . data ;
175+ return { payload : cached . data , errorMessage : message } ;
170176 }
171- return null ;
177+ return { payload : null , errorMessage : message } ;
172178 }
173179
174180 const contentType = response . headers . get ( 'content-type' ) ?? '' ;
175181 let json : unknown ;
176182 try {
177183 json = await response . json ( ) ;
178184 } catch ( error ) {
185+ const message = error instanceof Error ? error . message : String ( error ) ;
179186 debugLog ( 'fetch source parse error' , {
180187 url,
181188 fetchUrl,
182189 contentType,
183- message : error instanceof Error ? error . message : String ( error ) ,
190+ message,
184191 } ) ;
185- if ( cached ) return cached . data ;
186- return null ;
192+ if ( cached ) return { payload : cached . data , errorMessage : message } ;
193+ return { payload : null , errorMessage : message } ;
187194 }
188195 const parsed = EcosystemSourceSchema . safeParse ( json ) ;
189196 if ( ! parsed . success ) {
197+ const message = 'Invalid source payload' ;
190198 debugLog ( 'fetch source invalid payload' , {
191199 url,
192200 fetchUrl,
193201 issues : parsed . error . issues . map ( ( issue ) => issue . path . join ( '.' ) ) ,
194202 } ) ;
195- if ( cached ) return cached . data ;
196- return null ;
203+ if ( cached ) return { payload : cached . data , errorMessage : message } ;
204+ return { payload : null , errorMessage : message } ;
197205 }
198206
199207 const data = parsed . data ;
@@ -204,19 +212,20 @@ async function fetchSourceWithEtag(url: string): Promise<EcosystemSource | null>
204212 setTTLCached ( `source:${ cacheKey } ` , data , TTL_MS ) ;
205213 debugLog ( 'fetch source success' , { url, fetchUrl, apps : data . apps ?. length ?? 0 } ) ;
206214
207- return data ;
215+ return { payload : data } ;
208216 } catch ( error ) {
217+ const message = error instanceof Error ? error . message : String ( error ) ;
209218 debugLog ( 'fetch source error' , {
210219 url,
211220 fetchUrl,
212- message : error instanceof Error ? error . message : String ( error ) ,
221+ message,
213222 } ) ;
214223 // Fall back to cache on error
215224 if ( cached ) {
216225 setTTLCached ( `source:${ cacheKey } ` , cached . data , TTL_MS ) ;
217- return cached . data ;
226+ return { payload : cached . data , errorMessage : message } ;
218227 }
219- return null ;
228+ return { payload : null , errorMessage : message } ;
220229 }
221230}
222231
@@ -394,7 +403,8 @@ function normalizeAppFromSource(
394403}
395404
396405async function fetchSourceWithCache ( url : string ) : Promise < EcosystemSource | null > {
397- return fetchSourceWithEtag ( url ) ;
406+ const result = await fetchSourceWithEtag ( url ) ;
407+ return result . payload ;
398408}
399409
400410async function rebuildCachedAppsFromCache ( ) : Promise < void > {
@@ -454,38 +464,42 @@ export async function refreshSources(options?: { force?: boolean }): Promise<Min
454464 await Promise . all (
455465 enabledSources . map ( async ( source ) => {
456466 ecosystemActions . updateSourceStatus ( source . url , 'loading' ) ;
457- try {
458- const payload = await fetchSourceWithEtag ( source . url ) ;
467+ const result = await fetchSourceWithEtag ( source . url ) ;
468+ sourcePayloads . set ( source . url , result . payload ) ;
469+
470+ if ( result . errorMessage ) {
471+ ecosystemActions . updateSourceStatus ( source . url , 'error' , result . errorMessage ) ;
472+ } else if ( result . payload ) {
459473 ecosystemActions . updateSourceStatus ( source . url , 'success' ) ;
460- sourcePayloads . set ( source . url , payload ) ;
461- } catch ( error ) {
462- const message = error instanceof Error ? error . message : 'Unknown error' ;
463- ecosystemActions . updateSourceStatus ( source . url , 'error' , message ) ;
464- sourcePayloads . set ( source . url , null ) ;
465- } finally {
466- cachedApps = mergeAppsFromSources ( enabledSources , sourcePayloads ) ;
467- notifyApps ( ) ;
474+ } else {
475+ ecosystemActions . updateSourceStatus ( source . url , 'error' , 'Failed to fetch source' ) ;
468476 }
477+
478+ cachedApps = mergeAppsFromSources ( enabledSources , sourcePayloads ) ;
479+ notifyApps ( ) ;
469480 } ) ,
470481 ) ;
471482 return [ ...cachedApps ] ;
472483}
473484
474485export async function refreshSource ( url : string ) : Promise < void > {
475486 ecosystemActions . updateSourceStatus ( url , 'loading' ) ;
476- try {
477- invalidateTTLCache ( `source:${ url } ` ) ;
478- const payload = await fetchSourceWithEtag ( url ) ;
479- if ( payload ) {
480- ecosystemActions . updateSourceStatus ( url , 'success' ) ;
481- await rebuildCachedAppsFromCache ( ) ;
482- } else {
483- ecosystemActions . updateSourceStatus ( url , 'error' , 'Failed to fetch source' ) ;
484- }
485- } catch ( error ) {
486- const message = error instanceof Error ? error . message : 'Unknown error' ;
487- ecosystemActions . updateSourceStatus ( url , 'error' , message ) ;
487+ invalidateTTLCache ( `source:${ url } ` ) ;
488+ const result = await fetchSourceWithEtag ( url ) ;
489+
490+ if ( result . errorMessage ) {
491+ ecosystemActions . updateSourceStatus ( url , 'error' , result . errorMessage ) ;
492+ await rebuildCachedAppsFromCache ( ) ;
493+ return ;
488494 }
495+
496+ if ( result . payload ) {
497+ ecosystemActions . updateSourceStatus ( url , 'success' ) ;
498+ await rebuildCachedAppsFromCache ( ) ;
499+ return ;
500+ }
501+
502+ ecosystemActions . updateSourceStatus ( url , 'error' , 'Failed to fetch source' ) ;
489503}
490504
491505export async function loadSource ( url : string ) : Promise < EcosystemSource | null > {
0 commit comments