@@ -2,9 +2,38 @@ defmodule Ftpdb.DB do
22 require Logger
33
44 @ random_project_batch_size 100
5+ @ random_devlog_batch_size 100
56 @ random_project_cache_ttl :timer . minutes ( 30 )
67 @ default_project_banner_url "https://flavortown.hackclub.com/assets/default-banner-3d4e1b67.png"
78
9+ def warm_random_caches do
10+ [
11+ { :random_project_cache , random_project_batch_key ( 0 ) ,
12+ fn -> fetch_recent_project_batch ( 0 ) end } ,
13+ { :random_devlog_cache , random_devlog_batch_key ( 0 ) , fn -> fetch_recent_devlog_batch ( 0 ) end }
14+ ]
15+ |> Task . async_stream (
16+ fn { cache_name , key , fetcher } ->
17+ Cachex . fetch! (
18+ cache_name ,
19+ key ,
20+ fn _key -> fetcher . ( ) end ,
21+ expiration: @ random_project_cache_ttl
22+ )
23+ end ,
24+ max_concurrency: 2 ,
25+ ordered: false ,
26+ timeout: :infinity
27+ )
28+ |> Stream . run ( )
29+
30+ :ok
31+ rescue
32+ exception ->
33+ Logger . warning ( "Failed to warm random caches on startup: #{ Exception . message ( exception ) } " )
34+ :ok
35+ end
36+
837 defp project_duration_fields ( duration_seconds ) do
938 duration_seconds = duration_seconds || 0
1039
@@ -627,23 +656,61 @@ defmodule Ftpdb.DB do
627656 end )
628657 end
629658
630- defp get_max_devlog_id do
631- { :ok , response } =
632- Supabase.PostgREST . from ( client ( ) , "devlogs" )
633- |> Supabase.PostgREST . select ( [ "id" ] )
634- |> Supabase.PostgREST . order ( "id" , desc: true )
635- |> Supabase.PostgREST . limit ( 1 )
636- |> Map . put ( :method , :get )
637- |> Supabase.PostgREST . execute ( )
659+ def random_devlogs ( limit \\ 25 , excluded_devlog_ids \\ [ ] )
638660
639- case response . body do
640- [ % { "id" => max_id } ] -> max_id
641- _ -> 0
661+ def random_devlogs ( limit , excluded_devlog_ids )
662+ when is_integer ( limit ) and limit > 0 and is_list ( excluded_devlog_ids ) do
663+ excluded_devlog_ids =
664+ excluded_devlog_ids
665+ |> Enum . map ( & to_string / 1 )
666+ |> MapSet . new ( )
667+
668+ limit
669+ |> build_random_devlog_pool ( 0 , [ ] , excluded_devlog_ids )
670+ |> Enum . take_random ( limit )
671+ end
672+
673+ def random_devlogs ( _limit , _excluded_devlog_ids ) , do: [ ]
674+
675+ defp build_random_devlog_pool ( limit , _batch_index , acc , _excluded_devlog_ids )
676+ when length ( acc ) >= limit ,
677+ do: acc
678+
679+ defp build_random_devlog_pool ( limit , batch_index , acc , excluded_devlog_ids ) do
680+ batch =
681+ Cachex . fetch! (
682+ :random_devlog_cache ,
683+ random_devlog_batch_key ( batch_index ) ,
684+ fn _key -> fetch_recent_devlog_batch ( batch_index ) end ,
685+ expiration: @ random_project_cache_ttl
686+ )
687+
688+ case batch do
689+ [ ] ->
690+ acc
691+
692+ devlogs ->
693+ filtered_devlogs =
694+ Enum . reject ( devlogs , fn devlog ->
695+ MapSet . member? ( excluded_devlog_ids , to_string ( devlog . id ) )
696+ end )
697+
698+ build_random_devlog_pool (
699+ limit ,
700+ batch_index + 1 ,
701+ acc ++ filtered_devlogs ,
702+ excluded_devlog_ids
703+ )
642704 end
643705 end
644706
645- defp fetch_and_format_devlog ( id ) do
646- result =
707+ defp random_devlog_batch_key ( batch_index ) , do: "recent_devlogs_batch:#{ batch_index } "
708+
709+ defp fetch_recent_devlog_batch ( batch_index ) do
710+ start_index = batch_index * @ random_devlog_batch_size
711+ end_index = start_index + @ random_devlog_batch_size - 1
712+
713+ { :ok , response } =
647714 Supabase.PostgREST . from ( client ( ) , "devlogs" )
648715 |> Supabase.PostgREST . select ( [
649716 "id" ,
@@ -654,103 +721,101 @@ defmodule Ftpdb.DB do
654721 "media_urls" ,
655722 "project_id"
656723 ] )
657- |> Supabase.PostgREST . eq ( "id" , id )
658724 |> then ( fn req ->
659725 Supabase.Fetcher.Request . with_query ( req , % { "project_id" => "not.is.null" } )
660726 end )
727+ |> Supabase.PostgREST . order ( "created_at" , desc: true )
728+ |> Supabase.PostgREST . range ( start_index , end_index )
661729 |> Map . put ( :method , :get )
662730 |> Supabase.PostgREST . execute ( )
663731
664- case result do
665- { :ok , % { body: [ item ] } } when not is_nil ( item ) ->
666- duration = item [ "duration_seconds" ] || 0
667- media_urls = item [ "media_urls" ] || [ ]
668- project_id = item [ "project_id" ]
669-
670- # Fetch project
671- { :ok , project_resp } =
672- Supabase.PostgREST . from ( client ( ) , "projects" )
673- |> Supabase.PostgREST . select ( [ "title" , "banner_url" ] )
674- |> Supabase.PostgREST . eq ( "id" , project_id )
675- |> Map . put ( :method , :get )
676- |> Supabase.PostgREST . execute ( )
677-
678- { project_title , project_banner } =
679- case project_resp . body do
680- [ p ] -> { p [ "title" ] || "Unknown" , p [ "banner_url" ] }
681- _ -> { "Unknown" , nil }
682- end
732+ response . body
733+ |> List . wrap ( )
734+ |> Task . async_stream ( & format_recent_devlog_for_random / 1 ,
735+ max_concurrency: System . schedulers_online ( ) * 2 ,
736+ ordered: false ,
737+ timeout: :infinity
738+ )
739+ |> Enum . reduce ( [ ] , fn
740+ { :ok , nil } , acc ->
741+ acc
683742
684- # Fetch user
685- user_id = get_user_id ( project_id )
686-
687- { user_avatar , user_display_name } =
688- if user_id do
689- case get_user_info ( user_id ) do
690- [ u ] -> { u . avatar_url , u . display_name || "Unknown User" }
691- _ -> { nil , "Unknown User" }
692- end
693- else
694- { nil , "Unknown User" }
743+ { :ok , devlog } , acc ->
744+ [ devlog | acc ]
745+
746+ { :exit , reason } , acc ->
747+ Logger . warning ( "Failed to build random devlog batch #{ batch_index } : #{ inspect ( reason ) } " )
748+ acc
749+ end )
750+ |> Enum . reverse ( )
751+ end
752+
753+ defp format_recent_devlog_for_random ( item ) do
754+ duration = item [ "duration_seconds" ] || 0
755+ media_urls = item [ "media_urls" ] || [ ]
756+ project_id = item [ "project_id" ]
757+
758+ with project_id when not is_nil ( project_id ) <- project_id ,
759+ % { title: project_title , banner_url: project_banner } <-
760+ cached_random_devlog_project ( project_id ) do
761+ user_id = cached_user_id ( project_id )
762+
763+ { user_avatar , user_display_name } =
764+ if user_id do
765+ case cached_user_info ( user_id ) do
766+ [ user_info ] -> { user_info . avatar_url , user_info . display_name || "Unknown User" }
767+ _ -> { nil , "Unknown User" }
695768 end
769+ else
770+ { nil , "Unknown User" }
771+ end
696772
773+ % {
774+ id: item [ "id" ] ,
775+ body: item [ "body" ] ,
776+ total_hours: div ( duration , 3600 ) ,
777+ comments_count: item [ "comments_count" ] || 0 ,
778+ created_at: item [ "created_at" ] ,
779+ media_urls: media_urls ,
780+ project_id: project_id ,
781+ project_title: project_title ,
782+ project_banner: project_banner ,
783+ project_avatar: user_avatar ,
784+ user_id: user_id ,
785+ user_avatar: user_avatar ,
786+ user_display_name: user_display_name
787+ }
788+ else
789+ _ -> nil
790+ end
791+ end
792+
793+ defp cached_random_devlog_project ( project_id ) do
794+ Cachex . fetch! (
795+ :random_devlog_cache ,
796+ "devlog_project_summary:#{ project_id } " ,
797+ fn _key -> fetch_random_devlog_project ( project_id ) end ,
798+ expiration: @ random_project_cache_ttl
799+ )
800+ end
801+
802+ defp fetch_random_devlog_project ( project_id ) do
803+ { :ok , response } =
804+ Supabase.PostgREST . from ( client ( ) , "projects" )
805+ |> Supabase.PostgREST . select ( [ "title" , "banner_url" ] )
806+ |> Supabase.PostgREST . eq ( "id" , project_id )
807+ |> Map . put ( :method , :get )
808+ |> Supabase.PostgREST . execute ( )
809+
810+ case response . body do
811+ [ project ] ->
697812 % {
698- id: item [ "id" ] ,
699- body: item [ "body" ] ,
700- total_hours: div ( duration , 3600 ) ,
701- comments_count: item [ "comments_count" ] || 0 ,
702- created_at: item [ "created_at" ] ,
703- media_urls: media_urls ,
704- project_id: project_id ,
705- project_title: project_title ,
706- project_banner: project_banner ,
707- project_avatar: user_avatar ,
708- user_id: user_id ,
709- user_avatar: user_avatar ,
710- user_display_name: user_display_name
813+ title: project [ "title" ] || "Unknown" ,
814+ banner_url: project [ "banner_url" ]
711815 }
712816
713817 _ ->
714818 nil
715819 end
716820 end
717-
718- def random_devlogs do
719- max_id = get_max_devlog_id ( )
720-
721- if max_id == 0 do
722- [ ]
723- else
724- # We need 25 devlogs, but some IDs might be missing or without project_id
725- # Generate more IDs to ensure we get 25 valid ones.
726- random_ids =
727- 1 .. 200
728- |> Enum . map ( fn _ -> :rand . uniform ( max_id ) end )
729- |> Enum . uniq ( )
730-
731- random_ids
732- |> Enum . reduce_while ( [ ] , fn id , acc ->
733- if length ( acc ) >= 25 do
734- { :halt , acc }
735- else
736- devlog =
737- Cachex . fetch! (
738- :random_devlog_cache ,
739- to_string ( id ) ,
740- fn _key ->
741- fetch_and_format_devlog ( id )
742- end ,
743- expiration: :timer . minutes ( 30 )
744- )
745-
746- if devlog do
747- { :cont , [ devlog | acc ] }
748- else
749- { :cont , acc }
750- end
751- end
752- end )
753- |> Enum . sort_by ( fn devlog -> devlog . created_at end , :desc )
754- end
755- end
756821end
0 commit comments