Atui.Fetch (Atui v0.3.0)

Copy Markdown View Source

Runs a slow thing off the UI's process and sends the answer back to a view.

Atui.Runtime is a single process: it reads the keyboard, runs the view callbacks and paints the frame, one after another. Anything done inside a callback is therefore done instead of drawing, and a callback that waits on the network is a UI that has stopped — the keyboard included. A round trip that takes a fifth of a second is felt; one to a host that has gone away takes as long as its timeout, and the person at the keyboard cannot even quit.

So the work happens in a process of its own and the answer arrives as an ordinary event. Two details make that safe to do from a view:

  • The event is addressed to the view's module rather than to whichever view has focus, so an answer that lands while a popup is open still reaches the view that asked for it underneath.
  • The process cannot crash. Everything the function might raise, exit or throw is caught and handed back as {:error, message} — an unhandled exit would print a crash report onto a terminal the UI has drawn a frame on, which is a worse failure than whatever caused it.

Using it

Start it in the callback that decides something is needed, and handle the answer in Atui.View.handle_event/2:

def mount(opts) do
  {:ok, load(%{rows: [], loading?: true, error: nil})}
end

def handle_key({[:ctrl], "r"}, state), do: {:ok, load(state)}

def handle_event({:rows, {:ok, rows}}, state) do
  {:ok, %{state | rows: rows, loading?: false, error: nil}}
end

def handle_event({:rows, {:error, message}}, state) do
  {:ok, %{state | loading?: false, error: message}}
end

defp load(state) do
  Atui.Fetch.start(__MODULE__, :rows, fn -> Repo.slow_query() end)

  %{state | loading?: true}
end

A flag in the state is what makes the first frame draw before the answer is in, and what stops a second fetch being started while one is in flight.

Summary

Functions

Runs fun in another process and sends {tag, result} to module's view.

Functions

start(module, tag, fun)

@spec start(module(), atom(), (-> term())) :: :ok

Runs fun in another process and sends {tag, result} to module's view.

Call it from inside a view callback: it addresses the runtime the view is running in, which is the process making the call.

tag is how a view tells its fetches apart, and result is whatever fun answered — or {:error, message} if it could not answer at all.