@qooxdoo/framework
Version:
The JS Framework for Coders
739 lines (681 loc) • 29.3 kB
JavaScript
/* ************************************************************************
qooxdoo - the new era of web development
http://qooxdoo.org
Copyright:
2016 Zenesis Limited, http://www.zenesis.com
License:
MIT: https://opensource.org/licenses/MIT
See the LICENSE file in the project's top-level directory for details.
Authors:
* John Spackman (john.spackman@zenesis.com)
* Patryk Malinowski (pmalinowski@vmn.digital)
************************************************************************ */
/**
* Wrapper around a native promise, adding some extra helpful methods which are found in Bluebird.js,
* such as .map, .reduce, .filter, and many more.
*
* @ignore(AggregateError)
*/
qx.Class.define("qx.promise.NativeWrapper", {
extend: qx.core.Object,
/**
* @overload
* @param {(resolve: Function, reject: Function) => void} arg0 The executor for the promise
*
* @overload
* Wraps a native promise in the wrapper class
* @param {Promise} arg0 A native Promise
*/
construct(arg0) {
super();
if (typeof arg0 === "function") {
this.__promise = new Promise(arg0);
} else if (typeof arg0 === "object" && arg0.constructor === Promise) {
this.__promise = arg0;
}
},
members: {
/**
* @type {Object} The context that this promise is bound to
*/
__context: null,
/**
* Creates a new promise just like this one, but with a context set
* @see
* @param {Object} context
* @returns
*/
bind(context) {
let promise = new qx.promise.NativeWrapper(this.__promise);
return promise.__setContext(context);
},
/**
* Same as for Native Promise
* @returns {qx.promise.NativeWrapper}
*/
then(onResolved, onRejected) {
onResolved = onResolved.bind(this.__context);
if (onRejected) {
onRejected = onRejected.bind(this.__context);
}
return qx.promise.NativeWrapper.__wrap(
this.__promise.then(onResolved, onRejected)
).__setContext(this.__context);
},
/**
* Same as for Native Promise
* @returns {qx.promise.NativeWrapper}
*/
catch(handler) {
handler = handler.bind(this.__context);
return qx.promise.NativeWrapper.__wrap(
this.__promise.catch(handler)
).__setContext(this.__context);
},
/**
* Same as for Native Promise
* @returns {qx.promise.NativeWrapper}
*/
spread(fulfilledHandler) {
return this.then(values => fulfilledHandler(...values));
},
/**
* Same as for Native Promise
* @returns {qx.promise.NativeWrapper}
*/
finally(handler) {
handler = handler.bind(this.__context);
return qx.promise.NativeWrapper.__wrap(
this.__promise.finally(handler)
).__setContext(this.__context);
},
/**
* Due to the high complexity of implementing this feature, it is not supported in qx.promise.NativeWrapper
*/
cancel() {
throw new Error(
"qx.promise.NativeWrapper does not support canceling promises"
);
},
/**
* Note: Only call when this promise will resolve to an array
* Same as Promise.all, but passed with the array that this promise resolves to
* @returns {qx.promise.NativeWrapper}
*/
all(...args) {
return qx.promise.NativeWrapper.all(this, ...args);
},
/**
* Note: Only call when this promise will resolve to an array
* Same as Promise.race, but passed with the array that this promise resolves to
* @returns {qx.promise.NativeWrapper}
*/
race() {
return qx.promise.NativeWrapper.race(this);
},
/**
* Note: Only call when this promise will resolve to an array
* Same as Promise.any, but passed with the array that this promise resolves to
* @returns {qx.promise.NativeWrapper}
*/
any() {
return qx.promise.NativeWrapper.any(this);
},
/**
* Same as {@link qx.promise.NativeWrapper.some} except that it iterates over the value of this promise, when
* it is fulfilled; return a promise that is fulfilled as soon as count promises are fulfilled
* in the array. The fulfillment value is an array with count values in the order they were fulfilled.
*
* @param count {Integer}
* @return {qx.promise.NativeWrapper}
*/
some(count) {
return qx.promise.NativeWrapper.some(this, count);
},
/**
* Same as {@link qx.promise.NativeWrapper.each} except that it iterates over the value of this promise, when
* it is fulfilled; iterates over the values with the given <code>iterator</code> function with the signature
* <code>(value, index, length)</code> where <code>value</code> is the resolved value. Iteration happens
* serially. If any promise is rejected the returned promise is rejected as well.
*
* Resolves to the original array unmodified, this method is meant to be used for side effects. If the iterator
* function returns a promise or a thenable, then the result of the promise is awaited, before continuing with
* next iteration.
*
* @param iterator {Function} the callback, with <code>(value, index, length)</code>
* @return {qx.promise.NativeWrapper}
*/
each(iterator) {
return qx.promise.NativeWrapper.each(this, iterator);
},
/**
* Same as {@link qx.promise.NativeWrapper.filter} except that it iterates over the value of this promise, when it is fulfilled;
* iterates over all the values into an array and filter the array to another using the given filterer function.
*
* @param iterable {Iterable} An iterable object, such as an Array
* @param iterator {Function} the callback, with <code>(value, index, length)</code>
* @param options {Object?} options; can be:
* <code>concurrency</code> max nuber of simultaneous filters, default is <code>Infinity</code>
* @return {qx.promise.NativeWrapper}
*/
filter(iterator, options) {
return qx.promise.NativeWrapper.filter(this, iterator, options);
},
/**
* Same as {@link qx.promise.NativeWrapper.map} except that it iterates over the value of this promise, when it is fulfilled;
* iterates over all the values into an array and map the array to another using the given mapper function.
*
* Promises returned by the mapper function are awaited for and the returned promise doesn't fulfill
* until all mapped promises have fulfilled as well. If any promise in the array is rejected, or
* any promise returned by the mapper function is rejected, the returned promise is rejected as well.
*
* The mapper function for a given item is called as soon as possible, that is, when the promise
* for that item's index in the input array is fulfilled. This doesn't mean that the result array
* has items in random order, it means that .map can be used for concurrency coordination unlike
* .all.
*
* @param iterator {Function} the callback, with <code>(value, index, length)</code>
* @param options {Object?} * A native object with one key: <code>concurrency</code>: max number of simultaneous maps, default is <code>Infinity</code>
* @return {qx.promise.NativeWrapper}
*/
map(iterator, options) {
return qx.promise.NativeWrapper.map(this, iterator, options);
},
/**
* Same as {@link qx.promise.NativeWrapper.mapSeries} except that it iterates over the value of this promise, when
* it is fulfilled; iterates over all the values into an array and iterate over the array serially,
* in-order.
*
* Returns a promise for an array that contains the values returned by the iterator function in their
* respective positions. The iterator won't be called for an item until its previous item, and the
* promise returned by the iterator for that item are fulfilled. This results in a mapSeries kind of
* utility but it can also be used simply as a side effect iterator similar to Array#forEach.
*
* If any promise in the input array is rejected or any promise returned by the iterator function is
* rejected, the result will be rejected as well.
*
* @param iterator {Function} the callback, with <code>(value, index, length)</code>
* @return {qx.promise.NativeWrapper}
*/
mapSeries(iterator, options) {
return qx.promise.NativeWrapper.mapSeries(this, iterator, options);
},
/**
* Same as {@link qx.promise.NativeWrapper.reduce} except that it iterates over the value of this promise, when
* it is fulfilled; iterates over all the values in the <code>Iterable</code> into an array and
* reduce the array to a value using the given reducer function.
*
* If the reducer function returns a promise, then the result of the promise is awaited, before
* continuing with next iteration. If any promise in the array is rejected or a promise returned
* by the reducer function is rejected, the result is rejected as well.
*
* If initialValue is undefined (or a promise that resolves to undefined) and the iterable contains
* only 1 item, the callback will not be called and the iterable's single item is returned. If the
* iterable is empty, the callback will not be called and initialValue is returned (which may be
* undefined).
*
* qx.promise.NativeWrapper.reduce will start calling the reducer as soon as possible, this is why you might want to
* use it over qx.promise.NativeWrapper.all (which awaits for the entire array before you can call Array#reduce on it).
*
* @param reducer {Function} the callback, with <code>(value, index, length)</code>
* @param initialValue {Object?} optional initial value
* @return {qx.promise.NativeWrapper}
*/
reduce(reducer, initialValue) {
return qx.promise.NativeWrapper.reduce(this, reducer, initialValue);
},
/**
*
* @param {Object} context
* @returns {qx.promise.NativeWrapper} this object to support chaining
*/
__setContext(context) {
this.__context = context;
return this;
}
},
statics: {
/**
* Wraps a promise in a qx.promise.NativeWrapper
* @param {Promise} promise
* @returns
*/
__wrap(promise) {
if (qx.core.Environment.get("qx.debug")) {
if (promise.constructor !== Promise) {
throw new Error("Only native promises can be wrapped!");
}
}
return new qx.promise.NativeWrapper(promise);
},
/**
* Returns a Promise object that is resolved with the given value. If the value is a thenable (i.e.
* has a then method), the returned promise will "follow" that thenable, adopting its eventual
* state; otherwise the returned promise will be fulfilled with the value. Generally, if you
* don't know if a value is a promise or not, Promise.resolve(value) it instead and work with
* the return value as a promise.
*
* @param value {Object}
* @return {qx.promise.NativeWrapper}
*/
resolve(value) {
return qx.promise.NativeWrapper.__wrap(Promise.resolve(value));
},
/**
* Returns a Promise object that is rejected with the given reason.
* @param reason {Object?} Reason why this Promise rejected. A warning is generated if not instanceof Error. If undefined, a default Error is used.
* @return {qx.promise.NativeWrapper}
*/
reject(reason) {
return qx.promise.NativeWrapper.__wrap(Promise.reject(reason));
},
/**
* Returns a promise that resolves when all of the promises in the object properties have resolved,
* or rejects with the reason of the first passed promise that rejects. The result of each property
* is placed back in the object, replacing the promise. Note that non-promise values are untouched.
*
* @param value {var} An object
* @return {qx.promise.NativeWrapper}
*/
allOf(value) {
function action(value) {
var arr = [];
var names = [];
for (var name in value) {
if (value.hasOwnProperty(name) && qx.Promise.isPromise(value[name])) {
arr.push(value[name]);
names.push(name);
}
}
return qx.promise.NativeWrapper.all(arr).then(function (arr) {
arr.forEach(function (item, index) {
value[names[index]] = item;
});
return value;
});
}
return qx.Promise.isPromise(value) ? value.then(action) : action(value);
},
/**
* Returns a promise that resolves when all of the promises in the iterable argument have resolved,
* or rejects with the reason of the first passed promise that rejects. Note that non-promise values
* are untouched.
*
* @param iterable {Iterable} An iterable object, such as an Array
* @return {qx.promise.NativeWrapper}
*/
all(iterable) {
return qx.promise.NativeWrapper.resolve(iterable).then(iterable =>
qx.promise.NativeWrapper.__wrap(Promise.all(iterable))
);
},
/**
* Returns a promise that resolves or rejects as soon as one of the promises in the iterable resolves
* or rejects, with the value or reason from that promise.
* @param iterable {Iterable} An iterable object, such as an Array
* @return {qx.promise.NativeWrapper}
*/
race(iterable) {
return qx.promise.NativeWrapper.resolve(iterable).then(
iterableResolved =>
new qx.promise.NativeWrapper(Promise.race(iterableResolved))
);
},
/* *********************************************************************************
*
* Extension API methods
*
*/
/**
* Like Promise.some, with 1 as count. However, if the promise fulfills, the fulfillment value is not an
* array of 1 but the value directly.
*
* @param iterable {Iterable} An iterable object, such as an Array
* @return {qx.promise.NativeWrapper}
*/
any(iterable) {
return qx.promise.NativeWrapper.resolve(iterable).then(
iterableResolved =>
new qx.promise.NativeWrapper(Promise.any(iterableResolved))
);
},
/**
* Given an Iterable (arrays are Iterable), or a promise of an Iterable, which produces promises (or a mix
* of promises and values), iterate over all the values in the Iterable into an array and return a promise
* that is fulfilled as soon as count promises are fulfilled in the array. The fulfillment value is an
* array with count values in the order they were fulfilled.
*
* @param iterable {Iterable} An iterable object, such as an Array
* @param count {Integer}
* @return {qx.promise.NativeWrapper}
*/
some(iterable, count) {
return new qx.promise.NativeWrapper((resolve, reject) => {
qx.promise.NativeWrapper.resolve(iterable).then(iterable => {
let resolved = [];
let rejected = [];
let minToReject = iterable.length - count + 1;
const onResolved = value => {
if (resolved.length >= count) {
return;
}
resolved.push(value);
if (resolved.length == count) {
resolve(resolved);
}
};
const onRejected = reason => {
rejected.push(reason);
if (--minToReject == 0) {
reject(new AggregateError(rejected));
}
};
iterable.forEach((elem, index) => {
if (qx.Promise.isPromise(elem)) {
elem.then(onResolved, onRejected);
} else {
onResolved(elem);
}
});
});
});
},
/**
* Iterate over an array, or a promise of an array, which contains promises (or a mix of promises and values)
* with the given <code>iterator</code> function with the signature <code>(value, index, length)</code> where
* <code>value</code> is the resolved value of a respective promise in the input array. Iteration happens
* serially. If any promise in the input array is rejected the returned promise is rejected as well.
*
* Resolves to the original array unmodified, this method is meant to be used for side effects. If the iterator
* function returns a promise or a thenable, then the result of the promise is awaited, before continuing with
* next iteration.
*
* @param iterable {Iterable} An iterable object, such as an Array
* @param iterator {Function} the callback, with <code>(value, index, length)</code>
* @return {qx.promise.NativeWrapper}
*/
each(iterable, iterator) {
let f = async () => {
let iterableValue = await iterable;
let index = 0;
for (let item of iterableValue) {
let itemResolved = await item;
await iterator(itemResolved, index++, iterable.length);
}
};
return new qx.promise.NativeWrapper(f());
},
/**
* Given an Iterable(arrays are Iterable), or a promise of an Iterable, which produces promises (or a mix of
* promises and values), iterate over all the values in the Iterable into an array and filter the array to
* another using the given filterer function.
*
* It is essentially an efficient shortcut for doing a .map and then Array#filter:
* <pre>
* qx.promise.NativeWrapper.map(valuesToBeFiltered, function(value, index, length) {
* return Promise.all([filterer(value, index, length), value]);
* }).then(function(values) {
* return values.filter(function(stuff) {
* return stuff[0] == true
* }).map(function(stuff) {
* return stuff[1];
* });
* });
* </pre>
*
* @param iterable {Iterable} An iterable object, such as an Array
* @param iterator {Function} the callback, with <code>(value, index, length)</code>
* @param options {Object?} Either:
* A native object with one key: <code>concurrency</code>: max number of simultaneous filters, default is <code>Infinity</code>
* Or: any other object, in which case this will be the context for the iterator
* @return {qx.promise.NativeWrapper}
*/
filter(iterable, iterator, options) {
let limiter = new qx.util.ConcurrencyLimiter(options?.concurrency);
const doit = async () => {
let iterableResolved = await iterable;
let resultsPromises = iterableResolved.map((item, index) =>
limiter.add(async () => {
let itemResolved = await item;
let keep = await iterator(
itemResolved,
index,
iterableResolved.length
);
return { keep, val: itemResolved };
})
);
let values = await qx.promise.NativeWrapper.all(resultsPromises);
return values.filter(({ keep }) => keep).map(({ val }) => val);
};
return new qx.promise.NativeWrapper(doit());
},
/**
* Given an <code>Iterable</code> (arrays are <code>Iterable</code>), or a promise of an
* <code>Iterable</code>, which produces promises (or a mix of promises and values), iterate over
* all the values in the <code>Iterable</code> into an array and map the array to another using
* the given mapper function.
*
* Promises returned by the mapper function are awaited for and the returned promise doesn't fulfill
* until all mapped promises have fulfilled as well. If any promise in the array is rejected, or
* any promise returned by the mapper function is rejected, the returned promise is rejected as well.
*
* The mapper function for a given item is called as soon as possible, that is, when the promise
* for that item's index in the input array is fulfilled. This doesn't mean that the result array
* has items in random order, it means that .map can be used for concurrency coordination unlike
* .all.
*
* A common use of Promise.map is to replace the .push+Promise.all boilerplate:
*
* <pre>
* var promises = [];
* for (var i = 0; i < fileNames.length; ++i) {
* promises.push(fs.readFileAsync(fileNames[i]));
* }
* qx.promise.NativeWrapper.all(promises).then(function() {
* console.log("done");
* });
*
* // Using Promise.map:
* qx.promise.NativeWrapper.map(fileNames, function(fileName) {
* // Promise.map awaits for returned promises as well.
* return fs.readFileAsync(fileName);
* }).then(function() {
* console.log("done");
* });
* </pre>
*
* @param iterable {Iterable} An iterable object, such as an Array
* @param iterator {Function} the callback, with <code>(value, index, length)</code>
* @param options {Object?} * A native object with one key: <code>concurrency</code>: max number of simultaneous maps, default is <code>Infinity</code>
* @return {qx.promise.NativeWrapper}
*/
map(iterable, iterator, options) {
return qx.promise.NativeWrapper.resolve(iterable).then(iterable => {
let limiter = new qx.util.ConcurrencyLimiter(options?.concurrency);
let resultsPromises = iterable.map((item, index) =>
limiter.add(async () => {
let itemResolved = await item;
let result = await iterator(itemResolved, index, iterable.length);
return result;
})
);
return qx.promise.NativeWrapper.all(resultsPromises);
});
},
/**
* Given an <code>Iterable</code>(arrays are <code>Iterable</code>), or a promise of an
* <code>Iterable</code>, which produces promises (or a mix of promises and values), iterate over
* all the values in the <code>Iterable</code> into an array and iterate over the array serially,
* in-order.
*
* Returns a promise for an array that contains the values returned by the iterator function in their
* respective positions. The iterator won't be called for an item until its previous item, and the
* promise returned by the iterator for that item are fulfilled. This results in a mapSeries kind of
* utility but it can also be used simply as a side effect iterator similar to Array#forEach.
*
* If any promise in the input array is rejected or any promise returned by the iterator function is
* rejected, the result will be rejected as well.
*
* Example where .mapSeries(the instance method) is used for iterating with side effects:
*
* <pre>
* // Source: http://jakearchibald.com/2014/es7-async-functions/
* function loadStory() {
* return getJSON('story.json')
* .then(function(story) {
* addHtmlToPage(story.heading);
* return story.chapterURLs.map(getJSON);
* })
* .mapSeries(function(chapter) { addHtmlToPage(chapter.html); })
* .then(function() { addTextToPage("All done"); })
* .catch(function(err) { addTextToPage("Argh, broken: " + err.message); })
* .then(function() { document.querySelector('.spinner').style.display = 'none'; });
* }
* </pre>
*
* @param iterable {Iterable} An iterable object, such as an Array
* @param iterator {Function} the callback, with <code>(value, index, length)</code>
* @return {qx.promise.NativeWrapper}
*/
mapSeries(iterable, iterator) {
return new qx.promise.NativeWrapper(async (resolve, reject) => {
let failed = false;
const fail = reason => {
if (!failed) {
failed = true;
reject(reason);
}
};
//We must handle the rejections of promises ASAP
//to prevent unhandled promise rejections
qx.promise.NativeWrapper.all(iterable).catch(fail);
let result = [];
iterable = await iterable;
try {
let index = 0;
for (let promise of iterable) {
let value = await promise;
let mapped = await iterator(value, index++, iterable.length);
result.push(mapped);
}
} catch (ex) {
fail(ex);
}
resolve(result);
});
},
/**
* Given an <code>Iterable</code> (arrays are <code>Iterable</code>), or a promise of an
* <code>Iterable</code>, which produces promises (or a mix of promises and values), iterate
* over all the values in the <code>Iterable</code> into an array and reduce the array to a
* value using the given reducer function.
*
* If the reducer function returns a promise, then the result of the promise is awaited, before
* continuing with next iteration. If any promise in the array is rejected or a promise returned
* by the reducer function is rejected, the result is rejected as well.
*
* Read given files sequentially while summing their contents as an integer. Each file contains
* just the text 10.
*
* <pre>
* qx.promise.NativeWrapper.reduce(["file1.txt", "file2.txt", "file3.txt"], function(total, fileName) {
* return fs.readFileAsync(fileName, "utf8").then(function(contents) {
* return total + parseInt(contents, 10);
* });
* }, 0).then(function(total) {
* //Total is 30
* });
* </pre>
*
* If initialValue is undefined (or a promise that resolves to undefined) and the iterable contains
* only 1 item, the callback will not be called and the iterable's single item is returned. If the
* iterable is empty, the callback will not be called and initialValue is returned (which may be
* undefined).
*
* Promise.reduce will start calling the reducer as soon as possible, this is why you might want to
* use it over Promise.all (which awaits for the entire array before you can call Array#reduce on it).
*
* @param iterable {Iterable} An iterable object, such as an Array
* @param reducer {Function} the callback, with <code>(value, index, length)</code>
* @param initialValue {Object?} optional initial value
* @return {qx.promise.NativeWrapper}
*/
reduce(iterable, reducer, initialValue) {
return new qx.promise.NativeWrapper(async (resolve, reject) => {
let failed = false;
function fail(reason) {
if (!failed) {
failed = true;
reject(reason);
}
}
try {
let iterableResolved = await iterable;
//We must handle the rejections of promises ASAP
//to prevent unhandled promise rejections
iterableResolved.forEach((item, index) => {
if (qx.Promise.isPromise(item)) {
item.catch(fail);
}
});
let accum = initialValue;
let index = 0;
for (let promise of iterableResolved) {
let data = await promise;
accum = await reducer(accum, data, index, iterableResolved.length);
index++;
}
resolve(accum);
} catch (ex) {
fail(ex);
}
});
},
/**
* Returns a new function that wraps the given function fn. The new function will always return a promise that is
* fulfilled with the original functions return values or rejected with thrown exceptions from the original function.
* @param cb {Function}
* @return {Function}
*/
method(cb) {
return (...args) =>
new qx.promise.NativeWrapper(resolve =>
resolve(cb.call(this.__context, ...args))
);
},
/**
* Like .all but for object properties or Maps* entries instead of iterated values. Returns a promise that
* is fulfilled when all the properties of the object or the Map's' values** are fulfilled. The promise's
* fulfillment value is an object or a Map with fulfillment values at respective keys to the original object
* or a Map. If any promise in the object or Map rejects, the returned promise is rejected with the rejection
* reason.
*
* If object is a trusted Promise, then it will be treated as a promise for object rather than for its
* properties. All other objects (except Maps) are treated for their properties as is returned by
* Object.keys - the object's own enumerable properties.
*
* @param input {Object} An Object
* @return {qx.promise.NativeWrapper}
*/
props(input) {
return qx.promise.NativeWrapper.resolve(input).then(input => {
let entries = Object.entries(input);
let promises = entries.map(
entry =>
new qx.promise.NativeWrapper(async resolve => {
const value = await entry[1];
resolve([entry[0], value]);
})
);
return qx.promise.NativeWrapper.all(promises).then(values => {
let result = {};
values.forEach(entry => {
result[entry[0]] = entry[1];
});
return result;
});
});
}
}
});