diff --git a/index.html b/index.html index a0864a1..48018e0 100644 --- a/index.html +++ b/index.html @@ -22,6 +22,7 @@

+

diff --git a/index.js b/index.js index acb26e1..3ab16e0 100644 --- a/index.js +++ b/index.js @@ -9,50 +9,41 @@ Spy.prototype.handleEvent = function (e) { console.log("SPY", e); }; -function JQueryDriver() { - this.handlerMap = {}; +function JQueryEventRouter(selector, eventName) { + var self = this; + this.selector = selector; + this.eventName = eventName; + this.handler = + World.wrap(function (e) { World.send(["jQuery", self.selector, self.eventName, e]); }); + $(this.selector).on(this.eventName, this.handler); } -JQueryDriver.prototype.updateHandlerMap = function (routes) { - var newMap = {}; - for (var i = 0; i < routes.length; i++) { - var selector = routes[i].pattern[1]; - var eventName = routes[i].pattern[2]; - if (typeof(selector) === 'string' && typeof(eventName) === 'string') { - var key = JSON.stringify([selector, eventName]); - var handler = this.handlerMap[key]; - if (!handler) { - handler = (function (selector, eventName) { // JS is broken - return World.wrap(function (e) { - World.send(["jQuery", selector, eventName, e]); - }); - })(selector, eventName); - console.log("jQuery", "installing", selector, eventName); - $(selector).on(eventName, handler); - } - newMap[key] = handler; - } +JQueryEventRouter.prototype.handleEvent = function (e) { + if (e.type === "routes" && e.routes.length === 0) { + $(this.selector).off(this.eventName, this.handler); + World.exit(); } - for (var key in this.handlerMap) { - if (this.handlerMap.hasOwnProperty(key) && !(key in newMap)) { - var keyArray = JSON.parse(key); - var handler = this.handlerMap[key]; - var selector = keyArray[0]; - var eventName = keyArray[1]; - console.log("jQuery", "removing", selector, eventName); - $(selector).off(eventName, handler); - } - } - this.handlerMap = newMap; }; +function JQueryDriver() { + var self = this; + this.state = new DemandMatcher(true, function (r) { + var selector = r.pattern[1]; + var eventName = r.pattern[2]; + World.spawn(new JQueryEventRouter(selector, eventName), + [pub(["jQuery", selector, eventName, __]), + pub(["jQuery", selector, eventName, __], 0, 1)]); + }); +} + JQueryDriver.prototype.boot = function () { - World.updateRoutes([pub(["jQuery", __, __, __], 0, 1)]); + World.updateRoutes([pub(["jQuery", __, __, __], 0, 1), + sub(["jQuery", __, __, __], 0, 1)]); }; JQueryDriver.prototype.handleEvent = function (e) { if (e.type === "routes") { - this.updateHandlerMap(e.routes); + this.state.handleRoutes(e.routes); } }; @@ -61,19 +52,24 @@ var g = new Ground(function () { World.spawn(new Spy()); World.spawn(new JQueryDriver()); World.spawn({ - step: function () { console.log('dummy step'); }, + // step: function () { console.log('dummy step'); }, boot: function () { console.log('dummy boot'); - World.updateRoutes([sub(["jQuery", "#testButton", "click", __]), sub(__, 1)]); + World.updateRoutes([sub(["jQuery", "#testButton", "click", __]), + sub(["jQuery", "#testButton2", "click", __]), + sub(__, 1)]); World.send({msg: 'hello outer world'}, 1); World.send({msg: 'hello inner world'}, 0); }, handleEvent: function (e) { if (e.type === "send" && e.message[0] === "jQuery") { - console.log("got a click"); - World.updateRoutes([]); - } else { - console.log('dummy handleEvent', e); + if (e.message[1] === "#testButton") { + console.log("got a click"); + World.updateRoutes([sub(["jQuery", "#testButton2", "click", __])]); + } else { + console.log("got a click 2"); + // World.exit(); + } } } }); diff --git a/marketplace.js b/marketplace.js index 9e9c0df..e141955 100644 --- a/marketplace.js +++ b/marketplace.js @@ -81,6 +81,10 @@ 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]; +}; + function sub(pattern, metaLevel, level) { return new Route(true, pattern, metaLevel, level); } @@ -126,27 +130,31 @@ function liftRoutes(routes) { return result; } -function filterEvent(e, routes) { - switch (e.type) { - case "routes": - var result = []; - for (var i = 0; i < e.routes.length; i++) { - for (var j = 0; j < routes.length; j++) { - var ri = e.routes[i]; - var rj = routes[j]; - if (ri.isSubscription === !rj.isSubscription - && ri.metaLevel === rj.metaLevel - && ri.level < rj.level) - { - var u = unify(ri.pattern, rj.pattern); - if (u) { - var rk = new Route(ri.isSubscription, u.result, ri.metaLevel, ri.level); - result.push(rk); - } +function intersectRoutes(rs1, rs2, ignoreLevels) { + 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]; + if (ri.isSubscription === !rj.isSubscription + && ri.metaLevel === rj.metaLevel + && (ignoreLevels || (ri.level < rj.level))) + { + var u = unify(ri.pattern, rj.pattern); + if (u) { + var rk = new Route(ri.isSubscription, u.result, ri.metaLevel, ri.level); + result.push(rk); } } } - return updateRoutes(result); + } + return result; +} + +function filterEvent(e, routes) { + switch (e.type) { + case "routes": + return updateRoutes(intersectRoutes(e.routes, routes)); case "send": for (var i = 0; i < routes.length; i++) { var r = routes[i]; @@ -198,6 +206,10 @@ World.spawn = function (behavior, initialRoutes) { World.current().enqueueAction(spawn(behavior, initialRoutes)); }; +World.exit = function (exn) { + World.current().killActive(exn); +}; + World.withWorldStack = function (stack, f) { var oldStack = World.stack; World.stack = stack; @@ -229,6 +241,10 @@ World.wrap = function (f) { /* Instance methods */ +World.prototype.killActive = function (exn) { + this.kill(this.activePid, exn); +}; + World.prototype.enqueueAction = function (action) { this.processActions.push([this.activePid, action]); }; @@ -279,9 +295,9 @@ World.prototype.asChild = function (pid, f) { World.prototype.kill = function (pid, exn) { if (exn && exn.stack) { - console.log("Killed process", pid, exn, exn.stack); + console.log("Process exited", pid, exn, exn.stack); } else { - console.log("Killed process", pid, exn); + console.log("Process exited", pid, exn); } delete this.processTable[pid]; this.issueRoutingUpdate(); @@ -386,12 +402,89 @@ World.prototype.handleEvent = function (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.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 (probeRoute.isSubscription === !existingRoute.isSubscription + && probeRoute.metaLevel === existingRoute.metaLevel + && unify(probeRoute.pattern, existingRoute.pattern)) + { + return true; + } + } + return false; +}; + +/*---------------------------------------------------------------------------*/ +/* Utilities: matching demand for some service */ + +function DemandMatcher(demandSideIsSubscription, demandIncreaseHandler, supplyDecreaseHandler) { + this.demandSideIsSubscription = demandSideIsSubscription; + this.demandIncreaseHandler = demandIncreaseHandler; + this.supplyDecreaseHandler = supplyDecreaseHandler || function (r) { + console.error("Unexpected drop in supply for route", r); + }; + this.state = new PresenceDetector(); +} + +DemandMatcher.prototype.handleRoutes = function (routes) { + var changes = this.state.handleRoutes(routes); + for (var i = 0; i < changes.added.length; i++) { + if (changes.added[i].isSubscription === this.demandSideIsSubscription + && !this.state.presenceExistsFor(changes.added[i])) + { + this.demandIncreaseHandler(changes.added[i]); + } + } + for (var i = 0; i < changes.removed.length; i++) { + if (changes.removed[i].isSubscription === !this.demandSideIsSubscription + && this.state.presenceExistsFor(changes.removed[i])) + { + this.supplyDecreaseHandler(changes.removed[i]); + } + } +} + /*---------------------------------------------------------------------------*/ /* Ground interface */ function Ground(bootFn) { var self = this; this.stepperId = null; + this.state = new PresenceDetector(); World.withWorldStack([this], function () { self.world = new World(bootFn); }); @@ -409,8 +502,9 @@ Ground.prototype.stopStepping = World.prototype.stopStepping; Ground.prototype.enqueueAction = function (action) { if (action.type === 'routes') { - if (action.routes.length > 0) { - console.error("You have subscribed to a nonexistent event source.", action); + 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);