diff --git a/js/src/ack.js b/js/src/ack.js new file mode 100644 index 0000000..7e7f3c2 --- /dev/null +++ b/js/src/ack.js @@ -0,0 +1,41 @@ +// Utility protocol for measuring when a stateChange takes effect. + +var RandomID = require('./randomid.js'); +var Syndicate = require('./syndicate.js'); +var Route = require('./route.js'); +var Patch = require('./patch.js'); + +var $Ack = new Route.$Special('ack'); + +function Ack(metaLevel, id) { + this.metaLevel = metaLevel || 0; + this.id = id || RandomID.randomId(16); + this.done = false; +} + +Ack.prototype.arm = function () { + Syndicate.Network.stateChange(Patch.sub([$Ack, this.id], this.metaLevel)); + Syndicate.Network.send([$Ack, this.id], this.metaLevel); +}; + +Ack.prototype.disarm = function () { + Syndicate.Network.stateChange(Patch.unsub([$Ack, this.id], this.metaLevel)); +}; + +Ack.prototype.check = function (e) { + if (!this.done) { + if (e.type === 'message') { + var m = Patch.stripAtMeta(e.message, this.metaLevel); + if (m && m[0] === $Ack && m[1] === this.id) { + this.disarm(); + this.done = true; + } + } + } + return this.done; +}; + +/////////////////////////////////////////////////////////////////////////// + +module.exports.$Ack = $Ack; +module.exports.Ack = Ack; diff --git a/js/src/dom-driver.js b/js/src/dom-driver.js index 4139ede..1f5316a 100644 --- a/js/src/dom-driver.js +++ b/js/src/dom-driver.js @@ -33,6 +33,8 @@ function DOMFragment(selector, fragmentClass, fragmentSpec, domWrapFunction, jQu this.fragmentSpec = fragmentSpec; this.domWrapFunction = domWrapFunction; this.jQueryWrapFunction = jQueryWrapFunction; + this.demandExists = false; + this.subscriptionEstablished = new Syndicate.Ack(); this.nodes = this.buildNodes(); } @@ -45,22 +47,36 @@ DOMFragment.prototype.boot = function () { 1, self.jQueryWrapFunction); Network.spawn({ + demandExists: false, + subscriptionEstablished: new Syndicate.Ack(1), boot: function () { + this.subscriptionEstablished.arm(); return Patch.sub(Patch.advertise(specification), 1); }, handleEvent: function (e) { - if (e.type === "stateChange" && e.patch.hasRemoved()) { + this.subscriptionEstablished.check(e); + if (e.type === "stateChange") { + if (e.patch.hasAdded()) this.demandExists = true; + if (e.patch.hasRemoved()) this.demandExists = false; + } + if (this.subscriptionEstablished.done && !this.demandExists) { Network.exitNetwork(); } } }); })); + this.subscriptionEstablished.arm(); return Patch.sub(specification).andThen(Patch.pub(specification)); }; DOMFragment.prototype.handleEvent = function (e) { - if (e.type === "stateChange" && e.patch.hasRemoved()) { + this.subscriptionEstablished.check(e); + if (e.type === "stateChange") { + if (e.patch.hasAdded()) this.demandExists = true; + if (e.patch.hasRemoved()) this.demandExists = false; + } + if (this.subscriptionEstablished.done && !this.demandExists) { for (var i = 0; i < this.nodes.length; i++) { var n = this.nodes[i]; n.parentNode.removeChild(n); diff --git a/js/src/main.js b/js/src/main.js index 1c2984c..ace608c 100644 --- a/js/src/main.js +++ b/js/src/main.js @@ -19,7 +19,9 @@ copyKeys(['__', '_$', '$Capture', '$Special', module.exports.DemandMatcher = require('./demand-matcher.js').DemandMatcher; module.exports.Seal = require('./seal.js').Seal; +module.exports.Ack = require('./ack.js').Ack; +module.exports.RandomID = require('./randomid.js'); module.exports.DOM = require("./dom-driver.js"); module.exports.JQuery = require("./jquery-driver.js"); // module.exports.RoutingTableWidget = require("./routing-table-widget.js"); diff --git a/js/src/patch.js b/js/src/patch.js index f4e5b2e..c031f0c 100644 --- a/js/src/patch.js +++ b/js/src/patch.js @@ -33,6 +33,17 @@ function prependAtMeta(p, level) { return p; } +function stripAtMeta(p, level) { + while (level--) { + if (p.length === 2 && p[0] === $AtMeta) { + p = p[1]; + } else { + return null; + } + } + return p; +} + function observeAtMeta(p, level) { if (level === 0) { return Route.compilePattern(true, observe(p)); @@ -240,6 +251,7 @@ module.exports.isAtMeta = isAtMeta; module.exports.isAdvertise = isAdvertise; module.exports.prependAtMeta = prependAtMeta; +module.exports.stripAtMeta = stripAtMeta; module.exports.observeAtMeta = observeAtMeta; module.exports.assert = assert; module.exports.retract = retract; diff --git a/js/src/randomid.js b/js/src/randomid.js new file mode 100644 index 0000000..9e749c3 --- /dev/null +++ b/js/src/randomid.js @@ -0,0 +1,20 @@ +var randomId; + +if ((typeof window !== 'undefined') && + (typeof window.crypto !== 'undefined') && + (typeof window.crypto.getRandomValues !== 'undefined')) { + randomId = function (byteCount) { + var buf = new Uint8Array(byteCount); + window.crypto.getRandomValues(buf); + return btoa(String.fromCharCode.apply(null, buf)).replace(/=/g,''); + }; +} else if ((typeof crypto !== 'undefined') && + (typeof crypto.randomBytes !== 'undefined')) { + randomId = function (byteCount) { + return crypto.randomBytes(byteCount).base64Slice().replace(/=/g,''); + }; +} else { + console.warn('No suitable implementation for RandomID.randomId available.'); +} + +module.exports.randomId = randomId;