Background Scripts
Extension logic and state management in background scripts
Background Scripts Overview
Background scripts (service workers in Manifest V3) handle extension logic, state management, and coordination between content scripts and UI.
Service Worker Architecture
Basic Service Worker
// src/background/service-worker.ts
chrome.runtime.onInstalled.addListener((details) => {
if (details.reason === 'install') {
// First install
initializeExtension();
} else if (details.reason === 'update') {
// Extension updated
handleUpdate(details.previousVersion);
}
});
chrome.runtime.onStartup.addListener(() => {
// Browser started
initializeExtension();
});
Message Handling
Message Router
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
const { action, data } = message;
switch (action) {
case 'getSettings':
handleGetSettings(sendResponse);
return true; // Keep channel open
case 'saveSettings':
handleSaveSettings(data, sendResponse);
return true;
case 'toggleFeature':
handleToggleFeature(data, sendResponse);
return true;
default:
console.warn('Unknown action:', action);
sendResponse({ error: 'Unknown action' });
}
});
async function handleGetSettings(sendResponse: (response: any) => void) {
const settings = await chrome.storage.sync.get(null);
sendResponse({ settings });
}
async function handleSaveSettings(data: any, sendResponse: (response: any) => void) {
await chrome.storage.sync.set(data);
sendResponse({ success: true });
}
Broadcasting Messages
// Send message to all content scripts
async function broadcastToTabs(message: any) {
const tabs = await chrome.tabs.query({
url: ['*://*.seqta.com.au/*']
});
tabs.forEach(tab => {
if (tab.id) {
chrome.tabs.sendMessage(tab.id, message).catch(() => {
// Tab might not have content script loaded
});
}
});
}
// Usage
broadcastToTabs({
action: 'updateSettings',
settings: newSettings
});
State Management
Extension State
interface ExtensionState {
features: Record<string, boolean>;
settings: Record<string, any>;
tabs: Set<number>;
}
class StateManager {
private state: ExtensionState = {
features: {},
settings: {},
tabs: new Set()
};
async load() {
const stored = await chrome.storage.sync.get(['features', 'settings']);
this.state.features = stored.features || {};
this.state.settings = stored.settings || {};
}
async save() {
await chrome.storage.sync.set({
features: this.state.features,
settings: this.state.settings
});
}
getFeature(id: string): boolean {
return this.state.features[id] || false;
}
setFeature(id: string, enabled: boolean) {
this.state.features[id] = enabled;
this.save();
this.broadcastFeatureChange(id, enabled);
}
private broadcastFeatureChange(id: string, enabled: boolean) {
broadcastToTabs({
action: 'featureChanged',
featureId: id,
enabled
});
}
}
const stateManager = new StateManager();
Tab Management
Tracking Tabs
const activeTabs = new Set<number>();
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
if (changeInfo.status === 'complete' && tab.url?.includes('seqta.com.au')) {
activeTabs.add(tabId);
// Inject content script if needed
chrome.scripting.executeScript({
target: { tabId },
files: ['content.js']
});
}
});
chrome.tabs.onRemoved.addListener((tabId) => {
activeTabs.delete(tabId);
});
Tab Communication
async function sendToTab(tabId: number, message: any) {
try {
await chrome.tabs.sendMessage(tabId, message);
} catch (error) {
console.error('Failed to send message to tab:', error);
}
}
async function sendToAllTabs(message: any) {
const tabs = await chrome.tabs.query({
url: ['*://*.seqta.com.au/*']
});
await Promise.all(
tabs.map(tab => {
if (tab.id) {
return sendToTab(tab.id, message);
}
})
);
}
Storage Management
Settings Storage
class SettingsManager {
async get(key: string): Promise<any> {
const result = await chrome.storage.sync.get(key);
return result[key];
}
async set(key: string, value: any): Promise<void> {
await chrome.storage.sync.set({ [key]: value });
}
async getAll(): Promise<Record<string, any>> {
return await chrome.storage.sync.get(null);
}
async clear(): Promise<void> {
await chrome.storage.sync.clear();
}
onChanged(callback: (changes: Record<string, chrome.storage.StorageChange>) => void) {
chrome.storage.onChanged.addListener((changes, areaName) => {
if (areaName === 'sync') {
callback(changes);
}
});
}
}
const settings = new SettingsManager();
Feature Management
Feature Registry
interface Feature {
id: string;
name: string;
enabled: boolean;
dependencies?: string[];
}
class FeatureManager {
private features = new Map<string, Feature>();
register(feature: Feature) {
this.features.set(feature.id, feature);
}
async enable(id: string) {
const feature = this.features.get(id);
if (!feature) return;
// Check dependencies
if (feature.dependencies) {
for (const dep of feature.dependencies) {
if (!this.isEnabled(dep)) {
await this.enable(dep);
}
}
}
feature.enabled = true;
await this.saveFeatureState(id, true);
await this.notifyTabs(id, true);
}
async disable(id: string) {
const feature = this.features.get(id);
if (!feature) return;
feature.enabled = false;
await this.saveFeatureState(id, false);
await this.notifyTabs(id, false);
}
isEnabled(id: string): boolean {
return this.features.get(id)?.enabled || false;
}
private async saveFeatureState(id: string, enabled: boolean) {
const features = await settings.get('features') || {};
features[id] = enabled;
await settings.set('features', features);
}
private async notifyTabs(id: string, enabled: boolean) {
await sendToAllTabs({
action: 'featureChanged',
featureId: id,
enabled
});
}
}
const featureManager = new FeatureManager();
Error Handling
Error Reporting
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.action === 'error') {
handleError(message.error, sender);
sendResponse({ received: true });
}
});
function handleError(error: any, sender: chrome.runtime.MessageSender) {
console.error('Error from content script:', error);
// Log to storage for debugging
chrome.storage.local.get(['errors'], (result) => {
const errors = result.errors || [];
errors.push({
error: error.message || error,
tab: sender.tab?.id,
timestamp: Date.now()
});
// Keep only last 100 errors
if (errors.length > 100) {
errors.shift();
}
chrome.storage.local.set({ errors });
});
}
Best Practices
1. Keep Service Worker Alive
// Keep service worker active
setInterval(() => {
// Periodic check
}, 20000);
2. Handle Service Worker Restart
// Restore state on startup
chrome.runtime.onStartup.addListener(async () => {
await stateManager.load();
await initializeFeatures();
});
3. Efficient Message Handling
// ✅ Good - Async handling
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
handleMessage(message).then(response => {
sendResponse(response);
});
return true; // Keep channel open
});
// ❌ Avoid - Synchronous blocking
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
const response = handleMessageSync(message); // Blocks!
sendResponse(response);
});
Next Steps
- Content Scripts - Page enhancement
- Adding Features - Create new features
- Architecture - System architecture
On this page