Messaging
Messaging is a custom RPC channel between a gadget’s React frontend and its WASM backend. It runs parallel to the search pipeline and lets you call arbitrary backend logic from your views or settings panels: fetching data from a database, validating input, triggering actions, or anything else your UI needs that the search interface doesn’t cover.
Messaging is optional. Gadgets that only use the standard search result list don’t need it.
Frontend side
Section titled “Frontend side”The sendMessage function is available through the useGadgetRuntime hook:
import { useGadgetRuntime } from "@torchsnap/gadget-sdk/hooks";
const { sendMessage } = useGadgetRuntime();The function signature is:
type GadgetSendMessage = <TPayload = unknown, TResult = unknown>( method: string, payload: TPayload,) => Promise<TResult>;method identifies which operation to invoke on the backend. payload is
serialized as JSON and passed to the backend’s handle_message. The returned
promise resolves with the parsed JSON response, or rejects with an error string.
A launcher view that sent the message for something the user did shows the
rejection in the launcher footer with useLauncher().showError(), see
Reporting errors.
Example
Section titled “Example”const { sendMessage } = useGadgetRuntime();
async function clearHistory() { await sendMessage("clear_history", {});}
async function loadPage(page: number) { const data = await sendMessage<{ page: number }, HistoryPage>( "get_history", { page }, ); setEntries(data.entries);}Backend side
Section titled “Backend side”On the Rust side, sendMessage calls route to the gadget’s
handle-message export. The SDK’s
Messaging trait decodes each call into a Request enum you define: the
method string selects the variant, and the payload fills its fields. The
backend for the example above:
#[derive(Deserialize)]#[serde(tag = "method", content = "payload", rename_all = "snake_case")]enum Request { GetHistory { page: u32 }, ClearHistory,}
impl Messaging for MyGadget { type Request = Request;
fn handle(request: Request) -> Result<serde_json::Value, String> { match request { Request::GetHistory { page } => { let page = load_history_page(page)?; serde_json::to_value(&page).map_err(|e| format!("serialize history page: {e}")) } Request::ClearHistory => { clear_history_db()?; Ok(serde_json::Value::Bool(true)) } } }}rename_all = "snake_case" maps GetHistory to the method name
"get_history". ClearHistory has no fields, and the empty object the
frontend sends as its payload is accepted. An unknown method, or a payload that
does not fit its variant, rejects the sendMessage promise before handle
runs.
A gadget can also implement the generated MessagingGuest trait by hand and
work with the raw JSON strings. See the
Exports reference for the full
WIT contract, both traits and the SDK helpers.
JSON encoding
Section titled “JSON encoding”Both directions are JSON-encoded strings at the WIT boundary:
- Frontend → Backend:
payloadis serialized to JSON by the host before reachinghandle_message. - Backend → Frontend: the
Ok(String)return value is parsed as JSON by the host before resolving thesendMessagepromise.
The TypeScript generic parameters on sendMessage<TPayload, TResult> provide
type safety on the frontend side, but the actual wire format is always a JSON
string. Make sure your Rust types and TypeScript types agree on the shape.
WASM streaming limitation
Section titled “WASM streaming limitation”The sendMessage type signature includes an optional onMessage callback
parameter, but this is only functional for native (non-WASM) gadgets. For WASM
gadgets, the callback is never invoked. This limitation exists because the WIT
messaging::handle-message interface is one-shot by design.