UNPKG

machine-framework

Version:

self-assembling client-side web framework

2,545 lines (1,666 loc) 53.9 kB
function isPrivate(name){ return name[0] === '_' || (isAction(name) && name[1] === '_'); } function isAction(name){ return name[0] === '$'; } 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() { return true; } function LatchStream(name, f) { this.name = name; this.f = f || TRUE; 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}, }; 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(); const word = new Word(content); phrase.words.push(word); } } 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; 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 === '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_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); 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); 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); 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; }; 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._createNormalFrame(latchStreamBuilder(f)); return this; }; group(by) { this._createNormalFrame(groupStreamBuilder(by)); return this; }; all() { this._createNormalFrame(allStreamBuilder()); return this; }; first(count) { this._createNormalFrame(firstNStreamBuilder(count)); return this; }; last(count) { this._createNormalFrame(lastNStreamBuilder(count)); return this; }; prior() { this._createNormalFrame(priorStreamBuilder()); return this; }; run(f) { this._ASSERT_IS_FUNCTION(f); this._createNormalFrame(tapStreamBuilder(f)); return this; }; merge() { this._createMergingFrame(); return this; }; msg(fAny, context) { const f = FUNCTOR(fAny); this._createNormalFrame(msgStreamBuilder(f, context)); return this; }; name(str) { this._createNormalFrame(nameStreamBuilder(str)); return this; }; source(str) { this._createNormalFrame(nameStreamBuilder(str)); return this; }; write(data) { this._createNormalFrame(writeStreamBuilder(data)); return this; }; filter(f, context) { this._ASSERT_IS_FUNCTION(f); this._ASSERT_NOT_HOLDING(); this._createNormalFrame(filterStreamBuilder(f, context)); return this; }; filterMap(f, m) { this._ASSERT_IS_FUNCTION(f); this._ASSERT_NOT_HOLDING(); this._createNormalFrame(filterMapStreamBuilder(f, m)); return this; }; split() { this._createNormalFrame(splitStreamBuilder()); return this; }; skipDupes() { this._createNormalFrame(skipStreamBuilder()); return this; }; addSubscribe(name, data){ const source = new SubscribeSource(name, data, true); this.addSource(source); return this; }; addEvent(name, target, eventName, useCapture){ const source = new EventSource(name, target, eventName || name, useCapture); this.addSource(source); return this; }; toStream() { // merge, fork -> immutable stream? }; destroy() { if (this.dead) return this; this._dead = true; const sources = this._sources; const len = sources.length; for (let i = 0; i < len; i++) { const s = sources[i]; s.destroy(); } return this; }; } let idCounter = 0; function _destroyEach(arr){ let i = arr.length; while(i--){ arr[i].destroy(); } } class Scope{ constructor(name) { this._id = ++idCounter; this._name = name; this._parent = null; this._children = []; this._wires = {}; this._buses = []; this._dataMap = new Map(); this._valveMap = new Map(); this._mirrorMap = new Map(); this._dead = false; this._shared = false; // shared scopes can access private actions and states in their parent }; get name() { return this._name; }; get dead() { return this._dead; }; get children(){ return this._children.map((d) => d); }; bus(){ return new Bus(this); }; clear(){ if(this._dead) return; _destroyEach(this.children); _destroyEach(this._buses); _destroyEach(this._dataMap.values()); this._children = []; this._buses = []; this._dataMap.clear(); this._valveMap.clear(); this._mirrorMap.clear(); }; destroy(){ this.clear(); this.parent = null; this._dead = true; }; createChild(name){ let child = new Scope(name); child.parent = this; return child; }; insertParent(newParent){ newParent.parent = this.parent; this.parent = newParent; return this; }; 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; }; set valves(list){ for(const name of list){ this._valveMap.set(name, true); } } get valves(){ return Array.from(this._valveMap.keys());}; _createMirror(data){ const mirror = Object.create(data); mirror._writable = false; this._mirrorMap.set(data.name, mirror); return mirror; }; _createData(name){ const d = new Data(this, name); this._dataMap.set(name, d); if(!d._action && !d._private) // if a public state, create a read-only mirror this._createMirror(d); return d; }; demand(name){ return this.grab(name) || this._createData(name); }; wire(stateName){ const actionName = '$' + stateName; const state = this.demand(stateName); const action = this.demand(actionName); if(!this._wires[stateName]) { this._wires[stateName] = this.bus().meow(actionName + ' > ' + stateName); } return state; }; findDataSet(names, required){ const result = {}; for(const name of names){ result[name] = this.find(name, required); } return result; }; readDataSet(names, required){ const dataSet = this.findDataSet(names, required); const result = {}; for(const d of dataSet) { if (d) { if (d.present) result[d.name] = d.read(); } } return result; }; // created a flattened view of all data at and above this scope flatten(){ let scope = this; const result = new Map(); const appliedValves = new Map(); for(const [key, value] of scope._dataMap){ result.set(key, value); } while(scope = scope._parent){ const dataList = scope._dataMap; const valves = scope._valveMap; const mirrors = scope._mirrorMap; if(!dataList.size) continue; // further restrict valves with each new scope if(valves.size){ if(appliedValves.size) { for (const key of appliedValves.keys()) { if(!valves.has(key)) appliedValves.delete(key); } } else { for (const [key, value] of valves.entries()) { appliedValves.set(key, value); } } } const possibles = appliedValves.size ? appliedValves : dataList; for(const key of possibles.keys()) { if (!result.has(key)) { const data = mirrors.get(key) || dataList.get(key); if (data) result.set(key, data); } } } return result; }; find(name, required){ const localData = this.grab(name); if(localData) return localData; // if(this.shared){ // const sharedData = this._parent.grab(name); // if(sharedData) // return sharedData; // } let scope = this; while(scope = scope._parent){ const valves = scope._valveMap; // if valves exist and the name is not present, stop looking if(valves.size && !valves.has(name)){ break; } const mirror = scope._mirrorMap.get(name); if(mirror) return mirror; const d = scope.grab(name); if(d) { if (d._private) continue; return d; } } _ASSERT_NOT_REQUIRED(required); return null; }; findOuter(name, required){ _ASSERT_NO_OUTER_PRIVATE(name); let foundInner = false; const localData = this.grab(name); if(localData) foundInner = true; let scope = this; while(scope = scope._parent){ const valves = scope._valveMap; // if valves exist and the name is not present, stop looking if(valves.size && !valves.has(name)){ break; } const mirror = scope._mirrorMap.get(name); if(mirror) { if(foundInner) return mirror; foundInner = true; continue; } const d = scope.grab(name); if(d) { if(foundInner) return d; foundInner = true; } } if(required) throw new Error('Required data: ' + name + ' not found!'); return null; }; grab(name, required) { const data = this._dataMap.get(name); if(!data && required) throw new Error('Required Data: ' + name + ' not found!'); return data || null; }; // write key-values as a transaction write(writeHash){ const list = []; for(const k in writeHash){ const v = writeHash[k]; const d = this.find(k); // todo ASSERT_DATA_FOUND d.silentWrite(v); list.push(d); } for(const d of list){ d.refresh(); } return this; }; } function _ASSERT_NOT_REQUIRED(required){ if(required) throw new Er