Skip to content

Commit 3343afd

Browse files
committed
Added far faster random devlogs
1 parent a29b294 commit 3343afd

3 files changed

Lines changed: 138 additions & 80 deletions

File tree

assets/js/app.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -563,6 +563,7 @@ function initHomePage() {
563563
function initProjectsPage() {
564564
let currentFilter = "random"
565565
let allProjects = []
566+
const seenProjectIds = new Set()
566567
let displayedCount = 0
567568
const itemsPerLoad = 24
568569
const defaultFetchLimit = 120
@@ -612,6 +613,11 @@ function initProjectsPage() {
612613

613614
async function fetchProjects(filter, limit) {
614615
const params = new URLSearchParams({filter, limit: String(limit)})
616+
617+
if (filter === "random" && seenProjectIds.size > 0) {
618+
params.set("exclude_ids", Array.from(seenProjectIds).join(","))
619+
}
620+
615621
const response = await fetch(`/api/random_projects?${params.toString()}`)
616622

617623
if (!response.ok) {
@@ -650,6 +656,8 @@ function initProjectsPage() {
650656
hideEmpty()
651657

652658
batch.forEach((p, idx) => {
659+
seenProjectIds.add(String(p.id))
660+
653661
const card = document.createElement("div")
654662
card.className = "project-card"
655663
card.onclick = () => viewProject(p.id)

lib/ftpdb/DB.ex

Lines changed: 120 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
defmodule 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

lib/ftpdb_web/controllers/api_controller.ex

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ defmodule FtpdbWeb.ApiController do
9797

9898
def random_projects(conn, %{"filter" => filter} = params) do
9999
limit = String.to_integer(Map.get(params, "limit", "10"))
100+
excluded_project_ids = parse_excluded_project_ids(params)
100101

101102
case filter do
102103
"hottest" ->
@@ -109,13 +110,20 @@ defmodule FtpdbWeb.ApiController do
109110
json(conn, Ftpdb.DB.most_active_projects(limit))
110111

111112
_ ->
112-
json(conn, Ftpdb.DB.random_projects(limit))
113+
json(conn, Ftpdb.DB.random_projects(limit, excluded_project_ids))
113114
end
114115
end
115116

116117
def random_projects(conn, params) do
117118
limit = String.to_integer(Map.get(params, "limit", "10"))
118-
json(conn, Ftpdb.DB.random_projects(limit))
119+
excluded_project_ids = parse_excluded_project_ids(params)
120+
json(conn, Ftpdb.DB.random_projects(limit, excluded_project_ids))
121+
end
122+
123+
defp parse_excluded_project_ids(params) do
124+
params
125+
|> Map.get("exclude_ids", "")
126+
|> String.split(",", trim: true)
119127
end
120128

121129
def random_devlogs(conn, _params) do

0 commit comments

Comments
 (0)