Skip to main content

Background Service Worker

The background service worker (background.js) is the API gateway of Knowledge Tooltip. It runs independently from web pages and handles all external API communications.

File Location

source/background.js (359 lines)

Core Purpose

Solve the CORS problem: Content scripts run in the context of web pages and are subject to CORS (Cross-Origin Resource Sharing) restrictions. The background worker has elevated privileges and can make requests to any domain listed in host_permissions.
Manifest V3 background service workers replace the persistent background pages from Manifest V2. They can idle when inactive, improving performance.

Message Router

The worker uses a single listener to route all messages:
background.js:21
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  if (message.action === 'fetchWikipedia') {
    fetchWikipediaData(message.term, message.language)
      .then(data => sendResponse({ success: true, data }))
      .catch(error => sendResponse({ success: false, error: toErrMsg(error) }));
    return true; // Indicates async response
  }

  if (message.action === 'searchWikipedia') {
    searchWikipediaAPI(message.term, message.language)
      .then(result => sendResponse({ success: true, result }))
      .catch(error => sendResponse({ success: false, error: toErrMsg(error) }));
    return true;
  }

  if (message.action === 'fetchWiktionary') {
    fetchWiktionaryData(message.term, message.language)
      .then(data => sendResponse({ success: true, data }))
      .catch(error => sendResponse({ success: false, error: toErrMsg(error) }));
    return true;
  }

  if (message.action === 'fetchFreeDictionary') {
    fetchFreeDictionaryData(message.term, message.language)
      .then(data => sendResponse({ success: true, data }))
      .catch(error => sendResponse({ success: false, error: toErrMsg(error) }));
    return true;
  }

  if (message.action === 'searchWikidata') {
    searchWikidataEntity(message.term, message.language)
      .then(data => sendResponse({ success: true, data }))
      .catch(error => sendResponse({ success: false, error: toErrMsg(error) }));
    return true;
  }

  if (message.action === 'fetchWikidataEntity') {
    fetchWikidataEntity(message.entityId, message.language)
      .then(data => sendResponse({ success: true, data }))
      .catch(error => sendResponse({ success: false, error: toErrMsg(error) }));
    return true;
  }

  if (message.action === 'callOpenAI') {
    callOpenAI(message.messages, message.language, message.maxTokens)
      .then(data => sendResponse({ success: true, data }))
      .catch(error => sendResponse({ success: false, error: toErrMsg(error) }));
    return true;
  }

  if (message.action === 'checkOpenAIKey') {
    chrome.storage.local.get(['openaiKey'], (result) => {
      sendResponse({ hasKey: !!result.openaiKey });
    });
    return true;
  }
});
Returning true from the listener indicates an asynchronous response, keeping the message channel open until sendResponse is called.

API Handlers

1. Wikipedia Summary

Uses Wikipedia’s REST API v1:
background.js:80
async function fetchWikipediaData(term, language) {
  const wikiDomain = language === 'ar' ? 'ar.wikipedia.org' : 'en.wikipedia.org';
  const url = `https://${wikiDomain}/api/rest_v1/page/summary/${encodeURIComponent(term)}`;

  const response = await fetch(url);

  if (!response.ok) {
    throw new Error(`HTTP ${response.status}`);
  }

  return await response.json();
}
Returns: Title, extract, thumbnail, content URLs
The REST API v1 endpoint provides pre-formatted summaries optimized for mobile/extension use.
Fallback when direct page fetch fails:
background.js:94
async function searchWikipediaAPI(term, language) {
  const wikiDomain = language === 'ar' ? 'ar.wikipedia.org' : 'en.wikipedia.org';
  const url = `https://${wikiDomain}/w/api.php?action=opensearch&search=${encodeURIComponent(term)}&limit=1&format=json&origin=*`;

  const response = await fetch(url);

  if (!response.ok) {
    throw new Error(`HTTP ${response.status}`);
  }

  const data = await response.json();

  if (data[1] && data[1].length > 0) {
    return data[1][0]; // Return best match title
  }

  return null;
}
Returns: Best matching article title

3. Wiktionary Definitions

Uses Wiktionary REST API:
background.js:114
async function fetchWiktionaryData(term, language) {
  const wikiDomain = language === 'ar' ? 'ar.wiktionary.org' : 'en.wiktionary.org';
  const url = `https://${wikiDomain}/api/rest_v1/page/definition/${encodeURIComponent(term)}`;

  const response = await fetch(url);

  if (!response.ok) {
    throw new Error(`HTTP ${response.status}`);
  }

  return await response.json();
}
Returns: Definitions grouped by language and part of speech

4. Free Dictionary API

Fallback for dictionary definitions:
background.js:128
async function fetchFreeDictionaryData(term, language) {
  const langCode = language === 'ar' ? 'ar' : 'en';
  const url = `https://api.dictionaryapi.dev/api/v2/entries/${langCode}/${encodeURIComponent(term)}`;

  const response = await fetch(url);

  if (!response.ok) {
    throw new Error(`HTTP ${response.status}`);
  }

  return await response.json();
}
Returns: Word, phonetics, audio URL, meanings, etymology First step for Facts tab:
background.js:142
async function searchWikidataEntity(term, language) {
  const langCode = language === 'ar' ? 'ar' : 'en';
  const url = `https://www.wikidata.org/w/api.php?action=wbsearchentities&search=${encodeURIComponent(term)}&language=${langCode}&limit=1&format=json&origin=*`;

  const response = await fetch(url);

  if (!response.ok) {
    throw new Error(`HTTP ${response.status}`);
  }

  const data = await response.json();

  if (data.search && data.search.length > 0) {
    return data.search[0]; // Returns { id, label, description }
  }

  return null;
}
Returns: Entity ID (e.g., “Q42” for Douglas Adams)

6. Wikidata Entity Details

Fetches structured facts:
background.js:162
async function fetchWikidataEntity(entityId, language) {
  const url = `https://www.wikidata.org/wiki/Special:EntityData/${entityId}.json`;

  const response = await fetch(url);

  if (!response.ok) {
    throw new Error(`HTTP ${response.status}`);
  }

  const data = await response.json();
  const entity = data.entities[entityId];

  if (!entity) {
    throw new Error('Entity not found');
  }

  const langCode = language === 'ar' ? 'ar' : 'en';
  const fallbackLang = langCode === 'ar' ? 'en' : 'ar';

  // Extract label and description
  const label = entity.labels?.[langCode]?.value 
    || entity.labels?.[fallbackLang]?.value 
    || entityId;
  const description = entity.descriptions?.[langCode]?.value 
    || entity.descriptions?.[fallbackLang]?.value 
    || '';

  // Extract claims/properties
  const claims = entity.claims || {};
  const facts = [];

  // Property mapping: property ID -> human-readable label
  const propertyMap = {
    P31: language === 'ar' ? 'النوع' : 'Instance of',
    P569: language === 'ar' ? 'تاريخ الميلاد' : 'Born',
    P570: language === 'ar' ? 'تاريخ الوفاة' : 'Died',
    P27: language === 'ar' ? 'الجنسية' : 'Country',
    P106: language === 'ar' ? 'المهنة' : 'Occupation',
    // ... 20+ more properties
  };

  // Extract values for each property
  for (const propId of Object.keys(propertyMap)) {
    if (!claims[propId]) continue;

    const claim = claims[propId];
    const values = [];

    for (let i = 0; i < Math.min(claim.length, 3); i++) {
      const mainsnak = claim[i].mainsnak;
      if (!mainsnak || !mainsnak.datavalue) continue;

      const value = extractWikidataValue(mainsnak, langCode, entity);
      if (value) values.push(value);
    }

    if (values.length > 0) {
      facts.push({
        label: propertyMap[propId],
        value: values.join(', ')
      });
    }
  }

  return { label, description, facts };
}
Returns: Label, description, array of structured facts
Wikidata properties are identified by P-codes (e.g., P569 = date of birth). The worker maps these to readable labels in the user’s language.

7. Wikidata Value Extraction

Parses different Wikidata value types:
background.js:308
function extractWikidataValue(snak, langCode, entity) {
  const datavalue = snak.datavalue;
  if (!datavalue) return null;

  switch (datavalue.type) {
    case 'wikibase-entityid': {
      const id = datavalue.value.id;
      return id; // Could be enriched with another fetch
    }
    case 'time': {
      const time = datavalue.value.time;
      // Parse +YYYY-MM-DDT00:00:00Z format
      const match = time.match(/([+-]\d+)-(\d{2})-(\d{2})/);
      if (match) {
        const year = parseInt(match[1]);
        const month = parseInt(match[2]);
        const day = parseInt(match[3]);
        if (month === 0 && day === 0) return `${Math.abs(year)}`;
        const date = new Date(Math.abs(year), month - 1, day);
        return date.toLocaleDateString(langCode, {
          year: 'numeric',
          month: 'long',
          day: 'numeric'
        });
      }
      return time;
    }
    case 'quantity': {
      const amount = parseFloat(datavalue.value.amount);
      return amount.toLocaleString(langCode);
    }
    case 'monolingualtext':
      return datavalue.value.text;
    case 'string':
      return datavalue.value;
    case 'globecoordinate': {
      const lat = datavalue.value.latitude.toFixed(4);
      const lon = datavalue.value.longitude.toFixed(4);
      return `${lat}, ${lon}`;
    }
    default:
      return null;
  }
}
Wikidata uses structured data types (time, quantity, coordinates) that need special parsing for human-readable display.

8. OpenAI API

Handles AI features (Explain Simply, chat, translate):
background.js:252
async function callOpenAI(messages, language, maxTokens = 500) {
  const result = await chrome.storage.local.get(['openaiKey']);
  const apiKey = result.openaiKey;

  if (!apiKey) {
    throw new Error('NO_API_KEY');
  }

  const response = await fetch('https://api.openai.com/v1/chat/completions', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${apiKey}`
    },
    body: JSON.stringify({
      model: 'gpt-5-nano',
      messages: messages,
      max_completion_tokens: maxTokens,
      reasoning_effort: 'minimal'
    })
  });

  if (!response.ok) {
    const status = response.status;
    let errorDetail = '';
    try {
      const errorData = await response.json();
      errorDetail = errorData.error?.message || JSON.stringify(errorData);
    } catch (e) {
      errorDetail = await response.text();
    }

    console.error('OpenAI API error:', status, errorDetail);

    // Map HTTP errors to user-friendly codes
    if (status === 401) {
      throw new Error('INVALID_API_KEY');
    } else if (status === 429) {
      throw new Error('RATE_LIMITED');
    } else if (status === 402) {
      throw new Error('INSUFFICIENT_QUOTA');
    } else if (status === 400) {
      throw new Error(`INVALID_REQUEST: ${errorDetail}`);
    }
    throw new Error(`HTTP ${status}: ${errorDetail}`);
  }

  const data = await response.json();

  if (!data.choices || data.choices.length === 0) {
    throw new Error('No response from AI');
  }

  return data.choices[0].message.content;
}
Parameters:
  • messages: Array of { role, content } objects (system, user, assistant)
  • language: ‘en’ or ‘ar’ (used for error handling in content script)
  • maxTokens: Response length limit (default 500, 1000 for translate)
Error Handling: Maps HTTP status codes to semantic error strings:
  • 401 → INVALID_API_KEY
  • 429 → RATE_LIMITED
  • 402 → INSUFFICIENT_QUOTA
  • 400 → INVALID_REQUEST
The API key is stored in chrome.storage.local (not sync) for security. It’s never exposed to content scripts or web pages.

API Key Management

Migration handler ensures keys move to local storage:
background.js:6
chrome.runtime.onInstalled.addListener(async () => {
  const sync = await chrome.storage.sync.get(['openaiKey']);
  if (sync.openaiKey) {
    await chrome.storage.local.set({ openaiKey: sync.openaiKey });
    await chrome.storage.sync.remove('openaiKey');
  }
});
Why local storage? API keys are sensitive credentials. Local storage is device-specific and not synced across browsers, reducing exposure risk.

Error Handling

Consistent error formatting:
background.js:17
function toErrMsg(error) {
  return (error instanceof Error ? error.message : String(error)) || 'Unknown error';
}
All handlers use try-catch with sendResponse({ success: false, error: ... }).

CORS Bypass Mechanism

How it works:
  1. Content script (restricted by CORS):
    // This would fail due to CORS:
    // fetch('https://en.wikipedia.org/api/...')
    
    // Instead, send message to background:
    chrome.runtime.sendMessage({
      action: 'fetchWikipedia',
      term: 'Quantum Computing',
      language: 'en'
    });
    
  2. Background worker (CORS doesn’t apply):
    // This succeeds because extension has host_permissions
    const response = await fetch('https://en.wikipedia.org/api/...');
    
  3. Host permissions in manifest.json:
    "host_permissions": [
      "https://en.wikipedia.org/*",
      "https://api.openai.com/*"
    ]
    
Extensions with host_permissions can bypass CORS for listed domains. This is a core feature of the Chrome Extension platform.

Service Worker Lifecycle

Manifest V3 service workers:
  • Idle when inactive: Automatically terminates after ~30 seconds of inactivity
  • Event-driven: Wakes up when messages arrive
  • No persistent state: Use chrome.storage for data that must survive restarts
The background worker in Knowledge Tooltip is stateless—all data is passed through messages or stored in chrome.storage.

Performance Characteristics

  • Parallel requests: Content script can send multiple messages simultaneously
  • No queue buildup: Each message has its own promise/callback
  • Automatic throttling: Browser limits concurrent requests per domain

Security Considerations

  1. No eval/remote code: Manifest V3 prohibits eval() and remote code execution
  2. API key isolation: Keys stored in local storage, never transmitted to content scripts
  3. HTTPS only: All external APIs use HTTPS
  4. Minimal permissions: Only requests necessary host permissions

Next Steps

Message Passing

Communication protocol and examples

Content Script

How the UI consumes API data

Build docs developers (and LLMs) love