neiam
/apollos_crib
It's Apollo's Crib
public · Issues · Pulls 1 · Labels · Forks · Compare · Actions failure · Badges · Packages
⭐
Log in to mark this repository.
Stop the offering page loading a whole country every ten seconds
17836dc · Gabriel Morell · 2026-09-07 02:58
Message
{commit_body(@commit)}
Files changed
modified
apps/room_sanctum/lib/room_sanctum/storage.ex
+18
−0
@@ -1057,6 +1057,24 @@ defmodule RoomSanctum.Storage do
| 1057 | 1057 | |> Repo.all() |
| 1058 | 1058 | end |
| 1059 | 1059 | |
| 1060 | + @doc """ | |
| 1061 | + The first `limit` stops of a source, for callers that cannot use all of them. | |
| 1062 | + | |
| 1063 | + A map is the case this exists for. Germany's feed has stops by the hundred | |
| 1064 | + thousand, and drawing them is neither possible nor useful -- but *fetching* | |
| 1065 | + them is what actually broke: the offering page reloaded every stop on a ten | |
| 1066 | + second timer, which at that size took longer than the fifteen second query | |
| 1067 | + timeout and killed the LiveView on a loop. | |
| 1068 | + | |
| 1069 | + Ordered, so the same stops come back each time rather than whatever the | |
| 1070 | + planner felt like -- a map whose markers reshuffle every tick is worse than | |
| 1071 | + one showing a stable subset. | |
| 1072 | + """ | |
| 1073 | + def list_stops(source_id, limit) when is_integer(limit) do | |
| 1074 | + from(p in Stop, where: p.source_id == ^source_id, order_by: p.stop_id, limit: ^limit) | |
| 1075 | + |> Repo.all() | |
| 1076 | + end | |
| 1077 | + | |
| 1060 | 1078 | def list_stops(source_id, search_term) do |
| 1061 | 1079 | from(p in Stop, |
| 1062 | 1080 | where: |
modified
apps/room_sanctum/lib/room_sanctum_web/live/cfg_source_live/show.ex
+9
−0
@@ -30,6 +30,10 @@ defmodule RoomSanctumWeb.SourceLive.Show do
| 30 | 30 | |> assign(:vehicle_positions, []) |
| 31 | 31 | |> assign(:free_bikes, []) |
| 32 | 32 | |> assign(:stations, []) |
| 33 | + # Set here as well as on the tick: the dead render happens before the | |
| 34 | + # first tick, and a template reading an assign that is not there yet is | |
| 35 | + # a KeyError on the very first paint. | |
| 36 | + |> assign(:station_count, 0) | |
| 33 | 37 | # Both are otherwise only set by the :update_sec tick 200ms in, so any |
| 34 | 38 | # view touching them could be rendered before they exist. |
| 35 | 39 | |> assign(:station_statuses, []) |
@@ -76,6 +80,10 @@ defmodule RoomSanctumWeb.SourceLive.Show do
| 76 | 80 | |
| 77 | 81 | stations = Stations.for_source(socket.assigns.source) |
| 78 | 82 | |
| 83 | + # What the map is not showing. Counted rather than inferred from the list, | |
| 84 | + # which cannot tell a source with exactly the cap from one with far more. | |
| 85 | + station_count = Stations.count_for_source(socket.assigns.source) | |
| 86 | + | |
| 79 | 87 | # Add station status for GBFS sources |
| 80 | 88 | station_statuses = Stations.statuses_for_source(socket.assigns.source) |
| 81 | 89 |
@@ -104,6 +112,7 @@ defmodule RoomSanctumWeb.SourceLive.Show do
| 104 | 112 | |> assign(:available_tints, available_tints) |
| 105 | 113 | |> assign(:free_bikes, free_bikes) |
| 106 | 114 | |> assign(:stations, stations) |
| 115 | + |> assign(:station_count, station_count) | |
| 107 | 116 | |> assign(:station_statuses, station_statuses) |
| 108 | 117 | |> assign(:route_types, route_types) |
| 109 | 118 | |> assign(:aircraft, aircraft) |
modified
apps/room_sanctum/lib/room_sanctum_web/live/cfg_source_live/show.html.heex
+6
−0
@@ -491,6 +491,12 @@ gl_available_projects={@gitlab_available_projects}
| 491 | 491 | add_query_event="map-add-query" |
| 492 | 492 | queried_station_ids={Enum.map(@queries, &station_id_of/1)} |
| 493 | 493 | /> |
| 494 | + <%!-- A national feed has stops by the hundred thousand. | |
| 495 | + The map draws a bounded subset; saying so is better | |
| 496 | + than a map that looks complete and is not. --%> | |
| 497 | + <p :if={@station_count > length(@stations)} class="text-xs opacity-60 mt-1"> | |
| 498 | + Showing <%= length(@stations) %> of <%= @station_count %> stops. | |
| 499 | + </p> | |
| 494 | 500 | </div> |
| 495 | 501 | |
| 496 | 502 | <% @view_mode == :paint -> %> |
modified
apps/room_sanctum/lib/room_sanctum_web/live/cfg_source_live/stations.ex
+26
−1
@@ -25,6 +25,31 @@ defmodule RoomSanctumWeb.SourceLive.Stations do
| 25 | 25 | |
| 26 | 26 | @mappable [:gtfs, :gbfs, :aqi] |
| 27 | 27 | |
| 28 | + # How many stops one source may contribute to a map. | |
| 29 | + # | |
| 30 | + # A national feed has hundreds of thousands, and no map draws that -- but the | |
| 31 | + # cost that mattered was fetching them: the offering page reloads its | |
| 32 | + # stations on a ten second timer, and for Germany that query outran the | |
| 33 | + # fifteen second timeout and took the LiveView down with it, every tick. | |
| 34 | + # | |
| 35 | + # Two thousand is well past what is legible on a map and small enough that | |
| 36 | + # the query is never the slow part. `count_for_source/1` says how many there | |
| 37 | + # really are, so a capped map can admit to it rather than quietly lying about | |
| 38 | + # the size of the feed. | |
| 39 | + @map_cap 2000 | |
| 40 | + | |
| 41 | + @doc "The cap `for_source/1` applies to a single source's stops." | |
| 42 | + def map_cap, do: @map_cap | |
| 43 | + | |
| 44 | + @doc """ | |
| 45 | + How many places a source actually has, capped or not. | |
| 46 | + | |
| 47 | + Counted in the database rather than by measuring what came back, which | |
| 48 | + cannot tell a source with exactly the cap from one with far more. | |
| 49 | + """ | |
| 50 | + def count_for_source(%{type: :gtfs, id: id}), do: Storage.count_stops(id) | |
| 51 | + def count_for_source(source), do: source |> for_source() |> length() | |
| 52 | + | |
| 28 | 53 | @doc "Whether a source has geography of its own to draw." |
| 29 | 54 | def mappable?(%{type: type}), do: type in @mappable |
| 30 | 55 | def mappable?(_), do: false |
@@ -40,7 +65,7 @@ defmodule RoomSanctumWeb.SourceLive.Stations do
| 40 | 65 | def for_source(%{type: :gbfs, id: id}), do: Storage.list_gbfs_station_information(id) |
| 41 | 66 | |
| 42 | 67 | def for_source(%{type: :gtfs, id: id}), |
| 43 | − do: id |> Storage.list_stops() |> Enum.map(&stop_as_station/1) | |
| 68 | + do: id |> Storage.list_stops(@map_cap) |> Enum.map(&stop_as_station/1) | |
| 44 | 69 | |
| 45 | 70 | def for_source(%{type: :aqi, id: id}), |
| 46 | 71 | do: id |> Storage.list_aqi_stations() |> Enum.map(&site_as_station/1) |
Parents: f7912e4