Using chrome.tabs.executeScript to execute asynchronous function

I have a function that I want to execute on a page using chrome.tabs.executeScript

that is triggered from a browser action popup. The permissions are set up correctly and work fine with a synchronous callback:

chrome.tabs.executeScript(
    tab.id, 
    { code: `(function() { 
        // Do lots of things
        return true; 
    })()` },
    r => console.log(r[0])); // Logs true

      

The problem is that the function I want to call goes through multiple callbacks, so I want to use async

and await

:

chrome.tabs.executeScript(
    tab.id, 
    { code: `(async function() { 
        // Do lots of things with await
        return true; 
    })()` },
    async r => {
        console.log(r); // Logs array with single value [Object]
        console.log(await r[0]); // Logs empty Object {}
    }); 

      

The problem is that the result of the callback r

. It should be an array of the script's results, so I expect to r[0]

be a promise to be resolved when the script ends.

The Promise syntax (with .then()

) doesn't work either.

If I execute the same function on the page, it returns the promise as expected and it can be expected.

Any idea what I am doing wrong and is there any way out there?

+5


source to share


2 answers


The problem is that events and native objects are not directly accessible between the page and the extension. Basically, you get a serialized copy, something similar to what you would get if you did JSON.parse(JSON.stringify(obj))

.

This means that some of its own objects (for example, new Error

or new Promise

) will be cleared (become {}

), events will be lost, and no promise implementation can work across the border.

The solution is to use chrome.runtime.sendMessage

to return the message in the script and chrome.runtime.onMessage.addListener

in popup.js to listen to it:

chrome.tabs.executeScript(
    tab.id, 
    { code: '(async function() { 
        // Do lots of things with await
        let result = true;
        chrome.runtime.sendMessage(result, function (response) {
            console.log(response); // Logs 'true'
        });
    })()' }, 
    async emptyPromise => {

        // Create a promise that resolves when chrome.runtime.onMessage fires
        const message = new Promise(resolve => {
            const listener = request => {
                chrome.runtime.onMessage.removeListener(listener);
                resolve(request);
            };
            chrome.runtime.onMessage.addListener(listener);
        });

        const result = await message;
        console.log(result); // Logs true
    }); 

      

I've expanded this to a functionchrome.tabs.executeAsyncFunction

(as the part that "promises" the entire API): chrome-extension-async



function setupDetails(action, id) {
    // Wrap the async function in an await and a runtime.sendMessage with the result
    // This should always call runtime.sendMessage, even if an error is thrown
    const wrapAsyncSendMessage = action =>
        '(async function () {
    const result = { asyncFuncID: '${id}' };
    try {
        result.content = await (${action})();
    }
    catch(x) {
        // Make an explicit copy of the Error properties
        result.error = { 
            message: x.message, 
            arguments: x.arguments, 
            type: x.type, 
            name: x.name, 
            stack: x.stack 
        };
    }
    finally {
        // Always call sendMessage, as without it this might loop forever
        chrome.runtime.sendMessage(result);
    }
})()';

    // Apply this wrapper to the code passed
    let execArgs = {};
    if (typeof action === 'function' || typeof action === 'string')
        // Passed a function or string, wrap it directly
        execArgs.code = wrapAsyncSendMessage(action);
    else if (action.code) {
        // Passed details object https://developer.chrome.com/extensions/tabs#method-executeScript
        execArgs = action;
        execArgs.code = wrapAsyncSendMessage(action.code);
    }
    else if (action.file)
        throw new Error('Cannot execute ${action.file}. File based execute scripts are not supported.');
    else
        throw new Error('Cannot execute ${JSON.stringify(action)}, it must be a function, string, or have a code property.');

    return execArgs;
}

function promisifyRuntimeMessage(id) {
    // We don't have a reject because the finally in the script wrapper should ensure this always gets called.
    return new Promise(resolve => {
        const listener = request => {
            // Check that the message sent is intended for this listener
            if (request && request.asyncFuncID === id) {

                // Remove this listener
                chrome.runtime.onMessage.removeListener(listener);
                resolve(request);
            }

            // Return false as we don't want to keep this channel open https://developer.chrome.com/extensions/runtime#event-onMessage
            return false;
        };

        chrome.runtime.onMessage.addListener(listener);
    });
}

chrome.tabs.executeAsyncFunction = async function (tab, action) {

    // Generate a random 4-char key to avoid clashes if called multiple times
    const id = Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1);

    const details = setupDetails(action, id);
    const message = promisifyRuntimeMessage(id);

    // This will return a serialised promise, which will be broken
    await chrome.tabs.executeScript(tab, details);

    // Wait until we have the result message
    const { content, error } = await message;

    if (error)
        throw new Error('Error thrown in execution script: ${error.message}.
Stack: ${error.stack}')

    return content;
}

      

This executeAsyncFunction

can then be called like this:

const result = await chrome.tabs.executeAsyncFunction(
    tab.id, 
    // Async function to execute in the page
    async function() { 
        // Do lots of things with await
        return true; 
    });

      

This wraps chrome.tabs.executeScript

and chrome.runtime.onMessage.addListener

, and wraps the script in try

- finally

, before calling chrome.runtime.sendMessage

to resolve the promise.

+5


source


Passing promises from page to content script does not work, the solution is to use chrome.runtime.sendMessage and only send simple data between the two worlds, e.g . :



function doSomethingOnPage(data) {
  fetch(data.url).then(...).then(result => chrome.runtime.sendMessage(result));
}

let data = JSON.stringify(someHash);
chrome.tabs.executeScript(tab.id, { code: '(${doSomethingOnPage})(${data})' }, () => {
  new Promise(resolve => {
    chrome.runtime.onMessage.addListener(function listener(result) {
      chrome.runtime.onMessage.removeListener(listener);
      resolve(result);
    });
  }).then(result => {
    // we have received result here.
    // note: async/await are possible but not mandatory for this to work
    logger.error(result);
  }
});

      

0


source







All Articles