Skip to main content

lib/skuld/coroutine/foreign_suspensions.ex

defmodule Skuld.Coroutine.ForeignSuspensions do
  @moduledoc """
  Aggregate of all pending foreign suspensions, returned to the caller
  (e.g., the Hologram JS runtime) when the FiberPool scheduler exhausts
  internal work.

  The caller extracts the individual `ForeignSuspend` values from the
  `:suspensions` list, resolves them via the foreign platform's event loop,
  and resumes the computation by calling `Coroutine.call/2` with a map of
  `%{suspend_id => resolved_value}`.

  The `:resume` field is a closure created by the FiberPool scheduler that
  captures the `FiberPoolState` and scheduler loop. It takes the resolved
  map, wakes all resolved fibers at once by enqueuing them on the run queue,
  and returns the next Coroutine state.

  ## Fields

  - `id` — identifier for this aggregate
  - `suspensions` — the list of `ForeignSuspend` values to resolve
  - `env` — the computation environment
  - `resume` — closure `(%{id => value}) -> Coroutine.t()` that batch-wakes fibers
  """

  alias Skuld.Comp.Env
  alias Skuld.Comp.ForeignSuspend

  @type t :: %__MODULE__{
          id: term(),
          suspensions: [ForeignSuspend.t()],
          env: Env.t(),
          resume: (map() -> t())
        }

  defstruct [:id, :suspensions, :env, :resume]

  defimpl Skuld.Comp.ISentinel do
    def run(suspensions, env) do
      {suspensions, env}
    end

    def run!(%Skuld.Coroutine.ForeignSuspensions{}) do
      raise "Computation suspended on foreign resources — must be handled by a foreign scheduler"
    end

    def sentinel?(_), do: true
    def suspend?(_), do: true
    def error?(_), do: false

    def serializable_payload(%Skuld.Coroutine.ForeignSuspensions{}) do
      raise "ForeignSuspensions cannot be serialized — payloads are opaque foreign-platform handles"
    end
  end
end