11defmodule Ftpdb.DB do
22 require Logger
33
4+ @ random_project_batch_size 100
5+ @ random_project_cache_ttl :timer . minutes ( 30 )
6+ @ default_project_banner_url "https://flavortown.hackclub.com/assets/default-banner-3d4e1b67.png"
7+
48 defp project_duration_fields ( duration_seconds ) do
59 duration_seconds = duration_seconds || 0
610
@@ -258,23 +262,61 @@ defmodule Ftpdb.DB do
258262 end )
259263 end
260264
261- defp get_max_project_id do
262- { :ok , response } =
263- Supabase.PostgREST . from ( client ( ) , "projects" )
264- |> Supabase.PostgREST . select ( [ "id" ] )
265- |> Supabase.PostgREST . order ( "id" , desc: true )
266- |> Supabase.PostgREST . limit ( 1 )
267- |> Map . put ( :method , :get )
268- |> Supabase.PostgREST . execute ( )
265+ def random_projects ( limit \\ 10 , excluded_project_ids \\ [ ] )
269266
270- case response . body do
271- [ % { "id" => max_id } ] -> max_id
272- _ -> 0
267+ def random_projects ( limit , excluded_project_ids )
268+ when is_integer ( limit ) and limit > 0 and is_list ( excluded_project_ids ) do
269+ excluded_project_ids =
270+ excluded_project_ids
271+ |> Enum . map ( & to_string / 1 )
272+ |> MapSet . new ( )
273+
274+ limit
275+ |> build_random_project_pool ( 0 , [ ] , excluded_project_ids )
276+ |> Enum . take_random ( limit )
277+ end
278+
279+ def random_projects ( _limit , _excluded_project_ids ) , do: [ ]
280+
281+ defp build_random_project_pool ( limit , _batch_index , acc , _excluded_project_ids )
282+ when length ( acc ) >= limit ,
283+ do: acc
284+
285+ defp build_random_project_pool ( limit , batch_index , acc , excluded_project_ids ) do
286+ batch =
287+ Cachex . fetch! (
288+ :random_project_cache ,
289+ random_project_batch_key ( batch_index ) ,
290+ fn _key -> fetch_recent_project_batch ( batch_index ) end ,
291+ expiration: @ random_project_cache_ttl
292+ )
293+
294+ case batch do
295+ [ ] ->
296+ acc
297+
298+ projects ->
299+ filtered_projects =
300+ Enum . reject ( projects , fn project ->
301+ MapSet . member? ( excluded_project_ids , to_string ( project . id ) )
302+ end )
303+
304+ build_random_project_pool (
305+ limit ,
306+ batch_index + 1 ,
307+ acc ++ filtered_projects ,
308+ excluded_project_ids
309+ )
273310 end
274311 end
275312
276- defp fetch_and_format_project_for_random ( id ) do
277- result =
313+ defp random_project_batch_key ( batch_index ) , do: "recent_projects_batch:#{ batch_index } "
314+
315+ defp fetch_recent_project_batch ( batch_index ) do
316+ start_index = batch_index * @ random_project_batch_size
317+ end_index = start_index + @ random_project_batch_size - 1
318+
319+ { :ok , response } =
278320 Supabase.PostgREST . from ( client ( ) , "projects" )
279321 |> Supabase.PostgREST . select ( [
280322 "id" ,
@@ -284,84 +326,84 @@ defmodule Ftpdb.DB do
284326 "stat_hot_score" ,
285327 "stat_total_likes"
286328 ] )
287- |> Supabase.PostgREST . eq ( "id" , id )
329+ |> Supabase.PostgREST . order ( "id" , desc: true )
330+ |> Supabase.PostgREST . range ( start_index , end_index )
288331 |> Map . put ( :method , :get )
289332 |> Supabase.PostgREST . execute ( )
290333
291- case result do
292- { :ok , % { body: [ item ] } } when not is_nil ( item ) ->
293- banner_url = item [ "banner_url" ]
294-
295- if is_nil ( banner_url ) or
296- banner_url == "https://flavortown.hackclub.com/assets/default-banner-3d4e1b67.png" do
297- nil
298- else
299- user_id = get_user_id ( item [ "id" ] )
300-
301- { user_avatar , user_display_name } =
302- if user_id do
303- case get_user_info ( user_id ) do
304- [ u ] -> { u . avatar_url , u . display_name || "Unknown User" }
305- _ -> { nil , "Unknown User" }
306- end
307- else
308- { nil , "Unknown User" }
309- end
310-
311- duration = item [ "stat_total_duration_seconds" ] || 0
312-
313- % {
314- id: to_string ( item [ "id" ] ) ,
315- title: item [ "title" ] ,
316- banner_url: banner_url ,
317- display_name: user_display_name ,
318- avatar_url: user_avatar ,
319- stat_hot_score: item [ "stat_hot_score" ] || 0 ,
320- stat_total_likes: item [ "stat_total_likes" ] || 0 ,
321- total_duration_seconds: duration ,
322- total_hours: div ( duration , 3600 )
323- }
324- end
325-
326- _ ->
327- nil
328- end
334+ response . body
335+ |> List . wrap ( )
336+ |> Task . async_stream ( & format_recent_project_for_random / 1 ,
337+ max_concurrency: System . schedulers_online ( ) * 2 ,
338+ ordered: false ,
339+ timeout: :infinity
340+ )
341+ |> Enum . reduce ( [ ] , fn
342+ { :ok , nil } , acc ->
343+ acc
344+
345+ { :ok , project } , acc ->
346+ [ project | acc ]
347+
348+ { :exit , reason } , acc ->
349+ Logger . warning ( "Failed to build random project batch #{ batch_index } : #{ inspect ( reason ) } " )
350+ acc
351+ end )
352+ |> Enum . reverse ( )
329353 end
330354
331- def random_projects ( limit \\ 10 ) do
332- max_id = get_max_project_id ( )
355+ defp format_recent_project_for_random ( item ) do
356+ banner_url = item [ "banner_url" ]
333357
334- if max_id == 0 do
335- [ ]
358+ if is_nil ( banner_url ) or banner_url == @ default_project_banner_url do
359+ nil
336360 else
337- random_ids =
338- 1 .. ( limit * 10 )
339- |> Enum . map ( fn _ -> :rand . uniform ( max_id ) end )
340- |> Enum . uniq ( )
341-
342- random_ids
343- |> Enum . reduce_while ( [ ] , fn id , acc ->
344- if length ( acc ) >= limit do
345- { :halt , acc }
346- else
347- project =
348- Cachex . fetch! (
349- :random_project_cache ,
350- to_string ( id ) ,
351- fn _key -> fetch_and_format_project_for_random ( id ) end ,
352- expiration: :timer . minutes ( 30 )
353- )
361+ user_id = cached_user_id ( item [ "id" ] )
354362
355- if project do
356- { :cont , [ project | acc ] }
357- else
358- { :cont , acc }
363+ { user_avatar , user_display_name } =
364+ if user_id do
365+ case cached_user_info ( user_id ) do
366+ [ user_info ] -> { user_info . avatar_url , user_info . display_name || "Unknown User" }
367+ _ -> { nil , "Unknown User" }
359368 end
369+ else
370+ { nil , "Unknown User" }
360371 end
361- end )
372+
373+ duration = item [ "stat_total_duration_seconds" ] || 0
374+
375+ % {
376+ id: to_string ( item [ "id" ] ) ,
377+ title: item [ "title" ] ,
378+ banner_url: banner_url ,
379+ display_name: user_display_name ,
380+ avatar_url: user_avatar ,
381+ stat_hot_score: item [ "stat_hot_score" ] || 0 ,
382+ stat_total_likes: item [ "stat_total_likes" ] || 0 ,
383+ total_duration_seconds: duration ,
384+ total_hours: div ( duration , 3600 )
385+ }
362386 end
363387 end
364388
389+ defp cached_user_id ( project_id ) do
390+ Cachex . fetch! (
391+ :user_id_cache ,
392+ to_string ( project_id ) ,
393+ fn _key -> get_user_id ( project_id ) end ,
394+ expiration: @ random_project_cache_ttl
395+ )
396+ end
397+
398+ defp cached_user_info ( user_id ) do
399+ Cachex . fetch! (
400+ :user_cache ,
401+ to_string ( user_id ) ,
402+ fn _key -> get_user_info ( user_id ) end ,
403+ expiration: @ random_project_cache_ttl
404+ )
405+ end
406+
365407 def search_projects ( query ) when is_binary ( query ) do
366408 cleaned_query = String . trim ( query )
367409
0 commit comments