How to resolve all async deferred nested values of a lazy initializable data structure?

108 Views Asked by At

I'm looking for vocabulary or for a library that supports the following behaviour:

Imagine a Javascript object like the following one:

const foo = {
  id: 1,
  name: 'Some String value',
  supplier: async () => {
    return 'Some supplier name'
  },
  nested: async () => {
    return [
      {
        id: 2,
        name: async () => {
          return 'this is a name'
        }
      }
    ]
  }
}

It is composed by native types (numbers, strings...) and by functions.

I'd like this object being transformed to the following one:

const resolved = {
  id: 1,
  name: 'Some string value',
  supplier: 'Some supplier name',
  nested: [
    {
      id: 2,
      name: 'this is a name'
    }
  ]
}

As you see the transformed object does not have functions anymore but only native values.

If you are familiar with GraphQL resolvers, it might ring a bell to you.

I know I can write my own implementation of the behaviour but I'm sure this is something that already exists somewhere.

Do you have some keywords to share?

2

There are 2 best solutions below

0
Bergi On BEST ANSWER

I doubt there's a library that does exactly this, unless you're actually thinking of the graphql-js execute method.

But it's easy enough to implement yourself:

async function initialised(value) {
  if (typeof value == 'function') return initialised(await value());
  if (typeof value != 'object' || !value) return value;
  if (Array.isArray(value)) return Promise.all(value.map(initialised));
  return Object.fromEntries(await Promise.all(Object.entries(value).map(([k, v]) =>
    initialised(v).then(r => [k, r])
  )));
}

async function initialised(value) {
  if (typeof value == 'function') return initialised(await value());
  if (typeof value != 'object' || !value) return value;
  if (Array.isArray(value)) return Promise.all(value.map(initialised));
  return Object.fromEntries(await Promise.all(Object.entries(value).map(([k, v]) =>
    initialised(v).then(r => [k, r])
  )));
}

const foo = {
  id: 1,
  name: 'Some String value',
  supplier: async () => {
    return 'Some supplier name'
  },
  nested: async () => {
    return [
      {
        id: 2,
        name: async () => {
          return 'this is a name'
        }
      }
    ]
  }
};

initialised(foo).then(resolved => {
  console.log(resolved);
})

0
Peter Seliger On

Lazy initializable data-structures with deferred / asynchronous resolvable values like the one presented by the OP could be achieved by a two folded recursive approach where

  • one recursion is responsible for collecting all of an object's async entries/values

  • and the main recursion is responsible for creating and aggregating the resolved data-structure by awaiting all async values of the former collection and by calling itself again on each of the awaited results.

An implementation either could mutate the passed lazy initializable data-structure by resolving its deferred values, or it could create a deep but resolved copy of the former. The beneath provided code supports the resolved structured clone of the originally passed data.

function isAsynFunction(value) {
  return (/^\[object\s+AsyncFunction\]$/)
    .test(Object.prototype.toString.call(value));
}
function isObject(value) {
  return (value && ('object' === typeof value));
}

function collectAsyncEntriesRecursively(obj, resolved = {}) {
  return Object
    .entries(obj)
    .reduce((result, [key, value]) => {

      if (isAsynFunction(value)) {

        result.push({ type: obj, key });

      } else if (isObject(value)) {
        result
          .push(
            // recursion.
            ...collectAsyncEntriesRecursively(value)
          );
      }
      return result;

    }, []);
}

// - recursively aggregates (one deferred data layer
//   after the other) a real but deferred resolved
//   copy of the initially provided data-structure.
async function resolveLazyInitializableObject(obj) {
  const deferred = Object.assign({}, obj);

  // - in order to entirely mutate the initially provided
  //   data-structure delete the above assignement and
  //   replace every occurrence of `deferred` with `obj`.

  const deferredEntries = collectAsyncEntriesRecursively(deferred);
  if (deferredEntries.length >= 1) {

    const results = await Promise
      .all(
        deferredEntries
          .map(({ type, key }) => type[key]())
      );
    deferredEntries
      .forEach(({ type, key }, idx) => type[key] = results[idx]);

    // recursion.
    await resolveLazyInitializableObject(results);
  }
  return deferred;
}


const foo = {
  id: 1,
  name: 'Some String value',
  supplier: async () => {
    return 'Some supplier name'
  },
  nested: async () => {
    return [
      {
        id: 2,
        name: async () => {
          return 'this is a name'
        }
      }
    ]
  }
};
const complexDeferred = {
  id: 1,
  name: 'Some String value',
  supplier: async () => {
    return 'Some supplier name'
  },
  nested: async () => {
    return [{
      id: 2,
      name: async () => {
        return 'this is a name'
      }
    }, {
      id: 3,
      name: async () => {
        return 'this is another name'
      }
    }, {
      id: 4,
      nested: async () => {
        return [{
          id: 5,
          name: async () => {
            return 'this is yet another name'
          }
        }];
      }
    }];
  }
};

(async () => {
  const resolved = await resolveLazyInitializableObject(foo);
  console.log({ foo, resolved })
})();

resolveLazyInitializableObject(complexDeferred)
  .then(complexResolved =>
    console.log({ complexDeferred, complexResolved })
  );
.as-console-wrapper { min-height: 100%!important; top: 0; }