Dominik Dominik - 1 month ago 7
Node.js Question

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?

Answer Source

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))