Skip to main content

lib/skuld/fiber_pool/pending_work.ex

# Fiber-local accumulations of pending fibers and tasks.
#
# This struct consolidates the loose keys that were previously stored
# separately in `env.state` for tracking work to be scheduled:
#
# - `fibers` - Fibers spawned by `FiberPool.fiber()` waiting to be scheduled
# - `tasks` - Tasks spawned by `FiberPool.task()` waiting to be spawned
#
# ## Lifecycle
#
# Unlike `ChannelCoordinationState` which is shared across all fibers, `PendingWork` is
# fiber-local. It's accumulated during fiber execution via effect handlers,
# then extracted and cleared by Comp.run (via Main.drain_pending) and Scheduler:
#
# 1. Fiber calls `FiberPool.fiber(comp)` → handler adds to `fibers`
# 2. Fiber calls `FiberPool.task(thunk)` → handler adds to `tasks`
# 3. Fiber step completes → Scheduler extracts pending work
# 4. PendingWork is cleared before seeding env to next fiber
#
# This is managed via `Comp.with_scoped_state` for proper scoping.
#
# ## Key
#
# Use `env_key/0` to get the key under which this struct is stored in env.state.
defmodule Skuld.FiberPool.PendingWork do
  @moduledoc false

  alias Skuld.Coroutine

  @doc """
  The key under which PendingWork is stored in env.state.
  """
  @spec env_key() :: module()
  def env_key, do: __MODULE__

  @type fiber_id :: reference()
  @type task_info :: {fiber_id(), (-> term()), keyword()}

  @type t :: %__MODULE__{
          fibers: [{fiber_id(), Coroutine.t()}],
          tasks: [task_info()]
        }

  defstruct fibers: [],
            tasks: []

  @doc """
  Create a new empty PendingWork.
  """
  @spec new() :: t()
  def new do
    %__MODULE__{}
  end

  #############################################################################
  ## Fiber Management
  #############################################################################

  @doc """
  Add a fiber to pending work. Called by FiberPool.fiber() handler.
  """
  @spec add_fiber(t(), fiber_id(), Coroutine.t()) :: t()
  def add_fiber(%__MODULE__{fibers: fibers} = pending, fiber_id, fiber) do
    %{pending | fibers: [{fiber_id, fiber} | fibers]}
  end

  @doc """
  Take all pending fibers, returning them and an updated PendingWork with empty fibers.
  """
  @spec take_fibers(t()) :: {[{fiber_id(), Coroutine.t()}], t()}
  def take_fibers(%__MODULE__{fibers: fibers} = pending) do
    {fibers, %{pending | fibers: []}}
  end

  @doc """
  Check if there are pending fibers.
  """
  @spec has_fibers?(t()) :: boolean()
  def has_fibers?(%__MODULE__{fibers: []}), do: false
  def has_fibers?(%__MODULE__{}), do: true

  #############################################################################
  ## Task Management
  #############################################################################

  @doc """
  Add a task to pending work. Called by FiberPool.task() handler.
  """
  @spec add_task(t(), task_info()) :: t()
  def add_task(%__MODULE__{tasks: tasks} = pending, task_info) do
    %{pending | tasks: [task_info | tasks]}
  end

  @doc """
  Take all pending tasks, returning them and an updated PendingWork with empty tasks.
  """
  @spec take_tasks(t()) :: {[task_info()], t()}
  def take_tasks(%__MODULE__{tasks: tasks} = pending) do
    {tasks, %{pending | tasks: []}}
  end

  @doc """
  Check if there are pending tasks.
  """
  @spec has_tasks?(t()) :: boolean()
  def has_tasks?(%__MODULE__{tasks: []}), do: false
  def has_tasks?(%__MODULE__{}), do: true

  #############################################################################
  ## Combined Operations
  #############################################################################

  @doc """
  Check if there is any pending work (fibers or tasks).
  """
  @spec empty?(t()) :: boolean()
  def empty?(%__MODULE__{fibers: [], tasks: []}), do: true
  def empty?(%__MODULE__{}), do: false

  @doc """
  Take all pending work, returning fibers, tasks, and an empty PendingWork.
  """
  @spec take_all(t()) :: {[{fiber_id(), Coroutine.t()}], [task_info()], t()}
  def take_all(%__MODULE__{fibers: fibers, tasks: tasks}) do
    {fibers, tasks, %__MODULE__{}}
  end
end