Search code examples
javascriptnode.jses6-promise

Building a promise chain for a deep nested object


I got a deep object:

{
  "something": "Homepage",
  "else": [
    "[replaceme]",
    "[replaceme]"
  ],
  "aside": "[replaceme]",
  "test": {
    "test": {
      "testing": [
        "[replaceme]",
        "[replaceme]",
        "variable",
        {
          "testing": {
            "testing": {
              "something": "[replaceme]",
              "testing": {
                "testing": [
                  "[replaceme]",
                  "[replaceme]"
                ]
              }
            }
          }
        }
      ]
    }
  }
}

Now I need to replace every occurrence of [replaceme] with something that comes out of a async function. It is different each time.

I thought I reduce each level of the object and return a promise chain.

This is what I got so far:

const IterateObject = ( object ) => {
    return new Promise( ( resolve, reject ) => {
        Object.keys( object ).reduce( ( sequence, current ) => {
            const key = current;

            return sequence.then( () => {
                return new Promise( ( resolve, reject ) => {

                    if( typeof object[ key ] === 'object' ) {
                        IterateObject( object[ key ] )
                            .then( result => {
                                newObject[ key ] = result;
                        });
                        // ^----- How do I add the next level when it returns a promise?
                    }
                    else {
                        resolve( newObject[ key ] )
                    }
                });
            });
        }, Promise.resolve())
        .catch( error => reject( error ) )
        .then( () => {
            console.log('done');

            resolve();
        });
    });
}

Question

What is the best way to go about this issue? Maybe a promise chain is not the right tool?


Solution

  • Do not try to do it in single a function. This may result in common anti-pattern know as The Collection Kerfuffle Split this job into independend chunks: deep traverse object, async setter, etc. Collect all pending promises by traversing your object and await them all using Promise.all

    // traverse object obj deep using fn iterator
    const traverse = (obj, fn) => {  
      const process = (acc, value, key, object) => {
        const result =
          Array.isArray(value)
            ? value.map((item, index) => process(acc, item, index, value))
            : (
                typeof value === 'object' 
                  ? Object.keys(value).map(key => process(acc, value[key], key, value))
                  : [fn(value, key, object)]
              )
        return acc.concat(...result)
      }
    
      return process([], obj)
    }
    

    // fake async op
    const getAsync = value => new Promise(resolve => setTimeout(resolve, Math.random()*1000, value))
    
    // useful async setter
    const setAsync = (target, prop, pendingValue) => pendingValue.then(val => target[prop] = val)
    
    // traverse object obj deep using fn iterator
    const traverse = (obj, fn) => {  
      const process = (acc, value, key, object) => {
        const result =
          Array.isArray(value)
            ? value.map((item, index) => process(acc, item, index, value))
            : (
                typeof value === 'object' 
                  ? Object.keys(value).map(key => process(acc, value[key], key, value))
                  : [fn(value, key, object)]
              )
        return acc.concat(...result)
      }
      
      return process([], obj)
    }
    
    // set async value
    const replace = (value, prop, target) => {
      if( value === '[replaceme]') {
        return setAsync(target, prop, getAsync(`${prop} - ${Date.now()}`))
      }
      return value
    }
       
    const tmpl = {
      "something": "Homepage",
      "else": [
    "[replaceme]",
    "[replaceme]"
      ],
      "aside": "[replaceme]",
      "test": {
    "test": {
      "testing": [
        "[replaceme]",
        "[replaceme]",
        "variable",
        {
          "testing": {
            "testing": {
              "something": "[replaceme]",
              "testing": {
                "testing": [
                  "[replaceme]",
                  "[replaceme]"
                ]
              }
            }
          }
        }
      ]
    }
      }
    }
    
    Promise.all(
      traverse(tmpl, replace)
    )
    .then(() => console.log(tmpl))
    .catch(e => console.error(e))

    Or you might want to take a look at async/await that allows you to write more "synchronous like" code. But this implementation results in sequential execution.

        // using async/await
        const fillTemplate = async (tmpl, prop, object) => {
          if(Array.isArray(tmpl)) {
            await Promise.all(tmpl.map((item, index) => fillTemplate(item, index, tmpl)))
          }
          else if(typeof tmpl === 'object') {
            await Promise.all(Object.keys(tmpl).map(key => fillTemplate(tmpl[key], key, tmpl)))
          }
          else {
            await replace(tmpl, prop, object) 
          }
        }
    

        const tmpl = {
      "something": "Homepage",
      "else": [
        "[replaceme]",
        "[replaceme]"
      ],
      "aside": "[replaceme]",
      "test": {
        "test": {
          "testing": [
            "[replaceme]",
            "[replaceme]",
            "variable",
            {
              "testing": {
                "testing": {
                  "something": "[replaceme]",
                  "testing": {
                    "testing": [
                      "[replaceme]",
                      "[replaceme]"
                    ]
                  }
                }
              }
            }
          ]
        }
      }
    }
        
        // fake async op
        const getAsync = value => new Promise(resolve => setTimeout(resolve, Math.random()*1000, value))
    
        // useful async setter
        const setAsync = (target, prop, pendingValue) => pendingValue.then(val => target[prop] = val)
    
        // set async value
        const replace = (value, prop, target) => {
          if( value === '[replaceme]') {
            return setAsync(target, prop, getAsync(`${prop} - ${Date.now()}`))
          }
          return value
        }
    
        // using async/await
        const fillTemplate = async (tmpl, prop, object) => {
          if(Array.isArray(tmpl)) {
            await Promise.all(tmpl.map((item, index) => fillTemplate(item, index, tmpl)))
          }
          else if(typeof tmpl === 'object') {
            await Promise.all(Object.keys(tmpl).map(key => fillTemplate(tmpl[key], key, tmpl)))
          }
          else {
            await replace(tmpl, prop, object) 
          }
        }
        
        Promise
          .resolve(fillTemplate(tmpl))
          .then(() => console.log(tmpl))
          .catch(e => console.error(e))