bd-flux
Version:
a async flow and callback toolkit
309 lines (276 loc) • 13.5 kB
JavaScript
// bd-flux
// =======
// (c) 2017 Rüdiger Bund
// Lizenz/License: BSL.TXT / http://www.bundnetz.de/BSL.TXT
//
//
// bd-flux soll wohl im Browser als auch in Node.js verwendbar sein.
// Daher ist sowohl auf eine möglichst kompatible Notation (kein ES5/6)
// und die globale Definition für die Browser-nutzung geachtet. Die
// nun folgende Definition hat bei der Nutzung von `require` keine
// Bedeutung, doch wird dadurch die globale variable `fl` im Browser
// registriert.
var fl = (function() {
"use strict"; // jdi-disable-line
// Konstanten
// ----------
// Keine Verwendung des "const" Typen, da ggf. ältere Browser unterstützt werden sollen
var ERR_NOT_A_FUNCTION = " is not a function";
var ERR_NOT_AN_ARRAY = " is not an array";
// Private Klassenvariablen und -funktionen
// ----------------------------------------
// ### Allgemeine Hilfsfunktionen ###
// gibt es nur eine zum Erstellen einer Array-Kopie. Diese wird später gebraucht um eine
// Kopie für das `argument`-Objekt zu erstellen. Mir ist durchaus klar, dass dies auch
// mit Methoden der Array-Klasse mit weniger Quelltext möglich wäre, jedoch ist dies
// performanter (keine Relevanz hier) und ein Hinweis auf meinen Wunsch, Makros in
// Javascript einzuführen! :)
function copyArray(arr) { var i = 0, len = arr.length, res = new Array(len); for(; i < len; i++) res[i] = arr[i]; return(res); } // macro
// ### Klassenspezfische Hilfsfunktionen ###
// sind Funktionen oder Funktionsteile, die in Methoden mehrfach gebraucht werden.
// Die Funktion `next()` ruft die nächste Funktion aus dem Funktionsspeicher auf, wenn
// die aktuelle abgeschlossen ist. Geprüft wird dies immer dann, wenn entweder die
// Hauptroutine durchlaufen oder ein Callback "zurückgerufen" wurde.
// Abgeschlossen ist eine Funktion dann, wenn die Hauptroutine der Funktion
// durchlaufen wurde (markiert durch `closed`) _und_
// wenn auf keine weiteren Callbacks zu warten ist (bedeutet, wenn der
// Zähler der von `callbackcounter` gleich 0 ist.
function next($this) {
if ($this.closed && $this.callbackcounter === 0) {
if ($this.rindex < $this.queue.length) {
$this.callbackcounter = 0;
$this.closed = false;
$this.depth++;
$this.windex = $this.rindex+1;
$this.queue[$this.rindex++]($this.data);
$this.closed = true;
next($this);
$this.depth--;
}
}
}
// Die Funktion `cb_start()` wird bei Anforderung einer Callback-Funktion aufgerufen.
// Auch wenn sie momentan noch unscheibar ist, so halte ich es für besser diese
// für den späteren Gebrauch zu kapseln. Die Callback-Funktionen sollen keine
// Kenntnisse über die Verwaltung der Callbacks haben.
function cb_start($this) {
$this.callbackcounter++;
}
// Das Gleiche gilt für `cb_end()`.
function cb_end($this) {
$this.callbackcounter--;
next($this);
}
// MERKE: Beschreibung mergeJobData
function mergeJobData(targetdata, targetkey, sourcedata, id) {
var target = targetdata[targetkey];
if (!target) { target = {}; targetdata[targetkey] = target; }
for (var key in sourcedata) {
if (! target[key]) target[key] = [];
if (id !== undefined) target[key][id] = sourcedata[key];
else target[key].push(sourcedata[key]);
}
}
// Der Klassenprototyp
// -------------------
// Der Prototyp einer Klasse stellt i.d.R. Methoden bereit, die alle
// Instanzen der Klasse gemein haben. Instanzvariablen werden hingegen
// üblicherweise im Konstruktor erstellt.
var $proto = {
// Eine der wichtigsten Funktionen dieser Bibliothek ist es, eine auszuführende
// Funktion im Funktionsspeicher einzustellen. Dies wird durch die Methode `ex()`
// realisiert. `ex` steht dabei für "execute".
// Alle Parameter werden in der Reihenfolge der Übergabe angenommen,
// darauf geprüft ob sie Funktionen sind,
// und im Funktionsspeicher eingestellt.
ex : function() {
for (var i = 0; i < arguments.length; i++) {
if (typeof arguments[i] !== 'function') throw new TypeError(arguments[i] + ERR_NOT_A_FUNCTION);
this.queue.splice(this.windex++,0,arguments[i]);
}
return(this);
},
// Um eine Datenreihe, repräsentiert durch einen Array, mit nur einer Funktion
// abzuarbeiten, werden zwei Methoden zur Verfügung gestellt, die "job"-Methoden.
// Sie unterscheiden sich darin, dass eine die Daten sequentiell, also
// nacheinander abarbeitet, während die zweite dies parallel tut.
// Technisch gesehen wird für die Bearbeitung in der Iteration eine neue Instanz
// von bd-flux erstellt.
// MERKE: Beschreibung für sjob, pjob
// seriell
sjob : function(data_array, fn, reskey, noclear) {
if (typeof fn != "function") throw new TypeError(fn + ERR_NOT_A_FUNCTION);
if (!(Array.isArray(data_array))) throw new TypeError(data_array + ERR_NOT_AN_ARRAY);
var boss = new $constructor(0), $this = this;
if (data_array.length) {
for (var i = 0; i < data_array.length; i++) {
boss.ex(
(function(work, proc) {
var worker = function(d) {
proc(work, d);
};
return(worker);
})(data_array[i],fn),
(function(id) {
return(
function(d) {
mergeJobData($this.data, reskey, d, id);
if (!noclear) d.__fl.data = { "__fl": d.__fl };
});
})(i)
);
}
var waiter = this.cb();
boss.run(function(d) { delete $this.data[reskey].__fl; waiter(); });
} else this.data[reskey] = {};
return(this);
},
// parallel
pjob : function(data_array, fn, reskey) {
if (typeof fn != "function") throw new TypeError(fn + ERR_NOT_A_FUNCTION);
if (!(Array.isArray(data_array))) throw new TypeError(data_array + ERR_NOT_AN_ARRAY);
if (data_array.length) {
var boss = new $constructor(), $this = this;
for (var i = 0; i < data_array.length; i++) {
(function(work, proc, id, cb) {
var iboss = new $constructor();
iboss.run(
function(d) { proc(work, d); },
function(d) { mergeJobData($this.data, reskey, d, id); cb(); }
);
})(data_array[i],fn, i, boss.cb());
}
var waiter = this.cb();
boss.run(function(d) { delete $this.data[reskey].__fl; waiter(); });
} else this.data[reskey] = {};
return(this);
},
// Es kommt vor, dass es sinnvoll ist im aktuellen Ablauf einen neuen zu erstellen,
// speziell z.B. während der Ausführung von jobs (siehe oben).
// Dies ist jederzeit möglich indem eine neue Instanz eines bd-flux Objektes
// erstellt wird. Wird dieser Ablauf gestartet, läuft er "unabhängig" vom aktuellen,
// d.h. potentiell parallel ab. Kniffliger ist es, den neuen Ablauf mit dem aktuellen
// zu synchronisieren. Hierfür ist die Methode `sub()` gedacht. Sie erstellt ein
// neues bd-flux Objekt und gibt dieses zurück. Die aktuelle Funktion wird allerdings
// erst dann abgeschlossen, wenn zuvor der per `sub()` angeforderte neue Ablauf
// ausgeführt wurde.
sub : function() {
var flow = new $constructor();
var cb = this.cb();
flow.ex( function() { cb(); });
flow.windex--; // hmmm a bit hacky...
return(flow);
},
// Eine weitere essentielle Funktion ist die Kapselung von Callbacks.
// Ohne diese Kapselung ist unbekannt, ob die ausgeführte Funktion tatsächlich
// beendet ist und das ganze Modell wäre hinfällig.
// Generell kann zwischen zwei Arten von Callbacks unterschieden werden, einer
// generischen, die die übergebenen Parameter einfach weiterreicht, und der
// "klassischen", die zwei Parameter, den ersten zur Fehlerindikation und den
// zweiten als Ergebnis im Erfolgsfall, übergeben bekommt.
// Für beide Arten stellt bd-flux Methoden bereit, um diese einfach zu kapseln.
// Zuerst die generische:
// Die übergebenen Parameter werden als Schlüssel für das Datenobjekt
// interpretiert. Damit dies möglich ist, müssen die Argument in den Scope der
// zurückgegebenen Callback-Funktion übernommen werden.
// Die Callback-Funktion übernimmt die Zuordnung der Ergebnisse zu den evtl. zuvor
// angegebenen Schlüsseln. Dabei gibt es zwei Szenarien:
// 1. Es wurde ein Schlüssel für mehrere Ergebnisse angegeben
// 2. Es wurde für für jedes Ergebnis ein Schlüssel angegeben
// Fall 1 tritt dann ein, wenn mehrere Ergebnisse aber genau nur ein Schlüssel
// vorhanden ist. In dem Fall werden alle Ergebnisse als Array zum Schlüssel im
// Datenspeicher abgelegt. Gibt es nur einen Schlüssel und nur ein
// Ergebnis, so wird das eine Ergebnis direkt zum Schlüssel zugeordnet.
// Wenn es mehrere Schlüssel gibt, so werden die Ergebnisse in der gleichen
// Reihenfolge der Schlüssel zugeordnet. Überschüssige Ergebnisse, für die kein
// Schlüssel definiert wurde, werden ignoriert. Falls mehr Schlüssel als
// Ergebnisse vorhanden sind, werden die Werte der überschüssigen Schlüssel mit
// `undefined` gefüllt.
cb: function() {
var args = arguments, $this = this;
cb_start(this);
return( function() {
if (args.length == 1) $this.data[args[0]] = arguments.length == 1 ? arguments[0] : copyArray(arguments);
else for (var i = 0; i < args.length; i++) $this.data[args[i]] = i < arguments.length ? arguments[i] : undefined;
cb_end($this);
});
},
// Hier nun die Kapselung des "klassischen" Falles: ein Callback
// mit genau zwei Parametern, der erste im Fehler- und der zweite im
// Erfolgsfall. Optional kann der Schlüssel, unter dem das Ergebnis
// im Erfolgsfall im Datenobjekt gespeichert wird, angegeben
// werden. Sollte dieser nicht agegeben sein, so wird der Schlüssel auf
// "result" gesetzt.
// Ist ein Fehler aufgetreten, so wird dieser mit dem
// Schlüssel `error` im Datenspeicher abgelegt.
// Wenn der Fehler nicht gesetzt ist, ist vom Erfolgsfall auszugehen
// das das Ergebnis wird wie zuvor definiert abgespeichert.
cber: function(resultkey) {
var reskey = resultkey || "result", $this = this;
cb_start($this);
return(function(e,r) {
if (e) $this.data.error = e;
else $this.data[reskey] = r;
cb_end($this);
});
},
// Durch die Kapselung der auszuführenden Funktionen mittels der Methode
// `ex()` ist zwar bekannt, wann diese abgeschlossen sind, dies gilt
// jedoch nicht für den Aufrufer. Es muss der "Startschuss" gegeben
// werden, wann mit der Ausführung losgelegt werden soll. Dafür ist
// die Methode `run()` vorhanden. Zusätzlich können hier analog zur
// Methode `ex()` Funktionen angegeben werden, die auszuführen sind.
// Hintergrund ist, dass es ggf. ausreicht nur `run()` aufzurufen.
run: function() {
this.ex.apply(this, arguments);
this.closed = true;
next(this);
},
// Als Letztes kommt eine Methode zum Datenabruf bzw. -setzen in den
// Datenspeicher, die Methode `d()` (wie "Daten").
// Sie arbeitet je nach Parameteranzahl unterschiedlich.
// Bei keinem Parameter wird als Ergebnis das Datenspeicherobjekt
// zurückgegeben.
// Bei Angabe nur eines Parameters wird dieser als Schlüssel
// interpretiert und der dazu gespeicherte Wert aus dem
// Datenspeicher zurückgegeben.
// Bei zwei der mehr Parametern werden (momentan) nur die die ersten zwei
// berücksichtigt, der erste als Schlüssel und der zweite als Wert. Dieses
// Paar wird im Datenspeicherobjekt abgelegt.
d: function() {
switch (arguments.length) {
case 0: return(this.data);
case 1: return(this.data[arguments[0]]);
default: this.data[arguments[0]] = arguments[1];
}
return(this);
}
};
// Rein technisch: setzen des Prototypen, zuvor wurde dieser nur definiert.
$constructor.prototype = $proto;
// Der Konstruktor
// -------------------
function $constructor() {
if (! (this instanceof $constructor)) return new $constructor();
// - der Funktionsspeicher der aufzurufenden Funktionen
this.queue = [];
// - der Leseindex des Funktionsspeichers, der die als nächstes auszuführenden Funktion markiert
this.rindex = 0;
// - der Schreibindex des Funktionsspeichers, der die Stelle markiert wo weitere Funktionen eingefügt werden
this.windex = 0;
// - der Zähler für die angeforderten aber noch nicht aufgerufenen Callbacks
this.callbackcounter = 0;
// - der Datenspeicher für Variablen, der für alle Funktionen zur Verfügung steht
this.data = {__fl: this};
// - ein Indikator, ob die Hauptroutine der Funktion bereits ausgeführt wurde
// (und somit "nur" noch auf Callbacks zu warten ist)
this.closed = false;
// - ein Zähler für die Aufruftiefe der Funktionen untereinander, zum späteren Gebrauch
this.depth = 0;
}
// **Ende** - die zuvor definierte Klasse wird zurückgegeben
return($constructor);
})();
// Unterstützung für `require` i.d.R. im Node.js-Umfeld. Hier wird das zuvor erstelle
// Objekt exportiert.
if (typeof require !== "undefined" && typeof module !== "undefined") module.exports = fl;