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