lively.lang
Version:
JavaScript utils providing useful abstractions for working with collections, functions, objects.
241 lines (217 loc) • 8.06 kB
JavaScript
import { newUUID } from "./string.js";
import { pushIfNotIncluded, removeAt } from "./array.js";
/*global require, process, Promise, System*/
/*
* Methods helping with promises (Promise/A+ model). Not a promise shim.
*/
function promise(obj) {
// Promise object / function converter
// Example:
// promise("foo");
// // => Promise({state: "fullfilled", value: "foo"})
// lively.lang.promise({then: (resolve, reject) => resolve(23)})
// // => Promise({state: "fullfilled", value: 23})
// lively.lang.promise(function(val, thenDo) { thenDo(null, val + 1) })(3)
// // => Promise({state: "fullfilled", value: 4})
return (typeof obj === "function") ?
promise.convertCallbackFun(obj) :
Promise.resolve(obj);
}
function delay(ms, resolveVal) {
// Like `Promise.resolve(resolveVal)` but waits for `ms` milliseconds
// before resolving
return new Promise(resolve =>
setTimeout(resolve, ms, resolveVal));
}
function delayReject(ms, rejectVal) {
// like `promise.delay` but rejects
return new Promise((_, reject) =>
setTimeout(reject, ms, rejectVal));
}
function timeout(ms, promise) {
// Takes a promise and either resolves to the value of the original promise
// when it succeeds before `ms` milliseconds passed or fails with a timeout
// error
return new Promise((resolve, reject) => {
var done = false;
setTimeout(() => !done && (done = true) && reject(new Error('Promise timed out')), ms);
promise.then(
val => !done && (done = true) && resolve(val),
err => !done && (done = true) && reject(err))
});
}
function timeToRun(prom) {
let startTime = Date.now();
return Promise.resolve(prom).then(() => Date.now() - startTime);
}
function waitFor(ms, tester, timeoutObj) {
// Tests for a condition calling function `tester` until the result is
// truthy. Resolves with last return value of `tester`. If `ms` is defined
// and `ms` milliseconds passed, reject with timeout error
// if timeoutObj is passed will resolve(!) with this object instead of raise
// an error
if (typeof ms === "function") { tester = ms; ms = undefined; }
return new Promise((resolve, reject) => {
let stopped = false,
timedout = false,
timeoutValue = undefined,
error = undefined,
value = undefined,
i = setInterval(() => {
if (stopped) return clearInterval(i);
try { value = tester(); } catch (e) { error = e; }
if (!value && !error && !timedout) return;
stopped = true;
clearInterval(i);
if (error) return reject(error);
if (timedout)
return typeof timeoutObj === "undefined"
? reject(new Error("timeout"))
: resolve(timeoutObj);
return resolve(value);
}, 10);
if (typeof ms === "number") setTimeout(() => timedout = true, ms);
});
}
function deferred() {
// returns an object
// `{resolve: FUNCTION, reject: FUNCTION, promise: PROMISE}`
// that separates the resolve/reject handling from the promise itself
// Similar to the deprecated `Promise.defer()`
var resolve, reject,
promise = new Promise(function(_resolve, _reject) {
resolve = _resolve; reject = _reject; });
return {resolve: resolve, reject: reject, promise: promise};
}
function convertCallbackFun(func) {
// Takes a function that accepts a nodejs-style callback function as a last
// parameter and converts it to a function *not* taking the callback but
// producing a promise instead. The promise will be resolved with the
// *first* non-error argument.
// nodejs callback convention: a function that takes as first parameter an
// error arg and second+ parameters are the result(s).
// Example:
// var fs = require("fs"),
// readFile = promise.convertCallbackFun(fs.readFile);
// readFile("./some-file.txt")
// .then(content => console.log(String(content)))
// .catch(err => console.error("Could not read file!", err));
return function promiseGenerator(/*args*/) {
var args = Array.from(arguments), self = this;
return new Promise(function(resolve, reject) {
args.push(function(err, result) { return err ? reject(err) : resolve(result); });
func.apply(self, args);
});
};
}
function convertCallbackFunWithManyArgs(func) {
// like convertCallbackFun but the promise will be resolved with the
// all non-error arguments wrapped in an array.
return function promiseGenerator(/*args*/) {
var args = Array.from(arguments), self = this;
return new Promise(function(resolve, reject) {
args.push(function(/*err + args*/) {
var args = Array.from(arguments),
err = args.shift();
return err ? reject(err) : resolve(args);
});
func.apply(self, args);
});
};
}
function _chainResolveNext(promiseFuncs, prevResult, akku, resolve, reject) {
var next = promiseFuncs.shift();
if (!next) resolve(prevResult);
else {
try {
Promise.resolve(next(prevResult, akku))
.then(result => _chainResolveNext(promiseFuncs, result, akku, resolve, reject))
.catch(function(err) { reject(err); });
} catch (err) { reject(err); }
}
}
function chain(promiseFuncs) {
// Similar to Promise.all but takes a list of promise-producing functions
// (instead of Promises directly) that are run sequentially. Each function
// gets the result of the previous promise and a shared "state" object passed
// in. The function should return either a value or a promise. The result of
// the entire chain call is a promise itself that either resolves to the last
// returned value or rejects with an error that appeared somewhere in the
// promise chain. In case of an error the chain stops at that point.
// Example:
// lively.lang.promise.chain([
// () => Promise.resolve(23),
// (prevVal, state) => { state.first = prevVal; return prevVal + 2 },
// (prevVal, state) => { state.second = prevVal; return state }
// ]).then(result => console.log(result));
// // => prints {first: 23,second: 25}
return new Promise((resolve, reject) =>
_chainResolveNext(
promiseFuncs.slice(), undefined, {},
resolve, reject));
}
function promise_finally(promise, finallyFn) {
return Promise.resolve(promise)
.then(result => { try { finallyFn(); } catch(err) { console.error("Error in promise finally: " + err.stack || err); }; return result; })
.catch(err => { try { finallyFn(); } catch(err) { console.error("Error in promise finally: " + err.stack || err); }; throw err; });
}
function parallel(promiseGenFns, parallelLimit = Infinity) {
// Starts functions from promiseGenFns that are expected to return a promise
// Once `parallelLimit` promises are unresolved at the same time, stops
// spawning further promises until a running promise resolves
if (!promiseGenFns.length) return Promise.resolve([]);
let results = [],
error = null,
index = 0,
left = promiseGenFns.length,
resolve, reject;
return new Promise((res, rej) => {
resolve = () => res(results);
reject = err => rej(error = err);
spawnMore();
});
function spawn() {
parallelLimit--;
try {
let i = index++, prom = promiseGenFns[i]();
prom.then(result => {
parallelLimit++;
results[i] = result;
if (--left === 0) resolve();
else spawnMore();
}).catch(err => reject(err));
} catch (err) { reject(err); }
}
function spawnMore() {
while (!error && left > 0 && index < promiseGenFns.length && parallelLimit > 0)
spawn();
}
}
// FIXME!
Object.assign(promise, {
delay,
delayReject,
timeout,
waitFor,
deferred,
convertCallbackFun,
convertCallbackFunWithManyArgs,
chain,
"finally": promise_finally,
parallel
});
export default promise;
export {
promise,
delay,
delayReject,
timeout,
timeToRun,
waitFor,
deferred,
convertCallbackFun,
convertCallbackFunWithManyArgs,
chain,
promise_finally as finally,
parallel
}