@@ -193,18 +193,18 @@ export class RepoStorageBackend implements StorageBackend {
193193 }
194194
195195 /**
196- * Fetch files via raw.githubusercontent.com (no API rate limits) .
197- * All fetches run in parallel - raw content endpoint has no rate limiting.
198- * Adds cache-bust parameter to bypass CDN caching issues .
196+ * Fetch files via raw.githubusercontent.com with API fallback .
197+ * Uses raw endpoint first (no rate limits), falls back to Contents API
198+ * for files not found ( CDN caching can cause stale 404s) .
199199 */
200200 private async fetchFilesViaRaw (
201201 paths : string [ ]
202202 ) : Promise < { path : string ; content : string | null } [ ] > {
203203 const branch = await this . getBranch ( ) ;
204204 const baseUrl = `https://raw.githubusercontent.com/${ this . owner } /${ this . repo } /${ branch } ` ;
205- // Add cache-bust to avoid stale CDN responses after recent pushes
206205 const cacheBust = Date . now ( ) ;
207206
207+ // First pass: try raw.githubusercontent.com (fast, no rate limits)
208208 const results = await Promise . all (
209209 paths . map ( async ( path ) => {
210210 const content = await this . fetchRawFile (
@@ -214,9 +214,65 @@ export class RepoStorageBackend implements StorageBackend {
214214 } )
215215 ) ;
216216
217+ // Find files that returned null (might be CDN cache issue)
218+ const missingPaths = results . filter ( ( r ) => r . content === null ) . map ( ( r ) => r . path ) ;
219+
220+ // Fallback: use Contents API for missing files (rate limited but fresh)
221+ if ( missingPaths . length > 0 ) {
222+ this . logProgress ( `Raw CDN missed ${ String ( missingPaths . length ) } files, using API fallback` ) ;
223+ const apiFetched = await this . fetchFilesViaContentsApi ( missingPaths ) ;
224+ // Merge API results back
225+ for ( const result of results ) {
226+ const apiContent = apiFetched [ result . path ] ;
227+ if ( result . content === null && apiContent !== undefined ) {
228+ result . content = apiContent ;
229+ }
230+ }
231+ }
232+
217233 return results ;
218234 }
219235
236+ /**
237+ * Fetch files via Contents API (fallback for CDN cache misses).
238+ * This is rate-limited but always returns fresh data.
239+ */
240+ private async fetchFilesViaContentsApi ( paths : string [ ] ) : Promise < Record < string , string | null > > {
241+ const results : Record < string , string | null > = { } ;
242+
243+ // Fetch in parallel but limit concurrency to avoid rate limits
244+ const batchSize = 10 ;
245+ for ( let i = 0 ; i < paths . length ; i += batchSize ) {
246+ const batch = paths . slice ( i , i + batchSize ) ;
247+ const batchResults = await Promise . all (
248+ batch . map ( async ( path ) => {
249+ const content = await this . fetchFileViaContentsApi ( path ) ;
250+ return { path, content } ;
251+ } )
252+ ) ;
253+ for ( const { path, content } of batchResults ) {
254+ results [ path ] = content ;
255+ }
256+ }
257+
258+ return results ;
259+ }
260+
261+ /** Fetch a single file via Contents API */
262+ private async fetchFileViaContentsApi ( path : string ) : Promise < string | null > {
263+ try {
264+ const res = await this . fetchAllowNotFound ( `/contents/${ SYNC_DIR } /${ path } ` ) ;
265+ if ( ! res ?. ok ) return null ;
266+
267+ const data = ( await res . json ( ) ) as { content ?: string ; encoding ?: string } ;
268+ if ( ! data . content || data . encoding !== 'base64' ) return null ;
269+
270+ return Buffer . from ( data . content , 'base64' ) . toString ( 'utf-8' ) ;
271+ } catch {
272+ return null ;
273+ }
274+ }
275+
220276 /** Fetch a single file from raw.githubusercontent.com */
221277 private async fetchRawFile ( url : string ) : Promise < string | null > {
222278 try {
0 commit comments