Messaging API
Overview
The Messaging API provides a central publish/subscribe (pub/sub) message bus that routes messages between extensions and webviews. Any participant -- an extension runtime or a webview (popup, bottom sheet, webview tab) -- can subscribe to named channels and post messages to them. All subscribers on a channel receive every message posted to it, regardless of whether the sender is an extension or a webview.
The messaging system enables three communication patterns:
- Extension-to-extension -- One extension posts a message that another extension receives, allowing loosely coupled coordination between independent extensions.
- Extension-to-webview -- An extension posts data that a webview UI (popup, bottom sheet, or webview tab) receives and renders.
- Webview-to-extension -- A webview form or interactive UI posts user input back to the extension that opened it.
No manifest declaration is required. The messaging API is available to all extensions at runtime through the SDK's top-level listen() and post() functions.
RPC Methods
The messaging system is built on three RPC methods handled by the extension host.
messaging.listen
Subscribe the calling extension to a channel.
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Channel name to subscribe to. Must be 1--256 characters. |
Returns null. The subscription is registered immediately. Subsequent messages posted to this channel will be delivered to the calling extension as __message events.
messaging.unlisten
Unsubscribe the calling extension from a channel.
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Channel name to unsubscribe from. |
Returns null. If the extension was not subscribed to the channel, the call is a no-op.
messaging.post
Post a message to all subscribers on a channel.
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Channel name to post to. Must be 1--256 characters. |
message | any | No | Any JSON-serializable value. Can be a string, number, boolean, object, array, or null. |
Returns null. The message is delivered to all current subscribers on the channel -- both extensions and webviews -- including the sender if it is also subscribed.
Architecture
The messaging system uses a central message bus that routes messages between all participants. When a message is posted to a channel, the bus delivers it to every subscriber — both extensions and webviews. The bus resolves subscribers at delivery time, which means it automatically handles extensions that are deactivated or webviews that are disposed between subscription and delivery.
Channel ID Validation
Channel IDs must be between 1 and 256 characters. They are validated on messaging.listen and messaging.post calls.
Message Delivery
Delivery
When a message is posted to a channel:
- Extensions receive the message through the SDK's internal event system, which dispatches it to all callbacks registered via
editor.listen(). - Webviews receive the message through the injected message bridge, which dispatches it to all callbacks registered via
window.listen().
Both delivery paths catch and log errors from individual handlers, so one failing handler does not prevent delivery to other subscribers.
SDK API
The extension SDK exposes two top-level functions for messaging. These are not namespaced under a sub-object -- they are direct properties of the SDK module.
editor.listen(id, callback)
Subscribe to a message channel.
| Parameter | Type | Description |
|---|---|---|
id | string | Channel name to subscribe to. |
callback | function | Called with the message payload each time a message is posted to this channel. |
Returns: An unsubscribe function. Call it to remove this specific callback from the channel. When all callbacks for a channel are removed, the SDK automatically sends messaging.unlisten to the host to clean up the server-side subscription.
import editor from "@srcnexus/ext-sdk";
const unsubscribe = editor.listen("myExtension.dataReady", (message) => {
console.log("Received:", message);
});
// Later, when you no longer need the subscription:
unsubscribe();
editor.post(id, message)
Post a message to a channel.
| Parameter | Type | Description |
|---|---|---|
id | string | Channel name to post to. |
message | any | Any JSON-serializable value. |
Returns: Promise<void>
import editor from "@srcnexus/ext-sdk";
// Post a string
await editor.post("myExtension.dataReady", "Processing complete");
// Post an object
await editor.post("myExtension.dataReady", {
type: "result",
payload: { count: 42 },
timestamp: Date.now(),
});
Webview Messaging Bridge
Every extension webview (popup, bottom sheet, webview tab, fullscreen content screen) automatically receives a JavaScript bridge that connects it to the message bus. The bridge is injected at AT_DOCUMENT_START so it is available before any page scripts run.
Bridge Functions
window.listen(id, callback)
Subscribe the webview to a message channel.
// Inside a webview's HTML/JS
const unsubscribe = window.listen("myExtension.updates", function (message) {
document.getElementById("output").textContent = JSON.stringify(message);
});
Returns an unsubscribe function. When all callbacks for a channel are removed, the bridge automatically notifies the host to remove the webview's subscription.
window.post(id, message)
Post a message from the webview to the message bus.
// Inside a webview's HTML/JS — e.g., a form submit handler
document.getElementById("submit").addEventListener("click", function () {
var name = document.getElementById("name").value;
window.post("myExtension.formSubmitted", { name: name });
});
The message is delivered to all subscribers on the channel, including extensions and other webviews.
Cleanup
The messaging system provides automatic cleanup at multiple levels to prevent stale subscriptions and memory leaks.
Extension Deactivation
When an extension is deactivated, all of its channel subscriptions are automatically removed.
Webview Disposal
When a webview is disposed (popup closed, bottom sheet dismissed, webview tab closed), all of its channel subscriptions are automatically removed.
Stale Listener Auto-Removal
During message delivery, if a subscriber (extension or webview) is no longer available, it is automatically removed from the channel. This handles edge cases where a participant is disposed between the time a message is posted and the time delivery is attempted.
Full Reset
When the extension host is fully reset, all channel subscriptions for both extensions and webviews are cleared.
Message Flow Diagrams
Extension to Extension
Extension A Message Bus Extension B
| | |
| editor.listen("ch1", cb) | |
| ──── messaging.listen ────> | |
| | editor.listen("ch1", cb) |
| | <──── messaging.listen ──── |
| | |
| editor.post("ch1", data) | |
| ──── messaging.post ──────> | |
| | pushEvent(__message) |
| <── pushEvent(__message) ── | ───────────────────────────> |
| cb(data) | cb(data) |
Extension to Webview
Extension Message Bus Webview
| | |
| | window.listen("ch1", cb) |
| | <──── __msgListen("ch1") ── |
| | |
| editor.post("ch1", data) | |
| ──── messaging.post ──────> | |
| | evaluateJavascript( |
| | __dispatchMessage(data)) |
| | ───────────────────────────> |
| | cb(data) |
Webview to Extension
Webview Message Bus Extension
| | |
| | editor.listen("ch1", cb) |
| | <──── messaging.listen ──── |
| | |
| window.post("ch1", data) | |
| ──── __msgPost ──────────> | |
| | pushEvent(__message) |
| | ───────────────────────────> |
| | cb(data) |
Examples
Basic Extension-to-Extension Communication
Two extensions coordinate through a shared channel:
Extension A (producer):
import editor from "@srcnexus/ext-sdk";
editor.onLoad(() => {
editor.commands.registerCommand("extA.processData", async () => {
const result = await doExpensiveWork();
await editor.post("extA.dataProcessed", {
type: "result",
data: result,
timestamp: Date.now(),
});
});
});
Extension B (consumer):
import editor from "@srcnexus/ext-sdk";
editor.onLoad(() => {
editor.listen("extA.dataProcessed", (message) => {
console.log("Extension A finished:", message.data);
editor.window.showToast("Data processing complete");
});
});
Webview Form with Extension Callback
An extension opens a popup with a form and waits for the user to submit it:
import editor from "@srcnexus/ext-sdk";
async function showInputForm() {
// Set up the listener BEFORE opening the popup to avoid race conditions
let formResolve;
const formPromise = new Promise((resolve) => {
formResolve = resolve;
});
const unlisten = editor.listen("myExt.formSubmitted", (data) => {
formResolve(data);
});
// Open the popup with an HTML form
const popupId = await editor.window.showPopup({
title: "Enter Details",
contentType: "webview",
html: `
<html>
<body>
<input id="name" placeholder="Name" />
<button id="submit">Submit</button>
<script>
document.getElementById('submit').addEventListener('click', function() {
var name = document.getElementById('name').value.trim();
if (!name) return;
this.disabled = true;
window.post('myExt.formSubmitted', { name: name });
});
</script>
</body>
</html>
`,
});
// Wait for the webview to post form data
const formData = await formPromise;
unlisten();
await editor.window.closePopup(popupId);
editor.window.showToast("Hello, " + formData.name);
}
Echo / Two-Way Communication
An extension listens on one channel and echoes responses on another:
import editor from "@srcnexus/ext-sdk";
const TEST_CHANNEL = "myExt.requests";
const ECHO_CHANNEL = "myExt.responses";
editor.onLoad(() => {
editor.listen(TEST_CHANNEL, (message) => {
// Process the request and echo a response
editor.post(ECHO_CHANNEL, {
type: "echo",
original: message,
processedAt: Date.now(),
});
});
});
Unsubscribe on Dispose
Always clean up subscriptions when the extension is deactivated:
import editor from "@srcnexus/ext-sdk";
let unsubscribe;
editor.onLoad(() => {
unsubscribe = editor.listen("shared.events", (message) => {
console.log("Event:", message);
});
});
editor.onDispose(() => {
if (unsubscribe) unsubscribe();
});