-5

Consider the content script:

(async () => {

    function pause(delay) {
        return new Promise((resolve) => setTimeout(resolve, delay));
    }

    async function myHandler() {
        await pause(1000);
        return ['something 1', 'something 2'];
    }

    function waitForMessage() {
        return new Promise((resolve) => {
            async function onMessageListener(message, sender, sendResponse) {
                chrome.runtime.onMessage.removeListener(onMessageListener);
                resolve(true);
                
                // The following response is received just fine!
                // sendResponse({'results': ['something 1', 'something 2']});

                const lines = await myHandler();
                const response = {'results': lines};

                console.log('Responding', response); // All is fine, {'results': <array of strings>}
                sendResponse(response);              // However, the popup script receives `undefined`!
            }
            chrome.runtime.onMessage.addListener(onMessageListener);
        });
    }

    waitForMessage();
})();

Here is the part of the popup script sending the request:

async function requestFromContentScript() {
    const message = { type: 'request' };
    const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
    const tabId = tabs[0].id;

    return new Promise((resolve) => {
        chrome.tabs.sendMessage(tabId, message, (result) => {
            console.log('Got in callback:', result);
            resolve(result);
        });
    });
}

As the comments indicate, if I hardcode the response, it is received by the popup script just fine. However, if the response is computed using an asynchronous call, the popup script receives undefined. How can this happen?

AlwaysLearning
  • 7,257
  • 4
  • 33
  • 68
  • 1
    What is "the popup script"? – Pointy Jul 31 '23 at 13:02
  • @Keith Because it was not reopened despite me providing a complete reproducible example. When there are >10 negative comments, there is not much chance of reviving it. – AlwaysLearning Jul 31 '23 at 13:06
  • I'd have reopened, cleared the comments and even upvoted it in a heartbeat, *if it was actually a reproducible code sample.* – deceze Jul 31 '23 at 13:07
  • You only received 1 downvote in your original, and the comments didn't seem negative. They just appeared to be asking for clarification. – Keith Jul 31 '23 at 13:09
  • @deceze I have added the relevant part of the popup script (which is a whole React extension). Is it reproducible now? – AlwaysLearning Jul 31 '23 at 13:12
  • No. I _very strongly suspect_ that you're dealing with something like this: https://stackoverflow.com/q/11284663/476. Your `myHandler` returns an array which actually gets populated asynchronously after the fact, but happens to reflect to the console log, because the console is lazy about evaluating objects. However, passing it as a message, it gets serialised and deserialised behind the scenes, and you actually only get what was in the array at the time you passed the message. – deceze Jul 31 '23 at 13:14
  • @Pointy Response by Chat GPT: In the context of a Chrome extension, a "popup script" refers to the JavaScript code that runs within the popup window of the extension when it is opened by the user. – AlwaysLearning Jul 31 '23 at 13:16
  • 1
    Have you checked `runtime.lastError`? _"If an error occurs while connecting to the specified tab, the callback is called with no arguments and `runtime.lastError` is set to the error message."_ This would explain `undefined`. – jabaa Jul 31 '23 at 13:16
  • @deceze But `sendResponse()` should not get called before the promise returned by `myHandler` is resolved, at which time the array is fully populated! – AlwaysLearning Jul 31 '23 at 13:21
  • @jabaa Oops. It says `checked runtime.lastError: Could not establish connection. Receiving end does not exist.` But why does it work when I hardcode the response? – AlwaysLearning Jul 31 '23 at 13:24
  • That's the thing: is it, or are you interpreting the console output wrong? A clear test would be `console.log(JSON.stringify(response))`. If _that_ outputs the data, then it is indeed there and we can simply rule that out as a possible cause. Next, if you actually only receive `undefined`, then you should first and foremost inspect `runtime.lastError` as advised above, and the actualy response data is completely irrelevant for the moment. – deceze Jul 31 '23 at 13:24
  • Is the `myHandler` you've updated with, what your actually testing with?. A working snippet for a Chrome extension is not something we could test easy. But just to rule out `myHandler`, what do you get with -> `console.log('Responding', JSON.stringify(response));` – Keith Jul 31 '23 at 13:24
  • Can you try to remove `chrome.runtime.onMessage.removeListener(onMessageListener);`? If it's an async call, it's evaluated on the first break. In the case of hardcoded data, the first break is after `sendResponse({'results': ['something 1', 'something 2']});`. In the case of an async function, the first break is after `const lines = await myHandler();`. – jabaa Jul 31 '23 at 13:24
  • @jabaa Made no difference. – AlwaysLearning Jul 31 '23 at 13:26
  • 1
    Can you move `resolve(true);` to end of the function? – jabaa Jul 31 '23 at 13:27
  • ^^^ @jabaa That would make sense, I don't do extensions, but I wonder if Chrome only holds onto the return during the promise lifetime..mmmm – Keith Jul 31 '23 at 13:29
  • @Keith Yes. I am testing with the precisely exact code that is shown in this question. – AlwaysLearning Jul 31 '23 at 13:30
  • @jabaa Moving `resolve()` did not make a difference either. – AlwaysLearning Jul 31 '23 at 13:33
  • Actually it's not meant to be a Promise, you should just return `true` and not the Promise constructor.. If you want to do some async stuff in there, wrap it inside an async IIFE.. – Keith Jul 31 '23 at 13:34
  • @Keith In the actual non-toy code, I call `waitForMessage()` inside an infinite loop. I proceed to waiting for a next message only when the previous one has arrived and been handled. – AlwaysLearning Jul 31 '23 at 13:40
  • Cool!!, duplicate found. Although a 3rd options for the accepted answer was just use `then`.. rather than a seperate function. – Keith Jul 31 '23 at 14:35

1 Answers1

1

I don't do extensions, but a quick look at the docs -> (message: any, sender: MessageSender, sendResponse: function) => boolean | undefined

So I've a feeling your meant to return true if you handle this message. Currently your returning a Promise.

So with my limited knowledge of extensions, I would think this might work.

function onMessageListener(message, sender, sendResponse) {
  myHandler().then(async () => {  //or an async IIFE
     const lines = await myHandler();
     const response = {'results': lines};
     sendResponse(response);
  });
  return true; 
}

chrome.runtime.onMessage.addListener(onMessageListener);
Keith
  • 22,005
  • 2
  • 27
  • 44
  • In the actual non-toy code, I call `waitForMessage()` inside an infinite loop. I proceed to waiting for a next message only when the previous one has arrived and been handled. This is why I use a promise. Also, even if you are right that it's not supposed to be a promise, you did not explain how it caused the behavior reported in the question. – AlwaysLearning Jul 31 '23 at 13:42
  • Yes, but a Promise that returns true, is not the same as `true` and the callback as stated in the docs expects `boolean | undefined`, not `boolean | undefined | Promise` – Keith Jul 31 '23 at 13:43
  • Please link to the docs. – AlwaysLearning Jul 31 '23 at 13:46
  • https://developer.chrome.com/docs/extensions/reference/runtime/#event-onMessage – Keith Jul 31 '23 at 13:47
  • @AlwaysLearning Have you tried the above, please make sure you remove the `async`.. `async function onMessageHandler`, -> `function onMessageHandler` – Keith Jul 31 '23 at 13:48
  • I have added the code I tried at the end of the question. It did not solve the issue though. – AlwaysLearning Jul 31 '23 at 14:00
  • @AlwaysLearning If you copied it verbatim, you will have errors,. I've updated to include the missing bits. If it still doesn't work, I've no idea, and I'll delete this. – Keith Jul 31 '23 at 14:01
  • 1
    I based my code on a snippet from ChatGPT. What a lesson... – AlwaysLearning Jul 31 '23 at 14:11
  • :), yeah, to be fair ChatGPT didn't do too bad a job. I've upvoted your question, because I feel this whole question / answer / debugging trial might be useful for others.. And that's what SO is all about. – Keith Jul 31 '23 at 14:25
  • But this means I cannot use `await` inside the message handler and am stuck with convoluted nested `.then`... – AlwaysLearning Jul 31 '23 at 14:34
  • @AlwaysLearning Just the 1 then, the thenable can be `async` as shown above, so no nested then's.. – Keith Jul 31 '23 at 14:36
  • Actually, [this](https://stackoverflow.com/a/46628145/2725810) suggests that I can return a promise from the message handler... I am confused now as to what the problem was... – AlwaysLearning Jul 31 '23 at 14:38
  • @AlwaysLearning No it doesn't, the workarounds are there to prevent returning a Promise. Notice how its `(request, sender, sendResponse) => ` and not `async (request, sender, sendResponse)`.. That's the important bit. – Keith Jul 31 '23 at 14:40
  • @AlwaysLearning `asynchronous` doesn't mean it's a `Promise`, and indeed returning `true` to imply the response is `asynchronous` makes total sense, because your response will be `asynchronous`.. Hope that makes sense, I know it can all seem strange, but trust me it gets easier.. :) – Keith Jul 31 '23 at 14:44
  • @AlwaysLearning Yeah, unfortunately it could be the dupe that caused a few of them. But like you say it doesn't seem fair, you actually got -5 +1. In the future maybe wait a day or so before re-posting, or if you have activity on your original post use the @ symbol to let people who were already looking at your question know you have updated etc. I also use the watch feature on SO, so if I'm on a question that still needs attention I'll get informed.. – Keith Jul 31 '23 at 15:10