Skip to content

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.

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.

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);
}

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.

Both directions are JSON-encoded strings at the WIT boundary:

  • Frontend → Backend: payload is serialized to JSON by the host before reaching handle_message.
  • Backend → Frontend: the Ok(String) return value is parsed as JSON by the host before resolving the sendMessage promise.

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.

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.