0

tl;dr I'm trying to return a data object in a synchronous function (which cannot be declared async due to the API constraints), but some of my data is in the form of Promise objects. I can delay the callback until after the promises are resolved with Promise.all([<promises>].then(callback), but I'm still returning a data object full of Promises. How do I replace all Promises with their resolved values in the returned data object without having to go through every last item one-by-one?

Background

In this specific case, I'm trying to write a Chrome extension. If you aren't aware, Chrome extensions are divided into two parts: the background "service worker" that acts as the central extension script, and the "content scripts" that actually act on each page. There are many things that can only be done from the background script, such as getting the current tab of the window you're operating on. This necessitates a way to call functions from the content script through the message API. I'm limiting my frustration at this to a minimum for the purposes of this question.

Details

My current API looks something like this:

chrome.runtime.onMessage.addListener(messageHandler)

// Example "message" object: { get: { globalFunctions: ['getCurrentTab'] } }
function messageHandler(message, sender, callback) {
    const responseObject = {}
    const promises = []
    if (message.get) {
        const get = message.get
        const res = {}
        if (get.globalFunctions) {
            const funcNames = conformToStringArr(get.globalFunctions)
            if (!funcNames)
                return;
            
            res.globalFunctions = {}
            funcNames.forEach((funcName) => {
                res.globalFunctions[funcName] = globalFunctions[funcName](message, sender) // Functions can be async because they have to asynchronously retrieve values
            })
        }
        if (get.globalValues) {
            const globalNames = conformToStringArr(get.globalValues)
            if (!globalNames)
                return;
            
            res.globalValues = {}
            globalNames.forEach((valueName) => {
                res.globalValues[valueName] = globalValues[valueName]
            })
        
        }
        if (get.localStorage) {
            const toGet = conformToStringArr(get.localStorage)
            if (!toGet)
                return;
            
            res.localStorage = chrome.storage.local.get(toGet) // : Promise
        }
        
        responseObject.get = res
    }
    callback(responseObject)
    
    return true; // Required due to restrictions of the message API. I cannot return a Promise or use an asynchronous function.
}

Essentially, I'm doing this as though it were synchronous: filling an object with the relevant values and returning it. The problem is that sometimes these values are Promises, and Promise references do not automatically resolve to their values as far as I am aware. I'm starting to get a handle on how Promises work in terms of control-flow, but this kind of value-replacement is something that no one seems to mention online.

One thing I could do is only return the object once all of the promises are fulfilled by adding all Promises to an array and end the function with Promise.all(promises).then(callback), but that will still return an object full of Promises, right?

How do I return an object free of Promises from a non-async function like this?

SOLUTION

Wrap the assignment in a Promise instead. For example, instead of doing outObject.bar = asyncFoo() (where asyncFoo returns a Promise), do asyncFoo().then((value) => outObject.bar = value). To only return the object once all of those values have been filled in, add each promise to a "promises" array and put Promise.allSettled(promises).then(callback(outObject)) at the end of the function.

Final code:

function messageHandler(message, sender, callback) {
    const responseObject = {}
    const promises = []
    if (message.get) {
        const get = message.get
        responseObject.get = {}
        if (get.globalFunctions) {
            const funcNames = conformToStringArr(get.globalFunctions) 
            if (funcNames) {
                responseObject.get.globalFunctions = {}
                funcNames.forEach((name) => {
                    const functionResult = globalFunctions[name](message, sender)
                    if (functionResult instanceof Promise)
                        promises.push(functionResult.then((value) => responseObject.get.globalFunctions[name] = value))
                    else
                        responseObject.get.globalFunctions[name] = functionResult
                })
            }
        }
        if (get.globalValues) {
            const globalNames = conformToStringArr(get.globalValues)
            if (globalNames) {
                responseObject.get.globalValues = {}
                globalNames.forEach((valueName) => {
                    responseObject.get.globalValues[valueName] = globalValues[valueName]
                })
            }
        }
        if (get.localStorage) {
            const toGet = conformToStringArr(get.localStorage)
            if (toGet)
                promises.push(chrome.storage.local.get(toGet).then((value) => responseObject.get.localStorage = value))
        }
        
    }
    if (promises.length > 0) {
        Promise.allSettled(promises).then(() => callback(responseObject))
    } else {
        callback(responseObject)
    }
    
    return true;
}
Mark Rotteveel
  • 100,966
  • 191
  • 140
  • 197
  • 1
    Declare your function to be `async`. Then you can use `await` to call those functions and assign the results to the object properties. – Barmar Nov 14 '22 at 17:18
  • 3
    "*How do I return an object free of Promises from an asynchronous function*" - you cannot. It's asynchronous. – Bergi Nov 14 '22 at 17:38
  • I cannot declare the function to be `async`. It is forced to be a completely-synchronous function to operate within the API. – ByThePowerOfScience Nov 14 '22 at 17:47
  • @Bergi It's a *synchronous* function, not an asynchronous function. – ByThePowerOfScience Nov 14 '22 at 17:54
  • 1
    @ByThePowerOfScience Yes, no. It's doing asynchronous things, so it can't be synchronous, even if you'd like to have that. – Bergi Nov 14 '22 at 17:55
  • Promises are tools to manage asynchronous code. If it is, as you claim, synchronous, why do you have a promise? – Quentin Nov 14 '22 at 17:56
  • I got mixed up there trying to explain my point. The thing is, it's doing asynchronous things, but I need real values in the object that's returned. – ByThePowerOfScience Nov 14 '22 at 17:58
  • 2
    Needing something doesn't make it possible. You have a promise of some data *in the future*. You can have that data in the future. You can't have it now. It isn't here yet. – Quentin Nov 14 '22 at 17:59
  • @ByThePowerOfScience Judging from [the docs](https://developer.chrome.com/docs/extensions/mv3/messaging/), there is no reason for the message handler to be synchronous. Just do `chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { messageHandler(message, sender).then(sendResponse); return true; });` with `async function messageHandler(message, sender) { … await …; return … }` – Bergi Nov 14 '22 at 18:01
  • I'm delaying the execution of the callback until after the data is available with `Promise.all([promises]).then(callback(dataObject))`, but is there any way to un-promise those values inside of the object I'm returning now that the values exist? Trying to coordinate the data object with the array of promises seems way too complicated for something that must be a common issue. – ByThePowerOfScience Nov 14 '22 at 18:03
  • @Bergi Unfortunately, that doesn't work. See https://stackoverflow.com/questions/53024819/chrome-extension-sendresponse-not-waiting-for-async-function/53024910#53024910 – ByThePowerOfScience Nov 14 '22 at 18:04
  • @ByThePowerOfScience My comment has exactly what the "solution" in the answer you linked is – Bergi Nov 14 '22 at 18:06
  • @Bergi Oh, I see. It was difficult to understand with the inline code. I'll try that. I do wish there were a `Promise#value` function that let you just get the value of the promise if it has been resolved. – ByThePowerOfScience Nov 14 '22 at 18:08
  • @ByThePowerOfScience — That's `then` (and `await`). – Quentin Nov 14 '22 at 18:10
  • If I remember right, there were a few promise implementations that would let you get the status of a promise whenever you wanted. But that leads you down the path of trying to force an asynchronous flow towards something synchronous which will get you in trouble and cause timing issues. That's why modern implementations drop the idea and force you to use `then()`. – zero298 Nov 14 '22 at 18:14

0 Answers0