I'd also like to note the
similarly over-complicated official onMessage inside the offscreen document that uses several anti-patterns: async listener is not supported by the API so instead of handling it properly via "return true" they used a separate sendMessage which requires a separate onMessage in the initiator's context, which means that it doesn't scale to more complicated use cases where the original context's data should be used with the response and the only "solution" would be to complicate even further and introduce a global map for message contexts.
The proper solution is to use sendMessage + sendResponse + return true because it's both simpler and more functional as it receives the response in the original context.
chrome.runtime.onMessage.addListener(({cmd, data}, sender, sendResponse) => {
if (cmd === 'add-exclamationmarks-to-headings') {
sendResponse(addExclamationMarksToHeadings(data));
}
});
function addExclamationMarksToHeadings(str) {
const parser = new DOMParser();
const doc = parser.parseFromString(str, 'text/html');
for (const el of doc.querySelectorAll('h1')) el.append('!!!');
return doc.documentElement.outerHTML;
}
background.js:
chrome.action.onClicked.addListener(async () => {
const res = await offscreen(
'add-exclamationmarks-to-headings',
'<html><head></head><body><h1>Hello World</h1></body></html>'
);
console.log(res);
});
async function offscreen(cmd, data) {
try {
await chrome.offscreen.createDocument({
url: 'offscreen.html',
reasons: ['DOM_PARSER'],
justification: 'MV3 requirement',
});
} catch (error) {
if (!error.message.startsWith('Only a single offscreen'))
throw error;
}
return chrome.runtime.sendMessage({cmd, data});
}
P.S. You may also want to surface the errors from the offscreen document:
// offscreen.js
chrome.runtime.onMessage.addListener(({cmd, data}, sender, sendResponse) => {
let result, error;
try {
if (cmd === 'add-exclamationmarks-to-headings') {
result = addExclamationMarksToHeadings(data);
} else throw new Error('Unknown command ' + cmd);
} catch (err) {
error = err;
}
if (result instanceof Promise) {
result.then(res => sendResponse({result: res}), err => sendResponse({error: [err.message, err.stack]}));
return true;
}
sendResponse({result, error});
});
// background.js
async function offscreen(cmd, data) {
try {
await chrome.offscreen.createDocument({
url: 'offscreen.html',
reasons: ['DOM_PARSER'],
justification: 'MV3 requirement',
});
} catch (error) {
if (!error.message.startsWith('Only a single offscreen'))
throw error;
}
const {result, error} = await chrome.runtime.sendMessage({cmd, data});
if (error) throw Object.assign(new Error(error[0]), {stack: error[1]});
return result;
}