Packages
fnord
0.8.77
0.9.40
0.9.39
0.9.38
0.9.37
0.9.36
0.9.35
0.9.34
0.9.33
0.9.32
0.9.31
0.9.30
0.9.29
0.9.28
0.9.27
0.9.26
0.9.25
0.9.24
0.9.23
0.9.22
0.9.21
0.9.20
0.9.19
0.9.18
0.9.17
0.9.16
0.9.15
0.9.14
0.9.13
0.9.12
0.9.11
0.9.10
0.9.9
0.9.8
0.9.7
0.9.6
0.9.5
0.9.4
0.9.3
0.9.2
0.9.1
0.9.0
0.8.99
0.8.98
0.8.97
0.8.96
0.8.95
0.8.94
0.8.93
0.8.92
0.8.91
0.8.90
0.8.89
0.8.88
0.8.87
0.8.86
0.8.85
0.8.84
0.8.83
0.8.82
0.8.81
0.8.80
0.8.79
0.8.78
0.8.77
0.8.76
0.8.75
0.8.74
0.8.73
0.8.72
0.8.71
0.8.70
0.8.69
0.8.68
0.8.67
0.8.66
0.8.65
0.8.64
0.8.63
0.8.62
0.8.61
0.8.60
0.8.59
0.8.58
0.8.57
0.8.56
0.8.55
0.8.54
0.8.53
0.8.52
0.8.51
0.8.50
0.8.49
0.8.48
0.8.47
0.8.46
0.8.45
0.8.44
0.8.43
0.8.42
0.8.41
0.8.40
0.8.39
0.8.38
0.8.37
0.8.36
0.8.35
0.8.34
0.8.33
0.8.32
0.8.31
0.8.30
0.8.29
0.8.27
0.8.26
0.8.25
0.8.24
0.8.23
0.8.22
0.8.21
0.8.20
0.8.19
0.8.18
0.8.17
0.8.16
0.8.15
0.8.14
0.8.13
0.8.12
0.8.11
0.8.1
0.8.0
0.7.24
0.7.23
0.7.22
0.7.21
0.7.20
0.7.19
0.7.18
0.7.17
0.7.16
0.7.15
0.7.14
0.7.13
0.7.12
0.7.11
0.7.10
0.7.9
0.7.8
0.7.7
0.7.6
0.7.5
0.7.3
0.7.2
0.7.1
0.7.0
0.6.9
0.6.8
0.6.7
0.6.6
0.6.5
0.6.4
0.6.3
0.6.1
0.6.0
0.5.9
0.5.8
0.5.7
0.5.6
0.5.5
0.5.4
0.5.3
0.5.2
0.5.1
0.5.0
0.4.44
0.4.43
0.4.42
0.4.41
0.4.40
0.4.39
0.4.38
0.4.37
0.4.36
0.4.35
0.4.34
0.4.33
0.4.32
0.4.30
0.4.29
0.4.28
0.4.27
0.4.26
0.4.25
0.4.24
0.4.23
0.4.22
0.4.21
0.4.20
0.4.19
0.4.18
0.4.17
0.4.16
0.4.15
0.4.14
0.4.13
0.4.12
0.4.11
0.4.10
0.4.9
0.4.8
0.4.7
0.4.6
0.4.5
0.4.4
0.4.3
0.4.2
0.4.1
0.4.0
0.3.0
0.2.0
0.1.0
AI code archaeology
Current section
Files
Jump to
Current section
Files
lib/services/task.ex
defmodule Services.Task do
defstruct [
:next_id,
:lists
]
@type list_id :: non_neg_integer
@type task_id :: binary
@type task_data :: any
@type task_result :: any
@type task :: %{
id: task_id,
outcome: :todo | :done | :failed,
data: task_data,
result: task_result | nil
}
use GenServer
# ----------------------------------------------------------------------------
# Client API
# ----------------------------------------------------------------------------
@spec start_link(any) :: GenServer.on_start()
def start_link(opts \\ []) do
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
end
@spec start_list() :: list_id
def start_list() do
GenServer.call(__MODULE__, :start_list)
end
@doc """
Returns all active task list IDs.
"""
@spec list_ids() :: [list_id]
def list_ids() do
GenServer.call(__MODULE__, :list_ids)
end
@doc """
Fetches all tasks for the given list in chronological (oldest-first) order.
Returns `{:error, :not_found}` if the list does not exist.
"""
@spec get_list(list_id) :: [task] | {:error, :not_found}
def get_list(list_id) do
GenServer.call(__MODULE__, {:get_list, list_id})
end
@doc """
Appends a new :todo task with the given ID and data to the end of the list.
If the list does not exist or the task ID already exists, this call is ignored.
"""
@spec add_task(list_id, task_id, task_data) :: :ok
def add_task(list_id, task_id, task_data) do
GenServer.cast(__MODULE__, {:add_task, list_id, task_id, task_data})
end
@doc """
Inserts a new :todo task with the given ID and data to the front of the list.
If the list does not exist or the task ID already exists, this call is ignored.
"""
@spec push_task(list_id, task_id, task_data) :: :ok
def push_task(list_id, task_id, task_data) do
GenServer.cast(__MODULE__, {:push_task, list_id, task_id, task_data})
end
@doc """
Marks the first task matching `task_id` as :done and stores the result.
Only the first matching ID is updated; others are unchanged. No-op if list or task not found.
"""
@spec complete_task(list_id, task_id, task_result) :: :ok
def complete_task(list_id, task_id, result) do
GenServer.cast(__MODULE__, {:resolve, list_id, task_id, :done, result})
end
@doc """
Marks the first task matching `task_id` as :failed and stores the result.
Only the first matching ID is updated; others are unchanged. No-op if list or task not found.
"""
@spec fail_task(list_id, task_id, task_result) :: :ok
def fail_task(list_id, task_id, msg) do
GenServer.cast(__MODULE__, {:resolve, list_id, task_id, :failed, msg})
end
@doc """
Returns `{:ok, task}` for the first :todo task in chronological order.
Returns `{:error, :empty}` if no pending tasks, or `{:error, :not_found}` if the list does not exist.
"""
@spec peek_task(list_id) :: {:ok, task} | {:error, :not_found} | {:error, :empty}
def peek_task(list_id) do
GenServer.call(__MODULE__, {:peek_task, list_id})
end
# ----------------------------------------------------------------------------
# Formatting
# ----------------------------------------------------------------------------
@spec as_string(list_id | list(task), boolean) :: binary
def as_string(subject, detail? \\ false)
def as_string(list_id, detail?) when is_integer(list_id) do
list_id
|> get_list()
|> case do
{:error, :not_found} ->
"List #{list_id} not found"
tasks ->
contents =
tasks
|> as_string(detail?)
|> String.trim()
"""
Task List #{list_id}:
#{contents}
"""
end
end
def as_string([], _), do: ""
def as_string([task | remaining], detail?) do
[
as_string(task, detail?),
as_string(remaining, detail?)
]
|> Enum.join("\n")
end
def as_string(%{id: id, outcome: :done, result: result}, true) do
"[✓] #{id}: #{result}"
end
def as_string(%{id: id, outcome: :failed, result: result}, true) do
"[✗] #{id}: #{result}"
end
def as_string(%{id: id, outcome: :todo}, _), do: "[ ] #{id}"
def as_string(%{id: id, outcome: :done}, _), do: "[✓] #{id}"
def as_string(%{id: id, outcome: :failed}, _), do: "[✗] #{id}"
# ----------------------------------------------------------------------------
# Server Callbacks
# ----------------------------------------------------------------------------
@impl true
def init(_) do
{:ok, %__MODULE__{next_id: 1, lists: %{}}}
end
@impl true
def handle_call(:start_list, _from, %{lists: lists, next_id: next_id} = state) do
{:reply, next_id,
%{
state
| lists: Map.put(lists, next_id, []),
next_id: next_id + 1
}}
end
@impl true
def handle_call(:list_ids, _from, state) do
{:reply, Map.keys(state.lists), state}
end
@impl true
def handle_call({:get_list, list_id}, _from, state) do
state.lists
|> Map.get(list_id)
|> case do
nil ->
{:reply, {:error, :not_found}, state}
tasks ->
{:reply, tasks, state}
end
end
@impl true
def handle_call({:peek_task, list_id}, _from, state) do
case Map.fetch(state.lists, list_id) do
:error ->
{:reply, {:error, :not_found}, state}
{:ok, tasks} ->
Enum.find(tasks, fn task -> task.outcome == :todo end)
|> case do
nil -> {:reply, {:error, :empty}, state}
task -> {:reply, {:ok, task}, state}
end
end
end
@impl true
def handle_cast({:push_task, list_id, task_id, task_data}, state) do
# Silently ignore operations on nonexistent lists
case Map.fetch(state.lists, list_id) do
:error ->
{:noreply, state}
{:ok, tasks} ->
if Enum.any?(tasks, &(&1.id == task_id)) do
{:noreply, state}
else
task = %{id: task_id, data: task_data, outcome: :todo, result: nil}
# Prepend new task to the front of the list
new_lists = Map.put(state.lists, list_id, [task | tasks])
{:noreply, %{state | lists: new_lists}}
end
end
end
@impl true
def handle_cast({:add_task, list_id, task_id, task_data}, state) do
# Silently ignore operations on nonexistent lists
case Map.fetch(state.lists, list_id) do
:error ->
{:noreply, state}
{:ok, tasks} ->
if Enum.any?(tasks, &(&1.id == task_id)) do
{:noreply, state}
else
task = %{id: task_id, data: task_data, outcome: :todo, result: nil}
# Append new task to preserve insertion order
new_lists = Map.put(state.lists, list_id, tasks ++ [task])
{:noreply, %{state | lists: new_lists}}
end
end
end
@impl true
def handle_cast({:resolve, list_id, task_id, outcome, result}, state) do
case Map.fetch(state.lists, list_id) do
:error ->
{:noreply, state}
{:ok, tasks} ->
{updated_tasks, _} = resolve_task(tasks, task_id, outcome, result)
new_lists = Map.put(state.lists, list_id, updated_tasks)
{:noreply, %{state | lists: new_lists}}
end
end
# ----------------------------------------------------------------------------
# Internals
# ----------------------------------------------------------------------------
@spec resolve_task([task()], task_id(), :done | :failed, task_result()) :: {[task()], boolean()}
defp resolve_task(tasks, target_id, outcome, result) do
# Split the list into tasks before the match and the rest
{before, rest} = Enum.split_while(tasks, &(&1.id != target_id))
case rest do
[] ->
# No matching task: return original list and a flag indicating no change
{tasks, false}
[first | tail] ->
# Update only the first matching task's outcome and result
updated = %{first | outcome: outcome, result: result}
{before ++ [updated | tail], true}
end
end
end