# Batch executor registration and lookup. # # Executors are stored in env.state under a private key as a map of # `{batch_key_pattern => executor}`. Uses `Comp.scoped` for proper # scoping with automatic save/restore. # # Executors are functions that take a list of `{request_id, op}` tuples # and return a computation yielding `%{request_id => result}`. # # ## Pattern Matching # # Batch keys support wildcard matching with `:_`: # # # Exact match # comp # |> BatchExecutor.with_executor({:db_fetch, User}, user_executor) # # # Wildcard match - handles any schema # comp # |> BatchExecutor.with_executor({:db_fetch, :_}, generic_db_executor) # # Exact matches take precedence over wildcard matches. # # ## Example # # # Production - real DB executor # comp # |> BatchExecutor.with_executor({:db_fetch, :_}, &DB.Executors.fetch_executor/1) # |> Comp.run() # # # Test - mock executor # comp # |> BatchExecutor.with_executor({:db_fetch, User}, fn ops -> # Map.new(ops, fn {ref, _op} -> {ref, %User{id: 1}} end) # end) # |> Comp.run() defmodule Skuld.FiberPool.BatchExecutor do @moduledoc false alias Skuld.Comp alias Skuld.Comp.Env @state_key "Elixir.Skuld.FiberPool.BatchExecutor::executors" @type batch_key :: term() @type request_id :: reference() @type executor :: ([{request_id, term()}] -> Comp.Types.computation()) @type registry :: %{batch_key => executor} @doc """ Install a batch executor for a batch_key pattern in the current scope. The executor will be available for the duration of the computation and automatically removed when the scope exits. ## Parameters - `comp` - The computation to wrap - `batch_key` - The batch key pattern (supports `:_` wildcards) - `executor` - Function `([{request_id, op}] -> computation)` that executes the batch """ @spec with_executor(Comp.Types.computation(), batch_key, executor) :: Comp.Types.computation() def with_executor(comp, batch_key, executor) do Comp.scoped(comp, fn env -> previous = Env.get_state(env, @state_key, %{}) updated = Map.put(previous, batch_key, executor) {Env.put_state(env, @state_key, updated), fn value, e -> {value, Env.put_state(e, @state_key, previous)} end} end) end @doc """ Install multiple batch executors at once. ## Example comp |> BatchExecutor.with_executors([ {{:db_fetch, :_}, &DB.Executors.fetch_executor/1}, {{:db_fetch_all, :_, :_}, &DB.Executors.fetch_all_executor/1} ]) """ @spec with_executors(Comp.Types.computation(), [{batch_key, executor}]) :: Comp.Types.computation() def with_executors(comp, executors) do Enum.reduce(executors, comp, fn {key, exec}, acc -> with_executor(acc, key, exec) end) end @doc """ Look up executor for a batch_key. Tries exact match first, then falls back to wildcard pattern matching. Returns `nil` if no matching executor is found. """ @spec get_executor(Comp.Types.env(), batch_key) :: executor | nil def get_executor(env, batch_key) do registry = Env.get_state(env, @state_key, %{}) # Try exact match first case Map.get(registry, batch_key) do nil -> find_wildcard_match(registry, batch_key) executor -> executor end end @doc """ Get the current executor registry from env. Useful for debugging or introspection. """ @spec get_registry(Comp.Types.env()) :: registry def get_registry(env) do Env.get_state(env, @state_key, %{}) end # Match batch_key against patterns with :_ wildcards # e.g., {:db_fetch, User} matches {:db_fetch, :_} defp find_wildcard_match(registry, batch_key) do Enum.find_value(registry, fn {pattern, executor} -> if pattern_matches?(pattern, batch_key), do: executor end) end defp pattern_matches?(pattern, key) when is_tuple(pattern) and is_tuple(key) do pattern_list = Tuple.to_list(pattern) key_list = Tuple.to_list(key) length(pattern_list) == length(key_list) and Enum.zip(pattern_list, key_list) |> Enum.all?(fn {p, k} -> p == :_ or p == k end) end defp pattern_matches?(pattern, key), do: pattern == key end