Search code examples
javascriptobjectpromisefilereadertraversal

Resolve any number promises nested inside of an object


I have an object with nested File instances in various locations. I would like to recursively go through the object, check if an object is an instanceof File, use a promise to create a data url from the instance, and resolve the promise only when all of the promises have been resolved.

I have an existing functions that returns a Promise and resolves when the data URL from the file is ready.

export const parsePhoto = (file) => {
    return new Promise((resolve, reject) => {
        const reader = new FileReader();

        try {
            reader.readAsDataURL(file);

            reader.onloadend = () => {
                return resolve(reader.result);
            }
        } catch(e) {
            console.warn('Could not upload photo', e.target);
        }
    })
}

I have a function to recursively look for a File in the object.

export const convertPhotosToBase64 = (values) => {
    if (!values) return values;

    const converted = Object.keys(values).reduce((acc, key) => {
        if (values[key] instanceof File) {
            // Do something here
            acc[key] = parsePhoto(values[key]);
        }

        if (isArray(values[key])) {
            acc[key] = values[key].map(value => {
                if (typeof value === 'object' && !isArray(value)) {
                    return convertPhotosToBase64(value);
                }

                return value;
            })
        }

        // Recurse if object
        if (typeof values[key] === 'object' && !isArray(values[key])) {
            acc[key] = convertPhotosToBase64(values[key]);
        }

        return acc;
    }, values);

    return converted;
}

I want to keep the existing structure of the object passed (values) and only replace the File instances with the base64 string.

I'm also aware of Promise.all but unsure how to use it in this context.

How can I return convertPhotosToBase64 as a promise that resolves when all of the files have been converted to base64 strings?


Solution

  • Let's first simplify your function a bit, to reduce the duplication of all those conditions:

    export function convertPhotosToBase64(value) {
        if (typeof value !== 'object') return value;
    
        if (value instanceof File) return parsePhoto(value);
    
        if (isArray(value)) return value.map(convertPhotosToBase64);
    
        return Object.keys(value).reduce((acc, key) => {
            acc[key] = convertPhotosToBase64(value[key]);
            return acc;
        }, {});
    }
    

    Now, parsePhoto is asynchronous and returns a promise. This means that the whole convertPhotosToBase64 will need to become asynchronous and always return a promise. Given the four clearly distinct cases, that's actually simpler than it sounds:

    export function convertPhotosToBase64(value) {
        // wrap value
        if (typeof value !== 'object') return Promise.resolve(value);
    
        // already a promise
        if (value instanceof File) return parsePhoto(value);
    
        // map creates all the promises in parallel, use `Promise.all` to await them
        if (isArray(value)) return Promise.all(value.map(convertPhotosToBase64));
    
        // chain one after the other
        return Object.keys(value).reduce((accP, key) =>
            accP.then(acc =>
                convertPhotosToBase64(value[key]).then(res => {
                    acc[key] = res;
                    return acc;
                })
            )
        , Promise.resolve({}));
    }
    

    If you are ok with doing everything in parallel (not only the arrays), you can also simplify the last case to

        return Object.keys(value).reduce((accP, key) =>
            Promise.all([accP, convertPhotosToBase64(value[key])]).then([acc, res] => {
                acc[key] = res;
                return acc;
            })
        , Promise.resolve({}));
    

    or maybe better

        const keys = Object.keys(value);
        return Promise.all(keys.map(key => convertPhotosToBase64(value[key])).then(results => {
            const acc = {};
            for (const [key, i] of keys.entries())
                acc[key] = results[i];
            return acc;
        });