diff --git a/js/examples/jquery/index.html b/js/examples/jquery/index.html new file mode 100644 index 0000000..d665f89 --- /dev/null +++ b/js/examples/jquery/index.html @@ -0,0 +1,15 @@ + + + + Syndicate: jQuery Example + + + + + + +

jQuery example

+ +
0
+ + diff --git a/js/examples/jquery/index.js b/js/examples/jquery/index.js new file mode 100644 index 0000000..4cc0d5a --- /dev/null +++ b/js/examples/jquery/index.js @@ -0,0 +1,28 @@ +"use strict"; + +var G; +$(document).ready(function () { + var Network = Syndicate.Network; + var sub = Syndicate.sub; + var __ = Syndicate.__; + var _$ = Syndicate._$; + + G = new Syndicate.Ground(function () { + console.log('starting ground boot'); + + Syndicate.JQuery.spawnJQueryDriver(); + + Network.spawn({ + boot: function () { + return sub(['jQuery', '#clicker', 'click', __]); + }, + handleEvent: function (e) { + if (e.type === 'message' && e.message[0] === 'jQuery' && e.message[1] === '#clicker') { + var r = $('#result'); + r.html(Number(r.html()) + 1); + } + } + }); + }); + G.startStepping(); +}); diff --git a/js/src/demand-matcher.js b/js/src/demand-matcher.js new file mode 100644 index 0000000..6cf7040 --- /dev/null +++ b/js/src/demand-matcher.js @@ -0,0 +1,80 @@ +var Immutable = require('immutable'); +var Syndicate = require('./syndicate.js'); +var Route = require('./route.js'); +var Patch = require('./patch.js'); +var Util = require('./util.js'); + +function DemandMatcher(demandSpec, supplySpec, options) { + options = Util.extend({ + metaLevel: 0, + onDemandIncrease: function (captures) { + console.error("Syndicate: Unhandled increase in demand", captures); + }, + onSupplyDecrease: function (captures) { + console.error("Syndicate: Unhandled decrease in supply", captures); + } + }, options); + this.metaLevel = options.metaLevel; + this.onDemandIncrease = options.onDemandIncrease; + this.onSupplyDecrease = options.onSupplyDecrease; + this.demandSpec = demandSpec; + this.supplySpec = supplySpec; + this.demandPattern = Route.projectionToPattern(demandSpec); + this.supplyPattern = Route.projectionToPattern(supplySpec); + this.demandProjection = Route.compileProjection(demandSpec); + this.supplyProjection = Route.compileProjection(supplySpec); + this.currentDemand = Immutable.Set(); + this.currentSupply = Immutable.Set(); +} + +DemandMatcher.prototype.boot = function () { + return Patch.sub(this.demandPattern, this.metaLevel) + .andThen(Patch.sub(this.supplyPattern, this.metaLevel)); +}; + +DemandMatcher.prototype.handleEvent = function (e) { + if (e.type === "stateChange") { + this.handlePatch(e.patch); + } +}; + +DemandMatcher.prototype.handlePatch = function (p) { + var self = this; + + var addedDemand = Route.trieKeys(Route.project(p.added, self.demandProjection)); + var removedDemand = Route.trieKeys(Route.project(p.removed, self.demandProjection)); + var addedSupply = Route.trieKeys(Route.project(p.added, self.supplyProjection)); + var removedSupply = Route.trieKeys(Route.project(p.removed, self.supplyProjection)); + + if (addedDemand === null) { + throw new Error("Syndicate: wildcard demand detected:\n" + + self.demandSpec + "\n" + + p.pretty()); + } + if (addedSupply === null) { + throw new Error("Syndicate: wildcard supply detected:\n" + + self.supplySpec + "\n" + + p.pretty()); + } + + self.currentSupply = self.currentSupply.union(addedSupply); + self.currentDemand = self.currentDemand.subtract(removedDemand); + + removedSupply.forEach(function (captures) { + if (self.currentDemand.has(captures)) { + self.onSupplyDecrease(Route.captureToObject(captures, self.supplyProjection)); + } + }); + addedDemand.forEach(function (captures) { + if (!self.currentSupply.has(captures)) { + self.onDemandIncrease(Route.captureToObject(captures, self.demandProjection)); + } + }); + + self.currentSupply = self.currentSupply.subtract(removedSupply); + self.currentDemand = self.currentDemand.union(addedDemand); +}; + +/////////////////////////////////////////////////////////////////////////// + +module.exports.DemandMatcher = DemandMatcher; diff --git a/js/src/jquery-driver.js b/js/src/jquery-driver.js new file mode 100644 index 0000000..622f962 --- /dev/null +++ b/js/src/jquery-driver.js @@ -0,0 +1,90 @@ +// JQuery event driver +var Syndicate = require("./syndicate.js"); +var Patch = require("./patch.js"); +var DemandMatcher = require('./demand-matcher.js').DemandMatcher; +var Network = Syndicate.Network; +var __ = Syndicate.__; +var _$ = Syndicate._$; + +function spawnJQueryDriver(baseSelector, metaLevel, wrapFunction) { + metaLevel = metaLevel || 0; + wrapFunction = wrapFunction || defaultWrapFunction; + Network.spawn( + new DemandMatcher(Patch.observe(wrapFunction(_$('selector'), _$('eventName'), __)), + Patch.advertise(wrapFunction(_$('selector'), _$('eventName'), __)), + { + metaLevel: metaLevel, + onDemandIncrease: function (c) { + Network.spawn(new JQueryEventRouter(baseSelector, + c.selector, + c.eventName, + metaLevel, + wrapFunction)); + } + })); +} + +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 = + Network.wrap(function (e) { + Network.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 Patch.pub(this.wrapFunction(this.selector, this.eventName, __), this.metaLevel) + .andThen(Patch.sub(Patch.observe(this.wrapFunction(this.selector, this.eventName, __)), + this.metaLevel)); +}; + +JQueryEventRouter.prototype.handleEvent = function (e) { + if (e.type === "stateChange" && e.patch.hasRemoved()) { + this.computeNodes().off(this.eventName, this.handler); + Network.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; diff --git a/js/src/main.js b/js/src/main.js index b494805..57d881c 100644 --- a/js/src/main.js +++ b/js/src/main.js @@ -17,8 +17,10 @@ copyKeys(['__', '_$', '$Capture', '$Special', module.exports, module.exports.Route); +module.exports.DemandMatcher = require('./demand-matcher.js').DemandMatcher; + // module.exports.DOM = require("./dom-driver.js"); -// module.exports.JQuery = require("./jquery-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");