@@ -2,6 +2,7 @@ import { Pool, request } from 'undici'
22import * as semver from 'semver'
33import { CACHE_TTL , JSDELIVR_CDN_URL , MAX_CONCURRENT_REQUESTS , REQUEST_TIMEOUT } from '../config'
44import { getAllPackageData } from './npm-registry'
5+ import { OnBatchReadyCallback } from '../types'
56
67// Create a persistent connection pool for jsDelivr CDN with optimal settings
78// This enables connection reuse and HTTP/1.1 keep-alive for blazing fast requests
@@ -13,6 +14,10 @@ const jsdelivrPool = new Pool('https://cdn.jsdelivr.net', {
1314 connectTimeout : REQUEST_TIMEOUT , // 60 seconds connect timeout
1415} )
1516
17+ // Batch configuration for progressive loading
18+ const BATCH_SIZE = 5
19+ const BATCH_TIMEOUT_MS = 500
20+
1621// In-memory cache for package data
1722interface CacheEntry {
1823 data : { latestVersion : string ; allVersions : string [ ] }
@@ -62,16 +67,19 @@ async function fetchPackageJsonFromJsdelivr(
6267/**
6368 * Fetches package version data from jsdelivr CDN for multiple packages.
6469 * Uses undici connection pool for blazing fast performance with connection reuse.
65- * Falls back to npm registry in batches if jsdelivr doesn't have packages.
70+ * Falls back to npm registry immediately when jsdelivr fails (interleaved, not sequential).
71+ * Supports batched callbacks for progressive UI updates.
6672 * @param packageNames - Array of package names to fetch
6773 * @param currentVersions - Optional map of package names to their current versions
6874 * @param onProgress - Optional progress callback
75+ * @param onBatchReady - Optional callback for batch updates (fires every BATCH_SIZE packages or BATCH_TIMEOUT_MS)
6976 * @returns Map of package names to their version data
7077 */
7178export async function getAllPackageDataFromJsdelivr (
7279 packageNames : string [ ] ,
7380 currentVersions ?: Map < string , string > ,
74- onProgress ?: ( currentPackage : string , completed : number , total : number ) => void
81+ onProgress ?: ( currentPackage : string , completed : number , total : number ) => void ,
82+ onBatchReady ?: OnBatchReadyCallback
7583) : Promise < Map < string , { latestVersion : string ; allVersions : string [ ] } > > {
7684 const packageData = new Map < string , { latestVersion : string ; allVersions : string [ ] } > ( )
7785
@@ -82,11 +90,43 @@ export async function getAllPackageDataFromJsdelivr(
8290 const total = packageNames . length
8391 let completedCount = 0
8492
85- // Track packages that need npm fallback (not found on jsDelivr)
86- const failedPackages : string [ ] = [ ]
93+ // Batch buffer for progressive updates
94+ let batchBuffer : Array < { name : string ; data : { latestVersion : string ; allVersions : string [ ] } } > =
95+ [ ]
96+ let batchTimer : NodeJS . Timeout | null = null
97+
98+ // Helper to flush the current batch
99+ const flushBatch = ( ) => {
100+ if ( batchBuffer . length > 0 && onBatchReady ) {
101+ onBatchReady ( [ ...batchBuffer ] )
102+ batchBuffer = [ ]
103+ }
104+ if ( batchTimer ) {
105+ clearTimeout ( batchTimer )
106+ batchTimer = null
107+ }
108+ }
109+
110+ // Helper to add package to batch and flush if needed
111+ const addToBatch = (
112+ packageName : string ,
113+ data : { latestVersion : string ; allVersions : string [ ] }
114+ ) => {
115+ if ( onBatchReady ) {
116+ batchBuffer . push ( { name : packageName , data } )
87117
88- // Fire all jsDelivr requests simultaneously - undici pool handles concurrency internally
89- const allPromises = packageNames . map ( async ( packageName ) => {
118+ // Flush if batch is full
119+ if ( batchBuffer . length >= BATCH_SIZE ) {
120+ flushBatch ( )
121+ } else if ( ! batchTimer ) {
122+ // Set timer to flush batch after timeout
123+ batchTimer = setTimeout ( flushBatch , BATCH_TIMEOUT_MS )
124+ }
125+ }
126+ }
127+
128+ // Process individual package fetch with immediate npm fallback on failure
129+ const fetchPackageWithFallback = async ( packageName : string ) : Promise < void > => {
90130 const currentVersion = currentVersions ?. get ( packageName )
91131
92132 // Try to get from cache first
@@ -97,6 +137,7 @@ export async function getAllPackageDataFromJsdelivr(
97137 if ( onProgress ) {
98138 onProgress ( packageName , completedCount , total )
99139 }
140+ addToBatch ( packageName , cached . data )
100141 return
101142 }
102143
@@ -122,8 +163,23 @@ export async function getAllPackageDataFromJsdelivr(
122163 const majorResult = results [ 1 ]
123164
124165 if ( ! latestResult ) {
125- // Package not on jsDelivr, mark for npm fallback
126- failedPackages . push ( packageName )
166+ // Package not on jsDelivr, immediately try npm fallback
167+ const npmData = await getAllPackageData ( [ packageName ] )
168+ const result = npmData . get ( packageName )
169+
170+ if ( result ) {
171+ packageData . set ( packageName , result )
172+ packageCache . set ( packageName , {
173+ data : result ,
174+ timestamp : Date . now ( ) ,
175+ } )
176+ addToBatch ( packageName , result )
177+ }
178+
179+ completedCount ++
180+ if ( onProgress ) {
181+ onProgress ( packageName , completedCount , total )
182+ }
127183 return
128184 }
129185
@@ -152,34 +208,38 @@ export async function getAllPackageDataFromJsdelivr(
152208 if ( onProgress ) {
153209 onProgress ( packageName , completedCount , total )
154210 }
211+ addToBatch ( packageName , result )
155212 } catch ( error ) {
156- // On error, mark for npm fallback
157- failedPackages . push ( packageName )
158- }
159- } )
213+ // On error, immediately try npm fallback
214+ try {
215+ const npmData = await getAllPackageData ( [ packageName ] )
216+ const result = npmData . get ( packageName )
160217
161- // Wait for all jsDelivr requests to complete
162- await Promise . all ( allPromises )
218+ if ( result ) {
219+ packageData . set ( packageName , result )
220+ packageCache . set ( packageName , {
221+ data : result ,
222+ timestamp : Date . now ( ) ,
223+ } )
224+ addToBatch ( packageName , result )
225+ }
226+ } catch ( npmError ) {
227+ // If both fail, just continue
228+ }
163229
164- // Batch fetch all failed packages from npm registry in one call
165- if ( failedPackages . length > 0 ) {
166- const npmData = await getAllPackageData ( failedPackages , ( pkg , completed , npmTotal ) => {
167230 completedCount ++
168231 if ( onProgress ) {
169- onProgress ( pkg , completedCount , total )
232+ onProgress ( packageName , completedCount , total )
170233 }
171- } )
172-
173- // Merge npm data into results and cache it
174- for ( const [ packageName , data ] of npmData . entries ( ) ) {
175- packageData . set ( packageName , data )
176- packageCache . set ( packageName , {
177- data,
178- timestamp : Date . now ( ) ,
179- } )
180234 }
181235 }
182236
237+ // Fire all requests simultaneously - they handle fallback internally and immediately
238+ await Promise . all ( packageNames . map ( fetchPackageWithFallback ) )
239+
240+ // Flush any remaining batch items
241+ flushBatch ( )
242+
183243 // Clear the progress line and show completion time if no custom progress handler
184244 if ( ! onProgress ) {
185245 process . stdout . write ( '\r' + ' ' . repeat ( 80 ) + '\r' )
0 commit comments