connect.runtime & messaging
Introspection, one-shot messaging, long-lived Ports, and lifecycle events between your surfaces.
Introspect your extension and talk between its own surfaces — popup, background,
options, side panel, and content scripts. runtime needs no capability: an
extension can always reach itself.
- Capability: none (strictly intra-extension)
- Namespace:
connect.runtime
Cross-extension messaging is a different namespace —
connect.channel.
Walkthrough
Wire a popup to its background.
- In the background, register a receiver:
connect.runtime.onMessage((msg) => msg.type === 'ping' ? { pong: true } : undefined); - In the popup, send and read the reply:
const { reply } = await connect.runtime.sendMessage({ type: 'ping' }); // { pong: true } - Need a stream or disconnect signal? Use a Port instead of
one-shot
sendMessage. - Re-arm gated subscriptions from
onGrantsChanged(see below).
Introspection
runtime.getManifest()
How to use it
- Call it to read your own
{ id, name, version, apiVersion }— handy for version checks.
Signature — connect.runtime.getManifest() · Returns — Promise<{ id, name, version, apiVersion }>.
One-shot messaging
runtime.sendMessage(data)
Send a message to your instance's other surfaces.
How to use it
- Make sure another surface has an
onMessagelistener. await sendMessage(data)and read{ ok, reply }—ok:falsemeans nobody listened.
Signature — connect.runtime.sendMessage(data)
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
data | unknown | yes | JSON-serialisable message. |
Returns — Promise<{ ok, delivered, reply }>. The first listener returning a
non-undefined value answers; no listeners ⇒ ok: false.
runtime.onMessage(cb)
Receive messages. cb(data, sender) — return a value (or a promise) to reply. Register
once per surface; a throwing listener never answers.
How to use it
- Register once per surface (e.g. in the background).
- Inspect
data;returna value to reply, or nothing to stay silent.
// background.js
connect.runtime.onMessage((msg) => {
if (msg.type === 'ping') return { pong: true };
});
// popup.js
const { reply } = await connect.runtime.sendMessage({ type: 'ping' }); // { pong: true }Long-lived Ports
sendMessage is one-shot. A Port is a persistent, two-way channel — reach for it
when you need a stream of messages, per-connection state, or (the main reason) to know
when the other end goes away.
runtime.connect(options?)
Open a Port to your other surfaces.
How to use it
- Ensure the other end has an
onConnectlistener. await connect({ name }); it rejects if nobody is listening.- Use
port.postMessage/port.onMessageand clean up ononDisconnect.
Signature — connect.runtime.connect({ name? })
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
name | string | no | A label the receiver reads as port.name. |
Returns — Promise<Port>. Rejects when nothing is listening (like sendMessage's
ok:false).
runtime.onConnect(cb)
cb(port, sender) fires in your other surfaces when one calls connect().
How to use it
- In the background, register
onConnect. - Filter by
port.name, then wirepostMessage/onMessageand clean up ononDisconnect.
The Port object
| Member | Description |
|---|---|
port.name | The name the opener passed. |
port.postMessage(data) | Send to the other end. A no-op once disconnected (never throws). |
port.onMessage(cb) | cb(data). Returns an unsubscribe fn. |
port.onDisconnect(cb) | Fires when the other end closes or its surface dies. Subscribing after the fact still fires. Returns an unsubscribe fn. |
port.disconnect() | Close it; the other end gets onDisconnect. |
// popup.js — a live connection
const port = await connect.runtime.connect({ name: 'progress' });
port.onMessage(({ percent }) => render(percent));
port.postMessage({ start: true });
// background.js — one conversation per popup, cleaned up automatically
connect.runtime.onConnect((port) => {
if (port.name !== 'progress') return;
const timer = setInterval(() => port.postMessage({ percent: next() }), 100);
port.onDisconnect(() => clearInterval(timer)); // popup closed → stop working
});Rules & limits
- Intra-extension only — a port never leaves your instance. Cross-extension uses
connect.channel. - Content scripts can open a port out to their background, and a surface can open
one into a CS with
tabs.connect(tabId)(the CS accepts via itsruntime.onConnect). A CS port disconnects when its document navigates or the tab closes. - A suspended background is not woken to accept: with no live listener,
connect()rejects. Port users should declare apersistentbackground. - If several surfaces listen to
onConnect, they share one port. In the normal 1:1 case the behaviour matches Chrome. - Ports die with their surface, on uninstall/unload, and on dev-mode teardown — both ends
get
onDisconnect.
Lifecycle events
runtime.onInstalled(cb)
Setup hook — fires once after install. Sticky: a background subscribing on first boot still receives it.
How to use it
- Subscribe in the background to run one-time setup (seed storage, create menus).
- Branch on
reason('install'vs'update').
Payload — { reason: 'install' | 'update', version }.
runtime.onGrantsChanged(cb)
Fires when the user grants or revokes capabilities.
How to use it
- Put your gated
on*subscriptions in onearm()function. - Call
arm()at boot and fromonGrantsChanged, so a grant that lands after boot re-arms them (subscribing is idempotent).
Payload — { granted }.
function arm() { connect.tabs.onUpdated(handle); } // safe to call repeatedly
arm();
connect.runtime.onGrantsChanged(arm); // re-arm once the user grants tabs.readruntime.onSuspend(cb) · runtime.onResume(cb)
Background-lifecycle signals for suspendable / ephemeral backgrounds.
How to use it
- On
onSuspend, persist any in-memory state tostorage. - On
onResume(or next boot), rebuild it — there's no event replay.
Notes
- Register
onMessage/onConnectonce per surface. - For anything periodic that must survive suspension, drive it from
connect.alarms, not asetIntervalin a suspendable background.