defprotocol Skuld.ForeignResolver do @moduledoc """ Protocol for resolving `ForeignSuspend` values across different platforms. The protocol dispatches on the `payload` field of the first `ForeignSuspend` in the list. Consumers implement this protocol for their payload type to resolve foreign suspensions via platform-specific mechanisms (e.g., JS Promises, OS I/O completion ports). The protocol uses continuation-passing style — the implementation calls `continuation.(resolved)` with a map of `%{suspend_id => resolved_value}` when ready, enabling both synchronous and asynchronous resolution. ## Example (synchronous, for tests) defmodule MyPayload do defstruct [:value] end defimpl Skuld.ForeignResolver, for: MyPayload do def await_resolutions(suspends, continuation) do resolved = Map.new(suspends, &{&1.id, &1.payload.value}) continuation.(resolved) end end ## Example (async, for Hologram) defimpl Skuld.ForeignResolver, for: Hologram.JS.PromiseRef do def await_resolutions(suspends, continuation) do promises = Enum.map(suspends, & &1.payload.promise) JS.Promise.all(promises).then(fn results -> resolved = suspends |> Enum.zip(results) |> Map.new(fn {s, val} -> {s.id, val} end) continuation.(resolved) end) end end """ @doc """ Resolve the given foreign suspensions and call the `continuation` with a map of `%{ForeignSuspend.id => resolved_value}`. The first parameter is any `ForeignSuspend.payload` value from the list, used for protocol dispatch. The full list of suspends is also passed so the implementation can batch-resolve them. The implementation must call `continuation.(resolved)` exactly once. It may do so synchronously or asynchronously. """ @spec await_resolutions( payload :: term(), [Skuld.Comp.ForeignSuspend.t()], (map() -> term()) ) :: term() def await_resolutions(payload, suspends, continuation) end defimpl Skuld.ForeignResolver, for: Atom do def await_resolutions(:skuld_foreign_resolver_fallback, _suspends, _continuation) do :ok end end defmodule Skuld.ForeignResolver.Runner do @moduledoc """ Resolution loop for `ForeignSuspend` values. Runs a computation through the `ForeignResolver` protocol until all foreign suspensions are resolved and the computation completes. ## Example comp |> FiberPool.with_handler() |> ForeignResolver.Runner.run() """ alias Skuld.Comp alias Skuld.Coroutine.Completed alias Skuld.Coroutine.ForeignSuspensions @doc """ Run a computation through the foreign resolution loop. Returns the computation's final result once all foreign suspensions are resolved. """ @spec run(Comp.Types.computation()) :: term() def run(comp) do {result, _env} = Comp.run(comp) case result do %ForeignSuspensions{} = fs -> resolve_loop(fs) other -> other end end defp resolve_loop(fs) do first_payload = hd(fs.suspensions).payload Skuld.ForeignResolver.await_resolutions(first_payload, fs.suspensions, fn resolved -> next = Skuld.Coroutine.call(fs, resolved) case next do %ForeignSuspensions{} = next_fs -> resolve_loop(next_fs) %Completed{result: result} -> result end end) end end