Data Serialization
Relevant source files
The following files were used as context for generating this wiki page:Data serialization is a fundamental, yet largely internal, mechanism within webext-bridge. It ensures that all data, including complex objects like JavaScript Error instances, can be reliably transmitted between the different, isolated contexts of a web extension (e.g., background script, content scripts, popups, and devtools). The library guarantees that messages are converted into a format suitable for transport over underlying browser APIs like runtime.Port and window.postMessage, which primarily support JSON-compatible data.
This process is mostly transparent to the developer. The library automatically handles the serialization of outgoing messages and the deserialization of incoming messages, including the special handling required to preserve the properties of Error objects across contexts. This allows for robust error handling and consistent data structures throughout the extension's various parts.
Core Message Structure
All communication within webext-bridge is built around a standardized internal message format defined by the InternalMessage interface. This structure contains the payload, routing information, and metadata necessary to manage the message lifecycle.
Sources: src/types.ts:20-31
The diagram below illustrates the flow of an InternalMessage from a sender to a receiver, where it is processed by a registered listener.
This sequence shows how sendMessage initiates a transaction that is resolved when a reply is received.
Sources: src/internal/endpoint-runtime.ts:145-171
InternalMessage Interface
The InternalMessage interface is the canonical data structure for all messages.
| Field | Type | Description |
|---|---|---|
origin | Endpoint | The endpoint where the message originated. |
destination | Endpoint | The target endpoint for the message. |
transactionId | string | A unique ID to track a message and its corresponding reply. |
hops | string[] | An array of contexts the message has passed through, used for debugging. |
messageID | string | The user-defined ID (or key) of the message, used to route it to the correct listener. |
messageType | 'message' | 'reply' | Indicates if the message is a new request or a reply to a previous one. |
err | JsonValue | A serialized error object if an exception occurred while handling the message. |
data | JsonValue | void | The payload of the message or the return value of the handler. |
timestamp | number | The creation timestamp of the message. |
Sources: src/types.ts:20-31
Error Serialization
A critical feature of the serialization process is the handling of Error objects. When a message handler throws an error, webext-bridge catches it, serializes it, and sends it back to the caller. The sendMessage promise at the origin is then rejected with a reconstructed Error object that preserves the original error's properties.
This is accomplished using the serialize-error library, which is a production dependency of the project.
Sources: package.json:48
Serialization and Deserialization Flow
The following diagram details the process of an error being thrown in a message listener and propagated back to the original caller.
This flow ensures that developers can use standard try...catch blocks or .catch() on promises to handle errors that occur in remote contexts.
Sources: src/internal/endpoint-runtime.ts:77-117
Implementation Details
Serialization in the message handler:
When a listener throws, the catch block captures the error and serializeError converts it to a plain object.
// src/internal/endpoint-runtime.ts:101-104
}
catch (error) {
err = error
}
// src/internal/endpoint-runtime.ts:106
finally {
if (err) message.err = serializeError(err)
// ...
}
Sources: src/internal/endpoint-runtime.ts:101-107
Deserialization at the origin:
When a reply with an err payload arrives, the code reconstructs the error object before rejecting the promise.
// src/internal/endpoint-runtime.ts:60-70
if (err) {
const dehydratedErr = err as Record<string, string>
const errCtr = self[dehydratedErr.name] as any
const hydratedErr = new (
typeof errCtr === 'function' ? errCtr : Error
)(dehydratedErr.message)
// eslint-disable-next-line no-restricted-syntax
for (const prop in dehydratedErr)
hydratedErr[prop] = dehydratedErr[prop]
transactionP.reject(hydratedErr)
}
Sources: src/internal/endpoint-runtime.ts:60-70
Transport-Layer Payloads
The InternalMessage is often wrapped in another layer of data specific to the transport mechanism being used.
Port Messaging
For communication with the background script, webext-bridge uses runtime.Port. Messages sent over this port are wrapped in either a RequestMessage or a StatusMessage object. The PortMessage class provides static methods to create these messages.
// src/internal/port-message.ts
export class PortMessage {
static toBackground(port: Runtime.Port, message: RequestMessage) {
return port.postMessage(message)
}
static toExtensionContext(port: Runtime.Port, message: StatusMessage) {
return port.postMessage(message)
}
}
Sources: src/internal/port-message.ts:44-51
The RequestMessage is used by extension contexts to send messages to the background script, while StatusMessage is used by the background script to send updates from the background script to other contexts.
| Type | Direction | Description |
|---|---|---|
RequestMessage | Context -> Background | Wraps a message to be delivered ('deliver') or to sync pending messages ('sync'). |
StatusMessage | Background -> Context | Provides status updates like 'undeliverable', 'delivered', 'incoming', or 'terminated'. |
Sources: src/internal/port-message.ts:7-42
Window Post-Messaging
For communication between a content script and a script running in the page's window context, the library uses window.postMessage. This transport sends the InternalMessage directly, without an additional wrapper, as it's a direct peer-to-peer communication channel managed by usePostMessaging.
Sources: src/internal/post-message.ts:16-25, src/window.ts:8-9
Stream Serialization
The Stream API provides a way to establish a persistent, namespaced communication channel. Data sent over a stream must be a JsonValue, which enforces serializability from the start.
The send method of the Stream class takes an optional JsonValue payload. This payload is wrapped inside a special __crx_bridge_stream_transfer__ message, which is then sent using the standard sendMessage mechanism.
// src/internal/stream.ts:55-61
public send(msg?: JsonValue): void {
if (this.isClosed)
throw new Error('Attempting to send a message over closed stream. Use stream.onClose(<callback>) to keep an eye on stream status')
this.endpointRuntime.sendMessage('__crx_bridge_stream_transfer__', {
streamId: this.streamInfo.streamId,
streamTransfer: msg,
action: 'transfer',
}, this.streamInfo.endpoint)
}
Sources: src/internal/stream.ts:55-61
Conclusion
Data serialization is a core pillar of webext-bridge that enables seamless and reliable communication across the sandboxed environments of a browser extension. By abstracting away the complexities of data conversion and error propagation, the library provides a simple and powerful API for developers. The automatic handling of Error objects, in particular, makes debugging and error management significantly more robust compared to manual implementations.