Architecture Overview
Relevant source files
The following files were used as context for generating this wiki page:
webext-bridge is a library designed to simplify messaging between the different contexts of a web extension (e.g., background, content-script, popup). It provides a consistent API for sending and receiving messages, handling data synchronization, and managing communication channels across the entire extension. The architecture is built around a central message router in the background script, which manages connections and forwards messages between various endpoints.
The system is designed to be robust, especially for Manifest V3 extensions where the background service worker can be terminated. It achieves this through persistent port connections that automatically re-establish themselves. It also supports advanced features like namespaced streams for continuous communication and a clear request-reply pattern using Promises.
Sources: README.md, src/background.ts, src/internal/persistent-port.ts
Core Concepts
Endpoints
An "Endpoint" is a specific location within the extension that can send or receive messages. Each endpoint is uniquely identified by its context, and where applicable, its tab and frame ID.
Sources: src/types.ts:9-14, src/internal/endpoint.ts
The Endpoint interface is defined as:
// src/types.ts:9-14
export interface Endpoint {
context: RuntimeContext
tabId: number
frameId?: number
}
Endpoints are represented as a string, which is parsed and formatted by utility functions. The format is context[@tabId[.frameId]].
| Context | String Format Example | Description |
|---|---|---|
background | background | The extension's background script/service worker. |
popup | popup | The extension's popup page. |
options | options | The extension's options page. |
devtools | devtools@123 | A devtools panel, associated with a specific tab. |
content-script | content-script@123.0 | A content script injected into a page, associated with a tab and frame. |
window | window@123.0 | A script running in the page's main window context, communicating via a content script. |
Sources: src/internal/endpoint.ts:3-18, src/types.ts:3-8
Endpoint Runtime
Each context (background, popup, content-script, etc.) initializes its own EndpointRuntime. This is the core engine that provides the sendMessage and onMessage APIs for that context. It is created by the createEndpointRuntime factory function.
Its primary responsibilities include:
- Managing a map of open transactions (
openTransactions) to match replies with their original requests using atransactionId. - Holding a map of message listeners (
onMessageListeners) for theonMessageAPI. - Handling incoming messages by either resolving a pending transaction (if it's a reply) or invoking a registered listener (if it's a new message).
- Routing outgoing messages to the appropriate transport layer (e.g., a persistent port to the background script or
postMessageto the window).
Sources: src/internal/endpoint-runtime.ts:25-156
Internal Message Structure
All communication within the bridge is standardized through the InternalMessage interface. This object contains routing information, payload, and metadata.
| Field | Type | Description |
|---|---|---|
origin | Endpoint | The endpoint where the message originated. |
destination | Endpoint | The target endpoint for the message. |
transactionId | string | A unique ID for the request-reply lifecycle. |
hops | string[] | An array of contexts the message has passed through, for debugging. |
messageID | string | The user-defined ID of the message, used for onMessage listeners. |
messageType | 'message' | 'reply' | Indicates if it's a new message or a reply to a previous one. |
data | JsonValue | The serializable data payload. |
err | JsonValue | A serialized error object, if an error occurred while handling the message. |
timestamp | number | The creation timestamp of the message. |
Sources: src/types.ts:23-33
Communication Architecture
The library uses a hub-and-spoke architecture, with the background script acting as the central message hub. All other contexts are spokes that connect to this hub to send and receive messages.
Hub-and-Spoke Model
The background.ts script is the central router. It listens for connections from other contexts using browser.runtime.onConnect. It maintains a map (connMap) of all active connections, identified by their unique endpoint string. When a message arrives from one endpoint, the background script resolves the destination endpoint and forwards the message to the correct connection.
This diagram illustrates the central role of the background script.
This diagram shows the background script as the central hub, with other contexts connecting to it. Content scripts also act as a bridge to the window context.
Sources: src/background.ts, src/content-script.ts, src/popup.ts
Persistent Connections
To handle the ephemeral nature of background service workers in Manifest V3, client contexts like popup, options, and devtools do not connect directly. Instead, they use createPersistentPort. This utility wraps browser.runtime.connect and automatically re-establishes the connection if the port is disconnected (e.g., due to the service worker terminating).
The createPersistentPort module also manages a queue for undelivered messages. If a message is sent to an endpoint that is not yet available, the background script notifies the sender that the message is undeliverable. The persistent port then queues the message and waits for a deliverable signal from the background script, which is sent once the target endpoint connects.
Sources: src/internal/persistent-port.ts:1-125, src/background.ts:211-220
Content Script & Window Communication
Communication with scripts running in the main page's window context is a special case, as they cannot directly access browser.* APIs. The content-script acts as a proxy.
windowtocontent-script: Thewindowscript usespostMessageto send a message. A namespace must be configured viasetNamespace()to avoid conflicts with other scripts on the page.content-scripttobackground: Thecontent-scripthas a listener for thesepostMessageevents. Upon receiving one, it forwards the message to the background script through its own persistent port. It also sanitizes the message origin to ensure thewindowscript cannot masquerade as another context.backgroundtowindow: The flow is reversed. The background script sends the message to the content script's port, and the content script usespostMessageto deliver it to thewindow.
This flow is managed by the usePostMessaging utility.
Sources: src/content-script.ts:1-40, src/window.ts:1-19, src/internal/post-message.ts:1-51
The following diagram shows the message flow from a window script to the background.
This sequence shows how a message from an isolated window script is relayed through the content script to reach the background script for routing.
Sources: src/content-script.ts:15-22, src/window.ts:9-13
Message & Stream Flows
Request-Reply Message Flow
The sendMessage function returns a Promise that resolves with the response from the destination. This is managed through a system of transaction IDs.
- Sender: When
sendMessageis called, theEndpointRuntimecreates a uniquetransactionIdand stores the Promise'sresolveandrejectfunctions in theopenTransactionsmap. It then dispatches theInternalMessagewithmessageType: 'message'. - Background: The background script receives the message and routes it to the destination's port.
- Receiver: The receiver's
EndpointRuntimegets the message. It finds the correspondingonMessagelistener and executes it. The return value of the listener becomes thedatafor the reply. - Reply: The receiver's runtime constructs a reply message with the same
transactionIdandmessageType: 'reply', and sends it back. - Resolution: The reply travels back to the original sender. The sender's runtime receives the reply, finds the matching
transactionIdinopenTransactions, and uses the storedresolvefunction to fulfill the Promise with the reply data.
This diagram illustrates the lifecycle of a single request-response message, highlighting the role of the transaction ID.
Sources: src/internal/endpoint-runtime.ts:25-156
Stream API
For continuous, bi-directional communication, the library provides a Stream API. This is useful for scenarios that require more than a single request-reply, such as state synchronization.
onOpenStreamChannel(channel, callback): A context listens for incoming stream requests on a specific namedchannel.openStream(channel, destination): A context initiates a stream to a destination on a namedchannel.
When openStream is called, it creates a Stream instance and sends a special __crx_bridge_stream_open__ message to the destination. The receiving end, which has a listener set up via onOpenStreamChannel, then establishes its own Stream instance, completing the connection. Once established, both sides can use stream.send() and stream.onMessage() to communicate. The communication happens over a dedicated message ID __crx_bridge_stream_transfer__.
Sources: src/internal/stream.ts:1-168
Conclusion
The architecture of webext-bridge is centered around a robust, centralized message-passing system that leverages the browser's runtime.Port mechanism. By abstracting away the complexities of port management, context identification, and message routing, it provides a simple and powerful API for developers. The use of persistent ports ensures reliability in modern Manifest V3 environments, while the addition of the Stream API offers a flexible solution for more complex, stateful communication needs between different parts of a web extension.