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
server-cert.pem
MarketplaceChat.app.zip
MarketplaceChat.app/
scratch/
_site/
node_modules/

View File

@ -1,20 +1,5 @@
APP_NAME=MarketplaceChat.app
LIB_SOURCES=\
marketplace.js \
spy.js \
dom-driver.js \
jquery-driver.js \
routing-table-widget.js \
routing-table-widget.css \
wake-detector.js \
websocket-driver.js
APP_SOURCES=\
examples/chat/index.html \
examples/chat/index.js \
examples/chat/style.css
RESOURCES=$(wildcard examples/chat/app-resources/*)
all: $(APP_NAME).zip
all:
npm install .
keys: private-key.pem server-cert.pem
@ -31,22 +16,8 @@ server-cert.pem: private-key.pem
clean-keys:
rm -f private-key.pem server-cert.pem
$(APP_NAME).zip: $(APP_NAME)
zip -r $@ $<
$(APP_NAME): $(APP_SOURCES) $(LIB_SOURCES)
echo RESOURCES $(RESOURCES)
rm -rf $@
mkdir -p $@/Contents/MacOS
mkdir -p $@/Contents/Resources
cp examples/chat/app-resources/Info.plist $@/Contents
cp examples/chat/app-resources/boot.sh $@/Contents/MacOS
cp examples/chat/app-resources/app.icns $@/Contents/Resources
cp -r third-party $@/Contents/Resources
cp $(LIB_SOURCES) $@/Contents/Resources
mkdir -p $@/Contents/Resources/examples/chat
cp $(APP_SOURCES) $@/Contents/Resources/examples/chat
chmod a+x $@/Contents/MacOS/boot.sh
clean:
rm -rf $(APP_NAME) $(APP_NAME).zip
rm -f dist/*.js
veryclean: clean
rm -rf node_modules/

View File

@ -14,7 +14,7 @@ To install the Racket server:
To run the Racket server:
- `racket server.rkt` from the base directory of this repository.
racket -l minimart/examples/broker
The Racket server listens for tunnelled Network Calculus events via
websocket on ports 8000 (HTTP) and 8443 (HTTPS, if you have a

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>
<title>JS Marketplace: Chat Example</title>
<meta charset="utf-8">
<link href="../../third-party/bootstrap/css/bootstrap.min.css" rel="stylesheet">
<link href="../../third-party/bootstrap/css/bootstrap-responsive.min.css" rel="stylesheet">
<link href="style.css" rel="stylesheet">
<!-- <script src="../../third-party/bootstrap/js/bootstrap.min.js"></script> -->
<script src="../../third-party/jquery-2.0.3.min.js"></script>
<script src="../../marketplace.js"></script>
<script src="../../spy.js"></script>
<script src="../../dom-driver.js"></script>
<script src="../../routing-table-widget.js"></script>
<link href="../../routing-table-widget.css" rel="stylesheet">
<script src="../../jquery-driver.js"></script>
<script src="../../wake-detector.js"></script>
<script src="../../websocket-driver.js"></script>
<script src="../../dist/minimart.js"></script>
<script src="index.js"></script>
</head>
<body>
<div class="container-fluid">
<!-- <div class="row-fluid"> -->
<!-- <div class="span12"> -->
<!-- <h1>JS Marketplace</h1> -->
<!-- </div> -->
<!-- </div> -->
<div class="row-fluid">
<div class="span12">
<form class="form-horizontal" name="nym_form">

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

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>
<title>JS Marketplace: DOM Example</title>
<meta charset="utf-8">
<link href="../../third-party/bootstrap/css/bootstrap.min.css" rel="stylesheet">
<link href="../../third-party/bootstrap/css/bootstrap-responsive.min.css" rel="stylesheet">
<link href="style.css" rel="stylesheet">
<!-- <script src="../../third-party/bootstrap/js/bootstrap.min.js"></script> -->
<script src="../../third-party/jquery-2.0.3.min.js"></script>
<script src="../../marketplace.js"></script>
<script src="../../spy.js"></script>
<script src="../../dom-driver.js"></script>
<script src="../../routing-table-widget.js"></script>
<link href="../../routing-table-widget.css" rel="stylesheet">
<script src="../../jquery-driver.js"></script>
<script src="../../wake-detector.js"></script>
<script src="../../websocket-driver.js"></script>
<script src="../../dist/minimart.js"></script>
<script src="index.js"></script>
</head>
<body>

View File

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

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">
<script src="../../third-party/jquery-2.0.3.min.js"></script>
<script src="../../marketplace.js"></script>
<script src="../../spy.js"></script>
<script src="../../dom-driver.js"></script>
<script src="../../routing-table-widget.js"></script>
<link href="../../routing-table-widget.css" rel="stylesheet">
<script src="../../jquery-driver.js"></script>
<script src="../../wake-detector.js"></script>
<script src="../../websocket-driver.js"></script>
<script src="../../dist/minimart.js"></script>
<script src="index.js"></script>
</head>
<body>

View File

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

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
var Minimart = require("./minimart.js");
var World = Minimart.World;
var sub = Minimart.sub;
var pub = Minimart.pub;
var __ = Minimart.__;
function Spy(label, useJson) {
function Spy(label, useJson, observationLevel) {
this.label = label || "SPY";
this.observationLevel = observationLevel || 10; // arbitrary. Should be Infinity. TODO
this.useJson = useJson;
}
Spy.prototype.boot = function () {
World.updateRoutes([sub(__, 0, Infinity), pub(__, 0, Infinity)]);
return [sub(__, 0, this.observationLevel), pub(__, 0, this.observationLevel)];
};
Spy.prototype.handleEvent = function (e) {
switch (e.type) {
case "routes":
console.log(this.label, "routes", this.useJson ? JSON.stringify(e.routes) : e.routes);
console.log(this.label, "routes", e.gestalt.pretty());
break;
case "message":
var messageRepr;
@ -28,3 +34,5 @@ Spy.prototype.handleEvent = function (e) {
break;
}
};
module.exports.Spy = Spy;

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
// interrupted, and warns others about it
// Inspired by http://blog.alexmaccaw.com/javascript-wake-event
var Minimart = require("./minimart.js");
var World = Minimart.World;
var sub = Minimart.sub;
var pub = Minimart.pub;
var __ = Minimart.__;
function WakeDetector(period) {
this.message = "wake";
@ -12,8 +17,8 @@ function WakeDetector(period) {
WakeDetector.prototype.boot = function () {
var self = this;
World.updateRoutes([pub(this.message)]);
this.timerId = setInterval(World.wrap(function () { self.trigger(); }), this.period);
return [pub(this.message)];
};
WakeDetector.prototype.handleEvent = function (e) {};
@ -25,3 +30,5 @@ WakeDetector.prototype.trigger = function () {
}
this.mostRecentTrigger = now;
};
module.exports.WakeDetector = WakeDetector;

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
@ -8,14 +17,19 @@ var DEFAULT_PING_INTERVAL = DEFAULT_IDLE_TIMEOUT - 10000;
function WebSocketConnection(label, wsurl, shouldReconnect) {
this.label = label;
this.sendsAttempted = 0;
this.sendsTransmitted = 0;
this.receiveCount = 0;
this.sock = null;
this.wsurl = wsurl;
this.shouldReconnect = shouldReconnect ? true : false;
this.reconnectDelay = DEFAULT_RECONNECT_DELAY;
this.localRoutes = [];
this.peerRoutes = [];
this.localGestalt = Route.emptyGestalt;
this.peerGestalt = Route.emptyGestalt;
this.prevLocalRoutesMessage = null;
this.prevPeerRoutesMessage = null;
this.sock = null;
this.deduplicator = new Deduplicator();
this.deduplicator = new Minimart.Deduplicator();
this.connectionCount = 0;
this.activityTimestamp = 0;
this.idleTimeout = DEFAULT_IDLE_TIMEOUT;
@ -24,6 +38,20 @@ function WebSocketConnection(label, wsurl, shouldReconnect) {
this.pingTimer = null;
}
WebSocketConnection.prototype.debugState = function () {
return {
label: this.label,
sendsAttempted: this.sendsAttempted,
sendsTransmitted: this.sendsTransmitted,
receiveCount: this.receiveCount,
wsurl: this.wsurl,
shouldReconnect: this.shouldReconnect,
reconnectDelay: this.reconnectDelay,
connectionCount: this.connectionCount,
activityTimestamp: this.activityTimestamp
};
};
WebSocketConnection.prototype.clearHeartbeatTimers = function () {
if (this.idleTimer) { clearTimeout(this.idleTimer); this.idleTimer = null; }
if (this.pingTimer) { clearTimeout(this.pingTimer); this.pingTimer = null; }
@ -43,31 +71,19 @@ WebSocketConnection.prototype.statusRoute = function (status) {
return pub([this.label + "_state", status]);
};
WebSocketConnection.prototype.relayRoutes = function () {
// fresh copy each time, suitable for in-place extension/mutation
return [this.statusRoute(this.isConnected() ? "connected" : "disconnected"),
pub([this.label, __, __], 0, 1000),
sub([this.label, __, __], 0, 1000)];
WebSocketConnection.prototype.relayGestalt = function () {
return this.statusRoute(this.isConnected() ? "connected" : "disconnected")
.union(pub([this.label, __, __], 0, 10))
.union(sub([this.label, __, __], 0, 10));
// TODO: level 10 is ad-hoc; support infinity at some point in future
};
WebSocketConnection.prototype.aggregateRoutes = function () {
var rs = this.relayRoutes();
for (var i = 0; i < this.peerRoutes.length; i++) {
var r = this.peerRoutes[i];
rs.push(new Route(r.isSubscription,
// TODO: This is a horrible syntactic hack
// (in conjunction with the numberness-test
// in handleEvent's routes handler) for
// distinguishing routes published on behalf
// of the remote side from those published
// by the local side. See (**HACK**) mark
// below.
[this.label, __, r.pattern],
r.metaLevel,
r.level));
}
// console.log("WebSocketConnection.aggregateRoutes", this.label, rs);
return rs;
WebSocketConnection.prototype.aggregateGestalt = function () {
var self = this;
return this.peerGestalt.transform(function (m, metaLevel) {
return Route.compilePattern(true,
[self.label, metaLevel, Route.embeddedMatcher(m)]);
}).union(this.relayGestalt());
};
WebSocketConnection.prototype.boot = function () {
@ -84,48 +100,61 @@ WebSocketConnection.prototype.isConnected = function () {
WebSocketConnection.prototype.safeSend = function (m) {
try {
if (this.isConnected()) { this.sock.send(m); }
this.sendsAttempted++;
if (this.isConnected()) {
this.sock.send(m);
this.sendsTransmitted++;
}
} catch (e) {
console.warn("Trapped exn while sending", e);
}
};
WebSocketConnection.prototype.sendLocalRoutes = function () {
this.safeSend(JSON.stringify(encodeEvent(updateRoutes(this.localRoutes))));
var newLocalRoutesMessage =
JSON.stringify(Codec.encodeEvent(Minimart.updateRoutes([this.localGestalt])));
if (this.prevLocalRoutesMessage !== newLocalRoutesMessage) {
this.prevLocalRoutesMessage = newLocalRoutesMessage;
this.safeSend(newLocalRoutesMessage);
}
};
WebSocketConnection.prototype.collectMatchers = function (getAdvertisements, level, g) {
var extractMetaLevels = Route.compileProjection([this.label, _$, __]);
var mls = Route.matcherKeys(g.project(extractMetaLevels, getAdvertisements, 0, level));
for (var i = 0; i < mls.length; i++) {
var metaLevel = mls[i][0]; // only one capture in the projection
var extractMatchers = Route.compileProjection([this.label, metaLevel, _$]);
var m = g.project(extractMatchers, getAdvertisements, 0, level);
this.localGestalt = this.localGestalt.union(Route.simpleGestalt(getAdvertisements,
Route.embeddedMatcher(m),
metaLevel,
level));
}
};
WebSocketConnection.prototype.handleEvent = function (e) {
// console.log("WebSocketConnection.handleEvent", e);
switch (e.type) {
case "routes":
this.localRoutes = [];
for (var i = 0; i < e.routes.length; i++) {
var r = e.routes[i];
if (r.pattern.length && r.pattern.length === 3
&& r.pattern[0] === this.label
// TODO: This is a horrible syntactic hack (in
// conjunction with the use of __ in in
// aggregateRoutes) for distinguishing routes
// published on behalf of the remote side from those
// published by the local side. See (**HACK**) mark
// above.
&& typeof(r.pattern[1]) === "number")
{
this.localRoutes.push(new Route(r.isSubscription,
r.pattern[2],
r.pattern[1],
r.level));
}
// TODO: GROSS - erasing by pid!
var nLevels = e.gestalt.levelCount(0);
var relayGestalt = Route.fullGestalt(1, nLevels).label(World.activePid());
var g = e.gestalt.erasePath(relayGestalt);
this.localGestalt = Route.emptyGestalt;
for (var level = 0; level < nLevels; level++) {
this.collectMatchers(false, level, g);
this.collectMatchers(true, level, g);
}
this.sendLocalRoutes();
break;
case "message":
var m = e.message;
if (m.length && m.length === 3
&& m[0] === this.label
&& typeof(m[1]) === "number")
if (m.length && m.length === 3 && m[0] === this.label)
{
var encoded = JSON.stringify(encodeEvent(sendMessage(m[2], m[1], e.isFeedback)));
var encoded = JSON.stringify(Codec.encodeEvent(
Minimart.sendMessage(m[2], m[1], e.isFeedback)));
if (this.deduplicator.accept(encoded)) {
this.safeSend(encoded);
}
@ -149,15 +178,20 @@ WebSocketConnection.prototype.forceclose = function (keepReconnectDelay) {
WebSocketConnection.prototype.reconnect = function () {
var self = this;
this.forceclose(true);
this.connectionCount++;
this.sock = new WebSocket(this.wsurl);
this.sock.onopen = World.wrap(function (e) { return self.onopen(e); });
this.sock.onmessage = World.wrap(function (e) { return self.onmessage(e); });
this.sock.onmessage = World.wrap(function (e) {
self.receiveCount++;
return self.onmessage(e);
});
this.sock.onclose = World.wrap(function (e) { return self.onclose(e); });
};
WebSocketConnection.prototype.onopen = function (e) {
console.log("connected to " + this.sock.url);
this.reconnectDelay = DEFAULT_RECONNECT_DELAY;
this.prevLocalRoutesMessage = null;
this.sendLocalRoutes();
};
@ -173,13 +207,13 @@ WebSocketConnection.prototype.onmessage = function (wse) {
return; // recordActivity already took care of our timers
}
var e = decodeAction(j);
var e = Codec.decodeAction(j);
switch (e.type) {
case "routes":
if (this.prevPeerRoutesMessage !== wse.data) {
this.prevPeerRoutesMessage = wse.data;
this.peerRoutes = e.routes;
World.updateRoutes(this.aggregateRoutes());
this.peerGestalt = e.gestalt;
World.updateRoutes([this.aggregateGestalt()]);
}
break;
case "message":
@ -195,7 +229,7 @@ WebSocketConnection.prototype.onclose = function (e) {
console.log("onclose", e);
// Update routes to give clients some indication of the discontinuity
World.updateRoutes(this.aggregateRoutes());
World.updateRoutes([this.aggregateGestalt()]);
if (this.shouldReconnect) {
console.log("reconnecting to " + this.wsurl + " in " + this.reconnectDelay + "ms");
@ -209,32 +243,5 @@ WebSocketConnection.prototype.onclose = function (e) {
};
///////////////////////////////////////////////////////////////////////////
// Wire protocol representation of events and actions
function encodeEvent(e) {
switch (e.type) {
case "routes":
var rs = [];
for (var i = 0; i < e.routes.length; i++) {
rs.push(e.routes[i].toJSON());
}
return ["routes", rs];
case "message":
return ["message", e.message, e.metaLevel, e.isFeedback];
}
}
function decodeAction(j) {
switch (j[0]) {
case "routes":
var rs = [];
for (var i = 0; i < j[1].length; i++) {
rs.push(Route.fromJSON(j[1][i]));
}
return updateRoutes(rs);
case "message":
return sendMessage(j[1], j[2], j[3]);
default:
throw { message: "Invalid JSON-encoded action: " + JSON.stringify(j) };
}
}
module.exports.WebSocketConnection = WebSocketConnection;

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