Service worker and messaging
Add the extension service worker
Register a background service worker for browser events without assuming it remains alive or keeps global state.
So far everything in Page Notes starts from the popup. But a keyboard shortcut has no popup. Someone has to be listening when the browser fires that event, even if no extension page is open.
That someone is the extension service worker. In Manifest V3 it’s the only background context. Chrome starts it when an event arrives and stops it when there’s nothing to do.
Declaring it
Add a background section to the manifest:
{
"background": {
"service_worker": "service-worker.js",
"type": "module"
}
}
type: module lets you use import in the worker. Reload the extension and a service worker link appears on the extension card. That opens its console.
It doesn’t stay alive
This is the mental shift. The worker is not a long-running process. Chrome terminates it after about 30 seconds of inactivity, and it can be killed at other times too. The next event starts a brand new JavaScript instance.
Every global variable goes back to its initial value. A counter you incremented is zero again. A tab ID you cached is gone.
So think in independent events. Each listener reads what it needs from storage, does its work, writes back what must survive. Nothing lives in globals between events. And don’t try to keep the worker alive with timers or fake activity. It fights the platform and Chrome keeps closing that door.
Register listeners at the top level
Chrome wakes the worker for a specific event and then dispatches it. If the listener isn’t registered by the time the module finishes evaluating, the event is lost.
This is wrong:
const settings = await chrome.storage.local.get('settings')
chrome.commands.onCommand.addListener(handleCommand)
The await delays registration. Chrome may wake the worker for a command and find no listener. Register first, read inside:
chrome.commands.onCommand.addListener(async command => {
const { settings } = await chrome.storage.local.get('settings')
handleCommand(command, settings)
})
Two events people misread
chrome.runtime.onInstalled fires when the extension is installed or updated. It does not fire every time the worker starts. chrome.runtime.onStartup fires when the browser profile starts. Also not on every wake.
Neither is the place for “set things up so the next event works”. Put that setup in a function that’s safe to call many times, and call it from the event that needs it.
Try this experiment. Keep one counter in a module-level let and one in chrome.storage.session. Increment both on each command. Fire the command, wait a minute, fire it again. Open the worker console from chrome://extensions and compare. The global restarted from zero. The stored one didn’t. That’s the whole lesson in two numbers.
Lesson completed