UNPKG

state-trooper

Version:
791 lines (716 loc) • 15.9 kB
"use strict"; var Box = require("./impl/channels").Box; var csp = require("./csp.core"), go = csp.go, take = csp.take, put = csp.put, takeAsync = csp.takeAsync, putAsync = csp.putAsync, alts = csp.alts, chan = csp.chan, CLOSED = csp.CLOSED; function mapFrom(f, ch) { return { is_closed: function() { return ch.is_closed(); }, close: function() { ch.close(); }, _put: function(value, handler) { return ch._put(value, handler); }, _take: function(handler) { var result = ch._take({ is_active: function() { return handler.is_active(); }, commit: function() { var take_cb = handler.commit(); return function(value) { return take_cb(value === CLOSED ? CLOSED : f(value)); }; } }); if (result) { var value = result.value; return new Box(value === CLOSED ? CLOSED : f(value)); } else { return null; } } }; } function mapInto(f, ch) { return { is_closed: function() { return ch.is_closed(); }, close: function() { ch.close(); }, _put: function(value, handler) { return ch._put(f(value), handler); }, _take: function(handler) { return ch._take(handler); } }; } function filterFrom(p, ch, bufferOrN) { var out = chan(bufferOrN); go(function*() { while (true) { var value = yield take(ch); if (value === CLOSED) { out.close(); break; } if (p(value)) { yield put(out, value); } } }); return out; } function filterInto(p, ch) { return { is_closed: function() { return ch.is_closed(); }, close: function() { ch.close(); }, _put: function(value, handler) { if (p(value)) { return ch._put(value, handler); } else { return new Box(!ch.is_closed()); } }, _take: function(handler) { return ch._take(handler); } }; } function removeFrom(p, ch) { return filterFrom(function(value) { return !p(value); }, ch); } function removeInto(p, ch) { return filterInto(function(value) { return !p(value); }, ch); } function* mapcat(f, src, dst) { while (true) { var value = yield take(src); if (value === CLOSED) { dst.close(); break; } else { var seq = f(value); var length = seq.length; for (var i = 0; i < length; i++) { yield put(dst, seq[i]); } if (dst.is_closed()) { break; } } } } function mapcatFrom(f, ch, bufferOrN) { var out = chan(bufferOrN); go(mapcat, [f, ch, out]); return out; } function mapcatInto(f, ch, bufferOrN) { var src = chan(bufferOrN); go(mapcat, [f, src, ch]); return src; } function pipe(src, dst, keepOpen) { go(function*() { while (true) { var value = yield take(src); if (value === CLOSED) { if (!keepOpen) { dst.close(); } break; } if (!(yield put(dst, value))) { break; } } }); return dst; } function split(p, ch, trueBufferOrN, falseBufferOrN) { var tch = chan(trueBufferOrN); var fch = chan(falseBufferOrN); go(function*() { while (true) { var value = yield take(ch); if (value === CLOSED) { tch.close(); fch.close(); break; } yield put(p(value) ? tch : fch, value); } }); return [tch, fch]; } function reduce(f, init, ch) { return go(function*() { var result = init; while (true) { var value = yield take(ch); if (value === CLOSED) { return result; } else { result = f(result, value); } } }, [], true); } function onto(ch, coll, keepOpen) { return go(function*() { var length = coll.length; // FIX: Should be a generic looping interface (for...in?) for (var i = 0; i < length; i++) { yield put(ch, coll[i]); } if (!keepOpen) { ch.close(); } }); } // TODO: Bounded? function fromColl(coll) { var ch = chan(coll.length); onto(ch, coll); return ch; } function map(f, chs, bufferOrN) { var out = chan(bufferOrN); var length = chs.length; // Array holding 1 round of values var values = new Array(length); // TODO: Not sure why we need a size-1 buffer here var dchan = chan(1); // How many more items this round var dcount; // put callbacks for each channel var dcallbacks = new Array(length); for (var i = 0; i < length; i ++) { dcallbacks[i] = (function(i) { return function(value) { values[i] = value; dcount --; if (dcount === 0) { putAsync(dchan, values.slice(0)); } }; }(i)); } go(function*() { while (true) { dcount = length; // We could just launch n goroutines here, but for effciency we // don't for (var i = 0; i < length; i ++) { try { takeAsync(chs[i], dcallbacks[i]); } catch (e) { // FIX: Hmm why catching here? dcount --; } } var values = yield take(dchan); for (i = 0; i < length; i ++) { if (values[i] === CLOSED) { out.close(); return; } } yield put(out, f.apply(null, values)); } }); return out; } function merge(chs, bufferOrN) { var out = chan(bufferOrN); var actives = chs.slice(0); go(function*() { while (true) { if (actives.length === 0) { break; } var r = yield alts(actives); var value = r.value; if (value === CLOSED) { // Remove closed channel var i = actives.indexOf(r.channel); actives.splice(i, 1); continue; } yield put(out, value); } out.close(); }); return out; } function into(coll, ch) { var result = coll.slice(0); return reduce(function(result, item) { result.push(item); return result; }, result, ch); } function takeN(n, ch, bufferOrN) { var out = chan(bufferOrN); go(function*() { for (var i = 0; i < n; i ++) { var value = yield take(ch); if (value === CLOSED) { break; } yield put(out, value); } out.close(); }); return out; } var NOTHING = {}; function unique(ch, bufferOrN) { var out = chan(bufferOrN); var last = NOTHING; go(function*() { while (true) { var value = yield take(ch); if (value === CLOSED) { break; } if (value === last) { continue; } last = value; yield put(out, value); } out.close(); }); return out; } function partitionBy(f, ch, bufferOrN) { var out = chan(bufferOrN); var part = []; var last = NOTHING; go(function*() { while (true) { var value = yield take(ch); if (value === CLOSED) { if (part.length > 0) { yield put(out, part); } out.close(); break; } else { var newItem = f(value); if (newItem === last || last === NOTHING) { part.push(value); } else { yield put(out, part); part = [value]; } last = newItem; } } }); return out; } function partition(n, ch, bufferOrN) { var out = chan(bufferOrN); go(function*() { while (true) { var part = new Array(n); for (var i = 0; i < n; i++) { var value = yield take(ch); if (value === CLOSED) { if (i > 0) { yield put(out, part.slice(0, i)); } out.close(); return; } part[i] = value; } yield put(out, part); } }); return out; } // For channel identification var genId = (function() { var i = 0; return function() { i ++; return "" + i; }; })(); var ID_ATTR = "__csp_channel_id"; // TODO: Do we need to check with hasOwnProperty? function len(obj) { var count = 0; for (var p in obj) { count ++; } return count; } function chanId(ch) { var id = ch[ID_ATTR]; if (id === undefined) { id = ch[ID_ATTR] = genId(); } return id; } var Mult = function(ch) { this.taps = {}; this.ch = ch; }; var Tap = function(channel, keepOpen) { this.channel = channel; this.keepOpen = keepOpen; }; Mult.prototype.muxch = function() { return this.ch; }; Mult.prototype.tap = function(ch, keepOpen) { var id = chanId(ch); this.taps[id] = new Tap(ch, keepOpen); }; Mult.prototype.untap = function(ch) { delete this.taps[chanId(ch)]; }; Mult.prototype.untapAll = function() { this.taps = {}; }; function mult(ch) { var m = new Mult(ch); var dchan = chan(1); var dcount; function makeDoneCallback(tap) { return function(stillOpen) { dcount --; if (dcount === 0) { putAsync(dchan, true); } if (!stillOpen) { m.untap(tap.channel); } }; } go(function*() { while (true) { var value = yield take(ch); var id, t; var taps = m.taps; if (value === CLOSED) { for (id in taps) { t = taps[id]; if (!t.keepOpen) { t.channel.close(); } } // TODO: Is this necessary? m.untapAll(); break; } dcount = len(taps); // XXX: This is because putAsync can actually call back // immediately. Fix that var initDcount = dcount; // Put value on tapping channels... for (id in taps) { t = taps[id]; putAsync(t.channel, value, makeDoneCallback(t)); } // ... waiting for all puts to complete if (initDcount > 0) { yield take(dchan); } } }); return m; } mult.tap = function tap(m, ch, keepOpen) { m.tap(ch, keepOpen); return ch; }; mult.untap = function untap(m, ch) { m.untap(ch); }; mult.untapAll = function untapAll(m) { m.untapAll(); }; var Mix = function(ch) { this.ch = ch; this.stateMap = {}; this.change = chan(); this.soloMode = mix.MUTE; }; Mix.prototype._changed = function() { putAsync(this.change, true); }; Mix.prototype._getAllState = function() { var allState = {}; var stateMap = this.stateMap; var solos = []; var mutes = []; var pauses = []; var reads; for (var id in stateMap) { var chanData = stateMap[id]; var state = chanData.state; var channel = chanData.channel; if (state[mix.SOLO]) { solos.push(channel); } // TODO if (state[mix.MUTE]) { mutes.push(channel); } if (state[mix.PAUSE]) { pauses.push(channel); } } var i, n; if (this.soloMode === mix.PAUSE && solos.length > 0) { n = solos.length; reads = new Array(n + 1); for (i = 0; i < n; i++) { reads[i] = solos[i]; } reads[n] = this.change; } else { reads = []; for (id in stateMap) { chanData = stateMap[id]; channel = chanData.channel; if (pauses.indexOf(channel) < 0) { reads.push(channel); } } reads.push(this.change); } return { solos: solos, mutes: mutes, reads: reads }; }; Mix.prototype.admix = function(ch) { this.stateMap[chanId(ch)] = { channel: ch, state: {} }; this._changed(); }; Mix.prototype.unmix = function(ch) { delete this.stateMap[chanId(ch)]; this._changed(); }; Mix.prototype.unmixAll = function() { this.stateMap = {}; this._changed(); }; Mix.prototype.toggle = function(updateStateList) { // [[ch1, {}], [ch2, {solo: true}]]; var length = updateStateList.length; for (var i = 0; i < length; i++) { var ch = updateStateList[i][0]; var id = chanId(ch); var updateState = updateStateList[i][1]; var chanData = this.stateMap[id]; if (!chanData) { chanData = this.stateMap[id] = { channel: ch, state: {} }; } for (var mode in updateState) { chanData.state[mode] = updateState[mode]; } } this._changed(); }; Mix.prototype.setSoloMode = function(mode) { if (VALID_SOLO_MODES.indexOf(mode) < 0) { throw new Error("Mode must be one of: ", VALID_SOLO_MODES.join(", ")); } this.soloMode = mode; this._changed(); }; function mix(out) { var m = new Mix(out); go(function*() { var state = m._getAllState(); while (true) { var result = yield alts(state.reads); var value = result.value; var channel = result.channel; if (value === CLOSED) { delete m.stateMap[chanId(channel)]; state = m._getAllState(); continue; } if (channel === m.change) { state = m._getAllState(); continue; } var solos = state.solos; if (solos.indexOf(channel) > -1 || (solos.length === 0 && !(state.mutes.indexOf(channel) > -1))) { var stillOpen = yield put(out, value); if (!stillOpen) { break; } } } }); return m; } mix.MUTE = "mute"; mix.PAUSE = "pause"; mix.SOLO = "solo"; var VALID_SOLO_MODES = [mix.MUTE, mix.PAUSE]; mix.add = function admix(m, ch) { m.admix(ch); }; mix.remove = function unmix(m, ch) { m.unmix(ch); }; mix.removeAll = function unmixAll(m) { m.unmixAll(); }; mix.toggle = function toggle(m, updateStateList) { m.toggle(updateStateList); }; mix.setSoloMode = function setSoloMode(m, mode) { m.setSoloMode(mode); }; function constantlyNull() { return null; } var Pub = function(ch, topicFn, bufferFn) { this.ch = ch; this.topicFn = topicFn; this.bufferFn = bufferFn; this.mults = {}; }; Pub.prototype._ensureMult = function(topic) { var m = this.mults[topic]; var bufferFn = this.bufferFn; if (!m) { m = this.mults[topic] = mult(chan(bufferFn(topic))); } return m; }; Pub.prototype.sub = function(topic, ch, keepOpen) { var m = this._ensureMult(topic); return mult.tap(m, ch, keepOpen); }; Pub.prototype.unsub = function(topic, ch) { var m = this.mults[topic]; if (m) { mult.untap(m, ch); } }; Pub.prototype.unsubAll = function(topic) { if (topic === undefined) { this.mults = {}; } else { delete this.mults[topic]; } }; function pub(ch, topicFn, bufferFn) { bufferFn = bufferFn || constantlyNull; var p = new Pub(ch, topicFn, bufferFn); go(function*() { while (true) { var value = yield take(ch); var mults = p.mults; var topic; if (value === CLOSED) { for (topic in mults) { mults[topic].muxch().close(); } break; } // TODO: Somehow ensure/document that this must return a string // (otherwise use proper (hash)maps) topic = topicFn(value); var m = mults[topic]; if (m) { var stillOpen = yield put(m.muxch(), value); if (!stillOpen) { delete mults[topic]; } } } }); return p; } pub.sub = function sub(p, topic, ch, keepOpen) { return p.sub(topic, ch, keepOpen); }; pub.unsub = function unsub(p, topic, ch) { p.unsub(topic, ch); }; pub.unsubAll = function unsubAll(p, topic) { p.unsubAll(topic); }; module.exports = { mapFrom: mapFrom, mapInto: mapInto, filterFrom: filterFrom, filterInto: filterInto, removeFrom: removeFrom, removeInto: removeInto, mapcatFrom: mapcatFrom, mapcatInto: mapcatInto, pipe: pipe, split: split, reduce: reduce, onto: onto, fromColl: fromColl, map: map, merge: merge, into: into, take: takeN, unique: unique, partition: partition, partitionBy: partitionBy, mult: mult, mix: mix, pub: pub }; // Possible "fluid" interfaces: // thread( // [fromColl, [1, 2, 3, 4]], // [mapFrom, inc], // [into, []] // ) // thread( // [fromColl, [1, 2, 3, 4]], // [mapFrom, inc, _], // [into, [], _] // ) // wrap() // .fromColl([1, 2, 3, 4]) // .mapFrom(inc) // .into([]) // .unwrap();