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