Message Lifecycle
Relevant source files
The following files were used as context for generating this wiki page:The webext-bridge library provides a unified messaging system for communication between different parts of a web extension, such as the background script, content scripts, popups, options pages, and devtools. It abstracts the native runtime.sendMessage, tabs.sendMessage, and runtime.Port APIs into a simple, promise-based interface. This document details the internal architecture and the lifecycle of a message from its creation to its final resolution.
The system is designed around a central hub-and-spoke model, with the background script acting as the central message router. All other extension contexts establish a persistent connection to the background script to send and receive messages. A separate mechanism using window.postMessage is employed for communication between content scripts and in-page (window) scripts.
Core Components
The messaging system is built upon several key components that work together to manage the state, routing, and delivery of messages.
Endpoint Runtime
The EndpointRuntime is the heart of the messaging system within each context (background, popup, content-script, etc.). It is created by createEndpointRuntime and provides the primary public API (sendMessage, onMessage) and the internal logic for handling messages.
sendMessage: Initiates a new message. It creates anInternalMessagepayload, generates a uniquetransactionId, and stores aPromisein a map of open transactions. This allows it to wait for a reply.onMessage: Registers a callback function to handle incoming messages for a specificmessageID.handleMessage: The core internal handler. It processes all incoming messages. If a message is destined for the current context, it executes the appropriate listener. If it's a reply, it resolves the corresponding transaction'sPromise. If the message is destined for another context, it forwards it via arouteMessagefunction.- Transaction Management: It maintains a map of
openTransactionsto track outgoing messages that are awaiting a reply. When a reply is received, the corresponding transaction is resolved and removed. If a recipient terminates unexpectedly, the transaction is rejected.
Sources: src/internal/endpoint-runtime.ts
Background Script Hub
The background script acts as the central router for all messages between different extension contexts. It maintains a map of all active connections and is responsible for forwarding messages to their intended recipients.
- Connection Management: It uses
browser.runtime.onConnectto listen for incoming connections from other contexts. Each connection (aruntime.Port) is stored in aconnMapkeyed by a unique endpoint identifier (e.g.,content-script@123:0). - Message Routing: When it receives a message to be delivered, its internal
EndpointRuntimeresolves the destination endpoint and looks it up in theconnMap. If a port is found, the message is forwarded. - Delivery Status: The background script notifies senders about the status of their messages, including successful delivery (
delivered), undeliverability (undeliverable), and when a previously unavailable endpoint becomes available (deliverable). - Session Management: It handles port disconnections, cleans up the
connMap, and can notify other contexts about terminated sessions, preventing promises from hanging indefinitely.
Sources: src/background.ts
Communication Channels
Two primary communication channels are used depending on the source and destination of the message.
Persistent Port
For communication between the background script and other extension contexts (popup, options, content-script, devtools), a PersistentPort is used. This is a wrapper around the standard runtime.Port that automatically reconnects if the connection is lost, which is particularly important for Manifest V3 service workers that can be terminated at any time. It queues undelivered messages and attempts to resend them upon reconnection.
Sources: src/internal/persistent-port.ts
Post Messaging
For communication between a content script and a regular, non-extension script running in the page's window context, the system uses window.postMessage. The usePostMessaging utility abstracts this, requiring a shared namespace to be set via setNamespace (in window) and allowWindowMessaging (in content-script) to avoid conflicts with other scripts on the same page.
Sources: src/internal/post-message.ts, src/content-script.ts:28-32, src/window.ts:16-19
High-Level Architecture
The following diagram illustrates the hub-and-spoke architecture with the background script at the center.
This diagram shows how different extension contexts communicate with the central background script via persistent ports, while the content script acts as a bridge to the window context. The Stream API is built on top of this messaging foundation.
Sources: src/background.ts, src/content-script.ts, src/popup.ts, src/internal/stream.ts
Message Flow
The lifecycle of a message involves sending, routing, receiving, and replying. This flow ensures reliable, promise-based communication.
1. Sending a Message
The process begins when sendMessage is called from any context.
This diagram shows the initial steps: sendMessage creates an InternalMessage and a pending promise, then passes the message to the PersistentPort for delivery to the background script.
Sources: src/internal/endpoint-runtime.ts:182-205, src/internal/persistent-port.ts:153-158
2. Routing in Background
The background script receives the message and routes it to the correct destination.
This diagram illustrates the background script's role as a router: it receives a message, uses its EndpointRuntime and connMap to find the recipient's port, and forwards the message.
Sources: src/background.ts:285-303, src/background.ts:145-178
3. Receiving and Replying
The destination context receives the message, executes the user-defined callback, and sends a reply.
This sequence shows the final leg of the initial message journey. The destination's EndpointRuntime finds and executes the correct onMessage handler. The return value of this handler is then packaged into a reply message and sent back.
Sources: src/internal/endpoint-runtime.ts:89-126
4. Completing the Transaction
The reply travels back to the original sender, where the Promise returned by the initial sendMessage call is resolved.
This diagram shows the reply message arriving at the original sender. The EndpointRuntime finds the pending transaction and resolves the promise, delivering the result to the application code that called sendMessage.
Sources: src/internal/endpoint-runtime.ts:58-87
Data Structures
The message lifecycle relies on a set of well-defined data structures for communication.
InternalMessage
This is the core data structure that is passed between different contexts.
| Field | Type | Description |
|---|---|---|
origin | Endpoint | The context where the message originated. |
destination | Endpoint | The intended recipient of the message. |
transactionId | string | A unique ID to correlate a message with its reply. |
hops | string[] | An array of runtime IDs the message has passed through, for debugging. |
messageID | string | The user-defined ID for the message, used to look up the onMessage listener. |
messageType | 'message' | 'reply' | Indicates if it's a new message or a reply to a previous one. |
data | JsonValue | The payload of the message. |
timestamp | number | The creation timestamp of the message. |
err | JsonValue | Contains a serialized error if one occurred during processing. |
Sources: src/types.ts:20-31
Port Messages
Communication over the runtime.Port uses specific message types to manage delivery status.
Type (RequestMessage) | Description |
|---|---|
sync | Sent by a context upon connection to sync its state with the background script, including pending responses and deliveries. |
deliver | Wraps an InternalMessage to be delivered to another context via the background script. |
Status (StatusMessage) | Description |
|---|---|
undeliverable | Sent from background to sender if the destination is not currently connected. |
deliverable | Sent from background to sender when a previously unavailable destination connects. |
delivered | A receipt sent from background to sender confirming the message was passed to the destination's port. |
incoming | Sent from background to a destination, wrapping the InternalMessage. |
terminated | Sent from background to a sender if the destination's session ends before a reply is sent. |
Sources: src/internal/port-message.ts
Summary
The message lifecycle in webext-bridge is a robust process designed for reliability and ease of use. It leverages a central background hub for routing, persistent ports for resilient connections, and a transaction-based system to enable simple, promise-based APIs. This architecture effectively abstracts the complexities of cross-context communication in a web extension, allowing developers to focus on application logic rather than messaging infrastructure.
Sources: README.md:14-16, src/background.ts, src/internal/endpoint-runtime.ts