Exclosured.LiveView (exclosured v0.1.4)

Copy Markdown

LiveView integration for Exclosured WASM modules.

Provides call/5 to invoke WASM functions from a LiveView process, and sandbox/1 as a HEEx component for embedding WASM modules.

Declarative State Sync

The sandbox component supports a sync attribute that automatically pushes assign changes to the WASM module:

<Exclosured.LiveView.sandbox
  module={:renderer}
  sync={%{speed: @speed, color: @color}}
  canvas
/>

When @speed or @color changes, the new values are automatically pushed to the WASM module via wasm:state. No manual push_event calls needed.

Manual Usage

defmodule MyAppWeb.ProcessorLive do
  use Phoenix.LiveView

  def handle_event("run", %{"input" => input}, socket) do
    socket = Exclosured.LiveView.call(socket, :my_mod, "process", [input])
    {:noreply, socket}
  end

  def handle_info({:wasm_result, :my_mod, "process", result}, socket) do
    {:noreply, assign(socket, result: result)}
  end

  def handle_info({:wasm_emit, :my_mod, "progress", payload}, socket) do
    {:noreply, assign(socket, progress: payload)}
  end
end

Summary

Functions

Call a WASM function on the client. The result will arrive as a {:wasm_result, module, func, result} message via handle_info/2.

Push a state update to a WASM module.

HEEx component that renders a WASM sandbox container element.

Call a WASM function that streams results back incrementally.

Build a sync map from assigns using a list of keys.

Check if a WASM module has reported ready for this socket.

Functions

call(socket, module, func, args, opts \\ [])

Call a WASM function on the client. The result will arrive as a {:wasm_result, module, func, result} message via handle_info/2.

Options

  • :fallback - a function that receives the args list and returns a result. If WASM is not loaded yet, the fallback runs on the server and delivers the result via {:wasm_result, module, func, result}, the same shape as a WASM result. Your handle_info works identically either way.

Example

socket = Exclosured.LiveView.call(socket, :my_mod, "process", [text],
  fallback: fn [text] -> String.split(text) |> length() end
)

# This handler works regardless of whether WASM or fallback ran:
def handle_info({:wasm_result, :my_mod, "process", result}, socket) do
  {:noreply, assign(socket, result: result)}
end

push_state(socket, module, state)

Push a state update to a WASM module.

sandbox(assigns)

HEEx component that renders a WASM sandbox container element.

Attributes

  • module (required) - The WASM module name (atom)
  • id - Element ID (defaults to "wasm-{module}")
  • sync - Map of values to auto-sync to WASM on change (default: nil). When any value in the map changes between renders, the component pushes the entire sync map to the WASM module via wasm:state.
  • canvas - Whether to include a canvas element (default: false)
  • width - Canvas width (default: 800)
  • height - Canvas height (default: 600)
  • subscribe - List of broadcast channels to subscribe to
  • class - CSS class for the container

Attributes

  • module (:atom) (required)
  • id (:string) - Defaults to nil.
  • sync (:map) - Defaults to nil.
  • canvas (:boolean) - Defaults to false.
  • width (:integer) - Defaults to 800.
  • height (:integer) - Defaults to 600.
  • subscribe (:list) - Defaults to [].
  • class (:string) - Defaults to nil.

stream_call(socket, module, func, args, opts)

Call a WASM function that streams results back incrementally.

Instead of waiting for a single {:wasm_result, ...} message, this sets up handlers for streaming emit("chunk", ...) events from WASM. Each chunk triggers the on_chunk callback. When WASM emits "done", the on_done callback fires and the stream handler is cleaned up.

Options

  • :on_chunk (required) - fn payload, socket -> socket called for each chunk
  • :on_done - fn socket -> socket called when streaming completes (default: identity)
  • :chunk_event - event name WASM emits for chunks (default: "chunk")
  • :done_event - event name WASM emits on completion (default: "done")

Example

# In your LiveView:
def handle_event("analyze", %{"data" => data}, socket) do
  socket =
    socket
    |> Exclosured.LiveView.stream_call(:processor, "analyze", [data],
      on_chunk: fn chunk, socket ->
        update(socket, :results, &[chunk | &1])
      end,
      on_done: fn socket ->
        assign(socket, processing: false)
      end
    )

  {:noreply, assign(socket, processing: true, results: [])}
end

# In your Rust WASM:
# for item in data.chunks(100) {
#     exclosured::emit("chunk", &process(item));
# }
# exclosured::emit("done", "{}");

sync(assigns, keys)

Build a sync map from assigns using a list of keys.

Shorthand for building the sync attribute on the sandbox component. Bare atoms use the same name as the assign key. Keyword pairs map an assign to a different key name.

<%# Same-name shorthand: %>
sync={sync(assigns, ~w(frequency amplitude speed)a)}

<%# Mixed: four same-name, one renamed: %>
sync={sync(assigns, [:frequency, :amplitude, :speed, :color, wave: :wave_type])}

<%# The above produces: %>
%{frequency: @frequency, amplitude: @amplitude, speed: @speed,
  color: @color, wave: @wave_type}

wasm_ready?(socket, module)

Check if a WASM module has reported ready for this socket.