Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

chrome.runtime.onMessage response with async await

I want to use async await in an onMessage listener:

chrome.runtime.onMessage.addListener(async (request, sender, sendResponse) =>{
    var key = await getKey();
    sendResponse(key);
});

However I get undefined when I send a message.

From the documentation for chrome.runtime.onMessage.addListener:

This function becomes invalid when the event listener returns, unless you return true from the event listener to indicate you wish to send a response asynchronously (this will keep the message channel open to the other end until sendResponse is called).

This works when I use a callback.

chrome.runtime.onMessage.addListener(async (request, sender, sendResponse) =>{
    getKey(key => {
        sendResponse(key);
    });
    return true;
});

However I would like to leverage the await syntax. But it does not seem to work and still returns undefined:

chrome.runtime.onMessage.addListener(async (request, sender, sendResponse) =>{
    var key = await getKey();
    sendResponse(key);
    return true;
});
like image 569
Chris Avatar asked May 18 '17 19:05

Chris


4 Answers

Option #1 - Simplest

Extract to an async function.

chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
  doSomethingWith(request).then(sendResponse);
  return true; // return true to indicate you want to send a response asynchronously
});

async function doSomethingWith(request) {
  var key = await getKey();
  // await .....
  return key;
}

The return value of an async function is implicitly wrapped in Promise.resolve. See async doc.

The return true; statement did the trick. It tells Chrome that you want to send a response asynchronously.

See onMessage.

Option #2 - Utility

If you feel that you will use it most frequently, create a utility, something like:

const wrapAsyncFunction = (listener) => (request, sender, sendResponse) => {
  // the listener(...) might return a non-promise result (not an async function), so we wrap it with Promise.resolve()
  Promise.resolve(listener(request, sender)).then(sendResponse);
  return true; // return true to indicate you want to send a response asynchronously
};

chrome.runtime.onMessage.addListener(
  wrapAsyncFunction(async (request, sender) => {
    console.log(request, sender);

    const key = await getKey();
    // await .....
    return key;
  })
);

Option #3 - More generic

Use mozilla/webextension-polyfill if you prefer to "cross browsers" extension.

Example:

var browser = require("webextension-polyfill");

browser.runtime.onMessage.addListener(async (msg, sender) => {
  console.log("BG page received message", msg, "from", sender);
  console.log("Stored data", await browser.storage.local.get());
});

browser.browserAction.onClicked.addListener(() => {
  browser.tabs.executeScript({file: "content.js"});
});
like image 131
Ninh Pham Avatar answered Nov 09 '22 13:11

Ninh Pham


Not sure if Chrome extension runtime environment supports the async/await syntax, but you can use a transpiler (i.e. Babel) to have it converted to for example ES5. Then you can define a wrapper function like this:

function asChromeListener(listener) {
  return (message, sender, sendResponse) => {
    const returnValue = listener(message, sender);

    if (isPromise(returnValue)) {
      returnValue.then(sendResponse);
      return true;
    }
    else {
      if (typeof returnValue !== 'undefined') {
        sendResponse(returnValue);
      }
      return false;
    }
  };
}

function isPromise(value) {
  return typeof value === 'object' && value !== null && 'then' in value && 'catch' in value;
}

Which you can then use like:

chrome.runtime.onMessage.addListener(asChromeListener(async (message, sender) => {
  return await doMyAsyncWork(message);
});

Since we use TypeScript, here's also the snippet that we actually use (with generic types).

export function asChromeListener<M, S, R extends Function>(listener: (message: M, sender: S) => any) {
  return (message: M, sender: S, sendResponse: R) => {
    const returnValue = listener(message, sender);

    if (isPromise(returnValue)) {
      returnValue.then(sendResponse);
      return true;
    }
    else {
      if (typeof returnValue !== 'undefined') {
        sendResponse(returnValue);
      }
      return false;
    }
  };
}

function isPromise(value: any) {
  return typeof value === 'object' && value !== null && 'then' in value && 'catch' in value;
}
like image 25
davido Avatar answered Nov 09 '22 11:11

davido


Function to call (at most once) when you have a response. The argument should be any JSON-ifiable object. If you have more than one onMessage listener in the same document, then only one may send a response. This function becomes invalid when the event listener returns, unless you return true from the event listener to indicate you wish to send a response asynchronously (this will keep the message channel open to the other end until sendResponse is called).

https://developer.chrome.com/docs/extensions/reference/runtime/#event-onMessage

have to say the official doc is really hard to read.

like image 3
zyfyy Avatar answered Nov 09 '22 11:11

zyfyy


The closest I could get:

chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
    (async () => {
        var key = await getKey();
        sendResponse(key);
    })();
    return true;
});
like image 1
daghan Avatar answered Nov 09 '22 12:11

daghan