Compare commits

...

110 Commits

Author SHA1 Message Date
Tony Garnock-Jones 382391b518 Update build products 2014-08-30 14:52:33 -07:00
Tony Garnock-Jones 5621685052 Make boot() return (optional) initialGestalts instead of having a separate argument to spawn(). Fixes failing test case for initial actor route signalling. 2014-08-30 14:52:33 -07:00
Tony Garnock-Jones 85c6c228a3 Move kwApply from actor.js to util.js 2014-08-30 14:52:27 -07:00
Tony Garnock-Jones 18c4b184e5 Add failing test for actor initial route signalling 2014-08-30 13:19:04 -07:00
Tony Garnock-Jones 9fdf90db68 Add test case for initial route signalling 2014-08-30 13:18:37 -07:00
Tony Garnock-Jones 26245951ad Simple test harness for actors 2014-08-30 13:07:39 -07:00
Tony Garnock-Jones 7cf9dabca4 Rename tr.js to test-route.js and make it a proper mocha/expect.js suite 2014-08-30 12:50:14 -07:00
Tony Garnock-Jones 0584b4d6d3 Update build products 2014-08-25 18:39:55 -07:00
Tony Garnock-Jones d777418d5c Rearrange Actor instances so exceptions during construction are correctly blamed on the new actor 2014-08-25 18:39:45 -07:00
Tony Garnock-Jones 160938cec8 Update build products 2014-08-25 16:12:03 -07:00
Tony Garnock-Jones 1b3b355fea Invoke trapexit after removing the pid, in case of exn in the trapexit fn 2014-08-25 16:11:25 -07:00
Tony Garnock-Jones 440c91b4d7 Update build products 2014-08-25 13:02:04 -07:00
Tony Garnock-Jones 52bdc2eb3c Redacted debugState for WebSocketConnection and DemandMatcher 2014-08-25 13:01:49 -07:00
Tony Garnock-Jones 10abaa1724 Support separate supply-projection from demand-projection 2014-08-25 13:01:35 -07:00
Tony Garnock-Jones 7bb57e2c39 Permit actors to control what portion of their state is displayed in debug output 2014-08-25 13:01:10 -07:00
Tony Garnock-Jones 66670e7f6f Throw exception on ill-formed DOM specification 2014-08-25 13:00:33 -07:00
Tony Garnock-Jones 20d0f5d58d Correct documentation of interpretSpec 2014-08-25 13:00:18 -07:00
Tony Garnock-Jones f5e59bfcd8 Update textfield example to use Actor 2014-08-25 11:44:59 -07:00
Tony Garnock-Jones a0a6a3dbfe Update build products 2014-08-25 11:44:18 -07:00
Tony Garnock-Jones d080494664 Add singleton feature to Actor observation 2014-08-25 11:44:09 -07:00
Tony Garnock-Jones c5f7e9db2a Update build products 2014-08-22 17:12:50 -07:00
Tony Garnock-Jones a62f00b8a3 Sort process tree by numeric PID, not by PID string 2014-08-22 17:12:40 -07:00
Tony Garnock-Jones e9697c8171 Update build products 2014-08-22 17:06:11 -07:00
Tony Garnock-Jones d7baec744e Smarter process-tree-printing for Chrome 2014-08-22 17:06:06 -07:00
Tony Garnock-Jones 216935d60f Webworkerized DOM example 2014-08-05 17:25:40 -07:00
Tony Garnock-Jones 66709fe58e Update build products 2014-08-04 11:32:03 -07:00
Tony Garnock-Jones 318770e301 Fix missing Codec module reference 2014-08-04 11:28:47 -07:00
Tony Garnock-Jones bd06a8a09e Update build products 2014-08-02 09:00:44 -07:00
Tony Garnock-Jones 659ee24105 Experimental Web Worker support 2014-08-02 00:32:46 -07:00
Tony Garnock-Jones 2bfacbfc7b Split veryclean out from clean to avoid foolish mistakes 2014-08-02 00:31:18 -07:00
Tony Garnock-Jones e78696c621 Split out Ground, Codec 2014-08-02 00:30:52 -07:00
Tony Garnock-Jones f46fd52239 Filter out metaLevel mismatches 2014-08-02 00:25:18 -07:00
Tony Garnock-Jones e0def26f6c Initialize Actor presence/name/added/removed state variables 2014-07-28 17:43:08 -07:00
Tony Garnock-Jones 9965ce760e Update build products 2014-07-25 16:57:16 -07:00
Tony Garnock-Jones 76044b539e Update chat example to Actor; preserve non-Actor version; remove
currently-useless OSX app resources
2014-07-25 16:57:16 -07:00
Tony Garnock-Jones 55c9fa1d49 Convert DOM and smoketest examples to actor.js 2014-07-25 16:57:16 -07:00
Tony Garnock-Jones 95a4bc3c93 Finish actor.js 2014-07-25 16:57:09 -07:00
Tony Garnock-Jones 9ffbec107f More experimentation in test/tr.js 2014-07-25 16:56:06 -07:00
Tony Garnock-Jones fba2ee91a8 actor.js (WIP) 2014-07-25 16:56:06 -07:00
Tony Garnock-Jones 0b361137a2 Reflect.formalParameters 2014-07-25 16:56:06 -07:00
Tony Garnock-Jones e0b476cc0a matchPattern, projectObjects 2014-07-25 16:56:06 -07:00
Tony Garnock-Jones cfeb46ef62 Prefer Array.isArray() to "instanceof Array" tests 2014-07-25 16:56:06 -07:00
Tony Garnock-Jones 80befd60eb Named captures 2014-07-25 16:56:06 -07:00
Tony Garnock-Jones 28405b544f Remove unused bindings 2014-07-25 16:56:05 -07:00
Tony Garnock-Jones a605a438cf Multidom example (buggy!) 2014-07-25 16:56:05 -07:00
Tony Garnock-Jones 96d577b2d7 Permit overriding of DOM/jQuery patterns (enables remote connectivity) 2014-07-24 16:21:54 -07:00
Tony Garnock-Jones f7baa65a2d Use nested-arrays (sexpr style) instead of hashes for DOM fragments 2014-07-24 16:21:07 -07:00
Tony Garnock-Jones 8fde23d187 Fix module path in tests 2014-07-23 17:25:29 -07:00
Tony Garnock-Jones 96d3d3c621 Makefile tweaks 2014-07-23 17:24:48 -07:00
Tony Garnock-Jones 7bab9be492 Some modularity; general cleanup of codebase 2014-07-23 17:21:51 -07:00
Tony Garnock-Jones 4239c1ea33 Simply erase *all* entries mapping to the driver's PID 2014-07-22 10:24:11 -07:00
Tony Garnock-Jones 044cd84e60 Add route.fullGestalt function 2014-07-22 10:23:32 -07:00
Tony Garnock-Jones 48e1b04f90 Only keep a tombstone if there was a non-falsy exit reason 2014-07-22 09:36:04 -07:00
Tony Garnock-Jones 0025e6b3af Statistics; deduplication of outgoing routes 2014-07-21 17:42:29 -07:00
Tony Garnock-Jones a0b3ac3198 Track metalevels and levels in .transform; exploit this in websocket driver 2014-07-21 17:11:47 -07:00
Tony Garnock-Jones 19374f6926 Cheap and nasty hack to keep websocket relay from crashing 2014-07-21 16:40:38 -07:00
Tony Garnock-Jones 853533f5b1 Store tombstones for debugging purposes 2014-07-21 16:20:24 -07:00
Tony Garnock-Jones 2aa44a9142 processTree, textProcessTree 2014-07-21 16:06:51 -07:00
Tony Garnock-Jones 340d32e33c Moved server to minimart/.../broker.rkt 2014-07-18 10:24:48 -07:00
Tony Garnock-Jones 4e5bf4955f Switch to minimart built-in relay 2014-06-14 20:53:21 -04:00
Tony Garnock-Jones 44de4f118c No callers of rupdate() 2014-06-07 12:32:41 -04:00
Tony Garnock-Jones 73192735e0 Flip default demandSideIsSubscription to false. 2014-06-06 21:16:20 -04:00
Tony Garnock-Jones 53e3c23a5f Include route.js in OS X app 2014-05-28 22:11:21 -04:00
Tony Garnock-Jones fe2fcdb1c8 Bring chat example up to date 2014-05-28 19:45:12 -04:00
Tony Garnock-Jones a7212a02af Adjust projection to yield multiple values instead of a vector 2014-05-28 19:44:35 -04:00
Tony Garnock-Jones 420784bb11 Allow overriding of (de)serializeSuccess functions 2014-05-28 19:42:30 -04:00
Tony Garnock-Jones 5c68f2243a Support for embedding matchers in patterns 2014-05-28 17:22:08 -04:00
Tony Garnock-Jones 461f96d5d4 Initial attempt at porting server.rkt to fastrouting 2014-05-28 16:36:52 -04:00
Tony Garnock-Jones 95701d39c5 It's console.warn, not console.warning 2014-05-27 13:15:32 -04:00
Tony Garnock-Jones 424a00c675 Update textfield example for fastrouting 2014-05-26 23:29:29 -04:00
Tony Garnock-Jones 336b4f36ac More sensible argument order on Gestalt.project 2014-05-26 23:24:23 -04:00
Tony Garnock-Jones 03773b6d6d Adapt marketplace implementation to fastrouting 2014-05-26 14:36:57 -04:00
Tony Garnock-Jones 00b92fdf1c Avoid smearLevels inefficiency; instead, union after intersection 2014-05-26 13:45:09 -04:00
Tony Garnock-Jones 6f21190383 Matched values may not be JSONable, but must be matchable with __ 2014-05-26 13:44:15 -04:00
Tony Garnock-Jones 0dce7a5e34 More canonicalization in erasePath 2014-05-26 13:43:44 -04:00
Tony Garnock-Jones 201041ad2d Experimentally remove cofinity-restriction in erasePath 2014-05-26 13:42:12 -04:00
Tony Garnock-Jones f2504b5dbf More tests 2014-05-26 13:41:35 -04:00
Tony Garnock-Jones 96aad3d579 Pretty-print matchers with keys in sorted order 2014-05-26 13:41:24 -04:00
Tony Garnock-Jones c5eab99565 Distinguish input from output emptyLevels in mapLevels 2014-05-26 05:54:28 -04:00
Tony Garnock-Jones a5c80092cd Fix error in Gestalt.match 2014-05-26 05:54:07 -04:00
Tony Garnock-Jones 7075dbaa4c Avoid (accidental, erroneous) in-place modification of a GestaltLevel 2014-05-26 05:53:46 -04:00
Tony Garnock-Jones d3143b8e65 Actually shift levels for gestalt filtering 2014-05-26 05:53:27 -04:00
Tony Garnock-Jones fc5e2108a1 use GestaltLevels.isEmpty instead of (accidental) inlining it 2014-05-26 05:53:07 -04:00
Tony Garnock-Jones ef514b5b38 Add Gestalt.metaLevelCount, .levelCount 2014-05-26 05:52:45 -04:00
Tony Garnock-Jones f55b933292 Avoid accidental side cross-over in crossedGestaltLevelOp 2014-05-26 05:52:29 -04:00
Tony Garnock-Jones 96e651d4a1 Fix error in serializeMatcher 2014-05-26 05:51:59 -04:00
Tony Garnock-Jones 7ee78181a3 Add missing case in matcherKeys 2014-05-26 05:51:42 -04:00
Tony Garnock-Jones 19d4bc10a4 GestaltLevel.pretty 2014-05-26 05:51:02 -04:00
Tony Garnock-Jones 9f26792e40 Better optionality-checking 2014-05-26 05:50:45 -04:00
Tony Garnock-Jones ad0c151278 More accurate canonicalization in erasePath 2014-05-26 05:49:11 -04:00
Tony Garnock-Jones afa79e9d11 Fix error in matcherEquals 2014-05-26 05:46:24 -04:00
Tony Garnock-Jones 25277a2605 Gestalt (de)serialization 2014-05-25 14:09:42 -04:00
Tony Garnock-Jones 80e20f7a01 serializeMatcher, deserializeMatcher 2014-05-25 13:53:20 -04:00
Tony Garnock-Jones 55335fa296 Remove long-obsolete __length check 2014-05-25 13:53:01 -04:00
Tony Garnock-Jones 305289db57 In browsers, export more carefully 2014-05-25 13:34:32 -04:00
Tony Garnock-Jones 1ccc4377f3 Split out function gestaltUnion 2014-05-25 13:34:14 -04:00
Tony Garnock-Jones 714ba11cdd Default to 0 for simpleGestalt metaLevel and level 2014-05-25 13:33:20 -04:00
Tony Garnock-Jones f8d8c3b706 Eventually we should support Infinity as a level number. 2014-05-25 13:33:05 -04:00
Tony Garnock-Jones 813fb157fb .smallerOf and .largerOf were buggy; remove them 2014-05-25 13:32:47 -04:00
Tony Garnock-Jones e413e86588 Gestalt equality; also export matcherEquals 2014-05-25 13:32:06 -04:00
Tony Garnock-Jones d36a65d843 Improved pretty-printing of matchers 2014-05-25 13:30:45 -04:00
Tony Garnock-Jones 83e82658c5 Sundry set API renamings and additions 2014-05-25 13:30:17 -04:00
Tony Garnock-Jones 037abe45a5 Rename requal to matcherEquals 2014-05-25 13:28:42 -04:00
Tony Garnock-Jones a8f4c74de9 Permit use of _$ as a capture-of-wild - terser than _$() 2014-05-25 13:27:30 -04:00
Tony Garnock-Jones 74acc78aab More gestalt support 2014-05-24 22:26:56 -04:00
Tony Garnock-Jones 202a6e129b Start work on gestalts 2014-05-23 17:23:42 -04:00
Tony Garnock-Jones 1e3a8f7db0 It turns out that order-of-definition is a thing for browser JS, even if not for node.js 2014-05-23 14:37:48 -04:00
Tony Garnock-Jones 87c5aac69f Use JSON stringification to distinguish between numbers and strings in pattern literals 2014-05-23 14:30:36 -04:00
Tony Garnock-Jones e1eadbf664 erasePath fixes, and requal function 2014-05-23 14:23:42 -04:00
Tony Garnock-Jones cdea1507c4 Initial commit of port of minimart fastrouting 2014-05-23 00:43:32 -04:00
55 changed files with 7809 additions and 1512 deletions

3
.gitignore vendored
View File

@ -1,6 +1,5 @@
private-key.pem private-key.pem
server-cert.pem server-cert.pem
MarketplaceChat.app.zip
MarketplaceChat.app/
scratch/ scratch/
_site/ _site/
node_modules/

View File

@ -1,20 +1,5 @@
APP_NAME=MarketplaceChat.app all:
LIB_SOURCES=\ npm install .
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
keys: private-key.pem server-cert.pem keys: private-key.pem server-cert.pem
@ -31,22 +16,8 @@ server-cert.pem: private-key.pem
clean-keys: clean-keys:
rm -f private-key.pem server-cert.pem 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: clean:
rm -rf $(APP_NAME) $(APP_NAME).zip rm -f dist/*.js
veryclean: clean
rm -rf node_modules/

View File

@ -14,7 +14,7 @@ To install the Racket server:
To run 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 The Racket server listens for tunnelled Network Calculus events via
websocket on ports 8000 (HTTP) and 8443 (HTTPS, if you have a websocket on ports 8000 (HTTP) and 8443 (HTTPS, if you have a

1
dist/README.md vendored Normal file
View File

@ -0,0 +1 @@
Directory for build products, checked in to the repo for ease-of-use.

3215
dist/minimart.js vendored Normal file

File diff suppressed because one or more lines are too long

3
dist/minimart.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

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

View File

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

162
examples/chat-raw/index.js Normal file
View File

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

View File

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

View File

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

View File

@ -1,3 +0,0 @@
#!/bin/bash
cd "$(dirname "$0")"/../Resources
open examples/chat/index.html

View File

@ -3,32 +3,15 @@
<head> <head>
<title>JS Marketplace: Chat Example</title> <title>JS Marketplace: Chat Example</title>
<meta charset="utf-8"> <meta charset="utf-8">
<link href="../../third-party/bootstrap/css/bootstrap.min.css" rel="stylesheet"> <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="../../third-party/bootstrap/css/bootstrap-responsive.min.css" rel="stylesheet">
<link href="style.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="../../third-party/jquery-2.0.3.min.js"></script>
<script src="../../dist/minimart.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="index.js"></script> <script src="index.js"></script>
</head> </head>
<body> <body>
<div class="container-fluid"> <div class="container-fluid">
<!-- <div class="row-fluid"> -->
<!-- <div class="span12"> -->
<!-- <h1>JS Marketplace</h1> -->
<!-- </div> -->
<!-- </div> -->
<div class="row-fluid"> <div class="row-fluid">
<div class="span12"> <div class="span12">
<form class="form-horizontal" name="nym_form"> <form class="form-horizontal" name="nym_form">

View File

@ -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) { function chatEvent(nym, status, utterance, stamp) {
return ["chatEvent", nym, status, utterance, stamp || +(new Date())]; 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) { function outputItem(item) {
var stamp = $("<span/>").text((new Date()).toGMTString()).addClass("timestamp"); var stamp = $("<span/>").text((new Date()).toGMTString()).addClass("timestamp");
@ -15,13 +19,10 @@ function outputItem(item) {
return item; return item;
} }
function updateNymList(rs) { function updateNymList(allStatuses) {
var statuses = {}; var statuses = {};
for (var i = 0; i < rs.length; i++) { for (var i = 0; i < allStatuses.length; i++) {
var p = rs[i].pattern; statuses[allStatuses[i].nym] = allStatuses[i].status;
if (p[0] === "broker" && p[1] === 0 && p[2][0] === "chatEvent") {
statuses[chatEventNym(p[2])] = chatEventStatus(p[2]);
}
} }
var nyms = []; var nyms = [];
for (var nym in statuses) { nyms.push(nym); } for (var nym in statuses) { nyms.push(nym); }
@ -56,100 +57,81 @@ $(document).ready(function () {
$("#nym_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)); } 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'); console.log('starting ground boot');
// World.spawn(new Spy()); // World.spawn(new Spy());
spawnJQueryDriver(); Minimart.JQuery.spawnJQueryDriver();
spawnDOMDriver(); Minimart.DOM.spawnDOMDriver();
spawnRoutingTableWidget("#spy-holder", "spy"); Minimart.RoutingTableWidget.spawnRoutingTableWidget("#spy-holder", "spy");
World.spawn(new WakeDetector()); World.spawn(new Minimart.WakeDetector());
var wsconn = new WebSocketConnection("broker", $("#wsurl").val(), true); var wsconn = new Minimart.WebSocket.WebSocketConnection("broker", $("#wsurl").val(), true);
World.spawn(wsconn); World.spawn(wsconn);
World.spawn({ World.spawn(new Actor(function () {
// Monitor connection, notifying connectivity changes // Monitor connection, notifying connectivity changes
state: "crashed", // start with this to avoid spurious initial message print this.state = "crashed"; // start with this to avoid spurious initial message print
boot: function () {
World.updateRoutes([sub(["broker_state", __], 0, 1)]); Actor.observeAdvertisers(
}, function () { return ["broker_state", _$("newState")]; },
handleEvent: function (e) { { name: "states" },
if (e.type === "routes") { function () {
var newState = (e.routes.length > 0) ? e.routes[0].pattern[1] : "crashed"; var newState = this.states.length > 0 ? this.states[0].newState : "crashed";
if (this.state != newState) { if (this.state != newState) {
outputState(newState); outputState(newState);
this.state = newState; this.state = newState;
} }
} });
} }));
}); World.spawn(new Actor(function () {
World.spawn({
// Actual chat functionality // Actual chat functionality
peers: new PresenceDetector(), this.nym = function () { return $("#nym").val(); };
peerMap: {}, this.currentStatus = function () { return $("#status").val(); };
boot: function () {
World.updateRoutes(this.subscriptions()); Actor.subscribe(
}, function () { return "wake"; },
nym: function () { return $("#nym").val(); }, function () { wsconn.forceclose(); });
currentStatus: function () { return $("#status").val(); },
subscriptions: function () { Actor.advertise(
return [sub("wake"), function () { return ["broker", 0,
sub(["jQuery", "#send_chat", "click", __]), chatEvent(this.nym(), this.currentStatus(), __, __)]; });
sub(["jQuery", "#nym", "change", __]), Actor.observeAdvertisers(
sub(["jQuery", "#status", "change", __]), function () { return ["broker", 0,
sub(["jQuery", "#wsurl", "change", __]), chatEvent(_$("nym"), _$("status"), __, __)]; },
pub(["broker", 0, chatEvent(this.nym(), this.currentStatus(), __, __)]), { name: "allStatuses" },
sub(["broker", 0, chatEvent(__, __, __, __)], 0, 1)]; function () { updateNymList(this.allStatuses); });
},
handleEvent: function (e) { Actor.subscribe(
var self = this; function () { return ["jQuery", "#send_chat", "click", __]; },
switch (e.type) { function () {
case "routes": var inp = $("#chat_input");
updateNymList(e.routes); var utterance = inp.val();
break; inp.val("");
case "message": if (utterance) {
if (e.message === "wake") { World.send(["broker", 0, chatEvent(this.nym(),
wsconn.forceclose(); this.currentStatus(),
return; utterance)]);
} }
switch (e.message[0]) { });
case "jQuery":
switch (e.message[1]) { Actor.subscribe(
case "#send_chat": function () { return ["jQuery", "#nym", "change", __]; },
var inp = $("#chat_input"); function () { this.updateRoutes(); });
var utterance = inp.val();
inp.val(""); Actor.subscribe(
if (utterance) { function () { return ["jQuery", "#status", "change", __]; },
World.send(["broker", 0, chatEvent(this.nym(), function () { this.updateRoutes(); });
this.currentStatus(),
utterance)]); Actor.subscribe(
} function () { return ["jQuery", "#wsurl", "change", __]; },
break; function () {
case "#nym": wsconn.forceclose();
case "#status": wsconn.wsurl = $("#wsurl").val();
World.updateRoutes(this.subscriptions()); });
break;
case "#wsurl": Actor.subscribe(
wsconn.forceclose(); function () { return ["broker", 0, chatEvent(_$("who"), __, _$("what"), __)]; },
wsconn.wsurl = $("#wsurl").val(); function (who, what) { outputUtterance(who, what); });
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(); G.startStepping();
}); });

View File

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

49
examples/dom-raw/index.js Normal file
View File

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

View File

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

View File

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

View File

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

View File

@ -3,23 +3,10 @@
<head> <head>
<title>JS Marketplace: DOM Example</title> <title>JS Marketplace: DOM Example</title>
<meta charset="utf-8"> <meta charset="utf-8">
<link href="../../third-party/bootstrap/css/bootstrap.min.css" rel="stylesheet"> <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="../../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="../../third-party/jquery-2.0.3.min.js"></script>
<script src="../../dist/minimart.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="index.js"></script> <script src="index.js"></script>
</head> </head>
<body> <body>

View File

@ -1,40 +1,51 @@
var G; var G;
$(document).ready(function () { $(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'); console.log('starting ground boot');
// World.spawn(new Spy("GROUND", true)); // World.spawn(new Spy("GROUND", true));
spawnDOMDriver(); Minimart.DOM.spawnDOMDriver();
spawnRoutingTableWidget("#spy-holder", "spy"); Minimart.RoutingTableWidget.spawnRoutingTableWidget("#spy-holder", "spy");
World.spawn({ World.spawn(new Actor(function () {
handleEvent: function (e) { Actor.subscribe(
if (e.type === "message" && e.message[0] === "jQuery") { function () { return ["jQuery", "button.clicker", "click", __]; },
function () {
World.send("bump_count"); 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({ Actor.advertise(
counter: 0, function () { return "bump_count"; });
boot: function () { Actor.advertise(
this.updateState(); function () {
}, return ["DOM", "#clicker-holder", "clicker",
updateState: function () { ["button", ["span", [["style", "font-style: italic"]], "Click me!"]]];
World.updateRoutes([sub("bump_count"), });
pub(["DOM", "#counter-holder", "counter", }));
["div",
["p", "The current count is: ", this.counter]]])]); World.spawn(new Actor(function () {
}, this.counter = 0;
handleEvent: function (e) {
if (e.type === "message" && e.message === "bump_count") { Actor.subscribe(
function () { return "bump_count"; },
function () {
this.counter++; this.counter++;
this.updateState(); this.updateRoutes();
} });
}
}); Actor.advertise(
function () {
return ["DOM", "#counter-holder", "counter",
["div",
["p", "The current count is: ", this.counter]]];
});
}));
}); });
G.startStepping(); G.startStepping();
}); });

View File

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -9,16 +9,7 @@
<link href="style.css" rel="stylesheet"> <link href="style.css" rel="stylesheet">
<script src="../../third-party/jquery-2.0.3.min.js"></script> <script src="../../third-party/jquery-2.0.3.min.js"></script>
<script src="../../marketplace.js"></script> <script src="../../dist/minimart.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="index.js"></script> <script src="index.js"></script>
</head> </head>
<body> <body>

View File

@ -1,6 +1,13 @@
/////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////
// GUI // 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) { function piece(text, pos, lo, hi, cls) {
return "<span class='"+cls+"'>"+ return "<span class='"+cls+"'>"+
((pos >= lo && pos < hi) ((pos >= lo && pos < hi)
@ -10,51 +17,47 @@ function piece(text, pos, lo, hi, cls) {
} }
function spawnGui() { function spawnGui() {
World.spawn({ World.spawn(new Actor(function () {
boot: function () { Actor.subscribe(
World.updateRoutes([sub(["jQuery", "#inputRow", "+keypress", __]), function () { return ["jQuery", "#inputRow", "+keypress", _$("event")]; },
sub(["fieldContents", __, __], 0, 1), function (event) {
sub(["highlight", __], 0, 1)]); var keycode = event.keyCode;
}, var character = String.fromCharCode(event.charCode);
handleEvent: function (e) { if (keycode === 37 /* left */) {
switch (e.type) { World.send(["fieldCommand", "cursorLeft"]);
case "routes": } else if (keycode === 39 /* right */) {
var text = "", pos = 0, highlight = false; World.send(["fieldCommand", "cursorRight"]);
// BUG: escape text! } else if (keycode === 9 /* tab */) {
for (var i = 0; i < e.routes.length; i++) { // ignore
var r = e.routes[i]; } else if (keycode === 8 /* backspace */) {
if (r.pattern[0] === "fieldContents") { World.send(["fieldCommand", "backspace"]);
text = r.pattern[1]; } else if (character) {
pos = r.pattern[2]; World.send(["fieldCommand", ["insert", character]]);
} else if (r.pattern[0] === "highlight") { }
highlight = r.pattern[1]; });
}
} Actor.observeAdvertisers(
$("#fieldContents")[0].innerHTML = highlight function () { return ["fieldContents", _$("text"), _$("pos")]; },
? piece(text, pos, 0, highlight[0], "normal") + { singleton: "field" },
piece(text, pos, highlight[0], highlight[1], "highlight") + updateDisplay);
piece(text, pos, highlight[1], text.length + 1, "normal")
: piece(text, pos, 0, text.length + 1, "normal"); Actor.observeAdvertisers(
break; function () { return ["highlight", _$("state")]; },
case "message": { singleton: "highlight" },
if (e.message[0] === "jQuery") { // it's a keypress event updateDisplay);
var keycode = e.message[3].keyCode;
var character = String.fromCharCode(e.message[3].charCode); function updateDisplay() {
if (keycode === 37 /* left */) { // BUG: escape text!
World.send(["fieldCommand", "cursorLeft"]); var text = this.field ? this.field.text : "";
} else if (keycode === 39 /* right */) { var pos = this.field ? this.field.pos : 0;
World.send(["fieldCommand", "cursorRight"]); var highlight = this.highlight ? this.highlight.state : false;
} else if (keycode === 9 /* tab */) { $("#fieldContents")[0].innerHTML = highlight
// ignore ? piece(text, pos, 0, highlight[0], "normal") +
} else if (keycode === 8 /* backspace */) { piece(text, pos, highlight[0], highlight[1], "highlight") +
World.send(["fieldCommand", "backspace"]); piece(text, pos, highlight[1], text.length + 1, "normal")
} else if (character) { : piece(text, pos, 0, text.length + 1, "normal");
World.send(["fieldCommand", ["insert", character]]); }
} }));
}
}
}
});
} }
/////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////
@ -62,93 +65,80 @@ function spawnGui() {
function spawnModel() { function spawnModel() {
var initialContents = "initial"; var initialContents = "initial";
World.spawn({ World.spawn(new Actor(function () {
fieldContents: initialContents, this.fieldContents = initialContents;
cursorPos: initialContents.length, /* positions address gaps between characters */ this.cursorPos = initialContents.length; /* positions address gaps between characters */
boot: function () {
World.updateRoutes(this.subscriptions()); Actor.advertise(
}, function () { return ["fieldContents", this.fieldContents, this.cursorPos]; });
subscriptions: function () {
return [sub(["fieldCommand", __]), Actor.subscribe(
pub(["fieldContents", this.fieldContents, this.cursorPos])]; function () { return ["fieldCommand", _$("command")]; },
}, function (command) {
handleEvent: function (e) { if (command === "cursorLeft") {
switch (e.type) { this.cursorPos--;
case "message": if (this.cursorPos < 0)
if (e.message[1] === "cursorLeft") { this.cursorPos = 0;
this.cursorPos--; } else if (command === "cursorRight") {
if (this.cursorPos < 0) this.cursorPos++;
this.cursorPos = 0; if (this.cursorPos > this.fieldContents.length)
} else if (e.message[1] === "cursorRight") { this.cursorPos = this.fieldContents.length;
this.cursorPos++; } else if (command === "backspace" && this.cursorPos > 0) {
if (this.cursorPos > this.fieldContents.length) this.fieldContents =
this.cursorPos = this.fieldContents.length; this.fieldContents.substring(0, this.cursorPos - 1) +
} else if (e.message[1] === "backspace" && this.cursorPos > 0) { this.fieldContents.substring(this.cursorPos);
this.fieldContents = this.cursorPos--;
this.fieldContents.substring(0, this.cursorPos - 1) + } else if (command.constructor === Array && command[0] === "insert") {
this.fieldContents.substring(this.cursorPos); var newText = command[1];
this.cursorPos--; this.fieldContents =
} else if (e.message[1].constructor === Array && e.message[1][0] === "insert") { this.fieldContents.substring(0, this.cursorPos) +
var newText = e.message[1][1]; newText +
this.fieldContents = this.fieldContents.substring(this.cursorPos);
this.fieldContents.substring(0, this.cursorPos) + this.cursorPos += newText.length;
newText + }
this.fieldContents.substring(this.cursorPos); this.updateRoutes();
this.cursorPos += newText.length; });
} }));
World.updateRoutes(this.subscriptions());
break;
}
}
});
} }
/////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////
// Search engine // Search engine
function spawnSearch() { function spawnSearch() {
World.spawn({ World.spawn(new Actor(function () {
fieldContents: "", var self = this;
highlight: false, self.fieldContents = "";
boot: function () { self.highlight = false;
World.updateRoutes(this.subscriptions());
}, Actor.advertise(
subscriptions: function () { function () { return ["highlight", self.highlight]; });
return [sub(["jQuery", "#searchBox", "input", __]),
sub(["fieldContents", __, __], 0, 1), Actor.subscribe(
pub(["highlight", this.highlight])]; function () { return ["jQuery", "#searchBox", "input", _$("event")]; },
}, search);
search: function () {
var searchtext = $("#searchBox")[0].value; Actor.observeAdvertisers(
var oldHighlight = this.highlight; function () { return ["fieldContents", _$("text"), _$("pos")]; },
if (searchtext) { { singleton: "field" },
var pos = this.fieldContents.indexOf(searchtext); function () {
this.highlight = (pos !== -1) && [pos, pos + searchtext.length]; self.fieldContents = self.field ? self.field.text : "";
} else { search();
this.highlight = false; });
}
if (JSON.stringify(oldHighlight) !== JSON.stringify(this.highlight)) { function search() {
World.updateRoutes(this.subscriptions()); var searchtext = $("#searchBox")[0].value;
} var oldHighlight = self.highlight;
}, if (searchtext) {
handleEvent: function (e) { var pos = self.fieldContents.indexOf(searchtext);
switch (e.type) { self.highlight = (pos !== -1) && [pos, pos + searchtext.length];
case "routes": } else {
for (var i = 0; i < e.routes.length; i++) { self.highlight = false;
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();
}
}
} }
}); if (JSON.stringify(oldHighlight) !== JSON.stringify(self.highlight)) {
self.updateRoutes();
}
}
}));
} }
/////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////
@ -156,10 +146,10 @@ function spawnSearch() {
var G; var G;
$(document).ready(function () { $(document).ready(function () {
G = new Ground(function () { G = new Minimart.Ground(function () {
spawnJQueryDriver(); Minimart.JQuery.spawnJQueryDriver();
spawnDOMDriver(); Minimart.DOM.spawnDOMDriver();
spawnRoutingTableWidget("#spy-holder", "spy"); Minimart.RoutingTableWidget.spawnRoutingTableWidget("#spy-holder", "spy");
spawnGui(); spawnGui();
spawnModel(); spawnModel();

46
jquery-driver.js vendored
View File

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

View File

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

24
package.json Normal file
View File

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

View File

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

View File

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

View File

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

268
src/actor.js Normal file
View File

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

33
src/codec.js Normal file
View File

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

120
src/dom-driver.js Normal file
View File

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

61
src/ground.js Normal file
View File

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

88
src/jquery-driver.js vendored Normal file
View File

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

16
src/main.js Normal file
View File

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

530
src/minimart.js Normal file
View File

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

26
src/reflect.js Normal file
View File

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

1585
src/route.js Normal file

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -1,18 +1,24 @@
// Generic Spy // 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.label = label || "SPY";
this.observationLevel = observationLevel || 10; // arbitrary. Should be Infinity. TODO
this.useJson = useJson; this.useJson = useJson;
} }
Spy.prototype.boot = function () { 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) { Spy.prototype.handleEvent = function (e) {
switch (e.type) { switch (e.type) {
case "routes": case "routes":
console.log(this.label, "routes", this.useJson ? JSON.stringify(e.routes) : e.routes); console.log(this.label, "routes", e.gestalt.pretty());
break; break;
case "message": case "message":
var messageRepr; var messageRepr;
@ -28,3 +34,5 @@ Spy.prototype.handleEvent = function (e) {
break; break;
} }
}; };
module.exports.Spy = Spy;

23
src/util.js Normal file
View File

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

View File

@ -2,6 +2,11 @@
// suspension/sleeping!) has caused periodic activities to be // suspension/sleeping!) has caused periodic activities to be
// interrupted, and warns others about it // interrupted, and warns others about it
// Inspired by http://blog.alexmaccaw.com/javascript-wake-event // 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) { function WakeDetector(period) {
this.message = "wake"; this.message = "wake";
@ -12,8 +17,8 @@ function WakeDetector(period) {
WakeDetector.prototype.boot = function () { WakeDetector.prototype.boot = function () {
var self = this; var self = this;
World.updateRoutes([pub(this.message)]);
this.timerId = setInterval(World.wrap(function () { self.trigger(); }), this.period); this.timerId = setInterval(World.wrap(function () { self.trigger(); }), this.period);
return [pub(this.message)];
}; };
WakeDetector.prototype.handleEvent = function (e) {}; WakeDetector.prototype.handleEvent = function (e) {};
@ -25,3 +30,5 @@ WakeDetector.prototype.trigger = function () {
} }
this.mostRecentTrigger = now; this.mostRecentTrigger = now;
}; };
module.exports.WakeDetector = WakeDetector;

View File

@ -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 // WebSocket client driver
@ -8,14 +17,19 @@ var DEFAULT_PING_INTERVAL = DEFAULT_IDLE_TIMEOUT - 10000;
function WebSocketConnection(label, wsurl, shouldReconnect) { function WebSocketConnection(label, wsurl, shouldReconnect) {
this.label = label; this.label = label;
this.sendsAttempted = 0;
this.sendsTransmitted = 0;
this.receiveCount = 0;
this.sock = null;
this.wsurl = wsurl; this.wsurl = wsurl;
this.shouldReconnect = shouldReconnect ? true : false; this.shouldReconnect = shouldReconnect ? true : false;
this.reconnectDelay = DEFAULT_RECONNECT_DELAY; this.reconnectDelay = DEFAULT_RECONNECT_DELAY;
this.localRoutes = []; this.localGestalt = Route.emptyGestalt;
this.peerRoutes = []; this.peerGestalt = Route.emptyGestalt;
this.prevLocalRoutesMessage = null;
this.prevPeerRoutesMessage = null; this.prevPeerRoutesMessage = null;
this.sock = null; this.deduplicator = new Minimart.Deduplicator();
this.deduplicator = new Deduplicator(); this.connectionCount = 0;
this.activityTimestamp = 0; this.activityTimestamp = 0;
this.idleTimeout = DEFAULT_IDLE_TIMEOUT; this.idleTimeout = DEFAULT_IDLE_TIMEOUT;
@ -24,6 +38,20 @@ function WebSocketConnection(label, wsurl, shouldReconnect) {
this.pingTimer = null; 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 () { WebSocketConnection.prototype.clearHeartbeatTimers = function () {
if (this.idleTimer) { clearTimeout(this.idleTimer); this.idleTimer = null; } if (this.idleTimer) { clearTimeout(this.idleTimer); this.idleTimer = null; }
if (this.pingTimer) { clearTimeout(this.pingTimer); this.pingTimer = 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]); return pub([this.label + "_state", status]);
}; };
WebSocketConnection.prototype.relayRoutes = function () { WebSocketConnection.prototype.relayGestalt = function () {
// fresh copy each time, suitable for in-place extension/mutation return this.statusRoute(this.isConnected() ? "connected" : "disconnected")
return [this.statusRoute(this.isConnected() ? "connected" : "disconnected"), .union(pub([this.label, __, __], 0, 10))
pub([this.label, __, __], 0, 1000), .union(sub([this.label, __, __], 0, 10));
sub([this.label, __, __], 0, 1000)]; // TODO: level 10 is ad-hoc; support infinity at some point in future
}; };
WebSocketConnection.prototype.aggregateRoutes = function () { WebSocketConnection.prototype.aggregateGestalt = function () {
var rs = this.relayRoutes(); var self = this;
for (var i = 0; i < this.peerRoutes.length; i++) { return this.peerGestalt.transform(function (m, metaLevel) {
var r = this.peerRoutes[i]; return Route.compilePattern(true,
rs.push(new Route(r.isSubscription, [self.label, metaLevel, Route.embeddedMatcher(m)]);
// TODO: This is a horrible syntactic hack }).union(this.relayGestalt());
// (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.boot = function () { WebSocketConnection.prototype.boot = function () {
@ -84,48 +100,61 @@ WebSocketConnection.prototype.isConnected = function () {
WebSocketConnection.prototype.safeSend = function (m) { WebSocketConnection.prototype.safeSend = function (m) {
try { try {
if (this.isConnected()) { this.sock.send(m); } this.sendsAttempted++;
if (this.isConnected()) {
this.sock.send(m);
this.sendsTransmitted++;
}
} catch (e) { } catch (e) {
console.warn("Trapped exn while sending", e); console.warn("Trapped exn while sending", e);
} }
}; };
WebSocketConnection.prototype.sendLocalRoutes = function () { 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) { WebSocketConnection.prototype.handleEvent = function (e) {
// console.log("WebSocketConnection.handleEvent", e); // console.log("WebSocketConnection.handleEvent", e);
switch (e.type) { switch (e.type) {
case "routes": case "routes":
this.localRoutes = []; // TODO: GROSS - erasing by pid!
for (var i = 0; i < e.routes.length; i++) { var nLevels = e.gestalt.levelCount(0);
var r = e.routes[i]; var relayGestalt = Route.fullGestalt(1, nLevels).label(World.activePid());
if (r.pattern.length && r.pattern.length === 3 var g = e.gestalt.erasePath(relayGestalt);
&& r.pattern[0] === this.label this.localGestalt = Route.emptyGestalt;
// TODO: This is a horrible syntactic hack (in for (var level = 0; level < nLevels; level++) {
// conjunction with the use of __ in in this.collectMatchers(false, level, g);
// aggregateRoutes) for distinguishing routes this.collectMatchers(true, level, g);
// 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));
}
} }
this.sendLocalRoutes(); this.sendLocalRoutes();
break; break;
case "message": case "message":
var m = e.message; var m = e.message;
if (m.length && m.length === 3 if (m.length && m.length === 3 && m[0] === this.label)
&& m[0] === this.label
&& typeof(m[1]) === "number")
{ {
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)) { if (this.deduplicator.accept(encoded)) {
this.safeSend(encoded); this.safeSend(encoded);
} }
@ -149,15 +178,20 @@ WebSocketConnection.prototype.forceclose = function (keepReconnectDelay) {
WebSocketConnection.prototype.reconnect = function () { WebSocketConnection.prototype.reconnect = function () {
var self = this; var self = this;
this.forceclose(true); this.forceclose(true);
this.connectionCount++;
this.sock = new WebSocket(this.wsurl); this.sock = new WebSocket(this.wsurl);
this.sock.onopen = World.wrap(function (e) { return self.onopen(e); }); 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); }); this.sock.onclose = World.wrap(function (e) { return self.onclose(e); });
}; };
WebSocketConnection.prototype.onopen = function (e) { WebSocketConnection.prototype.onopen = function (e) {
console.log("connected to " + this.sock.url); console.log("connected to " + this.sock.url);
this.reconnectDelay = DEFAULT_RECONNECT_DELAY; this.reconnectDelay = DEFAULT_RECONNECT_DELAY;
this.prevLocalRoutesMessage = null;
this.sendLocalRoutes(); this.sendLocalRoutes();
}; };
@ -173,13 +207,13 @@ WebSocketConnection.prototype.onmessage = function (wse) {
return; // recordActivity already took care of our timers return; // recordActivity already took care of our timers
} }
var e = decodeAction(j); var e = Codec.decodeAction(j);
switch (e.type) { switch (e.type) {
case "routes": case "routes":
if (this.prevPeerRoutesMessage !== wse.data) { if (this.prevPeerRoutesMessage !== wse.data) {
this.prevPeerRoutesMessage = wse.data; this.prevPeerRoutesMessage = wse.data;
this.peerRoutes = e.routes; this.peerGestalt = e.gestalt;
World.updateRoutes(this.aggregateRoutes()); World.updateRoutes([this.aggregateGestalt()]);
} }
break; break;
case "message": case "message":
@ -195,7 +229,7 @@ WebSocketConnection.prototype.onclose = function (e) {
console.log("onclose", e); console.log("onclose", e);
// Update routes to give clients some indication of the discontinuity // Update routes to give clients some indication of the discontinuity
World.updateRoutes(this.aggregateRoutes()); World.updateRoutes([this.aggregateGestalt()]);
if (this.shouldReconnect) { if (this.shouldReconnect) {
console.log("reconnecting to " + this.wsurl + " in " + this.reconnectDelay + "ms"); 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) { module.exports.WebSocketConnection = WebSocketConnection;
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) };
}
}

56
src/worker.js Normal file
View File

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

99
test/test-actor.js Normal file
View File

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

544
test/test-route.js Normal file
View File

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