Skip to content

Commit ff6532d

Browse files
authored
perf(inventory): load reorder forecast metrics asynchronously (#27)
* perf(inventory): load reorder forecast metrics async on mount * perf(inventory): recompute reorder metrics async on control changes
1 parent 7b7d3db commit ff6532d

2 files changed

Lines changed: 82 additions & 25 deletions

File tree

lib/craftplan_web/live/manage/inventory_live/reorder.ex

Lines changed: 36 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -272,7 +272,7 @@ defmodule CraftplanWeb.InventoryLive.ReorderPlanner do
272272
|> assign_forecast_defaults(session, settings)
273273
|> assign_advanced_defaults(settings)
274274

275-
{:ok, load_metrics(socket)}
275+
{:ok, maybe_start_metrics(socket)}
276276
end
277277

278278
defp assign_advanced_defaults(socket, settings) do
@@ -349,7 +349,7 @@ defmodule CraftplanWeb.InventoryLive.ReorderPlanner do
349349
|> maybe_update_planned_weight(params)
350350
|> maybe_update_min_samples(params)
351351

352-
{:noreply, load_metrics(socket)}
352+
{:noreply, start_metrics_load(socket)}
353353
end
354354

355355
@impl true
@@ -369,7 +369,7 @@ defmodule CraftplanWeb.InventoryLive.ReorderPlanner do
369369
)
370370
|> assign(:min_samples, Map.get(settings, :forecast_min_samples) || 10)
371371

372-
{:noreply, load_metrics(socket)}
372+
{:noreply, start_metrics_load(socket)}
373373
end
374374

375375
defp maybe_update_lookback_days(socket, %{"lookback_days" => value}) do
@@ -410,22 +410,18 @@ defmodule CraftplanWeb.InventoryLive.ReorderPlanner do
410410

411411
## Metrics loading
412412

413-
defp refresh_metrics(socket, assigns) do
414-
socket
415-
|> assign(assigns)
416-
|> load_metrics()
413+
defp maybe_start_metrics(socket) do
414+
if connected?(socket), do: start_metrics_load(socket), else: socket
417415
end
418416

419-
defp load_metrics(%{assigns: %{horizon_days: horizon}} = socket) when horizon <= 0 do
417+
defp start_metrics_load(%{assigns: %{horizon_days: horizon}} = socket) when horizon <= 0 do
420418
socket
421419
end
422420

423-
defp load_metrics(socket) do
421+
defp start_metrics_load(socket) do
424422
days_range = build_days_range(socket.assigns.today, socket.assigns.horizon_days)
425423
actor = socket.assigns[:current_user]
426424

427-
socket = assign(socket, :metrics_loaded?, false)
428-
429425
opts = [
430426
service_level: socket.assigns.service_level,
431427
lookback_days: socket.assigns.lookback_days,
@@ -434,24 +430,39 @@ defmodule CraftplanWeb.InventoryLive.ReorderPlanner do
434430
min_samples: socket.assigns.min_samples
435431
]
436432

437-
rows = InventoryForecasting.owner_grid_rows(days_range, opts, actor)
438-
439433
socket
440-
|> assign(:forecast_rows, rows)
441-
|> assign(:metrics_loaded?, true)
434+
|> assign(:metrics_loaded?, false)
442435
|> assign(:forecast_error, nil)
443436
|> assign(:days_range, days_range)
444-
rescue
445-
exception ->
446-
Logger.error("Unable to load owner forecast metrics: #{Exception.message(exception)}",
447-
exception: exception,
448-
stacktrace: __STACKTRACE__
449-
)
437+
|> cancel_async(:forecast_metrics)
438+
|> start_async(:forecast_metrics, fn ->
439+
InventoryForecasting.owner_grid_rows(days_range, opts, actor)
440+
end)
441+
end
450442

451-
socket
452-
|> assign(:forecast_rows, [])
453-
|> assign(:metrics_loaded?, false)
454-
|> assign(:forecast_error, "Unable to load forecast metrics right now.")
443+
@impl true
444+
def handle_async(:forecast_metrics, {:ok, rows}, socket) do
445+
{:noreply,
446+
socket
447+
|> assign(:forecast_rows, rows)
448+
|> assign(:metrics_loaded?, true)
449+
|> assign(:forecast_error, nil)}
450+
end
451+
452+
def handle_async(:forecast_metrics, {:exit, reason}, socket) do
453+
Logger.error("Unable to load owner forecast metrics: #{inspect(reason)}")
454+
455+
{:noreply,
456+
socket
457+
|> assign(:forecast_rows, [])
458+
|> assign(:metrics_loaded?, false)
459+
|> assign(:forecast_error, "Unable to load forecast metrics right now.")}
460+
end
461+
462+
defp refresh_metrics(socket, assigns) do
463+
socket
464+
|> assign(assigns)
465+
|> start_metrics_load()
455466
end
456467

457468
defp build_days_range(start_date, days) when days > 0 do
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
defmodule CraftplanWeb.ManageInventoryReorderLiveTest do
2+
# async: false + shared sandbox — the page computes metrics in a start_async
3+
# task; shared mode guarantees that task can reach the test's DB connection.
4+
use CraftplanWeb.ConnCase, async: false
5+
6+
import Phoenix.LiveViewTest
7+
8+
setup do
9+
Ecto.Adapters.SQL.Sandbox.mode(Craftplan.Repo, {:shared, self()})
10+
:ok
11+
end
12+
13+
describe "async metrics load" do
14+
@tag role: :staff
15+
test "mount returns immediately with the loading state, then resolves async", %{conn: conn} do
16+
{:ok, view, html} = live(conn, ~p"/manage/inventory/forecast/reorder")
17+
18+
# Mount did NOT block on the ~2s computation: spinner is shown.
19+
assert html =~ "Loading inventory metrics"
20+
21+
# Awaiting the async assign clears the spinner and renders the band
22+
# (empty state is fine with no seeded data).
23+
resolved = render_async(view)
24+
refute resolved =~ "Loading inventory metrics"
25+
assert resolved =~ "No forecast rows available" or resolved =~ "owner-metrics-band"
26+
end
27+
28+
@tag role: :staff
29+
test "changing the horizon recomputes asynchronously (no blocking)", %{conn: conn} do
30+
{:ok, view, _html} = live(conn, ~p"/manage/inventory/forecast/reorder")
31+
render_async(view)
32+
33+
clicked =
34+
view
35+
|> element(~s(button[phx-click="set_horizon"][phx-value-days="28"]))
36+
|> render_click()
37+
38+
# The toggle handler returned without blocking on the recompute:
39+
# the spinner is shown again.
40+
assert clicked =~ "Loading inventory metrics"
41+
42+
resolved = render_async(view)
43+
refute resolved =~ "Loading inventory metrics"
44+
end
45+
end
46+
end

0 commit comments

Comments
 (0)