Implement Syndicate/js broker-client and chat app.

Support capturing with a pattern in the Syndicate/js DSL: ($foo = bar())

Struct has been cleaned up, and now offers proper Javascript objects
for its prefab-like structures. These can serialize and deserialize
themselves to/from JSON. They behave like prefabs in that two
StructureTypes created with the same label and arity behave
identically wrt Dataspaces and Tries. Sadly, prefab field names had to
go in order to support this.

Facets now track and terminate their children upon termination. This
is experimental; I suspect it is required for nested durings.

DemandMatcher can now support multiple specs, but this is less useful
than you might think since it tracks supply and demand quite naively.
It would have to have (surprise, surprise!) a mux-like structure to do
the job properly!

Added WakeDetector to main.js; adding the broker client will have to
wait until it is turned into a proper module in the src/ directory.
This commit is contained in:
Tony Garnock-Jones 2016-05-10 00:40:53 -04:00
parent bbca582b98
commit 8546e93e5d
24 changed files with 1112 additions and 271 deletions

View File

@ -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));
}
},

View File

@ -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;
};

41
js/examples/chat/codec.js Normal file
View File

@ -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")
};

View File

@ -0,0 +1,55 @@
<!doctype html>
<html>
<head>
<title>Syndicate: Chat</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="style.css" rel="stylesheet">
<script src="https://cdnjs.cloudflare.com/ajax/libs/immutable/3.8.1/immutable.min.js"></script> <!-- TODO: ??? -->
<script src="../../third-party/jquery-2.2.0.min.js"></script>
<script src="../../dist/syndicatecompiler.js"></script>
<script src="../../dist/syndicate.js"></script>
<script src="codec.js"></script>
<script src="broker-client.js"></script>
<script type="text/syndicate-js" src="index.js"></script>
</head>
<body>
<section>
<form name="nym_form">
<fieldset>
<label class="control-label" for="wsurl">Server:</label>
<input type="text" id="wsurl" name="wsurl" value="ws://localhost:8000/">
<label class="control-label" for="nym">Nym:</label>
<input type="text" id="nym" name="nym" value="">
<label class="control-label" for="status">Status:</label>
<input type="text" id="status" name="status" value="">
</fieldset>
</form>
</section>
<section>
<section id="messages">
<h1>Messages</h1>
<div id="chat_output"></div>
</section>
<section id="active_users">
<h1>Active Users</h1>
<ul id="nymlist"></ul>
</section>
</section>
<section>
<form id="chat_form" name="chat_form">
<fieldset>
<input type="text" id="chat_input" name="chat_input" value="" autocomplete="off">
<button id="send_chat">Send</button>
</fieldset>
</form>
</section>
<hr>
<pre id="ds-state"></pre>
</body>
</html>

116
js/examples/chat/index.js Normal file
View File

@ -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 = $("<span/>").text((new Date()).toGMTString()).addClass("timestamp");
var item = $("<div/>").append([stamp].concat(item));
var o = $("#chat_output");
o.append(item);
o[0].scrollTop = o[0].scrollHeight;
return item;
}
function outputState(state) {
outputItem([$("<span/>").text(state).addClass(state).addClass("state")])
.addClass("state_" + state);
}
function outputUtterance(who, what) {
outputItem([$("<span/>").text(who).addClass("nym"),
$("<span/>").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));
// });
});

View File

@ -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;
}

View File

@ -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);

View File

@ -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]);
}
}
});

View File

@ -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, '&amp;');
@ -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)

View File

@ -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;
}

View File

@ -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();
});
};
//---------------------------------------------------------------------------

View File

@ -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;
}
};

View File

@ -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));

View File

@ -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);

View File

@ -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) {

View File

@ -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");

View File

@ -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); });
}

View File

@ -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;

View File

@ -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;

View File

@ -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 = {

View File

@ -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;

View File

@ -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']);
});
});

View File

@ -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"]}']);

View File

@ -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'+
'>>>>>>>>']);