Compare commits
110 Commits
gh-pages-p
...
main
Author | SHA1 | Date |
---|---|---|
Tony Garnock-Jones | 382391b518 | |
Tony Garnock-Jones | 5621685052 | |
Tony Garnock-Jones | 85c6c228a3 | |
Tony Garnock-Jones | 18c4b184e5 | |
Tony Garnock-Jones | 9fdf90db68 | |
Tony Garnock-Jones | 26245951ad | |
Tony Garnock-Jones | 7cf9dabca4 | |
Tony Garnock-Jones | 0584b4d6d3 | |
Tony Garnock-Jones | d777418d5c | |
Tony Garnock-Jones | 160938cec8 | |
Tony Garnock-Jones | 1b3b355fea | |
Tony Garnock-Jones | 440c91b4d7 | |
Tony Garnock-Jones | 52bdc2eb3c | |
Tony Garnock-Jones | 10abaa1724 | |
Tony Garnock-Jones | 7bb57e2c39 | |
Tony Garnock-Jones | 66670e7f6f | |
Tony Garnock-Jones | 20d0f5d58d | |
Tony Garnock-Jones | f5e59bfcd8 | |
Tony Garnock-Jones | a0a6a3dbfe | |
Tony Garnock-Jones | d080494664 | |
Tony Garnock-Jones | c5f7e9db2a | |
Tony Garnock-Jones | a62f00b8a3 | |
Tony Garnock-Jones | e9697c8171 | |
Tony Garnock-Jones | d7baec744e | |
Tony Garnock-Jones | 216935d60f | |
Tony Garnock-Jones | 66709fe58e | |
Tony Garnock-Jones | 318770e301 | |
Tony Garnock-Jones | bd06a8a09e | |
Tony Garnock-Jones | 659ee24105 | |
Tony Garnock-Jones | 2bfacbfc7b | |
Tony Garnock-Jones | e78696c621 | |
Tony Garnock-Jones | f46fd52239 | |
Tony Garnock-Jones | e0def26f6c | |
Tony Garnock-Jones | 9965ce760e | |
Tony Garnock-Jones | 76044b539e | |
Tony Garnock-Jones | 55c9fa1d49 | |
Tony Garnock-Jones | 95a4bc3c93 | |
Tony Garnock-Jones | 9ffbec107f | |
Tony Garnock-Jones | fba2ee91a8 | |
Tony Garnock-Jones | 0b361137a2 | |
Tony Garnock-Jones | e0b476cc0a | |
Tony Garnock-Jones | cfeb46ef62 | |
Tony Garnock-Jones | 80befd60eb | |
Tony Garnock-Jones | 28405b544f | |
Tony Garnock-Jones | a605a438cf | |
Tony Garnock-Jones | 96d577b2d7 | |
Tony Garnock-Jones | f7baa65a2d | |
Tony Garnock-Jones | 8fde23d187 | |
Tony Garnock-Jones | 96d3d3c621 | |
Tony Garnock-Jones | 7bab9be492 | |
Tony Garnock-Jones | 4239c1ea33 | |
Tony Garnock-Jones | 044cd84e60 | |
Tony Garnock-Jones | 48e1b04f90 | |
Tony Garnock-Jones | 0025e6b3af | |
Tony Garnock-Jones | a0b3ac3198 | |
Tony Garnock-Jones | 19374f6926 | |
Tony Garnock-Jones | 853533f5b1 | |
Tony Garnock-Jones | 2aa44a9142 | |
Tony Garnock-Jones | 340d32e33c | |
Tony Garnock-Jones | 4e5bf4955f | |
Tony Garnock-Jones | 44de4f118c | |
Tony Garnock-Jones | 73192735e0 | |
Tony Garnock-Jones | 53e3c23a5f | |
Tony Garnock-Jones | fe2fcdb1c8 | |
Tony Garnock-Jones | a7212a02af | |
Tony Garnock-Jones | 420784bb11 | |
Tony Garnock-Jones | 5c68f2243a | |
Tony Garnock-Jones | 461f96d5d4 | |
Tony Garnock-Jones | 95701d39c5 | |
Tony Garnock-Jones | 424a00c675 | |
Tony Garnock-Jones | 336b4f36ac | |
Tony Garnock-Jones | 03773b6d6d | |
Tony Garnock-Jones | 00b92fdf1c | |
Tony Garnock-Jones | 6f21190383 | |
Tony Garnock-Jones | 0dce7a5e34 | |
Tony Garnock-Jones | 201041ad2d | |
Tony Garnock-Jones | f2504b5dbf | |
Tony Garnock-Jones | 96aad3d579 | |
Tony Garnock-Jones | c5eab99565 | |
Tony Garnock-Jones | a5c80092cd | |
Tony Garnock-Jones | 7075dbaa4c | |
Tony Garnock-Jones | d3143b8e65 | |
Tony Garnock-Jones | fc5e2108a1 | |
Tony Garnock-Jones | ef514b5b38 | |
Tony Garnock-Jones | f55b933292 | |
Tony Garnock-Jones | 96e651d4a1 | |
Tony Garnock-Jones | 7ee78181a3 | |
Tony Garnock-Jones | 19d4bc10a4 | |
Tony Garnock-Jones | 9f26792e40 | |
Tony Garnock-Jones | ad0c151278 | |
Tony Garnock-Jones | afa79e9d11 | |
Tony Garnock-Jones | 25277a2605 | |
Tony Garnock-Jones | 80e20f7a01 | |
Tony Garnock-Jones | 55335fa296 | |
Tony Garnock-Jones | 305289db57 | |
Tony Garnock-Jones | 1ccc4377f3 | |
Tony Garnock-Jones | 714ba11cdd | |
Tony Garnock-Jones | f8d8c3b706 | |
Tony Garnock-Jones | 813fb157fb | |
Tony Garnock-Jones | e413e86588 | |
Tony Garnock-Jones | d36a65d843 | |
Tony Garnock-Jones | 83e82658c5 | |
Tony Garnock-Jones | 037abe45a5 | |
Tony Garnock-Jones | a8f4c74de9 | |
Tony Garnock-Jones | 74acc78aab | |
Tony Garnock-Jones | 202a6e129b | |
Tony Garnock-Jones | 1e3a8f7db0 | |
Tony Garnock-Jones | 87c5aac69f | |
Tony Garnock-Jones | e1eadbf664 | |
Tony Garnock-Jones | cdea1507c4 |
|
@ -1,6 +1,5 @@
|
|||
private-key.pem
|
||||
server-cert.pem
|
||||
MarketplaceChat.app.zip
|
||||
MarketplaceChat.app/
|
||||
scratch/
|
||||
_site/
|
||||
node_modules/
|
||||
|
|
41
Makefile
41
Makefile
|
@ -1,20 +1,5 @@
|
|||
APP_NAME=MarketplaceChat.app
|
||||
LIB_SOURCES=\
|
||||
marketplace.js \
|
||||
spy.js \
|
||||
dom-driver.js \
|
||||
jquery-driver.js \
|
||||
routing-table-widget.js \
|
||||
routing-table-widget.css \
|
||||
wake-detector.js \
|
||||
websocket-driver.js
|
||||
APP_SOURCES=\
|
||||
examples/chat/index.html \
|
||||
examples/chat/index.js \
|
||||
examples/chat/style.css
|
||||
RESOURCES=$(wildcard examples/chat/app-resources/*)
|
||||
|
||||
all: $(APP_NAME).zip
|
||||
all:
|
||||
npm install .
|
||||
|
||||
keys: private-key.pem server-cert.pem
|
||||
|
||||
|
@ -31,22 +16,8 @@ server-cert.pem: private-key.pem
|
|||
clean-keys:
|
||||
rm -f private-key.pem server-cert.pem
|
||||
|
||||
$(APP_NAME).zip: $(APP_NAME)
|
||||
zip -r $@ $<
|
||||
|
||||
$(APP_NAME): $(APP_SOURCES) $(LIB_SOURCES)
|
||||
echo RESOURCES $(RESOURCES)
|
||||
rm -rf $@
|
||||
mkdir -p $@/Contents/MacOS
|
||||
mkdir -p $@/Contents/Resources
|
||||
cp examples/chat/app-resources/Info.plist $@/Contents
|
||||
cp examples/chat/app-resources/boot.sh $@/Contents/MacOS
|
||||
cp examples/chat/app-resources/app.icns $@/Contents/Resources
|
||||
cp -r third-party $@/Contents/Resources
|
||||
cp $(LIB_SOURCES) $@/Contents/Resources
|
||||
mkdir -p $@/Contents/Resources/examples/chat
|
||||
cp $(APP_SOURCES) $@/Contents/Resources/examples/chat
|
||||
chmod a+x $@/Contents/MacOS/boot.sh
|
||||
|
||||
clean:
|
||||
rm -rf $(APP_NAME) $(APP_NAME).zip
|
||||
rm -f dist/*.js
|
||||
|
||||
veryclean: clean
|
||||
rm -rf node_modules/
|
||||
|
|
|
@ -14,7 +14,7 @@ To install the Racket server:
|
|||
|
||||
To run the Racket server:
|
||||
|
||||
- `racket server.rkt` from the base directory of this repository.
|
||||
racket -l minimart/examples/broker
|
||||
|
||||
The Racket server listens for tunnelled Network Calculus events via
|
||||
websocket on ports 8000 (HTTP) and 8443 (HTTPS, if you have a
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
Directory for build products, checked in to the repo for ease-of-use.
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -1,91 +0,0 @@
|
|||
// DOM fragment display driver
|
||||
|
||||
function spawnDOMDriver() {
|
||||
var d = new DemandMatcher(["DOM", __, __, __], 0, {demandSideIsSubscription: false});
|
||||
d.onDemandIncrease = function (r) {
|
||||
var selector = r.pattern[1];
|
||||
var fragmentClass = r.pattern[2];
|
||||
var fragmentSpec = r.pattern[3];
|
||||
World.spawn(new DOMFragment(selector, fragmentClass, fragmentSpec),
|
||||
[sub(["DOM", selector, fragmentClass, fragmentSpec]),
|
||||
sub(["DOM", selector, fragmentClass, fragmentSpec], 0, 1)]);
|
||||
};
|
||||
World.spawn(d);
|
||||
}
|
||||
|
||||
function DOMFragment(selector, fragmentClass, fragmentSpec) {
|
||||
this.selector = selector;
|
||||
this.fragmentClass = fragmentClass;
|
||||
this.fragmentSpec = fragmentSpec;
|
||||
this.nodes = this.buildNodes();
|
||||
}
|
||||
|
||||
DOMFragment.prototype.boot = function () {
|
||||
var self = this;
|
||||
var monitoring = sub(["DOM", self.selector, self.fragmentClass, self.fragmentSpec], 1, 2);
|
||||
World.spawn(new World(function () {
|
||||
spawnJQueryDriver(self.selector+" > ."+self.fragmentClass, 1);
|
||||
World.spawn({
|
||||
handleEvent: function (e) {
|
||||
if (e.type === "routes") {
|
||||
var needed = false;
|
||||
for (var i = 0; i < e.routes.length; i++) {
|
||||
needed = needed || (e.routes[i].level === 0); // find participant peers
|
||||
}
|
||||
if (e.routes.length > 0 && !needed) {
|
||||
World.shutdownWorld();
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [monitoring]);
|
||||
}));
|
||||
};
|
||||
|
||||
DOMFragment.prototype.handleEvent = function (e) {
|
||||
if (e.type === "routes" && e.routes.length === 0) {
|
||||
for (var i = 0; i < this.nodes.length; i++) {
|
||||
var n = this.nodes[i];
|
||||
n.parentNode.removeChild(n);
|
||||
}
|
||||
World.exit();
|
||||
}
|
||||
};
|
||||
|
||||
DOMFragment.prototype.interpretSpec = function (spec) {
|
||||
// Fragment specs are roughly JSON-equivalents of SXML.
|
||||
// spec ::== ["tag", {"attr": "value", ...}, spec, spec, ...]
|
||||
// | ["tag", spec, spec, ...]
|
||||
// | "cdata"
|
||||
if (typeof(spec) === "string" || typeof(spec) === "number") {
|
||||
return document.createTextNode(spec);
|
||||
} else if ($.isArray(spec)) {
|
||||
var tagName = spec[0];
|
||||
var hasAttrs = $.isPlainObject(spec[1]);
|
||||
var attrs = hasAttrs ? spec[1] : {};
|
||||
var kidIndex = hasAttrs ? 2 : 1;
|
||||
|
||||
// Wow! Such XSS! Many hacks! So vulnerability! Amaze!
|
||||
var n = document.createElement(tagName);
|
||||
for (var attr in attrs) {
|
||||
if (attrs.hasOwnProperty(attr)) {
|
||||
n.setAttribute(attr, attrs[attr]);
|
||||
}
|
||||
}
|
||||
for (var i = kidIndex; i < spec.length; i++) {
|
||||
n.appendChild(this.interpretSpec(spec[i]));
|
||||
}
|
||||
return n;
|
||||
}
|
||||
};
|
||||
|
||||
DOMFragment.prototype.buildNodes = function () {
|
||||
var self = this;
|
||||
var nodes = [];
|
||||
$(self.selector).each(function (index, domNode) {
|
||||
var n = self.interpretSpec(self.fragmentSpec);
|
||||
n.classList.add(self.fragmentClass);
|
||||
domNode.appendChild(n);
|
||||
nodes.push(n);
|
||||
});
|
||||
return nodes;
|
||||
};
|
|
@ -0,0 +1,67 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title>JS Marketplace: Chat Example</title>
|
||||
<meta charset="utf-8">
|
||||
<link href="../../third-party/bootstrap/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="../../third-party/bootstrap/css/bootstrap-responsive.min.css" rel="stylesheet">
|
||||
<link href="style.css" rel="stylesheet">
|
||||
<script src="../../third-party/jquery-2.0.3.min.js"></script>
|
||||
<script src="../../dist/minimart.js"></script>
|
||||
<script src="index.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container-fluid">
|
||||
<div class="row-fluid">
|
||||
<div class="span12">
|
||||
<form class="form-horizontal" name="nym_form">
|
||||
<fieldset>
|
||||
<div class="control-group">
|
||||
<label class="control-label" for="wsurl">Server:</label>
|
||||
<div class="controls">
|
||||
<input type="text" id="wsurl" name="wsurl" value="ws://localhost:8000/">
|
||||
</div>
|
||||
<label class="control-label" for="nym">Nym:</label>
|
||||
<div class="controls">
|
||||
<input type="text" id="nym" name="nym" value="">
|
||||
</div>
|
||||
<label class="control-label" for="status">Status:</label>
|
||||
<div class="controls">
|
||||
<input type="text" id="status" name="status" value="">
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row-fluid">
|
||||
<div class="span9">
|
||||
<h2>Messages</h2>
|
||||
<div id="chat_output"></div>
|
||||
</div>
|
||||
<div class="span3">
|
||||
<h2>Active Users</h2>
|
||||
<div id="nymlist"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row-fluid">
|
||||
<div class="span12">
|
||||
<form class="form-horizontal" name="chat_form">
|
||||
<fieldset>
|
||||
<div class="control-group">
|
||||
<div class="controls">
|
||||
<input type="text" id="chat_input" name="chat_input" value="" autocomplete="off">
|
||||
<button id="send_chat">Send</button>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row-fluid">
|
||||
<div class="span12" id="spy-holder">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,162 @@
|
|||
var Route = Minimart.Route;
|
||||
var World = Minimart.World;
|
||||
var sub = Minimart.sub;
|
||||
var pub = Minimart.pub;
|
||||
var __ = Minimart.__;
|
||||
var _$ = Minimart._$;
|
||||
|
||||
function chatEvent(nym, status, utterance, stamp) {
|
||||
return ["chatEvent", nym, status, utterance, stamp || +(new Date())];
|
||||
}
|
||||
function chatEventNym(c) { return c[1]; }
|
||||
function chatEventStatus(c) { return c[2]; }
|
||||
function chatEventUtterance(c) { return c[3]; }
|
||||
function chatEventStamp(c) { return c[4]; }
|
||||
|
||||
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 updateNymList(g) {
|
||||
var statuses = {};
|
||||
var nymProj = ["broker", 0, ["chatEvent", _$, _$, __, __]];
|
||||
var matchedNyms = Route.matcherKeys(g.project(Route.compileProjection(nymProj), true, 0, 0));
|
||||
for (var i = 0; i < matchedNyms.length; i++) {
|
||||
statuses[matchedNyms[i][0]] = matchedNyms[i][1];
|
||||
}
|
||||
var nyms = [];
|
||||
for (var nym in statuses) { nyms.push(nym); }
|
||||
nyms.sort();
|
||||
|
||||
var container = $("#nymlist");
|
||||
container[0].innerHTML = ""; // remove all children
|
||||
for (var i = 0; i < nyms.length; i++) {
|
||||
var n = $("<span/>").text(nyms[i]).addClass("nym");
|
||||
var s = statuses[nyms[i]];
|
||||
if (s) {
|
||||
container.append($("<div/>").append([n, $("<span/>").text(s).addClass("nym_status")]));
|
||||
} else {
|
||||
container.append($("<div/>").append(n));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
var G;
|
||||
$(document).ready(function () {
|
||||
$("#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)); }
|
||||
|
||||
G = new Minimart.Ground(function () {
|
||||
console.log('starting ground boot');
|
||||
// World.spawn(new Spy());
|
||||
Minimart.JQuery.spawnJQueryDriver();
|
||||
Minimart.DOM.spawnDOMDriver();
|
||||
Minimart.RoutingTableWidget.spawnRoutingTableWidget("#spy-holder", "spy");
|
||||
|
||||
World.spawn(new Minimart.WakeDetector());
|
||||
var wsconn = new Minimart.WebSocket.WebSocketConnection("broker", $("#wsurl").val(), true);
|
||||
World.spawn(wsconn);
|
||||
World.spawn({
|
||||
// Monitor connection, notifying connectivity changes
|
||||
state: "crashed", // start with this to avoid spurious initial message print
|
||||
boot: function () {
|
||||
return [sub(["broker_state", __], 0, 1)];
|
||||
},
|
||||
handleEvent: function (e) {
|
||||
if (e.type === "routes") {
|
||||
var states =
|
||||
Route.matcherKeys(e.gestalt.project(Route.compileProjection([__, _$]),
|
||||
true, 0, 0));
|
||||
var newState = states.length > 0 ? states[0][0] : "crashed";
|
||||
if (this.state != newState) {
|
||||
outputState(newState);
|
||||
this.state = newState;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
World.spawn({
|
||||
// Actual chat functionality
|
||||
boot: function () {
|
||||
return this.subscriptions();
|
||||
},
|
||||
nym: function () { return $("#nym").val(); },
|
||||
currentStatus: function () { return $("#status").val(); },
|
||||
subscriptions: function () {
|
||||
return [sub("wake"),
|
||||
sub(["jQuery", "#send_chat", "click", __]),
|
||||
sub(["jQuery", "#nym", "change", __]),
|
||||
sub(["jQuery", "#status", "change", __]),
|
||||
sub(["jQuery", "#wsurl", "change", __]),
|
||||
pub(["broker", 0, chatEvent(this.nym(), this.currentStatus(), __, __)]),
|
||||
sub(["broker", 0, chatEvent(__, __, __, __)], 0, 1)];
|
||||
},
|
||||
handleEvent: function (e) {
|
||||
var self = this;
|
||||
switch (e.type) {
|
||||
case "routes":
|
||||
updateNymList(e.gestalt);
|
||||
break;
|
||||
case "message":
|
||||
if (e.message === "wake") {
|
||||
wsconn.forceclose();
|
||||
return;
|
||||
}
|
||||
switch (e.message[0]) {
|
||||
case "jQuery":
|
||||
switch (e.message[1]) {
|
||||
case "#send_chat":
|
||||
var inp = $("#chat_input");
|
||||
var utterance = inp.val();
|
||||
inp.val("");
|
||||
if (utterance) {
|
||||
World.send(["broker", 0, chatEvent(this.nym(),
|
||||
this.currentStatus(),
|
||||
utterance)]);
|
||||
}
|
||||
break;
|
||||
case "#nym":
|
||||
case "#status":
|
||||
World.updateRoutes(this.subscriptions());
|
||||
break;
|
||||
case "#wsurl":
|
||||
wsconn.forceclose();
|
||||
wsconn.wsurl = $("#wsurl").val();
|
||||
break;
|
||||
default:
|
||||
console.log("Got jquery event from as-yet-unhandled subscription",
|
||||
e.message[2], e.message[3]);
|
||||
}
|
||||
break;
|
||||
case "broker":
|
||||
if (e.message[2][0] === "chatEvent") {
|
||||
outputUtterance(chatEventNym(e.message[2]),
|
||||
chatEventUtterance(e.message[2]));
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
G.startStepping();
|
||||
});
|
|
@ -0,0 +1,73 @@
|
|||
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;
|
||||
}
|
|
@ -1,28 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>English</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>boot.sh</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>app.icns</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>com.leastfixedpoint.js-marketplace.chat-demo</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>chat-demo</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>chat-demo 0.0.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>0.0.0</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>10.6</string>
|
||||
</dict>
|
||||
</plist>
|
Binary file not shown.
|
@ -1,3 +0,0 @@
|
|||
#!/bin/bash
|
||||
cd "$(dirname "$0")"/../Resources
|
||||
open examples/chat/index.html
|
|
@ -3,32 +3,15 @@
|
|||
<head>
|
||||
<title>JS Marketplace: Chat Example</title>
|
||||
<meta charset="utf-8">
|
||||
|
||||
<link href="../../third-party/bootstrap/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="../../third-party/bootstrap/css/bootstrap-responsive.min.css" rel="stylesheet">
|
||||
<link href="style.css" rel="stylesheet">
|
||||
<!-- <script src="../../third-party/bootstrap/js/bootstrap.min.js"></script> -->
|
||||
<script src="../../third-party/jquery-2.0.3.min.js"></script>
|
||||
|
||||
<script src="../../marketplace.js"></script>
|
||||
<script src="../../spy.js"></script>
|
||||
<script src="../../dom-driver.js"></script>
|
||||
|
||||
<script src="../../routing-table-widget.js"></script>
|
||||
<link href="../../routing-table-widget.css" rel="stylesheet">
|
||||
|
||||
<script src="../../jquery-driver.js"></script>
|
||||
<script src="../../wake-detector.js"></script>
|
||||
<script src="../../websocket-driver.js"></script>
|
||||
<script src="../../dist/minimart.js"></script>
|
||||
<script src="index.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container-fluid">
|
||||
<!-- <div class="row-fluid"> -->
|
||||
<!-- <div class="span12"> -->
|
||||
<!-- <h1>JS Marketplace</h1> -->
|
||||
<!-- </div> -->
|
||||
<!-- </div> -->
|
||||
<div class="row-fluid">
|
||||
<div class="span12">
|
||||
<form class="form-horizontal" name="nym_form">
|
||||
|
|
|
@ -1,10 +1,14 @@
|
|||
var Route = Minimart.Route;
|
||||
var World = Minimart.World;
|
||||
var Actor = Minimart.Actor;
|
||||
var sub = Minimart.sub;
|
||||
var pub = Minimart.pub;
|
||||
var __ = Minimart.__;
|
||||
var _$ = Minimart._$;
|
||||
|
||||
function chatEvent(nym, status, utterance, stamp) {
|
||||
return ["chatEvent", nym, status, utterance, stamp || +(new Date())];
|
||||
}
|
||||
function chatEventNym(c) { return c[1]; }
|
||||
function chatEventStatus(c) { return c[2]; }
|
||||
function chatEventUtterance(c) { return c[3]; }
|
||||
function chatEventStamp(c) { return c[4]; }
|
||||
|
||||
function outputItem(item) {
|
||||
var stamp = $("<span/>").text((new Date()).toGMTString()).addClass("timestamp");
|
||||
|
@ -15,13 +19,10 @@ function outputItem(item) {
|
|||
return item;
|
||||
}
|
||||
|
||||
function updateNymList(rs) {
|
||||
function updateNymList(allStatuses) {
|
||||
var statuses = {};
|
||||
for (var i = 0; i < rs.length; i++) {
|
||||
var p = rs[i].pattern;
|
||||
if (p[0] === "broker" && p[1] === 0 && p[2][0] === "chatEvent") {
|
||||
statuses[chatEventNym(p[2])] = chatEventStatus(p[2]);
|
||||
}
|
||||
for (var i = 0; i < allStatuses.length; i++) {
|
||||
statuses[allStatuses[i].nym] = allStatuses[i].status;
|
||||
}
|
||||
var nyms = [];
|
||||
for (var nym in statuses) { nyms.push(nym); }
|
||||
|
@ -56,100 +57,81 @@ $(document).ready(function () {
|
|||
$("#nym_form").submit(function (e) { e.preventDefault(); return false; });
|
||||
if (!($("#nym").val())) { $("#nym").val("nym" + Math.floor(Math.random() * 65536)); }
|
||||
|
||||
G = new Ground(function () {
|
||||
G = new Minimart.Ground(function () {
|
||||
console.log('starting ground boot');
|
||||
// World.spawn(new Spy());
|
||||
spawnJQueryDriver();
|
||||
spawnDOMDriver();
|
||||
spawnRoutingTableWidget("#spy-holder", "spy");
|
||||
Minimart.JQuery.spawnJQueryDriver();
|
||||
Minimart.DOM.spawnDOMDriver();
|
||||
Minimart.RoutingTableWidget.spawnRoutingTableWidget("#spy-holder", "spy");
|
||||
|
||||
World.spawn(new WakeDetector());
|
||||
var wsconn = new WebSocketConnection("broker", $("#wsurl").val(), true);
|
||||
World.spawn(new Minimart.WakeDetector());
|
||||
var wsconn = new Minimart.WebSocket.WebSocketConnection("broker", $("#wsurl").val(), true);
|
||||
World.spawn(wsconn);
|
||||
World.spawn({
|
||||
World.spawn(new Actor(function () {
|
||||
// Monitor connection, notifying connectivity changes
|
||||
state: "crashed", // start with this to avoid spurious initial message print
|
||||
boot: function () {
|
||||
World.updateRoutes([sub(["broker_state", __], 0, 1)]);
|
||||
},
|
||||
handleEvent: function (e) {
|
||||
if (e.type === "routes") {
|
||||
var newState = (e.routes.length > 0) ? e.routes[0].pattern[1] : "crashed";
|
||||
this.state = "crashed"; // start with this to avoid spurious initial message print
|
||||
|
||||
Actor.observeAdvertisers(
|
||||
function () { return ["broker_state", _$("newState")]; },
|
||||
{ name: "states" },
|
||||
function () {
|
||||
var newState = this.states.length > 0 ? this.states[0].newState : "crashed";
|
||||
if (this.state != newState) {
|
||||
outputState(newState);
|
||||
this.state = newState;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
World.spawn({
|
||||
});
|
||||
}));
|
||||
World.spawn(new Actor(function () {
|
||||
// Actual chat functionality
|
||||
peers: new PresenceDetector(),
|
||||
peerMap: {},
|
||||
boot: function () {
|
||||
World.updateRoutes(this.subscriptions());
|
||||
},
|
||||
nym: function () { return $("#nym").val(); },
|
||||
currentStatus: function () { return $("#status").val(); },
|
||||
subscriptions: function () {
|
||||
return [sub("wake"),
|
||||
sub(["jQuery", "#send_chat", "click", __]),
|
||||
sub(["jQuery", "#nym", "change", __]),
|
||||
sub(["jQuery", "#status", "change", __]),
|
||||
sub(["jQuery", "#wsurl", "change", __]),
|
||||
pub(["broker", 0, chatEvent(this.nym(), this.currentStatus(), __, __)]),
|
||||
sub(["broker", 0, chatEvent(__, __, __, __)], 0, 1)];
|
||||
},
|
||||
handleEvent: function (e) {
|
||||
var self = this;
|
||||
switch (e.type) {
|
||||
case "routes":
|
||||
updateNymList(e.routes);
|
||||
break;
|
||||
case "message":
|
||||
if (e.message === "wake") {
|
||||
wsconn.forceclose();
|
||||
return;
|
||||
this.nym = function () { return $("#nym").val(); };
|
||||
this.currentStatus = function () { return $("#status").val(); };
|
||||
|
||||
Actor.subscribe(
|
||||
function () { return "wake"; },
|
||||
function () { wsconn.forceclose(); });
|
||||
|
||||
Actor.advertise(
|
||||
function () { return ["broker", 0,
|
||||
chatEvent(this.nym(), this.currentStatus(), __, __)]; });
|
||||
Actor.observeAdvertisers(
|
||||
function () { return ["broker", 0,
|
||||
chatEvent(_$("nym"), _$("status"), __, __)]; },
|
||||
{ name: "allStatuses" },
|
||||
function () { updateNymList(this.allStatuses); });
|
||||
|
||||
Actor.subscribe(
|
||||
function () { return ["jQuery", "#send_chat", "click", __]; },
|
||||
function () {
|
||||
var inp = $("#chat_input");
|
||||
var utterance = inp.val();
|
||||
inp.val("");
|
||||
if (utterance) {
|
||||
World.send(["broker", 0, chatEvent(this.nym(),
|
||||
this.currentStatus(),
|
||||
utterance)]);
|
||||
}
|
||||
switch (e.message[0]) {
|
||||
case "jQuery":
|
||||
switch (e.message[1]) {
|
||||
case "#send_chat":
|
||||
var inp = $("#chat_input");
|
||||
var utterance = inp.val();
|
||||
inp.val("");
|
||||
if (utterance) {
|
||||
World.send(["broker", 0, chatEvent(this.nym(),
|
||||
this.currentStatus(),
|
||||
utterance)]);
|
||||
}
|
||||
break;
|
||||
case "#nym":
|
||||
case "#status":
|
||||
World.updateRoutes(this.subscriptions());
|
||||
break;
|
||||
case "#wsurl":
|
||||
wsconn.forceclose();
|
||||
wsconn.wsurl = $("#wsurl").val();
|
||||
break;
|
||||
default:
|
||||
console.log("Got jquery event from as-yet-unhandled subscription",
|
||||
e.message[2], e.message[3]);
|
||||
}
|
||||
break;
|
||||
case "broker":
|
||||
if (e.message[2][0] === "chatEvent") {
|
||||
outputUtterance(chatEventNym(e.message[2]),
|
||||
chatEventUtterance(e.message[2]));
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Actor.subscribe(
|
||||
function () { return ["jQuery", "#nym", "change", __]; },
|
||||
function () { this.updateRoutes(); });
|
||||
|
||||
Actor.subscribe(
|
||||
function () { return ["jQuery", "#status", "change", __]; },
|
||||
function () { this.updateRoutes(); });
|
||||
|
||||
Actor.subscribe(
|
||||
function () { return ["jQuery", "#wsurl", "change", __]; },
|
||||
function () {
|
||||
wsconn.forceclose();
|
||||
wsconn.wsurl = $("#wsurl").val();
|
||||
});
|
||||
|
||||
Actor.subscribe(
|
||||
function () { return ["broker", 0, chatEvent(_$("who"), __, _$("what"), __)]; },
|
||||
function (who, what) { outputUtterance(who, what); });
|
||||
}));
|
||||
});
|
||||
G.startStepping();
|
||||
});
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title>JS Marketplace: DOM Example</title>
|
||||
<meta charset="utf-8">
|
||||
<link href="../../third-party/bootstrap/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="../../third-party/bootstrap/css/bootstrap-responsive.min.css" rel="stylesheet">
|
||||
<script src="../../third-party/jquery-2.0.3.min.js"></script>
|
||||
<script src="../../dist/minimart.js"></script>
|
||||
<script src="index.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<h1>DOM example</h1>
|
||||
<div class="container-fluid">
|
||||
<div class="row-fluid">
|
||||
<div class="span3 well" id="counter-holder">
|
||||
</div>
|
||||
<div class="span9 well" id="clicker-holder">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row-fluid">
|
||||
<div class="span12" id="spy-holder">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,49 @@
|
|||
var G;
|
||||
$(document).ready(function () {
|
||||
var World = Minimart.World;
|
||||
var sub = Minimart.sub;
|
||||
var pub = Minimart.pub;
|
||||
var __ = Minimart.__;
|
||||
var _$ = Minimart._$;
|
||||
|
||||
G = new Minimart.Ground(function () {
|
||||
console.log('starting ground boot');
|
||||
// World.spawn(new Spy("GROUND", true));
|
||||
Minimart.DOM.spawnDOMDriver();
|
||||
Minimart.RoutingTableWidget.spawnRoutingTableWidget("#spy-holder", "spy");
|
||||
|
||||
World.spawn({
|
||||
boot: function () {
|
||||
return [pub(["DOM", "#clicker-holder", "clicker",
|
||||
["button", ["span", [["style", "font-style: italic"]], "Click me!"]]]),
|
||||
pub("bump_count"),
|
||||
sub(["jQuery", "button.clicker", "click", __])];
|
||||
},
|
||||
handleEvent: function (e) {
|
||||
if (e.type === "message" && e.message[0] === "jQuery") {
|
||||
World.send("bump_count");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
World.spawn({
|
||||
counter: 0,
|
||||
boot: function () {
|
||||
this.updateState();
|
||||
},
|
||||
updateState: function () {
|
||||
World.updateRoutes([sub("bump_count"),
|
||||
pub(["DOM", "#counter-holder", "counter",
|
||||
["div",
|
||||
["p", "The current count is: ", this.counter]]])]);
|
||||
},
|
||||
handleEvent: function (e) {
|
||||
if (e.type === "message" && e.message === "bump_count") {
|
||||
this.counter++;
|
||||
this.updateState();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
G.startStepping();
|
||||
});
|
|
@ -0,0 +1,27 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title>JS Marketplace: DOM Example</title>
|
||||
<meta charset="utf-8">
|
||||
<link href="../../third-party/bootstrap/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="../../third-party/bootstrap/css/bootstrap-responsive.min.css" rel="stylesheet">
|
||||
<script src="../../third-party/jquery-2.0.3.min.js"></script>
|
||||
<script src="../../dist/minimart.js"></script>
|
||||
<script src="index.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<h1>DOM example</h1>
|
||||
<div class="container-fluid">
|
||||
<div class="row-fluid">
|
||||
<div class="span3 well" id="counter-holder">
|
||||
</div>
|
||||
<div class="span9 well" id="clicker-holder">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row-fluid">
|
||||
<div class="span12" id="spy-holder">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,35 @@
|
|||
var G;
|
||||
$(document).ready(function () {
|
||||
var World = Minimart.World;
|
||||
var Actor = Minimart.Actor;
|
||||
var sub = Minimart.sub;
|
||||
var pub = Minimart.pub;
|
||||
var __ = Minimart.__;
|
||||
var _$ = Minimart._$;
|
||||
|
||||
G = new Minimart.Ground(function () {
|
||||
console.log('starting ground boot');
|
||||
// World.spawn(new Spy("GROUND", true));
|
||||
Minimart.DOM.spawnDOMDriver();
|
||||
Minimart.RoutingTableWidget.spawnRoutingTableWidget("#spy-holder", "spy");
|
||||
|
||||
World.spawn(new Actor(function () {
|
||||
Actor.subscribe(
|
||||
function () { return ["jQuery", "button.clicker", "click", __]; },
|
||||
function () {
|
||||
World.send("bump_count");
|
||||
});
|
||||
|
||||
Actor.advertise(
|
||||
function () { return "bump_count"; });
|
||||
Actor.advertise(
|
||||
function () {
|
||||
return ["DOM", "#clicker-holder", "clicker",
|
||||
["button", ["span", [["style", "font-style: italic"]], "Click me!"]]];
|
||||
});
|
||||
}));
|
||||
|
||||
World.spawn(new Minimart.Worker("worker.js"));
|
||||
});
|
||||
G.startStepping();
|
||||
});
|
|
@ -0,0 +1,31 @@
|
|||
importScripts("../../dist/minimart.js");
|
||||
var World = Minimart.World;
|
||||
var Actor = Minimart.Actor;
|
||||
var sub = Minimart.sub;
|
||||
var pub = Minimart.pub;
|
||||
var __ = Minimart.__;
|
||||
var _$ = Minimart._$;
|
||||
|
||||
new Minimart.WorkerGround(function () {
|
||||
console.log('starting worker boot');
|
||||
|
||||
World.spawn(new Actor(function () {
|
||||
this.counter = 0;
|
||||
|
||||
Actor.subscribe(
|
||||
function () { return "bump_count"; },
|
||||
{ metaLevel: 1},
|
||||
function () {
|
||||
this.counter++;
|
||||
this.updateRoutes();
|
||||
});
|
||||
|
||||
Actor.advertise(
|
||||
function () {
|
||||
return ["DOM", "#counter-holder", "counter",
|
||||
["div",
|
||||
["p", "The current count is: ", this.counter]]];
|
||||
},
|
||||
{ metaLevel: 1});
|
||||
}));
|
||||
}).startStepping();
|
|
@ -3,23 +3,10 @@
|
|||
<head>
|
||||
<title>JS Marketplace: DOM Example</title>
|
||||
<meta charset="utf-8">
|
||||
|
||||
<link href="../../third-party/bootstrap/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="../../third-party/bootstrap/css/bootstrap-responsive.min.css" rel="stylesheet">
|
||||
<link href="style.css" rel="stylesheet">
|
||||
<!-- <script src="../../third-party/bootstrap/js/bootstrap.min.js"></script> -->
|
||||
<script src="../../third-party/jquery-2.0.3.min.js"></script>
|
||||
|
||||
<script src="../../marketplace.js"></script>
|
||||
<script src="../../spy.js"></script>
|
||||
<script src="../../dom-driver.js"></script>
|
||||
|
||||
<script src="../../routing-table-widget.js"></script>
|
||||
<link href="../../routing-table-widget.css" rel="stylesheet">
|
||||
|
||||
<script src="../../jquery-driver.js"></script>
|
||||
<script src="../../wake-detector.js"></script>
|
||||
<script src="../../websocket-driver.js"></script>
|
||||
<script src="../../dist/minimart.js"></script>
|
||||
<script src="index.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
|
|
|
@ -1,40 +1,51 @@
|
|||
var G;
|
||||
$(document).ready(function () {
|
||||
G = new Ground(function () {
|
||||
var World = Minimart.World;
|
||||
var Actor = Minimart.Actor;
|
||||
var sub = Minimart.sub;
|
||||
var pub = Minimart.pub;
|
||||
var __ = Minimart.__;
|
||||
var _$ = Minimart._$;
|
||||
|
||||
G = new Minimart.Ground(function () {
|
||||
console.log('starting ground boot');
|
||||
// World.spawn(new Spy("GROUND", true));
|
||||
spawnDOMDriver();
|
||||
spawnRoutingTableWidget("#spy-holder", "spy");
|
||||
Minimart.DOM.spawnDOMDriver();
|
||||
Minimart.RoutingTableWidget.spawnRoutingTableWidget("#spy-holder", "spy");
|
||||
|
||||
World.spawn({
|
||||
handleEvent: function (e) {
|
||||
if (e.type === "message" && e.message[0] === "jQuery") {
|
||||
World.spawn(new Actor(function () {
|
||||
Actor.subscribe(
|
||||
function () { return ["jQuery", "button.clicker", "click", __]; },
|
||||
function () {
|
||||
World.send("bump_count");
|
||||
}
|
||||
}
|
||||
}, [pub(["DOM", "#clicker-holder", "clicker",
|
||||
["button", ["span", {"style": "font-style: italic"}, "Click me!"]]]),
|
||||
pub("bump_count"),
|
||||
sub(["jQuery", "button.clicker", "click", __])]);
|
||||
});
|
||||
|
||||
World.spawn({
|
||||
counter: 0,
|
||||
boot: function () {
|
||||
this.updateState();
|
||||
},
|
||||
updateState: function () {
|
||||
World.updateRoutes([sub("bump_count"),
|
||||
pub(["DOM", "#counter-holder", "counter",
|
||||
["div",
|
||||
["p", "The current count is: ", this.counter]]])]);
|
||||
},
|
||||
handleEvent: function (e) {
|
||||
if (e.type === "message" && e.message === "bump_count") {
|
||||
Actor.advertise(
|
||||
function () { return "bump_count"; });
|
||||
Actor.advertise(
|
||||
function () {
|
||||
return ["DOM", "#clicker-holder", "clicker",
|
||||
["button", ["span", [["style", "font-style: italic"]], "Click me!"]]];
|
||||
});
|
||||
}));
|
||||
|
||||
World.spawn(new Actor(function () {
|
||||
this.counter = 0;
|
||||
|
||||
Actor.subscribe(
|
||||
function () { return "bump_count"; },
|
||||
function () {
|
||||
this.counter++;
|
||||
this.updateState();
|
||||
}
|
||||
}
|
||||
});
|
||||
this.updateRoutes();
|
||||
});
|
||||
|
||||
Actor.advertise(
|
||||
function () {
|
||||
return ["DOM", "#counter-holder", "counter",
|
||||
["div",
|
||||
["p", "The current count is: ", this.counter]]];
|
||||
});
|
||||
}));
|
||||
});
|
||||
G.startStepping();
|
||||
});
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title>JS Marketplace: Multi-user DOM Example</title>
|
||||
<meta charset="utf-8">
|
||||
<link href="../../third-party/bootstrap/css/bootstrap.min.css" rel="stylesheet">
|
||||
<script src="../../third-party/jquery-2.0.3.min.js"></script>
|
||||
<script src="../../dist/minimart.js"></script>
|
||||
<script src="index.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<h1>DOM example</h1>
|
||||
<div class="container-fluid">
|
||||
<div class="row-fluid">
|
||||
<div class="span3 well" id="counter-holder">
|
||||
</div>
|
||||
<div class="span9 well" id="clicker-holder">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row-fluid">
|
||||
<div class="span12" id="spy-holder">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,75 @@
|
|||
var G;
|
||||
$(document).ready(function () {
|
||||
var World = Minimart.World;
|
||||
var sub = Minimart.sub;
|
||||
var pub = Minimart.pub;
|
||||
var __ = Minimart.__;
|
||||
var _$ = Minimart._$;
|
||||
|
||||
G = new Minimart.Ground(function () {
|
||||
var localId = "instance-" + Math.floor(Math.random() * 65536);
|
||||
|
||||
function domWrap(selector, fragmentClass, fragmentSpec) {
|
||||
return ["broker", 0, ["multidom", "DOM", selector, fragmentClass, fragmentSpec]];
|
||||
}
|
||||
|
||||
function jQueryWrap(selector, eventName, eventValue) {
|
||||
var v = eventValue instanceof Event || eventValue instanceof $.Event
|
||||
? Minimart.JQuery.simplifyDOMEvent(eventValue)
|
||||
: eventValue;
|
||||
return ["broker", 0, ["multidom", "jQuery", selector, eventName, v]];
|
||||
}
|
||||
|
||||
var wsconn = new Minimart.WebSocket.WebSocketConnection(
|
||||
"broker", "ws://server.minimart.leastfixedpoint.com:8000/", true);
|
||||
World.spawn(wsconn);
|
||||
|
||||
Minimart.DOM.spawnDOMDriver(domWrap, jQueryWrap); // remote
|
||||
Minimart.DOM.spawnDOMDriver(); // local
|
||||
Minimart.RoutingTableWidget.spawnRoutingTableWidget("#spy-holder", "spy"); // local
|
||||
|
||||
World.spawn({
|
||||
boot: function () {
|
||||
return [pub(domWrap("#clicker-holder", localId + "-clicker",
|
||||
["button", ["span", [["style", "font-style: italic"]],
|
||||
"Click me! (" + localId + ")"]])),
|
||||
pub("bump_count"),
|
||||
sub(jQueryWrap("button."+localId+"-clicker", "click", __))];
|
||||
},
|
||||
handleEvent: function (e) {
|
||||
console.log(JSON.stringify(e));
|
||||
if (e.type === "message"
|
||||
&& e.message[0] === "broker"
|
||||
&& Array.isArray(e.message[2])
|
||||
&& e.message[2][0] === "multidom"
|
||||
&& e.message[2][1] === "jQuery")
|
||||
{
|
||||
World.send("bump_count");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
World.spawn({
|
||||
counter: 0,
|
||||
boot: function () {
|
||||
this.updateState();
|
||||
},
|
||||
updateState: function () {
|
||||
World.updateRoutes([sub("bump_count"),
|
||||
pub(domWrap("#counter-holder", localId + "-counter",
|
||||
["div",
|
||||
["p", "The current count for ",
|
||||
localId,
|
||||
" is: ",
|
||||
this.counter]]))]);
|
||||
},
|
||||
handleEvent: function (e) {
|
||||
if (e.type === "message" && e.message === "bump_count") {
|
||||
this.counter++;
|
||||
this.updateState();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
G.startStepping();
|
||||
});
|
|
@ -0,0 +1,20 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title>JS Marketplace: Smoketest with Web Workers</title>
|
||||
<meta charset="utf-8">
|
||||
<script src="../../third-party/jquery-2.0.3.min.js"></script>
|
||||
<script src="../../dist/minimart.js"></script>
|
||||
<script src="index.js"></script>
|
||||
<style>
|
||||
#gestalt-display {
|
||||
white-space: pre;
|
||||
font-family: monospace;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Smoketest with Web Workers</h1>
|
||||
<div id="gestalt-display"></div>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,49 @@
|
|||
var G;
|
||||
$(document).ready(function () {
|
||||
var World = Minimart.World;
|
||||
var Actor = Minimart.Actor;
|
||||
var sub = Minimart.sub;
|
||||
var pub = Minimart.pub;
|
||||
var __ = Minimart.__;
|
||||
var _$ = Minimart._$;
|
||||
|
||||
G = new Minimart.Ground(function () {
|
||||
console.log('starting ground boot');
|
||||
|
||||
World.spawn({
|
||||
name: 'GestaltDisplay',
|
||||
boot: function () {
|
||||
return [sub(__, 0, 10), pub(__, 0, 10)];
|
||||
},
|
||||
handleEvent: function (e) {
|
||||
if (e.type === "routes") {
|
||||
var gd = document.getElementById('gestalt-display');
|
||||
var t = document.createTextNode(G.world.textProcessTree() + '\n' +
|
||||
e.gestalt.pretty());
|
||||
gd.innerHTML = '';
|
||||
gd.appendChild(t);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
World.spawn(new Actor(function () {
|
||||
this.counter = 0;
|
||||
this.step = function () {
|
||||
if (this.listenerExists && this.counter < 10) {
|
||||
World.send(["beep", this.counter++]);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
Actor.advertise(function () { return ["beep", __]; });
|
||||
Actor.observeSubscribers(
|
||||
function () { return ["beep", __]; },
|
||||
{ presence: "listenerExists" });
|
||||
}));
|
||||
|
||||
World.spawn(new Minimart.Worker("worker.js"));
|
||||
});
|
||||
G.startStepping();
|
||||
});
|
|
@ -0,0 +1,19 @@
|
|||
importScripts("../../dist/minimart.js");
|
||||
var World = Minimart.World;
|
||||
var Actor = Minimart.Actor;
|
||||
var sub = Minimart.sub;
|
||||
var pub = Minimart.pub;
|
||||
var __ = Minimart.__;
|
||||
var _$ = Minimart._$;
|
||||
|
||||
new Minimart.WorkerGround(function () {
|
||||
console.log('starting worker boot');
|
||||
World.spawn(new Actor(function () {
|
||||
Actor.subscribe(
|
||||
function () { return ["beep", _$("counter")]; },
|
||||
{ metaLevel: 1 },
|
||||
function (counter) {
|
||||
console.log("beep!", counter);
|
||||
});
|
||||
}));
|
||||
}).startStepping();
|
|
@ -0,0 +1,13 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title>JS Marketplace: Smoketest</title>
|
||||
<meta charset="utf-8">
|
||||
<script src="../../third-party/jquery-2.0.3.min.js"></script>
|
||||
<script src="../../dist/minimart.js"></script>
|
||||
<script src="index.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Smoketest</h1>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,33 @@
|
|||
var G;
|
||||
$(document).ready(function () {
|
||||
var World = Minimart.World;
|
||||
var Actor = Minimart.Actor;
|
||||
var sub = Minimart.sub;
|
||||
var pub = Minimart.pub;
|
||||
var __ = Minimart.__;
|
||||
var _$ = Minimart._$;
|
||||
|
||||
G = new Minimart.Ground(function () {
|
||||
console.log('starting ground boot');
|
||||
World.spawn(new Minimart.Spy("GROUND", true));
|
||||
|
||||
World.spawn(new Actor(function () {
|
||||
this.counter = 0;
|
||||
this.step = function () {
|
||||
World.send(["beep", this.counter++]);
|
||||
return this.counter <= 10;
|
||||
};
|
||||
|
||||
Actor.advertise(function () { return ["beep", __]; });
|
||||
}));
|
||||
|
||||
World.spawn(new Actor(function () {
|
||||
Actor.subscribe(
|
||||
function () { return ["beep", _$("counter")]; },
|
||||
function (counter) {
|
||||
console.log("beep!", counter);
|
||||
});
|
||||
}));
|
||||
});
|
||||
G.startStepping();
|
||||
});
|
|
@ -9,16 +9,7 @@
|
|||
<link href="style.css" rel="stylesheet">
|
||||
|
||||
<script src="../../third-party/jquery-2.0.3.min.js"></script>
|
||||
<script src="../../marketplace.js"></script>
|
||||
<script src="../../spy.js"></script>
|
||||
<script src="../../dom-driver.js"></script>
|
||||
|
||||
<script src="../../routing-table-widget.js"></script>
|
||||
<link href="../../routing-table-widget.css" rel="stylesheet">
|
||||
|
||||
<script src="../../jquery-driver.js"></script>
|
||||
<script src="../../wake-detector.js"></script>
|
||||
<script src="../../websocket-driver.js"></script>
|
||||
<script src="../../dist/minimart.js"></script>
|
||||
<script src="index.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
|
|
|
@ -1,6 +1,13 @@
|
|||
///////////////////////////////////////////////////////////////////////////
|
||||
// GUI
|
||||
|
||||
var Actor = Minimart.Actor;
|
||||
var World = Minimart.World;
|
||||
var sub = Minimart.sub;
|
||||
var pub = Minimart.pub;
|
||||
var __ = Minimart.__;
|
||||
var _$ = Minimart._$;
|
||||
|
||||
function piece(text, pos, lo, hi, cls) {
|
||||
return "<span class='"+cls+"'>"+
|
||||
((pos >= lo && pos < hi)
|
||||
|
@ -10,51 +17,47 @@ function piece(text, pos, lo, hi, cls) {
|
|||
}
|
||||
|
||||
function spawnGui() {
|
||||
World.spawn({
|
||||
boot: function () {
|
||||
World.updateRoutes([sub(["jQuery", "#inputRow", "+keypress", __]),
|
||||
sub(["fieldContents", __, __], 0, 1),
|
||||
sub(["highlight", __], 0, 1)]);
|
||||
},
|
||||
handleEvent: function (e) {
|
||||
switch (e.type) {
|
||||
case "routes":
|
||||
var text = "", pos = 0, highlight = false;
|
||||
// BUG: escape text!
|
||||
for (var i = 0; i < e.routes.length; i++) {
|
||||
var r = e.routes[i];
|
||||
if (r.pattern[0] === "fieldContents") {
|
||||
text = r.pattern[1];
|
||||
pos = r.pattern[2];
|
||||
} else if (r.pattern[0] === "highlight") {
|
||||
highlight = r.pattern[1];
|
||||
}
|
||||
}
|
||||
$("#fieldContents")[0].innerHTML = highlight
|
||||
? piece(text, pos, 0, highlight[0], "normal") +
|
||||
piece(text, pos, highlight[0], highlight[1], "highlight") +
|
||||
piece(text, pos, highlight[1], text.length + 1, "normal")
|
||||
: piece(text, pos, 0, text.length + 1, "normal");
|
||||
break;
|
||||
case "message":
|
||||
if (e.message[0] === "jQuery") { // it's a keypress event
|
||||
var keycode = e.message[3].keyCode;
|
||||
var character = String.fromCharCode(e.message[3].charCode);
|
||||
if (keycode === 37 /* left */) {
|
||||
World.send(["fieldCommand", "cursorLeft"]);
|
||||
} else if (keycode === 39 /* right */) {
|
||||
World.send(["fieldCommand", "cursorRight"]);
|
||||
} else if (keycode === 9 /* tab */) {
|
||||
// ignore
|
||||
} else if (keycode === 8 /* backspace */) {
|
||||
World.send(["fieldCommand", "backspace"]);
|
||||
} else if (character) {
|
||||
World.send(["fieldCommand", ["insert", character]]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
World.spawn(new Actor(function () {
|
||||
Actor.subscribe(
|
||||
function () { return ["jQuery", "#inputRow", "+keypress", _$("event")]; },
|
||||
function (event) {
|
||||
var keycode = event.keyCode;
|
||||
var character = String.fromCharCode(event.charCode);
|
||||
if (keycode === 37 /* left */) {
|
||||
World.send(["fieldCommand", "cursorLeft"]);
|
||||
} else if (keycode === 39 /* right */) {
|
||||
World.send(["fieldCommand", "cursorRight"]);
|
||||
} else if (keycode === 9 /* tab */) {
|
||||
// ignore
|
||||
} else if (keycode === 8 /* backspace */) {
|
||||
World.send(["fieldCommand", "backspace"]);
|
||||
} else if (character) {
|
||||
World.send(["fieldCommand", ["insert", character]]);
|
||||
}
|
||||
});
|
||||
|
||||
Actor.observeAdvertisers(
|
||||
function () { return ["fieldContents", _$("text"), _$("pos")]; },
|
||||
{ singleton: "field" },
|
||||
updateDisplay);
|
||||
|
||||
Actor.observeAdvertisers(
|
||||
function () { return ["highlight", _$("state")]; },
|
||||
{ singleton: "highlight" },
|
||||
updateDisplay);
|
||||
|
||||
function updateDisplay() {
|
||||
// BUG: escape text!
|
||||
var text = this.field ? this.field.text : "";
|
||||
var pos = this.field ? this.field.pos : 0;
|
||||
var highlight = this.highlight ? this.highlight.state : false;
|
||||
$("#fieldContents")[0].innerHTML = highlight
|
||||
? piece(text, pos, 0, highlight[0], "normal") +
|
||||
piece(text, pos, highlight[0], highlight[1], "highlight") +
|
||||
piece(text, pos, highlight[1], text.length + 1, "normal")
|
||||
: piece(text, pos, 0, text.length + 1, "normal");
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
@ -62,93 +65,80 @@ function spawnGui() {
|
|||
|
||||
function spawnModel() {
|
||||
var initialContents = "initial";
|
||||
World.spawn({
|
||||
fieldContents: initialContents,
|
||||
cursorPos: initialContents.length, /* positions address gaps between characters */
|
||||
boot: function () {
|
||||
World.updateRoutes(this.subscriptions());
|
||||
},
|
||||
subscriptions: function () {
|
||||
return [sub(["fieldCommand", __]),
|
||||
pub(["fieldContents", this.fieldContents, this.cursorPos])];
|
||||
},
|
||||
handleEvent: function (e) {
|
||||
switch (e.type) {
|
||||
case "message":
|
||||
if (e.message[1] === "cursorLeft") {
|
||||
this.cursorPos--;
|
||||
if (this.cursorPos < 0)
|
||||
this.cursorPos = 0;
|
||||
} else if (e.message[1] === "cursorRight") {
|
||||
this.cursorPos++;
|
||||
if (this.cursorPos > this.fieldContents.length)
|
||||
this.cursorPos = this.fieldContents.length;
|
||||
} else if (e.message[1] === "backspace" && this.cursorPos > 0) {
|
||||
this.fieldContents =
|
||||
this.fieldContents.substring(0, this.cursorPos - 1) +
|
||||
this.fieldContents.substring(this.cursorPos);
|
||||
this.cursorPos--;
|
||||
} else if (e.message[1].constructor === Array && e.message[1][0] === "insert") {
|
||||
var newText = e.message[1][1];
|
||||
this.fieldContents =
|
||||
this.fieldContents.substring(0, this.cursorPos) +
|
||||
newText +
|
||||
this.fieldContents.substring(this.cursorPos);
|
||||
this.cursorPos += newText.length;
|
||||
}
|
||||
World.updateRoutes(this.subscriptions());
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
World.spawn(new Actor(function () {
|
||||
this.fieldContents = initialContents;
|
||||
this.cursorPos = initialContents.length; /* positions address gaps between characters */
|
||||
|
||||
Actor.advertise(
|
||||
function () { return ["fieldContents", this.fieldContents, this.cursorPos]; });
|
||||
|
||||
Actor.subscribe(
|
||||
function () { return ["fieldCommand", _$("command")]; },
|
||||
function (command) {
|
||||
if (command === "cursorLeft") {
|
||||
this.cursorPos--;
|
||||
if (this.cursorPos < 0)
|
||||
this.cursorPos = 0;
|
||||
} else if (command === "cursorRight") {
|
||||
this.cursorPos++;
|
||||
if (this.cursorPos > this.fieldContents.length)
|
||||
this.cursorPos = this.fieldContents.length;
|
||||
} else if (command === "backspace" && this.cursorPos > 0) {
|
||||
this.fieldContents =
|
||||
this.fieldContents.substring(0, this.cursorPos - 1) +
|
||||
this.fieldContents.substring(this.cursorPos);
|
||||
this.cursorPos--;
|
||||
} else if (command.constructor === Array && command[0] === "insert") {
|
||||
var newText = command[1];
|
||||
this.fieldContents =
|
||||
this.fieldContents.substring(0, this.cursorPos) +
|
||||
newText +
|
||||
this.fieldContents.substring(this.cursorPos);
|
||||
this.cursorPos += newText.length;
|
||||
}
|
||||
this.updateRoutes();
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// Search engine
|
||||
|
||||
function spawnSearch() {
|
||||
World.spawn({
|
||||
fieldContents: "",
|
||||
highlight: false,
|
||||
boot: function () {
|
||||
World.updateRoutes(this.subscriptions());
|
||||
},
|
||||
subscriptions: function () {
|
||||
return [sub(["jQuery", "#searchBox", "input", __]),
|
||||
sub(["fieldContents", __, __], 0, 1),
|
||||
pub(["highlight", this.highlight])];
|
||||
},
|
||||
search: function () {
|
||||
var searchtext = $("#searchBox")[0].value;
|
||||
var oldHighlight = this.highlight;
|
||||
if (searchtext) {
|
||||
var pos = this.fieldContents.indexOf(searchtext);
|
||||
this.highlight = (pos !== -1) && [pos, pos + searchtext.length];
|
||||
} else {
|
||||
this.highlight = false;
|
||||
}
|
||||
if (JSON.stringify(oldHighlight) !== JSON.stringify(this.highlight)) {
|
||||
World.updateRoutes(this.subscriptions());
|
||||
}
|
||||
},
|
||||
handleEvent: function (e) {
|
||||
switch (e.type) {
|
||||
case "routes":
|
||||
for (var i = 0; i < e.routes.length; i++) {
|
||||
var r = e.routes[i];
|
||||
if (r.pattern[0] === "fieldContents") {
|
||||
this.fieldContents = r.pattern[1];
|
||||
}
|
||||
}
|
||||
this.search();
|
||||
break;
|
||||
case "message":
|
||||
if (e.message[0] === "jQuery") { // it's a search box input event
|
||||
this.search();
|
||||
}
|
||||
}
|
||||
World.spawn(new Actor(function () {
|
||||
var self = this;
|
||||
self.fieldContents = "";
|
||||
self.highlight = false;
|
||||
|
||||
Actor.advertise(
|
||||
function () { return ["highlight", self.highlight]; });
|
||||
|
||||
Actor.subscribe(
|
||||
function () { return ["jQuery", "#searchBox", "input", _$("event")]; },
|
||||
search);
|
||||
|
||||
Actor.observeAdvertisers(
|
||||
function () { return ["fieldContents", _$("text"), _$("pos")]; },
|
||||
{ singleton: "field" },
|
||||
function () {
|
||||
self.fieldContents = self.field ? self.field.text : "";
|
||||
search();
|
||||
});
|
||||
|
||||
function search() {
|
||||
var searchtext = $("#searchBox")[0].value;
|
||||
var oldHighlight = self.highlight;
|
||||
if (searchtext) {
|
||||
var pos = self.fieldContents.indexOf(searchtext);
|
||||
self.highlight = (pos !== -1) && [pos, pos + searchtext.length];
|
||||
} else {
|
||||
self.highlight = false;
|
||||
}
|
||||
});
|
||||
if (JSON.stringify(oldHighlight) !== JSON.stringify(self.highlight)) {
|
||||
self.updateRoutes();
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
@ -156,10 +146,10 @@ function spawnSearch() {
|
|||
|
||||
var G;
|
||||
$(document).ready(function () {
|
||||
G = new Ground(function () {
|
||||
spawnJQueryDriver();
|
||||
spawnDOMDriver();
|
||||
spawnRoutingTableWidget("#spy-holder", "spy");
|
||||
G = new Minimart.Ground(function () {
|
||||
Minimart.JQuery.spawnJQueryDriver();
|
||||
Minimart.DOM.spawnDOMDriver();
|
||||
Minimart.RoutingTableWidget.spawnRoutingTableWidget("#spy-holder", "spy");
|
||||
|
||||
spawnGui();
|
||||
spawnModel();
|
||||
|
|
|
@ -1,46 +0,0 @@
|
|||
// JQuery event driver
|
||||
|
||||
function spawnJQueryDriver(baseSelector, metaLevel) {
|
||||
metaLevel = metaLevel || 0;
|
||||
var d = new DemandMatcher(["jQuery", __, __, __], metaLevel);
|
||||
d.onDemandIncrease = function (r) {
|
||||
var selector = r.pattern[1];
|
||||
var eventName = r.pattern[2];
|
||||
World.spawn(new JQueryEventRouter(baseSelector, selector, eventName, metaLevel),
|
||||
[pub(["jQuery", selector, eventName, __], metaLevel),
|
||||
pub(["jQuery", selector, eventName, __], metaLevel, 1)]);
|
||||
};
|
||||
World.spawn(d);
|
||||
}
|
||||
|
||||
function JQueryEventRouter(baseSelector, selector, eventName, metaLevel) {
|
||||
var self = this;
|
||||
this.baseSelector = baseSelector || null;
|
||||
this.selector = selector;
|
||||
this.eventName = eventName;
|
||||
this.metaLevel = metaLevel || 0;
|
||||
this.preventDefault = (this.eventName.charAt(0) !== "+");
|
||||
this.handler =
|
||||
World.wrap(function (e) {
|
||||
World.send(["jQuery", self.selector, self.eventName, e], self.metaLevel);
|
||||
if (self.preventDefault) e.preventDefault();
|
||||
return !self.preventDefault;
|
||||
});
|
||||
this.computeNodes().on(this.preventDefault ? this.eventName : this.eventName.substring(1),
|
||||
this.handler);
|
||||
}
|
||||
|
||||
JQueryEventRouter.prototype.handleEvent = function (e) {
|
||||
if (e.type === "routes" && e.routes.length === 0) {
|
||||
this.computeNodes().off(this.eventName, this.handler);
|
||||
World.exit();
|
||||
}
|
||||
};
|
||||
|
||||
JQueryEventRouter.prototype.computeNodes = function () {
|
||||
if (this.baseSelector) {
|
||||
return $(this.baseSelector).children(this.selector).addBack(this.selector);
|
||||
} else {
|
||||
return $(this.selector);
|
||||
}
|
||||
};
|
635
marketplace.js
635
marketplace.js
|
@ -1,635 +0,0 @@
|
|||
/*---------------------------------------------------------------------------*/
|
||||
/* Unification */
|
||||
|
||||
var __ = new Object(); /* wildcard marker */
|
||||
__.__ = "__";
|
||||
|
||||
function unificationFailed() {
|
||||
throw {unificationFailed: true};
|
||||
}
|
||||
|
||||
function unify1(a, b) {
|
||||
var i;
|
||||
|
||||
if (a === __) return b;
|
||||
if (b === __) return a;
|
||||
|
||||
if (a === b) return a;
|
||||
|
||||
if (Array.isArray(a) && Array.isArray(b)) {
|
||||
if (a.length !== b.length) unificationFailed();
|
||||
var result = new Array(a.length);
|
||||
for (i = 0; i < a.length; i++) {
|
||||
result[i] = unify1(a[i], b[i]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
if (typeof a === "object" && typeof b === "object") {
|
||||
/* TODO: consider other kinds of matching. I've chosen to
|
||||
require any field mentioned by either side to be present in
|
||||
both. Does that make sense? */
|
||||
var result = ({});
|
||||
for (i in a) { if (a.hasOwnProperty(i)) result[i] = true; }
|
||||
for (i in b) { if (b.hasOwnProperty(i)) result[i] = true; }
|
||||
for (i in result) {
|
||||
if (result.hasOwnProperty(i)) {
|
||||
result[i] = unify1(a[i], b[i]);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
unificationFailed();
|
||||
}
|
||||
|
||||
function unify(a, b) {
|
||||
try {
|
||||
// console.log("unify", JSON.stringify(a), JSON.stringify(b));
|
||||
return {result: unify1(a, b)};
|
||||
} catch (e) {
|
||||
if (e.unificationFailed) return undefined;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
function anyUnify(aa, bb) {
|
||||
for (var i = 0; i < aa.length; i++) {
|
||||
for (var j = 0; j < bb.length; j++) {
|
||||
if (unify(aa[i], bb[j])) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/*---------------------------------------------------------------------------*/
|
||||
/* Events and Actions */
|
||||
|
||||
function Route(isSubscription, pattern, metaLevel, level) {
|
||||
this.isSubscription = isSubscription;
|
||||
this.pattern = pattern;
|
||||
this.metaLevel = (metaLevel === undefined) ? 0 : metaLevel;
|
||||
this.level = (level === undefined) ? 0 : level;
|
||||
}
|
||||
|
||||
Route.prototype.drop = function () {
|
||||
if (this.metaLevel === 0) { return null; }
|
||||
return new Route(this.isSubscription, this.pattern, this.metaLevel - 1, this.level);
|
||||
};
|
||||
|
||||
Route.prototype.lift = function () {
|
||||
return new Route(this.isSubscription, this.pattern, this.metaLevel + 1, this.level);
|
||||
};
|
||||
|
||||
Route.prototype.toJSON = function () {
|
||||
return [this.isSubscription ? "sub" : "pub", this.pattern, this.metaLevel, this.level];
|
||||
};
|
||||
|
||||
Route.prototype.visibilityToRoute = function (other, overrideOtherLevel) {
|
||||
if (!this.isSubscription !== other.isSubscription) return undefined;
|
||||
if (this.metaLevel !== other.metaLevel) return undefined;
|
||||
if (this.level >= (overrideOtherLevel || other.level)) return undefined;
|
||||
return unify(this.pattern, other.pattern); // returns undefined if unification fails
|
||||
};
|
||||
|
||||
Route.fromJSON = function (j) {
|
||||
switch (j[0]) {
|
||||
case "sub": return new Route(true, j[1], j[2], j[3]);
|
||||
case "pub": return new Route(false, j[1], j[2], j[3]);
|
||||
default: throw { message: "Invalid JSON-encoded route: " + JSON.stringify(j) };
|
||||
}
|
||||
};
|
||||
|
||||
function sub(pattern, metaLevel, level) {
|
||||
return new Route(true, pattern, metaLevel, level);
|
||||
}
|
||||
|
||||
function pub(pattern, metaLevel, level) {
|
||||
return new Route(false, pattern, metaLevel, level);
|
||||
}
|
||||
|
||||
function spawn(behavior, initialRoutes) {
|
||||
return { type: "spawn",
|
||||
behavior: behavior,
|
||||
initialRoutes: (initialRoutes === undefined) ? [] : initialRoutes };
|
||||
}
|
||||
|
||||
function updateRoutes(routes) {
|
||||
return { type: "routes", routes: routes };
|
||||
}
|
||||
|
||||
function sendMessage(m, metaLevel, isFeedback) {
|
||||
return { type: "message",
|
||||
metaLevel: (metaLevel === undefined) ? 0 : metaLevel,
|
||||
message: m,
|
||||
isFeedback: (isFeedback === undefined) ? false : isFeedback };
|
||||
}
|
||||
|
||||
function shutdownWorld() {
|
||||
return { type: "shutdownWorld" };
|
||||
}
|
||||
|
||||
/*---------------------------------------------------------------------------*/
|
||||
/* Metafunctions */
|
||||
|
||||
function dropRoutes(routes) {
|
||||
var result = [];
|
||||
for (var i = 0; i < routes.length; i++) {
|
||||
var r = routes[i].drop();
|
||||
if (r) { result.push(r); }
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function liftRoutes(routes) {
|
||||
var result = [];
|
||||
for (var i = 0; i < routes.length; i++) {
|
||||
result.push(routes[i].lift());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function intersectRoutes(rs1, rs2) {
|
||||
var result = [];
|
||||
for (var i = 0; i < rs1.length; i++) {
|
||||
for (var j = 0; j < rs2.length; j++) {
|
||||
var ri = rs1[i];
|
||||
var rj = rs2[j];
|
||||
var u = ri.visibilityToRoute(rj);
|
||||
if (u) {
|
||||
var rk = new Route(ri.isSubscription, u.result, ri.metaLevel, ri.level);
|
||||
result.push(rk);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function filterEvent(e, routes) {
|
||||
switch (e.type) {
|
||||
case "routes":
|
||||
return updateRoutes(intersectRoutes(e.routes, routes));
|
||||
case "message":
|
||||
for (var i = 0; i < routes.length; i++) {
|
||||
var r = routes[i];
|
||||
if (e.metaLevel === r.metaLevel
|
||||
&& e.isFeedback === !r.isSubscription
|
||||
&& unify(e.message, r.pattern))
|
||||
{
|
||||
return e;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
default:
|
||||
throw { message: "Event type " + e.type + " not filterable",
|
||||
event: e };
|
||||
}
|
||||
}
|
||||
|
||||
/*---------------------------------------------------------------------------*/
|
||||
/* Configurations */
|
||||
|
||||
function World(bootFn) {
|
||||
this.nextPid = 0;
|
||||
this.alive = true;
|
||||
this.eventQueue = [];
|
||||
this.processTable = {};
|
||||
this.downwardRoutes = [];
|
||||
this.processActions = [];
|
||||
this.activePid = null;
|
||||
this.stepperId = null;
|
||||
this.asChild(-1, bootFn, true);
|
||||
}
|
||||
|
||||
/* Class state / methods */
|
||||
|
||||
World.stack = [];
|
||||
|
||||
World.current = function () {
|
||||
return World.stack[World.stack.length - 1];
|
||||
};
|
||||
|
||||
World.send = function (m, metaLevel, isFeedback) {
|
||||
World.current().enqueueAction(sendMessage(m, metaLevel, isFeedback));
|
||||
};
|
||||
|
||||
World.updateRoutes = function (routes) {
|
||||
World.current().enqueueAction(updateRoutes(routes));
|
||||
};
|
||||
|
||||
World.spawn = function (behavior, initialRoutes) {
|
||||
World.current().enqueueAction(spawn(behavior, initialRoutes));
|
||||
};
|
||||
|
||||
World.exit = function (exn) {
|
||||
World.current().killActive(exn);
|
||||
};
|
||||
|
||||
World.shutdownWorld = function () {
|
||||
World.current().enqueueAction(shutdownWorld());
|
||||
};
|
||||
|
||||
World.withWorldStack = function (stack, f) {
|
||||
var oldStack = World.stack;
|
||||
World.stack = stack;
|
||||
var result = null;
|
||||
try {
|
||||
result = f();
|
||||
} catch (e) {
|
||||
World.stack = oldStack;
|
||||
throw e;
|
||||
}
|
||||
World.stack = oldStack;
|
||||
return result;
|
||||
};
|
||||
|
||||
World.wrap = function (f) {
|
||||
var savedStack = World.stack.slice();
|
||||
var savedPid = World.current().activePid;
|
||||
return function () {
|
||||
var actuals = arguments;
|
||||
return World.withWorldStack(savedStack, function () {
|
||||
var result = World.current().asChild(savedPid, function () {
|
||||
return f.apply(null, actuals);
|
||||
});
|
||||
World.stack[0].startStepping();
|
||||
return result;
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
/* Instance methods */
|
||||
|
||||
World.prototype.killActive = function (exn) {
|
||||
this.kill(this.activePid, exn);
|
||||
};
|
||||
|
||||
World.prototype.enqueueAction = function (action) {
|
||||
this.processActions.push([this.activePid, action]);
|
||||
};
|
||||
|
||||
World.prototype.isQuiescent = function () {
|
||||
return this.eventQueue.length === 0 && this.processActions.length === 0;
|
||||
};
|
||||
|
||||
World.prototype.step = function () {
|
||||
this.dispatchEvents();
|
||||
this.performActions();
|
||||
return this.alive && (this.stepChildren() || !this.isQuiescent());
|
||||
};
|
||||
|
||||
World.prototype.startStepping = function () {
|
||||
var self = this;
|
||||
if (this.stepperId) return;
|
||||
if (this.step()) {
|
||||
this.stepperId = setTimeout(function () {
|
||||
self.stepperId = null;
|
||||
self.startStepping();
|
||||
}, 0);
|
||||
}
|
||||
};
|
||||
|
||||
World.prototype.stopStepping = function () {
|
||||
if (this.stepperId) {
|
||||
clearTimeout(this.stepperId);
|
||||
this.stepperId = null;
|
||||
}
|
||||
};
|
||||
|
||||
World.prototype.asChild = function (pid, f, omitLivenessCheck) {
|
||||
if (!(pid in this.processTable) && !omitLivenessCheck) {
|
||||
console.warn("World.asChild eliding invocation of dead process", pid);
|
||||
return;
|
||||
}
|
||||
|
||||
World.stack.push(this);
|
||||
var result = null;
|
||||
this.activePid = pid;
|
||||
try {
|
||||
result = f();
|
||||
} catch (e) {
|
||||
this.kill(pid, e);
|
||||
}
|
||||
this.activePid = null;
|
||||
if (World.stack.pop() !== this) {
|
||||
throw { message: "Internal error: World stack imbalance" };
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
World.prototype.kill = function (pid, exn) {
|
||||
if (exn && exn.stack) {
|
||||
console.log("Process exited", pid, exn, exn.stack);
|
||||
} else {
|
||||
console.log("Process exited", pid, exn);
|
||||
}
|
||||
var p = this.processTable[pid];
|
||||
if (p && p.behavior.trapexit) {
|
||||
this.asChild(pid, function () { return p.behavior.trapexit(exn); });
|
||||
}
|
||||
delete this.processTable[pid];
|
||||
this.issueRoutingUpdate();
|
||||
};
|
||||
|
||||
World.prototype.stepChildren = function () {
|
||||
var someChildBusy = false;
|
||||
for (var pid in this.processTable) {
|
||||
var p = this.processTable[pid];
|
||||
if (p.behavior.step /* exists, haven't called it yet */) {
|
||||
var childBusy = this.asChild(pid, function () { return p.behavior.step() });
|
||||
someChildBusy = someChildBusy || childBusy;
|
||||
}
|
||||
}
|
||||
return someChildBusy;
|
||||
};
|
||||
|
||||
World.prototype.performActions = function () {
|
||||
var queue = this.processActions;
|
||||
this.processActions = [];
|
||||
var item;
|
||||
while ((item = queue.shift()) && this.alive) {
|
||||
this.performAction(item[0], item[1]);
|
||||
}
|
||||
};
|
||||
|
||||
World.prototype.dispatchEvents = function () {
|
||||
var queue = this.eventQueue;
|
||||
this.eventQueue = [];
|
||||
var item;
|
||||
while ((item = queue.shift())) {
|
||||
this.dispatchEvent(item);
|
||||
}
|
||||
};
|
||||
|
||||
World.prototype.performAction = function (pid, action) {
|
||||
switch (action.type) {
|
||||
case "spawn":
|
||||
var pid = this.nextPid++;
|
||||
this.processTable[pid] = { routes: action.initialRoutes, behavior: action.behavior };
|
||||
if (action.behavior.boot) { this.asChild(pid, function () { action.behavior.boot() }); }
|
||||
this.issueRoutingUpdate();
|
||||
break;
|
||||
case "routes":
|
||||
if (pid in this.processTable) {
|
||||
// it may not be: this might be the routing update from a
|
||||
// kill of the process
|
||||
this.processTable[pid].routes = action.routes;
|
||||
}
|
||||
this.issueRoutingUpdate();
|
||||
break;
|
||||
case "message":
|
||||
if (action.metaLevel === 0) {
|
||||
this.eventQueue.push(action);
|
||||
} else {
|
||||
World.send(action.message, action.metaLevel - 1, action.isFeedback);
|
||||
}
|
||||
break;
|
||||
case "shutdownWorld":
|
||||
this.alive = false; // force us to stop doing things immediately
|
||||
World.exit();
|
||||
break;
|
||||
default:
|
||||
throw { message: "Action type " + action.type + " not understood",
|
||||
action: action };
|
||||
}
|
||||
};
|
||||
|
||||
World.prototype.aggregateRoutes = function (base) {
|
||||
var acc = base.slice();
|
||||
for (var pid in this.processTable) {
|
||||
var p = this.processTable[pid];
|
||||
for (var i = 0; i < p.routes.length; i++) {
|
||||
acc.push(p.routes[i]);
|
||||
}
|
||||
}
|
||||
return acc;
|
||||
};
|
||||
|
||||
World.prototype.issueLocalRoutingUpdate = function () {
|
||||
this.eventQueue.push(updateRoutes(this.aggregateRoutes(this.downwardRoutes)));
|
||||
};
|
||||
|
||||
World.prototype.issueRoutingUpdate = function () {
|
||||
this.issueLocalRoutingUpdate();
|
||||
World.updateRoutes(dropRoutes(this.aggregateRoutes([])));
|
||||
};
|
||||
|
||||
World.prototype.dispatchEvent = function (e) {
|
||||
for (var pid in this.processTable) {
|
||||
var p = this.processTable[pid];
|
||||
var e1 = filterEvent(e, p.routes);
|
||||
// console.log("filtering", e, p.routes, e1);
|
||||
if (e1) { this.asChild(pid, function () { p.behavior.handleEvent(e1) }); }
|
||||
}
|
||||
};
|
||||
|
||||
World.prototype.handleEvent = function (e) {
|
||||
switch (e.type) {
|
||||
case "routes":
|
||||
this.downwardRoutes = liftRoutes(e.routes);
|
||||
this.issueLocalRoutingUpdate();
|
||||
break;
|
||||
case "message":
|
||||
this.eventQueue.push(sendMessage(e.message, e.metaLevel + 1, e.isFeedback));
|
||||
break;
|
||||
default:
|
||||
throw { message: "Event type " + e.type + " not understood",
|
||||
event: e };
|
||||
}
|
||||
};
|
||||
|
||||
/*---------------------------------------------------------------------------*/
|
||||
/* Utilities: detecting presence/absence events via routing events */
|
||||
|
||||
function PresenceDetector(initialRoutes) {
|
||||
this.state = this._digestRoutes(initialRoutes === undefined ? [] : initialRoutes);
|
||||
}
|
||||
|
||||
PresenceDetector.prototype._digestRoutes = function (routes) {
|
||||
var newState = {};
|
||||
for (var i = 0; i < routes.length; i++) {
|
||||
newState[JSON.stringify(routes[i].toJSON())] = routes[i];
|
||||
}
|
||||
return newState;
|
||||
};
|
||||
|
||||
PresenceDetector.prototype.getRouteList = function () {
|
||||
var rs = [];
|
||||
for (var k in this.state) { rs.push(this.state[k]); }
|
||||
return rs;
|
||||
};
|
||||
|
||||
PresenceDetector.prototype.handleRoutes = function (routes) {
|
||||
var added = [];
|
||||
var removed = [];
|
||||
var newState = this._digestRoutes(routes);
|
||||
for (var k in newState) {
|
||||
if (!(k in this.state)) {
|
||||
added.push(newState[k]);
|
||||
} else {
|
||||
delete this.state[k];
|
||||
}
|
||||
}
|
||||
for (var k in this.state) {
|
||||
removed.push(this.state[k]);
|
||||
}
|
||||
this.state = newState;
|
||||
return { added: added, removed: removed };
|
||||
};
|
||||
|
||||
PresenceDetector.prototype.presenceExistsFor = function (probeRoute) {
|
||||
for (var k in this.state) {
|
||||
var existingRoute = this.state[k];
|
||||
if (existingRoute.visibleToRoute(probeRoute, Infinity) &&
|
||||
(existingRoute.level === probeRoute.level))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
/*---------------------------------------------------------------------------*/
|
||||
/* Utilities: matching demand for some service */
|
||||
|
||||
function DemandMatcher(pattern, metaLevel, options) {
|
||||
options = $.extend({
|
||||
demandLevel: 0,
|
||||
supplyLevel: 0,
|
||||
demandSideIsSubscription: true
|
||||
}, options);
|
||||
this.pattern = pattern;
|
||||
this.metaLevel = metaLevel;
|
||||
this.demandLevel = options.demandLevel;
|
||||
this.supplyLevel = options.supplyLevel;
|
||||
this.demandSideIsSubscription = options.demandSideIsSubscription;
|
||||
this.onDemandIncrease = function (r) {
|
||||
console.error("Unhandled increase in demand for route", r);
|
||||
};
|
||||
this.onSupplyDecrease = function (r) {
|
||||
console.error("Unhandled decrease in supply for route", r);
|
||||
};
|
||||
this.state = new PresenceDetector();
|
||||
}
|
||||
|
||||
DemandMatcher.prototype.boot = function () {
|
||||
World.updateRoutes([this.computeDetector(true),
|
||||
this.computeDetector(false)]);
|
||||
};
|
||||
|
||||
DemandMatcher.prototype.handleEvent = function (e) {
|
||||
if (e.type === "routes") {
|
||||
this.handleRoutes(e.routes);
|
||||
}
|
||||
};
|
||||
|
||||
DemandMatcher.prototype.computeDetector = function (demandSide) {
|
||||
var maxLevel = (this.demandLevel > this.supplyLevel ? this.demandLevel : this.supplyLevel);
|
||||
return new Route(this.demandSideIsSubscription ? !demandSide : demandSide,
|
||||
this.pattern,
|
||||
this.metaLevel,
|
||||
maxLevel + 1);
|
||||
};
|
||||
|
||||
DemandMatcher.prototype.handleRoutes = function (routes) {
|
||||
var changes = this.state.handleRoutes(routes);
|
||||
this.incorporateChanges(true, changes.added);
|
||||
this.incorporateChanges(false, changes.removed);
|
||||
};
|
||||
|
||||
DemandMatcher.prototype.incorporateChanges = function (isArrivals, routeList) {
|
||||
var relevantChangeDetector = this.computeDetector(isArrivals);
|
||||
var expectedChangeLevel = isArrivals ? this.demandLevel : this.supplyLevel;
|
||||
var expectedPeerLevel = isArrivals ? this.supplyLevel : this.demandLevel;
|
||||
for (var i = 0; i < routeList.length; i++) {
|
||||
var changed = routeList[i];
|
||||
if (changed.level != expectedChangeLevel) continue;
|
||||
var relevantChangedN = intersectRoutes([changed], [relevantChangeDetector]);
|
||||
if (relevantChangedN.length === 0) continue;
|
||||
var relevantChanged = relevantChangedN[0]; /* there can be only one */
|
||||
var peerDetector = new Route(relevantChanged.isSubscription,
|
||||
relevantChanged.pattern,
|
||||
relevantChanged.metaLevel,
|
||||
expectedPeerLevel + 1);
|
||||
var peerRoutes = intersectRoutes(this.state.getRouteList(), [peerDetector]);
|
||||
var peerExists = false;
|
||||
for (var j = 0; j < peerRoutes.length; j++) {
|
||||
if (peerRoutes[j].level == expectedPeerLevel) {
|
||||
peerExists = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (isArrivals && !peerExists) { this.onDemandIncrease(relevantChanged); }
|
||||
if (!isArrivals && peerExists) { this.onSupplyDecrease(relevantChanged); }
|
||||
}
|
||||
};
|
||||
|
||||
/*---------------------------------------------------------------------------*/
|
||||
/* Utilities: deduplicator */
|
||||
|
||||
function Deduplicator(ttl_ms) {
|
||||
this.ttl_ms = ttl_ms || 10000;
|
||||
this.queue = [];
|
||||
this.map = {};
|
||||
this.timerId = null;
|
||||
}
|
||||
|
||||
Deduplicator.prototype.accept = function (m) {
|
||||
var s = JSON.stringify(m);
|
||||
if (s in this.map) return false;
|
||||
var entry = [(+new Date()) + this.ttl_ms, s, m];
|
||||
this.map[s] = entry;
|
||||
this.queue.push(entry);
|
||||
|
||||
if (this.timerId === null) {
|
||||
var self = this;
|
||||
this.timerId = setInterval(function () { self.expireMessages(); },
|
||||
this.ttl_ms > 1000 ? 1000 : this.ttl_ms);
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
Deduplicator.prototype.expireMessages = function () {
|
||||
var now = +new Date();
|
||||
while (this.queue.length > 0 && this.queue[0][0] <= now) {
|
||||
var entry = this.queue.shift();
|
||||
delete this.map[entry[1]];
|
||||
}
|
||||
if (this.queue.length === 0) {
|
||||
clearInterval(this.timerId);
|
||||
this.timerId = null;
|
||||
}
|
||||
};
|
||||
|
||||
/*---------------------------------------------------------------------------*/
|
||||
/* Ground interface */
|
||||
|
||||
function Ground(bootFn) {
|
||||
var self = this;
|
||||
this.stepperId = null;
|
||||
this.state = new PresenceDetector();
|
||||
World.withWorldStack([this], function () {
|
||||
self.world = new World(bootFn);
|
||||
});
|
||||
}
|
||||
|
||||
Ground.prototype.step = function () {
|
||||
var self = this;
|
||||
return World.withWorldStack([this], function () {
|
||||
return self.world.step();
|
||||
});
|
||||
};
|
||||
|
||||
Ground.prototype.startStepping = World.prototype.startStepping;
|
||||
Ground.prototype.stopStepping = World.prototype.stopStepping;
|
||||
|
||||
Ground.prototype.enqueueAction = function (action) {
|
||||
if (action.type === 'routes') {
|
||||
var added = this.state.handleRoutes(action.routes).added;
|
||||
if (added.length > 0) {
|
||||
console.error("You have subscribed to a nonexistent event source.", added);
|
||||
}
|
||||
} else {
|
||||
console.error("You have sent a message into the outer void.", action);
|
||||
}
|
||||
};
|
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"name": "js-marketplace",
|
||||
"version": "0.0.0",
|
||||
"description": "Network Calculus in the browser",
|
||||
"homepage": "https://github.com/tonyg/js-marketplace",
|
||||
"main": "src/main.js",
|
||||
"scripts": {
|
||||
"clean": "rm -f dist/*",
|
||||
"build-debug": "browserify src/main.js -d -s Minimart -o dist/minimart.js",
|
||||
"build-min": "browserify src/main.js -s Minimart -o dist/_minimart.js && uglifyjs dist/_minimart.js -o dist/minimart.min.js && rm dist/_minimart.js",
|
||||
"build": "npm run build-debug && npm run build-min",
|
||||
"watch": "watchify src/main.js -d -s Minimart -o dist/minimart.js",
|
||||
"test": "mocha",
|
||||
"prepublish": "npm run build"
|
||||
},
|
||||
"author": "Tony Garnock-Jones <tonyg@ccs.neu.edu>",
|
||||
"devDependencies": {
|
||||
"watchify": "^0.6.1",
|
||||
"uglify-js": "^2.4.12",
|
||||
"browserify": "^3.30.4",
|
||||
"mocha": "^1.17.1",
|
||||
"expect.js": "^0.3.1"
|
||||
}
|
||||
}
|
|
@ -1,49 +0,0 @@
|
|||
.routing-table li { list-style-type: none; }
|
||||
|
||||
.routing-table .sub .pattern { background-color: lightblue; }
|
||||
.routing-table .sub .level { background-color: lightblue; }
|
||||
.routing-table .pub .pattern { background-color: lightgreen; }
|
||||
.routing-table .pub .level { background-color: lightgreen; }
|
||||
|
||||
.routing-table .route {
|
||||
display: inline-block;
|
||||
height: 2em;
|
||||
}
|
||||
|
||||
.routing-table .route .level { font-style: italic; line-height: 1em; padding: 0 0.5em; }
|
||||
.routing-table .route .polarity { display: none; }
|
||||
.routing-table .route .pattern { padding-right: 0.5em; }
|
||||
|
||||
.routing-table .route:before {
|
||||
content: " ";
|
||||
float: left;
|
||||
}
|
||||
|
||||
.routing-table .pub:before {
|
||||
border-right: 0.6em solid lightgreen;
|
||||
border-top: 0.75em solid transparent;
|
||||
border-bottom: 0.75em solid transparent;
|
||||
}
|
||||
|
||||
.routing-table .sub:before {
|
||||
border-left: 0.6em solid transparent;
|
||||
border-top: 0.75em solid lightblue;
|
||||
border-bottom: 0.75em solid lightblue;
|
||||
}
|
||||
|
||||
.routing-table .route:after {
|
||||
content: " ";
|
||||
float: right;
|
||||
}
|
||||
|
||||
.routing-table .pub:after {
|
||||
border-left: 0.6em solid lightgreen;
|
||||
border-top: 0.75em solid transparent;
|
||||
border-bottom: 0.75em solid transparent;
|
||||
}
|
||||
|
||||
.routing-table .sub:after {
|
||||
border-right: 0.6em solid transparent;
|
||||
border-top: 0.75em solid lightblue;
|
||||
border-bottom: 0.75em solid lightblue;
|
||||
}
|
|
@ -1,108 +0,0 @@
|
|||
function spawnRoutingTableWidget(selector, fragmentClass) {
|
||||
|
||||
function sortedBy(xs, f) {
|
||||
var keys = [];
|
||||
var result = [];
|
||||
for (var i = 0; i < xs.length; i++) {
|
||||
keys.push([f(xs[i]), i]);
|
||||
}
|
||||
keys.sort();
|
||||
for (var i = 0; i < xs.length; i++) {
|
||||
result.push(xs[keys[i][1]]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function count_uniqBy(xs, f) {
|
||||
var r = [];
|
||||
if (xs.length === 0) return [];
|
||||
var last = xs[0];
|
||||
var lastKey = f(xs[0]);
|
||||
var count = 1;
|
||||
function fin() {
|
||||
r.push([count, last]);
|
||||
}
|
||||
for (var i = 1; i < xs.length; i++) {
|
||||
var fi = f(xs[i]);
|
||||
if (fi === lastKey) {
|
||||
count++;
|
||||
} else {
|
||||
fin();
|
||||
last = xs[i];
|
||||
lastKey = fi;
|
||||
count = 1;
|
||||
}
|
||||
}
|
||||
fin();
|
||||
return r;
|
||||
}
|
||||
|
||||
World.spawn({
|
||||
boot: function () { this.updateState(); },
|
||||
|
||||
state: [],
|
||||
nextState: [],
|
||||
timer: false,
|
||||
|
||||
digestRoutes: function (rs) {
|
||||
var s = [];
|
||||
function key(r) { return JSON.stringify([r.pattern,
|
||||
r.isSubscription ? r.level : -r.level,
|
||||
r.isSubscription]); }
|
||||
for (var i = 0; i < rs.length; i++) {
|
||||
var p = rs[i].pattern;
|
||||
if (p[0] !== "DOM" || p[1] !== selector || p[2] !== fragmentClass) {
|
||||
s.push(rs[i]);
|
||||
}
|
||||
}
|
||||
s = sortedBy(s, key);
|
||||
s = count_uniqBy(s, key);
|
||||
return s;
|
||||
},
|
||||
|
||||
updateState: function () {
|
||||
var elts = ["ul", {"class": "routing-table"}];
|
||||
for (var i = 0; i < this.state.length; i++) {
|
||||
var r = this.state[i];
|
||||
var levelstr;
|
||||
switch (r[1].level) {
|
||||
case 0: levelstr = "participant"; break;
|
||||
case 1: levelstr = "observer"; break;
|
||||
case 2: levelstr = "metaobserver"; break;
|
||||
default: levelstr = "level " + r[1].level; break;
|
||||
}
|
||||
var polarity = r[1].isSubscription ? "sub" : "pub";
|
||||
var pat = JSON.stringify(r[1].pattern).replace(/{"__":"__"}/g, '★');
|
||||
elts.push(["li",
|
||||
["span", {"class": "repeatcount"}, r[0]],
|
||||
["span", {"class": "times"}, " × "],
|
||||
["span", {"class": polarity + " route"},
|
||||
["span", {"class": "level", "data-level": r[1].level}, levelstr],
|
||||
["span", {"class": "polarity"}, polarity],
|
||||
["span", {"class": "pattern"}, pat]]]);
|
||||
}
|
||||
World.updateRoutes([sub(__, 0, Infinity),
|
||||
pub(__, 0, Infinity),
|
||||
pub(["DOM", selector, fragmentClass, elts])]);
|
||||
},
|
||||
|
||||
handleEvent: function (e) {
|
||||
var self = this;
|
||||
if (e.type === "routes") {
|
||||
self.nextState = self.digestRoutes(e.routes);
|
||||
if (self.timer) {
|
||||
clearTimeout(self.timer);
|
||||
self.timer = false;
|
||||
}
|
||||
self.timer = setTimeout(World.wrap(function () {
|
||||
if (JSON.stringify(self.nextState) !== JSON.stringify(self.state)) {
|
||||
self.state = self.nextState;
|
||||
self.updateState();
|
||||
}
|
||||
self.timer = false;
|
||||
}), 50);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
}
|
133
server.rkt
133
server.rkt
|
@ -1,133 +0,0 @@
|
|||
#lang minimart
|
||||
;; Generic broker for WebSockets-based minimart/marketplace communication.
|
||||
|
||||
(require net/rfc6455)
|
||||
(require minimart/drivers/timer)
|
||||
(require minimart/drivers/websocket)
|
||||
(require minimart/demand-matcher)
|
||||
(require minimart/pattern)
|
||||
(require json)
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Main: start WebSocket server
|
||||
|
||||
(log-events-and-actions? #f)
|
||||
|
||||
(define ping-interval (* 1000 (max (- (ws-idle-timeout) 10) (* (ws-idle-timeout) 0.8))))
|
||||
|
||||
(spawn-timer-driver)
|
||||
(spawn-websocket-driver)
|
||||
|
||||
(define (spawn-server-listener port ssl-options)
|
||||
(define server-id (websocket-local-server port ssl-options))
|
||||
(spawn-demand-matcher (websocket-message (websocket-remote-client ?) server-id ?)
|
||||
#:meta-level 1
|
||||
#:demand-is-subscription? #f
|
||||
(match-lambda ;; arrived-demand-route, i.e. new connection publisher
|
||||
[(route _ (websocket-message c _ _) 1 _)
|
||||
(spawn-connection-handler c server-id)]
|
||||
[_ '()])))
|
||||
|
||||
(spawn-world
|
||||
(spawn-server-listener 8000 #f)
|
||||
(spawn-server-listener 8443 (websocket-ssl-options "server-cert.pem" "private-key.pem")))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Wire protocol representation of events and actions
|
||||
|
||||
(define (drop-json-pattern p)
|
||||
(pattern-subst p (hasheq '__ "__") ?))
|
||||
|
||||
(define (drop-json-route r)
|
||||
(match r
|
||||
[`(,pub-or-sub ,pattern ,meta-level ,level)
|
||||
(route (match pub-or-sub ["sub" #t] ["pub" #f])
|
||||
(drop-json-pattern pattern)
|
||||
meta-level
|
||||
level)]))
|
||||
|
||||
(define (drop-json-action j)
|
||||
(match j
|
||||
["ping" 'ping]
|
||||
["pong" 'pong]
|
||||
[`("routes" ,routes) (routing-update (map drop-json-route routes))]
|
||||
[`("message" ,body ,meta-level ,feedback?) (message body meta-level feedback?)]))
|
||||
|
||||
(define (lift-json-pattern p)
|
||||
(pattern-subst p ? (hasheq '__ "__")))
|
||||
|
||||
(define (lift-json-route r)
|
||||
(match r
|
||||
[(route sub? p ml l) `(,(if sub? "sub" "pub") ,(lift-json-pattern p) ,ml ,l)]))
|
||||
|
||||
(define (lift-json-event j)
|
||||
(match j
|
||||
['ping "ping"]
|
||||
['pong "pong"]
|
||||
[(routing-update rs) `("routes" ,(map lift-json-route rs))]
|
||||
[(message body meta-level feedback?) `("message" ,body ,meta-level ,feedback?)]))
|
||||
|
||||
(require racket/trace)
|
||||
(trace drop-json-action)
|
||||
(trace lift-json-event)
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Connections
|
||||
|
||||
(struct connection-state (client-id tunnelled-routes) #:transparent)
|
||||
|
||||
(define (spawn-connection-handler c server-id)
|
||||
(define (send-event e s)
|
||||
(send (websocket-message server-id
|
||||
(connection-state-client-id s)
|
||||
(jsexpr->string (lift-json-event e)))
|
||||
#:meta-level 1))
|
||||
(define ((handle-connection-routing-change rs) s)
|
||||
(match rs
|
||||
['() (transition s (quit))] ;; websocket connection closed
|
||||
[_ (transition s '())]))
|
||||
(define ((handle-tunnelled-routing-change rs) s)
|
||||
(transition s (send-event (routing-update rs) s)))
|
||||
(define ((handle-tunnellable-message m) s)
|
||||
(if (ormap (lambda (r) (route-accepts? r m)) (connection-state-tunnelled-routes s))
|
||||
(transition s (send-event m s))
|
||||
(transition s '())))
|
||||
(define relay-connections
|
||||
(list (sub (timer-expired c ?) #:meta-level 1)
|
||||
(sub (websocket-message c server-id ?) #:meta-level 1)
|
||||
(sub (websocket-message c server-id ?) #:meta-level 1 #:level 1)
|
||||
(pub (websocket-message server-id c ?) #:meta-level 1)))
|
||||
(define (connection-handler e s)
|
||||
(match e
|
||||
[(routing-update rs)
|
||||
(sequence-transitions
|
||||
(transition s '())
|
||||
(handle-connection-routing-change (intersect-routes rs relay-connections))
|
||||
(handle-tunnelled-routing-change
|
||||
(intersect-routes rs (connection-state-tunnelled-routes s))))]
|
||||
[(? message? m)
|
||||
(sequence-transitions
|
||||
(match m
|
||||
[(message (websocket-message from to data) 1 #f)
|
||||
(match (drop-json-action (string->jsexpr data))
|
||||
[(routing-update rs-unfiltered)
|
||||
(define rs (filter (lambda (r) (zero? (route-meta-level r))) rs-unfiltered))
|
||||
(transition (struct-copy connection-state s [tunnelled-routes rs])
|
||||
(routing-update (append rs relay-connections)))]
|
||||
[(? message? m)
|
||||
(transition s (if (zero? (message-meta-level m)) m '()))]
|
||||
['ping
|
||||
(transition s (send-event 'pong s))]
|
||||
['pong
|
||||
(transition s '())])]
|
||||
[(message (timer-expired _ _) 1 #f)
|
||||
(transition s (list (send (set-timer c ping-interval 'relative) #:meta-level 1)
|
||||
(send-event 'ping s)))]
|
||||
[_
|
||||
(transition s '())])
|
||||
(handle-tunnellable-message m))]
|
||||
[#f #f]))
|
||||
(list (send (set-timer c ping-interval 'relative) #:meta-level 1)
|
||||
(spawn connection-handler
|
||||
(connection-state c '())
|
||||
relay-connections)))
|
|
@ -0,0 +1,268 @@
|
|||
var Minimart = require("./minimart.js");
|
||||
var util = require("./util.js");
|
||||
var World = Minimart.World;
|
||||
var Route = Minimart.Route;
|
||||
|
||||
Actor._chunks = null;
|
||||
|
||||
function Actor(bootfn) {
|
||||
return {
|
||||
boot: function () {
|
||||
delete this.boot;
|
||||
var oldChunks = Actor._chunks;
|
||||
try {
|
||||
Actor._chunks = [];
|
||||
bootfn.call(this);
|
||||
var initialGestalt = finalizeActor(this, Actor._chunks);
|
||||
Actor._chunks = oldChunks;
|
||||
return [initialGestalt];
|
||||
} catch (e) {
|
||||
Actor._chunks = oldChunks;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function checkChunks(type) {
|
||||
if (!Actor._chunks) {
|
||||
throw new Error("Call to Actor."+type+" outside of Actor constructor");
|
||||
}
|
||||
}
|
||||
|
||||
function extractChunk(type, kind, defaultOptions, args) {
|
||||
var rawProjectionFn = args[0]
|
||||
var options = null;
|
||||
var handler = null;
|
||||
if (typeof rawProjectionFn !== 'function') {
|
||||
throw new Error("Actor."+type+" expects a function producing a pattern as first argument");
|
||||
}
|
||||
for (var i = 1; i < args.length; i++) { // NB: skip the first arg - it's rawProjectionFn
|
||||
if (typeof args[i] === 'function') {
|
||||
if (handler !== null) { throw new Error("Too many handler functions in Actor."+type); }
|
||||
handler = args[i];
|
||||
} else if (typeof args[i] === 'object') {
|
||||
if (options !== null) { throw new Error("Too many options arguments in Actor."+type); }
|
||||
options = args[i];
|
||||
} else {
|
||||
throw new Error("Unrecognised argument in Actor."+type);
|
||||
}
|
||||
}
|
||||
options = options || {};
|
||||
for (var k in options) {
|
||||
if (!(k in defaultOptions)) {
|
||||
throw new Error("Unrecognised option '"+k+"' in Actor."+type);
|
||||
}
|
||||
}
|
||||
for (var k in defaultOptions) {
|
||||
if (!(k in options)) {
|
||||
options[k] = defaultOptions[k];
|
||||
}
|
||||
}
|
||||
return {
|
||||
type: type,
|
||||
kind: kind,
|
||||
rawProjectionFn: rawProjectionFn,
|
||||
options: options,
|
||||
handler: handler
|
||||
};
|
||||
}
|
||||
|
||||
function recordChunk(chunk) {
|
||||
Actor._chunks.push(chunk);
|
||||
}
|
||||
|
||||
function chunkExtractor(type, kind, defaultOptions) {
|
||||
return function (/* ... */) {
|
||||
checkChunks(type);
|
||||
recordChunk(extractChunk(type,
|
||||
kind,
|
||||
defaultOptions,
|
||||
Array.prototype.slice.call(arguments)));
|
||||
};
|
||||
}
|
||||
|
||||
var participantDefaults = {
|
||||
metaLevel: 0,
|
||||
when: function () { return true; }
|
||||
};
|
||||
|
||||
var observerDefaults = {
|
||||
metaLevel: 0,
|
||||
level: 0,
|
||||
when: function () { return true; },
|
||||
presence: null,
|
||||
name: null,
|
||||
singleton: null,
|
||||
set: null,
|
||||
added: null,
|
||||
removed: null
|
||||
};
|
||||
|
||||
Actor.advertise = chunkExtractor('advertise', 'participant', participantDefaults);
|
||||
Actor.subscribe = chunkExtractor('subscribe', 'participant', participantDefaults);
|
||||
|
||||
Actor.observeAdvertisers = chunkExtractor('observeAdvertisers', 'observer', observerDefaults);
|
||||
Actor.observeSubscribers = chunkExtractor('observeSubscribers', 'observer', observerDefaults);
|
||||
|
||||
Actor.observeGestalt = function (gestaltFn, eventHandlerFn) {
|
||||
checkChunks('observeGestalt');
|
||||
recordChunk({
|
||||
type: 'observeGestalt',
|
||||
kind: 'raw',
|
||||
gestaltFn: gestaltFn,
|
||||
options: {
|
||||
when: function () { return true; }
|
||||
},
|
||||
eventHandlerFn: eventHandlerFn
|
||||
});
|
||||
};
|
||||
|
||||
function finalizeActor(behavior, chunks) {
|
||||
var oldHandleEvent = behavior.handleEvent;
|
||||
var projections = {};
|
||||
var compiledProjections = {};
|
||||
var previousObjs = {};
|
||||
|
||||
behavior._computeRoutes = function () {
|
||||
var newRoutes = Route.emptyGestalt;
|
||||
for (var i = 0; i < chunks.length; i++) {
|
||||
var chunk = chunks[i];
|
||||
if (chunk.options.when.call(this)) {
|
||||
switch (chunk.kind) {
|
||||
case 'raw':
|
||||
newRoutes = newRoutes.union(chunk.gestaltFn.call(this));
|
||||
break;
|
||||
case 'participant':
|
||||
var proj = chunk.rawProjectionFn.call(this);
|
||||
projections[i] = proj;
|
||||
var g = Route.simpleGestalt(chunk.type === 'advertise',
|
||||
Route.projectionToPattern(proj),
|
||||
chunk.options.metaLevel,
|
||||
0);
|
||||
newRoutes = newRoutes.union(g);
|
||||
break;
|
||||
case 'observer':
|
||||
var proj = chunk.rawProjectionFn.call(this);
|
||||
projections[i] = proj;
|
||||
compiledProjections[i] = Route.compileProjection(proj);
|
||||
var g = Route.simpleGestalt(chunk.type === 'observeSubscribers',
|
||||
Route.projectionToPattern(proj),
|
||||
chunk.options.metaLevel,
|
||||
chunk.options.level + 1);
|
||||
newRoutes = newRoutes.union(g);
|
||||
if (chunk.options.added || chunk.options.removed) {
|
||||
previousObjs[i] = Route.arrayToSet([]);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new Error("Unsupported chunk type/kind: "+chunk.type+"/"+chunk.kind);
|
||||
}
|
||||
}
|
||||
}
|
||||
return newRoutes;
|
||||
};
|
||||
|
||||
behavior.updateRoutes = function () {
|
||||
World.updateRoutes([this._computeRoutes()]);
|
||||
};
|
||||
|
||||
behavior.handleEvent = function (e) {
|
||||
if (oldHandleEvent) { oldHandleEvent.call(this, e); }
|
||||
for (var i = 0; i < chunks.length; i++) {
|
||||
var chunk = chunks[i];
|
||||
switch (chunk.kind) {
|
||||
case 'raw':
|
||||
chunk.eventHandlerFn.call(this, e);
|
||||
break;
|
||||
case 'participant':
|
||||
if (chunk.handler
|
||||
&& (e.type === 'message')
|
||||
&& (e.metaLevel === chunk.options.metaLevel)
|
||||
&& (e.isFeedback === (chunk.type === 'advertise')))
|
||||
{
|
||||
var matchResult = Route.matchPattern(e.message, projections[i]);
|
||||
if (matchResult) {
|
||||
util.kwApply(chunk.handler, this, matchResult);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'observer':
|
||||
if (e.type === 'routes') {
|
||||
var projectionResult = e.gestalt.project(compiledProjections[i],
|
||||
chunk.type !== 'observeSubscribers',
|
||||
chunk.options.metaLevel,
|
||||
chunk.options.level);
|
||||
|
||||
var isPresent = !Route.is_emptyMatcher(projectionResult);
|
||||
if (chunk.options.presence) {
|
||||
this[chunk.options.presence] = isPresent;
|
||||
}
|
||||
|
||||
var objs = [];
|
||||
if (isPresent) {
|
||||
var keys = Route.matcherKeys(projectionResult);
|
||||
if (keys === null) {
|
||||
console.warn("Wildcard detected while projecting ("
|
||||
+JSON.stringify(chunk.options)+")");
|
||||
} else {
|
||||
objs = Route.matcherKeysToObjects(keys, compiledProjections[i]);
|
||||
if (chunk.options.set) {
|
||||
for (var j = 0; j < objs.length; j++) {
|
||||
objs[j] = chunk.options.set.call(this, objs[j]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (chunk.options.name) {
|
||||
this[chunk.options.name] = objs;
|
||||
}
|
||||
if (chunk.options.singleton) {
|
||||
this[chunk.options.singleton] = objs.length === 1 ? objs[0] : undefined;
|
||||
}
|
||||
|
||||
if (chunk.options.added || chunk.options.removed) {
|
||||
var objSet = Route.arrayToSet(objs);
|
||||
|
||||
if (chunk.options.added) {
|
||||
this[chunk.options.added] =
|
||||
Route.setToArray(Route.setSubtract(objSet, previousObjs[i]));
|
||||
}
|
||||
|
||||
if (chunk.options.removed) {
|
||||
this[chunk.options.removed] =
|
||||
Route.setToArray(Route.setSubtract(previousObjs[i], objSet));
|
||||
}
|
||||
|
||||
previousObjs[i] = objSet;
|
||||
}
|
||||
|
||||
if (chunk.handler) {
|
||||
chunk.handler.call(this);
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new Error("Unsupported chunk type/kind: "+chunk.type+"/"+chunk.kind);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (behavior.boot) { behavior.boot(); }
|
||||
for (var i = 0; i < chunks.length; i++) {
|
||||
var chunk = chunks[i];
|
||||
if (chunk.kind === 'observer') {
|
||||
if (chunk.options.presence) { behavior[chunk.options.presence] = false; }
|
||||
if (chunk.options.name) { behavior[chunk.options.name] = []; }
|
||||
if (chunk.options.singleton) { behavior[chunk.options.singleton] = undefined; }
|
||||
if (chunk.options.added) { behavior[chunk.options.added] = []; }
|
||||
if (chunk.options.removed) { behavior[chunk.options.removed] = []; }
|
||||
}
|
||||
}
|
||||
|
||||
return behavior._computeRoutes();
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
module.exports.Actor = Actor;
|
|
@ -0,0 +1,33 @@
|
|||
// Wire protocol representation of events and actions
|
||||
|
||||
var Route = require("./route.js");
|
||||
|
||||
function _encode(e) {
|
||||
switch (e.type) {
|
||||
case "routes":
|
||||
return ["routes", e.gestalt.serialize(function (v) { return true; })];
|
||||
case "message":
|
||||
return ["message", e.message, e.metaLevel, e.isFeedback];
|
||||
}
|
||||
}
|
||||
|
||||
function _decode(what) {
|
||||
return function (j) {
|
||||
switch (j[0]) {
|
||||
case "routes":
|
||||
return Minimart.updateRoutes([
|
||||
Route.deserializeGestalt(j[1], function (v) { return true; })]);
|
||||
case "message":
|
||||
return Minimart.sendMessage(j[1], j[2], j[3]);
|
||||
default:
|
||||
throw { message: "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");
|
|
@ -0,0 +1,120 @@
|
|||
// DOM fragment display driver
|
||||
var Minimart = require("./minimart.js");
|
||||
var World = Minimart.World;
|
||||
var sub = Minimart.sub;
|
||||
var pub = Minimart.pub;
|
||||
var __ = Minimart.__;
|
||||
var _$ = Minimart._$;
|
||||
|
||||
function spawnDOMDriver(domWrapFunction, jQueryWrapFunction) {
|
||||
domWrapFunction = domWrapFunction || defaultWrapFunction;
|
||||
var d = new Minimart.DemandMatcher(domWrapFunction(_$, _$, _$));
|
||||
d.onDemandIncrease = function (captures) {
|
||||
var selector = captures[0];
|
||||
var fragmentClass = captures[1];
|
||||
var fragmentSpec = captures[2];
|
||||
World.spawn(new DOMFragment(selector,
|
||||
fragmentClass,
|
||||
fragmentSpec,
|
||||
domWrapFunction,
|
||||
jQueryWrapFunction));
|
||||
};
|
||||
World.spawn(d);
|
||||
}
|
||||
|
||||
function defaultWrapFunction(selector, fragmentClass, fragmentSpec) {
|
||||
return ["DOM", selector, fragmentClass, fragmentSpec];
|
||||
}
|
||||
|
||||
function DOMFragment(selector, fragmentClass, fragmentSpec, domWrapFunction, jQueryWrapFunction) {
|
||||
this.selector = selector;
|
||||
this.fragmentClass = fragmentClass;
|
||||
this.fragmentSpec = fragmentSpec;
|
||||
this.domWrapFunction = domWrapFunction;
|
||||
this.jQueryWrapFunction = jQueryWrapFunction;
|
||||
this.nodes = this.buildNodes();
|
||||
}
|
||||
|
||||
DOMFragment.prototype.boot = function () {
|
||||
var self = this;
|
||||
var monitoring =
|
||||
sub(self.domWrapFunction(self.selector, self.fragmentClass, self.fragmentSpec), 1, 2);
|
||||
|
||||
World.spawn(new World(function () {
|
||||
Minimart.JQuery.spawnJQueryDriver(self.selector+" > ."+self.fragmentClass,
|
||||
1,
|
||||
self.jQueryWrapFunction);
|
||||
World.spawn({
|
||||
boot: function () { return [monitoring] },
|
||||
handleEvent: function (e) {
|
||||
if (e.type === "routes") {
|
||||
var level = e.gestalt.getLevel(1, 0); // find participant peers
|
||||
if (!e.gestalt.isEmpty() && level.isEmpty()) {
|
||||
World.shutdownWorld();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}));
|
||||
|
||||
return [sub(self.domWrapFunction(self.selector, self.fragmentClass, self.fragmentSpec)),
|
||||
sub(self.domWrapFunction(self.selector, self.fragmentClass, self.fragmentSpec), 0, 1)]
|
||||
};
|
||||
|
||||
DOMFragment.prototype.handleEvent = function (e) {
|
||||
if (e.type === "routes" && e.gestalt.isEmpty()) {
|
||||
for (var i = 0; i < this.nodes.length; i++) {
|
||||
var n = this.nodes[i];
|
||||
n.parentNode.removeChild(n);
|
||||
}
|
||||
World.exit();
|
||||
}
|
||||
};
|
||||
|
||||
function isAttributes(x) {
|
||||
return Array.isArray(x) && ((x.length === 0) || Array.isArray(x[0]));
|
||||
}
|
||||
|
||||
DOMFragment.prototype.interpretSpec = function (spec) {
|
||||
// Fragment specs are roughly JSON-equivalents of SXML.
|
||||
// spec ::== ["tag", [["attr", "value"], ...], spec, spec, ...]
|
||||
// | ["tag", spec, spec, ...]
|
||||
// | "cdata"
|
||||
if (typeof(spec) === "string" || typeof(spec) === "number") {
|
||||
return document.createTextNode(spec);
|
||||
} else if ($.isArray(spec)) {
|
||||
var tagName = spec[0];
|
||||
var hasAttrs = isAttributes(spec[1]);
|
||||
var attrs = hasAttrs ? spec[1] : {};
|
||||
var kidIndex = hasAttrs ? 2 : 1;
|
||||
|
||||
// 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]);
|
||||
}
|
||||
for (var i = kidIndex; i < spec.length; i++) {
|
||||
n.appendChild(this.interpretSpec(spec[i]));
|
||||
}
|
||||
return n;
|
||||
} else {
|
||||
throw new Error("Ill-formed DOM specification");
|
||||
}
|
||||
};
|
||||
|
||||
DOMFragment.prototype.buildNodes = function () {
|
||||
var self = this;
|
||||
var nodes = [];
|
||||
$(self.selector).each(function (index, domNode) {
|
||||
var n = self.interpretSpec(self.fragmentSpec);
|
||||
n.classList.add(self.fragmentClass);
|
||||
domNode.appendChild(n);
|
||||
nodes.push(n);
|
||||
});
|
||||
return nodes;
|
||||
};
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
module.exports.spawnDOMDriver = spawnDOMDriver;
|
||||
module.exports.defaultWrapFunction = defaultWrapFunction;
|
|
@ -0,0 +1,61 @@
|
|||
/* Ground interface */
|
||||
var Minimart = require("./minimart.js");
|
||||
var World = Minimart.World;
|
||||
|
||||
function Ground(bootFn) {
|
||||
var self = this;
|
||||
this.stepperId = null;
|
||||
World.withWorldStack([[this, -1]], function () {
|
||||
self.world = new World(bootFn);
|
||||
});
|
||||
}
|
||||
|
||||
Ground.prototype.step = function () {
|
||||
var self = this;
|
||||
return World.withWorldStack([[this, -1]], function () {
|
||||
return self.world.step();
|
||||
});
|
||||
};
|
||||
|
||||
Ground.prototype.checkPid = function (pid) {
|
||||
if (pid !== -1) console.error("Weird pid in Ground markPidRunnable", pid);
|
||||
};
|
||||
|
||||
Ground.prototype.markPidRunnable = function (pid) {
|
||||
this.checkPid(pid);
|
||||
this.startStepping();
|
||||
};
|
||||
|
||||
Ground.prototype.startStepping = function () {
|
||||
var self = this;
|
||||
if (this.stepperId) return;
|
||||
if (this.step()) {
|
||||
this.stepperId = setTimeout(function () {
|
||||
self.stepperId = null;
|
||||
self.startStepping();
|
||||
}, 0);
|
||||
}
|
||||
};
|
||||
|
||||
Ground.prototype.stopStepping = function () {
|
||||
if (this.stepperId) {
|
||||
clearTimeout(this.stepperId);
|
||||
this.stepperId = null;
|
||||
}
|
||||
};
|
||||
|
||||
Ground.prototype.enqueueAction = function (pid, action) {
|
||||
this.checkPid(pid);
|
||||
if (action.type === 'routes') {
|
||||
if (!action.gestalt.isEmpty()) {
|
||||
console.error("You have subscribed to a nonexistent event source.",
|
||||
action.gestalt.pretty());
|
||||
}
|
||||
} else {
|
||||
console.error("You have sent a message into the outer void.", action);
|
||||
}
|
||||
};
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
module.exports.Ground = Ground;
|
|
@ -0,0 +1,88 @@
|
|||
// JQuery event driver
|
||||
var Minimart = require("./minimart.js");
|
||||
var World = Minimart.World;
|
||||
var sub = Minimart.sub;
|
||||
var pub = Minimart.pub;
|
||||
var __ = Minimart.__;
|
||||
var _$ = Minimart._$;
|
||||
|
||||
function spawnJQueryDriver(baseSelector, metaLevel, wrapFunction) {
|
||||
metaLevel = metaLevel || 0;
|
||||
wrapFunction = wrapFunction || defaultWrapFunction;
|
||||
var d = new Minimart.DemandMatcher(wrapFunction(_$, _$, __), metaLevel,
|
||||
{demandSideIsSubscription: true});
|
||||
d.onDemandIncrease = function (captures) {
|
||||
var selector = captures[0];
|
||||
var eventName = captures[1];
|
||||
World.spawn(new JQueryEventRouter(baseSelector,
|
||||
selector,
|
||||
eventName,
|
||||
metaLevel,
|
||||
wrapFunction));
|
||||
};
|
||||
World.spawn(d);
|
||||
}
|
||||
|
||||
function defaultWrapFunction(selector, eventName, eventValue) {
|
||||
return ["jQuery", selector, eventName, eventValue];
|
||||
}
|
||||
|
||||
function JQueryEventRouter(baseSelector, selector, eventName, metaLevel, wrapFunction) {
|
||||
var self = this;
|
||||
this.baseSelector = baseSelector || null;
|
||||
this.selector = selector;
|
||||
this.eventName = eventName;
|
||||
this.metaLevel = metaLevel || 0;
|
||||
this.wrapFunction = wrapFunction || defaultWrapFunction;
|
||||
this.preventDefault = (this.eventName.charAt(0) !== "+");
|
||||
this.handler =
|
||||
World.wrap(function (e) {
|
||||
World.send(self.wrapFunction(self.selector, self.eventName, e), self.metaLevel);
|
||||
if (self.preventDefault) e.preventDefault();
|
||||
return !self.preventDefault;
|
||||
});
|
||||
this.computeNodes().on(this.preventDefault ? this.eventName : this.eventName.substring(1),
|
||||
this.handler);
|
||||
}
|
||||
|
||||
JQueryEventRouter.prototype.boot = function () {
|
||||
return [pub(this.wrapFunction(this.selector, this.eventName, __), this.metaLevel),
|
||||
pub(this.wrapFunction(this.selector, this.eventName, __), this.metaLevel, 1)];
|
||||
};
|
||||
|
||||
JQueryEventRouter.prototype.handleEvent = function (e) {
|
||||
if (e.type === "routes" && e.gestalt.isEmpty()) {
|
||||
this.computeNodes().off(this.eventName, this.handler);
|
||||
World.exit();
|
||||
}
|
||||
};
|
||||
|
||||
JQueryEventRouter.prototype.computeNodes = function () {
|
||||
if (this.baseSelector) {
|
||||
return $(this.baseSelector).children(this.selector).addBack(this.selector);
|
||||
} else {
|
||||
return $(this.selector);
|
||||
}
|
||||
};
|
||||
|
||||
function simplifyDOMEvent(e) {
|
||||
var keys = [];
|
||||
for (var k in e) {
|
||||
var v = e[k];
|
||||
if (typeof v === 'object') continue;
|
||||
if (typeof v === 'function') continue;
|
||||
keys.push(k);
|
||||
}
|
||||
keys.sort();
|
||||
var simplified = [];
|
||||
for (var i = 0; i < keys.length; i++) {
|
||||
simplified.push([keys[i], e[keys[i]]]);
|
||||
}
|
||||
return simplified;
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
module.exports.spawnJQueryDriver = spawnJQueryDriver;
|
||||
module.exports.simplifyDOMEvent = simplifyDOMEvent;
|
||||
module.exports.defaultWrapFunction = defaultWrapFunction;
|
|
@ -0,0 +1,16 @@
|
|||
module.exports = require("./minimart.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.Reflect = require("./reflect.js");
|
||||
|
||||
module.exports.Ground = require("./ground.js").Ground;
|
||||
module.exports.Actor = require("./actor.js").Actor;
|
||||
module.exports.Spy = require("./spy.js").Spy;
|
||||
module.exports.WakeDetector = require("./wake-detector.js").WakeDetector;
|
||||
|
||||
var Worker = require("./worker.js");
|
||||
module.exports.Worker = Worker.Worker;
|
||||
module.exports.WorkerGround = Worker.WorkerGround;
|
|
@ -0,0 +1,530 @@
|
|||
var Route = require("./route.js");
|
||||
var Util = require("./util.js");
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// TODO: trigger-guards as per minimart
|
||||
|
||||
/*---------------------------------------------------------------------------*/
|
||||
/* Events and Actions */
|
||||
|
||||
var __ = Route.__;
|
||||
var _$ = Route._$;
|
||||
|
||||
function sub(pattern, metaLevel, level) {
|
||||
return Route.simpleGestalt(false, pattern, metaLevel, level);
|
||||
}
|
||||
|
||||
function pub(pattern, metaLevel, level) {
|
||||
return Route.simpleGestalt(true, pattern, metaLevel, level);
|
||||
}
|
||||
|
||||
function spawn(behavior) {
|
||||
return { type: "spawn", behavior: behavior };
|
||||
}
|
||||
|
||||
function updateRoutes(gestalts) {
|
||||
return { type: "routes", gestalt: Route.gestaltUnion(gestalts) };
|
||||
}
|
||||
|
||||
function pendingRoutingUpdate(aggregate, affectedSubgestalt, knownTarget) {
|
||||
return { type: "pendingRoutingUpdate",
|
||||
aggregate: aggregate,
|
||||
affectedSubgestalt: affectedSubgestalt,
|
||||
knownTarget: knownTarget };
|
||||
}
|
||||
|
||||
function sendMessage(m, metaLevel, isFeedback) {
|
||||
return { type: "message",
|
||||
metaLevel: (metaLevel === undefined) ? 0 : metaLevel,
|
||||
message: m,
|
||||
isFeedback: (isFeedback === undefined) ? false : isFeedback };
|
||||
}
|
||||
|
||||
function shutdownWorld() {
|
||||
return { type: "shutdownWorld" };
|
||||
}
|
||||
|
||||
/*---------------------------------------------------------------------------*/
|
||||
/* Configurations */
|
||||
|
||||
function World(bootFn) {
|
||||
this.alive = true;
|
||||
this.eventQueue = [];
|
||||
this.runnablePids = {};
|
||||
this.partialGestalt = Route.emptyGestalt; // Only gestalt from local processes
|
||||
this.fullGestalt = Route.emptyGestalt ;; // partialGestalt unioned with downwardGestalt
|
||||
this.processTable = {};
|
||||
this.tombstones = {};
|
||||
this.downwardGestalt = Route.emptyGestalt;
|
||||
this.processActions = [];
|
||||
this.asChild(-1, bootFn, true);
|
||||
}
|
||||
|
||||
/* Class state / methods */
|
||||
|
||||
World.nextPid = 0;
|
||||
|
||||
World.stack = [];
|
||||
|
||||
World.current = function () {
|
||||
return World.stack[World.stack.length - 1][0];
|
||||
};
|
||||
|
||||
World.activePid = function () {
|
||||
return World.stack[World.stack.length - 1][1];
|
||||
};
|
||||
|
||||
World.send = function (m, metaLevel, isFeedback) {
|
||||
World.current().enqueueAction(World.activePid(), sendMessage(m, metaLevel, isFeedback));
|
||||
};
|
||||
|
||||
World.updateRoutes = function (gestalts) {
|
||||
World.current().enqueueAction(World.activePid(), updateRoutes(gestalts));
|
||||
};
|
||||
|
||||
World.spawn = function (behavior) {
|
||||
World.current().enqueueAction(World.activePid(), spawn(behavior));
|
||||
};
|
||||
|
||||
World.exit = function (exn) {
|
||||
World.current().kill(World.activePid(), exn);
|
||||
};
|
||||
|
||||
World.shutdownWorld = function () {
|
||||
World.current().enqueueAction(World.activePid(), shutdownWorld());
|
||||
};
|
||||
|
||||
World.withWorldStack = function (stack, f) {
|
||||
var oldStack = World.stack;
|
||||
World.stack = stack;
|
||||
var result = null;
|
||||
try {
|
||||
result = f();
|
||||
} catch (e) {
|
||||
World.stack = oldStack;
|
||||
throw e;
|
||||
}
|
||||
World.stack = oldStack;
|
||||
return result;
|
||||
};
|
||||
|
||||
World.wrap = function (f) {
|
||||
var savedStack = World.stack.slice();
|
||||
return function () {
|
||||
var actuals = arguments;
|
||||
return World.withWorldStack(savedStack, function () {
|
||||
var result = World.current().asChild(World.activePid(), function () {
|
||||
return f.apply(null, actuals);
|
||||
});
|
||||
for (var i = World.stack.length - 1; i >= 0; i--) {
|
||||
World.stack[i][0].markPidRunnable(World.stack[i][1]);
|
||||
}
|
||||
return result;
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
/* Instance methods */
|
||||
|
||||
World.prototype.enqueueAction = function (pid, action) {
|
||||
this.processActions.push([pid, action]);
|
||||
};
|
||||
|
||||
// The code is written to maintain the runnablePids set carefully, to
|
||||
// ensure we can locally decide whether we're inert or not without
|
||||
// having to search the whole deep process tree.
|
||||
World.prototype.isInert = function () {
|
||||
return this.eventQueue.length === 0
|
||||
&& this.processActions.length === 0
|
||||
&& Route.is_emptySet(this.runnablePids);
|
||||
};
|
||||
|
||||
World.prototype.markPidRunnable = function (pid) {
|
||||
this.runnablePids[pid] = [pid];
|
||||
};
|
||||
|
||||
World.prototype.step = function () {
|
||||
this.dispatchEvents();
|
||||
this.performActions();
|
||||
this.stepChildren();
|
||||
return this.alive && !this.isInert();
|
||||
};
|
||||
|
||||
World.prototype.asChild = function (pid, f, omitLivenessCheck) {
|
||||
if (!(pid in this.processTable) && !omitLivenessCheck) {
|
||||
console.warn("World.asChild eliding invocation of dead process", pid);
|
||||
return;
|
||||
}
|
||||
|
||||
World.stack.push([this, pid]);
|
||||
var result = null;
|
||||
try {
|
||||
result = f();
|
||||
} catch (e) {
|
||||
this.kill(pid, e);
|
||||
}
|
||||
if (World.stack.pop()[0] !== this) {
|
||||
throw new Error("Internal error: World stack imbalance");
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
World.prototype.kill = function (pid, exn) {
|
||||
if (exn && exn.stack) {
|
||||
console.log("Process exited", pid, exn, exn.stack);
|
||||
} else {
|
||||
console.log("Process exited", pid, exn);
|
||||
}
|
||||
var p = this.processTable[pid];
|
||||
delete this.processTable[pid];
|
||||
if (p) {
|
||||
if (p.behavior.trapexit) {
|
||||
this.asChild(pid, function () { return p.behavior.trapexit(exn); }, true);
|
||||
}
|
||||
if (exn) {
|
||||
p.exitReason = exn;
|
||||
this.tombstones[pid] = p;
|
||||
}
|
||||
this.applyAndIssueRoutingUpdate(p.gestalt, Route.emptyGestalt);
|
||||
}
|
||||
};
|
||||
|
||||
World.prototype.stepChildren = function () {
|
||||
var pids = this.runnablePids;
|
||||
this.runnablePids = {};
|
||||
for (var pid in pids) {
|
||||
var p = this.processTable[pid];
|
||||
if (p && p.behavior.step /* exists, haven't called it yet */) {
|
||||
var childBusy = this.asChild(pid | 0, function () { return p.behavior.step() });
|
||||
if (childBusy) this.markPidRunnable(pid);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
World.prototype.performActions = function () {
|
||||
var queue = this.processActions;
|
||||
this.processActions = [];
|
||||
var item;
|
||||
while ((item = queue.shift()) && this.alive) {
|
||||
this.performAction(item[0], item[1]);
|
||||
}
|
||||
};
|
||||
|
||||
World.prototype.dispatchEvents = function () {
|
||||
var queue = this.eventQueue;
|
||||
this.eventQueue = [];
|
||||
var item;
|
||||
while ((item = queue.shift())) {
|
||||
this.dispatchEvent(item);
|
||||
}
|
||||
};
|
||||
|
||||
World.prototype.performAction = function (pid, action) {
|
||||
switch (action.type) {
|
||||
case "spawn":
|
||||
var pid = World.nextPid++;
|
||||
var entry = { gestalt: Route.emptyGestalt, behavior: action.behavior };
|
||||
this.processTable[pid] = entry;
|
||||
if (entry.behavior.boot) {
|
||||
var initialGestalts = this.asChild(pid, function () { return entry.behavior.boot() });
|
||||
if (initialGestalts) {
|
||||
entry.gestalt = Route.gestaltUnion(initialGestalts).label(pid);
|
||||
}
|
||||
this.markPidRunnable(pid);
|
||||
}
|
||||
if (!Route.emptyGestalt.equals(entry.gestalt)) {
|
||||
this.applyAndIssueRoutingUpdate(Route.emptyGestalt, entry.gestalt, pid);
|
||||
}
|
||||
break;
|
||||
case "routes":
|
||||
if (pid in this.processTable) {
|
||||
// it may not be: this might be the routing update from a
|
||||
// kill of the process
|
||||
var oldGestalt = this.processTable[pid].gestalt;
|
||||
var newGestalt = action.gestalt.label(pid|0);
|
||||
// ^ pid|0: convert pid from string (table key!) to integer
|
||||
this.processTable[pid].gestalt = newGestalt;
|
||||
this.applyAndIssueRoutingUpdate(oldGestalt, newGestalt, pid);
|
||||
}
|
||||
break;
|
||||
case "message":
|
||||
if (action.metaLevel === 0) {
|
||||
this.eventQueue.push(action);
|
||||
} else {
|
||||
World.send(action.message, action.metaLevel - 1, action.isFeedback);
|
||||
}
|
||||
break;
|
||||
case "shutdownWorld":
|
||||
this.alive = false; // force us to stop doing things immediately
|
||||
World.exit();
|
||||
break;
|
||||
default:
|
||||
var exn = new Error("Action type " + action.type + " not understood");
|
||||
exn.action = action;
|
||||
throw exn;
|
||||
}
|
||||
};
|
||||
|
||||
World.prototype.updateFullGestalt = function () {
|
||||
this.fullGestalt = this.partialGestalt.union(this.downwardGestalt);
|
||||
};
|
||||
|
||||
World.prototype.issueLocalRoutingUpdate = function (affectedSubgestalt, knownTarget) {
|
||||
this.eventQueue.push(pendingRoutingUpdate(this.fullGestalt,
|
||||
affectedSubgestalt,
|
||||
knownTarget));
|
||||
};
|
||||
|
||||
World.prototype.applyAndIssueRoutingUpdate = function (oldg, newg, knownTarget) {
|
||||
knownTarget = typeof knownTarget === 'undefined' ? null : knownTarget;
|
||||
this.partialGestalt = this.partialGestalt.erasePath(oldg).union(newg);
|
||||
this.updateFullGestalt();
|
||||
this.issueLocalRoutingUpdate(oldg.union(newg), knownTarget);
|
||||
World.updateRoutes([this.partialGestalt.drop()]);
|
||||
};
|
||||
|
||||
World.prototype.dispatchEvent = function (e) {
|
||||
switch (e.type) {
|
||||
case "pendingRoutingUpdate":
|
||||
var pids = e.affectedSubgestalt.match(e.aggregate);
|
||||
if (e.knownTarget !== null) pids.unshift(e.knownTarget);
|
||||
for (var i = 0; i < pids.length; i++) {
|
||||
var pid = pids[i];
|
||||
if (pid === "out") console.warn("Would have delivered a routing update to environment");
|
||||
var p = this.processTable[pid];
|
||||
if (p) {
|
||||
var g = e.aggregate.filter(p.gestalt);
|
||||
this.asChild(pid, function () { p.behavior.handleEvent(updateRoutes([g])) });
|
||||
this.markPidRunnable(pid);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case "message":
|
||||
var pids = this.partialGestalt.matchValue(e.message, e.metaLevel, e.isFeedback);
|
||||
for (var i = 0; i < pids.length; i++) {
|
||||
var pid = pids[i];
|
||||
var p = this.processTable[pid];
|
||||
this.asChild(pid, function () { p.behavior.handleEvent(e) });
|
||||
this.markPidRunnable(pid);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
var exn = new Error("Event type " + e.type + " not dispatchable");
|
||||
exn.event = e;
|
||||
throw exn;
|
||||
}
|
||||
};
|
||||
|
||||
World.prototype.boot = function () {
|
||||
// Needed in order for the new World to be marked as "runnable", so
|
||||
// its initial actions get performed.
|
||||
};
|
||||
|
||||
World.prototype.handleEvent = function (e) {
|
||||
switch (e.type) {
|
||||
case "routes":
|
||||
var oldDownward = this.downwardGestalt;
|
||||
this.downwardGestalt = e.gestalt.label("out").lift();
|
||||
this.updateFullGestalt();
|
||||
this.issueLocalRoutingUpdate(oldDownward.union(this.downwardGestalt), null);
|
||||
break;
|
||||
case "message":
|
||||
this.eventQueue.push(sendMessage(e.message, e.metaLevel + 1, e.isFeedback));
|
||||
break;
|
||||
default:
|
||||
var exn = new Error("Event type " + e.type + " not understood");
|
||||
exn.event = e;
|
||||
throw exn;
|
||||
}
|
||||
};
|
||||
|
||||
/* Debugging, management, and monitoring */
|
||||
|
||||
World.prototype.processTree = function () {
|
||||
var kids = [];
|
||||
for (var pid in this.processTable) {
|
||||
var p = this.processTable[pid];
|
||||
if (p.behavior instanceof World) {
|
||||
kids.push([pid, p.behavior.processTree()]);
|
||||
} else {
|
||||
kids.push([pid, p]);
|
||||
}
|
||||
}
|
||||
for (var pid in this.tombstones) {
|
||||
kids.push([pid, this.tombstones[pid]]);
|
||||
}
|
||||
kids.sort(function (a, b) { return a[0] - b[0] });
|
||||
return kids;
|
||||
};
|
||||
|
||||
World.prototype.textProcessTree = function (ownPid) {
|
||||
var lines = [];
|
||||
|
||||
function dumpProcess(prefix, pid, p) {
|
||||
if (Array.isArray(p)) {
|
||||
lines.push(prefix + '--+ ' + pid);
|
||||
for (var i = 0; i < p.length; i++) {
|
||||
dumpProcess(prefix + ' |', p[i][0], p[i][1]);
|
||||
}
|
||||
lines.push(prefix);
|
||||
} else {
|
||||
var label = p.behavior.name || p.behavior.constructor.name || '';
|
||||
var tombstoneString = p.exitReason ? ' (EXITED: ' + p.exitReason + ') ' : '';
|
||||
var stringifiedState;
|
||||
try {
|
||||
var rawState = p.behavior.debugState ? p.behavior.debugState() : p.behavior;
|
||||
stringifiedState = JSON.stringify(rawState, function (k, v) {
|
||||
return (k === 'name') ? undefined : v;
|
||||
});
|
||||
} catch (e) {
|
||||
stringifiedState = "(cannot convert process state to JSON)";
|
||||
}
|
||||
lines.push(prefix + '-- ' + pid + ': ' + label + tombstoneString + stringifiedState);
|
||||
}
|
||||
}
|
||||
|
||||
dumpProcess('', ownPid || '', this.processTree());
|
||||
return lines.join('\n');
|
||||
};
|
||||
|
||||
World.prototype.clearTombstones = function () {
|
||||
this.tombstones = {};
|
||||
for (var pid in this.processTable) {
|
||||
var p = this.processTable[pid];
|
||||
if (p.behavior instanceof World) {
|
||||
p.behavior.clearTombstones();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/*---------------------------------------------------------------------------*/
|
||||
/* Utilities: matching demand for some service */
|
||||
|
||||
function DemandMatcher(projection, metaLevel, options) {
|
||||
options = Util.extend({
|
||||
demandLevel: 0,
|
||||
supplyLevel: 0,
|
||||
demandSideIsSubscription: false,
|
||||
supplyProjection: projection
|
||||
}, options);
|
||||
this.demandPattern = Route.projectionToPattern(projection);
|
||||
this.supplyPattern = Route.projectionToPattern(options.supplyProjection);
|
||||
this.demandProjectionSpec = Route.compileProjection(projection);
|
||||
this.supplyProjectionSpec = Route.compileProjection(options.supplyProjection);
|
||||
this.metaLevel = metaLevel | 0;
|
||||
this.demandLevel = options.demandLevel;
|
||||
this.supplyLevel = options.supplyLevel;
|
||||
this.demandSideIsSubscription = options.demandSideIsSubscription;
|
||||
this.onDemandIncrease = function (captures) {
|
||||
console.error("Unhandled increase in demand for route", captures);
|
||||
};
|
||||
this.onSupplyDecrease = function (captures) {
|
||||
console.error("Unhandled decrease in supply for route", captures);
|
||||
};
|
||||
this.currentDemand = {};
|
||||
this.currentSupply = {};
|
||||
}
|
||||
|
||||
DemandMatcher.prototype.debugState = function () {
|
||||
return {
|
||||
demandPattern: this.demandPattern,
|
||||
supplyPattern: this.supplyPattern,
|
||||
metaLevel: this.metaLevel,
|
||||
demandLevel: this.demandLevel,
|
||||
supplyLevel: this.supplyLevel,
|
||||
demandSideIsSubscription: this.demandSideIsSubscription
|
||||
|
||||
// , currentDemand: this.currentDemand
|
||||
// , currentSupply: this.currentSupply
|
||||
};
|
||||
};
|
||||
|
||||
DemandMatcher.prototype.boot = function () {
|
||||
var observerLevel = 1 + Math.max(this.demandLevel, this.supplyLevel);
|
||||
return [sub(this.demandPattern, this.metaLevel, observerLevel),
|
||||
pub(this.supplyPattern, this.metaLevel, observerLevel)];
|
||||
};
|
||||
|
||||
DemandMatcher.prototype.handleEvent = function (e) {
|
||||
if (e.type === "routes") {
|
||||
this.handleGestalt(e.gestalt);
|
||||
}
|
||||
};
|
||||
|
||||
DemandMatcher.prototype.handleGestalt = function (gestalt) {
|
||||
var newDemandMatcher = gestalt.project(this.demandProjectionSpec,
|
||||
!this.demandSideIsSubscription,
|
||||
this.metaLevel,
|
||||
this.demandLevel);
|
||||
var newSupplyMatcher = gestalt.project(this.supplyProjectionSpec,
|
||||
this.demandSideIsSubscription,
|
||||
this.metaLevel,
|
||||
this.supplyLevel);
|
||||
var newDemand = Route.arrayToSet(Route.matcherKeys(newDemandMatcher));
|
||||
var newSupply = Route.arrayToSet(Route.matcherKeys(newSupplyMatcher));
|
||||
var demandDelta = Route.setSubtract(newDemand, this.currentDemand);
|
||||
var supplyDelta = Route.setSubtract(this.currentSupply, newSupply);
|
||||
var demandIncr = Route.setSubtract(demandDelta, newSupply);
|
||||
var supplyDecr = Route.setIntersect(supplyDelta, newDemand);
|
||||
this.currentDemand = newDemand;
|
||||
this.currentSupply = newSupply;
|
||||
for (var k in demandIncr) this.onDemandIncrease(demandIncr[k]);
|
||||
for (var k in supplyDecr) this.onSupplyDecrease(supplyDecr[k]);
|
||||
};
|
||||
|
||||
/*---------------------------------------------------------------------------*/
|
||||
/* Utilities: deduplicator */
|
||||
|
||||
function Deduplicator(ttl_ms) {
|
||||
this.ttl_ms = ttl_ms || 10000;
|
||||
this.queue = [];
|
||||
this.map = {};
|
||||
this.timerId = null;
|
||||
}
|
||||
|
||||
Deduplicator.prototype.accept = function (m) {
|
||||
var s = JSON.stringify(m);
|
||||
if (s in this.map) return false;
|
||||
var entry = [(+new Date()) + this.ttl_ms, s, m];
|
||||
this.map[s] = entry;
|
||||
this.queue.push(entry);
|
||||
|
||||
if (this.timerId === null) {
|
||||
var self = this;
|
||||
this.timerId = setInterval(function () { self.expireMessages(); },
|
||||
this.ttl_ms > 1000 ? 1000 : this.ttl_ms);
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
Deduplicator.prototype.expireMessages = function () {
|
||||
var now = +new Date();
|
||||
while (this.queue.length > 0 && this.queue[0][0] <= now) {
|
||||
var entry = this.queue.shift();
|
||||
delete this.map[entry[1]];
|
||||
}
|
||||
if (this.queue.length === 0) {
|
||||
clearInterval(this.timerId);
|
||||
this.timerId = null;
|
||||
}
|
||||
};
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
module.exports.__ = __;
|
||||
module.exports._$ = _$;
|
||||
|
||||
module.exports.sub = sub;
|
||||
module.exports.pub = pub;
|
||||
module.exports.spawn = spawn;
|
||||
module.exports.updateRoutes = updateRoutes;
|
||||
module.exports.sendMessage = sendMessage;
|
||||
module.exports.shutdownWorld = shutdownWorld;
|
||||
|
||||
module.exports.World = World;
|
||||
module.exports.DemandMatcher = DemandMatcher;
|
||||
module.exports.Deduplicator = Deduplicator;
|
||||
module.exports.Route = Route;
|
|
@ -0,0 +1,26 @@
|
|||
// Reflection on function formal parameter lists.
|
||||
// This module is based on Angular's "injector" code,
|
||||
// https://github.com/angular/angular.js/blob/master/src/auto/injector.js,
|
||||
// MIT licensed, and hence:
|
||||
// Copyright (c) 2010-2014 Google, Inc. http://angularjs.org
|
||||
// Copyright (c) 2014 Tony Garnock-Jones
|
||||
|
||||
var FN_ARGS = /^function\s*[^\(]*\(\s*([^\)]*)\)/m;
|
||||
var FN_ARG_SPLIT = /,/;
|
||||
var STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg;
|
||||
|
||||
function formalParameters(fn) {
|
||||
var result = [];
|
||||
|
||||
var fnText = fn.toString().replace(STRIP_COMMENTS, '');
|
||||
var argDecl = fnText.match(FN_ARGS);
|
||||
var args = argDecl[1].split(FN_ARG_SPLIT);
|
||||
for (var i = 0; i < args.length; i++) {
|
||||
var trimmed = args[i].trim();
|
||||
if (trimmed) { result.push(trimmed); }
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
module.exports.formalParameters = formalParameters;
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,56 @@
|
|||
var Minimart = require("./minimart.js");
|
||||
var Route = Minimart.Route;
|
||||
var World = Minimart.World;
|
||||
var sub = Minimart.sub;
|
||||
var pub = Minimart.pub;
|
||||
var __ = Minimart.__;
|
||||
|
||||
function spawnRoutingTableWidget(selector, fragmentClass, domWrap, observationLevel) {
|
||||
observationLevel = observationLevel || 10;
|
||||
// ^ arbitrary: should be Infinity, when route.js supports it. TODO
|
||||
domWrap = domWrap || Minimart.DOM.defaultWrapFunction;
|
||||
|
||||
World.spawn({
|
||||
boot: function () { this.updateState(); },
|
||||
|
||||
state: Route.emptyGestalt.serialize(),
|
||||
nextState: Route.emptyGestalt.serialize(),
|
||||
timer: false,
|
||||
|
||||
localGestalt: (sub( domWrap(selector, fragmentClass, __), 0, 2)
|
||||
.union(pub(domWrap(selector, fragmentClass, __), 0, 2))
|
||||
.telescoped()),
|
||||
|
||||
digestGestalt: function (g) {
|
||||
return g.stripLabel().erasePath(this.localGestalt).serialize();
|
||||
},
|
||||
|
||||
updateState: function () {
|
||||
var elts = ["pre", Route.deserializeGestalt(this.state).pretty()];
|
||||
World.updateRoutes([sub(__, 0, observationLevel),
|
||||
pub(__, 0, observationLevel),
|
||||
pub(domWrap(selector, fragmentClass, elts))]);
|
||||
},
|
||||
|
||||
handleEvent: function (e) {
|
||||
var self = this;
|
||||
if (e.type === "routes") {
|
||||
self.nextState = self.digestGestalt(e.gestalt);
|
||||
if (self.timer) {
|
||||
clearTimeout(self.timer);
|
||||
self.timer = false;
|
||||
}
|
||||
self.timer = setTimeout(World.wrap(function () {
|
||||
if (JSON.stringify(self.nextState) !== JSON.stringify(self.state)) {
|
||||
self.state = self.nextState;
|
||||
self.updateState();
|
||||
}
|
||||
self.timer = false;
|
||||
}), 50);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
module.exports.spawnRoutingTableWidget = spawnRoutingTableWidget;
|
|
@ -1,18 +1,24 @@
|
|||
// Generic Spy
|
||||
var Minimart = require("./minimart.js");
|
||||
var World = Minimart.World;
|
||||
var sub = Minimart.sub;
|
||||
var pub = Minimart.pub;
|
||||
var __ = Minimart.__;
|
||||
|
||||
function Spy(label, useJson) {
|
||||
function Spy(label, useJson, observationLevel) {
|
||||
this.label = label || "SPY";
|
||||
this.observationLevel = observationLevel || 10; // arbitrary. Should be Infinity. TODO
|
||||
this.useJson = useJson;
|
||||
}
|
||||
|
||||
Spy.prototype.boot = function () {
|
||||
World.updateRoutes([sub(__, 0, Infinity), pub(__, 0, Infinity)]);
|
||||
return [sub(__, 0, this.observationLevel), pub(__, 0, this.observationLevel)];
|
||||
};
|
||||
|
||||
Spy.prototype.handleEvent = function (e) {
|
||||
switch (e.type) {
|
||||
case "routes":
|
||||
console.log(this.label, "routes", this.useJson ? JSON.stringify(e.routes) : e.routes);
|
||||
console.log(this.label, "routes", e.gestalt.pretty());
|
||||
break;
|
||||
case "message":
|
||||
var messageRepr;
|
||||
|
@ -28,3 +34,5 @@ Spy.prototype.handleEvent = function (e) {
|
|||
break;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports.Spy = Spy;
|
|
@ -0,0 +1,23 @@
|
|||
var Reflect = require("./reflect.js");
|
||||
|
||||
module.exports.extend = function (what, _with) {
|
||||
for (var prop in _with) {
|
||||
if (_with.hasOwnProperty(prop)) {
|
||||
what[prop] = _with[prop];
|
||||
}
|
||||
}
|
||||
return what;
|
||||
};
|
||||
|
||||
module.exports.kwApply = function (f, thisArg, args) {
|
||||
var formals = Reflect.formalParameters(f);
|
||||
var actuals = []
|
||||
for (var i = 0; i < formals.length; i++) {
|
||||
var formal = formals[i];
|
||||
if (!(formal in args)) {
|
||||
throw new Error("Function parameter '"+formal+"' not present in args");
|
||||
}
|
||||
actuals.push(args[formal]);
|
||||
}
|
||||
return f.apply(thisArg, actuals);
|
||||
};
|
|
@ -2,6 +2,11 @@
|
|||
// suspension/sleeping!) has caused periodic activities to be
|
||||
// interrupted, and warns others about it
|
||||
// Inspired by http://blog.alexmaccaw.com/javascript-wake-event
|
||||
var Minimart = require("./minimart.js");
|
||||
var World = Minimart.World;
|
||||
var sub = Minimart.sub;
|
||||
var pub = Minimart.pub;
|
||||
var __ = Minimart.__;
|
||||
|
||||
function WakeDetector(period) {
|
||||
this.message = "wake";
|
||||
|
@ -12,8 +17,8 @@ function WakeDetector(period) {
|
|||
|
||||
WakeDetector.prototype.boot = function () {
|
||||
var self = this;
|
||||
World.updateRoutes([pub(this.message)]);
|
||||
this.timerId = setInterval(World.wrap(function () { self.trigger(); }), this.period);
|
||||
return [pub(this.message)];
|
||||
};
|
||||
|
||||
WakeDetector.prototype.handleEvent = function (e) {};
|
||||
|
@ -25,3 +30,5 @@ WakeDetector.prototype.trigger = function () {
|
|||
}
|
||||
this.mostRecentTrigger = now;
|
||||
};
|
||||
|
||||
module.exports.WakeDetector = WakeDetector;
|
|
@ -1,3 +1,12 @@
|
|||
var Minimart = require("./minimart.js");
|
||||
var Codec = require("./codec.js");
|
||||
var Route = Minimart.Route;
|
||||
var World = Minimart.World;
|
||||
var sub = Minimart.sub;
|
||||
var pub = Minimart.pub;
|
||||
var __ = Minimart.__;
|
||||
var _$ = Minimart._$;
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// WebSocket client driver
|
||||
|
||||
|
@ -8,14 +17,19 @@ var DEFAULT_PING_INTERVAL = DEFAULT_IDLE_TIMEOUT - 10000;
|
|||
|
||||
function WebSocketConnection(label, wsurl, shouldReconnect) {
|
||||
this.label = label;
|
||||
this.sendsAttempted = 0;
|
||||
this.sendsTransmitted = 0;
|
||||
this.receiveCount = 0;
|
||||
this.sock = null;
|
||||
this.wsurl = wsurl;
|
||||
this.shouldReconnect = shouldReconnect ? true : false;
|
||||
this.reconnectDelay = DEFAULT_RECONNECT_DELAY;
|
||||
this.localRoutes = [];
|
||||
this.peerRoutes = [];
|
||||
this.localGestalt = Route.emptyGestalt;
|
||||
this.peerGestalt = Route.emptyGestalt;
|
||||
this.prevLocalRoutesMessage = null;
|
||||
this.prevPeerRoutesMessage = null;
|
||||
this.sock = null;
|
||||
this.deduplicator = new Deduplicator();
|
||||
this.deduplicator = new Minimart.Deduplicator();
|
||||
this.connectionCount = 0;
|
||||
|
||||
this.activityTimestamp = 0;
|
||||
this.idleTimeout = DEFAULT_IDLE_TIMEOUT;
|
||||
|
@ -24,6 +38,20 @@ function WebSocketConnection(label, wsurl, shouldReconnect) {
|
|||
this.pingTimer = null;
|
||||
}
|
||||
|
||||
WebSocketConnection.prototype.debugState = function () {
|
||||
return {
|
||||
label: this.label,
|
||||
sendsAttempted: this.sendsAttempted,
|
||||
sendsTransmitted: this.sendsTransmitted,
|
||||
receiveCount: this.receiveCount,
|
||||
wsurl: this.wsurl,
|
||||
shouldReconnect: this.shouldReconnect,
|
||||
reconnectDelay: this.reconnectDelay,
|
||||
connectionCount: this.connectionCount,
|
||||
activityTimestamp: this.activityTimestamp
|
||||
};
|
||||
};
|
||||
|
||||
WebSocketConnection.prototype.clearHeartbeatTimers = function () {
|
||||
if (this.idleTimer) { clearTimeout(this.idleTimer); this.idleTimer = null; }
|
||||
if (this.pingTimer) { clearTimeout(this.pingTimer); this.pingTimer = null; }
|
||||
|
@ -43,31 +71,19 @@ WebSocketConnection.prototype.statusRoute = function (status) {
|
|||
return pub([this.label + "_state", status]);
|
||||
};
|
||||
|
||||
WebSocketConnection.prototype.relayRoutes = function () {
|
||||
// fresh copy each time, suitable for in-place extension/mutation
|
||||
return [this.statusRoute(this.isConnected() ? "connected" : "disconnected"),
|
||||
pub([this.label, __, __], 0, 1000),
|
||||
sub([this.label, __, __], 0, 1000)];
|
||||
WebSocketConnection.prototype.relayGestalt = function () {
|
||||
return this.statusRoute(this.isConnected() ? "connected" : "disconnected")
|
||||
.union(pub([this.label, __, __], 0, 10))
|
||||
.union(sub([this.label, __, __], 0, 10));
|
||||
// TODO: level 10 is ad-hoc; support infinity at some point in future
|
||||
};
|
||||
|
||||
WebSocketConnection.prototype.aggregateRoutes = function () {
|
||||
var rs = this.relayRoutes();
|
||||
for (var i = 0; i < this.peerRoutes.length; i++) {
|
||||
var r = this.peerRoutes[i];
|
||||
rs.push(new Route(r.isSubscription,
|
||||
// TODO: This is a horrible syntactic hack
|
||||
// (in conjunction with the numberness-test
|
||||
// in handleEvent's routes handler) for
|
||||
// distinguishing routes published on behalf
|
||||
// of the remote side from those published
|
||||
// by the local side. See (**HACK**) mark
|
||||
// below.
|
||||
[this.label, __, r.pattern],
|
||||
r.metaLevel,
|
||||
r.level));
|
||||
}
|
||||
// console.log("WebSocketConnection.aggregateRoutes", this.label, rs);
|
||||
return rs;
|
||||
WebSocketConnection.prototype.aggregateGestalt = function () {
|
||||
var self = this;
|
||||
return this.peerGestalt.transform(function (m, metaLevel) {
|
||||
return Route.compilePattern(true,
|
||||
[self.label, metaLevel, Route.embeddedMatcher(m)]);
|
||||
}).union(this.relayGestalt());
|
||||
};
|
||||
|
||||
WebSocketConnection.prototype.boot = function () {
|
||||
|
@ -84,48 +100,61 @@ WebSocketConnection.prototype.isConnected = function () {
|
|||
|
||||
WebSocketConnection.prototype.safeSend = function (m) {
|
||||
try {
|
||||
if (this.isConnected()) { this.sock.send(m); }
|
||||
this.sendsAttempted++;
|
||||
if (this.isConnected()) {
|
||||
this.sock.send(m);
|
||||
this.sendsTransmitted++;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("Trapped exn while sending", e);
|
||||
}
|
||||
};
|
||||
|
||||
WebSocketConnection.prototype.sendLocalRoutes = function () {
|
||||
this.safeSend(JSON.stringify(encodeEvent(updateRoutes(this.localRoutes))));
|
||||
var newLocalRoutesMessage =
|
||||
JSON.stringify(Codec.encodeEvent(Minimart.updateRoutes([this.localGestalt])));
|
||||
if (this.prevLocalRoutesMessage !== newLocalRoutesMessage) {
|
||||
this.prevLocalRoutesMessage = newLocalRoutesMessage;
|
||||
this.safeSend(newLocalRoutesMessage);
|
||||
}
|
||||
};
|
||||
|
||||
WebSocketConnection.prototype.collectMatchers = function (getAdvertisements, level, g) {
|
||||
var extractMetaLevels = Route.compileProjection([this.label, _$, __]);
|
||||
var mls = Route.matcherKeys(g.project(extractMetaLevels, getAdvertisements, 0, level));
|
||||
for (var i = 0; i < mls.length; i++) {
|
||||
var metaLevel = mls[i][0]; // only one capture in the projection
|
||||
var extractMatchers = Route.compileProjection([this.label, metaLevel, _$]);
|
||||
var m = g.project(extractMatchers, getAdvertisements, 0, level);
|
||||
this.localGestalt = this.localGestalt.union(Route.simpleGestalt(getAdvertisements,
|
||||
Route.embeddedMatcher(m),
|
||||
metaLevel,
|
||||
level));
|
||||
}
|
||||
};
|
||||
|
||||
WebSocketConnection.prototype.handleEvent = function (e) {
|
||||
// console.log("WebSocketConnection.handleEvent", e);
|
||||
switch (e.type) {
|
||||
case "routes":
|
||||
this.localRoutes = [];
|
||||
for (var i = 0; i < e.routes.length; i++) {
|
||||
var r = e.routes[i];
|
||||
if (r.pattern.length && r.pattern.length === 3
|
||||
&& r.pattern[0] === this.label
|
||||
// TODO: This is a horrible syntactic hack (in
|
||||
// conjunction with the use of __ in in
|
||||
// aggregateRoutes) for distinguishing routes
|
||||
// published on behalf of the remote side from those
|
||||
// published by the local side. See (**HACK**) mark
|
||||
// above.
|
||||
&& typeof(r.pattern[1]) === "number")
|
||||
{
|
||||
this.localRoutes.push(new Route(r.isSubscription,
|
||||
r.pattern[2],
|
||||
r.pattern[1],
|
||||
r.level));
|
||||
}
|
||||
// TODO: GROSS - erasing by pid!
|
||||
var nLevels = e.gestalt.levelCount(0);
|
||||
var relayGestalt = Route.fullGestalt(1, nLevels).label(World.activePid());
|
||||
var g = e.gestalt.erasePath(relayGestalt);
|
||||
this.localGestalt = Route.emptyGestalt;
|
||||
for (var level = 0; level < nLevels; level++) {
|
||||
this.collectMatchers(false, level, g);
|
||||
this.collectMatchers(true, level, g);
|
||||
}
|
||||
|
||||
this.sendLocalRoutes();
|
||||
break;
|
||||
case "message":
|
||||
var m = e.message;
|
||||
if (m.length && m.length === 3
|
||||
&& m[0] === this.label
|
||||
&& typeof(m[1]) === "number")
|
||||
if (m.length && m.length === 3 && m[0] === this.label)
|
||||
{
|
||||
var encoded = JSON.stringify(encodeEvent(sendMessage(m[2], m[1], e.isFeedback)));
|
||||
var encoded = JSON.stringify(Codec.encodeEvent(
|
||||
Minimart.sendMessage(m[2], m[1], e.isFeedback)));
|
||||
if (this.deduplicator.accept(encoded)) {
|
||||
this.safeSend(encoded);
|
||||
}
|
||||
|
@ -149,15 +178,20 @@ WebSocketConnection.prototype.forceclose = function (keepReconnectDelay) {
|
|||
WebSocketConnection.prototype.reconnect = function () {
|
||||
var self = this;
|
||||
this.forceclose(true);
|
||||
this.connectionCount++;
|
||||
this.sock = new WebSocket(this.wsurl);
|
||||
this.sock.onopen = World.wrap(function (e) { return self.onopen(e); });
|
||||
this.sock.onmessage = World.wrap(function (e) { return self.onmessage(e); });
|
||||
this.sock.onmessage = World.wrap(function (e) {
|
||||
self.receiveCount++;
|
||||
return self.onmessage(e);
|
||||
});
|
||||
this.sock.onclose = World.wrap(function (e) { return self.onclose(e); });
|
||||
};
|
||||
|
||||
WebSocketConnection.prototype.onopen = function (e) {
|
||||
console.log("connected to " + this.sock.url);
|
||||
this.reconnectDelay = DEFAULT_RECONNECT_DELAY;
|
||||
this.prevLocalRoutesMessage = null;
|
||||
this.sendLocalRoutes();
|
||||
};
|
||||
|
||||
|
@ -173,13 +207,13 @@ WebSocketConnection.prototype.onmessage = function (wse) {
|
|||
return; // recordActivity already took care of our timers
|
||||
}
|
||||
|
||||
var e = decodeAction(j);
|
||||
var e = Codec.decodeAction(j);
|
||||
switch (e.type) {
|
||||
case "routes":
|
||||
if (this.prevPeerRoutesMessage !== wse.data) {
|
||||
this.prevPeerRoutesMessage = wse.data;
|
||||
this.peerRoutes = e.routes;
|
||||
World.updateRoutes(this.aggregateRoutes());
|
||||
this.peerGestalt = e.gestalt;
|
||||
World.updateRoutes([this.aggregateGestalt()]);
|
||||
}
|
||||
break;
|
||||
case "message":
|
||||
|
@ -195,7 +229,7 @@ WebSocketConnection.prototype.onclose = function (e) {
|
|||
console.log("onclose", e);
|
||||
|
||||
// Update routes to give clients some indication of the discontinuity
|
||||
World.updateRoutes(this.aggregateRoutes());
|
||||
World.updateRoutes([this.aggregateGestalt()]);
|
||||
|
||||
if (this.shouldReconnect) {
|
||||
console.log("reconnecting to " + this.wsurl + " in " + this.reconnectDelay + "ms");
|
||||
|
@ -209,32 +243,5 @@ WebSocketConnection.prototype.onclose = function (e) {
|
|||
};
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// Wire protocol representation of events and actions
|
||||
|
||||
function encodeEvent(e) {
|
||||
switch (e.type) {
|
||||
case "routes":
|
||||
var rs = [];
|
||||
for (var i = 0; i < e.routes.length; i++) {
|
||||
rs.push(e.routes[i].toJSON());
|
||||
}
|
||||
return ["routes", rs];
|
||||
case "message":
|
||||
return ["message", e.message, e.metaLevel, e.isFeedback];
|
||||
}
|
||||
}
|
||||
|
||||
function decodeAction(j) {
|
||||
switch (j[0]) {
|
||||
case "routes":
|
||||
var rs = [];
|
||||
for (var i = 0; i < j[1].length; i++) {
|
||||
rs.push(Route.fromJSON(j[1][i]));
|
||||
}
|
||||
return updateRoutes(rs);
|
||||
case "message":
|
||||
return sendMessage(j[1], j[2], j[3]);
|
||||
default:
|
||||
throw { message: "Invalid JSON-encoded action: " + JSON.stringify(j) };
|
||||
}
|
||||
}
|
||||
module.exports.WebSocketConnection = WebSocketConnection;
|
|
@ -0,0 +1,56 @@
|
|||
/* Web Worker interface */
|
||||
var Ground = require("./ground.js").Ground;
|
||||
var Util = require("./util.js");
|
||||
var Codec = require("./codec.js");
|
||||
|
||||
var Minimart = require("./minimart.js");
|
||||
var World = Minimart.World;
|
||||
var sub = Minimart.sub;
|
||||
var pub = Minimart.pub;
|
||||
var __ = Minimart.__;
|
||||
|
||||
var BuiltinWorker = typeof window !== 'undefined' && window.Worker;
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
function Worker(scriptUrl) {
|
||||
this.scriptUrl = scriptUrl;
|
||||
this.w = new BuiltinWorker(scriptUrl);
|
||||
}
|
||||
|
||||
Worker.prototype.boot = function () {
|
||||
this.w.onmessage = World.wrap(function (e) {
|
||||
console.log("Received from worker", JSON.stringify(e.data));
|
||||
World.current().enqueueAction(World.activePid(), Codec.decodeAction(e.data));
|
||||
});
|
||||
};
|
||||
|
||||
Worker.prototype.handleEvent = function (e) {
|
||||
console.log("Sending to worker", JSON.stringify(Codec.encodeEvent(e)));
|
||||
this.w.postMessage(Codec.encodeEvent(e));
|
||||
};
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
function WorkerGround(bootFn) {
|
||||
var self = this;
|
||||
Ground.call(this, bootFn);
|
||||
onmessage = function (e) {
|
||||
console.log("Received from main page", JSON.stringify(e.data));
|
||||
self.world.handleEvent(Codec.decodeEvent(e.data));
|
||||
self.startStepping();
|
||||
};
|
||||
}
|
||||
|
||||
WorkerGround.prototype = Util.extend({}, Ground.prototype);
|
||||
|
||||
WorkerGround.prototype.enqueueAction = function (pid, action) {
|
||||
console.log("Sending to main page", JSON.stringify(Codec.encodeAction(action)));
|
||||
postMessage(Codec.encodeAction(action));
|
||||
console.log("Sent to main page");
|
||||
};
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
module.exports.Worker = Worker;
|
||||
module.exports.WorkerGround = WorkerGround;
|
|
@ -0,0 +1,99 @@
|
|||
var expect = require('expect.js');
|
||||
|
||||
var Minimart = require('../src/main.js');
|
||||
|
||||
var World = Minimart.World;
|
||||
var Actor = Minimart.Actor;
|
||||
var sub = Minimart.sub;
|
||||
var pub = Minimart.pub;
|
||||
var __ = Minimart.__;
|
||||
var _$ = Minimart._$;
|
||||
|
||||
function configurationTrace(bootConfiguration) {
|
||||
var eventLog = [];
|
||||
function trace(item) {
|
||||
eventLog.push(item);
|
||||
}
|
||||
|
||||
var G = new Minimart.Ground(function () {
|
||||
bootConfiguration(trace);
|
||||
});
|
||||
|
||||
while (G.step()) {
|
||||
// do nothing until G becomes inert
|
||||
}
|
||||
|
||||
return eventLog;
|
||||
}
|
||||
|
||||
function checkTrace(bootConfiguration, expected) {
|
||||
expect(configurationTrace(bootConfiguration)).to.eql(expected);
|
||||
}
|
||||
|
||||
describe("configurationTrace", function() {
|
||||
describe("with an inert configuration", function () {
|
||||
it("should yield an empty trace", function () {
|
||||
checkTrace(function (trace) {}, []);
|
||||
});
|
||||
});
|
||||
|
||||
describe("with a single trace in an inert configuration", function () {
|
||||
it("should yield that trace", function () {
|
||||
checkTrace(function (trace) { trace(1) }, [1]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("with some traced communication", function () {
|
||||
it("should yield an appropriate trace", function () {
|
||||
checkTrace(function (trace) {
|
||||
World.spawn({
|
||||
boot: function () { return [sub(__)] },
|
||||
handleEvent: function (e) {
|
||||
trace(e);
|
||||
}
|
||||
});
|
||||
World.send(123);
|
||||
World.send(234);
|
||||
}, [Minimart.updateRoutes([]),
|
||||
Minimart.sendMessage(123),
|
||||
Minimart.sendMessage(234)]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("nonempty initial routes", function () {
|
||||
it("should be immediately signalled to the process", function () {
|
||||
// Specifically, no Minimart.updateRoutes([]) first.
|
||||
checkTrace(function (trace) {
|
||||
World.spawn({
|
||||
boot: function () { return [pub(["A", __])] },
|
||||
handleEvent: function (e) {
|
||||
World.spawn({
|
||||
boot: function () { return [sub(["A", __], 0, 1)] },
|
||||
handleEvent: trace
|
||||
});
|
||||
}
|
||||
});
|
||||
}, [Minimart.updateRoutes([pub(["A", __]).label(1)])]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("actor with nonempty initial routes", function () {
|
||||
it("shouldn't see initial empty conversational context", function () {
|
||||
checkTrace(function (trace) {
|
||||
World.spawn({
|
||||
boot: function () { return [pub(["A", __])] },
|
||||
handleEvent: function (e) {
|
||||
World.spawn(new Actor(function () {
|
||||
Actor.observeAdvertisers(
|
||||
function () { return ["A", __] },
|
||||
{ presence: "isPresent" },
|
||||
function () {
|
||||
trace(["isPresent", this.isPresent]);
|
||||
});
|
||||
}));
|
||||
}
|
||||
});
|
||||
}, [["isPresent", true]]);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,544 @@
|
|||
var expect = require('expect.js');
|
||||
var util = require('util');
|
||||
var r = require("../src/route.js");
|
||||
|
||||
function checkPrettyMatcher(m, expected) {
|
||||
expect(r.prettyMatcher(m)).to.equal(expected.join('\n'));
|
||||
}
|
||||
|
||||
function checkPrettyGestalt(g, expected) {
|
||||
expect(g.pretty()).to.equal(expected.join('\n') + '\n');
|
||||
}
|
||||
|
||||
describe("basic pattern compilation", function () {
|
||||
var sAny = r.arrayToSet(['mAny']);
|
||||
var sAAny = r.arrayToSet(['mAAny']);
|
||||
var mAny = r.compilePattern(sAny, r.__);
|
||||
var mAAny = r.compilePattern(sAAny, ['A', r.__]);
|
||||
|
||||
it("should print as expected", function () {
|
||||
checkPrettyMatcher(mAny, [' ★ >{["mAny"]}']);
|
||||
checkPrettyMatcher(mAAny, [' < "A" ★ > >{["mAAny"]}']);
|
||||
});
|
||||
|
||||
describe("of wildcard", function () {
|
||||
it("should match anything", function () {
|
||||
expect(r.matchValue(mAny, 'hi')).to.eql(sAny);
|
||||
expect(r.matchValue(mAny, ['A', 'hi'])).to.eql(sAny);
|
||||
expect(r.matchValue(mAny, ['B', 'hi'])).to.eql(sAny);
|
||||
expect(r.matchValue(mAny, ['A', [['hi']]])).to.eql(sAny);
|
||||
});
|
||||
});
|
||||
|
||||
describe("of A followed by wildcard", function () {
|
||||
it("should match A followed by anything", function () {
|
||||
expect(r.matchValue(mAAny, 'hi')).to.be(null);
|
||||
expect(r.matchValue(mAAny, ['A', 'hi'])).to.eql(sAAny);
|
||||
expect(r.matchValue(mAAny, ['B', 'hi'])).to.be(null);
|
||||
expect(r.matchValue(mAAny, ['A', [['hi']]])).to.eql(sAAny);
|
||||
});
|
||||
});
|
||||
|
||||
it("should observe basic (in)equivalences", function () {
|
||||
expect(r.matcherEquals(mAny, mAAny)).to.be(false);
|
||||
expect(r.matcherEquals(mAny, mAny)).to.be(true);
|
||||
expect(r.matcherEquals(mAAny, mAAny)).to.be(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("unions", function () {
|
||||
it("should collapse common prefix wildcard", function () {
|
||||
checkPrettyMatcher(r.union(r.compilePattern(r.arrayToSet(['A']), [r.__, 'A']),
|
||||
r.compilePattern(r.arrayToSet(['B']), [r.__, 'B'])),
|
||||
[' < ★ "A" > >{["A"]}',
|
||||
' "B" > >{["B"]}']);
|
||||
});
|
||||
|
||||
it("should unroll wildcard unioned with nonwildcard", function () {
|
||||
checkPrettyMatcher(r.union(r.compilePattern(r.arrayToSet(['A']), [r.__, 'A']),
|
||||
r.compilePattern(r.arrayToSet(['W']), r.__)),
|
||||
[' ★ >{["W"]}',
|
||||
' < ★ "A" ★...> >{["W"]}',
|
||||
' > >{["A","W"]}',
|
||||
' ★...> >{["W"]}',
|
||||
' > >{["W"]}',
|
||||
' > >{["W"]}']);
|
||||
});
|
||||
|
||||
it("should properly multiply out", function () {
|
||||
checkPrettyMatcher(r.union(r.compilePattern(r.arrayToSet(['A']), [r.__, 2]),
|
||||
r.compilePattern(r.arrayToSet(['C']), [1, 3]),
|
||||
r.compilePattern(r.arrayToSet(['B']), [3, 4])),
|
||||
[' < 1 2 > >{["A"]}',
|
||||
' 3 > >{["C"]}',
|
||||
' 3 2 > >{["A"]}',
|
||||
' 4 > >{["B"]}',
|
||||
' ★ 2 > >{["A"]}']);
|
||||
|
||||
checkPrettyMatcher(r.union(r.compilePattern(r.arrayToSet(['C']), [1, 3]),
|
||||
r.compilePattern(r.arrayToSet(['B']), [3, 4])),
|
||||
[' < 1 3 > >{["C"]}',
|
||||
' 3 4 > >{["B"]}']);
|
||||
|
||||
checkPrettyMatcher(r.union(r.compilePattern(r.arrayToSet(['A']), [r.__, 2]),
|
||||
r.compilePattern(r.arrayToSet(['C']), [1, 3])),
|
||||
[' < 1 2 > >{["A"]}',
|
||||
' 3 > >{["C"]}',
|
||||
' ★ 2 > >{["A"]}']);
|
||||
|
||||
checkPrettyMatcher(r.union(r.compilePattern(r.arrayToSet(['A']), [r.__, 2]),
|
||||
r.compilePattern(r.arrayToSet(['B']), [3, 4])),
|
||||
[' < 3 2 > >{["A"]}',
|
||||
' 4 > >{["B"]}',
|
||||
' ★ 2 > >{["A"]}']);
|
||||
});
|
||||
|
||||
it("should correctly construct intermediate values", function () {
|
||||
var MU = r.emptyMatcher;
|
||||
MU = r.union(MU, r.compilePattern(r.arrayToSet(['A']), [r.__, 2]));
|
||||
checkPrettyMatcher(MU, [' < ★ 2 > >{["A"]}']);
|
||||
MU = r.union(MU, r.compilePattern(r.arrayToSet(['C']), [1, 3]));
|
||||
checkPrettyMatcher(MU, [' < 1 2 > >{["A"]}',
|
||||
' 3 > >{["C"]}',
|
||||
' ★ 2 > >{["A"]}']);
|
||||
MU = r.union(MU, r.compilePattern(r.arrayToSet(['B']), [3, 4]));
|
||||
checkPrettyMatcher(MU, [' < 1 2 > >{["A"]}',
|
||||
' 3 > >{["C"]}',
|
||||
' 3 2 > >{["A"]}',
|
||||
' 4 > >{["B"]}',
|
||||
' ★ 2 > >{["A"]}']);
|
||||
});
|
||||
|
||||
it("should handle identical patterns with different pids", function () {
|
||||
var m = r.union(r.compilePattern(r.arrayToSet('B'), [2]),
|
||||
r.compilePattern(r.arrayToSet('C'), [3]));
|
||||
checkPrettyMatcher(m, [' < 2 > >{["B"]}',
|
||||
' 3 > >{["C"]}']);
|
||||
m = r.union(r.compilePattern(r.arrayToSet('A'), [2]), m);
|
||||
checkPrettyMatcher(m, [' < 2 > >{["A","B"]}',
|
||||
' 3 > >{["C"]}']);
|
||||
});
|
||||
});
|
||||
|
||||
describe("projections", function () {
|
||||
describe("with picky structure", function () {
|
||||
var proj = r.compileProjection(r._$("v", [[r.__]]));
|
||||
|
||||
it("should include things that match as well as wildcards", function () {
|
||||
checkPrettyMatcher(r.project(r.union(r.compilePattern(r.arrayToSet(['A']), r.__),
|
||||
r.compilePattern(r.arrayToSet(['B']), [['b']])),
|
||||
proj),
|
||||
[' < < "b" > > >{["B","A"]}',
|
||||
' ★ > > >{["A"]}']);
|
||||
});
|
||||
|
||||
it("should exclude things that lack the required structure", function () {
|
||||
checkPrettyMatcher(r.project(r.union(r.compilePattern(r.arrayToSet(['A']), r.__),
|
||||
r.compilePattern(r.arrayToSet(['B']), ['b'])),
|
||||
proj),
|
||||
[' < < ★ > > >{["A"]}']);
|
||||
});
|
||||
});
|
||||
|
||||
describe("simple positional", function () {
|
||||
var proj = r.compileProjection([r._$, r._$]);
|
||||
|
||||
it("should collapse common prefixes", function () {
|
||||
checkPrettyMatcher(r.project(r.union(r.compilePattern(r.arrayToSet(['A']), [1, 2]),
|
||||
r.compilePattern(r.arrayToSet(['C']), [1, 3]),
|
||||
r.compilePattern(r.arrayToSet(['B']), [3, 4])),
|
||||
proj),
|
||||
[' 1 2 >{["A"]}',
|
||||
' 3 >{["C"]}',
|
||||
' 3 4 >{["B"]}']);
|
||||
});
|
||||
|
||||
it("should yield a correct set of results", function () {
|
||||
expect(r.matcherKeys(r.project(r.union(r.compilePattern(r.arrayToSet(['A']), [1, 2]),
|
||||
r.compilePattern(r.arrayToSet(['C']), [1, 3]),
|
||||
r.compilePattern(r.arrayToSet(['B']), [3, 4])),
|
||||
proj))).to.eql([[1, 2], [1, 3], [3, 4]]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("erasePath after union", function () {
|
||||
var R1 = r.compilePattern(r.arrayToSet(['A']), [r.__, "B"]);
|
||||
var R2 = r.compilePattern(r.arrayToSet(['B']), ["A", r.__]);
|
||||
var R12 = r.union(R1, R2);
|
||||
|
||||
it("should have sane preconditions", function () { // Am I doing this right?
|
||||
checkPrettyMatcher(R1, [' < ★ "B" > >{["A"]}']);
|
||||
checkPrettyMatcher(R2, [' < "A" ★ > >{["B"]}']);
|
||||
checkPrettyMatcher(R12, [' < "A" "B" > >{["B","A"]}',
|
||||
' ★ > >{["B"]}',
|
||||
' ★ "B" > >{["A"]}']);
|
||||
});
|
||||
|
||||
it("should yield the remaining ingredients of the union", function () {
|
||||
expect(r.matcherEquals(r.erasePath(R12, R1), R2)).to.be(true);
|
||||
expect(r.matcherEquals(r.erasePath(R12, R2), R1)).to.be(true);
|
||||
expect(r.matcherEquals(r.erasePath(R12, R1), R1)).to.be(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("basic gestalt construction", function () {
|
||||
it("should print as expected", function () {
|
||||
checkPrettyGestalt(r.simpleGestalt(false, "A", 0, 0),
|
||||
['GESTALT metalevel 0 level 0:',
|
||||
' - subs: "A" >{true}']);
|
||||
checkPrettyGestalt(r.simpleGestalt(true, "B", 0, 0),
|
||||
['GESTALT metalevel 0 level 0:',
|
||||
' - advs: "B" >{true}']);
|
||||
checkPrettyGestalt(r.simpleGestalt(false, "A", 0, 0).union(r.simpleGestalt(true, "B", 0, 0)),
|
||||
['GESTALT metalevel 0 level 0:',
|
||||
' - subs: "A" >{true}',
|
||||
' - advs: "B" >{true}']);
|
||||
|
||||
checkPrettyGestalt(r.simpleGestalt(false, "A", 2, 2),
|
||||
['GESTALT metalevel 2 level 2:',
|
||||
' - subs: "A" >{true}']);
|
||||
checkPrettyGestalt(r.simpleGestalt(true, "B", 2, 2),
|
||||
['GESTALT metalevel 2 level 2:',
|
||||
' - advs: "B" >{true}']);
|
||||
checkPrettyGestalt(r.simpleGestalt(false, "A", 2, 2).union(r.simpleGestalt(true, "B", 2, 2)),
|
||||
['GESTALT metalevel 2 level 2:',
|
||||
' - subs: "A" >{true}',
|
||||
' - advs: "B" >{true}']);
|
||||
});
|
||||
});
|
||||
|
||||
describe("matching", function () {
|
||||
function check1(gMetalevel, level, mMetalevel) {
|
||||
var g = r.simpleGestalt(false, "A", gMetalevel, level).label(123);
|
||||
var result = g.matchValue("A", mMetalevel, false);
|
||||
if (gMetalevel === mMetalevel) {
|
||||
it("should match at level "+level, function () {
|
||||
expect(result).to.eql([123]);
|
||||
});
|
||||
} else {
|
||||
it("should not match at level "+level, function () {
|
||||
expect(result).to.eql([]);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function gMetaLevelCheck(gMetalevel, mMetalevel) {
|
||||
describe("at gestalt metalevel "+gMetalevel+", message metalevel "+mMetalevel, function () {
|
||||
check1(gMetalevel, 0, mMetalevel);
|
||||
check1(gMetalevel, 1, mMetalevel);
|
||||
check1(gMetalevel, 2, mMetalevel);
|
||||
});
|
||||
}
|
||||
|
||||
function mMetaLevelCheck(mMetalevel) {
|
||||
gMetaLevelCheck(0, mMetalevel);
|
||||
gMetaLevelCheck(2, mMetalevel);
|
||||
}
|
||||
|
||||
mMetaLevelCheck(0);
|
||||
mMetaLevelCheck(1);
|
||||
mMetaLevelCheck(2);
|
||||
});
|
||||
|
||||
describe("gestalt filtering", function () {
|
||||
function check1(metalevel, observedLevel, observerLevel) {
|
||||
var observer = r.simpleGestalt(true, r.__, metalevel, observerLevel).label("observer");
|
||||
var observed = r.simpleGestalt(false, "A", metalevel, observedLevel).label("observed");
|
||||
var resultM = observed.filter(observer);
|
||||
var resultL = observed.match(observer);
|
||||
if (observedLevel < observerLevel) {
|
||||
it("should be able to see gestalt at level "+observedLevel, function () {
|
||||
expect(resultM.isEmpty()).to.be(false);
|
||||
expect(resultL).to.eql(["observer"]);
|
||||
});
|
||||
} else {
|
||||
it("should not be able to see gestalt at level "+observedLevel, function () {
|
||||
expect(resultM.isEmpty()).to.be(true);
|
||||
expect(resultL).to.eql([]);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function metalevelCheck(metalevel, observerLevel) {
|
||||
describe("observer at level "+observerLevel+" in metalevel "+metalevel, function () {
|
||||
check1(metalevel, 0, observerLevel);
|
||||
check1(metalevel, 1, observerLevel);
|
||||
check1(metalevel, 2, observerLevel);
|
||||
});
|
||||
}
|
||||
|
||||
function levelCheck(observerLevel) {
|
||||
metalevelCheck(0, observerLevel);
|
||||
metalevelCheck(2, observerLevel);
|
||||
}
|
||||
|
||||
levelCheck(0);
|
||||
levelCheck(1);
|
||||
levelCheck(2);
|
||||
});
|
||||
|
||||
describe("matcher equality", function () {
|
||||
it("should not rely on object identity", function () {
|
||||
expect(r.matcherEquals(r.union(r.compilePattern(r.arrayToSet(['A']), [r.__, 'A']),
|
||||
r.compilePattern(r.arrayToSet(['B']), [r.__, 'B'])),
|
||||
r.union(r.compilePattern(r.arrayToSet(['A']), [r.__, 'A']),
|
||||
r.compilePattern(r.arrayToSet(['B']), [r.__, 'B']))))
|
||||
.to.be(true);
|
||||
});
|
||||
|
||||
it("should respect commutativity of union", function () {
|
||||
expect(r.matcherEquals(r.union(r.compilePattern(r.arrayToSet(['A']), [r.__, 'A']),
|
||||
r.compilePattern(r.arrayToSet(['B']), [r.__, 'B'])),
|
||||
r.union(r.compilePattern(r.arrayToSet(['B']), [r.__, 'B']),
|
||||
r.compilePattern(r.arrayToSet(['A']), [r.__, 'A']))))
|
||||
.to.be(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("gestalt equality", function () {
|
||||
it("should distinguish emptyGestalt reliably", function () {
|
||||
expect(r.simpleGestalt(false, r.__, 0, 10)
|
||||
.union(r.simpleGestalt(true, r.__, 0, 10))
|
||||
.equals(r.emptyGestalt))
|
||||
.to.be(false);
|
||||
});
|
||||
|
||||
it("should not rely on object identity", function () {
|
||||
expect(r.simpleGestalt(false, "A", 0, 0).union(r.simpleGestalt(true, "B", 0, 0))
|
||||
.equals(r.simpleGestalt(false, "A", 0, 0).union(r.simpleGestalt(true, "B", 0, 0))))
|
||||
.to.be(true);
|
||||
});
|
||||
|
||||
it("should respect commutativity of union", function () {
|
||||
expect(r.simpleGestalt(false, "A", 0, 0).union(r.simpleGestalt(true, "B", 0, 0))
|
||||
.equals(r.simpleGestalt(true, "B", 0, 0).union(r.simpleGestalt(false, "A", 0, 0))))
|
||||
.to.be(true);
|
||||
});
|
||||
|
||||
it("should discriminate between advs and subs", function () {
|
||||
expect(r.simpleGestalt(false, "A", 0, 0).union(r.simpleGestalt(true, "B", 0, 0))
|
||||
.equals(r.simpleGestalt(false, "B", 0, 0).union(r.simpleGestalt(true, "A", 0, 0))))
|
||||
.to.be(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("matcherKeys on wild matchers", function () {
|
||||
var M = r.union(r.compilePattern(r.arrayToSet(['A']), [r.__, 2]),
|
||||
r.compilePattern(r.arrayToSet(['C']), [1, 3]),
|
||||
r.compilePattern(r.arrayToSet(['B']), [3, 4]));
|
||||
|
||||
it("should yield null to signal an infinite result", function () {
|
||||
expect(r.matcherKeys(r.project(M, r.compileProjection([r._$, r._$])))).to.be(null);
|
||||
});
|
||||
|
||||
it("should extract just the second array element successfully", function () {
|
||||
expect(r.matcherKeys(r.project(M, r.compileProjection([r.__, r._$])))).to.eql([[2],[3],[4]]);
|
||||
});
|
||||
|
||||
var M2 = r.project(M, r.compileProjection([r._$, r._$]));
|
||||
|
||||
it("should survive double-projection", function () {
|
||||
expect(r.matcherKeys(r.project(M2, r.compileProjection(r.__, r._$)))).to.eql([[2],[3],[4]]);
|
||||
});
|
||||
|
||||
it("should survive embedding and reprojection", function () {
|
||||
expect(r.matcherKeys(r.project(r.compilePattern(true, [r.embeddedMatcher(M2)]),
|
||||
r.compileProjection([r.__, r._$])))).to.eql([[2],[3],[4]]);
|
||||
expect(r.matcherKeys(r.project(r.compilePattern(true, [[r.embeddedMatcher(M2)]]),
|
||||
r.compileProjection([[r.__, r._$]])))).to.eql([[2],[3],[4]]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("matcherKeys using multiple-values in projections", function () {
|
||||
var M = r.union(r.compilePattern(r.arrayToSet(['A']), [1, 2]),
|
||||
r.compilePattern(r.arrayToSet(['C']), [1, 3]),
|
||||
r.compilePattern(r.arrayToSet(['B']), [3, 4]));
|
||||
var proj = r.compileProjection([r._$, r._$]);
|
||||
var M2 = r.project(M, proj);
|
||||
|
||||
it("should be able to extract ordinary values", function () {
|
||||
expect(r.matcherKeys(M2))
|
||||
.to.eql([[1,2],[1,3],[3,4]]);
|
||||
});
|
||||
|
||||
it("should be able to be reprojected as a sequence of more than one value", function () {
|
||||
expect(r.matcherKeys(r.project(M2, r.compileProjection(r._$, r._$))))
|
||||
.to.eql([[1,2],[1,3],[3,4]]);
|
||||
});
|
||||
|
||||
it("should be convertible into objects with $-indexed fields", function () {
|
||||
expect(r.matcherKeysToObjects(r.matcherKeys(M2), proj))
|
||||
.to.eql([{'$0': 1, '$1': 2}, {'$0': 1, '$1': 3}, {'$0': 3, '$1': 4}]);
|
||||
expect(r.projectObjects(M, proj))
|
||||
.to.eql([{'$0': 1, '$1': 2}, {'$0': 1, '$1': 3}, {'$0': 3, '$1': 4}]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("matcherKeys using multiple-values in projections, with names", function () {
|
||||
var M = r.union(r.compilePattern(r.arrayToSet(['A']), [1, 2]),
|
||||
r.compilePattern(r.arrayToSet(['C']), [1, 3]),
|
||||
r.compilePattern(r.arrayToSet(['B']), [3, 4]));
|
||||
|
||||
it("should yield named fields", function () {
|
||||
expect(r.projectObjects(M, r.compileProjection([r._$("fst"), r._$("snd")])))
|
||||
.to.eql([{'fst': 1, 'snd': 2}, {'fst': 1, 'snd': 3}, {'fst': 3, 'snd': 4}]);
|
||||
});
|
||||
|
||||
it("should yield numbered fields where names are missing", function () {
|
||||
expect(r.projectObjects(M, r.compileProjection([r._$, r._$("snd")])))
|
||||
.to.eql([{'$0': 1, 'snd': 2}, {'$0': 1, 'snd': 3}, {'$0': 3, 'snd': 4}]);
|
||||
expect(r.projectObjects(M, r.compileProjection([r._$("fst"), r._$])))
|
||||
.to.eql([{'fst': 1, '$1': 2}, {'fst': 1, '$1': 3}, {'fst': 3, '$1': 4}]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("serializeMatcher", function () {
|
||||
var M = r.union(r.compilePattern(r.arrayToSet(['A']), [r.__, 2]),
|
||||
r.compilePattern(r.arrayToSet(['C']), [1, 3]),
|
||||
r.compilePattern(r.arrayToSet(['D']), [r.__, 3]),
|
||||
r.compilePattern(r.arrayToSet(['B']), [3, 4]));
|
||||
var S = r.serializeMatcher(M, r.setToArray);
|
||||
|
||||
it("should basically work", function () {
|
||||
expect(S).to.eql(
|
||||
[ [ [ '(' ],
|
||||
[ [ 1,
|
||||
[ [ 2, [ [ [ ')' ], [ [ [ ')' ], [ '', [ 'A' ] ] ] ] ] ] ],
|
||||
[ 3, [ [ [ ')' ], [ [ [ ')' ], [ '', [ 'C', 'D' ] ] ] ] ] ] ] ] ],
|
||||
[ 3,
|
||||
[ [ 2, [ [ [ ')' ], [ [ [ ')' ], [ '', [ 'A' ] ] ] ] ] ] ],
|
||||
[ 3, [ [ [ ')' ], [ [ [ ')' ], [ '', [ 'D' ] ] ] ] ] ] ],
|
||||
[ 4, [ [ [ ')' ], [ [ [ ')' ], [ '', [ 'B' ] ] ] ] ] ] ] ] ],
|
||||
[ [ '__' ],
|
||||
[ [ 2, [ [ [ ')' ], [ [ [ ')' ], [ '', [ 'A' ] ] ] ] ] ] ],
|
||||
[ 3, [ [ [ ')' ], [ [ [ ')' ], [ '', [ 'D' ] ] ] ] ] ] ] ] ] ] ] ]);
|
||||
});
|
||||
|
||||
it("should deserialize to something equivalent", function () {
|
||||
expect(r.matcherEquals(M, r.deserializeMatcher(S, r.arrayToSet))).to.be(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("serialize Gestalts", function () {
|
||||
var G = r.simpleGestalt(false, "A", 0, 0).union(r.simpleGestalt(true, "B", 2, 2));
|
||||
var S = G.serialize();
|
||||
|
||||
it("should basically work", function () {
|
||||
expect(S).to.eql(
|
||||
[ 'gestalt',
|
||||
[ [ [ [ [ 'A', [ [ [ ')' ], [ '', true ] ] ] ] ], [] ] ],
|
||||
[],
|
||||
[ [ [], [] ],
|
||||
[ [], [] ],
|
||||
[ [], [ [ 'B', [ [ [ ')' ], [ '', true ] ] ] ] ] ] ] ] ]);
|
||||
});
|
||||
|
||||
it("should deserialize to something equivalent", function () {
|
||||
expect(G.equals(r.deserializeGestalt(S))).to.be(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("complex erasure", function () {
|
||||
var A = r.compilePattern(r.arrayToSet(['A']), r.__);
|
||||
var B = r.union(r.compilePattern(r.arrayToSet(['B']), [[[["foo"]]]]),
|
||||
r.compilePattern(r.arrayToSet(['B']), [[[["bar"]]]]));
|
||||
describe("after a union", function () {
|
||||
var R0 = r.union(A, B);
|
||||
var R1a = r.erasePath(R0, B);
|
||||
var R1b = r.erasePath(R0, A);
|
||||
|
||||
it("should yield the other parts of the union", function () {
|
||||
expect(r.matcherEquals(R1a, A)).to.be(true);
|
||||
expect(r.matcherEquals(R1b, B)).to.be(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("embedding matchers in patterns", function () {
|
||||
var M1a =
|
||||
r.compilePattern(r.arrayToSet(['A']),
|
||||
[1, r.embeddedMatcher(r.compilePattern(r.arrayToSet(['B']), [2, 3])), 4]);
|
||||
var M1b =
|
||||
r.compilePattern(r.arrayToSet(['A']), [1, [2, 3], 4]);
|
||||
var M2a =
|
||||
r.compilePattern(r.arrayToSet(['A']),
|
||||
[r.embeddedMatcher(r.compilePattern(r.arrayToSet(['B']), [1, 2])),
|
||||
r.embeddedMatcher(r.compilePattern(r.arrayToSet(['C']), [3, 4]))]);
|
||||
var M2b =
|
||||
r.compilePattern(r.arrayToSet(['A']), [[1, 2], [3, 4]]);
|
||||
|
||||
it("should yield matchers equivalent to the original patterns", function () {
|
||||
expect(r.matcherEquals(M1a, M1b)).to.be(true);
|
||||
expect(r.matcherEquals(M2a, M2b)).to.be(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("calls to matchPattern", function () {
|
||||
it("should yield appropriately-named/-numbered fields", function () {
|
||||
expect(r.matchPattern([1, 2, 3], [r.__, 2, r._$])).to.eql({'$0': 3, 'length': 1});
|
||||
expect(r.matchPattern([1, 2, 3], [r.__, 2, r._$("three")])).to.eql({'three': 3, 'length': 1});
|
||||
expect(r.matchPattern([1, 2, 3], [r._$, 2, r._$("three")]))
|
||||
.to.eql({'$0': 1, 'three': 3, 'length': 2});
|
||||
expect(r.matchPattern([1, 2, 3], [r._$("one"), 2, r._$]))
|
||||
.to.eql({'one': 1, '$1': 3, 'length': 2});
|
||||
expect(r.matchPattern([1, 2, 3], [r._$("one"), 2, r._$("three")]))
|
||||
.to.eql({'one': 1, 'three': 3, 'length': 2});
|
||||
});
|
||||
|
||||
it("should fail on value mismatch", function () {
|
||||
expect(r.matchPattern([1, 2, 3], [r.__, 999, r._$("three")])).to.be(null);
|
||||
});
|
||||
|
||||
it("should fail on array length mismatch", function () {
|
||||
expect(r.matchPattern([1, 2, 3], [r.__, 2, r._$("three"), 4])).to.be(null);
|
||||
});
|
||||
|
||||
it("matches substructure", function () {
|
||||
expect(r.matchPattern([1, [2, 999], 3], [r._$("one"), r._$(null, [2, r.__]), r._$("three")]))
|
||||
.to.eql({ one: 1, '$1': [ 2, 999 ], three: 3, length: 3 });
|
||||
expect(r.matchPattern([1, [2, 999], 3], [r._$("one"), r._$("two", [2, r.__]), r._$("three")]))
|
||||
.to.eql({ one: 1, two: [ 2, 999 ], three: 3, length: 3 });
|
||||
expect(r.matchPattern([1, [999, 2], 3], [r._$("one"), r._$(null, [2, r.__]), r._$("three")]))
|
||||
.to.be(null);
|
||||
expect(r.matchPattern([1, [999, 2], 3], [r._$("one"), r._$("two", [2, r.__]), r._$("three")]))
|
||||
.to.be(null);
|
||||
});
|
||||
|
||||
it("matches nested captures", function () {
|
||||
expect(r.matchPattern([1, [2, 999], 3], [r._$("one"), r._$(null, [2, r._$]), r._$("three")]))
|
||||
.to.eql({ one: 1, '$2': 999, '$1': [ 2, 999 ], three: 3, length: 4 });
|
||||
expect(r.matchPattern([1, [2, 999], 3], [r._$("one"), r._$("two", [2, r._$]), r._$("three")]))
|
||||
.to.eql({ one: 1, '$2': 999, two: [ 2, 999 ], three: 3, length: 4 });
|
||||
});
|
||||
});
|
||||
|
||||
describe("Projection with no captures", function () {
|
||||
it("should yield the empty sequence when there's a match", function () {
|
||||
var emptySequence = [' >{["A"]}'];
|
||||
|
||||
checkPrettyMatcher(r.project(r.compilePattern(r.arrayToSet(['A']), ["X", r.__]),
|
||||
r.compileProjection(r.__)),
|
||||
emptySequence);
|
||||
checkPrettyMatcher(r.project(r.compilePattern(r.arrayToSet(['A']), ["X", r.__]),
|
||||
r.compileProjection([r.__, r.__])),
|
||||
emptySequence);
|
||||
checkPrettyMatcher(r.project(r.compilePattern(r.arrayToSet(['A']), ["X", r.__]),
|
||||
r.compileProjection(["X", r.__])),
|
||||
emptySequence);
|
||||
});
|
||||
|
||||
it("should yield null when there's no match", function () {
|
||||
expect(r.project(r.compilePattern(r.arrayToSet(['A']), ["X", r.__]),
|
||||
r.compileProjection(["Y", r.__]))).to.be(null);
|
||||
});
|
||||
|
||||
it("should yield nonempty sequences when there are captures after all", function () {
|
||||
checkPrettyMatcher(r.project(r.compilePattern(r.arrayToSet(['A']), ["X", r.__]),
|
||||
r.compileProjection([r.__, r._$])),
|
||||
[' ★ >{["A"]}']);
|
||||
checkPrettyMatcher(r.project(r.compilePattern(r.arrayToSet(['A']), ["X", r.__]),
|
||||
r.compileProjection([r._$, r._$])),
|
||||
[' "X" ★ >{["A"]}']);
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue