@@ -25,6 +25,8 @@ import { extractArtifact, normalizeScmUrl } from '../extract'
2525import { resolveLatestVersion } from '../metadata'
2626import { resolveRegistryBaseUrl } from '../registry'
2727
28+ import { parseCsv } from './csv'
29+
2830// ─── Config ───────────────────────────────────────────────────────────────────
2931
3032const LIBRARIES_IO_KEY = process . env . LIBRARIES_IO_API_KEY ?? ''
@@ -84,83 +86,31 @@ async function safeGet<T>(
8486 url : string ,
8587 params ?: Record < string , string > ,
8688) : Promise < T | null > {
87- try {
88- const res = await client . get < T > ( url , { params } )
89- return res . data
90- } catch ( err ) {
91- if ( axios . isAxiosError ( err ) ) {
92- const status = err . response ?. status
93- if ( status === 404 || status === 422 ) return null
94- if ( status === 403 || status === 429 ) {
95- const reset = err . response ?. headers ?. [ 'x-ratelimit-reset' ]
96- const wait = reset ? Math . max ( 0 , Number ( reset ) * 1000 - Date . now ( ) ) + 1000 : 60_000
97- console . warn ( ` Rate limited — waiting ${ Math . round ( wait / 1000 ) } s` )
98- await sleep ( wait )
99- return safeGet < T > ( client , url , params )
89+ const MAX_RETRIES = 5
90+ for ( let attempt = 0 ; attempt < MAX_RETRIES ; attempt ++ ) {
91+ try {
92+ const res = await client . get < T > ( url , { params } )
93+ return res . data
94+ } catch ( err ) {
95+ if ( axios . isAxiosError ( err ) ) {
96+ const status = err . response ?. status
97+ if ( status === 404 || status === 422 ) return null
98+ if ( status === 403 || status === 429 ) {
99+ const reset = err . response ?. headers ?. [ 'x-ratelimit-reset' ]
100+ const wait = reset ? Math . max ( 0 , Number ( reset ) * 1000 - Date . now ( ) ) + 1000 : 60_000
101+ console . warn (
102+ ` Rate limited (attempt ${ attempt + 1 } /${ MAX_RETRIES } ) — waiting ${ Math . round ( wait / 1000 ) } s` ,
103+ )
104+ await sleep ( wait )
105+ continue
106+ }
107+ console . warn ( ` HTTP ${ status } for ${ url } ` )
100108 }
101- console . warn ( ` HTTP ${ status } for ${ url } ` )
109+ return null
102110 }
103- return null
104111 }
105- }
106-
107- // ─── CSV parser ───────────────────────────────────────────────────────────────
108-
109- function parseCsv ( content : string ) : Record < string , string > [ ] {
110- const rows : string [ ] [ ] = [ ]
111- let cur = ''
112- let inQuote = false
113- let row : string [ ] = [ ]
114-
115- for ( let i = 0 ; i < content . length ; i ++ ) {
116- const ch = content [ i ]
117- const next = content [ i + 1 ]
118-
119- if ( inQuote ) {
120- if ( ch === '"' && next === '"' ) {
121- cur += '"'
122- i ++
123- } else if ( ch === '"' ) {
124- inQuote = false
125- } else {
126- cur += ch
127- }
128- } else {
129- if ( ch === '"' ) {
130- inQuote = true
131- } else if ( ch === ',' ) {
132- row . push ( cur )
133- cur = ''
134- } else if ( ch === '\r' && next === '\n' ) {
135- row . push ( cur )
136- cur = ''
137- rows . push ( row )
138- row = [ ]
139- i ++
140- } else if ( ch === '\n' ) {
141- row . push ( cur )
142- cur = ''
143- rows . push ( row )
144- row = [ ]
145- } else {
146- cur += ch
147- }
148- }
149- }
150- if ( cur || row . length ) {
151- row . push ( cur )
152- rows . push ( row )
153- }
154-
155- if ( rows . length === 0 ) return [ ]
156- const headers = rows [ 0 ]
157- return rows . slice ( 1 ) . map ( ( r ) => {
158- const obj : Record < string , string > = { }
159- headers . forEach ( ( h , i ) => {
160- obj [ h . trim ( ) ] = ( r [ i ] ?? '' ) . trim ( )
161- } )
162- return obj
163- } )
112+ console . warn ( ` Giving up after ${ MAX_RETRIES } rate-limit retries: ${ url } ` )
113+ return null
164114}
165115
166116function csvEscape ( value : string ) : string {
0 commit comments