bhiv
Version:
Extended asynchronous execution controller with composer syntax
1,562 lines (1,385 loc) • 53.2 kB
JavaScript
/*!
* Name: Bhiv
* Version: 3.1.44
* Date: 2016-12-07T12:00:00+02:00
* Description: Extended asynchronous execution controller with composer syntax
* Author: Nicolas Pelletier
* Maintainer: Nicolas Pelletier (nicolas [dot] pelletier [at] wivora [dot] fr)
* License: Released under the MIT license
* Flags: @preserve
*/
/**
* Pronounced: /bi hajv/ "Bee Hive"
*
* Hello world:
* 1. var bhiv = new Bhiv();
* 2. var func = new bhiv.Bee().then(function (d, c) { return c(null, 'hello '+d) }).end();
* 3. func('world', console.log.bind(console));
*
* Lexique:
* - dsn: Data Source Name (e.g. my.data.is.located.into.several.sub.objects)
* - fsn: Function Source Name (e.g. My.Function.Name.Is.that)
* - type: Type to check validity and reformat data
* - intype: before executing a before
* - outtype: on function output
* - glue: Pattern defining how data have to be inserted into another one
* - inglue: before executing a before
* - outglue: on function output
*
* Bhiv Constructor Arguments:
* - require(<fsn>, <callback>): facultative, a async function to require module
* object: it can be an object of functions
* - locals: facultative, a object populating locals
* - typer: facultative, a typer object
*
* Bee Builder Syntax:
* Note:
* - keyword with first letter uppercased must be closed with .close()
* - for parallel tasks, you can limit execution by closing with object e.g. .close({ max: 2 })
* Behaviour settings keywords:
* - set(<key>, <data>): put <data> into locals (at compose time)
* - on(<event>, <listener>): add a listener to a event
* - wait(<slot>): define this bee can't start before a listener gift to <slot> has been called
* Workflow constructor keywords:
* - Trap(<pattern>, <work>, <inglue>, <outglue>): do <work> if an error match <pattern>
* - Map(<path>, <key>, <value>): iter on <path> and do work for each elements
* -> replace the old list value by result
* - Go([<replacement_field>[, ...]]): create a une parallel waterfall
* - close(<properties>): close task, and set properties
* - emit(<event>, <data>): to emit an event (maybe catched by a bucket)
* - add(<glue>): allow you to add some thing in the flow
* - pipe(<work>, <inglue>, <outglue>): execute a task after the previous one if any
* -> replace the result
* - then(<work>, <inglue>, <outglue>): execute a task after the previous one if any
* -> merge the result
* - waterfall(<tasks>): do all tasks sequencially, with data replacing the previous one
* - bucket(<event>, <outglue>, <end>): retrieve data for an <event> even until the <end> event
* - extract(<inglue>, <outpath>): extract <inglue> pattern from data and set to <outpath> or '.'
* - keep(<fields>): keep only listed fields
* - end(<data>, <callback>): execute the bee or wrap either the <data> or the <callback>
*
* Bee Context Methods:
* - abort(): to stop the execution (terminate after all pending asynchronous call)
* - get(<key>): to retrieve a value stored in the locals
* - emit(<event>, <data>): to emit a event (may be catched by on or bucket keywork)
* - bee(): to create a sub workflow conserving events and locals
*
* Bee Glue Pattern Syntax:
* Note:
* - Only serializable json is allowed
* - To inject data use "${" as opener and "}" as closer
* Examples:
* - { some: { my_key: 'text', '${key}': 42, from: '${path.to.my.variable}' } }
* - ['${a}', 'toto-${b}']
* - 'some text'
*
* Bee Match Pattern Syntax:
* - TODO
*
* Module Properties:
* - method(<input>, <callback>): a asynchronous function to execute
* - intype: facultative, the type which be used to format input
* - outtype: the type which be used to format the output
*
* Typer Properties:
* - process(<type>, <data>, <callback>): The only method to format data
*
* Roadmap v3.2:
* - timeout for bee
* - stacktrace
* - integrate the typer
* - renew the stack to avoid stack overflow
*
* Roadmap v3.3:
* - add unabortable sequence (for transactions)
* - add suspend() and resume() method to the context
*
* TODO:
* - codify errors
* - fix callback missing error when an error is throwed inside a parallel
* - use event emitter instead of the lite one
* - add switch case with pattern matching mecanisme
* - fix Trap behaviour
*
*/
var globalize = function (alpha) {
if (typeof module != 'undefined') module.exports = alpha;
else if (typeof window != 'undefined') window[alpha.name] = alpha;
else if (typeof global != 'undefined') global[alpha.name] = alpha;
return alpha;
};
var Bhiv = globalize(function Bhiv(require, locals, typer) {
'use strict';
/***************/
var parseTask = function (work, inglue, outglue) {
if (work == null) work = function (data, callback) { return callback(); };
if (typeof work.type === 'function' && work instanceof work.type) return work;
var task = null;
if (typeof work === 'string') {
var isSync = /^\s*=\s*/.exec(work);
if (isSync) {
task = new Task.Synchronous();
work = work.substr(isSync[0].length);
} else {
task = new Task.Asynchronous();
}
task.fsn = work;
var module = Bhiv.getIn(options.module, work);
if (module) {
task.method = module.method;
if (module.scope) task.scope = module.scope;
if (module.input) task.input = module.input;
}
} else if (typeof work === 'function') {
var task = work.length < 2 ? new Task.Synchronous() : new Task.Asynchronous();
task.method = work;
task.fsn = work.name || Bhiv.generateId('Function.Anonymous');
} else if (typeof work === 'object') {
var task = new Task.Merge();
task.glue = work;
} else {
throw Bhiv.Error('Unable to produce a valide Asynchronous Task');
}
if (inglue) task.inglue = inglue;
if (outglue) task.outglue = outglue;
return task;
};
var parseTaskPipe = function (then) {
return Bhiv.pipe.call(this, parseTask, then);
};
var functionFinalized = function () {
throw Bhiv.Error('This function is disbaled because the bee has been finalized');
};
var beeNotRunning = function () {
throw Bhiv.Error('This function can be run only when running a bee');
};
/***************/
var bhiv = this;
var options = new (function BhivOption(require, locals, typer) {
this.modules = {};
switch (typeof require) {
case 'object':
this.require = Bhiv.serve(require);
break ;
case 'function':
this.require = require;
break ;
}
this.typer = typer;
this.locals = locals || {};
})(require, locals, typer);
/***************/
/* Bhiv Tasks Declation */
var Task = new function () {
var Task = this;
/* Simple */
this.Simple = function SimpleTask() { this.type = SimpleTask; };
this.Simple.prototype.execute = function (runtime) {
return runtime.callback.pop()(null, runtime);
};
this.Simple.prototype.clone = function () {
var task = new this.type();
for (var i in this)
if (this.hasOwnProperty(i)) {
if (i === 'type') continue ;
switch (Object.prototype.toString.call(this[i])) {
case '[object Array]':
task[i] = this[i].slice();
break ;
case '[object Object]':
task[i] = {};
for (var j in this[i])
task[i][j] = this[i][j];
break ;
default:
task[i] = this[i];
break ;
}
}
return task;
};
/* Closable */
this.Closable = function ClosableTask() { this.type = ClosableTask; };
this.Closable.prototype = new this.Simple();
this.Closable.prototype.end = function () {};
/* Concurent */
this.Concurent = function ConcurentTask() { this.type = ConcurentTask; };
this.Concurent.prototype = new this.Closable();
/* Asynchronous */
this.Asynchronous = function AsynchronousTask() {
this.type = AsynchronousTask;
this.fsn = null;
this.method = null;
this.intype = null;
this.outtype = null;
this.scope = null;
this.inglue = null;
this.outglue = null;
this.replace = false;
};
this.Asynchronous.prototype = new this.Simple();
this.Asynchronous.prototype.execute = function (runtime) {
var async = this;
if (runtime.status.aborted)
return runtime.callback.pop()(Bhiv.Error('Aborted at: ' + runtime.id), runtime);
this.lookup(runtime, function (error, method) {
if (error) return (runtime.callback.pop() || Bhiv.noop)(error, runtime);
var data = async.prepare(runtime);
var hasResponded = false;
// if (method.length != 2) warn !!
return method.call(data.context, data.input, function (error, output) {
if (hasResponded) throw Bhiv.Error(error || 'Already reponded');
hasResponded = true;
if (error) return (runtime.callback.pop() || Bhiv.noop)(Bhiv.Error(error), runtime);
if (arguments.length >= 2) async.insertOutput(runtime, output);
return runtime.callback.pop()(null, runtime);
});
});
};
this.Asynchronous.prototype.lookup = function (runtime, callback) {
if (typeof this.method === 'function') return callback(null, this.method);
if (typeof this.fsn === 'string') return this.invokeMethod(runtime, callback);
return callback(Bhiv.Error('Not enough element to execute this task'));
};
this.Asynchronous.prototype.prepare = function (runtime) {
var input = this.extractInput(runtime);
var context = new Context(runtime);
return { context: context, input: input };
};
this.Asynchronous.prototype.invokeMethod = function (runtime, callback) {
var fsn = this.fsn;
return options.require.call(runtime, this.fsn, function (err, module) {
if (err) {
return callback(err);
} else if (module == null) {
return callback(Bhiv.Error('method not found: ' + fsn, 'ERRFNNOTFOUND'));
} else if (typeof module === 'function') {
return callback(null, module);
} else if (typeof module.method === 'function') {
return callback(null, module.method);
} else {
return callback(Bhiv.Error('Unable to define a method: ' + fsn));
}
});
};
this.Asynchronous.prototype.extractInput = function (runtime) {
if (this.inglue == null) {
return runtime.data;
} else {
return Bhiv.extract(this.inglue, runtime.data, runtime.locals);
}
};
this.Asynchronous.prototype.insertOutput = function (runtime, output) {
var alpha = this.outglue == null ? output : Bhiv.extract(this.outglue, output, runtime.data);
if (this.replace) {
runtime.data = alpha;
} else {
if (alpha != null) {
runtime.data = Bhiv.ingest(runtime.data, alpha);
} else {
/* no replacement for runtime.data */
}
}
};
/* Synchronous */
this.Synchronous = function SynchronousTask() {
this.type = SynchronousTask;
this.fsn = null;
this.method = null;
this.intype = null;
this.outtype = null;
this.scope = null;
this.inglue = null;
this.outglue = null;
this.replace = false;
};
this.Synchronous.prototype = new this.Asynchronous();
this.Synchronous.prototype.execute = function (runtime) {
var sync = this;
if (runtime.status.aborted)
return runtime.callback.pop()(Bhiv.Error('Aborted at: ' + runtime.id), runtime);
this.lookup(runtime, function (error, method) {
if (error) return (runtime.callback.pop() || Bhiv.noop)(error, runtime);
var data = sync.prepare(runtime);
try {
var output = method.call(data.context, data.input);
} catch (error) {
return runtime.callback.pop()(error, runtime);
}
sync.insertOutput(runtime, output);
return setImmediate(runtime.callback.pop(), null, runtime);
});
};
/* Waterfall */
this.Waterfall = function WaterfallTask() {
this.type = WaterfallTask;
this.tasks = [];
this.merge = null;
};
this.Waterfall.prototype = new this.Simple();
this.Waterfall.prototype.execute = function (runtime) {
var initialData = runtime.data;
return (function loop(waterfall, index, runtime) {
if (waterfall.tasks.length === index) {
if (waterfall.merge instanceof Array) {
if (initialData == null || typeof initialData != 'object') initialData = {};
for (var i = 0; i < waterfall.merge.length; i++) {
var path = waterfall.merge[i];
Bhiv.setIn(initialData, path, Bhiv.getIn(runtime.data, path));
}
} else if (waterfall.merge === true) {
runtime.data = Bhiv.ingest(initialData, runtime.data);
}
return runtime.callback.pop()(null, runtime);
} else {
runtime.callback.push(function WaterfallCallback(error, runtime) {
if (error) return (runtime.callback.pop() || Bhiv.noop)(error, runtime);
return loop(waterfall, index + 1, runtime);
});
return waterfall.tasks[index].execute(runtime);
}
})(this, 0, runtime);
};
/* Parallel */
this.Parallel = function ParallelTask(max) {
this.type = ParallelTask;
this.tasks = [];
this.max = max || Infinity;
this.fields = null;
};
this.Parallel.prototype = new this.Concurent();
this.Parallel.prototype.execute = function (initialRuntime) {
var failure = null;
var space = { done: 0, running: 0 };
return (function loop(parallel, space, runtime) {
if (failure) {
return ; // kill the loop
} else if (space.done === parallel.tasks.length) {
return initialRuntime.callback.pop()(null, initialRuntime);
} else {
while (space.running < parallel.max && space.done + space.running < parallel.tasks.length)
(function (task) {
space.running += 1;
var newRuntime = runtime.fork(false, true);
newRuntime.callback.push(function ParallelCallback(error, runtime) {
space.running -= 1;
space.done += 1;
if (error) {
failure = error;
return (initialRuntime.callback.pop() || Bhiv.noop)(error, initialRuntime);
}
return loop(parallel, space, initialRuntime);
});
return task.execute(newRuntime);
})(parallel.tasks[space.done + space.running]);
}
})(this, space, initialRuntime);
};
/* Map */
this.Map = function MapTask() {
this.type = MapTask;
this.source = null;
this.single = false;
this.key = null;
this.value = null;
this.task = new Task.Waterfall();
this.max = Infinity;
};
this.Map.prototype = new this.Concurent();
this.Map.prototype.execute = function (runtime) {
if (!runtime.data || typeof runtime.data !== 'object')
if (error) return runtime.callback.pop()(null, runtime);
var map = this;
var space = { done: 0, running: 0, tasks: [], failure: null, result: null };
if (map.source.indexOf('!') === 0) {
map.source = map.source.substr(1);
if (map.source == '') map.source = '.';
map.single = true;
}
var source = Bhiv.getIn(runtime.data, map.source);
if (map.single) {
space.result = null;
space.tasks.push({ value: source });
} else if (source instanceof Array) {
space.result = new Array(source.length);
for (var i = 0; i < source.length; i++)
space.tasks.push({ key: i, value: source[i] });
} else if (source && typeof source === 'object') {
space.result = {};
for (var i in source)
space.tasks.push({ key: i, value: source[i] });
} else {
return runtime.callback.pop()(null, runtime);
}
return this.loop(runtime, space);
};
this.Map.prototype.loop = function (runtime, space) {
var map = this;
if (space.running < 1 && space.tasks.length < 1)
return this.returns(runtime, space);
if (space.running >= map.max || space.tasks.length < 1) return ;
space.running += 1;
var task = space.tasks.shift();
var newRuntime = runtime.fork(false);
if (map.key == null && map.value == null) {
newRuntime.data = task.value;
} else {
var iterator = Object.create(runtime.data);
if (typeof map.key === 'string') iterator[map.key] = task.key;
if (typeof map.value === 'string') iterator[map.value] = task.value;
newRuntime.data = Object.create(iterator);
}
newRuntime.callback.push(function (error, newRuntime) {
space.running -= 1;
space.done += 1;
if (space.failure) return ;
if (error) {
task.error = error;
space.failure = task;
return runtime.callback.pop()(task, runtime);
}
map.processResult(runtime, space, task, newRuntime.data);
return map.loop(runtime, space);
});
map.task.execute(newRuntime);
return space.tasks.length <= 0 ? Bhiv.noop() : this.loop(runtime, space);
};
this.Map.prototype.returns = function (runtime, space) {
if (!this.source || this.source === '.')
runtime.data = space.result;
else
Bhiv.setIn(runtime.data, this.source, space.result);
return runtime.callback.pop()(null, runtime);
};
this.Map.prototype.processResult = function (runtime, space, task, data) {
if (this.single) {
return space.result = data;
} else {
if (data && typeof data === 'object') {
if (data.contructor === Array) {
return space.result[task.key] = data.slice();
} else {
var record = task.value && typeof task.value === 'object' ? Object.create(task.value) : {};
var altered = false;
for (var key in data) {
if (data.hasOwnProperty(key)) {
record[key] = data[key];
altered = true;
}
}
if (!altered) return space.result[task.key] = task.value;
return space.result[task.key] = record;
}
}
return space.result[task.key] = data;
}
};
/* Each */
this.Each = function EachTask() {
this.type = EachTask;
this.task = new Task.Waterfall();
};
this.Each.prototype = new this.Map();
this.Each.prototype.processResult = function (runtime, space, task, data) {
runtime.data = Bhiv.merge(runtime.data, data);
};
this.Each.prototype.returns = function (runtime, space) {
return runtime.callback.pop()(null, runtime);
};
/* Trap */
this.Trap = function TrapTask(pattern, task) {
this.type = TrapTask;
this.pattern = pattern || {};
this.task = task || null;
};
this.Trap.prototype = new this.Closable();
this.Trap.prototype.execute = function (runtime) {
if (runtime.callback.length > 1) {
var trap = this;
var parentIndex = runtime.callback.length - 2;
var parentCallback = runtime.callback[parentIndex];
runtime.callback[parentIndex] = function TrapCallback(error, runtime) {
if (!error) return parentCallback(null, runtime);
if (!Bhiv.match(trap.pattern, error.error || error)) return parentCallback(error, runtime);
runtime.callback.push(parentCallback);
runtime.data = Bhiv.ingest(runtime.data, { error: error });
return trap.task.execute(runtime);
};
}
return runtime.callback.pop()(null, runtime);
};
/* Escape */
this.Escape = function EscapeTask(condition, glue) {
this.condition = condition;
this.glue = glue;
};
this.Escape.prototype = new this.Simple();
this.Escape.prototype.execute = function (runtime) {
var result = Bhiv.extract(this.condition, runtime.data, runtime.locals);
if (result) {
if (this.glue != null)
runtime.data = Bhiv.extract(this.glue, runtime.data, runtime.locals);
return runtime.callback.shift()(null, runtime);
} else {
return runtime.callback.pop()(null, runtime);
}
};
/* Merge */
this.Merge = function MergeTask(glue) {
this.type = MergeTask;
this.glue = glue || null;
this.replace = false;
};
this.Merge.prototype = new this.Simple();
this.Merge.prototype.execute = function (runtime) {
var alpha = Bhiv.extract(this.glue, runtime.data, runtime.locals);
runtime.data = this.replace ? alpha : Bhiv.merge(runtime.data, alpha);
return runtime.callback.pop()(null, runtime);
};
/* Bucket */
this.Bucket = function TaskBucket(event, outglue) {
this.type = TaskBucket;
this.id = Bhiv.generateId('Bhiv.Task.Bucket');
this.event = event;
this.outglue = outglue || null;
this.customEnd = null;
};
this.Bucket.prototype = new this.Simple();
this.Bucket.prototype.execute = function (runtime) {
var bucket = this;
var space = runtime.temp[this.id];
if (bucket.customEnd === null) space.terminated = true;
if (space.terminated)
return this.next(runtime, space.chunks);
else
space.callback = function (chunks) { bucket.next(runtime, chunks); };
};
this.Bucket.prototype.receive = function (runtime, chunk) {
runtime.temp[this.id].chunks.push(chunk);
};
this.Bucket.prototype.terminate = function (runtime) {
var space = runtime.temp[this.id];
space.terminated = true;
if (typeof space.callback)
return setImmediate(space.callback, space.chunks);
};
this.Bucket.prototype.next = function (runtime, chunks) {
if (this.outglue == null) {
runtime.data = chunks;
} else {
var data = {};
data[this.event] = chunks;
var alpha = Bhiv.extract(this.outglue, data, runtime.data);
runtime.data = Bhiv.merge(runtime.data, alpha);
}
return runtime.callback.pop()(null, runtime);
};
/***************/
for (var constructor in this) {
if (typeof this[constructor] !== 'function') continue ;
this[constructor].valueOf = (function (name) {
var constructorName = '[BhivTask ' + name + ']';
return function () { return constructorName; };
})(constructor);
}
};
/***************/
/* Bee Construction Breadcrumb */
var Breadcrumb = function () {
this._path = [];
};
Breadcrumb.prototype.add = function (/* task, ... */) {
for (var i = 0; i < arguments.length; i++)
this._path.unshift(arguments[i]);
return i;
};
Breadcrumb.prototype.count = function () {
return this._path.length;
};
Breadcrumb.prototype.has = function (type) {
if (!(type instanceof Array)) type = [type];
for (var i = 0; i < this._path.length; i++) {
var current = this._path[i];
if (~type.indexOf(current.type))
return true;
else if (current instanceof Task.Closable)
return false;
}
return false;
};
Breadcrumb.prototype.hasClosable = function () {
for (var i = 0; i < this._path.length; i++) {
if (this._path[i] instanceof Task.Closable)
return true;
}
return false;
};
Breadcrumb.prototype.get = function (type) {
if (!type) return this._path[0] || null;
if (!(type instanceof Array)) type = [type];
for (var i = 0; i < this._path.length; i++)
if (~type.indexOf(this._path[i].type)) return this._path[i];
return null;
};
Breadcrumb.prototype.getClosable = function () {
for (var i = 0; i < this._path.length; i++)
if (this._path[i] instanceof Task.Closable)
return this._path[i];
return null;
};
Breadcrumb.prototype.remove = function (type, included) {
if (!type) return this._path.splice(0, Infinity);
if (!(type instanceof Array)) type = [type];
for (var i = 0; i < this._path.length; i++) {
var currentType = this._path[i].type;
if (~type.indexOf(currentType)) break ;
}
if (included && ~type.indexOf(currentType)) i++;
return this._path.splice(0, i);
};
/***************/
/* Bee Runtime */
var Runtime = function BeeRuntime(id, callback) {
this.id = Bhiv.generateId(id);
this.callback = [];
if (!callback) this.callback.push(Bhiv.noop);
else this.callback.push(function FinalCallback(err, runtime) {
if (err) return callback(err);
return callback(null, runtime.data);
});
};
Runtime.prototype.fork = function (keepCallbacks, keepData) {
var runtime = new Runtime(this.id);
for (var key in this) {
if (!this.hasOwnProperty(key)) continue ;
if (key in runtime) continue ;
runtime[key] = this[key];
}
if (runtime.data instanceof Object && !keepData)
runtime.data = Object.create(runtime.data);
runtime.callback = keepCallbacks ? this.callback : [];
return runtime;
};
/* Bee Runtime Context */
var Context = function BeeRuntimeContext(runtime) {
this.bee = function () {
return new bhiv.Bee(runtime);
};
this.pipe = function () {
if (arguments.length === 0) return new bhiv.Bee(runtime);
var bee = new bhiv.Bee(runtime);
return bee.pipe.apply(bee, arguments);
};
this.then = function () {
if (arguments.length === 0) return new bhiv.Bee(runtime);
var bee = new bhiv.Bee(runtime);
return bee.then.apply(bee, arguments);
};
this.get = function (key) {
return Bhiv.getIn(runtime.locals, key);
};
this.set = function (key, value) {
return Bhiv.setIn(runtime.locals, key, value);
};
this.emit = function (event, data) {
var emitter = runtime.events[event];
if (typeof emitter === 'function') emitter(runtime, data);
return this;
};
this.abort = function () {
runtime.status.aborted = true;
return this;
};
};
/***************/
/* set */
this.set = function (key, value) {
Bhiv.setIn(options.locals, key, value);
return this;
};
this.execute = function (fsn, data, callback) {
return new bhiv.Bee().pipe(fsn).end(data, callback || Bhiv.noop);
};
/* Bee */
this.Bee = function Bee(_runtime) {
if (!_runtime) _runtime = {};
var bee = this;
var ready = true;
var onReady = [];
this._workflow = null;
this._breadcrumb = new Breadcrumb();
this._id = _runtime.id || Bhiv.generateId('Bhiv.Bee.id');
this._temp = {};
this._locals = Object.create(_runtime.locals ? _runtime.locals : options.locals);
this._events = _runtime.events ? Object.create(_runtime.events) : {};
this._createRuntime = function (data, callback) {
var runtime = new Runtime(this._id, callback);
runtime.status = _runtime.status || { aborted: false };
runtime.data = data;
runtime.locals = Object.create(this._locals);
runtime.events = Object.create(this._events);
runtime.temp = Bhiv.extract(this._temp);
runtime.context = new Context(runtime);
return runtime;
};
this._finalize = function () {
this._breadcrumb = null;
for (var keyword in Bee.prototype)
bee[keyword] = functionFinalized;
};
this._ready = function (status) {
ready = !!status;
if (ready)
while (onReady.length > 0)
onReady.shift()();
};
this._execute = function (data, callback) {
if (bee._breadcrumb) throw Bhiv.Error('This bee is not finalized');
var workflow = bee._workflow;
if (workflow == null) return callback(null, data);
if (typeof callback != 'function' && typeof callback.createCallback == 'function')
callback = callback.createCallback();
var runtime = bee._createRuntime(data, callback);
if (this && this.locals) {
for (var k in this.locals)
if (!(k in runtime.locals)) // TODO check if it necessary
runtime.locals[k] = this.locals[k];
}
if (!ready) {
onReady.push(function () { workflow.execute(runtime); });
} else {
workflow.execute(runtime);
}
return runtime.context;
};
};
/* Compile Time Method */
/* .on */
this.Bee.prototype.on = function (event, listener) {
if (this._events.hasOwnProperty(event))
throw Bhiv.Error('can not bind "'+event+'" because it is already used');
this._events[event] = listener;
return this;
};
/* .set */
this.Bee.prototype.set = function (dsn, value) {
Bhiv.setIn(this._locals, dsn, value);
return this;
};
/* .get */
this.Bee.prototype.get = function (dsn) {
return Bhiv.getIn(this._locals, dsn);
};
/* .wait */
this.Bee.prototype.wait = function (slot) {
var bee = this;
if (this._workflow) throw new Bhiv.Error('Usage of "wait" have to be first');
bee._ready(false);
slot(function () { bee._ready(true); });
return this;
};
/* .store */
this.Bee.prototype.store = function (glue, path) {
var task = new Task.Synchronous();
task.replace = true;
task.method = function (data) {
var extract = Bhiv.extract(glue, data);
this.set(path, extract);
return data;
};
return this.then(task);
};
/* Contruction Methods */
/* .end */
this.Bee.prototype.end = function end(data, callback) {
var bee = this;
this._finalize();
switch (arguments.length) {
case 0:
return function (data, callback) { return bee._execute.call(this, data, callback); };
case 1:
if (typeof arguments[0] == 'function')
return function (data) { return bee._execute.call(this, data, callback); };
else
return function (callback) { return bee._execute.call(this, data, callback); };
case 2: default:
if (typeof callback == 'function')
return bee._execute.call(this, data, callback);
else
return function (callback) { return bee._execute.call(this, data, callback); };
}
};
/* .then */
this.Bee.prototype.then = parseTaskPipe(function (task) {
if (this._breadcrumb.has(Task.Waterfall)) {
// is in waterfall
var waterfall = this._breadcrumb.get(Task.Waterfall);
waterfall.tasks.push(task);
this._breadcrumb.remove(Task.Waterfall);
this._breadcrumb.add(task);
} else if (this._breadcrumb.hasClosable()) {
// is in closable without waterfall
var chainedTask = this._breadcrumb.getClosable();
var waterfall = new Task.Waterfall();
if (chainedTask.tasks instanceof Array) {
var key = chainedTask.tasks.length - 1;
var last = chainedTask.tasks[key];
waterfall.tasks.push(last, task);
chainedTask.tasks[key] = waterfall;
} else if (chainedTask.task) {
waterfall.tasks.push(chainedTask.task, task);
chainedTask.task = waterfall;
} else {
throw Bhiv.Error('Unknow where to add this task');
}
this._breadcrumb.remove(chainedTask.type);
this._breadcrumb.add(waterfall, task);
} else if (this._workflow == null) {
// is the first task
this._workflow = task;
this._breadcrumb.add(task);
} else if (this._breadcrumb.count() <= 1) {
// is the second task then wrapped into a waterfall
var last = this._workflow;
var waterfall = new Task.Waterfall();
if (last != null) waterfall.tasks.push(last)
waterfall.tasks.push(task);
this._workflow = waterfall;
this._breadcrumb.remove();
this._breadcrumb.add(waterfall, task);
} else {
debugger;
throw Bhiv.Error('usage of "then" in unknown situation');
}
return this;
});
/* .pipe */
this.Bee.prototype.pipe = parseTaskPipe(function (task) {
task.replace = true;
return this.then(task);
});
/* .Map */
this.Bee.prototype.Map = function (source, key, value) {
var map = new Task.Map();
map.source = source;
map.key = key || null;
map.value = value || null;
if (this._breadcrumb.has(Task.Concurent))
map.max = 1;
this.then(map);
this._breadcrumb.add(map.task);
return this;
};
/* .map */
this.Bee.prototype.map = function (source, work, inglue, outglue) {
var map = new Task.Map();
map.source = source;
map.task = parseTask(work, inglue, outglue);
map.task.replace = true;
if (this._breadcrumb.has(Task.Concurent))
map.max = 1;
this.then(map);
this._breadcrumb.add(map.task);
return this.close();
};
/* .Each */
this.Bee.prototype.Each = function (source, key, value) {
var each = new Task.Each();
each.source = source;
each.key = key || null;
each.value = value || null;
if (this._breadcrumb.has(Task.Concurent))
each.max = 1;
this.then(each);
this._breadcrumb.add(each.task);
return this;
};
/* .Go */
this.Bee.prototype.Go = function () {
var fields = Array.prototype.slice.call(arguments);
var waterfall = new Task.Waterfall();
waterfall.merge = fields.length > 0 ? fields : true;
if (this._breadcrumb.has(Task.Parallel) == false) {
// create a new parallel
var parallel = new Task.Parallel();
parallel.tasks.push(waterfall);
if (this._breadcrumb.has(Task.Waterfall, [Task.Parallel])) {
// is in waterfall
var parent = this._breadcrumb.get(Task.Waterfall);
parent.tasks.push(parallel);
this._breadcrumb.remove(Task.Waterfall);
this._breadcrumb.add(parallel, waterfall);
} else if (this._workflow == null) {
// is the first task
this._workflow = parallel;
this._breadcrumb.add(parallel, waterfall);
} else {
debugger;
throw Bhiv.Error('usage of "go" in unknown situation');
}
} else {
// is in parallel
var parallel = this._breadcrumb.get(Task.Parallel);
parallel.tasks.push(waterfall);
this._breadcrumb.remove(Task.Parallel);
this._breadcrumb.add(waterfall);
}
return this;
};
/* .Trap */
this.Bee.prototype.Trap = function (pattern, work, inglue, outglue) {
var task = parseTask(work, inglue, outglue);
task.replace = true;
var trap = new Task.Trap(pattern, task);
return this.then(trap);
};
/* .close */
this.Bee.prototype.close = function (properties) {
if (this._breadcrumb.hasClosable()) {
// is in closable
var task = this._breadcrumb.getClosable();
task.end();
for (var key in properties) task[key] = properties[key];
this._breadcrumb.remove(task.type, true);
} else {
debugger;
throw Bhiv.Error('usage of "close" in unknown situation');
}
return this;
};
/* .trap */
this.Bee.prototype.trap = function () {
return this.Trap.apply(this, arguments).close();
};
/* .emit */
this.Bee.prototype.emit = function (event, value) {
var task = new Task.Synchronous();
task.method = function (data) { this.emit(event, Bhiv.extract(value, data)); };
return this.then(task);
};
/* .extract */
this.Bee.prototype.extract = function (glue, outpath) {
var task = new Task.Synchronous();
task.replace = true;
task.method = function (data) {
var extract = Bhiv.extract(glue, data);
if (!outpath) return extract;
Bhiv.setIn(data, outpath, extract);
return data;
};
return this.then(task);
};
/* .keep */
this.Bee.prototype.keep = function (fields) {
if (arguments.length === 0) fields = [];
else if (!(fields instanceof Array)) fields = [fields];
var task = new Task.Synchronous();
task.replace = true;
task.method = function (data) {
var newData = {};
for (var i = 0; i < fields.length; i++) {
var value = Bhiv.getIn(data, fields[i]);
if (value != null) Bhiv.setIn(newData, fields[i], value);
}
return newData;
};
return this.then(task);
};
/* .waterfall */
this.Bee.prototype.waterfall = function (tasks) {
for (var i = 0; i < tasks.length; i++) this.pipe(tasks[i]);
return this;
};
/* .bucket */
this.Bee.prototype.bucket = function (dataEvent, outglue, endEvent) {
var bee = this;
var bucket = new Task.Bucket(dataEvent, outglue);
this._temp[bucket.id] = { chunks: [], terminated: false, callback: null };
this.on(dataEvent, function (runtime, chunk) { return bucket.receive(runtime, chunk); })
if (typeof endEvent == 'string') {
bucket.customEnd = endEvent;
this.on(endEvent, function (runtime) { return bucket.terminate(runtime); });
}
return this.then(bucket);
};
/* .add */
this.Bee.prototype.add = function (glue) {
return this.then(new Task.Merge(glue));
};
/* .flatten */
this.Bee.prototype.flatten = function () {
var task = parseTask(function (data, callback) {
return callback(null, Bhiv.flatten(data));
});
return this.then(task);
};
/* .breakIf */
this.Bee.prototype.breakIf = function (pattern, glue) {
var task = new Task.Escape(pattern, glue);
return this.then(task);
};
/* .dump */
this.Bee.prototype.dump = function (glue) {
var task = new Task.Synchronous();
task.inglue = glue || null;
task.method = function (data) { console.log(data); };
return this.then(task);
};
/* .debug */
this.Bee.prototype.debug = function () {
var task = new Task.Synchronous();
task.method = function (data) { debugger; };
return this.then(task);
};
});
/***********************************/
Bhiv.Error = function (msg, code) {
var error = null;
if (msg instanceof Error) {
error = msg;
} else if (msg instanceof Object) {
if (msg.error instanceof Error)
error = msg.error;
else if (typeof msg.error == 'string')
error = new Error(msg.error);
else if (typeof msg.message === 'string')
error = new Error(msg.message);
} else {
error = new Error(msg);
}
var BhivError = function BhivError(code) {
switch (typeof code) {
case 'number': case 'string': this.code = code; break ;
case 'object': for (var k in code) this[k] = code[k]; break ;
}
this.getExtra = function () { return code; };
};
BhivError.prototype = error;
return new BhivError(code);
};
Bhiv.toString = function () { return '[Contructor Bhiv]'; };
Bhiv.noop = function () {};
Bhiv.identity = function (data, callback) { return callback(null, data); };
Bhiv.pipe = function (f1, f2) {
return function () { return f2.call(this, f1.apply(this, arguments)); };
};
Bhiv.dotSplit = /\s*\.\s*/;
Bhiv.getIn = function (source, path, alternative) {
if (path === '.') return source;
if (source instanceof Object) {
if (typeof path === 'string') {
var chain = path.split(Bhiv.dotSplit);
for (var i = 0, l = chain.length, node = source; i < l; ++i) {
if (node == null) break ;
node = node[chain[i]];
}
if (i == l && node != null) return node;
} else if (path in source) {
return source[path];
}
}
if (alternative != null) {
Bhiv.setIn(source, path, alternative);
return alternative;
}
return null;
};
Bhiv.setIn = function (target, path, value) {
var chain = ('' + path).split(Bhiv.dotSplit);
while (chain.length > 0) {
if (chain.length == 1) {
target[chain.shift()] = value;
} else if (target.hasOwnProperty(chain[0]) && target[chain[0]] instanceof Object) {
target = target[chain.shift()];
} else {
target = target[chain[0]] = target[chain[0]] ? Object.create(target[chain[0]]) : {};
chain.shift();
}
}
return value;
};
Bhiv.iter = function (collection, iterator) {
switch (Object.prototype.toString.call(collection)) {
case '[object Object]':
for (var i in collection)
if (collection.hasOwnProperty(i))
iterator.call(collection, collection[i], i);
break ;
case '[object Array]':
for (var i = 0, l = collection.length; i < l; ++i)
iterator.call(collection, collection[i], i);
break ;
}
};
Bhiv.copy = function (source, target) {
switch (Object.prototype.toString.call(source)) {
case '[object Object]':
if (Object.prototype.toString.call(target) != '[object Object]')
target = {};
break ;
case '[object Array]':
if (Object.prototype.toString.call(target) != '[object Array]')
target = [];
break ;
}
Bhiv.iter(source, function (child, index) { target[index] = child; });
return target;
};
Bhiv.match = function match(pattern, alpha) {
switch (Object.prototype.toString.call(pattern)) {
case '[object Array]': case '[Object arguments]':
if (pattern.length === 0 && alpha instanceof Array) return true;
if (!(alpha instanceof Array)) return false;
for (var i = 0; i < alpha.length; i++)
if (!match(pattern[0], alpha[i]))
return false;
return true;
case '[object Object]':
if (!(alpha instanceof Object)) return false;
for (var i in pattern) {
if (!alpha) return false;
if (!match(pattern[i], alpha[i])) return false;
}
return true;
case '[object Function]':
switch (pattern) {
case Boolean: return '[object Boolean]' === Object.prototype.toString.call(alpha);
case Number: return '[object Number]' === Object.prototype.toString.call(alpha);
case String: return '[object String]' === Object.prototype.toString.call(alpha);
case Object: return '[object Object]' === Object.prototype.toString.call(alpha);
case Array: return '[object Array]' === Object.prototype.toString.call(alpha);
case Function: return '[object Function]' === Object.prototype.toString.call(alpha);
case Date: return '[object Date]' === Object.prototype.toString.call(alpha);
case RegExp: return '[object RegExp]' === Object.prototype.toString.call(alpha);
default: return !!pattern(alpha);
}
case '[object RegExp]':
return pattern.test(alpha);
default:
return pattern == alpha;
}
};
Bhiv.process = function process(pattern, sources) {
var replacement = null;
var hasMagic = false;
var result = pattern.replace(/(^\$\{[^\}]+\}$)|(\$\{[^\}]+\})/g, function (_, full, partial) {
hasMagic = true;
if (full) {
if (sources[0] == null) return '';
var path = full.substring(2, full.length - 1);
for (var i = 0; replacement == null && i < sources.length; i++)
replacement = Bhiv.getIn(sources[i], path);
return '';
} else {
var path = partial.substring(2, partial.length - 1);
var result = null;
for (var i = 0; result == null && i < sources.length; i++)
result = Bhiv.getIn(sources[i], path);
if (typeof result === 'string') return result;
return JSON.stringify(result);
}
});
if (result !== '') return result;
if (replacement != null) return replacement;
if (hasMagic) return null;
return pattern;
};
Bhiv.extract = function extract(glue, alpha/*, ...*/) {
switch (Object.prototype.toString.call(glue)) {
case '[object Array]':
if (alpha != null) {
if (!(alpha instanceof Array)) alpha = [alpha];
if (glue.length == 1 && glue[0] instanceof Object) {
var result = [];
glue = glue[0];
for (var i = 0; i < alpha.length; i++)
result.push(extract(glue, alpha[i]));
return result;
}
}
var result = new Array(glue.length);
var args = Array.prototype.slice.call(arguments);
for (var i = 0; i < glue.length; i++) {
args[0] = glue[i];
result[i] = extract.apply(this, args);
}
return result;
case '[object Object]':
var result = {};
for (var i in glue) {
var args = Array.prototype.slice.call(arguments);
args[0] = glue[i];
var key = Bhiv.process(i);
var value = extract.apply(this, args);
if (key in result) {
if (result[key] instanceof Array) {
result[key].push(value);
} else {
result[key] = Bhiv.ingest(result[key], value);
}
} else {
result[key] = value;
}
}
return result;
case '[object String]':
return Bhiv.process(glue, Array.prototype.slice.call(arguments, 1));
case '[object Date]':
return new Date(glue.toISOSTring());
case '[object RegExp]':
var gim = (glue.global ? 'g' : '') + (glue.ignoreCase ? 'i' : '') + (glue.multiline ? 'm' : '');
return new RegExp(glue.source, gim);
default:
return glue;
}
};
Bhiv.ingest = function ingest(holder, alpha) {
switch (Object.prototype.toString.call(alpha)) {
case '[object Array]': case '[object Arguments]':
if (holder instanceof Array) {
var result = new Array(alpha.length);
for (var i = 0; i < alpha.length; i++)
result[i] = ingest(holder[i], alpha[i]);
return result;
}
return alpha;
case '[object Object]':
if (alpha.constructor !== Object) return alpha;
if (!(holder instanceof Object)) return alpha;
var result = Object.create(holder);
var hasAssignation = false;
for (var i in alpha) {
if (holder[i] === alpha[i]) continue ;
if (i in holder) result[i] = ingest(holder[i], alpha[i]);
else result[i] = alpha[i];
hasAssignation = true;
}
if (!hasAssignation) return result;
if (holder.constructor !== Object) return result;
for (var i in holder) {
if (!holder.hasOwnProperty(i)) continue ;
if (!result.hasOwnProperty(i)) result[i] = holder[i];
}
return result;
case '[object Undefined]':
return holder;
default:
return alpha;
}
};
Bhiv.merge = function merge(holder, alpha) {
switch (Object.prototype.toString.call(alpha)) {
case '[object Array]': case '[object Arguments]':
var result = holder instanceof Array ? holder.slice() : [];
return alpha;
case '[object Object]':
if (alpha.constructor !== Object) {
return alpha;
} else {
var isObject = holder && holder.constructor === Object;
var result = isObject ? holder : {};
for (var i in alpha)
if (alpha.hasOwnProperty(i)) {
result[i] = (holder && holder[i] != null)
? merge(holder[i], alpha[i])
: alpha[i];
}
return result;
}
case '[object Undefined]':
return holder;
default:
return alpha;
}
};
Bhiv.flatten = function flatten(data) {
switch (Object.prototype.toString.call(data)) {
case '[object Object]':
var result = {};
for (var i in data)
result[i] = flatten(data[i]);
return result;
case '[object Array]':
var result = new Array(data.length);
for (var i = 0; i < data.length; i++)
result[i] = flatten(data[i]);
return result;
default:
return data;
}
};
Bhiv.throttleCache = function (method, cache) {
var throttled = Bhiv.throttle(method);
return function (data, callback) {
if (typeof data !== 'string') throw Bhiv.Error('unable to cache this call');
if (data in cache) return callback(null, cache[data]);
return throttled.call(this, data, function (err, result) {
if (err) return callback(err);
if (!(data in cache)) cache[data] = result;
return callback(null, result);
});
};
};
Bhiv.throttle = function (method) {
var pending = {};
return function (data, callback) {
if (typeof data !== 'string') throw Bhiv.Error('unable to throttle this call');
if (pending[data]) return pending[data].push(callback);
pending[data] = [callback];
return method.call(this, data, function (err, value) {
var listeners = pending[data];
pending[data] = null;
for (var i = 0; i < listeners.length; i++)
listeners[i].apply(this, arguments);
});
};
};
Bhiv.generateId = (function () {
var chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
return function (prefix, separator) {
var id = prefix ? prefix + (separator || ':') : '';
for (var i = 0; i < 4; i++) id += chars[Math.random() * chars.length | 0];
return id;
};
})();
Bhiv.serve = function (object) {
return function (key, callback) {
var value = Bhiv.getIn(object, key);
return callback(null, value);
};
};
Bhiv.wrap = function (alpha) {
return function (_, callback) {
return callback(null, alpha);
};
};
Bhiv.waiter = function () {
var pool = [];
var ready = false;
var waiter = function () {
if (ready) throw new Error('Do not wait anymore');
pool.push({ context: this, args: arguments });
};
waiter.ready = function (method) {
if (ready) return ;
ready = true;
while (pool.length > 0) {
var task = pool.shift();
method.apply(task.context, task.args);
}
};
return waiter;
};
Bhiv.callWhenReady = function (context) {
var ready = false;
var toCall = [];
var args = null;
var caller = function (fn, ctx) {
if (ready) return fn.apply(ctx || context || null, args);
else toCall.push([fn, ctx || context]);
};
caller.ready = function () {
if (ready) throw new Error('Waiter already ready');
ready = true;
args = Array.prototype.slice.call(arguments);
args.unshift(null);