1- import request , { assertNoGraphQLErrors , isTooExpensive } from '../utils/request' ;
1+ import request , { assertNoGraphQLErrors , isTooExpensive , restRequest } from '../utils/request' ;
22import { shouldFetchNextPage } from '../const/pagination' ;
33import { withDataCache , kvGetFlag , kvSetFlag , requestStartedAt } from '../utils/data-cache' ;
44
@@ -54,15 +54,8 @@ const fetcher = (token: string, variables: any) => {
5454 company
5555 location
5656 websiteUrl
57- repositories(first: 100, privacy:PUBLIC, isFork: false, ownerAffiliations: OWNER) {
57+ repositories(first: 1, privacy:PUBLIC, isFork: false, ownerAffiliations: OWNER) {
5858 totalCount
59- nodes {
60- stargazerCount
61- }
62- pageInfo {
63- endCursor
64- hasNextPage
65- }
6659 }
6760 contributionsCollection {
6861 contributionCalendar {
@@ -117,15 +110,8 @@ const coreFetcher = (token: string, variables: any) => {
117110 company
118111 location
119112 websiteUrl
120- repositories(first: 100, privacy:PUBLIC, isFork: false, ownerAffiliations: OWNER) {
113+ repositories(first: 1, privacy:PUBLIC, isFork: false, ownerAffiliations: OWNER) {
121114 totalCount
122- nodes {
123- stargazerCount
124- }
125- pageInfo {
126- endCursor
127- hasNextPage
128- }
129115 }
130116 }
131117 }
@@ -251,77 +237,33 @@ async function fetchCalendarWeeks(username: string, token: string): Promise<Cale
251237
252238// Rebuilds the exact `user` object shape of the combined UserDetails query
253239// from the three split queries, so the code after the cache boundary doesn't
254- // care which path produced it. Star pagination starts as soon as the core
255- // query (which owns the first page's cursor) resolves and runs CONCURRENTLY
256- // with the calendar/years/counts queries — the split path only exists for
257- // very active accounts, exactly the ones with many star pages, and running
258- // the two serially is what pushed sindresorhus-class renders past Vercel's
259- // 30s kill (killed functions cache nothing, so they never converged).
260- async function fetchUserDetailsSplit (
261- username : string ,
262- token : string ,
263- startedAt : number
264- ) : Promise < { user : any ; totalStars : number } > {
265- const corePromise = coreFetcher ( token , { login : username } ) ;
266- const starsPromise = corePromise . then ( coreRes => {
267- assertNoGraphQLErrors ( coreRes , 'GetProfileDetails (core) failed' ) ;
268- return paginateStars ( coreRes . data . data . user . repositories , username , token , startedAt ) ;
269- } ) ;
270- const [ coreRes , totalStars , weeks , yearsRes , countsRes ] = await Promise . all ( [
271- corePromise ,
272- starsPromise ,
240+ // care which path produced it. Star totals are NOT fetched here — they come
241+ // from the REST pagination that getProfileDetails runs concurrently with
242+ // whichever GraphQL path is taken.
243+ async function fetchUserDetailsSplit ( username : string , token : string ) : Promise < any > {
244+ const [ coreRes , weeks , yearsRes , countsRes ] = await Promise . all ( [
245+ coreFetcher ( token , { login : username } ) ,
273246 fetchCalendarWeeks ( username , token ) ,
274247 contributionYearsFetcher ( token , { login : username } ) ,
275248 countsFetcher ( token , { login : username } )
276249 ] ) ;
250+ assertNoGraphQLErrors ( coreRes , 'GetProfileDetails (core) failed' ) ;
277251 assertNoGraphQLErrors ( yearsRes , 'GetProfileDetails (years) failed' ) ;
278252 assertNoGraphQLErrors ( countsRes , 'GetProfileDetails (counts) failed' ) ;
279253 const core = coreRes . data . data . user ;
280254 const counts = countsRes . data . data . user ;
281255 return {
282- user : {
283- ...core ,
284- contributionsCollection : {
285- contributionCalendar : { weeks} ,
286- contributionYears : yearsRes . data . data . user . contributionsCollection . contributionYears
287- } ,
288- repositoriesContributedTo : counts . repositoriesContributedTo ,
289- pullRequests : counts . pullRequests ,
290- issues : counts . issues
256+ ...core ,
257+ contributionsCollection : {
258+ contributionCalendar : { weeks} ,
259+ contributionYears : yearsRes . data . data . user . contributionsCollection . contributionYears
291260 } ,
292- totalStars
261+ repositoriesContributedTo : counts . repositoriesContributedTo ,
262+ pullRequests : counts . pullRequests ,
263+ issues : counts . issues
293264 } ;
294265}
295266
296- // Lightweight follow-up query used only to finish the star count for accounts
297- // with more than 100 repos — the heavy fields (contribution calendar etc.) all
298- // come from the first page.
299- const starsFetcher = ( token : string , variables : any ) => {
300- return request (
301- {
302- Authorization : `bearer ${ token } `
303- } ,
304- {
305- query : `
306- query UserStars($login: String!, $endCursor: String!) {
307- user(login: $login) {
308- repositories(first: 100, after: $endCursor, privacy:PUBLIC, isFork: false, ownerAffiliations: OWNER) {
309- nodes {
310- stargazerCount
311- }
312- pageInfo {
313- endCursor
314- hasNextPage
315- }
316- }
317- }
318- }
319- ` ,
320- variables
321- }
322- ) ;
323- } ;
324-
325267// ---- compact cache payload ----
326268// The raw user object is dominated by the contribution calendar: ~365 verbose
327269// day objects pushed the cached profile to ~19KB, and profile keys were the
@@ -411,38 +353,31 @@ function splitFlagKey(username: string): string {
411353 return `v1:pdx:${ username . toLowerCase ( ) } ` ;
412354}
413355
414- // The main query only covers the first 100 repos; accounts with more were
415- // undercounting stars (#164). Keep summing with the lightweight star-only
416- // query — unbounded off Vercel, bounded on it. The budget is measured from
417- // the profile fetch's start, not the pagination's, so the phases can't stack.
418- async function paginateStars ( firstPage : any , username : string , token : string , startedAt : number ) : Promise < number > {
419- let stars : number = firstPage . nodes . reduce (
420- ( acc : number , curr : { stargazerCount : number } ) => acc + curr . stargazerCount ,
421- 0
422- ) ;
423- let starsCursor : string | null = firstPage . pageInfo ?. endCursor ?? null ;
424- let starsPages = 1 ;
425- let starsHasNextPage = shouldFetchNextPage (
426- ! ! firstPage . pageInfo ?. hasNextPage ,
427- starsPages ,
428- undefined ,
429- startedAt ,
430- PD_FETCH_BUDGET_MS
431- ) ;
432- while ( starsHasNextPage && starsCursor ) {
433- const starsRes : any = await starsFetcher ( token , { login : username , endCursor : starsCursor } ) ;
434- assertNoGraphQLErrors ( starsRes , 'GetProfileDetails failed' ) ;
435- const repos = starsRes . data . data . user . repositories ;
436- stars += repos . nodes . reduce ( ( acc : number , curr : { stargazerCount : number } ) => acc + curr . stargazerCount , 0 ) ;
437- starsCursor = repos . pageInfo ?. endCursor ?? null ;
438- starsPages += 1 ;
439- starsHasNextPage = shouldFetchNextPage (
440- ! ! repos . pageInfo ?. hasNextPage ,
441- starsPages ,
442- undefined ,
443- startedAt ,
444- PD_FETCH_BUDGET_MS
445- ) ;
356+ // Star totals come from REST repo pagination (stargazer_count rides along on
357+ // GET /users/:login/repos) instead of GraphQL pages: REST draws on a separate,
358+ // otherwise-idle hourly quota, and dropping the 100-node repos page from the
359+ // GraphQL documents also lowers their cost-estimator score — fewer split
360+ // rejections for heavy accounts. Fork filtering matches the old isFork: false;
361+ // REST only lists public repos, matching privacy: PUBLIC. Pagination is
362+ // unbounded off Vercel and bounded on it (#164 semantics unchanged); the
363+ // budget is measured from the profile fetch's start so the phases can't stack.
364+ async function fetchTotalStars ( username : string , token : string , startedAt : number ) : Promise < number > {
365+ let stars = 0 ;
366+ let pages = 0 ;
367+ let hasNextPage = true ;
368+ while ( hasNextPage ) {
369+ const res = await restRequest ( token , `/users/${ encodeURIComponent ( username ) } /repos` , {
370+ per_page : 100 ,
371+ page : pages + 1 ,
372+ type : 'owner'
373+ } ) ;
374+ const repos : any [ ] = Array . isArray ( res . data ) ? res . data : [ ] ;
375+ for ( const repo of repos ) {
376+ if ( repo . fork ) continue ;
377+ stars += repo . stargazer_count ?? 0 ;
378+ }
379+ pages += 1 ;
380+ hasNextPage = shouldFetchNextPage ( repos . length === 100 , pages , undefined , startedAt , PD_FETCH_BUDGET_MS ) ;
446381 }
447382 return stars ;
448383}
@@ -461,8 +396,15 @@ export async function getProfileDetails(username: string, token: string): Promis
461396 // instead of a 30s FUNCTION_INVOCATION_TIMEOUT that caches nothing.
462397 throw new Error ( `Profile fetch for ${ username } timed out before completion` ) ;
463398 }
399+ // REST star pagination runs concurrently with whichever GraphQL path
400+ // is taken — separate quota pools, so they don't contend. The no-op
401+ // catch keeps a stars failure from surfacing as an unhandled rejection
402+ // while the GraphQL path is still the one that throws first; the real
403+ // rejection still propagates through the await below.
404+ const starsPromise = fetchTotalStars ( username , token , startedAt ) ;
405+ starsPromise . catch ( ( ) => undefined ) ;
406+
464407 let fetchedUser : any = null ;
465- let totalStars : number | null = null ;
466408 const useSplit = await kvGetFlag ( splitFlagKey ( username ) ) ;
467409 if ( ! useSplit ) {
468410 try {
@@ -480,14 +422,10 @@ export async function getProfileDetails(username: string, token: string): Promis
480422 }
481423 if ( fetchedUser === null ) {
482424 // Rejected now or flagged earlier — same fields via three smaller
483- // queries, with star pagination running concurrently.
484- const split = await fetchUserDetailsSplit ( username , token , startedAt ) ;
485- fetchedUser = split . user ;
486- totalStars = split . totalStars ;
487- }
488- if ( totalStars === null ) {
489- totalStars = await paginateStars ( fetchedUser . repositories , username , token , startedAt ) ;
425+ // queries.
426+ fetchedUser = await fetchUserDetailsSplit ( username , token ) ;
490427 }
428+ const totalStars = await starsPromise ;
491429
492430 return compressProfile ( fetchedUser , totalStars ) ;
493431 } ) ;
0 commit comments