I am a per-session evaluation GenServer.
Each instance corresponds to a GT view's evaluation context
(LeSharedSnippetContext). All snippets within the same view
share one Eval process (same bindings).
I track object IDs registered in GtBridge.ObjectRegistry during
my lifetime. When I terminate (session closed), I batch-remove
all tracked objects from the registry.
Cleanup
GT's BeamSessionFinalizer sends POST /SESSION_CLOSE when the
per-view GtSharedVariablesBindings is GC'd (page/inspector closed).
The router calls EvalRegistry.remove/1 which terminates me, and
terminate/2 batch-removes all tracked objects from the registry.
Summary
Functions
Returns a specification to start this module under a supervisor.
I encode an evaluation result for the wire.
I evaluate code in a fresh process with no per-page bindings.
I evaluate in the calling process and return the value, so the answer can travel back in the HTTP response rather than as a second message.
I drain and return all messages received by the eval process.
I return the current bindings as a map of name→serialized value. Internal bindings (:port, :command_id, :pid) are filtered out. Non-primitive values are registered in ObjectRegistry.
I return documentation for a module, function, or type.
Remove an object from the registry. Called by GT when a proxy object is garbage collected.
Types
@type t() :: %GtBridge.Eval{ bindings: Code.binding() | nil, env: Macro.Env.t() | nil, port: non_neg_integer(), registered_ids: MapSet.t(non_neg_integer()) }
Functions
Returns a specification to start this module under a supervisor.
See Supervisor.
@spec complete(GenServer.server(), String.t(), String.t() | nil) :: [String.t()]
I encode an evaluation result for the wire.
I register complex values in GtBridge.ObjectRegistry and send an
%{exid, exclass} reference, so GT receives a proxy it can inspect
lazily rather than a fully materialised copy. Primitives travel by
value.
@spec eval(GenServer.server(), String.t(), String.t() | nil) :: any()
@spec eval_stateless(String.t(), String.t() | nil, pos_integer() | nil) :: :ok
I evaluate code in a fresh process with no per-page bindings.
GT-side evaluateAndWait calls that don't pass a sessionId (view-
block fetches, proxy-GC finalizers, browser fan-out queries) used
to land on a single shared "default" Eval GenServer, which
serialized them through one mailbox and cascaded any slow call
into a caller timeout storm.
Per-page snippet evals still use the session-bound eval/3 path so
bindings persist across snippets on the same page. This stateless
path is for everything else — parallelism is bounded only by the
BEAM scheduler, not by a shared GenServer.
Registered objects in ObjectRegistry live until GT GCs the
corresponding proxy and fires Eval.remove/1.
@spec eval_stateless_sync(String.t(), String.t() | nil, pos_integer() | nil) :: term()
I evaluate in the calling process and return the value, so the answer can travel back in the HTTP response rather than as a second message.
The Task in eval_stateless/3 existed to keep concurrent queries off
one shared mailbox; a request already runs in its own process, so
running inline gives the same isolation.
@spec flush() :: [term()]
I drain and return all messages received by the eval process.
Like IEx's flush/0. Useful when user code subscribes the eval
process to event brokers and you want to see what arrived.
flush()
@spec get_bindings(GenServer.server()) :: map()
I return the current bindings as a map of name→serialized value. Internal bindings (:port, :command_id, :pid) are filtered out. Non-primitive values are registered in ObjectRegistry.
I return documentation for a module, function, or type.
Bound as h in every eval session. Because I am a macro, I can
parse dot-syntax like h(Enum.map) and h(Enum.map/2).
h(Enum)
h(Enum.map)
h(Enum.map/2)
h({Enum, :map})
h({Enum, :map, 2})
@spec remove(non_neg_integer()) :: :ok
Remove an object from the registry. Called by GT when a proxy object is garbage collected.