2016-05-15 10 views
3

Ich bin verwirrt von etwas in der ES6 Promise API. Ich kann einen klaren Anwendungsfall sehen, wenn mehrere asynchrone Jobs gleichzeitig eingereicht und beim ersten Erfolg "aufgelöst" werden. Dies würde zum Beispiel einer Situation dienen, in der mehrere äquivalente Server verfügbar sind, aber einige sind möglicherweise nicht verfügbar und andere stark geladen und langsam. Mein Ziel wäre also, eine Antwort von der ersten zu erhalten und den Rest zu ignorieren (ja, ich weiß, das ist ein widerlicher Weg für einen Client aus der Perspektive eines Servers zu handeln, aber es ist großartig für den Endbenutzer;)Lösen Sie ES6 Versprechen mit ersten Erfolg?

Soweit ich jedoch sehen kann, habe ich entweder "alle" oder "Rennen "Verhaltensweisen mit denen man spielen kann. Das "alles" Verhalten scheint zu warten, bis alle Anfragen abgeschlossen sind, was bedeutet, dass ich auf die langsamsten warten muss, selbst wenn ein Server bereits fertig ist (tatsächlich muss ich vielleicht auf eine Zeitüberschreitung warten, was ein Desaster wäre) für dieses Szenario.) Das "Rassen" -Verhalten scheint mir jedoch den ersten zu vervollständigen, was, wenn dies ein Fehler ist, auch ein Desaster ist.

Gibt es etwas in der API, das ein "raceToSuccess" -Verhalten erlaubt, oder muss ich es manuell erstellen? Wie würde ich es von Hand bauen?

Als Nebenbemerkung fand ich das gleiche Puzzle in der Java 8 CompletableFuture, die eine eng parallele API zu sein scheint. Also, vermisse ich etwas auf einer philosophischen Ebene?

Antwort

16

Dies ist ein klassisches Beispiel, bei dem das Invertieren Ihrer Logik es viel klarer macht. Ihre "Rasse" ist in diesem Fall, dass Sie wollen, dass Ihr Ablehnungsverhalten tatsächlich ein Erfolgsverhalten ist.

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) 
); 
} 
+0

Plus eine für die Vermeidung der Verhinderer Konstruktor Anti-Pattern. – jib

+1

Ja, es ist eines dieser Dinge, nach denen ich Ausschau halte, wenn vielversprechende Fragen auftauchen. Wenn Sie "neues Versprechen" für etwas anderes als das Konvertieren einer Callback-API in eine Promise-API verwenden, brauchen Sie einen echten Grund. – loganfsmyth

+1

Gach, so einfach, wenn Sie es so sagen. Danke für eine sehr elegante Lösung. Ich gebe zu, ich kann immer noch nicht helfen zu denken, dass es in die API gehört! –

5

Sie können dies ganz einfach selbst schreiben.

function raceToSuccess(promises) { 
    return new Promise(
    resolve => 
     promises.forEach(
     promise => 
      promise.then(resolve) 
    ) 
); 
} 

Dies schlägt alle Versprechen ab, und wenn irgendein löst mit seinem Wert das neue Versprechen erfolgreich ist. Fehlgeschlagene Versprechen werden ignoriert. Nachfolgende erfolgreiche Versprechungen verursachen nichts, da das neue Versprechen bereits gelöst wurde. Beachten Sie, dass das resultierende Versprechen niemals aufgelöst oder abgelehnt wird, wenn keine der Eingabeversprechen aufgelöst wird.

Hier ist eine modifizierte Version, die eine Ablehnung Versprechen zurück, wenn alle Eingangs Versprechen ablehnen:

function raceToSuccess(promises) { 
    let numRejected = 0; 

    return new Promise(
    (resolve, reject) => 
     promises.forEach(
     promise => 
      promise . 
      then(resolve) . 
      catch(
      () => { 
       if (++numRejected === promises.length) reject(); 
       } 
      ) 
     ) 
); 
} 

I @ loganfsmyth Ansatz mag; Sie sollten es wahrscheinlich für seine konzeptionelle Klarheit upvote. Hier ist eine Variation davon:

function invertPromise(promise) { 
    return new Promise(
    (resolve, reject) => 
     promise.then(reject, resolve) 
); 
} 

function raceToSuccess(promises) { 
    return invertPromise(
    Promise.all(
     promises.map(invertPromise))); 
} 

Eine weitere Idee ist es, die gebrochenen Versprechen in Versprechungen zu machen, die weder Vorsatz noch ablehnen (in anderen Worten, anhängig permanent), dann verwenden Sie Promise.race:

function pendingPromise()  { return new Promise(() => { }); } 
function killRejected(promise) { return promise.catch(pendingPromise); } 

function raceToSuccess(promises) { 
    return Promise.race(promises.map(killRejected)); 
} 

Sie mag oder nicht wie das Verhalten von diesem. Das zurückgegebene Versprechen wird niemals erfüllt oder abgelehnt, wenn keines der eingegebenen Versprechen erfüllt wird. Es ist auch möglich, dass die permanent ausstehenden Versprechungen nicht gecallt werden, oder dass einige Engines sich über sie beschweren.

+0

Nützliche Diskussionen, vielen Dank für diese Zugabe. Wie Sie bemerken, mag ich die saubere Einfachheit von @ Loganfsmyths Antwort, aber Ihre ist definitiv wertschöpfend. –

1

Altes Thema, aber hier ist mein Eintrag; es ist im Wesentlichen @ loganfsmyth-Lösung, aber mit ein paar mehr Kontrollen Konventionen von Promise.all() etabliert entsprechen:

  • Leere Array als Eingabe zurückkehrt (synchron) ein bereits gelöst Versprechen
  • Nicht Versprechen Einträge in den Array-Ergebnisse in die erste solcher Eintrag als der aufgelösten Wert

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); 
 
});

0
verwendet werden

erweiterte ich den @loganfsmyth Ansatz mit Timeouts und ich schrieb eine kleine Funktion, die:

  • alle Versprechen läuft,
  • warten auf die Versprechungen für nicht mehr als eine bestimmte Menge an Zeit, um erfolgreich zu sein (options.timeOutMs),
  • die ersten zurückgeben, die erfolgreich sind.

Im folgenden Code-Schnipsel, können Sie es testen:

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));

Verwandte Themen