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 var label = maybeLabel.numChildren === 1
? maybeLabel.children[0].interval.contents ? maybeLabel.children[0].interval.contents
: JSON.stringify(typeName.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) + ');'; 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! 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) { identifier: function(_name) {
var i = this.interval.contents; var i = this.interval.contents;
if (i[0] === '$') { if (i[0] === '$' && i.length > 1) {
switch (this.args.mode) { switch (this.args.mode) {
case 'pattern': this.args.acc.push('_'); break; case 'pattern': this.args.acc.push('_'); break;
case 'instantiated': this.args.acc.push(i.slice(1)); break; case 'instantiated': this.args.acc.push(i.slice(1)); break;
case 'projection': this.args.acc.push('(Syndicate._$(' + JSON.stringify(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); default: throw new Error('Unexpected buildSubscription mode ' + this.args.mode);
} }
} else { } else {
this.args.acc.push(i); this.args.acc.push(i);
@ -328,7 +349,7 @@ semantics.addAttribute('bindings', {
semantics.addOperation('pushBindings(accumulator)', { semantics.addOperation('pushBindings(accumulator)', {
identifier: function(_name) { identifier: function(_name) {
var i = this.interval.contents; var i = this.interval.contents;
if (i[0] === '$') { if (i[0] === '$' && i.length > 1) {
this.args.accumulator.push(i.slice(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) { handleEvent: function (e) {
if (e.type === 'message' if (e.type === 'message'
&& Syndicate.JQuery.jQueryEvent.isClassOf(e.message) && Syndicate.JQuery.jQueryEvent.isClassOf(e.message)
&& e.message.selector === '#clicker') && e.message[0] === '#clicker')
{ {
var r = $('#result'); var r = $('#result');
r.html(Number(r.html()) + 1); r.html(Number(r.html()) + 1);

View File

@ -1,6 +1,6 @@
"use strict"; "use strict";
var beep = Syndicate.Struct.makeStructureConstructor('beep', ['counter']); var beep = Syndicate.Struct.makeConstructor('beep', ['counter']);
var G; var G;
$(document).ready(function () { $(document).ready(function () {
@ -26,7 +26,7 @@ $(document).ready(function () {
boot: function () { return sub(beep.pattern); }, boot: function () { return sub(beep.pattern); },
handleEvent: function (e) { handleEvent: function (e) {
if (e.type === 'message') { 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 _$ = Syndicate._$;
var jQueryEvent = Syndicate.JQuery.jQueryEvent; var jQueryEvent = Syndicate.JQuery.jQueryEvent;
var fieldContents = Syndicate.Struct.makeStructureConstructor('fieldContents', ['text', 'pos']); var fieldContents = Syndicate.Struct.makeConstructor('fieldContents', ['text', 'pos']);
var highlight = Syndicate.Struct.makeStructureConstructor('highlight', ['state']); var highlight = Syndicate.Struct.makeConstructor('highlight', ['state']);
var fieldCommand = Syndicate.Struct.makeStructureConstructor('fieldCommand', ['detail']); var fieldCommand = Syndicate.Struct.makeConstructor('fieldCommand', ['detail']);
function escapeText(text) { function escapeText(text) {
text = text.replace(/&/g, '&amp;'); text = text.replace(/&/g, '&amp;');
@ -47,7 +47,7 @@ function spawnGui() {
var self = this; var self = this;
switch (e.type) { switch (e.type) {
case "message": case "message":
var event = e.message.eventValue; var event = e.message[2];
var keycode = event.keyCode; var keycode = event.keyCode;
var character = String.fromCharCode(event.charCode); var character = String.fromCharCode(event.charCode);
if (keycode === 37 /* left */) { if (keycode === 37 /* left */) {
@ -105,7 +105,7 @@ function spawnModel() {
handleEvent: function (e) { handleEvent: function (e) {
if (e.type === "message" && fieldCommand.isClassOf(e.message)) { if (e.type === "message" && fieldCommand.isClassOf(e.message)) {
var command = e.message.detail; var command = e.message[0];
if (command === "cursorLeft") { if (command === "cursorLeft") {
this.cursorPos--; this.cursorPos--;
if (this.cursorPos < 0) if (this.cursorPos < 0)

View File

@ -5,7 +5,7 @@ var Dataspace = require('./dataspace.js').Dataspace;
var Struct = require('./struct.js'); var Struct = require('./struct.js');
var Patch = require('./patch.js'); var Patch = require('./patch.js');
var ack = Struct.makeStructureConstructor('ack', ['id']); var ack = Struct.makeConstructor('ack', ['id']);
function Ack(metaLevel, id) { function Ack(metaLevel, id) {
this.metaLevel = metaLevel || 0; this.metaLevel = metaLevel || 0;
@ -26,7 +26,7 @@ Ack.prototype.check = function (e) {
if (!this.done) { if (!this.done) {
if (e.type === 'message') { if (e.type === 'message') {
var m = Patch.stripAtMeta(e.message, this.metaLevel); 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.disarm();
this.done = true; this.done = true;
} }

View File

@ -19,14 +19,17 @@ function Actor(state, bootFn) {
this.mux = new Mux.Mux(); this.mux = new Mux.Mux();
this.boot = function() { this.boot = function() {
bootFn.call(this.state); var self = this;
this.checkForTermination(); withCurrentFacet(null, function () {
bootFn.call(self.state);
});
self.checkForTermination();
}; };
} }
Actor.prototype.handleEvent = function(e) { Actor.prototype.handleEvent = function(e) {
this.facets.forEach(function (f) { this.facets.forEach(function (f) {
f.handleEvent(e); withCurrentFacet(f, function () { f.handleEvent(e); });
}); });
this.checkForTermination(); this.checkForTermination();
}; };
@ -56,14 +59,32 @@ function Facet(actor) {
this.endpoints = Immutable.Map(); this.endpoints = Immutable.Map();
this.initBlocks = Immutable.List(); this.initBlocks = Immutable.List();
this.doneBlocks = 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) { Facet.prototype.handleEvent = function(e) {
var facet = this; var facet = this;
this.endpoints.forEach(function(endpoint) { facet.endpoints.forEach(function(endpoint) {
endpoint.handlerFn.call(facet.actor.state, e); endpoint.handlerFn.call(facet.actor.state, e);
}); });
this.refresh(); facet.refresh();
}; };
Facet.prototype.addAssertion = function(assertionFn) { Facet.prototype.addAssertion = function(assertionFn) {
@ -157,6 +178,9 @@ Facet.prototype.refresh = function() {
Facet.prototype.completeBuild = function() { Facet.prototype.completeBuild = function() {
var facet = this; var facet = this;
this.actor.addFacet(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); }); this.initBlocks.forEach(function(b) { b.call(facet.actor.state); });
}; };
@ -169,8 +193,14 @@ Facet.prototype.terminate = function() {
}); });
Dataspace.stateChange(aggregate); Dataspace.stateChange(aggregate);
this.endpoints = Immutable.Map(); this.endpoints = Immutable.Map();
if (this.parent) {
this.parent.children = this.parent.children.remove(this);
}
this.actor.removeFacet(this); this.actor.removeFacet(this);
this.doneBlocks.forEach(function(b) { b.call(facet.actor.state); }); 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 // Class state and methods
Dataspace.noisy = false;
Dataspace.stack = Immutable.List(); Dataspace.stack = Immutable.List();
Dataspace.current = function () { Dataspace.current = function () {
@ -140,7 +141,7 @@ Dataspace.prototype.asChild = function (pid, f, omitLivenessCheck) {
Dataspace.prototype.kill = function (pid, exn) { Dataspace.prototype.kill = function (pid, exn) {
if (exn && exn.stack) { if (exn && exn.stack) {
console.log("Process exiting", pid, exn, exn.stack); console.log("Process exiting", pid, exn, exn.stack);
} else { } else if (exn || Dataspace.noisy) {
console.log("Process exiting", pid, exn); console.log("Process exiting", pid, exn);
} }
var p = this.processTable.get(pid); var p = this.processTable.get(pid);
@ -222,53 +223,53 @@ Dataspace.prototype.interpretAction = function (pid, action) {
var self = this; var self = this;
switch (action.type) { switch (action.type) {
case 'stateChange': case 'stateChange':
var oldMux = this.mux.shallowCopy(); var oldMux = this.mux.shallowCopy();
this.deliverPatches(oldMux, this.mux.updateStream(pid, action.patch)); this.deliverPatches(oldMux, this.mux.updateStream(pid, action.patch));
return true; return true;
case 'message': case 'message':
if (Patch.observe.isClassOf(action.message)) { if (Patch.observe.isClassOf(action.message)) {
console.warn('Process ' + pid + ' send message containing query', action.message); console.warn('Process ' + pid + ' send message containing query', action.message);
} }
if (pid !== 'meta' && Patch.atMeta.isClassOf(action.message)) { if (pid !== 'meta' && Patch.atMeta.isClassOf(action.message)) {
Dataspace.send(action.message.assertion); Dataspace.send(action.message[0]);
} else { } else {
this.mux.routeMessage(action.message).forEach(function (pid) { this.mux.routeMessage(action.message).forEach(function (pid) {
self.deliverEvent(pid, action); self.deliverEvent(pid, action);
}); });
} }
return true; return true;
case 'spawn': case 'spawn':
var oldMux = this.mux.shallowCopy(); var oldMux = this.mux.shallowCopy();
var p = { behavior: action.behavior }; var p = { behavior: action.behavior };
var pid = this.mux.nextPid; var pid = this.mux.nextPid;
this.processTable = this.processTable.set(pid, p); this.processTable = this.processTable.set(pid, p);
var initialPatch = Patch.emptyPatch; var initialPatch = Patch.emptyPatch;
if (p.behavior.boot) { if (p.behavior.boot) {
initialPatch = this.asChild(pid, function () { return p.behavior.boot() }); initialPatch = this.asChild(pid, function () { return p.behavior.boot() });
initialPatch = initialPatch || Patch.emptyPatch; initialPatch = initialPatch || Patch.emptyPatch;
this.markRunnable(pid); this.markRunnable(pid);
} }
this.deliverPatches(oldMux, this.mux.addStream(initialPatch)); this.deliverPatches(oldMux, this.mux.addStream(initialPatch));
return true; return true;
case 'terminate': case 'terminate':
var oldMux = this.mux.shallowCopy(); var oldMux = this.mux.shallowCopy();
this.deliverPatches(oldMux, this.mux.removeStream(pid)); this.deliverPatches(oldMux, this.mux.removeStream(pid));
console.log("Process exit complete", pid); if (Dataspace.noisy) console.log("Process exit complete", pid);
this.processTable = this.processTable.remove(pid); this.processTable = this.processTable.remove(pid);
return true; return true;
case 'terminateDataspace': case 'terminateDataspace':
Dataspace.exit(); Dataspace.exit();
return false; return false;
default: default:
var exn = new Error("Action type " + action.type + " not understood"); var exn = new Error("Action type " + action.type + " not understood");
exn.action = action; exn.action = action;
throw exn; throw exn;
} }
}; };

View File

@ -3,34 +3,78 @@ var Trie = require('./trie.js');
var Patch = require('./patch.js'); var Patch = require('./patch.js');
var Util = require('./util.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({ options = Util.extend({
metaLevel: 0, metaLevel: 0,
onDemandIncrease: function (captures) { demandMetaLevel: null,
console.error("Syndicate: Unhandled increase in demand", captures); supplyMetaLevel: null,
}, onDemandIncrease: defaultHandler('demand', 'increase'),
onSupplyDecrease: function (captures) { onDemandDecrease: function (captures) {},
console.error("Syndicate: Unhandled decrease in supply", captures); onSupplyIncrease: function (captures) {},
} onSupplyDecrease: defaultHandler('supply', 'decrease')
}, options); }, 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.onDemandIncrease = options.onDemandIncrease;
this.onDemandDecrease = options.onDemandDecrease;
this.onSupplyIncrease = options.onSupplyIncrease;
this.onSupplyDecrease = options.onSupplyDecrease; 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.currentDemand = Immutable.Set();
this.currentSupply = Immutable.Set(); this.currentSupply = Immutable.Set();
} }
DemandMatcher.prototype.boot = function () { DemandMatcher.prototype.boot = function () {
return Patch.sub(this.demandPattern, this.metaLevel) var p = Patch.emptyPatch;
.andThen(Patch.sub(this.supplyPattern, this.metaLevel)); 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) { 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) { DemandMatcher.prototype.handlePatch = function (p) {
var self = this; var self = this;
var dN = self.demandProjectionNames.length; var dN = self.demandProjectionNames.length;
var sN = self.supplyProjectionNames.length; var sN = self.supplyProjectionNames.length;
var addedDemand = Trie.trieKeys(Trie.project(p.added, self.demandProjection), dN); var addedDemand = this.extractKeys(p.added, self.demandProjections, dN, 'demand');
var removedDemand = Trie.trieKeys(Trie.project(p.removed, self.demandProjection), dN); var removedDemand = this.extractKeys(p.removed, self.demandProjections, dN, 'demand');
var addedSupply = Trie.trieKeys(Trie.project(p.added, self.supplyProjection), sN); var addedSupply = this.extractKeys(p.added, self.supplyProjections, sN, 'supply');
var removedSupply = Trie.trieKeys(Trie.project(p.removed, self.supplyProjection), sN); var removedSupply = this.extractKeys(p.removed, self.supplyProjections, sN, 'supply');
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());
}
self.currentSupply = self.currentSupply.union(addedSupply); self.currentSupply = self.currentSupply.union(addedSupply);
self.currentDemand = self.currentDemand.subtract(removedDemand); self.currentDemand = self.currentDemand.subtract(removedDemand);
@ -68,6 +115,17 @@ DemandMatcher.prototype.handlePatch = function (p) {
self.onSupplyDecrease(Trie.captureToObject(captures, self.supplyProjectionNames)); 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) { addedDemand.forEach(function (captures) {
if (!self.currentSupply.has(captures)) { if (!self.currentSupply.has(captures)) {
self.onDemandIncrease(Trie.captureToObject(captures, self.demandProjectionNames)); self.onDemandIncrease(Trie.captureToObject(captures, self.demandProjectionNames));

View File

@ -10,14 +10,14 @@ var Dataspace = Dataspace_.Dataspace;
var __ = Dataspace_.__; var __ = 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) { function spawnDOMDriver(domWrapFunction, jQueryWrapFunction) {
domWrapFunction = domWrapFunction || DOM; domWrapFunction = domWrapFunction || DOM;
var spec = domWrapFunction(_$('selector'), _$('fragmentClass'), _$('fragmentSpec')); var spec = domWrapFunction(_$('selector'), _$('fragmentClass'), _$('fragmentSpec'));
Dataspace.spawn( Dataspace.spawn(
new DemandMatcher(spec, new DemandMatcher([spec],
Patch.advertise(spec), // TODO: are the embedded captures problematic here? If not, why not? [Patch.advertise(spec)],
{ {
onDemandIncrease: function (c) { onDemandIncrease: function (c) {
Dataspace.spawn(new DOMFragment(c.selector, Dataspace.spawn(new DOMFragment(c.selector,
@ -106,7 +106,7 @@ DOMFragment.prototype.interpretSpec = function (spec) {
var attrs = hasAttrs ? spec[1] : {}; var attrs = hasAttrs ? spec[1] : {};
var kidIndex = hasAttrs ? 2 : 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); var n = document.createElement(tagName);
for (var i = 0; i < attrs.length; i++) { for (var i = 0; i < attrs.length; i++) {
n.setAttribute(attrs[i][0], attrs[i][1]); n.setAttribute(attrs[i][0], attrs[i][1]);
@ -124,6 +124,9 @@ DOMFragment.prototype.buildNodes = function () {
var self = this; var self = this;
var nodes = []; var nodes = [];
$(self.selector).each(function (index, domNode) { $(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); var n = self.interpretSpec(self.fragmentSpec.sealContents);
if ('classList' in n) { if ('classList' in n) {
n.classList.add(self.fragmentClass); n.classList.add(self.fragmentClass);

View File

@ -8,14 +8,14 @@ var Dataspace = Dataspace_.Dataspace;
var __ = Dataspace_.__; var __ = 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) { function spawnJQueryDriver(baseSelector, metaLevel, wrapFunction) {
metaLevel = metaLevel || 0; metaLevel = metaLevel || 0;
wrapFunction = wrapFunction || jQueryEvent; wrapFunction = wrapFunction || jQueryEvent;
Dataspace.spawn( Dataspace.spawn(
new DemandMatcher(Patch.observe(wrapFunction(_$('selector'), _$('eventName'), __)), new DemandMatcher([Patch.observe(wrapFunction(_$('selector'), _$('eventName'), __))],
Patch.advertise(wrapFunction(_$('selector'), _$('eventName'), __)), [Patch.advertise(wrapFunction(_$('selector'), _$('eventName'), __))],
{ {
metaLevel: metaLevel, metaLevel: metaLevel,
onDemandIncrease: function (c) { onDemandIncrease: function (c) {

View File

@ -29,8 +29,7 @@ module.exports.Ack = require('./ack.js').Ack;
module.exports.RandomID = require('./randomid.js'); module.exports.RandomID = require('./randomid.js');
module.exports.DOM = require("./dom-driver.js"); module.exports.DOM = require("./dom-driver.js");
module.exports.JQuery = require("./jquery-driver.js"); module.exports.JQuery = require("./jquery-driver.js");
// module.exports.RoutingTableWidget = require("./routing-table-widget.js"); module.exports.WakeDetector = require("./wake-detector-driver.js");
// module.exports.WebSocket = require("./websocket-driver.js");
module.exports.Reflect = require("./reflect.js"); module.exports.Reflect = require("./reflect.js");
module.exports.Patch = require("./patch.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 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")); var onlyMeta = Trie.trieSuccess(Immutable.Set.of("meta"));
function echoCancelledTrie(t) { function echoCancelledTrie(t) {
@ -60,8 +60,7 @@ function computeEvents(oldMux, newMux, updateStreamResult) {
var deltaAggregate = updateStreamResult.deltaAggregate; var deltaAggregate = updateStreamResult.deltaAggregate;
var deltaAggregateNoEcho = (actingPid === "meta") var deltaAggregateNoEcho = (actingPid === "meta")
? delta // because echo-cancellation means that meta-SCNs are always new information ? delta // because echo-cancellation means that meta-SCNs are always new information
: new Patch.Patch(Trie.triePruneBranch(deltaAggregate.added, atMetaBranchKeys), : deltaAggregate.withoutAtMeta();
Trie.triePruneBranch(deltaAggregate.removed, atMetaBranchKeys));
var oldRoutingTable = oldMux.routingTable; var oldRoutingTable = oldMux.routingTable;
var newRoutingTable = newMux.routingTable; var newRoutingTable = newMux.routingTable;
var affectedPids = var affectedPids =
@ -95,8 +94,7 @@ function computeEvents(oldMux, newMux, updateStreamResult) {
function computeAffectedPids(routingTable, delta) { function computeAffectedPids(routingTable, delta) {
var cover = Trie._union(delta.added, delta.removed); var cover = Trie._union(delta.added, delta.removed);
routingTable = routingTable = Trie.trieStep(routingTable, Patch.observe.meta.arity, Patch.observe.meta);
Trie.trieStep(routingTable, Patch.observe.meta.arguments.length, Patch.observe.meta);
return Trie.matchTrie(cover, routingTable, Immutable.Set(), return Trie.matchTrie(cover, routingTable, Immutable.Set(),
function (v1, v2, acc) { return acc.union(v2); }); 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 removeEverythingPatch = new Patch(Trie.emptyTrie, Trie.compilePattern(true, __));
var trueLabel = Trie.trieSuccess(true); var trueLabel = Trie.trieSuccess(true);
var observe = Struct.makeStructureConstructor('observe', ['assertion']); var observe = Struct.makeConstructor('observe', ['assertion']);
var atMeta = Struct.makeStructureConstructor('atMeta', ['assertion']); var atMeta = Struct.makeConstructor('at-meta', ['assertion']);
var advertise = Struct.makeStructureConstructor('advertise', ['assertion']); var advertise = Struct.makeConstructor('advertise', ['assertion']);
function prependAtMeta(p, level) { function prependAtMeta(p, level) {
while (level--) { while (level--) {
@ -30,7 +30,7 @@ function prependAtMeta(p, level) {
function stripAtMeta(p, level) { function stripAtMeta(p, level) {
while (level--) { while (level--) {
if (atMeta.isClassOf(p)) { if (atMeta.isClassOf(p)) {
p = p.assertion; p = p[0];
} else { } else {
return null; return null;
} }
@ -97,11 +97,11 @@ Patch.prototype.isNonEmpty = function () {
}; };
Patch.prototype.hasAdded = function () { Patch.prototype.hasAdded = function () {
return this.added !== Trie.emptyTrie; return !Trie.is_emptyTrie(this.added);
}; };
Patch.prototype.hasRemoved = function () { Patch.prototype.hasRemoved = function () {
return this.removed !== Trie.emptyTrie; return !Trie.is_emptyTrie(this.removed);
}; };
Patch.prototype.lift = function () { Patch.prototype.lift = function () {
@ -196,7 +196,7 @@ function computePatch(oldBase, newBase) {
} }
function biasedIntersection(object, subject) { 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); }); 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" + return ("<<<<<<<< Removed:\n" + Trie.prettyTrie(this.removed) + "\n" +
"======== Added:\n" + Trie.prettyTrie(this.added) + "\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.patchSeq = patchSeq;
module.exports.computePatch = computePatch; module.exports.computePatch = computePatch;
module.exports.biasedIntersection = biasedIntersection; module.exports.biasedIntersection = biasedIntersection;
module.exports.fromJSON = fromJSON;

View File

@ -1,61 +1,106 @@
"use strict"; "use strict";
// "Structures": Simple named-tuple-like records. // "Structures": Simple named-tuple-like records.
// TODO: shore up $SyndicateMeta$, making it a proper object
var Immutable = require("immutable"); var Immutable = require("immutable");
var $Special = require('./special.js'); 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 */ var __ = new $Special("wildcard"); /* wildcard marker */
function instantiateStructure($SyndicateMeta$, argvals) { function StructureType(label, arity) {
var result = {"$SyndicateMeta$": $SyndicateMeta$}; this.label = label;
var argnames = $SyndicateMeta$.arguments; this.arity = arity;
for (var i = 0; i < argnames.length; i++) { this.pattern = this.instantiate(Immutable.Repeat(__, arity).toArray());
result[argnames[i]] = argvals[i];
}
return result;
}
function makeStructureConstructor(label, argumentNames) { var self = this;
var $SyndicateMeta$ = { this.ctor = function () {
label: label, return self.instantiate(Array.prototype.slice.call(arguments));
arguments: argumentNames
}; };
var ctor = function() { this.ctor.meta = this;
return instantiateStructure($SyndicateMeta$, arguments); this.ctor.pattern = this.pattern;
}; this.ctor.isClassOf = function (v) { return self.isClassOf(v); };
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;
} }
function isSyndicateMeta(m) { function makeConstructor(label, fieldNames) {
// TODO: include more structure in $SyndicateMeta$ objects to make return new StructureType(label, fieldNames.length).ctor;
// this judgement less sloppy.
return m && m.label && Array.isArray(m.arguments);
} }
function isStructure(s) { StructureType.prototype.equals = function (other) {
return (s !== null) && (typeof s === 'object') && ("$SyndicateMeta$" in s); if (!(other instanceof StructureType)) return false;
} return this.arity === other.arity && this.label === other.label;
};
function structureToArray(s, excludeLabel) { StructureType.prototype.instantiate = function (fields) {
var result = excludeLabel ? [] : [s.$SyndicateMeta$.label]; return new Structure(this, fields);
var args = s.$SyndicateMeta$.arguments; };
for (var i = 0; i < args.length; i++) {
result.push(s[args[i]]); 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.__ = __;
module.exports.instantiateStructure = instantiateStructure; module.exports.StructureType = StructureType;
module.exports.makeStructureConstructor = makeStructureConstructor; module.exports.makeConstructor = makeConstructor;
module.exports.isSyndicateMeta = isSyndicateMeta; module.exports.Structure = Structure;
module.exports.reviveStructs = reviveStructs;
module.exports.reviver = reviver;
module.exports.isStructureType = isStructureType;
module.exports.isStructure = isStructure; module.exports.isStructure = isStructure;
module.exports.structureToArray = structureToArray;

View File

@ -186,11 +186,10 @@ function compilePattern(v, p) {
} }
if (Struct.isStructure(p)) { if (Struct.isStructure(p)) {
var args = p.$SyndicateMeta$.arguments; for (var i = p.meta.arity - 1; i >= 0; i--) {
for (var i = args.length - 1; i >= 0; i--) { acc = walk(p[i], acc);
acc = walk(p[args[i]], acc);
} }
return rseq(args.length, p.$SyndicateMeta$, acc); return rseq(p.meta.arity, p.meta, acc);
} }
if (p instanceof $Embedded) { if (p instanceof $Embedded) {
@ -222,13 +221,10 @@ function matchPattern(v, p) {
if (p === __) return; if (p === __) return;
if (Struct.isStructure(p) if (Struct.isStructure(p) && Struct.isStructure(v) && (p.meta.equals(v.meta)))
&& Struct.isStructure(v)
&& (p.$SyndicateMeta$ === v.$SyndicateMeta$))
{ {
var args = p.$SyndicateMeta$.arguments; for (var i = 0; i < p.meta.arity; i++) {
for (var i = 0; i < args.length; i++) { walk(v[i], p[i]);
walk(v[args[i]], p[args[i]]);
} }
return; return;
} }
@ -431,8 +427,8 @@ function matchValue(r, v, failureResultOpt) {
r = rlookup(r, v.length, SOA); r = rlookup(r, v.length, SOA);
vs = Immutable.List(v).concat(vs); vs = Immutable.List(v).concat(vs);
} else if (Struct.isStructure(v)) { } else if (Struct.isStructure(v)) {
r = rlookup(r, v.$SyndicateMeta$.arguments.length, v.$SyndicateMeta$); r = rlookup(r, v.meta.arity, v.meta);
vs = Immutable.List(Struct.structureToArray(v, true)).concat(vs); vs = Immutable.List(v.fields).concat(vs);
} else { } else {
r = rlookup(r, 0, v); r = rlookup(r, 0, v);
} }
@ -501,17 +497,19 @@ function appendTrie(m, mTailFn) {
} }
} }
function triePruneBranch(m, arityKeys) { // DANGEROUS: prefer subtract() instead.
if (arityKeys.isEmpty()) return emptyTrie; //
if (!(m instanceof $Branch)) return m; // function triePruneBranch(m, arityKeys) {
var arityKey = arityKeys.first(); // if (arityKeys.isEmpty()) return emptyTrie;
var rest = arityKeys.shift(); // if (!(m instanceof $Branch)) return m;
var arity = arityKey[0]; // var arityKey = arityKeys.first();
var key = arityKey[1]; // var rest = arityKeys.shift();
m = rcopybranch(m); // var arity = arityKey[0];
rupdate_inplace(m, arity, key, triePruneBranch(rlookup(m, arity, key), rest)); // var key = arityKey[1];
return collapse(m); // m = rcopybranch(m);
} // rupdate_inplace(m, arity, key, triePruneBranch(rlookup(m, arity, key), rest));
// return collapse(m);
// }
function trieStep(m, arity, key) { function trieStep(m, arity, key) {
if (typeof key === 'undefined') { if (typeof key === 'undefined') {
@ -550,8 +548,7 @@ function projectionNames(p) {
} }
if (Struct.isStructure(p)) { if (Struct.isStructure(p)) {
var args = p.$SyndicateMeta$.arguments; for (var i = 0; i < p.meta.arity; i++) walk(p[i]);
for (var i = 0; i < args.length; i++) walk(p[args[i]]);
return; return;
} }
} }
@ -576,12 +573,11 @@ function projectionToPattern(p) {
} }
if (Struct.isStructure(p)) { if (Struct.isStructure(p)) {
var result = {"$SyndicateMeta$": p.$SyndicateMeta$}; var resultFields = [];
var args = p.$SyndicateMeta$.arguments; for (var i = 0; i < p.meta.arity; i++) {
for (var i = 0; i < args.length; i++) { resultFields[i] = walk(p[i]);
result[args[i]] = walk(p[args[i]]);
} }
return result; return new Struct.Structure(p.meta, resultFields);
} }
return p; return p;
@ -652,15 +648,13 @@ function projectMany(t, wholeSpecs, projectSuccessOpt, combinerOpt) {
} }
if (Struct.isStructure(spec)) { if (Struct.isStructure(spec)) {
var arity = spec.$SyndicateMeta$.arguments.length;
var key = spec.$SyndicateMeta$;
var intermediate = walk(isCapturing, var intermediate = walk(isCapturing,
rlookup(t, arity, key), rlookup(t, spec.meta.arity, spec.meta),
Immutable.List(Struct.structureToArray(spec, true)), Immutable.List(spec.fields),
function (intermediate) { function (intermediate) {
return walk(isCapturing, intermediate, specsRest, kont); 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)) { if (Array.isArray(spec)) {
@ -687,7 +681,7 @@ function reconstructSequence(key, items) {
if (key === SOA) { if (key === SOA) {
return items.toArray(); return items.toArray();
} else { } 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 if (result === false) return false; // break out of iteration
var piece; 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) { piece = walk(k, arity, Immutable.List(), function (items, m1) {
var item = reconstructSequence(key, items); var item = reconstructSequence(key, items);
return walk(m1, takeCount - 1, valsRev.unshift(item), kont); return walk(m1, takeCount - 1, valsRev.unshift(item), kont);
@ -798,10 +792,11 @@ function prettyTrie(m, initialIndent) {
} }
acc.push(" "); acc.push(" ");
if (key === SOA) key = '<' + arity + '>'; 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 (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); acc.push(key);
walk(i + key.length + 1, k); 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.__ = __;
module.exports.SOA = SOA; module.exports.SOA = SOA;
module.exports.$Capture = $Capture; module.exports.$Capture = $Capture;
@ -833,7 +900,7 @@ module.exports.subtract = subtract;
module.exports.matchValue = matchValue; module.exports.matchValue = matchValue;
module.exports.matchTrie = matchTrie; module.exports.matchTrie = matchTrie;
module.exports.appendTrie = appendTrie; module.exports.appendTrie = appendTrie;
module.exports.triePruneBranch = triePruneBranch; // module.exports.triePruneBranch = triePruneBranch;
module.exports.trieStep = trieStep; module.exports.trieStep = trieStep;
module.exports.trieSuccess = rsuccess; module.exports.trieSuccess = rsuccess;
module.exports.relabel = relabel; module.exports.relabel = relabel;
@ -847,6 +914,8 @@ module.exports.captureToObject = captureToObject;
module.exports.trieKeysToObjects = trieKeysToObjects; module.exports.trieKeysToObjects = trieKeysToObjects;
module.exports.projectObjects = projectObjects; module.exports.projectObjects = projectObjects;
module.exports.prettyTrie = prettyTrie; module.exports.prettyTrie = prettyTrie;
module.exports.trieToJSON = trieToJSON;
module.exports.trieFromJSON = trieFromJSON;
// For testing // For testing
module.exports._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}'], [' <2> 1 2 {true}'],
[' ::: nothing']); [' ::: nothing']);
checkPrettyPatch(Patch.assert([1, 2], 1), checkPrettyPatch(Patch.assert([1, 2], 1),
[' atMeta<1> <2> 1 2 {true}'], [' at-meta<1> <2> 1 2 {true}'],
[' ::: nothing']); [' ::: nothing']);
checkPrettyPatch(Patch.assert([1, 2], 2), 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']); [' ::: nothing']);
checkPrettyPatch(Patch.sub([1, 2], 0), checkPrettyPatch(Patch.sub([1, 2], 0),
[' observe<1> <2> 1 2 {true}'], [' observe<1> <2> 1 2 {true}'],
[' ::: nothing']); [' ::: nothing']);
checkPrettyPatch(Patch.sub([1, 2], 1), checkPrettyPatch(Patch.sub([1, 2], 1),
[' atMeta<1> observe<1> <2> 1 2 {true}', [' at-meta<1> observe<1> <2> 1 2 {true}',
' observe<1> atMeta<1> <2> 1 2 {true}'], ' observe<1> at-meta<1> <2> 1 2 {true}'],
[' ::: nothing']); [' ::: nothing']);
checkPrettyPatch(Patch.sub([1, 2], 2), checkPrettyPatch(Patch.sub([1, 2], 2),
[' atMeta<1> atMeta<1> observe<1> <2> 1 2 {true}', [' at-meta<1> at-meta<1> observe<1> <2> 1 2 {true}',
' observe<1> atMeta<1> <2> 1 2 {true}', ' observe<1> at-meta<1> <2> 1 2 {true}',
' observe<1> atMeta<1> atMeta<1> <2> 1 2 {true}'], ' observe<1> at-meta<1> at-meta<1> <2> 1 2 {true}'],
[' ::: nothing']); [' ::: nothing']);
}); });
}); });
@ -111,14 +111,14 @@ describe('patch sequencing', function () {
describe('patch lifting', function () { describe('patch lifting', function () {
it('should basically work', function () { it('should basically work', function () {
checkPrettyPatch(Patch.assert([1, 2]).lift(), checkPrettyPatch(Patch.assert([1, 2]).lift(),
[' atMeta<1> <2> 1 2 {true}'], [' at-meta<1> <2> 1 2 {true}'],
[' ::: nothing']); [' ::: nothing']);
checkPrettyPatch(Patch.sub([1, 2]).lift(), checkPrettyPatch(Patch.sub([1, 2]).lift(),
[' atMeta<1> observe<1> <2> 1 2 {true}'], [' at-meta<1> observe<1> <2> 1 2 {true}'],
[' ::: nothing']); [' ::: nothing']);
checkPrettyPatch(Patch.assert([1, 2]).andThen(Patch.assert(Patch.atMeta([1, 2]))).lift(), checkPrettyPatch(Patch.assert([1, 2]).andThen(Patch.assert(Patch.atMeta([1, 2]))).lift(),
[' atMeta<1> atMeta<1> <2> 1 2 {true}', [' at-meta<1> at-meta<1> <2> 1 2 {true}',
' <2> 1 2 {true}'], ' <2> 1 2 {true}'],
[' ::: nothing']); [' ::: nothing']);
}); });
}); });

View File

@ -402,7 +402,7 @@ describe("calls to matchPattern", function () {
}); });
it("matches structures", 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")))) expect(r.matchPattern(ctor(123, 234), ctor(r._$("bar"), r._$("zot"))))
.to.eql({ bar: 123, zot: 234, length: 2 }); .to.eql({ bar: 123, zot: 234, length: 2 });
// Previously, structures were roughly the same as arrays: // Previously, structures were roughly the same as arrays:
@ -466,75 +466,76 @@ describe('intersect', function () {
}); });
}); });
describe('triePruneBranch', function () { // describe('triePruneBranch', function () {
it('should not affect empty trie', function () { // it('should not affect empty trie', function () {
checkPrettyTrie(r.triePruneBranch(r.emptyTrie, Immutable.List([])), [' ::: nothing']); // 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([r.SOA])), [' ::: nothing']);
checkPrettyTrie(r.triePruneBranch(r.emptyTrie, Immutable.List(["x"])), [' ::: nothing']); // checkPrettyTrie(r.triePruneBranch(r.emptyTrie, Immutable.List(["x"])), [' ::: nothing']);
checkPrettyTrie(r.triePruneBranch(r.emptyTrie, Immutable.List([r.SOA, "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 () { describe('makeConstructor', 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 () {
it('should produce the right metadata', 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); var inst = ctor(123, 234);
expect(inst.$SyndicateMeta$.label).to.equal('foo'); expect(inst.meta.label).to.equal('foo');
expect(inst.$SyndicateMeta$.arguments).to.eql(['bar', 'zot']); expect(inst.meta.arity).to.equal(2);
expect(ctor.meta).to.equal(inst.meta);
}); });
it('should produce the right instance data', function () { 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); var inst = ctor(123, 234);
expect(inst.bar).to.equal(123); expect(inst[0]).to.equal(123);
expect(inst.zot).to.equal(234); expect(inst[1]).to.equal(234);
}); });
it('should work with compilePattern and matchValue', function () { it('should work with compilePattern and matchValue', function () {
var sA = Immutable.Set(["A"]); 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 inst = ctor(123, 234);
var x = r.compilePattern(sA, ctor(123, r.__)); var x = r.compilePattern(sA, ctor(123, r.__));
checkPrettyTrie(x, [' foo<2> 123 ★ {["A"]}']); checkPrettyTrie(x, [' foo<2> 123 ★ {["A"]}']);

View File

@ -105,10 +105,10 @@ describe("nested actor with an echoey protocol", function () {
}, ['<<<<<<<< Removed:\n'+ }, ['<<<<<<<< Removed:\n'+
' ::: nothing\n'+ ' ::: nothing\n'+
'======== Added:\n'+ '======== Added:\n'+
' atMeta<1> "X" {["meta"]}\n'+ ' at-meta<1> "X" {["meta"]}\n'+
'>>>>>>>>', '>>>>>>>>',
'<<<<<<<< Removed:\n'+ '<<<<<<<< Removed:\n'+
' atMeta<1> "X" {["meta"]}\n'+ ' at-meta<1> "X" {["meta"]}\n'+
'======== Added:\n'+ '======== Added:\n'+
' ::: nothing\n'+ ' ::: nothing\n'+
'>>>>>>>>']); '>>>>>>>>']);