I'm learning how to build chrome extensions with manifest v3, what I'm trying to do is the following
In my extension background.js (service worker) I want to do this:
I need to do these tasks while not relying on having a port open with the popup or a content script.
I'm using Chrome Alarms to wake up the service worker
it may sound weird that I have to reconnect every time the service worker wakes up considering chrome is shutting the service worker down like every 15s or less once I close the extensions dev tools (which makes me cry blood) but it is better than sending XHR periodically using Chrome alarms, which would result in a lot more requests being sent to an API, so reconnecting to the Websocket seems less problematic.
I'm having a super hard time debugging my service worker (background script) in my chrome extension. The problem is when I have dev tools open the service worker will NEVER go inactive, and what I'm trying to do is identify when the SW wakes up to perform tasks, super-duper weird because I need dev tools open to debugging...
how do you debug an extension SW without devtools open?
do you/anyone reading this have any recommendations/thoughts on what I want to do with this extension and the pain process for debugging the SW?
here is the code I have for the background.js
const extension = {
count: 0,
disconnected: false,
port: {} as any,
ws: null,
};
const fetchData = () => {
return fetch(
'https://api.coingecko.com/api/v3/coins/ethereum?localization=incididuntelit&tickers=false&market_data=true&community_data=true&developer_data=true&sparkline=true'
).then((res) => res.json());
};
// Chrome Alarms
const setupAlarms = () => {
console.log('###ALARMS-SETUP');
chrome.alarms.get('data-fetch', (alarm) => {
if (!alarm) {
chrome.alarms.create('data-fetch', { periodInMinutes: 0.1 });
}
});
chrome.alarms.get('client-pinging-server', (alarm) => {
if (!alarm) {
chrome.alarms.create('client-pinging-server', { periodInMinutes: 0.1 });
}
});
chrome.alarms.onAlarm.addListener((e) => {
if (e.name === 'data-fetch') {
fetchData()
.then((res) => {
// console.log('###ETHEREUM:', res.market_data.current_price.cad);
chrome.action.setBadgeText({ text: `${Math.round(Math.random() * 100)}` });
})
.catch((error) => console.error('###ERROR', error));
}
if (e.name === 'client-pinging-server') {
if (!extension.ws || !extension.ws.getInstance()) {
console.log('\n');
console.log('###reconnecting...', extension.ws);
console.log('\n');
extension.ws = WebSocketClient();
extension.ws.connect();
}
if (extension.ws.getInstance()) {
console.log('###sending-client-ping');
extension.ws.getInstance().send(JSON.stringify({ message: 'client ping - keep alive' }));
}
}
});
};
// ON INSTALL
chrome.runtime.onInstalled.addListener(async (e) => {
const API_URL = 'ws://localhost:8080';
chrome.storage.local.set({ apiUrl: API_URL, count: 0 });
console.info('###Extension installed', e);
chrome.action.setBadgeText({ text: '0' });
chrome.action.setBadgeBackgroundColor({ color: '#FF9900' });
});
// ON SUSPEND
chrome.runtime.onSuspend.addListener(() => {
console.log('Unloading.');
chrome.action.setBadgeText({ text: `off` });
});
function WebSocketClient() {
let instance = null;
const connect = () => {
return new Promise((resolve, reject) => {
const ws = new WebSocket('ws://localhost:8080');
const onOpen = () => {
instance = ws;
console.log('###websocket:connected', instance);
return resolve(ws);
};
const onError = (event) => {
console.log('###INIT-FAILED', event);
ws.close(1000, 'closing due to unknown error');
return reject('failed to connect to websocket');
};
const onClose = () => {
console.log('###websocket:disconnected');
instance = null;
// reconnect is happening in the alarm callback
};
ws.onopen = onOpen;
ws.onerror = onError;
ws.onclose = onClose;
});
};
const getInstance = () => {
return instance;
};
return {
connect,
getInstance,
};
}
self.addEventListener('install', async (event) => {
console.log('====install', event);
chrome.action.setBadgeBackgroundColor({ color: '#a6e22e' });
});
self.addEventListener('activate', async (event) => {
console.log('====activate', event);
chrome.action.setBadgeBackgroundColor({ color: '#FF9900' });
extension.ws = WebSocketClient();
extension.ws.connect();
setupAlarms();
});
self.addEventListener('push', function (event) {
// Keep the service worker alive until the notification is created.
event.waitUntil(
self.registration.showNotification('Testing PUSH API', {
body: 'coming from push event',
})
);
});
Since Devtools can attach to multiple contexts at once, you can open it for another context so the SW will be secondary and thus will be able to unload normally.
chrome-extension://ID/manifest.json
where ID
is the extension's idApplication
tab, then choose Service worker
on the left.start
(if shown) to start the service worker, click the background script name to open it in the Sources panel, set breakpoints, etc.stop
to stop the service worker, optionally click Update
at the top, and skip waiting
in the middle (if shown) to force an update.start
again - your breakpoints will trigger.chrome-extension://ID/manifest.json
where ID
is the extension's idApplication
tab, then choose Service worker
on the left.start
button (or stop
first, then start
). In certain cases you may also see skip waiting
in the middle, click it then.When the service worker is started you can see its context in the Sources panel on the left (in the files panel), on the top-right (in the threads panel), in the console toolbar (the context selector).
This may seem unwieldy at first, but once you try and get the knack of it, it's quite trivial and can even beat devtools that's shown when clicking the "service worker" link in chrome://extensions page because this one a) shows extension's localStorage/IndexedDB in devtools, b) provides control over service worker lifetime/execution, c) supports performance profiling of SW startup.
Note that the ManifestV3 documentation's claims about benefits provided by service workers for extensions are largely exaggerated or completely false, e.g. in your case it's clear that restarting the service worker is bad, so you should use a port to prolong the SW's lifetime as much as possible.