41

I've been working on a small 2D game library for my own use, and I've run into a bit of a problem. There is a particular function in the library called loadGame that takes dependency info as input (resource files, and a list of scripts ot be executed). Here's an example.

loadGame({
    "root" : "/source/folder/for/game/",

    "resources" : {
        "soundEffect" : "audio/sound.mp3",
        "someImage" : "images/something.png",
        "someJSON" : "json/map.json"
    },

    "scripts" : [
        "js/helperScript.js",
        "js/mainScript.js"
    ]
})

Each item in resources has a key that is used by the game to access that particular resource. The loadGame function converts the resources into an object of promises.

The problem is that it tries to use Promises.all to check for when they're all ready, but Promise.all accepts only iterables as inputs - so an object like what I have is out of the question.

So I tried to convert the object into an array, this works great, except each resource is just an element in an array and doesn't have a key to identify them.

Here's the code for loadGame:

var loadGame = function (game) {
    return new Promise(function (fulfill, reject) {
        // the root folder for the game
        var root = game.root || '';

        // these are the types of files that can be loaded
        // getImage, getAudio, and getJSON are defined elsewhere in my code - they return promises
        var types = {
            jpg : getImage,
            png : getImage,
            bmp : getImage,

            mp3 : getAudio,
            ogg : getAudio,
            wav : getAudio,

            json : getJSON
        };

        // the object of promises is created using a mapObject function I made
        var resources = mapObject(game.resources, function (path) {
            // get file extension for the item
            var extension = path.match(/(?:\.([^.]+))?$/)[1];

            // find the correct 'getter' from types
            var get = types[extension];

            // get it if that particular getter exists, otherwise, fail
            return get ? get(root + path) :
                reject(Error('Unknown resource type "' + extension + '".'));
        });

        // load scripts when they're done
        // this is the problem here
        // my 'values' function converts the object into an array
        // but now they are nameless and can't be properly accessed anymore
        Promise.all(values(resources)).then(function (resources) {
            // sequentially load scripts
            // maybe someday I'll use a generator for this
            var load = function (i) {
                // load script
                getScript(root + game.scripts[i]).then(function () {
                    // load the next script if there is one
                    i++;

                    if (i < game.scripts.length) {
                        load(i);
                    } else {
                        // all done, fulfill the promise that loadGame returned
                        // this is giving an array back, but it should be returning an object full of resources
                        fulfill(resources);
                    }
                });
            };

            // load the first script
            load(0);
        });
    });
};

Ideally I'd like for some way to properly manage a list of promises for resources while still mantaining an identifier for each item. Any help would be appreciated, thanks.

Matt
  • 666
  • 1
  • 6
  • 11

18 Answers18

15

First of all: Scrap that Promise constructor, this usage is an antipattern!


Now, to your actual problem: As you have correctly identified, you are missing the key for each value. You will need to pass it inside each promise, so that you can reconstruct the object after having awaited all items:

function mapObjectToArray(obj, cb) {
    var res = [];
    for (var key in obj)
        res.push(cb(obj[key], key));
    return res;
}

return Promise.all(mapObjectToArray(input, function(arg, key) {
    return getPromiseFor(arg, key).then(function(value) {
         return {key: key, value: value};
    });
}).then(function(arr) {
    var obj = {};
    for (var i=0; i<arr.length; i++)
        obj[arr[i].key] = arr[i].value;
    return obj;
});

Mightier libraries such as Bluebird will also provide this as a helper function, like Promise.props.


Also, you shouldn't use that pseudo-recursive load function. You can simply chain promises together:

….then(function (resources) {
    return game.scripts.reduce(function(queue, script) {
        return queue.then(function() {
            return getScript(root + script);
        });
    }, Promise.resolve()).then(function() {
        return resources;
    });
});
Community
  • 1
  • 1
Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • Do you have any more resources on the anti-pattern thing? Im very new to promises and Im having trouble wrapping my head around the way it works. Thx for the script chaining code btw, that looks a lot better. – Matt Mar 27 '15 at 05:55
  • Hm, I thought the linked question and its answers are pretty elaborate. What is missing? – Bergi Mar 27 '15 at 06:01
  • ES6 promises take an iterable (at least in theory) which can be utilized - this answer is good from a teaching standpoint though :) – Benjamin Gruenbaum Mar 27 '15 at 08:08
  • @BenjaminGruenbaum: You mean like `function* mapObject(obj, cb) { for (var key in obj) yield cb(obj[key], key); }`? – Bergi Mar 27 '15 at 08:14
  • Yeah but then again you'd also use a for... of and such - it doesn't work yet though as far as I know though. – Benjamin Gruenbaum Mar 27 '15 at 08:16
  • I'm not entirely confident of the use of promises yet, and I've been struggling to grasp the "deferred antipattern". If you don't use `new Promise(..`, how can you get a promise to return in the first place? – Daniel B Mar 27 '15 at 15:07
  • @DanielB: All your low-level `getAudio`/`getImage`/`getJSON`/`getScript` functions already return promises. We just need to compose them together, all chaining (`then`) and composition (`all` etc) function return us new promises - one of which we can return in the end. No need for the `Promise` constructor in higher-level functions. See also [these rules of thumb](http://stackoverflow.com/a/25756564/1048572) :-) – Bergi Mar 27 '15 at 15:10
  • I see, but in the low level functions we should explicitly use the promise constructor `new Promise`? This might be obvious but I'm having a hard time really understanding when the named antipattern occurs. – Daniel B Mar 27 '15 at 15:16
  • 1
    @DanielB: Yes, in the low-level functions, that cannot use other promise-returning functions, you would then use `new Promise`. Though often enough you actually don't even have to, as [there are helper functions](http://stackoverflow.com/q/22519784/1048572) to avoid boilerplate code. – Bergi Mar 27 '15 at 15:25
  • Bergi, `{key: key, value:res}`, should that be `{key: key, value:value}`, and what happened to the `types` lookup? – Roamer-1888 Mar 27 '15 at 19:44
  • @Roamer-1888: Thanks, fixed. The lookup and call was concealed in `getPromiseFor(arg, key)` – Bergi Mar 28 '15 at 11:46
  • 1
    Ah ok, I guess the code for `getPromiseFor()` is pretty simple to derive from the question. – Roamer-1888 Mar 28 '15 at 14:10
  • `mapObjectToArray()` probably needs `if(obj.hasOwnProperty(key)) {...}` to reject any inherited properties. – Roamer-1888 Mar 28 '15 at 14:11
  • @Roamer-1888: I expected the `obj`s to be plain objects that inherit from `Object.prototype` only - PODs so to say. That's what does get reconstructed anyway. – Bergi Mar 28 '15 at 14:26
  • @Roamer-1888: In all honesty, I do give a * about linters. They try to give advice without understanding my code. – Bergi Mar 28 '15 at 14:49
15

If you use lodash library, you can achieve this by a one-liner function:

Promise.allValues = async (object) => {
  return _.zipObject(_.keys(object), await Promise.all(_.values(object)))
}
david.sevcik
  • 336
  • 2
  • 4
13

I actually created a library just for that and published it to github and npm:

https://github.com/marcelowa/promise-all-properties
https://www.npmjs.com/package/promise-all-properties

The only thing is that you will need to assign a property name for each promise in the object... here's an example from the README

import promiseAllProperties from 'promise-all-properties';

const promisesObject = {
  someProperty: Promise.resolve('resolve value'),
  anotherProperty: Promise.resolve('another resolved value'),
};

const promise = promiseAllProperties(promisesObject);

promise.then((resolvedObject) => {
  console.log(resolvedObject);
  // {
  //   someProperty: 'resolve value',
  //   anotherProperty: 'another resolved value'
  // }
});
Marcelo Waisman
  • 556
  • 3
  • 11
8

Here is @Matt's answer, with some types and some renames, and using ECMA-2019 Object.fromEntries.

// delayName :: (k, Promise a) -> Promise (k, a)
const delayName = ([name, promise]) => promise.then((result) => [name, result]);

export type PromiseValues<TO> = {
    [TK in keyof TO]: Promise<TO[TK]>;
};

// promiseObjectAll :: {k: Promise a} -> Promise {k: a}
export const promiseObjectAll = <T>(object: PromiseValues<T>): Promise<T> => {
    const promiseList = Object.entries(object).map(delayName);
    return Promise.all(promiseList).then(Object.fromEntries);
};

Mathieu CAROFF
  • 1,230
  • 13
  • 19
7
function resolveObject(obj) {
    return Promise.all(
      Object.entries(obj).map(async ([k, v]) => [k, await v])
    ).then(Object.fromEntries);
}

credit to Cyril Auburtin for that genius at https://esdiscuss.org/topic/modify-promise-all-to-accept-an-object-as-a-parameter

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

async function test() {
    console.time(1);
    console.log(await resolveObject({
        foo: delay(5).then(()=>1),
        bar: delay(120).then(()=>2)
    }));
    console.timeEnd(1);
}
Tom Larkworthy
  • 2,104
  • 1
  • 20
  • 29
  • 1
    If anybody's interested in getting types for the values returned by `resolveObject`, it's `function resolveObject>(obj: T): Promise<{ [K in keyof T]: Awaited }>` – bmdelacruz Dec 28 '22 at 14:15
6

Here is a simple ES2015 function that takes an object with properties that might be promises and returns a promise of that object with resolved properties.

function promisedProperties(object) {

  let promisedProperties = [];
  const objectKeys = Object.keys(object);

  objectKeys.forEach((key) => promisedProperties.push(object[key]));

  return Promise.all(promisedProperties)
    .then((resolvedValues) => {
      return resolvedValues.reduce((resolvedObject, property, index) => {
        resolvedObject[objectKeys[index]] = property;
        return resolvedObject;
      }, object);
    });

}

Usage:

promisedProperties({a:1, b:Promise.resolve(2)}).then(r => console.log(r))
//logs Object {a: 1, b: 2}

class User {
  constructor() {
    this.name = 'James Holden';
    this.ship = Promise.resolve('Rocinante');
  }
}

promisedProperties(new User).then(r => console.log(r))
//logs User {name: "James Holden", ship: "Rocinante"}

Note that @Bergi's answer will return a new object, not mutate the original object. If you do want a new object, just change the initializer value that is passed into the reduce function to {}

Zak Henry
  • 2,075
  • 2
  • 25
  • 36
4

Using async/await and lodash:

// If resources are filenames
const loadedResources = _.zipObject(_.keys(resources), await Promise.all(_.map(resources, filename => {
    return promiseFs.readFile(BASE_DIR + '/' + filename);
})))

// If resources are promises
const loadedResources = _.zipObject(_.keys(resources), await Promise.all(_.values(resources)));
Congelli501
  • 2,403
  • 2
  • 27
  • 28
  • Thank you very much @Congelli501, it just works! I have created a jsFiddle with your solution to understand it better and see it in action: https://jsfiddle.net/natterstefan/69yjkm2p/. – natterstefan Jun 11 '18 at 09:19
2

Edit: This question seems to be gaining a little traction lately, so I thought I'd add my current solution to this problem which I'm using in a couple projects now. It's a lot better than the code at the bottom of this answer which I wrote two years ago.

The new loadAll function assume its input is an object mapping asset names to promises, and it also makes use of the experimental function Object.entries, which may not be available in all environments.

// fromEntries :: [[a, b]] -> {a: b}
// Does the reverse of Object.entries.
const fromEntries = list => {
    const result = {};

    for (let [key, value] of list) {
        result[key] = value;
    }

    return result;
};

// addAsset :: (k, Promise a) -> Promise (k, a)
const addAsset = ([name, assetPromise]) =>
    assetPromise.then(asset => [name, asset]);

// loadAll :: {k: Promise a} -> Promise {k: a}
const loadAll = assets =>
    Promise.all(Object.entries(assets).map(addAsset)).then(fromEntries);

So I've come up with the proper code based on Bergi's answer. Here it is if anyone else is having the same problem.

// maps an object and returns an array
var mapObjectToArray = function (obj, action) {
    var res = [];

    for (var key in obj) res.push(action(obj[key], key));

    return res;
};

// converts arrays back to objects
var backToObject = function (array) {
    var object = {};

    for (var i = 0; i < array.length; i ++) {
        object[array[i].name] = array[i].val;
    }

    return object;
};

// the actual load function
var load = function (game) {
    return new Promise(function (fulfill, reject) {
        var root = game.root || '';

        // get resources
        var types = {
            jpg : getImage,
            png : getImage,
            bmp : getImage,

            mp3 : getAudio,
            ogg : getAudio,
            wav : getAudio,

            json : getJSON
        };

        // wait for all resources to load
        Promise.all(mapObjectToArray(game.resources, function (path, name) {
            // get file extension
            var extension = path.match(/(?:\.([^.]+))?$/)[1];

            // find the getter
            var get = types[extension];

            // reject if there wasn't one
            if (!get) return reject(Error('Unknown resource type "' + extension + '".'));

            // get it and convert to 'object-able'
            return get(root + path, name).then(function (resource) {
                return {val : resource, name : name};
            });

            // someday I'll be able to do this
            // return get(root + path, name).then(resource => ({val : resource, name : name}));
        })).then(function (resources) {
            // convert resources to object
            resources = backToObject(resources);

            // attach resources to window
            window.resources = resources;

            // sequentially load scripts
            return game.scripts.reduce(function (queue, path) {
                return queue.then(function () {
                    return getScript(root + path);
                });
            }, Promise.resolve()).then(function () {
                // resources is final value of the whole promise
                fulfill(resources);
            });
        });
    });
};
Simon B.
  • 2,530
  • 24
  • 30
Matt
  • 666
  • 1
  • 6
  • 11
2

Based off the accepted answer here, I thought I'd offer a slightly different approach that seems easier to follow:

// Promise.all() for objects
Object.defineProperty(Promise, 'allKeys', {
  configurable: true,
  writable: true,
  value: async function allKeys(object) {
    const resolved = {}
    const promises = Object
      .entries(object)
      .map(async ([key, promise]) =>
        resolved[key] = await promise
      )

    await Promise.all(promises)

    return resolved
  }
})

// usage
Promise.allKeys({
  a: Promise.resolve(1),
  b: 2,
  c: Promise.resolve({})
}).then(results => {
  console.log(results)
})

Promise.allKeys({
  bad: Promise.reject('bad error'),
  good: 'good result'
}).then(results => {
  console.log('never invoked')
}).catch(error => {
  console.log(error)
})

Usage:

try {
  const obj = await Promise.allKeys({
    users: models.User.find({ rep: { $gt: 100 } }).limit(100).exec(),
    restrictions: models.Rule.find({ passingRep: true }).exec()
  })

  console.log(obj.restrictions.length)
} catch (error) {
  console.log(error)
}

I looked up Promise.allKeys() to see if someone had already implemented this after writing this answer, and apparently this npm package does have an implementation for it, so use that if you like this little extension.

Patrick Roberts
  • 49,224
  • 10
  • 102
  • 153
  • Shouldn't this be called `allValues` instead, or even better, make `Promise.all` accept object parameters and behave this way? – Augustin Riedinger Dec 07 '17 at 15:21
  • @AugustinRiedinger sure, naming convention can be whatever you want it to, since this is non-standard anyway. As for your other suggestion, I could have done that, but wanted to avoid it because people might mistakenly think that's the standard implementation of `Promise.all()`. As long as you understand that, you can include this functionality in your code however you want. – Patrick Roberts Dec 07 '17 at 19:11
  • Sure. Interesting that we have a different approach. When I use `defineProperty` or `prototype`, it means I see the code as a polyfill for some feature I'd like to have instead of some custom personal code. – Augustin Riedinger Dec 08 '17 at 07:52
  • @AugustinRiedinger personally, when asked for a solution to problem that appears to be widely reusable, I'm not adverse to modifying the `prototype` if it's understood that the code is not part of some published or redistributed project that other people will be using. – Patrick Roberts Dec 08 '17 at 18:18
2

Missing Promise.obj() method

A shorter solution with vanilla JavaScript, no libraries, no loops, no mutation

Here is a shorter solution than other answers, using modern JavaScript syntax.

The middle line process = ... is recursive and handles deep objects.

This creates a missing Promise.obj() method that works like Promise.all() but for objects:

const asArray = obj => [].concat(...Object.entries(obj));
const process = ([key, val, ...rest], aggregated = {}) =>
  rest.length ?
    process(rest, {...aggregated, [key]: val}) :
    {...aggregated, [key]: val};
const promisedAttributes = obj => Promise.all(asArray(obj)).then(process);
// Promise.obj = promisedAttributes;

Better not use the last line! A much better idea is that you export this promisedAttributes as a utility function that you reuse.

Simon B.
  • 2,530
  • 24
  • 30
rsp
  • 107,747
  • 29
  • 201
  • 177
2

I've written function that recursively awaits promises within an object and returns the constructed object back to you.

/**
* function for mimicking async action
*/
function load(value) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(value);
    }, Math.random() * 1000);
  });
}

/**
* Recursively iterates over object properties and awaits all promises. 
*/
async function fetch(obj) {
  if (obj instanceof Promise) {
    obj = await obj;
    return fetch(obj);
  } else if (Array.isArray(obj)) {
    return await Promise.all(obj.map((item) => fetch(item)));
  } else if (obj.constructor === Object) {
    const keys = Object.keys(obj);
    for (let i = 0; i < keys.length; i++) {
      const key = keys[i];
      obj[key] = await fetch(obj[key]);
    }
    return obj;
  } else {
    return obj;
  }
}


// now lets load a world object which consists of a bunch of promises nested in each other

let worldPromise = {
    level: load('world-01'),
    startingPoint: {
      x: load('0'),
      y: load('0'),
    },
    checkpoints: [
      {
        x: load('10'),
        y: load('20'),
      }
    ],
    achievments: load([
      load('achievement 1'),
      load('achievement 2'),
      load('achievement 3'),
    ]),
    mainCharacter: {
    name: "Artas",
    gear: {
      helmet: load({
        material: load('steel'),
        level: load(10),
      }),
      chestplate: load({
        material: load('steel'),
        level: load(20),
      }),
      boots: load({
        material: load('steel'),
        level: load(20),
        buff: load('speed'),
      }),
    }
  }
};

//this will result an object like this
/*
{
  level: Promise { <pending> },
  startingPoint: { 
    x: Promise { <pending> },
    y: Promise { <pending> } 
  },
  checkpoints: [ { x: [Promise], y: [Promise] } ],
  achievments: Promise { <pending> },
  mainCharacter: {
    name: 'Artas',
    gear: { 
    helmet: [Promise],
    chestplate: [Promise],
    boots: [Promise] 
    }
  }
}
*/


//Now by calling fetch function, all promise values will be populated 
//And you can see that computation time is ~1000ms which means that all processes are being computed in parallel.
(async () => {
  console.time('start');
  console.log(worldPromise);
  let world = await fetch(worldPromise);
  console.log(world);
  console.timeEnd('start');
})();
    
  • This was the only solution that worked recursively (nested objects). Thanks. I had to make one change, since I was working with JS classes and not direct objects - instead of obj.constructor === Object I used (obj instanceof Object). – seeARMS Aug 29 '20 at 23:29
2

So for these promises

const obj = { foo: promise1, bar: promise2 }

if you want object of values then await the promises

const newObj = {}
for (let key in obj)
  newObj[key] = await obj[key]

but if you want an object of settled promises await them all without reassigning.

await Promise.all(Object.values(obj))
Caveman
  • 2,527
  • 1
  • 17
  • 18
1

I recommend Sindre Sorhus' p-props. His stuff is always excellent.

Nick Heiner
  • 119,074
  • 188
  • 476
  • 699
1

Need this, including good TypeScript support?

combine-promises can mix object values of different types and infer a good return type.

https://github.com/slorber/combine-promises

const result: { user: User; company: Company } = await combinePromises({
  user: fetchUser(),
  company: fetchCompany(),
});
Sebastien Lorber
  • 89,644
  • 67
  • 288
  • 419
1

Here's a simple (but effective) TypeScript solution:

export async function promiseAllMap<T, M extends Record<string, T | PromiseLike<T>>>(map: M):
  Promise<{[P in keyof M]: Awaited<M[P]>}>
{
  const resolvedArray = await Promise.all(Object.values(map));
  const resolvedMap: any = {};

  Object.keys(map).forEach((key, index) => {
    resolvedMap[key] = resolvedArray[index];
  });

  return resolvedMap;
}

(I've borrowed some of this from Sebastien's solution above, but adapted to use the Awaited utility type from TypeScript 4.5)

It returns a fully typed map of the retrieved results, correctly handles chained promise types and, like the native Promise.all(), it also allows non-promise values to be supplied in the input map, and returned unchanged in the results.

Dan King
  • 3,412
  • 5
  • 24
  • 23
0

One simple and easiest way to do it is

Promise.all([yourObject]).then((result)=>{
  yourObject={...result}
}).catch((error)=>{console.log(error)})
0

Create function:

const promiseAllFromObject = async promisesObject => (
    Object.keys(promisesObject).reduce(async (acc, key) => {
        const lastResult = await acc;
        return Object.assign(lastResult, { [key]: await promisesObject[key] });
    }, Promise.resolve({}))
);

Usage:

promiseAllFromObject({
    abc: somePromise,
    xyz: someOtherPromise,
});

Result:

{
    abc: theResult,
    xyz: theOtherResult,
}
0

A simple and fast ES6 await approach:

let promObj = getAnObjectFullOfPromises();
let dataObj = {};

// Let all the promises wrap up asynchronously
await Promise.all(Object.values(promObj)); 

// Grab the results from the promises retaining the key-value mapping
await Object.keys(promObj).map(async (key) => dataObj[key] = await promObj[key]);
tiz.io
  • 12,510
  • 1
  • 18
  • 9