@@ -248,6 +248,10 @@ export async function scanDependents(input: {
248248 const unscopedNames = candidateNames . filter ( ( n ) => ! n . startsWith ( '@' ) )
249249 const scopedNames = candidateNames . filter ( ( n ) => n . startsWith ( '@' ) )
250250
251+ // Neither loop below heartbeated before — for a large candidate pool (many
252+ // unscoped batches, or many scoped names run one HTTP call at a time through
253+ // mapWithConcurrency) this whole download-count phase could run past the
254+ // dependents stage's heartbeatTimeout with nothing heartbeating in between.
251255 for ( let i = 0 ; i < unscopedNames . length ; i += 128 ) {
252256 const batch = unscopedNames . slice ( i , i + 128 )
253257 const result = await fetchBulkPointRange ( batch , isoDate ( rangeStart ) , isoDate ( rangeEnd ) )
@@ -256,15 +260,21 @@ export async function scanDependents(input: {
256260 downloads . set ( name , count )
257261 } )
258262 }
263+ onProgress ?.( )
259264 }
260265
261266 // fetchBulkPointRange doesn't support scoped package names; fetch those individually
262267 // so scoped candidates aren't silently ranked at 0 downloads and excluded from topN.
268+ let scopedProcessed = 0
263269 await mapWithConcurrency ( scopedNames , SCAN_CONCURRENCY , async ( name ) => {
264270 const result = await fetchPointRange ( name , isoDate ( rangeStart ) , isoDate ( rangeEnd ) )
265271 if ( ! isFetchError ( result ) ) {
266272 downloads . set ( name , result . count )
267273 }
274+ scopedProcessed ++
275+ if ( onProgress && scopedProcessed % 200 === 0 ) {
276+ onProgress ( )
277+ }
268278 } )
269279
270280 // Phase 2: walk candidates ranked by downloads (descending), fetching the full packument
0 commit comments