cognition-js
Version:
self-assembling client-side web framework
2,439 lines (1,620 loc) • 108 kB
JavaScript
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define(factory) :
(global.Machine = factory());
}(this, (function () { 'use strict';
// the PathResolver is a namespace that uses a browser hack to generate an
// absolute path from a url string -- using an anchor tag's href.
// it combines the aliasMap with a url and possible root directory.
const PathResolver = {};
const ANCHOR = document.createElement('a');
PathResolver.resolveUrl = function resolveUrl(aliasMap, url, root) {
url = aliasMap ? (aliasMap[url] || url) : url;
if(!url){
console.log('argh',url);
}
if(root && url.indexOf('http') !== 0) {
root = aliasMap ? (aliasMap[root] || root) : root;
const lastChar = root.substr(-1);
url = (lastChar !== '/') ? root + '/' + url : root + url;
}
ANCHOR.href = url;
return ANCHOR.href;
};
PathResolver.resolveRoot = function resolveRoot(aliasMap, url, root){
return toRoot(PathResolver.resolveUrl(aliasMap, url, root));
};
function toRoot(path){
const i = path.lastIndexOf('/');
return path.substring(0, i + 1);
}
// holds a cache of all scripts loaded by url
const ScriptLoader = {};
const status = { loaded: {}, failed: {}, fetched: {}};
const cache = {};
const listenersByUrl = {}; // loaded only, use init timeouts to request again
function cleanup(e){
const target = e.target;
target.onload = target.onerror = null;
}
ScriptLoader.currentScript = null;
ScriptLoader.onError = function onError(e){
const src = e.target.src;
const f = status.failed[src] || 0;
status.failed[src] = f + 1;
status.fetched[src] = false;
cleanup(e);
if(f < 3) {
setTimeout(ScriptLoader.load, f * 1000, src);
}
console.log('script err', e);
};
ScriptLoader.onLoad = function onLoad(e){
const src = e.target.src;
status.loaded[src] = true;
cache[src] = ScriptLoader.currentScript;
if(ScriptLoader.currentScript.__machine) { // to avoid modifying AMD libs
ScriptLoader.currentScript.url = src;
ScriptLoader.currentScript.root = toRoot$1(src);
}
//console.log(cache);
cleanup(e);
const listeners = listenersByUrl[src] || [];
const len = listeners.length;
for(let i = 0; i < len; ++i){
const f = listeners[i];
f.call(null, src);
}
listenersByUrl[src] = [];
};
ScriptLoader.read = function read(path){
return cache[path];
};
ScriptLoader.has = function has(path){
return !!status.loaded[path];
};
ScriptLoader.request = function request(path, callback){
if(status.loaded[path])
return callback.call(null, path);
const listeners = listenersByUrl[path] = listenersByUrl[path] || [];
const i = listeners.indexOf(callback);
if(i === -1){
listeners.push(callback);
}
ScriptLoader.load(path);
};
ScriptLoader.load = function(path){
if(status.fetched[path]) // also true if loaded, this only clears on error
return;
const script = document.createElement("script");
script.onerror = ScriptLoader.onError;
script.onload = ScriptLoader.onLoad;
script.async = true;
script.charset = "utf-8";
script.src = path;
status.fetched[path] = true;
document.head.appendChild(script);
};
function toRoot$1(path){
const i = path.lastIndexOf('/');
return path.substring(0, i + 1);
}
// todo add ability to share this among cogs, add additional paths with new callback
// thus entire trees can mount at once
function ScriptMonitor(paths, callback){
this.callback = callback;
this.needs = notReady(paths);
this.needs.length === 0 ? this.callback() : requestNeeds(this);
}
function requestNeeds(monitor){
const callback = onNeedReady.bind(monitor);
const paths = monitor.needs;
const len = paths.length;
for (let i = 0; i < len; i++) {
const path = paths[i];
ScriptLoader.request(path, callback);
}
}
function onNeedReady(path){
const needs = this.needs;
const i = needs.indexOf(path);
needs.splice(i, 1);
if(!needs.length)
this.callback();
}
function notReady(arr){
const remaining = [];
for(let i = 0; i < arr.length; i++){
const path = arr[i];
if(!ScriptLoader.has(path))
remaining.push(path);
}
return remaining;
}
// whenever new aliases or valves (limiting access to aliases) are encountered,
// a new aliasContext is created and used to resolve urls and directories.
// it inherits the aliases from above and then extends or limits those.
//
// the resolveUrl and resolveRoot methods determine a path from a url and/or
// directory combination (either can be an alias). if no directory is
// given -- and the url or alias is not an absolute path -- then a relative path
// is generated from the current url (returning a new absolute path).
//
// (all method calls are cached here for performance reasons)
function AliasContext(sourceRoot, aliasMap, valveMap){
this.sourceRoot = sourceRoot;
this.aliasMap = aliasMap ? restrict(copy(aliasMap), valveMap) : {};
this.urlCache = {}; // 2 level cache (first root, then url)
this.rootCache = {}; // 2 level cache (first root, then url)
this.shared = false; // shared once used by another lower cog
}
AliasContext.prototype.clone = function(newRoot){
return new AliasContext(newRoot || this.sourceRoot, this.aliasMap);
};
AliasContext.prototype.restrictAliasList = function(valveMap){
this.aliasMap = restrict(this.aliasMap, valveMap);
return this;
};
AliasContext.prototype.injectAlias = function(alias){
this.aliasMap[alias.name] = this.resolveUrl(alias.url, alias.root);
return this;
};
AliasContext.prototype.injectAliasList = function(aliasList){
for(let i = 0; i < aliasList.length; i++){
this.injectAlias(aliasList[i]);
}
return this;
};
AliasContext.prototype.injectAliasHash = function(aliasHash){
const list = [];
const hash = {};
for(const name in aliasHash){
let url = '', root = '';
const val = aliasHash[name];
const parts = val.trim().split(' ');
if(parts.length === 1){
url = parts[0];
} else {
root = parts[0];
url = parts[1];
}
const alias = {name: name, url: url, root: root, dependent: false, placed: false};
hash[name] = alias;
list.push(alias);
}
for(let i = 0; i < list.length; i++){
const alias = list[i];
if(alias.root && hash.hasOwnProperty(alias.root)){
alias.dependent = true; // locally dependent on other aliases in this list
}
}
const addedList = [];
while(addedList.length < list.length) {
let justAdded = 0;
for (let i = 0; i < list.length; i++) {
const alias = list[i];
if(!alias.placed) {
if (!alias.dependent || hash[alias.root].placed) {
justAdded++;
alias.placed = true;
addedList.push(alias);
}
}
}
if(justAdded === 0){
throw new Error('Cyclic Alias Dependency!');
}
}
for(let i = 0; i < addedList.length; i++){
this.injectAlias(addedList[i]);
}
return this;
};
// given a list of objects with url and root, get urls not yet downloaded
AliasContext.prototype.freshUrls = function freshUrls(list) {
const result = [];
if(!list)
return result;
for(let i = 0; i < list.length; i++){
const url = this.itemToUrl(list[i]);
if(!ScriptLoader.has(url) && result.indexOf(url) === -1)
result.push(url);
}
return result;
};
AliasContext.prototype.itemToUrl = function applyUrl(item) {
return this.resolveUrl(item.url, item.root);
};
AliasContext.prototype.resolveUrl = function resolveUrl(url, root){
const parts = url.trim().split(' ');
if(parts.length === 1){
url = parts[0];
} else {
root = parts[0];
url = parts[1];
}
const cache = this.urlCache;
root = root || this.sourceRoot || '';
const baseCache = cache[root] = cache[root] || {};
return baseCache[url] = baseCache[url] ||
PathResolver.resolveUrl(this.aliasMap, url, root);
};
AliasContext.prototype.resolveRoot = function resolveRoot(url, base){
const cache = this.rootCache;
base = base || this.sourceRoot || '';
const baseCache = cache[base] = cache[base] || {};
return baseCache[url] = baseCache[url] ||
PathResolver.resolveRoot(this.aliasMap, url, base);
};
AliasContext.applySplitUrl = function applySplitUrl(def){
let url = def.url || '';
const parts = url.trim().split(' ');
if(parts.length === 1){
def.url = parts[0];
def.root = '';
} else {
def.root = parts[0];
def.url = parts[1];
}
};
// limits the source hash to only have keys found in the valves hash (if present)
function restrict(source, valves){
if(!valves)
return source;
const result = {};
for(const k in valves){
result[k] = source[k];
}
return result;
}
// creates a shallow copy of the source hash
function copy(source, target){
target = target || {};
for(const k in source){
target[k] = source[k];
}
return target;
}
function isPrivate(name){
return name.slice(0,1) === '_';
}
function isAction(name){
return name.slice(-1) === '$';
}
class Data {
// should only be created via Scope methods
constructor(scope, name) {
this._scope = scope;
this._action = isAction(name);
this._name = name;
this._dead = false;
this._value = undefined;
this._present = false; // true if a value has been received
this._private = isPrivate(name);
this._readable = !this._action;
this._writable = true; // false when mirrored or calculated?
this._subscribers = [];
};
get scope() { return this._scope; };
get name() { return this._name; };
get dead() { return this._dead; };
get present() { return this._present; };
get private() { return this._private; };
destroy(){
this._scope = null;
this._subscribers = null;
this._dead = true;
};
subscribe(listener, pull){
this._subscribers.unshift(listener);
if(pull && this._present)
listener.call(null, this._value, this._name);
return this;
};
unsubscribe(listener){
let i = this._subscribers.indexOf(listener);
if(i !== -1)
this._subscribers.splice(i, 1);
return this;
};
silentWrite(msg){
this.write(msg, true);
};
read(){
return this._value;
};
write(msg, silent){
_ASSERT_WRITE_ACCESS(this);
if(!this._action) { // states store the last value seen
this._present = true;
this._value = msg;
}
if(!silent) {
let i = this._subscribers.length;
while (i--) {
this._subscribers[i].call(null, msg, this._name);
}
}
};
refresh(){
if(this._present)
this.write(this._value);
return this;
};
toggle(){
this.write(!this._value);
return this;
};
}
function _ASSERT_WRITE_ACCESS(d){
if(!d._writable)
throw new Error('States accessed from below are read-only. Named: ' + d._name);
}
function NoopSource() {
this.name = '';
}
NoopSource.prototype.init = function init() {};
NoopSource.prototype.pull = function pull() {};
NoopSource.prototype.destroy = function destroy() {};
const stubs = {init:'init', pull:'pull', destroy:'destroy'};
NoopSource.prototype.addStubs = function addStubs(sourceClass) {
for(const name in stubs){
const ref = stubs[name];
const f = NoopSource.prototype[ref];
if(typeof sourceClass.prototype[name] !== 'function'){
sourceClass.prototype[name] = f;
}
}
};
const NOOP_SOURCE = new NoopSource();
function NoopStream() {
this.name = '';
}
NoopStream.prototype.handle = function handle(msg, source) {};
NoopStream.prototype.reset = function reset() {};
NoopStream.prototype.emit = function emit() {};
NoopStream.prototype.resetDefault = function reset() {
this.next.reset();
};
const stubs$1 = {handle:'handle', reset:'resetDefault', emit:'emit'};
NoopStream.prototype.addStubs = function addStubs(streamClass) {
for(const name in stubs$1){
const ref = stubs$1[name];
const f = NoopStream.prototype[ref];
if(typeof streamClass.prototype[name] !== 'function'){
streamClass.prototype[name] = f;
}
}
};
const NOOP_STREAM = new NoopStream();
function PassStream(name) {
this.name = name || '';
this.next = NOOP_STREAM;
}
PassStream.prototype.handle = function passHandle(msg, source) {
const n = this.name || source;
this.next.handle(msg, n);
};
NOOP_STREAM.addStubs(PassStream);
function SubscribeSource(name, data, canPull){
this.name = name;
this.data = data;
this.canPull = canPull;
const stream = this.stream = new PassStream(name);
this.callback = function(msg, source){ stream.handle(msg, source); };
data.subscribe(this.callback);
}
SubscribeSource.prototype.pull = function pull(){
!this.dead && this.canPull && this.emit();
};
SubscribeSource.prototype.emit = function emit(){
const data = this.data;
if(data.present) {
const stream = this.stream;
const msg = data.read();
const source = this.name;
stream.handle(msg, source);
}
};
SubscribeSource.prototype.destroy = function destroy(){
const callback = this.callback;
this.data.unsubscribe(callback);
this.dead = true;
};
NOOP_SOURCE.addStubs(SubscribeSource);
function EventSource(name, target, eventName, useCapture){
function toStream(msg){
stream.handle(msg, eventName, null);
}
this.name = name;
this.target = target;
this.eventName = eventName;
this.useCapture = !!useCapture;
this.on = target.addEventListener || target.addListener || target.on;
this.off = target.removeEventListener || target.removeListener || target.off;
this.stream = new PassStream(name);
this.callback = toStream;
const stream = this.stream;
this.on.call(target, eventName, toStream, useCapture);
}
EventSource.prototype.destroy = function destroy(){
this.off.call(this.target, this.eventName, this.callback, this.useCapture);
this.dead = true;
};
NOOP_SOURCE.addStubs(EventSource);
function ForkStream(name, fork) {
this.name = name;
this.next = NOOP_STREAM;
this.fork = fork;
}
ForkStream.prototype.handle = function handle(msg, source) {
const n = this.name;
this.next.handle(msg, n);
this.fork.handle(msg, n);
};
ForkStream.prototype.reset = function reset(msg){
this.next.reset(msg);
this.fork.reset(msg);
};
NOOP_STREAM.addStubs(ForkStream);
function BatchStream(name) {
this.name = name;
this.next = NOOP_STREAM;
this.msg = undefined;
this.latched = false;
}
BatchStream.prototype.handle = function handle(msg, source) {
this.msg = msg;
if(!this.latched){
this.latched = true;
Catbus.enqueue(this);
}
};
BatchStream.prototype.emit = function emit() { // called from enqueue scheduler
const msg = this.msg;
const source = this.name;
this.latched = false; // can queue again
this.next.handle(msg, source);
};
BatchStream.prototype.reset = function reset() {
this.latched = false;
this.msg = undefined;
// doesn't continue on as in default
};
NOOP_STREAM.addStubs(BatchStream);
function ResetStream(name, head) {
this.head = head; // stream at the head of the reset process
this.name = name;
this.next = NOOP_STREAM;
}
ResetStream.prototype.handle = function handle(msg, source) {
this.next.handle(msg, source);
this.head.reset(msg, source);
};
ResetStream.prototype.reset = function(){
// catch reset from head, does not continue
};
function IDENTITY$1(d) { return d; }
function TapStream(name, f) {
this.name = name;
this.f = f || IDENTITY$1;
this.next = NOOP_STREAM;
}
TapStream.prototype.handle = function handle(msg, source) {
const n = this.name || source;
const f = this.f;
f(msg, n);
this.next.handle(msg, n);
};
NOOP_STREAM.addStubs(TapStream);
function IDENTITY$2(msg, source) { return msg; }
function MsgStream(name, f, context) {
this.name = name;
this.f = f || IDENTITY$2;
this.context = context;
this.next = NOOP_STREAM;
}
MsgStream.prototype.handle = function msgHandle(msg, source) {
const f = this.f;
this.next.handle(f.call(this.context, msg, source), source);
};
NOOP_STREAM.addStubs(MsgStream);
function IDENTITY$3(d) { return d; }
function FilterStream(name, f, context) {
this.name = name;
this.f = f || IDENTITY$3;
this.context = context || null;
this.next = NOOP_STREAM;
}
FilterStream.prototype.handle = function filterHandle(msg, source) {
const f = this.f;
f.call(this.context, msg, source) && this.next.handle(msg, source);
};
NOOP_STREAM.addStubs(FilterStream);
function IS_PRIMITIVE_EQUAL(a, b) {
return a === b && typeof a !== 'object' && typeof a !== 'function';
}
function SkipStream(name) {
this.name = name;
this.msg = undefined;
this.hasValue = true;
this.next = NOOP_STREAM;
}
SkipStream.prototype.handle = function handle(msg, source) {
if(!this.hasValue) {
this.hasValue = true;
this.msg = msg;
this.next.handle(msg, source);
} else if (!IS_PRIMITIVE_EQUAL(this.msg, msg)) {
this.msg = msg;
this.next.handle(msg, source);
}
};
NOOP_STREAM.addStubs(SkipStream);
function LastNStream(name, count) {
this.name = name;
this.count = count || 1;
this.next = NOOP_STREAM;
this.msg = [];
}
LastNStream.prototype.handle = function handle(msg, source) {
const c = this.count;
const m = this.msg;
const n = this.name || source;
m.push(msg);
if(m.length > c)
m.shift();
this.next.handle(m, n);
};
LastNStream.prototype.reset = function(msg, source){
this.msg = [];
this.next.reset();
};
NOOP_STREAM.addStubs(LastNStream);
function FirstNStream(name, count) {
this.name = name;
this.count = count || 1;
this.next = NOOP_STREAM;
this.msg = [];
}
FirstNStream.prototype.handle = function handle(msg, source) {
const c = this.count;
const m = this.msg;
const n = this.name || source;
if(m.length < c)
m.push(msg);
this.next.handle(m, n);
};
FirstNStream.prototype.reset = function(msg, source){
this.msg = [];
};
NOOP_STREAM.addStubs(FirstNStream);
function AllStream(name) {
this.name = name;
this.next = NOOP_STREAM;
this.msg = [];
}
AllStream.prototype.handle = function handle(msg, source) {
const m = this.msg;
const n = this.name || source;
m.push(msg);
this.next.handle(m, n);
};
AllStream.prototype.reset = function(msg, source){
this.msg = [];
};
NOOP_STREAM.addStubs(AllStream);
const FUNCTOR$1 = function(d) {
return typeof d === 'function' ? d : function() { return d;};
};
function IMMEDIATE(msg, source) { return 0; }
function callback(stream, msg, source){
const n = stream.name || source;
stream.next.handle(msg, n);
}
function DelayStream(name, f) {
this.name = name;
this.f = arguments.length ? FUNCTOR$1(f) : IMMEDIATE;
this.next = NOOP_STREAM;
}
DelayStream.prototype.handle = function handle(msg, source) {
const delay = this.f(msg, source);
setTimeout(callback, delay, this, msg, source);
};
NOOP_STREAM.addStubs(DelayStream);
function BY_SOURCE(msg, source) { return source; }
const FUNCTOR$2 = function(d) {
return typeof d === 'function' ? d : function() { return d;};
};
function GroupStream(name, f, seed) {
this.name = name;
this.f = f || BY_SOURCE;
this.seed = arguments.length === 3 ? FUNCTOR$2(seed) : FUNCTOR$2({});
this.next = NOOP_STREAM;
this.msg = this.seed();
}
GroupStream.prototype.handle = function handle(msg, source) {
const f = this.f;
const v = f(msg, source);
const n = this.name || source;
const m = this.msg;
if(v){
m[v] = msg;
} else {
for(const k in msg){
m[k] = msg[k];
}
}
this.next.handle(m, n);
};
GroupStream.prototype.reset = function reset(msg) {
const m = this.msg = this.seed(msg);
this.next.reset(m);
};
NOOP_STREAM.addStubs(GroupStream);
function TRUE$1() { return true; }
function LatchStream(name, f) {
this.name = name;
this.f = f || TRUE$1;
this.next = NOOP_STREAM;
this.latched = false;
}
LatchStream.prototype.handle = function handle(msg, source) {
const n = this.name;
if(this.latched){
this.next.handle(msg, n);
return;
}
const f = this.f;
const v = f(msg, source);
if(v) {
this.latched = true;
this.next.handle(msg, n);
}
};
LatchStream.prototype.reset = function(seed){
this.latched = false;
this.next.reset(seed);
};
NOOP_STREAM.addStubs(LatchStream);
function ScanStream(name, f) {
this.name = name;
this.f = f;
this.hasValue = false;
this.next = NOOP_STREAM;
this.value = undefined;
}
ScanStream.prototype.handle = function handle(msg, source) {
const f = this.f;
this.value = this.hasValue ? f(this.value, msg, source) : msg;
this.next.handle(this.value, source);
};
ScanStream.prototype.reset = function reset() {
this.hasValue = false;
this.value = undefined;
this.next.reset();
};
NOOP_STREAM.addStubs(ScanStream);
const FUNCTOR$3 = function(d) {
return typeof d === 'function' ? d : function() { return d;};
};
function ScanWithSeedStream(name, f, seed) {
this.name = name;
this.f = f;
this.seed = FUNCTOR$3(seed);
this.next = NOOP_STREAM;
this.value = this.seed();
}
ScanWithSeedStream.prototype.handle = function scanWithSeedHandle(msg, source) {
const f = this.f;
this.value = f(this.value, msg, source);
this.next.handle(this.value, source);
};
ScanWithSeedStream.prototype.reset = function reset(msg) {
this.value = this.seed(msg);
this.next.reset(this.value);
};
NOOP_STREAM.addStubs(ScanWithSeedStream);
// const scanStreamBuilder = function(f, seed) {
// const hasSeed = arguments.length === 2;
// return function(name) {
// return hasSeed? new ScanWithSeedStream(name, f, seed) : new ScanStream(name, f);
// }
// };
function SplitStream(name) {
this.name = name;
this.next = NOOP_STREAM;
}
SplitStream.prototype.handle = function splitHandle(msg, source) {
if(Array.isArray(msg)){
this.withArray(msg, source);
} else {
this.withIteration(msg, source);
}
};
SplitStream.prototype.withArray = function(msg, source){
const len = msg.length;
for(let i = 0; i < len; ++i){
this.next.handle(msg[i], source);
}
};
SplitStream.prototype.withIteration = function(msg, source){
const next = this.next;
for(const m of msg){
next.handle(m, source);
}
};
NOOP_STREAM.addStubs(SplitStream);
function WriteStream(name, data) {
this.name = name;
this.data = data;
this.next = NOOP_STREAM;
}
WriteStream.prototype.handle = function handle(msg, source) {
this.data.write(msg);
this.next.handle(msg, source);
};
NOOP_STREAM.addStubs(WriteStream);
function IDENTITY$4(d) { return d; }
function FilterMapStream(name, f, m, context) {
this.name = name || '';
this.f = f || IDENTITY$4;
this.m = m || IDENTITY$4;
this.context = context || null;
this.next = NOOP_STREAM;
}
FilterMapStream.prototype.handle = function filterHandle(msg, source) {
const f = this.f;
const m = this.m;
f.call(this.context, msg, source) && this.next.handle(
m.call(this.context, msg, source));
};
NOOP_STREAM.addStubs(FilterMapStream);
function PriorStream(name) {
this.name = name;
this.values = [];
this.next = NOOP_STREAM;
}
PriorStream.prototype.handle = function handle(msg, source) {
const arr = this.values;
arr.push(msg);
if(arr.length === 1)
return;
if(arr.length > 2)
arr.shift();
this.next.handle(arr[0], source);
};
PriorStream.prototype.reset = function(msg, source){
this.msg = [];
this.next.reset();
};
NOOP_STREAM.addStubs(PriorStream);
function FUNCTOR$4(d) {
return typeof d === 'function' ? d : function() { return d; };
}
function ReduceStream(name, f, seed) {
this.name = name;
this.seed = FUNCTOR$4(seed);
this.v = this.seed() || 0;
this.f = f;
this.next = NOOP_STREAM;
}
ReduceStream.prototype.reset = function(){
this.v = this.seed() || 0;
this.next.reset();
};
ReduceStream.prototype.handle = function(msg, source){
const f = this.f;
this.next.handle(this.v = f(msg, this.v), source);
};
NOOP_STREAM.addStubs(ReduceStream);
function SkipNStream(name, count) {
this.name = name;
this.count = count || 0;
this.next = NOOP_STREAM;
this.seen = 0;
}
SkipNStream.prototype.handle = function handle(msg, source) {
const c = this.count;
const s = this.seen;
if(this.seen < c){
this.seen = s + 1;
} else {
this.next.handle(msg, source);
}
};
SkipNStream.prototype.reset = function(){
this.seen = 0;
};
NOOP_STREAM.addStubs(SkipNStream);
function TakeNStream(name, count) {
this.name = name;
this.count = count || 0;
this.next = NOOP_STREAM;
this.seen = 0;
}
TakeNStream.prototype.handle = function handle(msg, source) {
const c = this.count;
const s = this.seen;
if(this.seen < c){
this.seen = s + 1;
this.next.handle(msg, source);
}
};
TakeNStream.prototype.reset = function(){
this.seen = 0;
};
NOOP_STREAM.addStubs(TakeNStream);
function Spork(bus) {
this.bus = bus;
this.streams = [];
this.first = NOOP_STREAM;
this.last = NOOP_STREAM;
this.initialized = false;
}
Spork.prototype.handle = function(msg, source) {
this.first.reset();
this._split(msg, source);
this.last.handle(this.last.v, source);
};
Spork.prototype.withArray = function withArray(msg, source){
const len = msg.length;
for(let i = 0; i < len; ++i){
this.first.handle(msg[i], source);
}
};
Spork.prototype.withIteration = function withIteration(msg, source){
const first = this.first;
for(const i of msg){
first.handle(i, source);
}
};
Spork.prototype._split = function(msg, source){
if(Array.isArray(msg)){
this.withArray(msg, source);
} else {
this.withIteration(msg, source);
}
};
Spork.prototype._extend = function(stream) {
if(!this.initialized){
this.initialized = true;
this.first = stream;
this.last = stream;
} else {
this.streams.push(stream);
this.last.next = stream;
this.last = stream;
}
};
Spork.prototype.msg = function msg(f) {
this._extend(new MsgStream('', f));
return this;
};
Spork.prototype.skipDupes = function skipDupes() {
this._extend(new SkipStream(''));
return this;
};
Spork.prototype.skip = function skip(n) {
this._extend(new SkipNStream('', n));
return this;
};
Spork.prototype.take = function take(n) {
this._extend(new TakeNStream('', n));
return this;
};
Spork.prototype.reduce = function reduce(f, seed) {
this._extend(new ReduceStream('', f, seed));
this.bus._spork = null;
return this.bus;
};
Spork.prototype.filter = function filter(f) {
this._extend(new FilterStream('', f));
return this;
};
Spork.prototype.filterMap = function filterMap(f, m) {
this._extend(new FilterMapStream('', f, m));
return this;
};
class Frame {
constructor(bus) {
this.bus = bus;
this.index = bus._frames.length;
this.streams = [];
};
}
const MeowParser = {};
const phraseCmds = {
'&': {name: 'AND_READ', react: false, process: true, output: false, can_maybe: true, can_alias: true, can_prop: true},
'>': {name: 'WRITE', react: false, process: true, output: true, can_maybe: true, can_alias: true, can_prop: true},
'|': {name: 'THEN_READ', react: false, process: true, output: false, can_maybe: true, can_alias: true, can_prop: true},
'@': {name: 'EVENT', react: true, process: false, output: false, can_maybe: true, can_alias: true, can_prop: true},
'}': {name: 'WATCH_TOGETHER', react: true, process: false, output: false, can_maybe: true, can_alias: true, can_prop: true},
'#': {name: 'HOOK', react: false, process: true, output: true, can_maybe: false, can_alias: false, can_prop: false},
'*': {name: 'METHOD', react: false, process: true, output: true, can_maybe: false, can_alias: false, can_prop: false},
'%': {name: 'FILTER', react: false, process: true, output: false, can_maybe: false, can_alias: false, can_prop: false},
'{': {name: 'WATCH_EACH', react: true, process: false, output: false, can_maybe: true, can_alias: true, can_prop: true},
'~': {name: 'WATCH_SOME', react: true, process: false, output: false, can_maybe: true, can_alias: true, can_prop: true}
};
const wordModifiers = {
':': 'ALIAS',
'?': 'MAYBE',
'.': 'PROP'
};
function Phrase(cmd, content){
this.content = content;
this.cmd = cmd;
this.words = [];
if(cmd.name === 'HOOK')
parseHook(this);
else
parseWords(this);
}
function Word(content){
this.content = content;
this.name = '';
this.alias = '';
this.maybe = false;
this.args = [];
parseSyllables(this);
}
function parseHook(phrase){
const chunks = splitHookDelimiters(phrase.content);
while(chunks.length) {
const content = chunks.shift();
phrase.words.push(content);
}
}
function parseWords(phrase){
const chunks = splitWordDelimiters(phrase.content);
while(chunks.length) {
const content = chunks.shift();
const word = new Word(content);
phrase.words.push(word);
}
}
function parseSyllables(word){
const chunks = splitSyllableDelimiters(word.content);
let arg = null;
if(chunks[0] === '.'){ // default as props, todo clean this while parse thing up :)
arg = {name: 'props', maybe: false};
}
while(chunks.length) {
const syllable = chunks.shift();
const modifier = wordModifiers[syllable];
if(!modifier && !arg){
arg = {name: syllable, maybe: false};
} else if(modifier === 'ALIAS' && chunks.length) {
word.alias = chunks.shift();
break;
} else if(modifier === 'PROP' && chunks.length){
if(arg)
word.args.push(arg);
arg = null;
} else if(modifier === 'MAYBE' && arg){
arg.maybe = true;
word.args.push(arg);
arg = null;
}
}
if(arg)
word.args.push(arg);
// word name is first arg collected
if(word.args.length){
const firstArg = word.args.shift();
word.name = firstArg.name;
word.maybe = firstArg.maybe;
}
// default to last extracted property as alias if not specified
if(word.args.length && !word.alias){
const lastArg = word.args[word.args.length - 1];
word.alias = lastArg.name;
}
word.alias = word.alias || word.name;
}
function parse(text){
const phrases = [];
const chunks = splitPhraseDelimiters(text);
while(chunks.length){
let chunk = chunks.shift();
let cmd = phraseCmds[chunk];
let content;
if(!cmd && !phrases.length) { // default first cmd is WATCH_TOGETHER
cmd = phraseCmds['}'];
content = !phraseCmds[chunk] && chunk;
} else if(cmd && chunks.length) {
content = chunks.shift();
content = !phraseCmds[content] && content;
} else {
// error, null content
}
const phrase = new Phrase(cmd, content);
phrases.push(phrase);
}
return phrases;
}
function filterEmptyStrings(arr){
let result = [];
for(let i = 0; i < arr.length; i++){
const c = arr[i].trim();
if(c)
result.push(c);
}
return result;
}
function splitPhraseDelimiters(text){
let chunks = text.split(/([&>|@~*%#{}])/);
return filterEmptyStrings(chunks);
}
function splitWordDelimiters(text){
let chunks = text.split(',');
return filterEmptyStrings(chunks);
}
function splitHookDelimiters(text){
let chunks = text.split(' ');
return filterEmptyStrings(chunks);
}
function splitSyllableDelimiters(text){
let chunks = text.split(/([:?.])/);
return filterEmptyStrings(chunks);
}
MeowParser.parse = parse;
function runMeow(bus, meow){
const phrases = Array.isArray(meow) ? meow : MeowParser.parse(meow);
for(let i = 0; i < phrases.length; i++){
const phrase = phrases[i];
runPhrase(bus, phrase);
}
}
function runPhrase(bus, phrase){
const name = phrase.cmd.name;
const scope = bus.scope;
const words = phrase.words;
const context = bus.context();
const target = bus.target();
const multiple = words.length > 1;
if(name === 'HOOK'){
const hook = words.shift();
Catbus.runHook(bus, hook, words);
}
else if(name === 'THEN_READ'){
if(multiple){
bus.msg(getThenReadMultiple(scope, words));
} else {
const word = words[0];
bus.msg(getThenReadOne(scope, word).read);
bus.source(word.alias);
}
} else if(name === 'AND_READ'){
bus.msg(getThenReadMultiple(scope, words, true));
} else if(name === 'METHOD'){
for(let i = 0; i < words.length; i++) {
const word = words[i];
const method = context[word.name];
bus.msg(method, context);
}
} else if(name === 'FILTER'){
for(let i = 0; i < words.length; i++) {
const word = words[i];
const method = context[word.name];
bus.filter(method, context);
}
} else if(name === 'EVENT'){
watchEvents(bus, words);
} else if(name === 'WATCH_EACH'){
watchWords(bus, words);
} else if(name === 'WATCH_SOME'){
watchWords(bus, words);
if(multiple)
bus.merge().group();
bus.batch();
} else if(name === 'WATCH_TOGETHER'){
watchWords(bus, words);
if(multiple)
bus.merge().group();
bus.batch();
if(multiple)
bus.hasKeys(toAliasList(words));
} else if(name === 'WRITE'){
if(multiple) {
// todo transaction, no actions
} else {
const word = words[0];
const data = scope.find(word.name, true);
bus.write(data);
}
}
}
// todo throw errors
function extractProperties(word, value){
let maybe = word.maybe;
let args = word.args;
for(let i = 0; i < args.length; i++){
const arg = args[i];
if(!value && maybe)
return value;
value = value[arg.name];
maybe = arg.maybe;
}
return value;
}
function toAliasList(words){
const list = [];
for(let i = 0; i < words.length; i++) {
const word = words[i];
list.push(word.alias);
}
return list;
}
function watchWords(bus, words){
const scope = bus.scope;
for(let i = 0; i < words.length; i++) {
const word = words[i];
const watcher = createWatcher(scope, word);
bus.add(watcher);
}
}
function createWatcher(scope, word){
const watcher = scope.bus();
const data = scope.find(word.name, true);
watcher.addSubscribe(word.alias, data);
if(word.args.length) {
watcher.msg(function (msg) {
return extractProperties(word, msg);
});
}
if(!isAction(word.name)){
watcher.skipDupes();
}
return watcher;
}
function watchEvents(bus, words){
const scope = bus.scope;
const target = bus.target();
for(let i = 0; i < words.length; i++) { // todo add capture to words
const word = words[i];
const eventBus = createEventBus(scope, target, word);
bus.add(eventBus);
}
}
function createEventBus(scope, target, word){
const eventBus = scope.bus();
eventBus.addEvent(word.alias, target, word.name);
if(word.args.length) {
eventBus.msg(function (msg) {
return extractProperties(word, msg);
});
}
return eventBus;
}
// function getWriteTransaction(scope, words){
//
// const writeSourceNames = [];
// const writeTargetNames = [];
// const targetsByName = {};
//
// for(let i = 0; i < words.length; i++){
// const word = words[i];
// const sourceName = word.name;
// const targetName = word.alias;
// const target = scope.find()
// }
//
// return function writeTransaction(msg, source){
//
// for(let i = 0; i < words.length; i++){
//
// }
//
// };
//
//
//
// return reader;
//
// }
function getThenReadOne(scope, word){
const state = scope.find(word.name, true);
const reader = {};
reader.read = function read(){
const value = state.read();
return extractProperties(word, value);
};
reader.present = function present(){
return state.present;
};
return reader;
}
function getThenReadMultiple(scope, words, usingAnd){
const readers = [];
for(let i = 0; i < words.length; i++){
const word = words[i];
readers.push(getThenReadOne(scope, word));
}
return function thenReadMultiple(msg, source){
const result = {};
if(usingAnd) {
if (source) {
result[source] = msg;
} else {
for (const p in msg) {
result[p] = msg[p];
}
}
}
for(let i = 0; i < words.length; i++){
const word = words[i];
const prop = word.alias;
const reader = readers[i];
if(reader.present())
result[prop] = reader.read();
}
return result;
}
}
const MeowRunner = {
runMeow: runMeow,
};
const FUNCTOR = function(d) {
return typeof d === 'function' ? d : function() { return d;};
};
const batchStreamBuilder = function() {
return function(name) {
return new BatchStream(name);
}
};
const resetStreamBuilder = function(head) {
return function(name) {
return new ResetStream(name, head);
}
};
const tapStreamBuilder = function(f) {
return function(name) {
return new TapStream(name, f);
}
};
const msgStreamBuilder = function(f, context) {
return function(name) {
return new MsgStream(name, f, context);
}
};
const filterStreamBuilder = function(f, context) {
return function(name) {
return new FilterStream(name, f, context);
}
};
const skipStreamBuilder = function(f) {
return function(name) {
return new SkipStream(name, f);
}
};
const lastNStreamBuilder = function(count) {
return function(name) {
return new LastNStream(name, count);
}
};
const priorStreamBuilder = function() {
return function(name) {
return new PriorStream(name);
}
};
const firstNStreamBuilder = function(count) {
return function(name) {
return new FirstNStream(name, count);
}
};
const allStreamBuilder = function() {
return function(name) {
return new AllStream(name);
}
};
const delayStreamBuilder = function(delay) {
return function(name) {
return new DelayStream(name, delay);
}
};
const groupStreamBuilder = function(by) {
return function(name) {
return new GroupStream(name, by);
}
};
const nameStreamBuilder = function(name) {
return function() {
return new PassStream(name);
}
};
const latchStreamBuilder = function(f) {
return function(name) {
return new LatchStream(name, f);
}
};
const scanStreamBuilder = function(f, seed) {
const hasSeed = arguments.length === 2;
return function(name) {
return hasSeed ?
new ScanWithSeedStream(name, f, seed) : new ScanStream(name, f);
}
};
const splitStreamBuilder = function() {
return function(name) {
return new SplitStream(name);
}
};
const writeStreamBuilder = function(data) {
return function(name) {
return new WriteStream(name, data);
}
};
const filterMapStreamBuilder = function(f, m, context) {
return function(name) {
return new FilterMapStream(name, f, m, context);
}
};
function getHasKeys(keys){
const len = keys.length;
return function _hasKeys(msg, source){
if(typeof msg !== 'object')
return false;
for(let i = 0; i < len; i++){
const k = keys[i];
if(!msg.hasOwnProperty(k))
return false;
}
return true;
}
}
class Bus {
// todo buses can't be added to each other cyclically
// each bus gets one source, then children pull
constructor(scope) {
this._frames = [];
this._sources = [];
this._dead = false;
this._scope = scope;
this._children = []; // from forks
this._parent = null;
this._context = null; // for methods
this._target = null; // e.g. dom node for events, styles, etc.
// temporary api states (used for interactively building the bus)
this._spork = null; // beginning frame of split sub process
this._holding = false; // multiple commands until duration function
this._head = null; // point to reset accumulators
this._locked = false; // prevents additional sources from being added
if(scope)
scope._buses.push(this);
const f = new Frame(this);
this._frames.push(f);
this._currentFrame = f;
};
get children(){
return this._children.map((d) => d);
};
get parent() { return this._parent; };
set parent(newParent){
const oldParent = this.parent;
if(oldParent === newParent)
return;
if(oldParent) {
const i = oldParent._children.indexOf(this);
oldParent._children.splice(i, 1);
}
this._parent = newParent;
if(newParent) {
newParent._children.push(this);
}
return this;
};
get dead() {
return this._dead;
};
get holding() {
return this._holding;
};
get scope() {
return this._scope;
}
_createMergingFrame() {
const f1 = this._currentFrame;
const f2 = this._currentFrame = new Frame(this);
this._frames.push(f2);
const source_streams = f1.streams;
const target_streams = f2.streams;
const merged_stream = new PassStream();
target_streams.push(merged_stream);
const len = source_streams.length;
for(let i = 0; i < len; i++){
const s1 = source_streams[i];
s1.next = merged_stream;
}
return f2;
};
_createNormalFrame(streamBuilder) {
const f1 = this._currentFrame;
const f2 = this._currentFrame = new Frame(this);
this._frames.push(f2);
const source_streams = f1.streams;
const target_streams = f2.streams;
const len = source_streams.length;
for(let i = 0; i < len; i++){
const s1 = source_streams[i];
const s2 = streamBuilder ? streamBuilder(s1.name) : new PassStream(s1.name);
s1.next = s2;
target_streams.push(s2);
}
return f2;
};
_createForkingFrame(forkedTargetFrame) {
const f1 = this._currentFrame;
const f2 = this._currentFrame = new Frame(this);
this._frames.push(f2);
const source_streams = f1.streams;
const target_streams = f2.streams;
const forked_streams = forkedTargetFrame.streams;
const len = source_streams.length;
for(let i = 0; i < len; i++){
const s1 = source_streams[i];
const s3 = new PassStream(s1.name);
const s2 = new ForkStream(s1.name, s3);
s1.next = s2;
target_streams.push(s2);
forked_streams.push(s3);
}
return f2;
};
_ASSERT_IS_FUNCTION(f) {
if(typeof f !== 'function')
throw new Error('Argument must be a function.');
};
_ASSERT_NOT_HOLDING() {
if (this.holding)
throw new Error('Method cannot be invoked while holding messages in the frame.');
};
_ASSERT_IS_HOLDING(){
if(!this.holding)
throw new Error('Method cannot be invoked unless holding messages in the frame.');
};
_ASSERT_HAS_HEAD(){
if(!this._head)
throw new Error('Cannot reset without an upstream accumulator.');
};
_ASSERT_NOT_LOCKED(){
if(this._locked)
throw new Error('Cannot add sources after other operations.');
};
_ASSERT_NOT_SPORKING(){
if(this._spork)
throw new Error('Cannot do this while sporking.');
};
addSource(source){
this._ASSERT_NOT_LOCKED();
this._sources.push(source);
this._currentFrame.streams.push(source.stream);
return this;
}
meow(str){ // meow string -- or accept meow array if parsed earlier
MeowRunner.runMeow(this, str);
return this;
}
process(meow){
return this.meow(meow);
}
fork() {
this._ASSERT_NOT_HOLDING();
const fork = new Bus(this.scope);
fork.parent = this;
this._createForkingFrame(fork._currentFrame);
return fork;
};
back() {
if(!this._parent)
throw new Error('Cannot exit fork, parent does not exist!');
return this.parent;
};
hook(name, words, scope, context, target){
};
fuse(bus) {
this.add(bus);
this.merge();
this.group();
return this;
};
join() {
const parent = this.back();
parent.add(this);
return parent;
};
add(bus) {
this._children.push(bus);
const nf = this._createNormalFrame(); // extend this bus
bus._createForkingFrame(nf); // outside bus then forks into this bus
return this;
};
fromMany(buses) {
const nf = this._createNormalFrame(); // extend this bus
const len = buses.length;
for(let i = 0; i < len; i++) {
const bus = buses[i];
bus._createForkingFrame(nf); // outside bus then forks into this bus
// add sources from buses
// bus._createTerminalFrame
}
return this;
};
addMany(buses) {
const nf = this._createNormalFrame(); // extend this bus
const len = buses.length;
for(let i = 0; i < len; i++) {
const bus = buses[i];
bus._createForkingFrame(nf); // outside bus then forks into this bus
}
return this;
};
spork() {
this._ASSERT_NOT_HOLDING();
this._ASSERT_NOT_SPORKING();
const spork = new Spork(this);
function sporkBuilder(){
return spork;
}
this._createNormalFrame(sporkBuilder);
return this._spork = spork;
};
// defer() {
// return this.timer(F.getDeferTimer);
// };
context(obj){
if(arguments.length){
this._context = obj;
return this;
}
return this._context;
}
target(obj){
if(arguments.length){
this._target = obj;
return this;
}
return this._target;
}
batch() {
this._createNormalFrame(batchStreamBuilder());
this._holding = false;
return this;
};
// throttle(fNum) {
// return this.timer(F.getThrottleTimer, fNum);
// };
hold() {
this._ASSERT_NOT_HOLDING();
this._holding = true;
this._head = this._createNormalFrame();
return this;
};
reset() {
this._ASSERT_HAS_HEAD();
this._createNormalFrame(resetStreamBuilder(this._head));
return this;
}
pull() {
const len = this._sources.length;
for(let i = 0; i < len; i++) {
const s = this._sources[i];
s.pull();
}
for(let i = 0; i < this._children.length; i++) {
const c = this._children[i];
c.pull();
}
return this;
};
scan(f, seed){
this._createNormalFrame(scanStreamBuilder(f, seed));
return this;
};
delay(fNum) {
this._createNormalFrame(delayStreamBuilder(fNum));
return this;
};
hasKeys(keys) {
const f = getHasKeys(keys);
this._createNormalFra