问题
I'm puzzled by something in the ES6 Promise API. I can see a clear use case for submitting multiple async jobs concurrently, and "resolving" on the first success. This would, for example, serve a situation where multiple equivalent servers are available, but some are perhaps down, and others heavily loaded and slow, so my goal would be to get a response from the first one to succeed, and ignore the rest (yes, I know this is an obnoxious way for a client to behave from a server's perspective, but it's great for the end user ;)
However, as far as I can see, I have either "all" or "race" behaviors to play with. The "all" behavior seems to wait until all the requests have completed, which means that I have to wait for the slowest, even if a server has already completed (indeed, I might have to wait for a timeout, with would be a disaster for this scenario.) The "race" behavior, however, seems to give me the first to complete, which if that happens to be a failure, is also a disaster.
Is there something in the API that permits a "raceToSuccess" kind of behavior, or do I have to build it by hand. For that matter, how would I build it by hand?
As a side note, I found the same puzzle in the Java 8 CompletableFuture, which seems to be a closely parallel API. So, am I missing something at a philosophical level?
回答1:
This is a classic example where inverting your logic makes it much clearer. Your "race" in this case is that you want your rejection behavior to in fact be success behavior.
function oneSuccess(promises){
return Promise.all(promises.map(p => {
// If a request fails, count that as a resolution so it will keep
// waiting for other possible successes. If a request succeeds,
// treat it as a rejection so Promise.all immediately bails out.
return p.then(
val => Promise.reject(val),
err => Promise.resolve(err)
);
})).then(
// If '.all' resolved, we've just got an array of errors.
errors => Promise.reject(errors),
// If '.all' rejected, we've got the result we wanted.
val => Promise.resolve(val)
);
}
回答2:
You can write this quite easily yourself.
function raceToSuccess(promises) {
return new Promise(
resolve =>
promises.forEach(
promise =>
promise.then(resolve)
)
);
}
This kicks off all the promises, and when any succeeds resolves the new promise with its value. Failed promises are ignored. Subsequent successful promises cause nothing to happen, since the new promise has already been resolved. Note that the resulting promise will never resolve or reject if none of the input promises resolve.
Here is a modified version which returns a rejected promise if all of the input promises reject:
function raceToSuccess(promises) {
let numRejected = 0;
return new Promise(
(resolve, reject) =>
promises.forEach(
promise =>
promise .
then(resolve) .
catch(
() => {
if (++numRejected === promises.length) reject();
}
)
)
);
}
I like @loganfsmyth's approach; you should probably upvote it for its conceptual clarity. Here's a variation of it:
function invertPromise(promise) {
return new Promise(
(resolve, reject) =>
promise.then(reject, resolve)
);
}
function raceToSuccess(promises) {
return invertPromise(
Promise.all(
promises.map(invertPromise)));
}
Another idea is to turn the failed promises into promises which neither resolve nor reject (in other words, are permanently pending), then use Promise.race
:
function pendingPromise() { return new Promise(() => { }); }
function killRejected(promise) { return promise.catch(pendingPromise); }
function raceToSuccess(promises) {
return Promise.race(promises.map(killRejected));
}
You may or not like the behavior of this. The returned promise will never fulfill or reject if none of the input promises fulfill. It's also possible that the permanently pending promises will not get GC'd, or some engines might eventually complain about them.
回答3:
I'm using a function based on Promise.race() but with a twist: it ignores rejects, unless all given promises reject:
// ignores any rejects except if all promises rejects
Promise.firstResolve = function (promises) {
return new Promise(function (fulfil, reject) {
var rejectCount = 0;
promises.forEach(function (promise) {
promise.then(fulfil, () => {
rejectCount++;
if(rejectCount == promises.length) {
reject('All promises were rejected');
}
});
});
});
};
It's based on Rich Harris's Promise polyfill race method. I just made the looping promise reject conditional: it only rejects the main promise, if all given promises failed, otherwise it ignores rejects and resolves the first success.
Usage:
// fastest promise to end, but is a reject (gets ignored)
var promise1 = new Promise((resolve, reject) => {
setTimeout(() => {
reject("foo")
}, 100);
})
// fastest promise to resolve (wins the race)
var promise2 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("bar")
}, 200);
})
// Another, slower resolve (gets ignored)
var promise3 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("baz")
}, 300);
})
Promise.firstResolve([promise1, promise2, promise3])
.then((res) => {
console.log(res) // "bar"
})
.catch(err => {
console.log(err) // "All promises were rejected" (if all promises were to fail)
})
The reason I use this instead of the promise inverting approach, is because in my opinion this is more readable.
To please the question in the strictest way, below there is a version that resolves the first successful promise but doesn't do anything if all given promises fail:
// ignores any and all rejects
Promise.firstResolve = function (promises) {
return new Promise(function (fulfil) {
promises.forEach(function (promise) {
promise.then(fulfil, () => {});
});
});
};
(usage same as above)
Edit: This is in fact the same as @user663031's suggestion. Which I haven't realized until just now.
回答4:
Old topic but here's my entry; it's essentially @loganfsmyth's solution, but with a few more checks to conform to conventions established by Promise.all():
- Empty array as input returns (synchronously) an already resolved promise
- Non-promise entries in the array results in the 1st such entry to be used as the resolved value
Promise.any = a => {
return !a.length ?
Promise.resolve() :
Promise.all(a.map(
e => (typeof e.then !== 'function') ?
Promise.reject(e) :
e.then(
result => Promise.reject(result),
failure => Promise.resolve(failure)
)
)).then(
allRejected => Promise.reject(allRejected),
firstResolved => Promise.resolve(firstResolved)
);
};
// Testing...
function delayed(timeout, result, rejected) {
return new Promise((resolve, reject) => {
setTimeout(
() => rejected ? reject(result) : resolve(result),
timeout);
});
}
Promise.any([
delayed(800, 'a'),
delayed(500, 'b'),
delayed(250, 'c', true)
]).then(e => {
console.log('First resolved (expecting b):', e);
});
Promise.any([
delayed(800, 'a', true),
delayed(500, 'b', true),
delayed(250, 'c', true)
]).then(null, e => {
console.log('All rejected (expecting array of failures):', e);
});
Promise.any([
delayed(800, 'a'),
delayed(500, 'b'),
delayed(250, 'c', true),
'd',
'e'
]).then(e => {
console.log('First non-promise (expecting d):', e);
});
// Because this is the only case to resolve synchronously,
// its output should appear before the others
Promise.any([]).then(e => {
console.log('Empty input (expecting undefined):', e);
});
回答5:
I expanded the @loganfsmyth approach with timeouts and I wrote a small function that:
- runs all the promises,
- wait for the promises to succeed for no more than a specified amount of time (options.timeOutMs),
- return the first that succeed.
In the following snippet, you can test it:
const firstThatCompleteSuccessfullyES6 = (options) => {
// return the first promise that resolve
const oneSuccess = (promises) => Promise.all(promises.map(p => {
// If a request fails, count that as a resolution so it will keep
// waiting for other possible successes. If a request succeeds,
// treat it as a rejection so Promise.all immediately bails out.
return p.then(
(val) => { return Promise.reject(val); },
(err) => { return Promise.resolve(err); }
);
})
).then(
// If '.all' resolved, we've just got an array of errors.
(errors) => { return Promise.reject(errors); },
// If '.all' rejected, we've got the result we wanted.
(val) => { return Promise.resolve(val); }
);
// return the promise or reect it if timeout occur first
const timeoutPromise = (ms, promise) => new Promise(function(resolve, reject) {
setTimeout(() => reject(new Error('timeout')), ms);
promise.then(resolve, reject);
});
if (options.subsystems.length < 1) {
return Promise.reject('Parameters error, no subSystems specified');
}
const timedOutSubsystems = options.subsystems.map(function(subsystem){
return timeoutPromise(options.timeOutMs, subsystem(options));
});
const startDate = Date.now();
return oneSuccess(
timedOutSubsystems
)
.then((result) => {
const elapsedTime = Math.abs((startDate - Date.now()) / 1000);
console.log('firstThatCompleteSuccessfully() done, after s: ' + elapsedTime + ': '+ result);
return result;
})
.catch((error) => {
const elapsedTime = Math.abs((startDate - Date.now()) / 1000);
console.error('firstThatCompleteSuccessfully() error/nodata: ' + error);
});
}
// example of use with two promises (subsystem1 & subsystem2) that resolves after a fixed amount of time
const subsystem1 = (options) => new Promise(function(resolve, reject) {
setTimeout(function(){
console.log('subsystem1 finished');
resolve('subsystem 1 OK');
}, 1000);
});
const subsystem2 = (options) => new Promise(function(resolve, reject) {
setTimeout(function(){
console.log('subsystem2 finished');
resolve('subsystem 2 OK');
}, 2000);
});
firstThatCompleteSuccessfullyES6({
subsystems: [subsystem1, subsystem2],
timeOutMs: 2000
})
.then((result) => console.log("Finished: "+result));
回答6:
To resolve this problem i used a Promise.rice
with a Promise.allSettled
.
The next code wait with the Promise.rice
a success value. but if no hay a success result. return a array with all errors.
const PromiseRiceSuccess = <T = unknown>(promises: Promise<T>[]) => {
let done: (reason?: T) => void;
const waitEndAllPromises = new Promise((resolve, reject) => done = reject);
const waitCatchs = promise => Promise.resolve(promise).catch(() => waitEndAllPromises);
Promise.allSettled(promises).then(r => done(r));
return Promise.race(promises.map(waitCatchs));
};
Example:
PromiseRiceSuccess([
Promise.reject(1),
new Promise((r) => setTimeout(() => r(2), 4000)),
]);
// 2
PromiseRiceSuccess([
Promise.reject(1),
new Promise((resolve, reject) => setTimeout(() => reject(2), 4000)),
]);
// Uncaught (in promise) (2) [{…}, {…}]
来源:https://stackoverflow.com/questions/37234191/how-do-you-implement-a-racetosuccess-helper-given-a-list-of-promises