Dane Looman
Dane Looman

Reputation: 89

Optimal Syntax for object creation requiring async calls in Javascript

I have an object that has properties that are generated via async functions. I feel like it is calling one, then calling the next, etc. I don't need it to do that but I do need it all be complete before moving on. Below is how I currently do it but I am looking for a more efficient method. I have thought about calling all the functions using Promise.All() in variables. Then creating the object and setting the properties to the variables but I have a feeling someone has a more elegant solution somewhere.

let obj = {
prop1 : await functionName(something),
prop2 : await anotherFunction(somethingElse)}

Upvotes: 1

Views: 100

Answers (2)

Danny
Danny

Reputation: 590

UPDATE Using just a single function

This function match the behavior and signature of Object.assign()
but handles values that are Promises.

/**
 * @param {{[key: PropertyKey]: unknown}} target 
 * @param {...{[key: PropertyKey]: unknown | Promise<unknown>}} asyncSources 
 */
async function assignAsync(target, ...asyncSources) {

  /**
   * @param {{[key: PropertyKey]: unknown}} target
   * @param {PropertyKey} property
   * @returns {boolean}
   */
  const propertyIsEnumerable = (target, property) => {
    return Object.prototype.propertyIsEnumerable.call(target, property)
  }

  const sources = await Promise.all(
    asyncSources
    .filter(asyncSource => asyncSource != null)
    .map(async asyncSource => {

      const asyncSourceOwnEnumerableProperties = (
        Reflect.ownKeys(asyncSource)
        .filter(property => propertyIsEnumerable(asyncSource, property))
      )

      /**
       * @type {{[key: PropertyKey]: unknown}}
       */
      const source = {}

      await Promise.all(
        asyncSourceOwnEnumerableProperties.map(property => {
          const valueOrPromise = asyncSource[property]

          if (valueOrPromise instanceof Promise) {
            return valueOrPromise.then(value => {
              source[property] = value
            })
          }

          source[property] = valueOrPromise
        })
      )

      return source

    })
  )

  Object.assign(target, ...sources)

  return target

}

Usage

const data = await assignAsync({}, {
  apiValue_1: fetchAPI_1(),
  apiValue_2: fetchAPI_2(),
})

console.log(data)

Old Answer

Interesting question. I was thinking and come up with 2 solutions.

The 1st one is by using Promise.all as you mentioned:

function delay(ms = 0) {
  return new Promise(resolve => setTimeout(resolve, ms))
}

async function functionName() {
  await delay(1000)
  return 'value1'
}

async function anotherFunction() {
  await delay(500)
  return 'value2'
}

async function createAsyncObject() {
  const obj = {}

  // Set properties

  await Promise.all([
    functionName().then(v => obj.prop1 = v),
    anotherFunction().then(v => obj.prop2 = v)
  ])

  return obj
}

const obj = await createAsyncObject()

The 2nd one I make use of Proxy object to make it handle the Promises to set the object's properties:

function delay(ms = 0) {
  return new Promise(resolve => setTimeout(resolve, ms))
}

async function functionName() {
  await delay(1000)
  return 'value1'
}

async function anotherFunction() {
  await delay(500)
  return 'value2'
}

function asyncObjectWrapper(obj) {
  const promises = []
  const then = function then(callback) {
    return Promise.all(promises).then(() => callback())
  }

  return new Proxy(obj, {
    get: (target, property, receiver) => {
      if (property !== 'then') return

      return then
    },
    set: (target, property, value) => {
      if (!(value instanceof Promise)) return true

      promises.push(value)

      value.then(v => target[property] = v)

      return true
    }
  })
}

async function createAsyncObject() {
  const obj = {}
  const wrapper = asyncObjectWrapper(obj)

  // Set properties

  await Object.assign(wrapper, {
    prop1: functionName(),
    prop2: anotherFunction()
  })

  return obj
}

const obj = await createAsyncObject()

Upvotes: 2

Terry Lennox
Terry Lennox

Reputation: 30705

You could create a list of name, value pairs by using Promise.all() and your property retrieval functions.

Once you have this list of entries you can use Object.fromEntries() to create your new object.

In this example, the property retrieval functions are getA(), getB() and getC(), they could, of course be called anything.

function getA() {
    return new Promise(resolve => setTimeout(resolve, 500, 'value a'))
}

function getB() {
    return new Promise(resolve => setTimeout(resolve, 500, 'value b'))
}

function getC() {
    return new Promise(resolve => setTimeout(resolve, 500, 'value c'))
}

async function getProperty(name, fn) {
    let value = await fn();
    return [name, value];
}

async function getObj() {
    console.log('Getting properties...');
    const entries = await Promise.all([ getProperty('a', getA), getProperty('b', getB), getProperty('c', getC) ]);
    console.log('Properties:', entries);

    const newObj = Object.fromEntries(entries);
    console.log('New obj:', newObj)
}

getObj()
.as-console-wrapper { max-height: 100% !important; }

Upvotes: 1

Related Questions