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:
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:
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.
2. Wikipedia Search
Fallback when direct page fetch fails:
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:
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:
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
5. Wikidata Entity Search
First step for Facts tab:
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:
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.
Parses different Wikidata value types:
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):
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:
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:
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:
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'
});
Background worker (CORS doesn’t apply):
// This succeeds because extension has host_permissions
const response = await fetch ( 'https://en.wikipedia.org/api/...' );
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.
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
No eval/remote code : Manifest V3 prohibits eval() and remote code execution
API key isolation : Keys stored in local storage, never transmitted to content scripts
HTTPS only : All external APIs use HTTPS
Minimal permissions : Only requests necessary host permissions
Next Steps
Message Passing Communication protocol and examples
Content Script How the UI consumes API data