Internal Communication
Relevant source files
The following files were used as context for generating this wiki page:
Internal communication within Ghostbird is the mechanism that enables interaction between the add-on's isolated components, primarily the background script (background.js) and the compose window script (compose.js). The background script manages the WebSocket connection to the GhostText server, while the compose script interacts with the DOM of the email compose window. Because these scripts operate in different execution contexts, a robust messaging system is required to relay data and commands between them.
This communication is built upon the Thunderbird WebExtensions runtime.Port API, which provides a persistent, two-way communication channel. The architecture abstracts this low-level API into more manageable, purpose-built components to handle connection management, message passing, and state synchronization. For a higher-level overview of the system architecture, see the Design Document.
Sources: doc/design.md:3-23
Core Architecture
The internal communication architecture is centered around two main scripts:
- Background Script (
background.js): A persistent script that handles application-level logic, including managing connections to the GhostText server and orchestrating sessions across different compose windows. - Compose Script (
compose.js): A script injected into each Thunderbird mail compose window. It handles user interactions within that window (like button clicks) and manipulates the email body content.
The following diagram illustrates the high-level relationship between these scripts and the communication channel that links them.
This diagram shows that the PortConnector in the background script establishes a runtime.Port connection to the PortHandler in the compose script.
Sources: doc/design.md:25-52, src/root/background.ts, src/root/compose.ts
Communication Channel: runtime.Port
The foundation of the internal communication is the runtime.Port API. This provides a long-lived connection between the background script and a specific compose window script, identified by its tabId.
Connection Lifecycle
- Initiation: The background script initiates the connection to a compose script in a specific tab using
browser.tabs.connect(tabId). This is managed by thePortConnectorclass. - Listening: The compose script listens for incoming connections via
browser.runtime.onConnect. This is handled by thePortHandlerclass. - Message Passing: Once connected, both sides can send and receive JSON-serializable messages using
port.postMessage()and theport.onMessageevent. - Termination: The connection remains active until the compose window is closed, the background script is suspended, or either side explicitly calls
port.disconnect(). TheonDisconnectevent allows for cleanup on both ends.
Sources: doc/design.md:65-82, src/app-background/port_connector.ts:18-20, src/app-compose/port_handler.ts:21-23
PortConnector (Background Script)
The PortConnector class is responsible for creating and managing outgoing port connections from the background script to compose scripts. Its primary role is to connect to a specific tab and return a Port object that can be used for communication.
// src/app-background/port_connector.ts:18-20
export class PortConnector implements PortConnectorApi {
connect(tabId: number): Port {
return this.messenger.tabs.connect(tabId, { name: "ghostbird" })
}
Sources: src/app-background/port_connector.ts:15-22
PortHandler (Compose Script)
The PortHandler class is the counterpart to PortConnector. It runs in the compose script and is responsible for handling incoming port connections from the background script. It listens for new connections and, upon receiving one, attaches message and disconnection handlers.
// src/app-compose/port_handler.ts:21-29
listen(
onReceive: (msg: unknown) => void,
onDisconnect: (p: Port) => void,
): void {
this.messenger.runtime.onConnect.addListener((port) => {
this.port = port
port.onMessage.addListener(onReceive)
port.onDisconnect.addListener(onDisconnect)
})
}
Sources: src/app-compose/port_handler.ts:14-31
Message Passing Abstractions
To simplify the event-driven nature of runtime.Port, Ghostbird uses several layers of abstraction. These wrappers provide features like promise-based request/response patterns and strongly-typed interfaces.
PromisifiedMessagePort
The PromisifiedMessagePort class is a crucial abstraction that wraps a raw Port to provide a Promise-based request-response mechanism. This allows one side to "call" a function on the other side and await its result, hiding the underlying postMessage and onMessage event handling.
It works by assigning a unique ID to each request. The receiving end processes the request and sends a response back with the same ID. The PromisifiedMessagePort on the sending side listens for this response and resolves the corresponding promise.
The following diagram shows the class structure of PromisifiedMessagePort.
BackgroundMessenger
Building on top of PromisifiedMessagePort, the BackgroundMessenger provides a high-level, strongly-typed API for the compose script to communicate with the background script. It exposes specific methods that correspond to commands the background script can execute. This encapsulates the message format and command strings, providing a clean interface for the compose script.
The BackgroundMessenger is instantiated with a PromisifiedMessagePort and provides methods like start, stop, and update.
| Method | Description |
|---|---|
start(state) | Requests the background script to start a new GhostText session with the initial email state. |
stop() | Requests the background script to terminate the current session. |
update(edit) | Sends an update from the compose window to the background script (planned for future versions). |
attach(listener) | Attaches a listener to handle messages coming from the background script. |
Sources: src/thunderbird/compose_util/background_messenger.ts:13-43
Data Flow Sequence
The following sequence diagram illustrates the complete internal communication flow when a user initiates a Ghostbird session by clicking the toolbar button in a compose window.
This diagram shows how a user action in compose.js triggers a command to background.js, which then establishes a port connection back to compose.js to start the session and relay data from the GhostText server.
Sources: doc/design.md:54-100
Summary
Ghostbird's internal communication system is a layered architecture designed for maintainability and clarity. It uses the standard runtime.Port API as its foundation but builds higher-level abstractions like PromisifiedMessagePort and BackgroundMessenger on top. This approach effectively decouples the background and compose scripts, allowing them to communicate through well-defined, promise-based interfaces while hiding the complexity of the underlying asynchronous, event-based messaging channel. This separation of concerns is critical for managing state and data flow in a distributed browser extension environment.