Carmen: ADVANCED - promise.all in Verbindung mit decorator functions

Hallo,

Bin in einer Situation, wo ich einige Promises startem will, ohne genau zu wissen, wie viele. Sobald ALLE Promises resolved werden, soll trotzdem Promise.all feuern.

Wollte das ohne Einsatz globaler Variablen lösen, und habe mich daher mit decorator functions beschäftigt.

Das promiseArr sollte bis zum Schluss eigentlich zu keinem Zeitpunkt leer sein und Promise.all daher nur einmal ganz zum Schluss resolved werden (Konsole: "All done!" - siehe angehängtes Code Snippet)

Hier, das Szenario, von dem ich denke, dass es eigentlich ablaufen sollte:

Das erste Mal wird decoratedPromise() mit einem setTimeout von 0 sofort aufgerufen und im promiseArr aufgenommen. Noch BEVOR das erste Promise nach 5 Sekunden resolved wird, befinden sich bereits zwei weitere Promises in promiseArr, nämlich die per setTimeout nach 2 und 3 Sekunden aufgerufen werden. Diese beiden Promises werden wieder jeweils nach 5 Sekunden resolved, daher nach 7 Sekunden (2+5 Sekunden) respektive nach 8 Sekunden (3+5 Sekunden).

==> Nach resolve des dritten Promise befindet sich bereits ein weiteres Promise im promiseArr, nämlich das nach 6 Sekunden aufgerufene!

Meiner Meinung nach müsste "All done!" genau einmal, nämlich nach 11 Sekunden feuern, dann nämlich hat auch das letzte aufgerufene Promise seinen Zyklus (6+5 Sekunden) abgeschlossen.

Effektiv feuert es nach fünf Sekunden dreimal und eine Sekunde darauf ein viertes Mal.

Bin ratlos, bitte um Hilfestellung! Lösungsansätzen bin ich auch nicht allzu sehr abgeneigt.

Danke

	const myPromise = new Promise((resolve, reject) => {
	  setTimeout(() => {resolve()}, 5000);
	});
	
	const promiseDecorator = (promise) => {
	
		let promiseArr = [];
		
		const promiseResolver = () => {
			Promise.all(promiseArr).then(() => {
				console.log("All done!");
			});
		};
		
		return () => {
			promiseArr.push(promise);
			promise;
			promiseResolver();
		}
	}
	
	const decoratedPromise = promiseDecorator(myPromise);

	setTimeout(() => {decoratedPromise()}, 0);
	setTimeout(() => {decoratedPromise()}, 2000);
	setTimeout(() => {decoratedPromise()}, 3000);
	setTimeout(() => {decoratedPromise()}, 6000);
  1. Tach!

    Bin in einer Situation, wo ich einige Promises startem will, ohne genau zu wissen, wie viele. Sobald ALLE Promises resolved werden, soll trotzdem Promise.all feuern.

    Das scheint mir seltsam und nicht zu Ende gedacht zu sein. Was soll passieren, wenn noch gar nicht alle Promises erzeugt sind, aber die bereits erzeugten schon alle erfüllt sind? Was ist dann das Kriterium? "Alle erfüllt" ist ja bereits eingetreten. "Alle erfüllt" kann das Promise.all() feststellen, aber "hab alle bekommen", kann es nicht. Das muss sowieso außerhalb gehandhabt und geprüft werden.

    Andererseits laufen Promises auch einfach so vor sich hin, ohne dass sie einem Promise.all() übergeben worden sind. Du kannst die Promises erstmal in einem Array sammeln, und erst wenn du kein neues mehr bekommst, übergibst du das Array dem Promise.all(). Das wartet dann darauf, das die restlichen Promises fertig sind, oder feuert sofort, wenn alle bereits erfüllt sind.

    Das promiseArr sollte bis zum Schluss eigentlich zu keinem Zeitpunkt leer sein und Promise.all daher nur einmal ganz zum Schluss resolved werden (Konsole: "All done!" - siehe angehängtes Code Snippet)

    Du füllst das Array schrittweise, und mit jedem Schritt startest du ein neues Promise.all(). Es ist nicht so, dass nur ein Promise.all() läuft und das Array überwacht, und sich jedes Mal neu organisiert, wenn es sich ändert. Du hast am Ende vier Promise.all() laufen, die jeweils einzeln feuern.

    Das erste Mal wird decoratedPromise() mit einem setTimeout von 0 sofort aufgerufen und im promiseArr aufgenommen.

    Das eine myPromise wird darin aufgenommen. Das wurde beim Erstellen von decoratedPromise durch das Aufrufen von promiseDecorator() als Parameter übergeben, und dort hängt es quasi erstmal rum. Das Ergebnis des Aufrufs war ja, dass die anonyme Funktion mit dem push() in decoratedPromise landet. Und immer wenn du sie aufrufst, wird das eine Promise, das da immer noch im Funktionsparameter von PromiseDecorator hängt, erneut ins Array geschoben.

    Noch BEVOR das erste Promise nach 5 Sekunden resolved wird, befinden sich bereits zwei weitere Promises in promiseArr, nämlich die per setTimeout nach 2 und 3 Sekunden aufgerufen werden.

    Es wird kein neues Promise erzeugt. Am Ende hängen 4 Referenzen auf das eine Promise in dem Array.

    Meiner Meinung nach müsste "All done!" genau einmal, nämlich nach 11 Sekunden feuern, dann nämlich hat auch das letzte aufgerufene Promise seinen Zyklus (6+5 Sekunden) abgeschlossen.

    Effektiv feuert es nach fünf Sekunden dreimal und eine Sekunde darauf ein viertes Mal.

    Das eine Promise feuert nach 5 Sekunden, und löst die drei laufenden Promise.all() aus. Dann kommt noch das 600er setTimeout, und in seiner Folge wird mit dem Array mit den nun 4 Referenzen auf das eine Promise, das außerdem bereits erfüllt ist, das vierte Promise.all() gestartet, das sofort die Erledigung meldet.

    dedlfix.

  2. Hallo Carmen,

    zusätzlich zu dem, was dedlfix sozusagen in dem Moment schrieb, wo ich deinen Code endlich durchschaut hatte, noch ein paar Anmerkungen.

    • Promise.all führt keine Live-Überwachung eines Promise-Arrays durch. Es nimmt die Promises, die im Moment des all()-Aufrufs im Array stehen, und überwacht genau diese und kein weiteres.

    • D.h. wenn Du ein System bauen willst, dass in dem Moment zuschlägt, wo das letzte Promise in einem Pool erfüllt wird, solltest Du jedenmal, wenn ein Teil-Promise erfüllt wird, dieses aus dem Pool entfernen. Und wenn der Pool leer ist, wird das Gesamtpromise erfüllt. Ggf. sollte der Promisemanager auch noch so was wie einen Sicherungsschalter bekommen, den Du erst freigibst, wenn das letzte Promise drin ist. Damit kannst Du verhindern, dass für den Fall, dass der Pool je nach Timing zwischendurch mal leer läuft, ein "Fertig" gemeldet wird.

    Das ist aber etwas, das Du selbst steuern musst, Promise.all() leistet das nicht.

    D.h. mein Vorschlag wäre entweder eine class PromisePool, die das Thema mit Eigenschaften und Methoden eines PromisePool-Objekts löst, oder eine Funktion getPromisePool, die vergleichbares mit einer Closure löst (also das, was Du im Moment hast). Ob man das "alles fertig" sinnvollerweise mit einem weiteren Promise löst (so wie Promise.all) oder einfach einen Callback festlegt, darüber kann man geteilter Meinung sein.

    Rolf

    --
    sumpsi - posui - obstruxi
    1. Hallo Rolf,

      mein Vorschlag für einen Promise-Controller sähe so aus:

      function getPromiseController() {
        let resolveAllDone,
            allDone = new Promise((resolve, reject) => resolveAllDone = resolve),
            pool = [];
      
        function complete(promise) {
          let p = pool.indexOf(promise);
          if (p >= 0)
            pool.splice(p, 1);
          if (pool.length == 0)
            resolveAllDone();
        }
      
        return {
          get whenDone() {
            return allDone;
          },
          watch(promise) {
            pool.push(promise);
            return promise.finally( () => complete(promise) );
          }
        };
      }
      
      let pc = getPromiseController();
      
      pc.watch(
         new Promise(...).then(result => /* verarbeite result 1 */)
      );
      pc.watch(
         new Promise(...).then(result => /* verarbeite result 2 */)
      );
      pc.watch(
         new Promise(...).then(result => /* verarbeite result 3 */)
      );
      
      pc.whenDone.then(() => console.log("All Done"));
      

      getPromiseController erzeugt ein Objekt mit zwei Eigenschaften: ein whenDone-Property, das ein Promise zurückgibt, das sich bei "all done" erfüllt. Und eine watch-Methode, der man ein Promise übergibt. Sobald es resolved oder rejected, wird überprüft, ob es das letzte offene Promise war. Wenn ja, wird "all done" resolved.

      Wichtig ist hier, dass Du der watch-Methode das Promise übergibst, das als Ergebnis deiner eigenen then/catch-Aufrufe entsteht. Ich habe das durch die Einrückungen der watch-Aufrufe dargestellt. Die watch-Methode kann es auch anders, sie gibt das finally-Promise wieder zurück, d.h. Du kannst auch auf das Ergebnis von watch weitere then-Handler registrieren. Der Unterschied ist das Timing. Ein then innerhalb von watch wird ausgeführt, bevor "all done" ausgelöst wird. Und ein then außerhalb von watch erfolgt erst nach dem "all done".

      Du musst den Parameter für watch natürlich nicht an Ort und Stelle erzeugen. Das kannst Du auch vorher tun und in eine Variable legen, oder Du rufst eine Funktion auf, die ein Promise zurückgibt. Beispielsweise eine Promise-Kapsel um setTimeout:

      function delay(zeit, info) {
         return new Promise( (res, rej) => setTimeout(() => res(info), zeit));
      }
      
      pc.watch(
         delay(1000, "1s vorbei")
         .then(result => console.log("Ergebnis: " + result))
      );
      

      Rolf

      --
      sumpsi - posui - obstruxi
      1. Danke euch, mir schwirrt jetzt der Kopf :)

        Stimmt, dass ich versehentlich mehrere Promise.all() starte, ergibt natürlich keinen Sinn. Muss mir eure Vorschläge wohl noch mehrere Male gewissenhaft anschauen um sie auch zu durchblicken und sie dann auch entsprechend zu adaptieren.

    2. Tach!

      • D.h. wenn Du ein System bauen willst, dass in dem Moment zuschlägt, wo das letzte Promise in einem Pool erfüllt wird, solltest Du jedenmal, wenn ein Teil-Promise erfüllt wird, dieses aus dem Pool entfernen. Und wenn der Pool leer ist, wird das Gesamtpromise erfüllt.

      Damit hat man das Problem des "Buffer Underrun", dass es schon fertig wird, wenn alle bisher bekannten Promises fertig sind, das ich am Anfang meiner Antwort erwähnte.

      Ggf. sollte der Promisemanager auch noch so was wie einen Sicherungsschalter bekommen, den Du erst freigibst, wenn das letzte Promise drin ist.

      Dafür braucht es keinen komplizierten Manager. Dann kann man die Promises auch in einem Array sammeln, dass man erst dann dem Promise.all() übergibt. Wenn man unbedingt will, sähe der Verwalter so aus:

      function observer() {
          const pool = [];
          return {
              next(promise) {
                  promise instanceof Promise && pool.push(promise);
              },
              complete(promise) {
                  this.next(promise);
                  return Promise.all(pool);
              }
          }
      }
      
      
      const o = observer();
      o.next();
      o.next();
      o.next();
      o.complete().then();
      

      Den next()s und auch dem complete() kann man die Promises übergeben.

      P.s. Man nehme const statt let, wenn sich die Werte nicht ändern.

      dedlfix.

      1. Hallo dedlfix,

        wenn ich mir das anschaue, frage ich mich, warum ich das bei meinen eigenen Überlegungen gestrichen habe. Ich war da mal, und es ist sinnvoller, ja.

        Aus meiner Ingrid-Antwort bleiben dann die Hinweise auf das Timing der then-Handler relevant, damit die Reihenfolge von Einzel-Resolve und Gesamt-Resolve passt.

        Was man noch überlegen muss, ist das Verhalten bei reject. Promise.all läuft auf rejected, sobald eins der Promises rejected wird. Mein Controller würde warten, bis das letzte Promise finalisiert ist, egal ob resolve oder reject.

        Das kann man in deiner Version natürlich auch haben, dazu müsste jedes einzelne Promise, das an next übergeben wird und rejecten könnte, einen catch-Handler haben, der den reject unterdrückt. Ich will das nicht als Nachteil verstanden wissen, nur als "es ist anders".

        Rolf

        --
        sumpsi - posui - obstruxi
        1. Tach!

          Aus meiner Ingrid-Antwort bleiben dann die Hinweise auf das Timing der then-Handler relevant, damit die Reihenfolge von Einzel-Resolve und Gesamt-Resolve passt.

          Nö. Die Reihenfolge stört im Prinzip nicht. Man weiß eh nie, wann die Promises fertig sind, weil sie üblicherweise nicht mit setTimeout laufen, sondern von der Laufzeit externer Systeme abhängen. Wichtig ist nur, dass sie irgendwann fertig sind, denn nur dann kann man Folgeverarbeitung starten.

          Was man noch überlegen muss, ist das Verhalten bei reject. Promise.all läuft auf rejected, sobald eins der Promises rejected wird. Mein Controller würde warten, bis das letzte Promise finalisiert ist, egal ob resolve oder reject.

          Es gibt auch Promise.allSettled(), das wartet bis der letzte Kandidat fertig ist, egal mit welchem Ergebnis.

          dedlfix.