return result of async function in synchronous function

520 Views Asked by At

I am implementing a custom render function for marked which will check a few conditions on images, does an async request and then returns an image from a different source. However, since the new request is asynchronous, I will only ever get a promise back instead of a "real" image url.

attachmentService.getBlobUrl is an async function which does an http request and returns a promise.

My render function looks like this:

marked.use({
    renderer: {
        image: (src, title, text) => {
            if (someCondition) {

                // Some code of parsing the src attribute
                // ...

                return this.attachmentService.getBlobUrl(attachment)
                    .then(url => {
                        return Promise.resolve(`<img src="${url}" alt="${text}" title="${title}"/>`)
                    })
            }

            return `<img src="${src}" alt="${text}" title="${title}"/>`
        },
    }
})

I already tried returning the image tag directly:

// ...
return this.attachmentService.getBlobUrl(attachment)
    .then(url => {
        return `<img src="${url}" alt="${text}" title="${title}"/>`
    })
// ...

I have also tried wrapping the function in an async call and returning that (not wrapped in a Promise.resolve):

// ...
return (async () => {
    return await this.attachmentService.getBlobUrl(attachment)
        .then(url => {
            return `<img src="${url}" alt="${text}" title="${title}"/>`
        })
})()
// ...

However, this also only gives me a promise.

I cannot use await because the render function itself must be synchronous - that's not something I have control over.

3

There are 3 best solutions below

2
On

You can defer your asynchronous operation:

  1. In your custom renderer, just add some unique class name to the img elements that should be treated differently. You could also change the src attribute to some loading image.
  2. Then, before any of these elements are getting rendered, create a MutationObserver and only listen for those elements getting added. In the MutationObserver's callback you can then perform your asynchronous operation and update the element's src.
1
On

the render function itself must be synchronous

Then it is not possible to use an asynchronous function like getBlobUrl in there. You can never make this work, it's completely impossible.

Instead, you will need to redesign your approach. Evaluate the conditions and do the async requests before calling marked. Then pass in the data that you can render synchronously.

0
On

I ended up adding a new class to the img elements requiring special treatment and looped over them after the markdown was compiled to html:

marked.use({
    renderer: {
        image: (src, title, text) => {

            title = title ? ` title="${title}` : ''

            if (someCondition) {
                return `<img data-src="${src}" alt="${text}" ${title} class="attachment-image"/>`
            }

            return `<img src="${src}" alt="${text}" ${title}/>`
        },
    }
})

this.preview = DOMPurify.sanitize(marked(this.text))

// Since the render function is synchronous, we can't do async http requests in it.
// Therefore, we can't resolve the blob url at (markdown) compile time.
// To work around this, we modify the url after rendering it in the vue component.
// We're doing the whole thing in the next tick to ensure the image elements are
// available in the dom tree. If we're calling this right after setting this.preview 
// it could be the images were already made available.
this.$nextTick(() => {
    document.getElementsByClassName('attachment-image').forEach(img => {

        // ...
        // some code ...
        // ...

        this.attachmentService.getBlobUrl(attachment)
            .then(url => {
                img.src = url
            })
    })
})

This is happening inside a vue js component which renders this.preview as v-html to an element.

Calling this in this.$nextTick is needed to ensure the img elements are available in order to make modifications to them.

This solution is kind of what @shilch proposed, but more "vue-y" I guess.