Messaging Decorators
The Messaging system in deco-ext provides a type-safe way to communicate between different parts of your extension. It is inspired by webext-bridge and offers similar benefits with tight integration with the decorator pattern.
Type System
Extending the ProtocolMap
First, extend the ProtocolMap
interface to define your message types:
import { ProtocolMap, ProtocolWithReturn } from 'deco-ext';
declare module 'deco-ext' {
interface ProtocolMap {
// Simple message with data but no return type
'user:settings': { theme: string; notifications: boolean };
// Function-like message with arguments and return type
'fetch:data': (query: string) => Promise<{ results: any[] }>;
// Message with explicit data and return types
'auth:login': ProtocolWithReturn<
{ username: string; password: string },
{ success: boolean; token?: string; error?: string }
>;
}
}
By extending the ProtocolMap
, you get full type checking for both message data and return values.
Method Decorators
onMessage
This decorator allows you to handle messages with a specific name in your service. It can be used in both background and content scripts.
import { onMessage, InjectableService } from 'deco-ext';
@InjectableService()
class AuthService {
@onMessage({ key: 'auth:login' })
handleLogin(arg: {
data: { username: string; password: string },
sender: browser.Runtime.MessageSender
}) {
console.log(`Login attempt from ${arg.data.username}`);
// Authenticate user
if (arg.data.username === 'admin' && arg.data.password === 'password') {
return { success: true, token: 'fake-jwt-token' };
} else {
return { success: false, error: 'Invalid credentials' };
}
}
}
Parameter Decorators
messageData
Used with onMessage
to extract the data from the message:
import { onMessage, messageData, InjectableService } from 'deco-ext';
@InjectableService()
class AuthService {
@onMessage({ key: 'auth:login' })
handleLogin(
@messageData() credentials: { username: string; password: string },
@messageData('username') username: string
) {
console.log(`Login attempt from ${username}`);
// Access full credentials object or specific properties
if (credentials.username === 'admin' && credentials.password === 'password') {
return { success: true, token: 'fake-jwt-token' };
} else {
return { success: false, error: 'Invalid credentials' };
}
}
}
messageSender
Used with onMessage
to extract sender information:
import { onMessage, messageData, messageSender, InjectableService } from 'deco-ext';
@InjectableService()
class ContentScriptCommunicator {
@onMessage({ key: 'content:report' })
handleContentReport(
@messageData() data: { url: string; content: string },
@messageSender() sender: browser.Runtime.MessageSender,
@messageSender('tab') tab: browser.Tabs.Tab | undefined,
@messageSender('id') extensionId: string
) {
console.log(`Message from tab ${tab?.id} at URL ${data.url}`);
console.log(`Extension ID: ${extensionId}`);
// Process content report
return { received: true, timestamp: Date.now() };
}
}
Sending Messages
sendMessageToBackground
Send messages from content scripts, popup, options pages, or other non-background contexts to the background script:
import { sendMessageToBackground } from 'deco-ext';
// Send a simple message
const response = await sendMessageToBackground('user:settings', {
theme: 'dark',
notifications: true
});
// Response is typed based on the ProtocolMap
// Send a login request
const loginResult = await sendMessageToBackground('auth:login', {
username: 'admin',
password: 'password'
});
// loginResult is typed as { success: boolean; token?: string; error?: string }
sendMessageToContent
Send messages from the background script to content scripts:
import { sendMessageToContent } from 'deco-ext';
// Get the active tab ID
const tabs = await browser.tabs.query({ active: true, currentWindow: true });
const tabId = tabs[0].id;
// Send message to the content script in that tab
const response = await sendMessageToContent('content:update', {
action: 'highlight',
selector: '.important'
}, { tabId });
Important:
sendMessageToContent
should only be used in background scripts, as only they have the necessary permissions to send messages to specific tabs.
Important:
sendMessageToBackground
should never be used in background scripts themselves. It's intended for use in content scripts, popup pages, options pages, etc.
Complete Example
Here's a complete example showing bi-directional communication:
Background Script:
import { onMessage, messageData, InjectableService, sendMessageToContent } from 'deco-ext';
@InjectableService()
class DataService {
private cache = new Map<string, any>();
@onMessage({ key: 'fetch:data' })
async handleFetchData(@messageData() query: string) {
console.log(`Fetching data for query: ${query}`);
// Check cache first
if (this.cache.has(query)) {
return { results: this.cache.get(query) };
}
// Simulate API call
const results = await this.fetchFromApi(query);
this.cache.set(query, results);
return { results };
}
private async fetchFromApi(query: string) {
// Simulate API request
return [{ id: 1, name: 'Result 1' }, { id: 2, name: 'Result 2' }];
}
async sendUpdateToAllTabs(update: any) {
const tabs = await browser.tabs.query({});
for (const tab of tabs) {
if (tab.id) {
sendMessageToContent('data:update', { update }, { tabId: tab.id });
}
}
}
}
Content Script:
import { onMessage, messageData, sendMessageToBackground, InjectableService } from 'deco-ext';
@InjectableService()
class ContentApp {
@onMessage({ key: 'data:update' })
handleDataUpdate(@messageData('update') update: any) {
console.log('Received data update:', update);
// Update UI with new data
this.updateUI(update);
}
private updateUI(data: any) {
// Update the page DOM
document.getElementById('data-container')!.textContent = JSON.stringify(data);
}
async searchData(query: string) {
const result = await sendMessageToBackground('fetch:data', query);
this.updateUI(result.results);
return result;
}
}
Implementation Details
The messaging system uses a singleton pattern to ensure only one message listener is registered, and then routes messages to the appropriate handlers based on the message name. When a message is received:
- The message is checked for a valid
name
property - If a handler is registered for that name, it's called with the message data and sender
- For each handler:
- The class instance is resolved from the dependency injection container
- If the class has an
init
method, it's called before handling the message - If parameter decorators are used, the message data and sender are transformed accordingly
- The method is called with the appropriate parameters
- The return value is passed back as the response to the message
The messaging decorators can only be used on methods within classes that have been decorated with the InjectableService
decorator from deco-ext.