@@ -55,6 +55,10 @@ type IssuesWithStatsResponse struct {
5555 Issues []stats.IssuesPerDay `json:"issues"`
5656}
5757
58+ type ForksWithStatsResponse struct {
59+ Forks []stats.ForksPerDay `json:"forks"`
60+ }
61+
5862func getEnv (key , fallback string ) string {
5963 value , exists := os .LookupEnv (key )
6064 if ! exists {
@@ -106,9 +110,11 @@ func main() {
106110 cacheOverall := cache .New [string , * stats.RepoStats ]()
107111 cacheStars := cache .New [string , StarsWithStatsResponse ]()
108112 cacheIssues := cache .New [string , IssuesWithStatsResponse ]()
113+ cacheForks := cache .New [string , ForksWithStatsResponse ]()
109114
110115 onGoingStars := make (map [string ]bool )
111116 onGoingIssues := make (map [string ]bool )
117+ onGoingForks := make (map [string ]bool )
112118
113119 ghStatClients := make (map [string ]* repostats.ClientGQL )
114120
@@ -454,6 +460,110 @@ func main() {
454460 return c .JSON (res )
455461 })
456462
463+ app .Get ("/allForks" , func (c * fiber.Ctx ) error {
464+ param := c .Query ("repo" )
465+ randomIndex := rand .Intn (len (maps .Keys (ghStatClients )))
466+ clientKey := c .Query ("client" , maps .Keys (ghStatClients )[randomIndex ])
467+ forceRefetch := c .Query ("forceRefetch" , "false" ) == "true"
468+
469+ client , ok := ghStatClients [clientKey ]
470+ if ! ok {
471+ return c .Status (404 ).SendString ("Resource not found" )
472+ }
473+
474+ repo , err := url .QueryUnescape (param )
475+ if err != nil {
476+ return err
477+ }
478+
479+ // needed because c.Query cannot be used as a map key
480+ repo = fmt .Sprintf ("%s" , repo )
481+ repo = strings .ToLower (repo )
482+
483+ ip := c .Get ("X-Forwarded-For" )
484+
485+ // If X-Forwarded-For is empty, fallback to RemoteIP
486+ if ip == "" {
487+ ip = c .IP ()
488+ }
489+
490+ userAgent := c .Get ("User-Agent" )
491+ log .Printf ("Forks Request from IP: %s, Repo: %s User-Agent: %s\n " , ip , repo , userAgent )
492+
493+ if strings .Contains (userAgent , "python-requests" ) {
494+ return c .Status (404 ).SendString ("Custom 404 Error: Resource not found" )
495+ }
496+
497+ span := trace .SpanFromContext (c .UserContext ())
498+ span .SetAttributes (attribute .String ("github.repo" , repo ))
499+ span .SetAttributes (attribute .String ("caller.ip" , ip ))
500+
501+ if forceRefetch {
502+ cacheForks .Delete (repo )
503+ }
504+
505+ if res , hit := cacheForks .Get (repo ); hit {
506+ return c .JSON (res )
507+ }
508+
509+ // if another request is already getting the data, skip and rely on SSE updates
510+ if _ , hit := onGoingForks [repo ]; hit {
511+ return c .SendStatus (fiber .StatusNoContent )
512+ }
513+
514+ onGoingForks [repo ] = true
515+
516+ updateChannel := make (chan int )
517+ var allForks []stats.ForksPerDay
518+
519+ eg , ctx := errgroup .WithContext (ctx )
520+
521+ eg .Go (func () error {
522+ allForks , err = client .GetAllForksHistory (ctx , repo , updateChannel )
523+ if err != nil {
524+ return err
525+ }
526+ return nil
527+ })
528+
529+ for progress := range updateChannel {
530+ // fmt.Printf("Progress: %d\n", progress)
531+
532+ wg := & sync.WaitGroup {}
533+
534+ for _ , s := range currentSessions .Sessions {
535+ wg .Add (1 )
536+ go func (cs * session.Session ) {
537+ defer wg .Done ()
538+ if cs .Repo == repo {
539+ cs .StateChannel <- progress
540+ }
541+ }(s )
542+ }
543+ wg .Wait ()
544+ }
545+
546+ if err := eg .Wait (); err != nil {
547+ delete (onGoingForks , repo )
548+ return err
549+ }
550+
551+ // defer close(updateChannel)
552+
553+ res := ForksWithStatsResponse {
554+ Forks : allForks ,
555+ }
556+
557+ now := time .Now ()
558+ nextDay := now .UTC ().Truncate (24 * time .Hour ).Add (DAY_CACHED * 24 * time .Hour )
559+ durationUntilEndOfDay := nextDay .Sub (now )
560+
561+ cacheForks .Set (repo , res , cache .WithExpiration (durationUntilEndOfDay ))
562+ delete (onGoingForks , repo )
563+
564+ return c .JSON (res )
565+ })
566+
457567 app .Get ("/limits" , func (c * fiber.Ctx ) error {
458568 client , ok := ghStatClients ["PAT" ]
459569 if ! ok {
0 commit comments