diff --git a/js/compiler/compiler.js b/js/compiler/compiler.js index e1f742d..0ce2aae 100644 --- a/js/compiler/compiler.js +++ b/js/compiler/compiler.js @@ -122,7 +122,7 @@ var modifiedSourceActions = { var label = maybeLabel.numChildren === 1 ? maybeLabel.children[0].interval.contents : JSON.stringify(typeName.interval.contents); - return 'var ' + typeName.asES5 + ' = Syndicate.Struct.makeStructureConstructor(' + + return 'var ' + typeName.asES5 + ' = Syndicate.Struct.makeConstructor(' + label + ', ' + JSON.stringify(formals) + ');'; }, @@ -293,14 +293,35 @@ semantics.addOperation('buildSubscription(acc,mode)', { v.children[0].buildSubscription(this.args.acc, this.args.mode); // both branches! }, + AssignmentExpression_assignment: function (lhsExpr, _assignmentOperator, rhsExpr) { + var i = lhsExpr.interval.contents; + if (i[0] === '$' && i.length > 1) { + switch (this.args.mode) { + case 'pattern': rhsExpr.buildSubscription(this.args.acc, this.args.mode); break; + case 'instantiated': lhsExpr.buildSubscription(this.args.acc, this.args.mode); break; + case 'projection': { + this.args.acc.push('(Syndicate._$(' + JSON.stringify(i.slice(1)) + ','); + rhsExpr.buildSubscription(this.args.acc, this.args.mode); + this.args.acc.push('))'); + break; + } + default: throw new Error('Unexpected buildSubscription mode ' + this.args.mode); + } + } else { + lhsExpr.buildSubscription(this.args.acc, this.args.mode); + _assignmentOperator.buildSubscription(this.args.acc, this.args.mode); + rhsExpr.buildSubscription(this.args.acc, this.args.mode); + } + }, + identifier: function(_name) { var i = this.interval.contents; - if (i[0] === '$') { + if (i[0] === '$' && i.length > 1) { switch (this.args.mode) { - case 'pattern': this.args.acc.push('_'); break; - case 'instantiated': this.args.acc.push(i.slice(1)); break; - case 'projection': this.args.acc.push('(Syndicate._$(' + JSON.stringify(i.slice(1)) + '))'); break; - default: throw new Error('Unexpected buildSubscription mode ' + this.args.mode); + case 'pattern': this.args.acc.push('_'); break; + case 'instantiated': this.args.acc.push(i.slice(1)); break; + case 'projection': this.args.acc.push('(Syndicate._$(' + JSON.stringify(i.slice(1)) + '))'); break; + default: throw new Error('Unexpected buildSubscription mode ' + this.args.mode); } } else { this.args.acc.push(i); @@ -328,7 +349,7 @@ semantics.addAttribute('bindings', { semantics.addOperation('pushBindings(accumulator)', { identifier: function(_name) { var i = this.interval.contents; - if (i[0] === '$') { + if (i[0] === '$' && i.length > 1) { this.args.accumulator.push(i.slice(1)); } }, diff --git a/js/examples/chat/broker-client.js b/js/examples/chat/broker-client.js new file mode 100644 index 0000000..eafd665 --- /dev/null +++ b/js/examples/chat/broker-client.js @@ -0,0 +1,237 @@ +"use strict"; +// WebSocket-based Syndicate broker client + +var Trie = Syndicate.Trie; +var Patch = Syndicate.Patch; +var Dataspace = Syndicate.Dataspace; +var Struct = Syndicate.Struct; +var DemandMatcher = Syndicate.DemandMatcher; +var __ = Syndicate.__; +var _$ = Syndicate._$; + +var DEFAULT_RECONNECT_DELAY = 100; // ms +var MAX_RECONNECT_DELAY = 30000; // ms +var DEFAULT_IDLE_TIMEOUT = 300000; // ms; i.e., 5 minutes +var DEFAULT_PING_INTERVAL = DEFAULT_IDLE_TIMEOUT - 10000; // ms + +var toBroker = Struct.makeConstructor('toBroker', ['url', 'assertion']); +var fromBroker = Struct.makeConstructor('fromBroker', ['url', 'assertion']); +var brokerConnection = Struct.makeConstructor('brokerConnection', ['url']); +var brokerConnected = Struct.makeConstructor('brokerConnected', ['url']); +var forceBrokerDisconnect = Struct.makeConstructor('forceBrokerDisconnect', ['url']); + +function spawnBrokerClientDriver() { + var URL = _$('url'); // capture used to extract URL + Dataspace.spawn( + new Dataspace(function () { + Dataspace.spawn( + new DemandMatcher([brokerConnection(URL)], + [brokerConnection(URL)], + { + demandMetaLevel: 1, + supplyMetaLevel: 0, + onDemandIncrease: function (c) { + Dataspace.spawn(new BrokerClientConnection(c.url)); + } + })); + })); +} + +function BrokerClientConnection(wsurl) { + this.wsurl = wsurl; + this.sock = null; + + this.sendsAttempted = 0; + this.sendsTransmitted = 0; + this.receiveCount = 0; + this.connectionCount = 0; + + this.reconnectDelay = DEFAULT_RECONNECT_DELAY; + this.idleTimeout = DEFAULT_IDLE_TIMEOUT; + this.pingInterval = DEFAULT_PING_INTERVAL; + + this.localAssertions = Trie.emptyTrie; + this.remoteAssertions = Trie.emptyTrie; + + this.activityTimestamp = 0; + this.idleTimer = null; + this.pingTimer = null; +} + +BrokerClientConnection.prototype.clearHeartbeatTimers = function () { + if (this.idleTimer) { clearTimeout(this.idleTimer); this.idleTimer = null; } + if (this.pingTimer) { clearTimeout(this.pingTimer); this.pingTimer = null; } +}; + +BrokerClientConnection.prototype.recordActivity = function () { + var self = this; + this.activityTimestamp = +(new Date()); + this.clearHeartbeatTimers(); + this.idleTimer = setTimeout(function () { self.forceclose(); }, this.idleTimeout); + this.pingTimer = setTimeout(function () { self.safeSend(JSON.stringify("ping")) }, + this.pingInterval); +}; + +BrokerClientConnection.prototype.boot = function () { + this.reconnect(); + var initialAssertions = + Patch.sub(toBroker(this.wsurl, __), 1) // read assertions to go out + .andThen(Patch.sub(Patch.observe(fromBroker(this.wsurl, __)), 1)) // and monitor interests + .andThen(Patch.assert(brokerConnection(this.wsurl))) // signal to DemandMatcher that we exist + .andThen(Patch.sub(brokerConnection(this.wsurl), 1)) // track demand + .andThen(Patch.sub(forceBrokerDisconnect(this.wsurl), 1)) + ; + return initialAssertions; +}; + +BrokerClientConnection.prototype.trapexit = function () { + this.forceclose(); +}; + +BrokerClientConnection.prototype.isConnected = function () { + return this.sock && this.sock.readyState === this.sock.OPEN; +}; + +BrokerClientConnection.prototype.safeSend = function (m) { + // console.log('safeSend', m); + try { + this.sendsAttempted++; + if (this.isConnected()) { + this.sock.send(m); + this.sendsTransmitted++; + } + } catch (e) { + console.warn("Trapped exn while sending", e); + } +}; + +BrokerClientConnection.prototype.sendPatch = function (p) { + var j = JSON.stringify(Codec.encodeEvent(Syndicate.stateChange(p))); + this.safeSend(j); +}; + +BrokerClientConnection.prototype.handleEvent = function (e) { + // console.log("BrokerClientConnection.handleEvent", e); + switch (e.type) { + case "stateChange": + if (e.patch.project(Patch.atMeta(brokerConnection(_$))).hasRemoved()) { + // console.log("Client is no longer interested in this connection", this.wsurl); + Dataspace.exit(); + } + + var pTo = e.patch.project(Patch.atMeta(toBroker(__, _$))); + + var pObsFrom = e.patch.project(Patch.atMeta(Patch.observe(fromBroker(__, _$)))); + pObsFrom = new Patch.Patch( + Trie.compilePattern(true, Patch.observe(Trie.embeddedTrie(pObsFrom.added))), + Trie.compilePattern(true, Patch.observe(Trie.embeddedTrie(pObsFrom.removed)))); + + var newLocalAssertions = this.localAssertions; + newLocalAssertions = pTo.label(Immutable.Set.of("to")).applyTo(newLocalAssertions); + newLocalAssertions = pObsFrom.label(Immutable.Set.of("obsFrom")).applyTo(newLocalAssertions); + + var trueSet = Immutable.Set.of(true); + var alwaysTrueSet = function (v) { return trueSet; }; + var p = Patch.computePatch(Trie.relabel(this.localAssertions, alwaysTrueSet), + Trie.relabel(newLocalAssertions, alwaysTrueSet)); + + this.localAssertions = newLocalAssertions; + // console.log("localAssertions"); + // console.log(Trie.prettyTrie(this.localAssertions)); + // console.log(p.pretty()); + this.sendPatch(p); + break; + + case "message": + var m = e.message; + if (Patch.atMeta.isClassOf(m)) { + m = m[0]; + if (toBroker.isClassOf(m)) { + var j = JSON.stringify(Codec.encodeEvent(Syndicate.message(m[1]))); + this.safeSend(j); + } else if (forceBrokerDisconnect.isClassOf(m)) { + this.forceclose(); + } + } + break; + } +}; + +BrokerClientConnection.prototype.forceclose = function (keepReconnectDelay) { + if (!keepReconnectDelay) { + this.reconnectDelay = DEFAULT_RECONNECT_DELAY; + } + this.clearHeartbeatTimers(); + if (this.sock) { + console.log("BrokerClientConnection.forceclose called"); + this.sock.close(); + this.sock = null; + } +}; + +BrokerClientConnection.prototype.reconnect = function () { + var self = this; + this.forceclose(true); + this.connectionCount++; + this.sock = new WebSocket(this.wsurl); + this.sock.onopen = Dataspace.wrap(function (e) { return self.onopen(e); }); + this.sock.onmessage = Dataspace.wrap(function (e) { + self.receiveCount++; + return self.onmessage(e); + }); + this.sock.onclose = Dataspace.wrap(function (e) { return self.onclose(e); }); +}; + +BrokerClientConnection.prototype.onopen = function (e) { + console.log("connected to " + this.sock.url); + this.recordActivity(); + Dataspace.stateChange(Patch.assert(brokerConnected(this.wsurl), 1)); + this.reconnectDelay = DEFAULT_RECONNECT_DELAY; + this.sendPatch((new Patch.Patch(this.localAssertions, Trie.emptyTrie)).strip()); +}; + +BrokerClientConnection.prototype.onmessage = function (wse) { + // console.log("onmessage", wse); + this.recordActivity(); + + var j = JSON.parse(wse.data, Struct.reviver); + if (j === "ping") { + this.safeSend(JSON.stringify("pong")); + return; + } else if (j === "pong") { + return; // recordActivity already took care of our timers + } + + var e = Codec.decodeAction(j); + switch (e.type) { + case "stateChange": { + var added = fromBroker(this.wsurl, Trie.embeddedTrie(e.patch.added)); + var removed = fromBroker(this.wsurl, Trie.embeddedTrie(e.patch.removed)); + var p = Patch.assert(added, 1).andThen(Patch.retract(removed, 1)); + // console.log('incoming stateChange'); + // console.log(p.pretty()); + Dataspace.stateChange(p); + break; + } + case "message": { + Dataspace.send(fromBroker(this.wsurl, e.message), 1); + break; + } + } +}; + +BrokerClientConnection.prototype.onclose = function (e) { + var self = this; + + // console.log("onclose", e); + Dataspace.stateChange(Patch.retract(brokerConnected(this.wsurl), 1)); + + console.log("reconnecting to " + this.wsurl + " in " + this.reconnectDelay + "ms"); + setTimeout(Dataspace.wrap(function () { self.reconnect(); }), this.reconnectDelay); + + this.reconnectDelay = this.reconnectDelay * 1.618 + (Math.random() * 1000); + this.reconnectDelay = + this.reconnectDelay > MAX_RECONNECT_DELAY + ? MAX_RECONNECT_DELAY + (Math.random() * 1000) + : this.reconnectDelay; +}; diff --git a/js/examples/chat/codec.js b/js/examples/chat/codec.js new file mode 100644 index 0000000..43489ec --- /dev/null +++ b/js/examples/chat/codec.js @@ -0,0 +1,41 @@ +"use strict"; +// Wire protocol representation of events and actions + +var Trie = Syndicate.Trie; +var Struct = Syndicate.Struct; + +function _encode(e) { + switch (e.type) { + case "stateChange": + return ["patch", e.patch.toJSON()]; + case "message": + return ["message", e.message]; + } +} + +function _decode(what) { + return function (j) { + switch (j[0]) { + case "patch": + return Syndicate.stateChange(Patch.fromJSON(j[1])); + case "message": + return Syndicate.message(j[1]); + default: + throw new Error("Invalid JSON-encoded " + what + ": " + JSON.stringify(j)); + } + }; +} + +/////////////////////////////////////////////////////////////////////////// + +// module.exports.encodeEvent = _encode; +// module.exports.decodeEvent = _decode("event"); +// module.exports.encodeAction = _encode; +// module.exports.decodeAction = _decode("action"); + +var Codec = { + encodeEvent: _encode, + decodeEvent: _decode("event"), + encodeAction: _encode, + decodeAction: _decode("action") +}; diff --git a/js/examples/chat/index.html b/js/examples/chat/index.html new file mode 100644 index 0000000..f8632af --- /dev/null +++ b/js/examples/chat/index.html @@ -0,0 +1,55 @@ + + + + Syndicate: Chat + + + + + + + + + + + + +
+
+
+ + + + + + + + +
+
+
+ +
+
+

Messages

+
+
+
+

Active Users

+ +
+
+ +
+
+
+ + +
+
+
+ +
+

+  
+
diff --git a/js/examples/chat/index.js b/js/examples/chat/index.js
new file mode 100644
index 0000000..0c95747
--- /dev/null
+++ b/js/examples/chat/index.js
@@ -0,0 +1,116 @@
+assertion type present(name, status);
+assertion type says(who, message);
+
+var DOM = Syndicate.DOM.DOM;
+var jQueryEvent = Syndicate.JQuery.jQueryEvent;
+
+///////////////////////////////////////////////////////////////////////////
+// Application
+
+function spawnChatApp() {
+  $("#chat_form").submit(function (e) { e.preventDefault(); return false; });
+  $("#nym_form").submit(function (e) { e.preventDefault(); return false; });
+  if (!($("#nym").val())) { $("#nym").val("nym" + Math.floor(Math.random() * 65536)); }
+
+  actor {
+    forever {
+      on asserted jQueryInput('#nym',    $v) { this.nym = v; }
+      on asserted jQueryInput('#status', $v) { this.status = v; }
+
+      on asserted brokerConnected($url) { outputState('connected to ' + url); }
+      on retracted brokerConnected($url) { outputState('disconnected from ' + url); }
+
+      during jQueryInput('#wsurl', $url) {
+        assert brokerConnection(url);
+
+        on message Syndicate.WakeDetector.wakeEvent() {
+          :: forceBrokerDisconnect(url);
+        }
+
+        assert toBroker(url, present(this.nym, this.status));
+        during fromBroker(url, present($who, $status)) {
+          assert DOM('#nymlist', 'present-nym', Syndicate.seal(
+            ["li",
+             ["span", [["class", "nym"]], who],
+             ["span", [["class", "nym_status"]], status]]));
+        }
+
+        on message jQueryEvent('#send_chat', 'click', _) {
+          var inp = $("#chat_input");
+          var utterance = inp.val();
+          inp.val("");
+          if (utterance) :: toBroker(url, says(this.nym, utterance));
+        }
+
+        on message fromBroker(url, says($who, $what)) {
+          outputUtterance(who, what);
+        }
+      }
+    }
+  }
+}
+
+///////////////////////////////////////////////////////////////////////////
+// Adding items to the transcript panel (plain Javascript/jQuery)
+
+function outputItem(item) {
+  var stamp = $("").text((new Date()).toGMTString()).addClass("timestamp");
+  var item = $("
").append([stamp].concat(item)); + var o = $("#chat_output"); + o.append(item); + o[0].scrollTop = o[0].scrollHeight; + return item; +} + +function outputState(state) { + outputItem([$("").text(state).addClass(state).addClass("state")]) + .addClass("state_" + state); +} + +function outputUtterance(who, what) { + outputItem([$("").text(who).addClass("nym"), + $("").text(what).addClass("utterance")]).addClass("utterance"); +} + +/////////////////////////////////////////////////////////////////////////// +// Input control value monitoring + +assertion type jQueryInput(selector, value); + +function spawnInputChangeMonitor() { + actor { + forever { + on asserted Syndicate.observe(jQueryInput($selector, _)) { + actor { + this.value = $(selector).val(); + state { + assert jQueryInput(selector, this.value); + on message jQueryEvent(selector, 'change', $e) { + this.value = e.target.value; + } + } until { + case retracted Syndicate.observe(jQueryInput(selector, _)); + } + } + } + } + } +} + +/////////////////////////////////////////////////////////////////////////// +// Main + +$(document).ready(function () { + ground dataspace G { + Syndicate.JQuery.spawnJQueryDriver(); + Syndicate.DOM.spawnDOMDriver(); + Syndicate.WakeDetector.spawnWakeDetector(); + spawnBrokerClientDriver(); + spawnInputChangeMonitor(); + spawnChatApp(); + } + + // G.dataspace.setOnStateChange(function (mux, patch) { + // $("#ds-state").text(Syndicate.prettyTrie(mux.routingTable)); + // }); +}); diff --git a/js/examples/chat/style.css b/js/examples/chat/style.css new file mode 100644 index 0000000..a0fe84b --- /dev/null +++ b/js/examples/chat/style.css @@ -0,0 +1,97 @@ +h1 { + background: lightgrey; +} + +body > section { + display: flex; +} + +body > section > section { + margin: 1em; +} + +section#messages { + flex-grow: 3; +} + +section#active_users { + flex-grow: 1; +} + +form#chat_form { + flex: 1 100%; +} + +span.timestamp { + color: #d0d0d0; +} + +span.timestamp:after { + content: " "; +} + +.utterance span.nym:after { + content: ": "; +} + +span.arrived:after { + content: " arrived"; +} + +span.departed:after { + content: " departed"; +} + +div.notification { + background-color: #eeeeff; +} + +span.state.connected, span.arrived { + color: #00c000; +} + +span.state.disconnected, span.departed { + color: #c00000; +} + +span.state.crashed { + color: white; + background: red; +} + +span.state.crashed:after { + content: "; please reload the page"; +} + +div.state_disconnected { + background-color: #ffeeee; +} + +div.state_connected { + background-color: #eeffee; +} + +#chat_output { + height: 15em; + overflow-y: scroll; +} + +#chat_input { + width: 80%; +} + +.nym { + color: #00c000; +} + +.nym_status:before { + content: " ("; +} + +.nym_status:after { + content: ")"; +} + +.nym_status { + font-size: smaller; +} diff --git a/js/examples/jquery/index.js b/js/examples/jquery/index.js index 41d24ea..891cc0b 100644 --- a/js/examples/jquery/index.js +++ b/js/examples/jquery/index.js @@ -19,7 +19,7 @@ $(document).ready(function () { handleEvent: function (e) { if (e.type === 'message' && Syndicate.JQuery.jQueryEvent.isClassOf(e.message) - && e.message.selector === '#clicker') + && e.message[0] === '#clicker') { var r = $('#result'); r.html(Number(r.html()) + 1); diff --git a/js/examples/smoketest/index.js b/js/examples/smoketest/index.js index 3d4cae5..a960659 100644 --- a/js/examples/smoketest/index.js +++ b/js/examples/smoketest/index.js @@ -1,6 +1,6 @@ "use strict"; -var beep = Syndicate.Struct.makeStructureConstructor('beep', ['counter']); +var beep = Syndicate.Struct.makeConstructor('beep', ['counter']); var G; $(document).ready(function () { @@ -26,7 +26,7 @@ $(document).ready(function () { boot: function () { return sub(beep.pattern); }, handleEvent: function (e) { if (e.type === 'message') { - console.log("beep!", e.message.counter); + console.log("beep!", e.message[0]); } } }); diff --git a/js/examples/textfield/index.js b/js/examples/textfield/index.js index a29361b..ec8ff04 100644 --- a/js/examples/textfield/index.js +++ b/js/examples/textfield/index.js @@ -8,9 +8,9 @@ var __ = Syndicate.__; var _$ = Syndicate._$; var jQueryEvent = Syndicate.JQuery.jQueryEvent; -var fieldContents = Syndicate.Struct.makeStructureConstructor('fieldContents', ['text', 'pos']); -var highlight = Syndicate.Struct.makeStructureConstructor('highlight', ['state']); -var fieldCommand = Syndicate.Struct.makeStructureConstructor('fieldCommand', ['detail']); +var fieldContents = Syndicate.Struct.makeConstructor('fieldContents', ['text', 'pos']); +var highlight = Syndicate.Struct.makeConstructor('highlight', ['state']); +var fieldCommand = Syndicate.Struct.makeConstructor('fieldCommand', ['detail']); function escapeText(text) { text = text.replace(/&/g, '&'); @@ -47,7 +47,7 @@ function spawnGui() { var self = this; switch (e.type) { case "message": - var event = e.message.eventValue; + var event = e.message[2]; var keycode = event.keyCode; var character = String.fromCharCode(event.charCode); if (keycode === 37 /* left */) { @@ -105,7 +105,7 @@ function spawnModel() { handleEvent: function (e) { if (e.type === "message" && fieldCommand.isClassOf(e.message)) { - var command = e.message.detail; + var command = e.message[0]; if (command === "cursorLeft") { this.cursorPos--; if (this.cursorPos < 0) diff --git a/js/src/ack.js b/js/src/ack.js index 9db2da4..32057bc 100644 --- a/js/src/ack.js +++ b/js/src/ack.js @@ -5,7 +5,7 @@ var Dataspace = require('./dataspace.js').Dataspace; var Struct = require('./struct.js'); var Patch = require('./patch.js'); -var ack = Struct.makeStructureConstructor('ack', ['id']); +var ack = Struct.makeConstructor('ack', ['id']); function Ack(metaLevel, id) { this.metaLevel = metaLevel || 0; @@ -26,7 +26,7 @@ Ack.prototype.check = function (e) { if (!this.done) { if (e.type === 'message') { var m = Patch.stripAtMeta(e.message, this.metaLevel); - if (ack.isClassOf(m) && m.id === this.id) { + if (ack.isClassOf(m) && m[0] === this.id) { this.disarm(); this.done = true; } diff --git a/js/src/actor.js b/js/src/actor.js index 8e71383..ba963fc 100644 --- a/js/src/actor.js +++ b/js/src/actor.js @@ -19,14 +19,17 @@ function Actor(state, bootFn) { this.mux = new Mux.Mux(); this.boot = function() { - bootFn.call(this.state); - this.checkForTermination(); + var self = this; + withCurrentFacet(null, function () { + bootFn.call(self.state); + }); + self.checkForTermination(); }; } Actor.prototype.handleEvent = function(e) { this.facets.forEach(function (f) { - f.handleEvent(e); + withCurrentFacet(f, function () { f.handleEvent(e); }); }); this.checkForTermination(); }; @@ -56,14 +59,32 @@ function Facet(actor) { this.endpoints = Immutable.Map(); this.initBlocks = Immutable.List(); this.doneBlocks = Immutable.List(); + this.children = Immutable.Set(); + this.parent = Facet.current; +} + +Facet.current = null; + +function withCurrentFacet(facet, f) { + var previous = Facet.current; + Facet.current = facet; + var result; + try { + result = f(); + } catch (e) { + Facet.current = previous; + throw e; + } + Facet.current = previous; + return result; } Facet.prototype.handleEvent = function(e) { var facet = this; - this.endpoints.forEach(function(endpoint) { + facet.endpoints.forEach(function(endpoint) { endpoint.handlerFn.call(facet.actor.state, e); }); - this.refresh(); + facet.refresh(); }; Facet.prototype.addAssertion = function(assertionFn) { @@ -157,6 +178,9 @@ Facet.prototype.refresh = function() { Facet.prototype.completeBuild = function() { var facet = this; this.actor.addFacet(this); + if (this.parent) { + this.parent.children = this.parent.children.add(this); + } this.initBlocks.forEach(function(b) { b.call(facet.actor.state); }); }; @@ -169,8 +193,14 @@ Facet.prototype.terminate = function() { }); Dataspace.stateChange(aggregate); this.endpoints = Immutable.Map(); + if (this.parent) { + this.parent.children = this.parent.children.remove(this); + } this.actor.removeFacet(this); this.doneBlocks.forEach(function(b) { b.call(facet.actor.state); }); + this.children.forEach(function (child) { + child.terminate(); + }); }; //--------------------------------------------------------------------------- diff --git a/js/src/dataspace.js b/js/src/dataspace.js index 3cf5e9e..6234748 100644 --- a/js/src/dataspace.js +++ b/js/src/dataspace.js @@ -41,6 +41,7 @@ function Dataspace(bootFn) { // Class state and methods +Dataspace.noisy = false; Dataspace.stack = Immutable.List(); Dataspace.current = function () { @@ -140,7 +141,7 @@ Dataspace.prototype.asChild = function (pid, f, omitLivenessCheck) { Dataspace.prototype.kill = function (pid, exn) { if (exn && exn.stack) { console.log("Process exiting", pid, exn, exn.stack); - } else { + } else if (exn || Dataspace.noisy) { console.log("Process exiting", pid, exn); } var p = this.processTable.get(pid); @@ -222,53 +223,53 @@ Dataspace.prototype.interpretAction = function (pid, action) { var self = this; switch (action.type) { - case 'stateChange': - var oldMux = this.mux.shallowCopy(); - this.deliverPatches(oldMux, this.mux.updateStream(pid, action.patch)); - return true; + case 'stateChange': + var oldMux = this.mux.shallowCopy(); + this.deliverPatches(oldMux, this.mux.updateStream(pid, action.patch)); + return true; - case 'message': - if (Patch.observe.isClassOf(action.message)) { - console.warn('Process ' + pid + ' send message containing query', action.message); - } - if (pid !== 'meta' && Patch.atMeta.isClassOf(action.message)) { - Dataspace.send(action.message.assertion); - } else { - this.mux.routeMessage(action.message).forEach(function (pid) { - self.deliverEvent(pid, action); - }); - } - return true; + case 'message': + if (Patch.observe.isClassOf(action.message)) { + console.warn('Process ' + pid + ' send message containing query', action.message); + } + if (pid !== 'meta' && Patch.atMeta.isClassOf(action.message)) { + Dataspace.send(action.message[0]); + } else { + this.mux.routeMessage(action.message).forEach(function (pid) { + self.deliverEvent(pid, action); + }); + } + return true; - case 'spawn': - var oldMux = this.mux.shallowCopy(); - var p = { behavior: action.behavior }; - var pid = this.mux.nextPid; - this.processTable = this.processTable.set(pid, p); - var initialPatch = Patch.emptyPatch; - if (p.behavior.boot) { - initialPatch = this.asChild(pid, function () { return p.behavior.boot() }); - initialPatch = initialPatch || Patch.emptyPatch; - this.markRunnable(pid); - } - this.deliverPatches(oldMux, this.mux.addStream(initialPatch)); - return true; + case 'spawn': + var oldMux = this.mux.shallowCopy(); + var p = { behavior: action.behavior }; + var pid = this.mux.nextPid; + this.processTable = this.processTable.set(pid, p); + var initialPatch = Patch.emptyPatch; + if (p.behavior.boot) { + initialPatch = this.asChild(pid, function () { return p.behavior.boot() }); + initialPatch = initialPatch || Patch.emptyPatch; + this.markRunnable(pid); + } + this.deliverPatches(oldMux, this.mux.addStream(initialPatch)); + return true; - case 'terminate': - var oldMux = this.mux.shallowCopy(); - this.deliverPatches(oldMux, this.mux.removeStream(pid)); - console.log("Process exit complete", pid); - this.processTable = this.processTable.remove(pid); - return true; + case 'terminate': + var oldMux = this.mux.shallowCopy(); + this.deliverPatches(oldMux, this.mux.removeStream(pid)); + if (Dataspace.noisy) console.log("Process exit complete", pid); + this.processTable = this.processTable.remove(pid); + return true; - case 'terminateDataspace': - Dataspace.exit(); - return false; + case 'terminateDataspace': + Dataspace.exit(); + return false; - default: - var exn = new Error("Action type " + action.type + " not understood"); - exn.action = action; - throw exn; + default: + var exn = new Error("Action type " + action.type + " not understood"); + exn.action = action; + throw exn; } }; diff --git a/js/src/demand-matcher.js b/js/src/demand-matcher.js index 73f6729..c6cdd55 100644 --- a/js/src/demand-matcher.js +++ b/js/src/demand-matcher.js @@ -3,34 +3,78 @@ var Trie = require('./trie.js'); var Patch = require('./patch.js'); var Util = require('./util.js'); -function DemandMatcher(demandSpec, supplySpec, options) { +function ensureMatchingProjectionNames(specs) { + if (!(specs.length > 0)) { + throw new Error("Syndicate: DemandMatcher needs at least one spec"); + } + + var names = null; + specs.forEach(function (spec) { + if (names === null) { + names = Trie.projectionNames(spec); + } else { + if (JSON.stringify(names) !== JSON.stringify(Trie.projectionNames(spec))) { + throw new Error("Syndicate: DemandMatcher needs identical capture names"); + } + } + }); + return names; +} + +function defaultHandler(side, movement) { + return function (captures) { + console.error("Syndicate: Unhandled "+movement+" in "+side, captures); + }; +} + +function DemandMatcher(demandSpecs, supplySpecs, options) { options = Util.extend({ metaLevel: 0, - onDemandIncrease: function (captures) { - console.error("Syndicate: Unhandled increase in demand", captures); - }, - onSupplyDecrease: function (captures) { - console.error("Syndicate: Unhandled decrease in supply", captures); - } + demandMetaLevel: null, + supplyMetaLevel: null, + onDemandIncrease: defaultHandler('demand', 'increase'), + onDemandDecrease: function (captures) {}, + onSupplyIncrease: function (captures) {}, + onSupplyDecrease: defaultHandler('supply', 'decrease') }, options); - this.metaLevel = options.metaLevel; + + this.demandProjectionNames = ensureMatchingProjectionNames(demandSpecs); + this.supplyProjectionNames = ensureMatchingProjectionNames(supplySpecs); + + this.demandSpecs = demandSpecs; + this.supplySpecs = supplySpecs; + + this.demandPatterns = demandSpecs.map(function (s) { return Trie.projectionToPattern(s); }); + this.supplyPatterns = supplySpecs.map(function (s) { return Trie.projectionToPattern(s); }); + + this.demandMetaLevel = + (options.demandMetaLevel === null) ? options.metaLevel : options.demandMetaLevel; + this.supplyMetaLevel = + (options.supplyMetaLevel === null) ? options.metaLevel : options.supplyMetaLevel; + + function metaWrap(n) { + return function (s) { return Patch.prependAtMeta(s, n); }; + } + this.demandProjections = demandSpecs.map(metaWrap(this.demandMetaLevel)); + this.supplyProjections = supplySpecs.map(metaWrap(this.supplyMetaLevel)); + this.onDemandIncrease = options.onDemandIncrease; + this.onDemandDecrease = options.onDemandDecrease; + this.onSupplyIncrease = options.onSupplyIncrease; this.onSupplyDecrease = options.onSupplyDecrease; - this.demandSpec = demandSpec; - this.supplySpec = supplySpec; - this.demandPattern = Trie.projectionToPattern(demandSpec); - this.supplyPattern = Trie.projectionToPattern(supplySpec); - this.demandProjection = Patch.prependAtMeta(demandSpec, this.metaLevel); - this.supplyProjection = Patch.prependAtMeta(supplySpec, this.metaLevel); - this.demandProjectionNames = Trie.projectionNames(this.demandProjection); - this.supplyProjectionNames = Trie.projectionNames(this.supplyProjection); + this.currentDemand = Immutable.Set(); this.currentSupply = Immutable.Set(); } DemandMatcher.prototype.boot = function () { - return Patch.sub(this.demandPattern, this.metaLevel) - .andThen(Patch.sub(this.supplyPattern, this.metaLevel)); + var p = Patch.emptyPatch; + function extend(ml) { + return function (pat) { p = p.andThen(Patch.sub(pat, ml)); }; + } + this.demandPatterns.forEach(extend(this.demandMetaLevel)); + this.supplyPatterns.forEach(extend(this.supplyMetaLevel)); + return p; }; DemandMatcher.prototype.handleEvent = function (e) { @@ -39,26 +83,29 @@ DemandMatcher.prototype.handleEvent = function (e) { } }; +DemandMatcher.prototype.extractKeys = function (trie, projections, keyCount, whichSide) { + var ks = Immutable.Set(); + projections.forEach(function (proj) { + var moreKs = Trie.trieKeys(Trie.project(trie, proj), keyCount); + if (!moreKs) { + throw new Error("Syndicate: wildcard "+whichSide+" detected:\n" + + JSON.stringify(proj) + "\n" + + Trie.prettyTrie(trie)); + } + ks = ks.union(moreKs); + }); + return ks; +}; + DemandMatcher.prototype.handlePatch = function (p) { var self = this; var dN = self.demandProjectionNames.length; var sN = self.supplyProjectionNames.length; - var addedDemand = Trie.trieKeys(Trie.project(p.added, self.demandProjection), dN); - var removedDemand = Trie.trieKeys(Trie.project(p.removed, self.demandProjection), dN); - var addedSupply = Trie.trieKeys(Trie.project(p.added, self.supplyProjection), sN); - var removedSupply = Trie.trieKeys(Trie.project(p.removed, self.supplyProjection), sN); - - if (addedDemand === null) { - throw new Error("Syndicate: wildcard demand detected:\n" + - self.demandSpec + "\n" + - p.pretty()); - } - if (addedSupply === null) { - throw new Error("Syndicate: wildcard supply detected:\n" + - self.supplySpec + "\n" + - p.pretty()); - } + var addedDemand = this.extractKeys(p.added, self.demandProjections, dN, 'demand'); + var removedDemand = this.extractKeys(p.removed, self.demandProjections, dN, 'demand'); + var addedSupply = this.extractKeys(p.added, self.supplyProjections, sN, 'supply'); + var removedSupply = this.extractKeys(p.removed, self.supplyProjections, sN, 'supply'); self.currentSupply = self.currentSupply.union(addedSupply); self.currentDemand = self.currentDemand.subtract(removedDemand); @@ -68,6 +115,17 @@ DemandMatcher.prototype.handlePatch = function (p) { self.onSupplyDecrease(Trie.captureToObject(captures, self.supplyProjectionNames)); } }); + addedSupply.forEach(function (captures) { + if (!self.currentDemand.has(captures)) { + self.onSupplyIncrease(Trie.captureToObject(captures, self.supplyProjectionNames)); + } + }); + + removedDemand.forEach(function (captures) { + if (self.currentSupply.has(captures)) { + self.onDemandDecrease(Trie.captureToObject(captures, self.demandProjectionNames)); + } + }); addedDemand.forEach(function (captures) { if (!self.currentSupply.has(captures)) { self.onDemandIncrease(Trie.captureToObject(captures, self.demandProjectionNames)); diff --git a/js/src/dom-driver.js b/js/src/dom-driver.js index cc31969..e63dcfa 100644 --- a/js/src/dom-driver.js +++ b/js/src/dom-driver.js @@ -10,14 +10,14 @@ var Dataspace = Dataspace_.Dataspace; var __ = Dataspace_.__; var _$ = Dataspace_._$; -var DOM = Struct.makeStructureConstructor('DOM', ['selector', 'fragmentClass', 'fragmentSpec']); +var DOM = Struct.makeConstructor('DOM', ['selector', 'fragmentClass', 'fragmentSpec']); function spawnDOMDriver(domWrapFunction, jQueryWrapFunction) { domWrapFunction = domWrapFunction || DOM; var spec = domWrapFunction(_$('selector'), _$('fragmentClass'), _$('fragmentSpec')); Dataspace.spawn( - new DemandMatcher(spec, - Patch.advertise(spec), // TODO: are the embedded captures problematic here? If not, why not? + new DemandMatcher([spec], + [Patch.advertise(spec)], { onDemandIncrease: function (c) { Dataspace.spawn(new DOMFragment(c.selector, @@ -106,7 +106,7 @@ DOMFragment.prototype.interpretSpec = function (spec) { var attrs = hasAttrs ? spec[1] : {}; var kidIndex = hasAttrs ? 2 : 1; - // Wow! Such XSS! Many hacks! So vulnerability! Amaze! + // TODO: Wow! Such XSS! Many hacks! So vulnerability! Amaze! var n = document.createElement(tagName); for (var i = 0; i < attrs.length; i++) { n.setAttribute(attrs[i][0], attrs[i][1]); @@ -124,6 +124,9 @@ DOMFragment.prototype.buildNodes = function () { var self = this; var nodes = []; $(self.selector).each(function (index, domNode) { + if (!(self.fragmentSpec instanceof Syndicate.Seal)) { + throw new Error("DOM fragmentSpec not contained in a Syndicate.Seal: " + JSON.stringify(self.fragmentSpec)); + } var n = self.interpretSpec(self.fragmentSpec.sealContents); if ('classList' in n) { n.classList.add(self.fragmentClass); diff --git a/js/src/jquery-driver.js b/js/src/jquery-driver.js index 9a7c883..8f6ab2d 100644 --- a/js/src/jquery-driver.js +++ b/js/src/jquery-driver.js @@ -8,14 +8,14 @@ var Dataspace = Dataspace_.Dataspace; var __ = Dataspace_.__; var _$ = Dataspace_._$; -var jQueryEvent = Struct.makeStructureConstructor('jQueryEvent', ['selector', 'eventName', 'eventValue']); +var jQueryEvent = Struct.makeConstructor('jQueryEvent', ['selector', 'eventName', 'eventValue']); function spawnJQueryDriver(baseSelector, metaLevel, wrapFunction) { metaLevel = metaLevel || 0; wrapFunction = wrapFunction || jQueryEvent; Dataspace.spawn( - new DemandMatcher(Patch.observe(wrapFunction(_$('selector'), _$('eventName'), __)), - Patch.advertise(wrapFunction(_$('selector'), _$('eventName'), __)), + new DemandMatcher([Patch.observe(wrapFunction(_$('selector'), _$('eventName'), __))], + [Patch.advertise(wrapFunction(_$('selector'), _$('eventName'), __))], { metaLevel: metaLevel, onDemandIncrease: function (c) { diff --git a/js/src/main.js b/js/src/main.js index 5cf862b..f2130ee 100644 --- a/js/src/main.js +++ b/js/src/main.js @@ -29,8 +29,7 @@ module.exports.Ack = require('./ack.js').Ack; module.exports.RandomID = require('./randomid.js'); module.exports.DOM = require("./dom-driver.js"); module.exports.JQuery = require("./jquery-driver.js"); -// module.exports.RoutingTableWidget = require("./routing-table-widget.js"); -// module.exports.WebSocket = require("./websocket-driver.js"); +module.exports.WakeDetector = require("./wake-detector-driver.js"); module.exports.Reflect = require("./reflect.js"); module.exports.Patch = require("./patch.js"); diff --git a/js/src/mux.js b/js/src/mux.js index ff4ba4e..5058bf1 100644 --- a/js/src/mux.js +++ b/js/src/mux.js @@ -45,7 +45,7 @@ Mux.prototype.updateStream = function (pid, unclampedPatch) { }; var atMetaEverything = Trie.compilePattern(true, Patch.atMeta(Trie.__)); -var atMetaBranchKeys = Immutable.List([[Patch.atMeta.meta.arguments.length, Patch.atMeta.meta]]); +var atMetaBranchKeys = Immutable.List([[Patch.atMeta.meta.arity, Patch.atMeta.meta]]); var onlyMeta = Trie.trieSuccess(Immutable.Set.of("meta")); function echoCancelledTrie(t) { @@ -60,8 +60,7 @@ function computeEvents(oldMux, newMux, updateStreamResult) { var deltaAggregate = updateStreamResult.deltaAggregate; var deltaAggregateNoEcho = (actingPid === "meta") ? delta // because echo-cancellation means that meta-SCNs are always new information - : new Patch.Patch(Trie.triePruneBranch(deltaAggregate.added, atMetaBranchKeys), - Trie.triePruneBranch(deltaAggregate.removed, atMetaBranchKeys)); + : deltaAggregate.withoutAtMeta(); var oldRoutingTable = oldMux.routingTable; var newRoutingTable = newMux.routingTable; var affectedPids = @@ -95,8 +94,7 @@ function computeEvents(oldMux, newMux, updateStreamResult) { function computeAffectedPids(routingTable, delta) { var cover = Trie._union(delta.added, delta.removed); - routingTable = - Trie.trieStep(routingTable, Patch.observe.meta.arguments.length, Patch.observe.meta); + routingTable = Trie.trieStep(routingTable, Patch.observe.meta.arity, Patch.observe.meta); return Trie.matchTrie(cover, routingTable, Immutable.Set(), function (v1, v2, acc) { return acc.union(v2); }); } diff --git a/js/src/patch.js b/js/src/patch.js index 60973f3..8ee3339 100644 --- a/js/src/patch.js +++ b/js/src/patch.js @@ -16,9 +16,9 @@ var emptyPatch = new Patch(Trie.emptyTrie, Trie.emptyTrie); var removeEverythingPatch = new Patch(Trie.emptyTrie, Trie.compilePattern(true, __)); var trueLabel = Trie.trieSuccess(true); -var observe = Struct.makeStructureConstructor('observe', ['assertion']); -var atMeta = Struct.makeStructureConstructor('atMeta', ['assertion']); -var advertise = Struct.makeStructureConstructor('advertise', ['assertion']); +var observe = Struct.makeConstructor('observe', ['assertion']); +var atMeta = Struct.makeConstructor('at-meta', ['assertion']); +var advertise = Struct.makeConstructor('advertise', ['assertion']); function prependAtMeta(p, level) { while (level--) { @@ -30,7 +30,7 @@ function prependAtMeta(p, level) { function stripAtMeta(p, level) { while (level--) { if (atMeta.isClassOf(p)) { - p = p.assertion; + p = p[0]; } else { return null; } @@ -97,11 +97,11 @@ Patch.prototype.isNonEmpty = function () { }; Patch.prototype.hasAdded = function () { - return this.added !== Trie.emptyTrie; + return !Trie.is_emptyTrie(this.added); }; Patch.prototype.hasRemoved = function () { - return this.removed !== Trie.emptyTrie; + return !Trie.is_emptyTrie(this.removed); }; Patch.prototype.lift = function () { @@ -196,7 +196,7 @@ function computePatch(oldBase, newBase) { } function biasedIntersection(object, subject) { - subject = Trie.trieStep(subject, observe.meta.arguments.length, observe.meta); + subject = Trie.trieStep(subject, observe.meta.arity, observe.meta); return Trie.intersect(object, subject, function (v1, v2) { return Trie.trieSuccess(v1); }); } @@ -226,6 +226,27 @@ Patch.prototype.pretty = function () { return ("<<<<<<<< Removed:\n" + Trie.prettyTrie(this.removed) + "\n" + "======== Added:\n" + Trie.prettyTrie(this.added) + "\n" + ">>>>>>>>"); +}; + +// Completely ignores success-values in t. +Patch.prototype.prunedBy = function (t) { + return new Patch(Trie.subtract(this.added, t, function (v1, v2) { return Trie.emptyTrie; }), + Trie.subtract(this.removed, t, function (v1, v2) { return Trie.emptyTrie; })); +}; + +var atMetaEverything = Trie.compilePattern(true, atMeta.pattern); +Patch.prototype.withoutAtMeta = function () { + return this.prunedBy(atMetaEverything); +}; + +/////////////////////////////////////////////////////////////////////////// + +Patch.prototype.toJSON = function () { + return [Trie.trieToJSON(this.added), Trie.trieToJSON(this.removed)]; +}; + +function fromJSON(j) { + return new Patch(Trie.trieFromJSON(j[0]), Trie.trieFromJSON(j[1])); } /////////////////////////////////////////////////////////////////////////// @@ -251,3 +272,5 @@ module.exports.unpub = unpub; module.exports.patchSeq = patchSeq; module.exports.computePatch = computePatch; module.exports.biasedIntersection = biasedIntersection; + +module.exports.fromJSON = fromJSON; diff --git a/js/src/struct.js b/js/src/struct.js index fddb12b..935dfce 100644 --- a/js/src/struct.js +++ b/js/src/struct.js @@ -1,61 +1,106 @@ "use strict"; // "Structures": Simple named-tuple-like records. -// TODO: shore up $SyndicateMeta$, making it a proper object var Immutable = require("immutable"); var $Special = require('./special.js'); -/* Defined here rather than in trie.js because we need it in makeStructureConstructor. */ +/* Defined here rather than in trie.js because we need it in makeConstructor. */ var __ = new $Special("wildcard"); /* wildcard marker */ -function instantiateStructure($SyndicateMeta$, argvals) { - var result = {"$SyndicateMeta$": $SyndicateMeta$}; - var argnames = $SyndicateMeta$.arguments; - for (var i = 0; i < argnames.length; i++) { - result[argnames[i]] = argvals[i]; - } - return result; -} +function StructureType(label, arity) { + this.label = label; + this.arity = arity; + this.pattern = this.instantiate(Immutable.Repeat(__, arity).toArray()); -function makeStructureConstructor(label, argumentNames) { - var $SyndicateMeta$ = { - label: label, - arguments: argumentNames + var self = this; + this.ctor = function () { + return self.instantiate(Array.prototype.slice.call(arguments)); }; - var ctor = function() { - return instantiateStructure($SyndicateMeta$, arguments); - }; - ctor.prototype._get = function(i) { return this[this.$SyndicateMeta$.arguments[i]]; }; - ctor.meta = $SyndicateMeta$; - ctor.isClassOf = function (v) { return v && v.$SyndicateMeta$ === $SyndicateMeta$; }; - ctor.pattern = ctor.apply(null, Immutable.Repeat(__, argumentNames.length).toArray()); - return ctor; + this.ctor.meta = this; + this.ctor.pattern = this.pattern; + this.ctor.isClassOf = function (v) { return self.isClassOf(v); }; } -function isSyndicateMeta(m) { - // TODO: include more structure in $SyndicateMeta$ objects to make - // this judgement less sloppy. - return m && m.label && Array.isArray(m.arguments); +function makeConstructor(label, fieldNames) { + return new StructureType(label, fieldNames.length).ctor; } -function isStructure(s) { - return (s !== null) && (typeof s === 'object') && ("$SyndicateMeta$" in s); -} +StructureType.prototype.equals = function (other) { + if (!(other instanceof StructureType)) return false; + return this.arity === other.arity && this.label === other.label; +}; -function structureToArray(s, excludeLabel) { - var result = excludeLabel ? [] : [s.$SyndicateMeta$.label]; - var args = s.$SyndicateMeta$.arguments; - for (var i = 0; i < args.length; i++) { - result.push(s[args[i]]); +StructureType.prototype.instantiate = function (fields) { + return new Structure(this, fields); +}; + +StructureType.prototype.isClassOf = function (v) { + return v && (v instanceof Structure) && (v.meta.equals(this)); +}; + +function Structure(meta, fields) { + if (!isStructureType(meta)) { + throw new Error("Structure: requires structure type"); } - return result; + if (fields.length !== meta.arity) { + throw new Error("Structure: cannot instantiate meta "+JSON.stringify(meta.label)+ + " expecting "+meta.arity+" fields with "+fields.length+" fields"); + } + this.meta = meta; + this.length = meta.arity; + this.fields = fields.slice(0); + for (var i = 0; i < fields.length; i++) { + this[i] = fields[i]; + } +} + +function reviveStructs(j) { + if (Array.isArray(j)) { + return j.map(reviveStructs); + } + + if ((j !== null) && typeof j === 'object') { + if ((typeof j['@type'] === 'string') && Array.isArray(j['fields'])) { + return (new StructureType(j['@type'], j['fields'].length)).instantiate(j['fields']); + } else { + for (var k in j) { + if (Object.prototype.hasOwnProperty.call(j, k)) { + j[k] = reviveStructs(j[k]); + } + } + return j; + } + } + + return j; +} + +function reviver(k, v) { + if (k === '') { + return reviveStructs(v); + } + return v; +}; + +Structure.prototype.toJSON = function () { + return { '@type': this.meta.label, 'fields': this.fields }; +}; + +function isStructureType(v) { + return v && (v instanceof StructureType); +} + +function isStructure(v) { + return v && (v instanceof Structure); } /////////////////////////////////////////////////////////////////////////// module.exports.__ = __; -module.exports.instantiateStructure = instantiateStructure; -module.exports.makeStructureConstructor = makeStructureConstructor; -module.exports.isSyndicateMeta = isSyndicateMeta; +module.exports.StructureType = StructureType; +module.exports.makeConstructor = makeConstructor; +module.exports.Structure = Structure; +module.exports.reviveStructs = reviveStructs; +module.exports.reviver = reviver; +module.exports.isStructureType = isStructureType; module.exports.isStructure = isStructure; -module.exports.structureToArray = structureToArray; diff --git a/js/src/trie.js b/js/src/trie.js index bca7154..629c060 100644 --- a/js/src/trie.js +++ b/js/src/trie.js @@ -186,11 +186,10 @@ function compilePattern(v, p) { } if (Struct.isStructure(p)) { - var args = p.$SyndicateMeta$.arguments; - for (var i = args.length - 1; i >= 0; i--) { - acc = walk(p[args[i]], acc); + for (var i = p.meta.arity - 1; i >= 0; i--) { + acc = walk(p[i], acc); } - return rseq(args.length, p.$SyndicateMeta$, acc); + return rseq(p.meta.arity, p.meta, acc); } if (p instanceof $Embedded) { @@ -222,13 +221,10 @@ function matchPattern(v, p) { if (p === __) return; - if (Struct.isStructure(p) - && Struct.isStructure(v) - && (p.$SyndicateMeta$ === v.$SyndicateMeta$)) + if (Struct.isStructure(p) && Struct.isStructure(v) && (p.meta.equals(v.meta))) { - var args = p.$SyndicateMeta$.arguments; - for (var i = 0; i < args.length; i++) { - walk(v[args[i]], p[args[i]]); + for (var i = 0; i < p.meta.arity; i++) { + walk(v[i], p[i]); } return; } @@ -431,8 +427,8 @@ function matchValue(r, v, failureResultOpt) { r = rlookup(r, v.length, SOA); vs = Immutable.List(v).concat(vs); } else if (Struct.isStructure(v)) { - r = rlookup(r, v.$SyndicateMeta$.arguments.length, v.$SyndicateMeta$); - vs = Immutable.List(Struct.structureToArray(v, true)).concat(vs); + r = rlookup(r, v.meta.arity, v.meta); + vs = Immutable.List(v.fields).concat(vs); } else { r = rlookup(r, 0, v); } @@ -501,17 +497,19 @@ function appendTrie(m, mTailFn) { } } -function triePruneBranch(m, arityKeys) { - if (arityKeys.isEmpty()) return emptyTrie; - if (!(m instanceof $Branch)) return m; - var arityKey = arityKeys.first(); - var rest = arityKeys.shift(); - var arity = arityKey[0]; - var key = arityKey[1]; - m = rcopybranch(m); - rupdate_inplace(m, arity, key, triePruneBranch(rlookup(m, arity, key), rest)); - return collapse(m); -} +// DANGEROUS: prefer subtract() instead. +// +// function triePruneBranch(m, arityKeys) { +// if (arityKeys.isEmpty()) return emptyTrie; +// if (!(m instanceof $Branch)) return m; +// var arityKey = arityKeys.first(); +// var rest = arityKeys.shift(); +// var arity = arityKey[0]; +// var key = arityKey[1]; +// m = rcopybranch(m); +// rupdate_inplace(m, arity, key, triePruneBranch(rlookup(m, arity, key), rest)); +// return collapse(m); +// } function trieStep(m, arity, key) { if (typeof key === 'undefined') { @@ -550,8 +548,7 @@ function projectionNames(p) { } if (Struct.isStructure(p)) { - var args = p.$SyndicateMeta$.arguments; - for (var i = 0; i < args.length; i++) walk(p[args[i]]); + for (var i = 0; i < p.meta.arity; i++) walk(p[i]); return; } } @@ -576,12 +573,11 @@ function projectionToPattern(p) { } if (Struct.isStructure(p)) { - var result = {"$SyndicateMeta$": p.$SyndicateMeta$}; - var args = p.$SyndicateMeta$.arguments; - for (var i = 0; i < args.length; i++) { - result[args[i]] = walk(p[args[i]]); + var resultFields = []; + for (var i = 0; i < p.meta.arity; i++) { + resultFields[i] = walk(p[i]); } - return result; + return new Struct.Structure(p.meta, resultFields); } return p; @@ -652,15 +648,13 @@ function projectMany(t, wholeSpecs, projectSuccessOpt, combinerOpt) { } if (Struct.isStructure(spec)) { - var arity = spec.$SyndicateMeta$.arguments.length; - var key = spec.$SyndicateMeta$; var intermediate = walk(isCapturing, - rlookup(t, arity, key), - Immutable.List(Struct.structureToArray(spec, true)), + rlookup(t, spec.meta.arity, spec.meta), + Immutable.List(spec.fields), function (intermediate) { return walk(isCapturing, intermediate, specsRest, kont); }); - return isCapturing ? rseq(arity, key, intermediate) : intermediate; + return isCapturing ? rseq(spec.meta.arity, spec.meta, intermediate) : intermediate; } if (Array.isArray(spec)) { @@ -687,7 +681,7 @@ function reconstructSequence(key, items) { if (key === SOA) { return items.toArray(); } else { - return Struct.instantiateStructure(key, items); + return key.instantiate(items); } } @@ -722,7 +716,7 @@ function trieKeys(m, takeCount0) { if (result === false) return false; // break out of iteration var piece; - if (Struct.isSyndicateMeta(key) || key === SOA) { // TODO: this is sloppy + if (Struct.isStructureType(key) || key === SOA) { piece = walk(k, arity, Immutable.List(), function (items, m1) { var item = reconstructSequence(key, items); return walk(m1, takeCount - 1, valsRev.unshift(item), kont); @@ -798,10 +792,11 @@ function prettyTrie(m, initialIndent) { } acc.push(" "); if (key === SOA) key = '<' + arity + '>'; - else if (Struct.isSyndicateMeta(key)) key = key.label + '<' + arity + '>'; + else if (Struct.isStructureType(key)) key = key.label + '<' + arity + '>'; else if (key instanceof $Special) key = key.name; - else if (typeof key === 'undefined') key = 'undefined'; - else key = JSON.stringify(key); + else key = JSON.stringify(key); + + if (typeof key === 'undefined') key = 'undefined'; acc.push(key); walk(i + key.length + 1, k); }); @@ -815,6 +810,78 @@ function prettyTrie(m, initialIndent) { /////////////////////////////////////////////////////////////////////////// +function parenTypeToString(key) { + if (Struct.isStructureType(key)) { + return ':' + key.label; + } else { + return 'L'; + } +} + +function stringToParenType(arity, key) { + if (key[0] === ':') { + return new Struct.StructureType(key.slice(1), arity); + } else if (key === 'L') { + return SOA; + } + throw new Error("Unsupported JSON trie paren type: "+key); +} + +function trieToJSON(t) { + if (is_emptyTrie(t)) { return []; } + if (t instanceof $Success) { return [true]; } // TODO: consider generalizing + + // It's a $Branch. + + var jParens = []; + var jAtoms = []; + t.edges.forEach(function (keymap, arity) { + keymap.forEach(function (k, key) { + var jk = trieToJSON(k); + if (Struct.isStructureType(key) || key === SOA) { + jParens.push([arity, parenTypeToString(key), jk]); + } else { + jAtoms.push([key, jk]); + } + }); + }); + return [jParens, trieToJSON(t.wild), jAtoms]; +} + +function badJSON(j) { + die("Cannot deserialize JSON trie: " + JSON.stringify(j)); +} + +function trieFromJSON(j) { + return decode(j); + + function decode(j) { + if (!Array.isArray(j)) badJSON(j); + + switch (j.length) { + case 0: return emptyTrie; + case 1: return rsuccess(true); // TODO: consider generalizing + case 3: { + var result = rcopybranch(expand(rwild(decode(j[1])))); + j[0].forEach(function (entry) { + var arity = entry[0]; + if (typeof arity !== 'number') badJSON(j); + var key = stringToParenType(arity, entry[1]); + rupdate_inplace(result, arity, key, decode(entry[2])); + }); + j[2].forEach(function (entry) { + var key = entry[0]; + rupdate_inplace(result, 0, key, decode(entry[1])); + }); + return collapse(result); + } + default: badJSON(j); + } + } +} + +/////////////////////////////////////////////////////////////////////////// + module.exports.__ = __; module.exports.SOA = SOA; module.exports.$Capture = $Capture; @@ -833,7 +900,7 @@ module.exports.subtract = subtract; module.exports.matchValue = matchValue; module.exports.matchTrie = matchTrie; module.exports.appendTrie = appendTrie; -module.exports.triePruneBranch = triePruneBranch; +// module.exports.triePruneBranch = triePruneBranch; module.exports.trieStep = trieStep; module.exports.trieSuccess = rsuccess; module.exports.relabel = relabel; @@ -847,6 +914,8 @@ module.exports.captureToObject = captureToObject; module.exports.trieKeysToObjects = trieKeysToObjects; module.exports.projectObjects = projectObjects; module.exports.prettyTrie = prettyTrie; +module.exports.trieToJSON = trieToJSON; +module.exports.trieFromJSON = trieFromJSON; // For testing module.exports._testing = { diff --git a/js/src/wake-detector-driver.js b/js/src/wake-detector-driver.js new file mode 100644 index 0000000..1534e60 --- /dev/null +++ b/js/src/wake-detector-driver.js @@ -0,0 +1,47 @@ +"use strict"; +// Wake detector - notices when something (such as +// suspension/sleeping!) has caused periodic activities to be +// interrupted, and warns others about it +// Inspired by http://blog.alexmaccaw.com/javascript-wake-event + +var Patch = require("./patch.js"); +var Struct = require('./struct.js'); + +var Dataspace_ = require("./dataspace.js"); +var Dataspace = Dataspace_.Dataspace; +var __ = Dataspace_.__; +var _$ = Dataspace_._$; + +var wakeEvent = Struct.makeConstructor('wakeEvent', []); + +function spawnWakeDetector(periodOpt) { + Dataspace.spawn(new WakeDetector(periodOpt)); +} + +function WakeDetector(periodOpt) { + this.period = periodOpt || 10000; + this.mostRecentTrigger = +(new Date()); + this.timerId = null; +} + +WakeDetector.prototype.boot = function () { + var self = this; + this.timerId = setInterval(Dataspace.wrap(function () { self.trigger(); }), this.period); + return Patch.pub(wakeEvent()); +}; + +WakeDetector.prototype.handleEvent = function (e) {}; + +WakeDetector.prototype.trigger = function () { + var now = +(new Date()); + if (now - this.mostRecentTrigger > this.period * 1.5) { + Dataspace.send(wakeEvent()); + } + this.mostRecentTrigger = now; +}; + +/////////////////////////////////////////////////////////////////////////// + +module.exports.spawnWakeDetector = spawnWakeDetector; +module.exports.WakeDetector = WakeDetector; +module.exports.wakeEvent = wakeEvent; diff --git a/js/test/test-patch.js b/js/test/test-patch.js index 4fe7ba6..6bca0c0 100644 --- a/js/test/test-patch.js +++ b/js/test/test-patch.js @@ -40,23 +40,23 @@ describe('basic patch compilation', function () { [' <2> 1 2 {true}'], [' ::: nothing']); checkPrettyPatch(Patch.assert([1, 2], 1), - [' atMeta<1> <2> 1 2 {true}'], + [' at-meta<1> <2> 1 2 {true}'], [' ::: nothing']); checkPrettyPatch(Patch.assert([1, 2], 2), - [' atMeta<1> atMeta<1> <2> 1 2 {true}'], + [' at-meta<1> at-meta<1> <2> 1 2 {true}'], [' ::: nothing']); checkPrettyPatch(Patch.sub([1, 2], 0), [' observe<1> <2> 1 2 {true}'], [' ::: nothing']); checkPrettyPatch(Patch.sub([1, 2], 1), - [' atMeta<1> observe<1> <2> 1 2 {true}', - ' observe<1> atMeta<1> <2> 1 2 {true}'], + [' at-meta<1> observe<1> <2> 1 2 {true}', + ' observe<1> at-meta<1> <2> 1 2 {true}'], [' ::: nothing']); checkPrettyPatch(Patch.sub([1, 2], 2), - [' atMeta<1> atMeta<1> observe<1> <2> 1 2 {true}', - ' observe<1> atMeta<1> <2> 1 2 {true}', - ' observe<1> atMeta<1> atMeta<1> <2> 1 2 {true}'], + [' at-meta<1> at-meta<1> observe<1> <2> 1 2 {true}', + ' observe<1> at-meta<1> <2> 1 2 {true}', + ' observe<1> at-meta<1> at-meta<1> <2> 1 2 {true}'], [' ::: nothing']); }); }); @@ -111,14 +111,14 @@ describe('patch sequencing', function () { describe('patch lifting', function () { it('should basically work', function () { checkPrettyPatch(Patch.assert([1, 2]).lift(), - [' atMeta<1> <2> 1 2 {true}'], + [' at-meta<1> <2> 1 2 {true}'], [' ::: nothing']); checkPrettyPatch(Patch.sub([1, 2]).lift(), - [' atMeta<1> observe<1> <2> 1 2 {true}'], + [' at-meta<1> observe<1> <2> 1 2 {true}'], [' ::: nothing']); checkPrettyPatch(Patch.assert([1, 2]).andThen(Patch.assert(Patch.atMeta([1, 2]))).lift(), - [' atMeta<1> atMeta<1> <2> 1 2 {true}', - ' <2> 1 2 {true}'], + [' at-meta<1> at-meta<1> <2> 1 2 {true}', + ' <2> 1 2 {true}'], [' ::: nothing']); }); }); diff --git a/js/test/test-route.js b/js/test/test-route.js index ef1d7dc..5e5b6c1 100644 --- a/js/test/test-route.js +++ b/js/test/test-route.js @@ -402,7 +402,7 @@ describe("calls to matchPattern", function () { }); it("matches structures", function () { - var ctor = Struct.makeStructureConstructor('foo', ['bar', 'zot']); + var ctor = Struct.makeConstructor('foo', ['bar', 'zot']); expect(r.matchPattern(ctor(123, 234), ctor(r._$("bar"), r._$("zot")))) .to.eql({ bar: 123, zot: 234, length: 2 }); // Previously, structures were roughly the same as arrays: @@ -466,75 +466,76 @@ describe('intersect', function () { }); }); -describe('triePruneBranch', function () { - it('should not affect empty trie', function () { - checkPrettyTrie(r.triePruneBranch(r.emptyTrie, Immutable.List([])), [' ::: nothing']); - checkPrettyTrie(r.triePruneBranch(r.emptyTrie, Immutable.List([r.SOA])), [' ::: nothing']); - checkPrettyTrie(r.triePruneBranch(r.emptyTrie, Immutable.List(["x"])), [' ::: nothing']); - checkPrettyTrie(r.triePruneBranch(r.emptyTrie, Immutable.List([r.SOA, "x"])), [' ::: nothing']); - }); +// describe('triePruneBranch', function () { +// it('should not affect empty trie', function () { +// checkPrettyTrie(r.triePruneBranch(r.emptyTrie, Immutable.List([])), [' ::: nothing']); +// checkPrettyTrie(r.triePruneBranch(r.emptyTrie, Immutable.List([r.SOA])), [' ::: nothing']); +// checkPrettyTrie(r.triePruneBranch(r.emptyTrie, Immutable.List(["x"])), [' ::: nothing']); +// checkPrettyTrie(r.triePruneBranch(r.emptyTrie, Immutable.List([r.SOA, "x"])), [' ::: nothing']); +// }); +// +// it('should leave a hole in a full trie', function () { +// var full = r.compilePattern(true, r.__); +// checkPrettyTrie(r.triePruneBranch(full, Immutable.List([])), [' ::: nothing']); +// checkPrettyTrie(r.triePruneBranch(full, Immutable.List([[0, r.SOA]])), +// [' ★ {true}', +// ' <0> ::: nothing']); +// checkPrettyTrie(r.triePruneBranch(full, Immutable.List([[0, "x"]])), +// [' ★ {true}', +// ' "x" ::: nothing']); +// checkPrettyTrie(r.triePruneBranch(full, Immutable.List([[2, r.SOA], [0, "x"]])), +// [' ★ {true}', +// ' <2> ★ ★ {true}', +// ' "x" ::: nothing']); +// }); +// +// it('should prune in a finite tree and leave the rest alone', function () { +// var A = r.compilePattern(true, ["y"]) +// var B = r.union(r.compilePattern(true, ["x"]), A); +// var C = r.union(r.compilePattern(true, "z"), B); +// checkPrettyTrie(r.triePruneBranch(A, Immutable.List([])), [' ::: nothing']); +// checkPrettyTrie(r.triePruneBranch(B, Immutable.List([])), [' ::: nothing']); +// checkPrettyTrie(r.triePruneBranch(C, Immutable.List([])), [' ::: nothing']); +// checkPrettyTrie(r.triePruneBranch(A, Immutable.List([[0, "z"]])), [' <1> "y" {true}']); +// checkPrettyTrie(r.triePruneBranch(B, Immutable.List([[0, "z"]])), [' <1> "x" {true}', +// ' "y" {true}']); +// checkPrettyTrie(r.triePruneBranch(C, Immutable.List([[0, "z"]])), [' <1> "x" {true}', +// ' "y" {true}']); +// checkPrettyTrie(r.triePruneBranch(A, Immutable.List([[1, r.SOA]])), [' ::: nothing']); +// checkPrettyTrie(r.triePruneBranch(B, Immutable.List([[1, r.SOA]])), [' ::: nothing']); +// checkPrettyTrie(r.triePruneBranch(C, Immutable.List([[1, r.SOA]])), [' "z" {true}']); +// var px = [[1, r.SOA], [0, "x"]]; +// checkPrettyTrie(r.triePruneBranch(A, Immutable.List(px)), [' <1> "y" {true}']); +// checkPrettyTrie(r.triePruneBranch(B, Immutable.List(px)), [' <1> "y" {true}']); +// checkPrettyTrie(r.triePruneBranch(C, Immutable.List(px)), [' "z" {true}', +// ' <1> "y" {true}']); +// var py = [[1, r.SOA], [0, "y"]]; +// checkPrettyTrie(r.triePruneBranch(A, Immutable.List(py)), [' ::: nothing']); +// checkPrettyTrie(r.triePruneBranch(B, Immutable.List(py)), [' <1> "x" {true}']); +// checkPrettyTrie(r.triePruneBranch(C, Immutable.List(py)), [' "z" {true}', +// ' <1> "x" {true}']); +// }); +// }); - it('should leave a hole in a full trie', function () { - var full = r.compilePattern(true, r.__); - checkPrettyTrie(r.triePruneBranch(full, Immutable.List([])), [' ::: nothing']); - checkPrettyTrie(r.triePruneBranch(full, Immutable.List([[0, r.SOA]])), - [' ★ {true}', - ' <0> ::: nothing']); - checkPrettyTrie(r.triePruneBranch(full, Immutable.List([[0, "x"]])), - [' ★ {true}', - ' "x" ::: nothing']); - checkPrettyTrie(r.triePruneBranch(full, Immutable.List([[2, r.SOA], [0, "x"]])), - [' ★ {true}', - ' <2> ★ ★ {true}', - ' "x" ::: nothing']); - }); - - it('should prune in a finite tree and leave the rest alone', function () { - var A = r.compilePattern(true, ["y"]) - var B = r.union(r.compilePattern(true, ["x"]), A); - var C = r.union(r.compilePattern(true, "z"), B); - checkPrettyTrie(r.triePruneBranch(A, Immutable.List([])), [' ::: nothing']); - checkPrettyTrie(r.triePruneBranch(B, Immutable.List([])), [' ::: nothing']); - checkPrettyTrie(r.triePruneBranch(C, Immutable.List([])), [' ::: nothing']); - checkPrettyTrie(r.triePruneBranch(A, Immutable.List([[0, "z"]])), [' <1> "y" {true}']); - checkPrettyTrie(r.triePruneBranch(B, Immutable.List([[0, "z"]])), [' <1> "x" {true}', - ' "y" {true}']); - checkPrettyTrie(r.triePruneBranch(C, Immutable.List([[0, "z"]])), [' <1> "x" {true}', - ' "y" {true}']); - checkPrettyTrie(r.triePruneBranch(A, Immutable.List([[1, r.SOA]])), [' ::: nothing']); - checkPrettyTrie(r.triePruneBranch(B, Immutable.List([[1, r.SOA]])), [' ::: nothing']); - checkPrettyTrie(r.triePruneBranch(C, Immutable.List([[1, r.SOA]])), [' "z" {true}']); - var px = [[1, r.SOA], [0, "x"]]; - checkPrettyTrie(r.triePruneBranch(A, Immutable.List(px)), [' <1> "y" {true}']); - checkPrettyTrie(r.triePruneBranch(B, Immutable.List(px)), [' <1> "y" {true}']); - checkPrettyTrie(r.triePruneBranch(C, Immutable.List(px)), [' "z" {true}', - ' <1> "y" {true}']); - var py = [[1, r.SOA], [0, "y"]]; - checkPrettyTrie(r.triePruneBranch(A, Immutable.List(py)), [' ::: nothing']); - checkPrettyTrie(r.triePruneBranch(B, Immutable.List(py)), [' <1> "x" {true}']); - checkPrettyTrie(r.triePruneBranch(C, Immutable.List(py)), [' "z" {true}', - ' <1> "x" {true}']); - }); -}); - -describe('makeStructureConstructor', function () { +describe('makeConstructor', function () { it('should produce the right metadata', function () { - var ctor = Struct.makeStructureConstructor('foo', ['bar', 'zot']); + var ctor = Struct.makeConstructor('foo', ['bar', 'zot']); var inst = ctor(123, 234); - expect(inst.$SyndicateMeta$.label).to.equal('foo'); - expect(inst.$SyndicateMeta$.arguments).to.eql(['bar', 'zot']); + expect(inst.meta.label).to.equal('foo'); + expect(inst.meta.arity).to.equal(2); + expect(ctor.meta).to.equal(inst.meta); }); it('should produce the right instance data', function () { - var ctor = Struct.makeStructureConstructor('foo', ['bar', 'zot']); + var ctor = Struct.makeConstructor('foo', ['bar', 'zot']); var inst = ctor(123, 234); - expect(inst.bar).to.equal(123); - expect(inst.zot).to.equal(234); + expect(inst[0]).to.equal(123); + expect(inst[1]).to.equal(234); }); it('should work with compilePattern and matchValue', function () { var sA = Immutable.Set(["A"]); - var ctor = Struct.makeStructureConstructor('foo', ['bar', 'zot']); + var ctor = Struct.makeConstructor('foo', ['bar', 'zot']); var inst = ctor(123, 234); var x = r.compilePattern(sA, ctor(123, r.__)); checkPrettyTrie(x, [' foo<2> 123 ★ {["A"]}']); diff --git a/js/test/test-syndicate.js b/js/test/test-syndicate.js index 91b5edb..7588c04 100644 --- a/js/test/test-syndicate.js +++ b/js/test/test-syndicate.js @@ -105,10 +105,10 @@ describe("nested actor with an echoey protocol", function () { }, ['<<<<<<<< Removed:\n'+ ' ::: nothing\n'+ '======== Added:\n'+ - ' atMeta<1> "X" {["meta"]}\n'+ + ' at-meta<1> "X" {["meta"]}\n'+ '>>>>>>>>', '<<<<<<<< Removed:\n'+ - ' atMeta<1> "X" {["meta"]}\n'+ + ' at-meta<1> "X" {["meta"]}\n'+ '======== Added:\n'+ ' ::: nothing\n'+ '>>>>>>>>']);