diff --git a/dom-driver.js b/dom-driver.js
index da88ec6..caf5cfc 100644
--- a/dom-driver.js
+++ b/dom-driver.js
@@ -1,11 +1,11 @@
// 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];
+ var d = new DemandMatcher(["DOM", _$, _$, _$], 0, {demandSideIsSubscription: false});
+ d.onDemandIncrease = function (captures) {
+ var selector = captures[0];
+ var fragmentClass = captures[1];
+ var fragmentSpec = captures[2];
World.spawn(new DOMFragment(selector, fragmentClass, fragmentSpec),
[sub(["DOM", selector, fragmentClass, fragmentSpec]),
sub(["DOM", selector, fragmentClass, fragmentSpec], 0, 1)]);
@@ -28,11 +28,8 @@ DOMFragment.prototype.boot = function () {
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) {
+ var level = e.gestalt.getLevel(1, 0); // find participant peers
+ if (!e.gestalt.isEmpty() && level.isEmpty()) {
World.shutdownWorld();
}
}
@@ -42,7 +39,7 @@ DOMFragment.prototype.boot = function () {
};
DOMFragment.prototype.handleEvent = function (e) {
- if (e.type === "routes" && e.routes.length === 0) {
+ 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);
diff --git a/examples/dom/index.html b/examples/dom/index.html
index 080f9a0..c3331e3 100644
--- a/examples/dom/index.html
+++ b/examples/dom/index.html
@@ -10,6 +10,7 @@
+
diff --git a/examples/smoketest/index.html b/examples/smoketest/index.html
new file mode 100644
index 0000000..779a255
--- /dev/null
+++ b/examples/smoketest/index.html
@@ -0,0 +1,15 @@
+
+
+
+ JS Marketplace: Smoketest
+
+
+
+
+
+
+
+
+ Smoketest
+
+
diff --git a/examples/smoketest/index.js b/examples/smoketest/index.js
new file mode 100644
index 0000000..d3b8423
--- /dev/null
+++ b/examples/smoketest/index.js
@@ -0,0 +1,24 @@
+var G;
+$(document).ready(function () {
+ G = new Ground(function () {
+ console.log('starting ground boot');
+ World.spawn(new Spy("GROUND", true));
+ World.spawn({
+ counter: 0,
+ handleEvent: function (e) {},
+ step: function () {
+ World.send(["beep", this.counter++]);
+ return this.counter <= 10;
+ }
+ }, [pub(["beep", __])]);
+
+ World.spawn({
+ handleEvent: function (e) {
+ if (e.type === "message" && e.message[0] === "beep") {
+ console.log("beep!", e.message[1]);
+ }
+ }
+ }, [sub(["beep", __])]);
+ });
+ G.startStepping();
+});
diff --git a/jquery-driver.js b/jquery-driver.js
index 85eb9a8..d3e2f43 100644
--- a/jquery-driver.js
+++ b/jquery-driver.js
@@ -2,10 +2,10 @@
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];
+ var d = new DemandMatcher(["jQuery", _$, _$, __], metaLevel);
+ d.onDemandIncrease = function (captures) {
+ var selector = captures[0];
+ var eventName = captures[1];
World.spawn(new JQueryEventRouter(baseSelector, selector, eventName, metaLevel),
[pub(["jQuery", selector, eventName, __], metaLevel),
pub(["jQuery", selector, eventName, __], metaLevel, 1)]);
@@ -31,7 +31,7 @@ function JQueryEventRouter(baseSelector, selector, eventName, metaLevel) {
}
JQueryEventRouter.prototype.handleEvent = function (e) {
- if (e.type === "routes" && e.routes.length === 0) {
+ if (e.type === "routes" && e.gestalt.isEmpty()) {
this.computeNodes().off(this.eventName, this.handler);
World.exit();
}
diff --git a/marketplace.js b/marketplace.js
index 4672f17..991daf8 100644
--- a/marketplace.js
+++ b/marketplace.js
@@ -1,121 +1,34 @@
-/*---------------------------------------------------------------------------*/
-/* 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;
-}
+// TODO: trigger-guards as per minimart
/*---------------------------------------------------------------------------*/
/* 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) };
- }
-};
+var __ = route.__;
+var _$ = route._$;
function sub(pattern, metaLevel, level) {
- return new Route(true, pattern, metaLevel, level);
+ return route.simpleGestalt(false, pattern, metaLevel, level);
}
function pub(pattern, metaLevel, level) {
- return new Route(false, pattern, metaLevel, level);
+ return route.simpleGestalt(true, pattern, metaLevel, level);
}
-function spawn(behavior, initialRoutes) {
+function spawn(behavior, initialGestalts) {
return { type: "spawn",
behavior: behavior,
- initialRoutes: (initialRoutes === undefined) ? [] : initialRoutes };
+ initialGestalt: route.gestaltUnion(initialGestalts || []) };
}
-function updateRoutes(routes) {
- return { type: "routes", routes: routes };
+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) {
@@ -129,104 +42,53 @@ 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.runnablePids = {};
+ this.partialGestalt = route.emptyGestalt; // Only gestalt from local processes
+ this.fullGestalt = route.emptyGestalt ;; // partialGestalt unioned with downwardGestalt
this.processTable = {};
- this.downwardRoutes = [];
+ this.downwardGestalt = route.emptyGestalt;
this.processActions = [];
- this.activePid = null;
- this.stepperId = null;
this.asChild(-1, bootFn, true);
}
/* Class state / methods */
+World.nextPid = 0;
+
World.stack = [];
World.current = function () {
- return World.stack[World.stack.length - 1];
+ 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(sendMessage(m, metaLevel, isFeedback));
+ World.current().enqueueAction(World.activePid(), sendMessage(m, metaLevel, isFeedback));
};
-World.updateRoutes = function (routes) {
- World.current().enqueueAction(updateRoutes(routes));
+World.updateRoutes = function (gestalts) {
+ World.current().enqueueAction(World.activePid(), updateRoutes(gestalts));
};
-World.spawn = function (behavior, initialRoutes) {
- World.current().enqueueAction(spawn(behavior, initialRoutes));
+World.spawn = function (behavior, initialGestalts) {
+ World.current().enqueueAction(World.activePid(), spawn(behavior, initialGestalts));
};
World.exit = function (exn) {
- World.current().killActive(exn);
+ World.current().kill(World.activePid(), exn);
};
World.shutdownWorld = function () {
- World.current().enqueueAction(shutdownWorld());
+ World.current().enqueueAction(World.activePid(), shutdownWorld());
};
World.withWorldStack = function (stack, f) {
@@ -245,14 +107,15 @@ World.withWorldStack = function (stack, f) {
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 () {
+ var result = World.current().asChild(World.activePid(), function () {
return f.apply(null, actuals);
});
- World.stack[0].startStepping();
+ for (var i = World.stack.length - 1; i >= 0; i--) {
+ World.stack[i][0].markPidRunnable(World.stack[i][1]);
+ }
return result;
});
};
@@ -260,40 +123,28 @@ World.wrap = function (f) {
/* Instance methods */
-World.prototype.killActive = function (exn) {
- this.kill(this.activePid, exn);
+World.prototype.enqueueAction = function (pid, action) {
+ this.processActions.push([pid, action]);
};
-World.prototype.enqueueAction = function (action) {
- this.processActions.push([this.activePid, 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.isQuiescent = function () {
- return this.eventQueue.length === 0 && this.processActions.length === 0;
+World.prototype.markPidRunnable = function (pid) {
+ this.runnablePids[pid] = [pid];
};
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;
- }
+ this.stepChildren();
+ return this.alive && !this.isInert();
};
World.prototype.asChild = function (pid, f, omitLivenessCheck) {
@@ -302,17 +153,15 @@ World.prototype.asChild = function (pid, f, omitLivenessCheck) {
return;
}
- World.stack.push(this);
+ World.stack.push([this, pid]);
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" };
+ if (World.stack.pop()[0] !== this) {
+ throw new Error("Internal error: World stack imbalance");
}
return result;
};
@@ -328,19 +177,21 @@ World.prototype.kill = function (pid, exn) {
this.asChild(pid, function () { return p.behavior.trapexit(exn); });
}
delete this.processTable[pid];
- this.issueRoutingUpdate();
+ if (p) {
+ this.applyAndIssueRoutingUpdate(p.gestalt, route.emptyGestalt);
+ }
};
World.prototype.stepChildren = function () {
- var someChildBusy = false;
- for (var pid in this.processTable) {
+ var pids = this.runnablePids;
+ this.runnablePids = {};
+ for (var pid in pids) {
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;
+ 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);
}
}
- return someChildBusy;
};
World.prototype.performActions = function () {
@@ -364,18 +215,25 @@ World.prototype.dispatchEvents = function () {
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();
+ var pid = World.nextPid++;
+ var newGestalt = action.initialGestalt.label(pid);
+ this.processTable[pid] = { gestalt: newGestalt, behavior: action.behavior };
+ if (action.behavior.boot) {
+ this.asChild(pid, function () { action.behavior.boot() });
+ this.markPidRunnable(pid);
+ }
+ this.applyAndIssueRoutingUpdate(route.emptyGestalt, newGestalt, 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
- this.processTable[pid].routes = action.routes;
+ 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);
}
- this.issueRoutingUpdate();
break;
case "message":
if (action.metaLevel === 0) {
@@ -389,179 +247,138 @@ World.prototype.performAction = function (pid, action) {
World.exit();
break;
default:
- throw { message: "Action type " + action.type + " not understood",
- action: action };
+ var exn = new Error("Action type " + action.type + " not understood");
+ exn.action = action;
+ throw exn;
}
};
-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.updateFullGestalt = function () {
+ this.fullGestalt = this.partialGestalt.union(this.downwardGestalt);
};
-World.prototype.issueLocalRoutingUpdate = function () {
- this.eventQueue.push(updateRoutes(this.aggregateRoutes(this.downwardRoutes)));
+World.prototype.issueLocalRoutingUpdate = function (affectedSubgestalt, knownTarget) {
+ this.eventQueue.push(pendingRoutingUpdate(this.fullGestalt,
+ affectedSubgestalt,
+ knownTarget));
};
-World.prototype.issueRoutingUpdate = function () {
- this.issueLocalRoutingUpdate();
- World.updateRoutes(dropRoutes(this.aggregateRoutes([])));
+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) {
- 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) }); }
+ 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.warning("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.handleEvent = function (e) {
switch (e.type) {
case "routes":
- this.downwardRoutes = liftRoutes(e.routes);
- this.issueLocalRoutingUpdate();
+ 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:
- throw { message: "Event type " + e.type + " not understood",
- event: e };
+ var exn = new Error("Event type " + e.type + " not understood");
+ exn.event = e;
+ throw exn;
}
};
-/*---------------------------------------------------------------------------*/
-/* 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) {
+function DemandMatcher(projection, metaLevel, options) {
options = $.extend({
demandLevel: 0,
supplyLevel: 0,
demandSideIsSubscription: true
}, options);
- this.pattern = pattern;
+ this.pattern = route.projectionToPattern(projection);
+ this.projectionSpec = route.compileProjection(projection);
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.onDemandIncrease = function (captures) {
+ console.error("Unhandled increase in demand for route", captures);
};
- this.onSupplyDecrease = function (r) {
- console.error("Unhandled decrease in supply for route", r);
+ this.onSupplyDecrease = function (captures) {
+ console.error("Unhandled decrease in supply for route", captures);
};
- this.state = new PresenceDetector();
+ this.currentDemand = {};
+ this.currentSupply = {};
}
DemandMatcher.prototype.boot = function () {
- World.updateRoutes([this.computeDetector(true),
- this.computeDetector(false)]);
+ var observerLevel = 1 + Math.max(this.demandLevel, this.supplyLevel);
+ World.updateRoutes([sub(this.pattern, this.metaLevel, observerLevel),
+ pub(this.pattern, this.metaLevel, observerLevel)]);
};
DemandMatcher.prototype.handleEvent = function (e) {
if (e.type === "routes") {
- this.handleRoutes(e.routes);
+ this.handleGestalt(e.gestalt);
}
};
-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); }
- }
+DemandMatcher.prototype.handleGestalt = function (gestalt) {
+ var newDemandMatcher = gestalt.project(this.metaLevel,
+ this.demandLevel,
+ !this.demandSideIsSubscription,
+ this.projectionSpec);
+ var newSupplyMatcher = gestalt.project(this.metaLevel,
+ this.supplyLevel,
+ this.demandSideIsSubscription,
+ this.projectionSpec)
+ 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]);
};
/*---------------------------------------------------------------------------*/
@@ -607,27 +424,51 @@ Deduplicator.prototype.expireMessages = function () {
function Ground(bootFn) {
var self = this;
this.stepperId = null;
- this.state = new PresenceDetector();
- World.withWorldStack([this], function () {
+ World.withWorldStack([[this, -1]], function () {
self.world = new World(bootFn);
});
}
Ground.prototype.step = function () {
var self = this;
- return World.withWorldStack([this], function () {
+ return World.withWorldStack([[this, -1]], function () {
return self.world.step();
});
};
-Ground.prototype.startStepping = World.prototype.startStepping;
-Ground.prototype.stopStepping = World.prototype.stopStepping;
+Ground.prototype.checkPid = function (pid) {
+ if (pid !== -1) console.error("Weird pid in Ground markPidRunnable", pid);
+};
-Ground.prototype.enqueueAction = function (action) {
+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') {
- var added = this.state.handleRoutes(action.routes).added;
- if (added.length > 0) {
- console.error("You have subscribed to a nonexistent event source.", added);
+ 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);
diff --git a/routing-table-widget.js b/routing-table-widget.js
index 70f065a..dc96b1b 100644
--- a/routing-table-widget.js
+++ b/routing-table-widget.js
@@ -1,95 +1,34 @@
-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;
- }
+function spawnRoutingTableWidget(selector, fragmentClass, observationLevel) {
+ observationLevel = observationLevel || 10;
+ // ^ arbitrary: should be Infinity, when route.js supports it. TODO
World.spawn({
boot: function () { this.updateState(); },
- state: [],
- nextState: [],
+ state: route.emptyGestalt.serialize(),
+ nextState: route.emptyGestalt.serialize(),
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;
+ localGestalt: (sub( ["DOM", selector, fragmentClass, __], 0, 2)
+ .union(pub(["DOM", selector, fragmentClass, __], 0, 2))
+ .telescoped()),
+
+ digestGestalt: function (g) {
+ return g.stripLabel().erasePath(this.localGestalt).serialize();
},
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),
+ var elts = ["ul", {"class": "routing-table"},
+ ["li", ["pre", route.deserializeGestalt(this.state).pretty()]]];
+ World.updateRoutes([sub(__, 0, observationLevel),
+ pub(__, 0, observationLevel),
pub(["DOM", selector, fragmentClass, elts])]);
},
handleEvent: function (e) {
var self = this;
if (e.type === "routes") {
- self.nextState = self.digestRoutes(e.routes);
+ self.nextState = self.digestGestalt(e.gestalt);
if (self.timer) {
clearTimeout(self.timer);
self.timer = false;
diff --git a/spy.js b/spy.js
index c1139b3..2abb991 100644
--- a/spy.js
+++ b/spy.js
@@ -1,18 +1,19 @@
// Generic Spy
-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)]);
+ World.updateRoutes([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;