bd-flux
Version:
a async flow and callback toolkit
348 lines (314 loc) • 12.4 kB
Markdown
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.
```js
var fl = (function() {
```
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! :)
```js
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.
```js
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.
```js
function cb_start($this) {
$this.callbackcounter++;
}
```
Das Gleiche gilt für `cb_end()`.
```js
function cb_end($this) {
$this.callbackcounter--;
next($this);
}
```
MERKE: Beschreibung mergeJobData
```js
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.
```js
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.
```js
ex : function() {
for (var i = 0; i < arguments.length; i++) {
if (typeof arguments[i] !== 'function') throw new TypeError(arguments[i] + ' is 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
```js
sjob : function(data_array, fn, reskey, noclear) {
var boss = new $constructor(0), $this = this;
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(); });
return(this);
},
```
parallel
```js
pjob : function(data_array, fn, reskey) {
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(); });
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.
```js
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.
```js
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.
```js
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.
```js
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.
```js
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.
```js
$constructor.prototype = $proto;
```
Der Konstruktor
-------------------
```js
function $constructor() {
if (! (this instanceof $constructor)) return new $constructor();
```
- der Funktionsspeicher der aufzurufenden Funktionen
```js
this.queue = [];
```
- der Leseindex des Funktionsspeichers, der die als nächstes auszuführenden Funktion markiert
```js
this.rindex = 0;
```
- der Schreibindex des Funktionsspeichers, der die Stelle markiert wo weitere Funktionen eingefügt werden
```js
this.windex = 0;
```
- der Zähler für die angeforderten aber noch nicht aufgerufenen Callbacks
```js
this.callbackcounter = 0;
```
- der Datenspeicher für Variablen, der für alle Funktionen zur Verfügung steht
```js
this.data = {__fl: this};
```
- ein Indikator, ob die Hauptroutine der Funktion bereits ausgeführt wurde
(und somit "nur" noch auf Callbacks zu warten ist)
```js
this.closed = false;
```
- ein Zähler für die Aufruftiefe der Funktionen untereinander, zum späteren Gebrauch
```js
this.depth = 0;
}
```
**Ende** - die zuvor definierte Klasse wird zurückgegeben
```js
return($constructor);
})();
```
Unterstützung für `require` i.d.R. im Node.js-Umfeld. Hier wird das zuvor erstelle
Objekt exportiert.
```js
if (typeof require !== "undefined" && typeof module !== "undefined") module.exports = fl;
```
------------------------
Generated _Sun Mar 05 2017 19:35:03 GMT+0100 (Mitteleuropäische Zeit)_ from [Ⓢ bd-flux.js](bd-flux.js "View in source")