From d78481c1ceea86b39cdafad8cc718b3d9d611f48 Mon Sep 17 00:00:00 2001 From: David Greenspan Date: Sun, 16 Sep 2012 02:28:41 -0700 Subject: [PATCH 01/17] introduce Meteor.deps.ContextSet, use in Session --- packages/deps/deps-utils.js | 42 ++++++++++++++++++++++ packages/deps/package.js | 2 +- packages/session/session.js | 72 +++++++++++-------------------------- 3 files changed, 64 insertions(+), 52 deletions(-) create mode 100644 packages/deps/deps-utils.js diff --git a/packages/deps/deps-utils.js b/packages/deps/deps-utils.js new file mode 100644 index 0000000000..3010a0e087 --- /dev/null +++ b/packages/deps/deps-utils.js @@ -0,0 +1,42 @@ +(function () { + + // Constructor for an empty ContextSet. + var ContextSet = function () { + this._contextsById = {}; + }; + + // Adds the Context `ctx` to the set (if it is + // not already present). The Context will only + // remain in the set as long as it has not been + // invalidated. + // Returns true if the context was newly added. + ContextSet.prototype.add = function (ctx) { + var self = this; + if (ctx && ! (ctx.id in self._contextsById)) { + self._contextsById[ctx.id] = ctx; + ctx.on_invalidate(function () { + delete self._contextsById[ctx.id]; + }); + return true; + } + return false; + }; + + // Invalidate all Contexts in the set and remove + // them from the set. + ContextSet.prototype.invalidateAll = function () { + var self = this; + for (var id in self._contextsById) + self._contextsById[id].invalidate(); + }; + + // Returns true if there are no Contexts in this set. + ContextSet.prototype.isEmpty = function () { + var self = this; + for(var id in self._contextsById) + return false; + return true; + }; + + Meteor.deps.ContextSet = ContextSet; +})(); \ No newline at end of file diff --git a/packages/deps/package.js b/packages/deps/package.js index 9949cfdd73..f21ff22073 100644 --- a/packages/deps/package.js +++ b/packages/deps/package.js @@ -9,5 +9,5 @@ Package.on_use(function (api, where) { where = where || ['client', 'server']; api.use('underscore', where); - api.add_files('deps.js', where); + api.add_files(['deps.js', 'deps-utils.js'], where); }); diff --git a/packages/session/session.js b/packages/session/session.js index 11b39c580b..f2ca6683f5 100644 --- a/packages/session/session.js +++ b/packages/session/session.js @@ -1,31 +1,9 @@ // XXX could use some tests Session = _.extend({}, { - keys: {}, - key_deps: {}, // key -> context id -> context - key_value_deps: {}, // key -> value -> context id -> context - - // XXX remove debugging method (or improve it, but anyway, don't - // ship it in production) - dump_state: function () { - var self = this; - console.log("=== Session state ==="); - for (var key in self.key_deps) { - var ids = _.keys(self.key_deps[key]); - if (!ids.length) - continue; - console.log(key + ": " + _.reject(ids, function (x) {return x === "_once"}).join(' ')); - } - - for (var key in self.key_value_deps) { - for (var value in self.key_value_deps[key]) { - var ids = _.keys(self.key_value_deps[key][value]); - if (!ids.length) - continue; - console.log(key + "(" + value + "): " + _.reject(ids, function (x) {return x === "_once";}).join(' ')); - } - } - }, + keys: {}, // key -> value + key_deps: {}, // key -> ContextSet + key_value_deps: {}, // key -> value -> ContextSet set: function (key, value) { var self = this; @@ -35,30 +13,24 @@ Session = _.extend({}, { return; self.keys[key] = value; - var invalidate = function (map) { - if (map) - for (var id in map) - map[id].invalidate(); + var invalidateAll = function (set) { + set && set.invalidateAll(); }; - self._ensureKey(key); - invalidate(self.key_deps[key]); - invalidate(self.key_value_deps[key][old_value]); - invalidate(self.key_value_deps[key][value]); + invalidateAll(self.key_deps[key]); + if (self.key_value_deps[key]) { + invalidateAll(self.key_value_deps[key][old_value]); + invalidateAll(self.key_value_deps[key][value]); + } }, get: function (key) { var self = this; var context = Meteor.deps.Context.current; - self._ensureKey(key); - - if (context && !(context.id in self.key_deps[key])) { - self.key_deps[key][context.id] = context; - context.on_invalidate(function () { - delete self.key_deps[key][context.id]; - }); + if (context) { + self._ensureKey(key); + self.key_deps[key].add(context); } - return self.keys[key]; }, @@ -74,19 +46,17 @@ Session = _.extend({}, { if (context) { self._ensureKey(key); + if (!(value in self.key_value_deps[key])) - self.key_value_deps[key][value] = {}; + self.key_value_deps[key][value] = new Meteor.deps.ContextSet; - if (!(context.id in self.key_value_deps[key][value])) { - self.key_value_deps[key][value][context.id] = context; + var isNew = self.key_value_deps[key][value].add(context); + if (isNew) { context.on_invalidate(function () { - delete self.key_value_deps[key][value][context.id]; - // clean up [key][value] if it's now empty, so we don't use // O(n) memory for n = values seen ever - for (var x in self.key_value_deps[key][value]) - return; - delete self.key_value_deps[key][value]; + if (self.key_value_deps[key][value].isEmpty()) + delete self.key_value_deps[key][value]; }); } } @@ -97,8 +67,8 @@ Session = _.extend({}, { _ensureKey: function (key) { var self = this; if (!(key in self.key_deps)) { - self.key_deps[key] = {}; - self.key_value_deps[key] = {}; + self.key_deps[key] = new Meteor.deps.ContextSet; + self.key_value_deps[key] = new Meteor.deps.ContextSet; } } }); From 16777fc5e64d8b6287f0049c14aa20328f8618a4 Mon Sep 17 00:00:00 2001 From: David Greenspan Date: Sun, 16 Sep 2012 02:32:57 -0700 Subject: [PATCH 02/17] tweaks for style --- packages/session/session.js | 41 +++++++++++++++++++------------------ 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/packages/session/session.js b/packages/session/session.js index f2ca6683f5..3e77dfd8e8 100644 --- a/packages/session/session.js +++ b/packages/session/session.js @@ -2,25 +2,25 @@ Session = _.extend({}, { keys: {}, // key -> value - key_deps: {}, // key -> ContextSet - key_value_deps: {}, // key -> value -> ContextSet + keyDeps: {}, // key -> ContextSet + keyValueDeps: {}, // key -> value -> ContextSet set: function (key, value) { var self = this; - var old_value = self.keys[key]; - if (value === old_value) + var oldValue = self.keys[key]; + if (value === oldValue) return; self.keys[key] = value; - var invalidateAll = function (set) { - set && set.invalidateAll(); + var invalidateAll = function (cset) { + cset && cset.invalidateAll(); }; - invalidateAll(self.key_deps[key]); - if (self.key_value_deps[key]) { - invalidateAll(self.key_value_deps[key][old_value]); - invalidateAll(self.key_value_deps[key][value]); + invalidateAll(self.keyDeps[key]); + if (self.keyValueDeps[key]) { + invalidateAll(self.keyValueDeps[key][oldValue]); + invalidateAll(self.keyValueDeps[key][value]); } }, @@ -29,7 +29,7 @@ Session = _.extend({}, { var context = Meteor.deps.Context.current; if (context) { self._ensureKey(key); - self.key_deps[key].add(context); + self.keyDeps[key].add(context); } return self.keys[key]; }, @@ -41,22 +41,23 @@ Session = _.extend({}, { if (typeof value !== 'string' && typeof value !== 'number' && typeof value !== 'boolean' && - value !== null && value !== undefined) + typeof value !== 'undefined' && + value !== null) throw new Error("Session.equals: value can't be an object"); if (context) { self._ensureKey(key); - if (!(value in self.key_value_deps[key])) - self.key_value_deps[key][value] = new Meteor.deps.ContextSet; + if (!(value in self.keyValueDeps[key])) + self.keyValueDeps[key][value] = new Meteor.deps.ContextSet; - var isNew = self.key_value_deps[key][value].add(context); + var isNew = self.keyValueDeps[key][value].add(context); if (isNew) { context.on_invalidate(function () { // clean up [key][value] if it's now empty, so we don't use // O(n) memory for n = values seen ever - if (self.key_value_deps[key][value].isEmpty()) - delete self.key_value_deps[key][value]; + if (self.keyValueDeps[key][value].isEmpty()) + delete self.keyValueDeps[key][value]; }); } } @@ -66,9 +67,9 @@ Session = _.extend({}, { _ensureKey: function (key) { var self = this; - if (!(key in self.key_deps)) { - self.key_deps[key] = new Meteor.deps.ContextSet; - self.key_value_deps[key] = new Meteor.deps.ContextSet; + if (!(key in self.keyDeps)) { + self.keyDeps[key] = new Meteor.deps.ContextSet; + self.keyValueDeps[key] = new Meteor.deps.ContextSet; } } }); From b070f5cf49440d221b97c1dd6c9d7b3520d4d074 Mon Sep 17 00:00:00 2001 From: David Greenspan Date: Sun, 16 Sep 2012 02:42:01 -0700 Subject: [PATCH 03/17] provide contextSet.addCurrentContext() --- packages/deps/deps-utils.js | 11 +++++++++++ packages/session/session.js | 7 ++----- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/packages/deps/deps-utils.js b/packages/deps/deps-utils.js index 3010a0e087..a1020a3b5d 100644 --- a/packages/deps/deps-utils.js +++ b/packages/deps/deps-utils.js @@ -22,6 +22,17 @@ return false; }; + // Adds the current Context to the set, if there is + // one. Returns true if there is a current Context + // and it is new to the set. + ContextSet.prototype.addCurrentContext = function () { + var self = this; + var context = Meteor.deps.Context.current; + if (! context) + return false; + return self.add(context); + }; + // Invalidate all Contexts in the set and remove // them from the set. ContextSet.prototype.invalidateAll = function () { diff --git a/packages/session/session.js b/packages/session/session.js index 3e77dfd8e8..46c681c567 100644 --- a/packages/session/session.js +++ b/packages/session/session.js @@ -26,11 +26,8 @@ Session = _.extend({}, { get: function (key) { var self = this; - var context = Meteor.deps.Context.current; - if (context) { - self._ensureKey(key); - self.keyDeps[key].add(context); - } + self._ensureKey(key); + self.keyDeps[key].addCurrentContext(); return self.keys[key]; }, From 3094db17ff04dc8b2cb7490361d7f77b8a720355 Mon Sep 17 00:00:00 2001 From: David Greenspan Date: Sun, 16 Sep 2012 02:43:09 -0700 Subject: [PATCH 04/17] replace connection status logic with three LoCs --- packages/stream/stream_client.js | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/packages/stream/stream_client.js b/packages/stream/stream_client.js index a4fbbe05ec..dae14aba6d 100644 --- a/packages/stream/stream_client.js +++ b/packages/stream/stream_client.js @@ -45,11 +45,10 @@ Meteor._Stream = function (url) { status: "connecting", connected: false, retry_count: 0 }; - self.status_listeners = {}; // context.id -> context + self.status_listeners = (Meteor.deps && new Meteor.deps.ContextSet); self.status_changed = function () { - _.each(self.status_listeners, function (context) { - context.invalidate(); - }); + if (self.status_listeners) + self.status_listeners.invalidateAll(); }; //// Retry logic @@ -118,13 +117,8 @@ _.extend(Meteor._Stream.prototype, { // Get current status. Reactive. status: function () { var self = this; - var context = Meteor.deps && Meteor.deps.Context.current; - if (context && !(context.id in self.status_listeners)) { - self.status_listeners[context.id] = context; - context.on_invalidate(function () { - delete self.status_listeners[context.id]; - }); - } + if (self.status_listeners) + self.status_listeners.addCurrentContext(); return self.current_status; }, From 9d89feb9ab384717d73724fb9ef1517422d6d559 Mon Sep 17 00:00:00 2001 From: David Greenspan Date: Sun, 16 Sep 2012 02:51:22 -0700 Subject: [PATCH 05/17] use ContextSet in test driver --- packages/test-in-browser/driver.js | 29 ++++++----------------------- 1 file changed, 6 insertions(+), 23 deletions(-) diff --git a/packages/test-in-browser/driver.js b/packages/test-in-browser/driver.js index eb37196ca9..4c31723ba1 100644 --- a/packages/test-in-browser/driver.js +++ b/packages/test-in-browser/driver.js @@ -12,19 +12,12 @@ Meteor.startup(function () { }); Template.test_table.running = function() { - var cx = Meteor.deps.Context.current; - if (cx) { - resultDeps.push(cx); - } - + resultDeps.addCurrentContext(); return running; }; Template.test_table.passed = function() { - var cx = Meteor.deps.Context.current; - if (cx) { - resultDeps.push(cx); - } + resultDeps.addCurrentContext(); // walk whole tree to look for failed tests var walk = function (groups) { @@ -53,10 +46,7 @@ Template.test_table.passed = function() { Template.test_table.total_test_time = function() { - var cx = Meteor.deps.Context.current; - if (cx) { - resultDeps.push(cx); - } + resultDeps.addCurrentContext(); // walk whole tree to get all tests var walk = function (groups) { @@ -79,11 +69,7 @@ Template.test_table.total_test_time = function() { Template.test_table.data = function() { - var cx = Meteor.deps.Context.current; - if (cx) { - resultDeps.push(cx); - } - + resultDeps.addCurrentContext(); return resultTree; }; @@ -185,13 +171,10 @@ Template.event.is_debuggable = function() { var resultTree = []; -var resultDeps = []; +var resultDeps = new Meteor.deps.ContextSet; var _resultsChanged = function() { - _.each(resultDeps, function(cx) { - cx.invalidate(); - }); - resultDeps.length = 0; + resultDeps.invalidateAll(); }; var _testTime = function(t) { From 26acda1e4f31f03b9aee251132cab525cd1278a3 Mon Sep 17 00:00:00 2001 From: David Greenspan Date: Sun, 16 Sep 2012 02:54:02 -0700 Subject: [PATCH 06/17] use ContextSet in ReactiveVar --- packages/test-helpers/reactivevar.js | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/packages/test-helpers/reactivevar.js b/packages/test-helpers/reactivevar.js index cf369255fd..07f7881475 100644 --- a/packages/test-helpers/reactivevar.js +++ b/packages/test-helpers/reactivevar.js @@ -19,19 +19,11 @@ var ReactiveVar = function(initialValue) { this._value = (typeof initialValue === "undefined" ? null : initialValue); - this._deps = {}; + this._deps = new Meteor.deps.ContextSet; }; ReactiveVar.prototype.get = function() { - var context = Meteor.deps.Context.current; - if (context && !(context.id in this._deps)) { - this._deps[context.id] = context; - var self = this; - context.on_invalidate(function() { - delete self._deps[context.id]; - }); - } - + this._deps.addCurrentContext(); return this._value; }; @@ -43,11 +35,9 @@ ReactiveVar.prototype.set = function(newValue) { this._value = newValue; - for(var id in this._deps) - this._deps[id].invalidate(); - + this._deps.invalidateAll(); }; ReactiveVar.prototype.numListeners = function() { - return _.keys(this._deps).length; + return _.keys(this._deps._contextsById).length; }; From efce49331bfb032398a1352c37cee3ef9a4ae81c Mon Sep 17 00:00:00 2001 From: David Greenspan Date: Sun, 16 Sep 2012 02:55:48 -0700 Subject: [PATCH 07/17] include deps-utils in spark.js --- admin/spark-standalone.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/admin/spark-standalone.sh b/admin/spark-standalone.sh index 2b38d4a5a3..e84c3bdbd0 100755 --- a/admin/spark-standalone.sh +++ b/admin/spark-standalone.sh @@ -9,6 +9,7 @@ PACKAGES_DIR=`dirname $0`/../packages echo 'Meteor = {};' cat $PACKAGES_DIR/uuid/uuid.js cat $PACKAGES_DIR/deps/deps.js +cat $PACKAGES_DIR/deps/deps-utils.js cat $PACKAGES_DIR/liverange/liverange.js cat $PACKAGES_DIR/universal-events/listener.js cat $PACKAGES_DIR/universal-events/events-ie.js From d07c760a0dc932cb50851d4270ca88423e370dc6 Mon Sep 17 00:00:00 2001 From: David Greenspan Date: Sun, 16 Sep 2012 03:09:44 -0700 Subject: [PATCH 08/17] docs improvements --- packages/deps/deps-utils.js | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/packages/deps/deps-utils.js b/packages/deps/deps-utils.js index a1020a3b5d..5f71923e90 100644 --- a/packages/deps/deps-utils.js +++ b/packages/deps/deps-utils.js @@ -1,15 +1,17 @@ (function () { // Constructor for an empty ContextSet. + // + // A ContextSet is used to hold a set of Meteor.deps.Contexts that + // are to be invalidated at some future time. If a Context in the + // set becomes invalidated for any reason, it's immediately removed + // from the set. var ContextSet = function () { this._contextsById = {}; }; - // Adds the Context `ctx` to the set (if it is - // not already present). The Context will only - // remain in the set as long as it has not been - // invalidated. - // Returns true if the context was newly added. + // Adds the Context `ctx` to this set if it is not already + // present. Returns true if the context is new to this set. ContextSet.prototype.add = function (ctx) { var self = this; if (ctx && ! (ctx.id in self._contextsById)) { @@ -22,9 +24,8 @@ return false; }; - // Adds the current Context to the set, if there is - // one. Returns true if there is a current Context - // and it is new to the set. + // Adds the current Context to this set if there is one. Returns + // true if there is a current Context and it's new to the set. ContextSet.prototype.addCurrentContext = function () { var self = this; var context = Meteor.deps.Context.current; @@ -33,8 +34,8 @@ return self.add(context); }; - // Invalidate all Contexts in the set and remove - // them from the set. + // Invalidate all Contexts in this set. They will be removed + // from the set as a consequence. ContextSet.prototype.invalidateAll = function () { var self = this; for (var id in self._contextsById) From fb765dd5f3d573a19d9e2a3f204bd3f201c822bc Mon Sep 17 00:00:00 2001 From: David Greenspan Date: Sun, 16 Sep 2012 04:07:49 -0700 Subject: [PATCH 09/17] Meteor.autorun --- .../template-demo/client/template-demo.js | 28 +---------------- packages/deps/deps-utils.js | 30 +++++++++++++++++++ 2 files changed, 31 insertions(+), 27 deletions(-) diff --git a/examples/other/template-demo/client/template-demo.js b/examples/other/template-demo/client/template-demo.js index 0f2f9fe4bd..877f54706d 100644 --- a/examples/other/template-demo/client/template-demo.js +++ b/examples/other/template-demo/client/template-demo.js @@ -137,32 +137,6 @@ Template.timer.destroyed = function () { /////////////////////////////////////////////////////////////////////////////// -// Run f(). Record its dependencies. Rerun it whenever the -// dependencies change. -// -// Returns an object with a stop() method. Call stop() to stop the -// rerunning. -// -// XXX this should go into Meteor core as Meteor.autorun -var autorun = function (f) { - var ctx; - var slain = false; - var rerun = function () { - if (slain) - return; - ctx = new Meteor.deps.Context; - ctx.run(f); - ctx.on_invalidate(rerun); - }; - rerun(); - return { - stop: function () { - slain = true; - ctx.invalidate(); - } - }; -}; - Template.d3Demo.left = function () { return { group: "left" }; }; @@ -230,7 +204,7 @@ Template.circles.rendered = function () { if (! self.handle) { d3.select(self.node).append("rect"); - self.handle = autorun(function () { + self.handle = Meteor.autorun(function () { var circle = d3.select(self.node).selectAll("circle") .data(Circles.find({group: data.group}).fetch(), function (d) { return d._id; }); diff --git a/packages/deps/deps-utils.js b/packages/deps/deps-utils.js index 5f71923e90..3e050f2b02 100644 --- a/packages/deps/deps-utils.js +++ b/packages/deps/deps-utils.js @@ -1,5 +1,7 @@ (function () { + ////////// Meteor.deps.ContextSet + // Constructor for an empty ContextSet. // // A ContextSet is used to hold a set of Meteor.deps.Contexts that @@ -51,4 +53,32 @@ }; Meteor.deps.ContextSet = ContextSet; + + ////////// Meteor.autorun + + // Run f(). Record its dependencies. Rerun it whenever the + // dependencies change. + // + // Returns an object with a stop() method. Call stop() to stop the + // rerunning. Also passes this object as an argument to f. + Meteor.autorun = function (f) { + var ctx; + var slain = false; + var handle = { + stop: function () { + slain = true; + ctx.invalidate(); + } + }; + var rerun = function () { + if (slain) + return; + ctx = new Meteor.deps.Context; + ctx.run(function () { f.call(this, handle); }); + ctx.on_invalidate(rerun); + }; + rerun(); + return handle; + }; + })(); \ No newline at end of file From b11ec664e81f6d3725000ec845c2b2cd7c676365 Mon Sep 17 00:00:00 2001 From: David Greenspan Date: Sun, 16 Sep 2012 04:08:01 -0700 Subject: [PATCH 10/17] Spark.isolate in terms of Meteor.autorun :) --- packages/spark/spark.js | 49 +++++++++++++++++++++-------------------- 1 file changed, 25 insertions(+), 24 deletions(-) diff --git a/packages/spark/spark.js b/packages/spark/spark.js index 8d9a6efc33..d12fa00a8c 100644 --- a/packages/spark/spark.js +++ b/packages/spark/spark.js @@ -767,32 +767,33 @@ Spark.isolate = function (htmlFunc) { if (!renderer) return htmlFunc(); - var ctx = new Meteor.deps.Context; - - return renderer.annotate( - ctx.run(htmlFunc), Spark._ANNOTATION_ISOLATE, function (range) { - range.finalize = function () { - // Spark.finalize() was called on us (presumably because we were - // removed from the document.) Tear down our structures without - // doing any more updates. note that range is about to be - // destroyed by finalize. - range = null; - ctx.invalidate(); - }; - - var refresh = function () { - if (! range) - return; // killed by finalize. range has already been destroyed. - - ctx = new Meteor.deps.Context; - Spark.renderToRange(range, function () { - return ctx.run(htmlFunc); + var range; + var firstRun = true; + var retHtml; + Meteor.autorun(function (handle) { + if (firstRun) { + retHtml = renderer.annotate( + htmlFunc(), Spark._ANNOTATION_ISOLATE, + function (r) { + // annotation used; got a range + range = r; + range.finalize = function () { + // Spark.finalize() was called on our range (presumably + // because it was removed from the document.) Kill + // this context and stop rerunning. + handle.stop(); + }; + }, function () { + // annotation not used; kill our context + handle.stop(); }); - ctx.on_invalidate(refresh); - }; + firstRun = false; + } else { + Spark.renderToRange(range, htmlFunc); + } + }); - ctx.on_invalidate(refresh); - }); + return retHtml; }; /******************************************************************************/ From c96ee60132eb8a2e277fafb45ae984a147947568 Mon Sep 17 00:00:00 2001 From: David Greenspan Date: Sun, 16 Sep 2012 04:23:19 -0700 Subject: [PATCH 11/17] Catch errors in Meteor.flush() --- packages/deps/deps.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/deps/deps.js b/packages/deps/deps.js index d7a7630df4..9f410c954b 100644 --- a/packages/deps/deps.js +++ b/packages/deps/deps.js @@ -59,7 +59,11 @@ _.each(pending, function (ctx) { _.each(ctx._callbacks, function (f) { - f(ctx); // XXX wrap in try? + try { + f(ctx); + } catch (e) { + Meteor._debug("Exception from Meteor.flush:", e); + } }); delete ctx._callbacks; // maybe help the GC }); From bb9f1fe94a010c6c73c1c284fb2d4a8842830a4b Mon Sep 17 00:00:00 2001 From: David Greenspan Date: Sun, 16 Sep 2012 04:23:38 -0700 Subject: [PATCH 12/17] extract Meteor.atFlush from spark --- packages/deps/deps-utils.js | 33 +++++++++++++++++++++++++++++++++ packages/spark/spark.js | 37 +++---------------------------------- 2 files changed, 36 insertions(+), 34 deletions(-) diff --git a/packages/deps/deps-utils.js b/packages/deps/deps-utils.js index 3e050f2b02..47db2e79a9 100644 --- a/packages/deps/deps-utils.js +++ b/packages/deps/deps-utils.js @@ -81,4 +81,37 @@ return handle; }; + ////////// Meteor.atFlush + + // Run 'f' at Meteor.flush()-time. If atFlush is called multiple times, + // we guarantee that the 'f's will run in the same order that + // atFlush was called on them. If we are inside a Meteor.flush() already, + // f will be scheduled as part of the current flush(). + + var atFlushQueue = []; + var atFlushContext = null; + Meteor.atFlush = function (f) { + atFlushQueue.push(f); + + if (! atFlushContext) { + atFlushContext = new Meteor.deps.Context; + atFlushContext.on_invalidate(function () { + var f; + while ((f = atFlushQueue.shift())) { + // Since atFlushContext is truthy, if f() calls atFlush + // reentrantly, it's guaranteed to append to atFlushQueue and + // not contruct a new atFlushContext. + try { + f(); + } catch (e) { + Meteor._debug("Exception from Meteor.atFlush:", e); + } + } + atFlushContext = null; + }); + + atFlushContext.invalidate(); + } + }; + })(); \ No newline at end of file diff --git a/packages/spark/spark.js b/packages/spark/spark.js index d12fa00a8c..9b3f036726 100644 --- a/packages/spark/spark.js +++ b/packages/spark/spark.js @@ -22,7 +22,7 @@ // XXX in landmark-demo, if Template.timer.created throws an exception, // then it is never called again, even if you push the 'create a -// timer' button again. the problem is almost certainly in atFlushTime +// timer' button again. the problem is almost certainly in atFlush // (not hard to see what it is.) (function() { @@ -323,8 +323,7 @@ var scheduleOnscreenSetup = function (frag, landmarkRanges) { finalized = true; }; - var ctx = new Meteor.deps.Context; - ctx.on_invalidate(function () { + Meteor.atFlush(function () { if (finalized) return; @@ -378,8 +377,6 @@ var scheduleOnscreenSetup = function (frag, landmarkRanges) { notifyWatchers(renderedRange.firstNode(), renderedRange.lastNode()); renderedRange.destroy(); }); - - ctx.invalidate(); }; Spark.render = function (htmlFunc) { @@ -800,34 +797,6 @@ Spark.isolate = function (htmlFunc) { /* Lists */ /******************************************************************************/ -// Run 'f' at flush()-time. If atFlushTime is called multiple times, -// we guarantee that the 'f's will run in the order of their -// respective atFlushTime calls. -// -// XXX either break this out into a separate package or fold it into -// deps -var atFlushQueue = []; -var atFlushContext = null; -var atFlushTime = function (f) { - atFlushQueue.push(f); - - if (! atFlushContext) { - atFlushContext = new Meteor.deps.Context; - atFlushContext.on_invalidate(function () { - var f; - while ((f = atFlushQueue.shift())) { - // Since atFlushContext is truthy, if f() calls atFlushTime - // reentrantly, it's guaranteed to append to atFlushQueue and - // not contruct a new atFlushContext. - f(); - } - atFlushContext = null; - }); - - atFlushContext.invalidate(); - } -}; - Spark.list = function (cursor, itemFunc, elseFunc) { elseFunc = elseFunc || function () { return ''; }; @@ -911,7 +880,7 @@ Spark.list = function (cursor, itemFunc, elseFunc) { }; var later = function (f) { - atFlushTime(function () { + Meteor.atFlush(function () { if (! stopped) f(); }); From d8de162bbc5d56efd4c83167444ceadafa7c8e4a Mon Sep 17 00:00:00 2001 From: David Greenspan Date: Sun, 16 Sep 2012 04:37:20 -0700 Subject: [PATCH 13/17] use atFlush in UI test helpers --- packages/test-helpers/onscreendiv.js | 4 +--- packages/test-helpers/wrappedfrag.js | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/test-helpers/onscreendiv.js b/packages/test-helpers/onscreendiv.js index 0495f28abc..0bf7cc0a80 100644 --- a/packages/test-helpers/onscreendiv.js +++ b/packages/test-helpers/onscreendiv.js @@ -50,11 +50,9 @@ OnscreenDiv.prototype.kill = function() { if (self.div.parentNode) self.div.parentNode.removeChild(self.div); - var cx = new Meteor.deps.Context; - cx.on_invalidate(function() { + Meteor.atFlush(function () { Spark.finalize(self.div); }); - cx.invalidate(); }; // remove the DIV from the document diff --git a/packages/test-helpers/wrappedfrag.js b/packages/test-helpers/wrappedfrag.js index 5c45b26dd5..e99ff653f5 100644 --- a/packages/test-helpers/wrappedfrag.js +++ b/packages/test-helpers/wrappedfrag.js @@ -31,13 +31,11 @@ WrappedFrag.prototype.release = function() { // decrement frag's GC protection reference count // Clean up on flush, if hits 0. Wait to decrement // so no one else cleans it up first. - var cx = new Meteor.deps.Context; - cx.on_invalidate(function() { + Meteor.atFlush(function () { if (! --frag["_protect"]) { Spark.finalize(frag); } }); - cx.invalidate(); return this; }; From dcf87f1ffb7c98dacd47809812787b24c5ff854f Mon Sep 17 00:00:00 2001 From: David Greenspan Date: Sun, 16 Sep 2012 05:27:48 -0700 Subject: [PATCH 14/17] Clean up annotate in Spark Annotations that need clean-up were all using the `what` and `unusedFunc` arguments to `annotate` and relying on the fact that one of them is called. Combine them into one callback that's always called if given. --- packages/spark/spark.js | 96 +++++++++++++++++++++++------------------ 1 file changed, 55 insertions(+), 41 deletions(-) diff --git a/packages/spark/spark.js b/packages/spark/spark.js index 9b3f036726..57d708660b 100644 --- a/packages/spark/spark.js +++ b/packages/spark/spark.js @@ -127,9 +127,19 @@ Spark._Renderer = function () { _.extend(Spark._Renderer.prototype, { // `what` can be a function that takes a LiveRange, or just a set of - // attributes to add to the liverange. tag and what are optional. - // if no tag is passed, no liverange will be created. - annotate: function (html, type, what, unusedFunc) { + // attributes to add to the liverange. type and what are optional. + // if no type is passed, no liverange will be created. + // If what is a function, it will be called no matter what, even + // if the annotated HTML was not used and no LiveRange was created, + // in which case it gets null as an argument. + annotate: function (html, type, what) { + if (typeof what !== 'function') { + var attribs = what; + what = function (range) { + if (range) + _.extend(range, attribs); + }; + } // The annotation tags that we insert into HTML strings must be // unguessable in order to not create potential cross-site scripting // attack vectors, so we use random strings. Even a well-written app @@ -139,20 +149,15 @@ _.extend(Spark._Renderer.prototype, { // and not arbitrary user-entered data. var id = (type || '') + ":" + Spark._createId(); this.annotations[id] = function (start, end) { - if (! start) { - // materialize called us with no args because this annotation - // wasn't used - unusedFunc && unusedFunc(); + if ((! start) || (! type)) { + // ! start: materialize called us with no args because this + // annotation wasn't used + // ! type: no type given, don't generate a LiveRange + what(null); return; } - if (! type) - // no type given; don't generate a LiveRange - return; var range = makeRange(type, start, end); - if (what instanceof Function) - what(range); - else - _.extend(range, what); + what(range); }; return "<$" + id + ">" + html + ""; @@ -693,6 +698,9 @@ Spark.attachEvents = withRenderer(function (eventMap, html, _renderer) { html = _renderer.annotate( html, Spark._ANNOTATION_EVENTS, function (range) { + if (! range) + return; + _.each(eventTypes, function (t) { listener.addType(t); }); @@ -772,17 +780,18 @@ Spark.isolate = function (htmlFunc) { retHtml = renderer.annotate( htmlFunc(), Spark._ANNOTATION_ISOLATE, function (r) { - // annotation used; got a range - range = r; - range.finalize = function () { - // Spark.finalize() was called on our range (presumably - // because it was removed from the document.) Kill - // this context and stop rerunning. + if (! r) { + // annotation not used; kill our context handle.stop(); - }; - }, function () { - // annotation not used; kill our context - handle.stop(); + } else { + range = r; + range.finalize = function () { + // Spark.finalize() was called on our range (presumably + // because it was removed from the document.) Kill + // this context and stop rerunning. + handle.stop(); + }; + } }); firstRun = false; } else { @@ -824,8 +833,8 @@ Spark.list = function (cursor, itemFunc, elseFunc) { // Get the renderer, if any var renderer = Spark._currentRenderer.get(); - var annotate = renderer ? - _.bind(renderer.annotate, renderer) : + var maybeAnnotate = renderer ? + _.bind(renderer.annotate, renderer) : function (html) { return html; }; // Render the initial contents. If we have a renderer, create a @@ -839,11 +848,11 @@ Spark.list = function (cursor, itemFunc, elseFunc) { else { for (var i = 0; i < initialContents.length; i++) { (function (i) { - html += annotate(itemFunc(initialContents[i]), - Spark._ANNOTATION_LIST_ITEM, - function (range) { - itemRanges[i] = range; - }); + html += maybeAnnotate(itemFunc(initialContents[i]), + Spark._ANNOTATION_LIST_ITEM, + function (range) { + itemRanges[i] = range; + }); })(i); // scope i to closure } } @@ -853,13 +862,15 @@ Spark.list = function (cursor, itemFunc, elseFunc) { handle.stop(); stopped = true; }; - html = annotate(html, Spark._ANNOTATION_LIST, function (range) { - outerRange = range; - outerRange.finalize = cleanup; - }, function () { - // We never ended up on the screen (caller discarded our return - // value) - cleanup(); + html = maybeAnnotate(html, Spark._ANNOTATION_LIST, function (range) { + if (! range) { + // We never ended up on the screen (caller discarded our return + // value) + cleanup(); + } else { + outerRange = range; + outerRange.finalize = cleanup; + } }); // No renderer? Then we have no way to update the returned html and @@ -1074,6 +1085,12 @@ Spark.createLandmark = function (options, htmlFunc) { var html = htmlFunc(landmark); return renderer.annotate( html, Spark._ANNOTATION_LANDMARK, function (range) { + if (! range) { + // annotation not used + options.destroyed && options.destroyed.call(landmark); + return; + } + _.extend(range, { preserve: preserve, constant: !! options.constant, @@ -1090,9 +1107,6 @@ Spark.createLandmark = function (options, htmlFunc) { landmark._range = range; renderer.landmarkRanges.push(range); - }, function () { - // "annotation not used" callback - options.destroyed && options.destroyed.call(landmark); }); }; From dcb399533fd4b13f4080f55be09517f97f59541f Mon Sep 17 00:00:00 2001 From: David Greenspan Date: Sun, 16 Sep 2012 14:19:06 -0700 Subject: [PATCH 15/17] fix Session --- packages/session/session.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/session/session.js b/packages/session/session.js index 46c681c567..3e9bd39438 100644 --- a/packages/session/session.js +++ b/packages/session/session.js @@ -66,7 +66,7 @@ Session = _.extend({}, { var self = this; if (!(key in self.keyDeps)) { self.keyDeps[key] = new Meteor.deps.ContextSet; - self.keyValueDeps[key] = new Meteor.deps.ContextSet; + self.keyValueDeps[key] = {}; } } }); From 6ac7b274101b80c998bd4be6e634d9a5f61926a1 Mon Sep 17 00:00:00 2001 From: David Greenspan Date: Mon, 17 Sep 2012 18:04:16 -0700 Subject: [PATCH 16/17] camelCase onInvalidate --- packages/deps/deps-utils.js | 6 +++--- packages/session/session.js | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/deps/deps-utils.js b/packages/deps/deps-utils.js index 47db2e79a9..f9a350be5c 100644 --- a/packages/deps/deps-utils.js +++ b/packages/deps/deps-utils.js @@ -18,7 +18,7 @@ var self = this; if (ctx && ! (ctx.id in self._contextsById)) { self._contextsById[ctx.id] = ctx; - ctx.on_invalidate(function () { + ctx.onInvalidate(function () { delete self._contextsById[ctx.id]; }); return true; @@ -75,7 +75,7 @@ return; ctx = new Meteor.deps.Context; ctx.run(function () { f.call(this, handle); }); - ctx.on_invalidate(rerun); + ctx.onInvalidate(rerun); }; rerun(); return handle; @@ -95,7 +95,7 @@ if (! atFlushContext) { atFlushContext = new Meteor.deps.Context; - atFlushContext.on_invalidate(function () { + atFlushContext.onInvalidate(function () { var f; while ((f = atFlushQueue.shift())) { // Since atFlushContext is truthy, if f() calls atFlush diff --git a/packages/session/session.js b/packages/session/session.js index 3e9bd39438..dd2cea2938 100644 --- a/packages/session/session.js +++ b/packages/session/session.js @@ -50,7 +50,7 @@ Session = _.extend({}, { var isNew = self.keyValueDeps[key][value].add(context); if (isNew) { - context.on_invalidate(function () { + context.onInvalidate(function () { // clean up [key][value] if it's now empty, so we don't use // O(n) memory for n = values seen ever if (self.keyValueDeps[key][value].isEmpty()) From 20e29d2851fb9b9fc4f2b4b8e534d0727b75e2bf Mon Sep 17 00:00:00 2001 From: David Glasser Date: Fri, 5 Oct 2012 15:26:22 -0700 Subject: [PATCH 17/17] Make everything in deps-utils private for now. --- .../template-demo/client/template-demo.js | 2 +- packages/deps/deps-utils.js | 29 ++++++++++--------- packages/session/session.js | 8 ++--- packages/spark/spark.js | 6 ++-- packages/stream/stream_client.js | 2 +- packages/test-helpers/onscreendiv.js | 2 +- packages/test-helpers/reactivevar.js | 2 +- packages/test-helpers/wrappedfrag.js | 2 +- packages/test-in-browser/driver.js | 2 +- 9 files changed, 28 insertions(+), 27 deletions(-) diff --git a/examples/other/template-demo/client/template-demo.js b/examples/other/template-demo/client/template-demo.js index 877f54706d..f498443ff8 100644 --- a/examples/other/template-demo/client/template-demo.js +++ b/examples/other/template-demo/client/template-demo.js @@ -204,7 +204,7 @@ Template.circles.rendered = function () { if (! self.handle) { d3.select(self.node).append("rect"); - self.handle = Meteor.autorun(function () { + self.handle = Meteor._autorun(function () { var circle = d3.select(self.node).selectAll("circle") .data(Circles.find({group: data.group}).fetch(), function (d) { return d._id; }); diff --git a/packages/deps/deps-utils.js b/packages/deps/deps-utils.js index f9a350be5c..569d2deadd 100644 --- a/packages/deps/deps-utils.js +++ b/packages/deps/deps-utils.js @@ -1,20 +1,21 @@ (function () { + // XXX Document, test, and remove the leading underscore from everything. - ////////// Meteor.deps.ContextSet + ////////// Meteor.deps._ContextSet - // Constructor for an empty ContextSet. + // Constructor for an empty _ContextSet. // - // A ContextSet is used to hold a set of Meteor.deps.Contexts that + // A _ContextSet is used to hold a set of Meteor.deps.Contexts that // are to be invalidated at some future time. If a Context in the // set becomes invalidated for any reason, it's immediately removed // from the set. - var ContextSet = function () { + var _ContextSet = function () { this._contextsById = {}; }; // Adds the Context `ctx` to this set if it is not already // present. Returns true if the context is new to this set. - ContextSet.prototype.add = function (ctx) { + _ContextSet.prototype.add = function (ctx) { var self = this; if (ctx && ! (ctx.id in self._contextsById)) { self._contextsById[ctx.id] = ctx; @@ -28,7 +29,7 @@ // Adds the current Context to this set if there is one. Returns // true if there is a current Context and it's new to the set. - ContextSet.prototype.addCurrentContext = function () { + _ContextSet.prototype.addCurrentContext = function () { var self = this; var context = Meteor.deps.Context.current; if (! context) @@ -38,30 +39,30 @@ // Invalidate all Contexts in this set. They will be removed // from the set as a consequence. - ContextSet.prototype.invalidateAll = function () { + _ContextSet.prototype.invalidateAll = function () { var self = this; for (var id in self._contextsById) self._contextsById[id].invalidate(); }; // Returns true if there are no Contexts in this set. - ContextSet.prototype.isEmpty = function () { + _ContextSet.prototype.isEmpty = function () { var self = this; for(var id in self._contextsById) return false; return true; }; - Meteor.deps.ContextSet = ContextSet; + Meteor.deps._ContextSet = _ContextSet; - ////////// Meteor.autorun + ////////// Meteor._autorun // Run f(). Record its dependencies. Rerun it whenever the // dependencies change. // // Returns an object with a stop() method. Call stop() to stop the // rerunning. Also passes this object as an argument to f. - Meteor.autorun = function (f) { + Meteor._autorun = function (f) { var ctx; var slain = false; var handle = { @@ -81,7 +82,7 @@ return handle; }; - ////////// Meteor.atFlush + ////////// Meteor._atFlush // Run 'f' at Meteor.flush()-time. If atFlush is called multiple times, // we guarantee that the 'f's will run in the same order that @@ -90,7 +91,7 @@ var atFlushQueue = []; var atFlushContext = null; - Meteor.atFlush = function (f) { + Meteor._atFlush = function (f) { atFlushQueue.push(f); if (! atFlushContext) { @@ -104,7 +105,7 @@ try { f(); } catch (e) { - Meteor._debug("Exception from Meteor.atFlush:", e); + Meteor._debug("Exception from Meteor._atFlush:", e); } } atFlushContext = null; diff --git a/packages/session/session.js b/packages/session/session.js index c9bd5fa3ff..c55d5b39e5 100644 --- a/packages/session/session.js +++ b/packages/session/session.js @@ -2,8 +2,8 @@ Session = _.extend({}, { keys: {}, // key -> value - keyDeps: {}, // key -> ContextSet - keyValueDeps: {}, // key -> value -> ContextSet + keyDeps: {}, // key -> _ContextSet + keyValueDeps: {}, // key -> value -> _ContextSet set: function (key, value) { var self = this; @@ -52,7 +52,7 @@ Session = _.extend({}, { self._ensureKey(key); if (!(value in self.keyValueDeps[key])) - self.keyValueDeps[key][value] = new Meteor.deps.ContextSet; + self.keyValueDeps[key][value] = new Meteor.deps._ContextSet; var isNew = self.keyValueDeps[key][value].add(context); if (isNew) { @@ -71,7 +71,7 @@ Session = _.extend({}, { _ensureKey: function (key) { var self = this; if (!(key in self.keyDeps)) { - self.keyDeps[key] = new Meteor.deps.ContextSet; + self.keyDeps[key] = new Meteor.deps._ContextSet; self.keyValueDeps[key] = {}; } } diff --git a/packages/spark/spark.js b/packages/spark/spark.js index 1dfb172b61..ee4d0b69be 100644 --- a/packages/spark/spark.js +++ b/packages/spark/spark.js @@ -345,7 +345,7 @@ var scheduleOnscreenSetup = function (frag, landmarkRanges) { finalized = true; }; - Meteor.atFlush(function () { + Meteor._atFlush(function () { if (finalized) return; @@ -795,7 +795,7 @@ Spark.isolate = function (htmlFunc) { var range; var firstRun = true; var retHtml; - Meteor.autorun(function (handle) { + Meteor._autorun(function (handle) { if (firstRun) { retHtml = renderer.annotate( htmlFunc(), Spark._ANNOTATION_ISOLATE, @@ -911,7 +911,7 @@ Spark.list = function (cursor, itemFunc, elseFunc) { }; var later = function (f) { - Meteor.atFlush(function () { + Meteor._atFlush(function () { if (! stopped) withEventGuard(f); }); diff --git a/packages/stream/stream_client.js b/packages/stream/stream_client.js index 513e2b38ec..6b03f33c76 100644 --- a/packages/stream/stream_client.js +++ b/packages/stream/stream_client.js @@ -47,7 +47,7 @@ Meteor._Stream = function (url) { retry_count: 0 }; - self.status_listeners = (Meteor.deps && new Meteor.deps.ContextSet); + self.status_listeners = (Meteor.deps && new Meteor.deps._ContextSet); self.status_changed = function () { if (self.status_listeners) self.status_listeners.invalidateAll(); diff --git a/packages/test-helpers/onscreendiv.js b/packages/test-helpers/onscreendiv.js index 0bf7cc0a80..b594627e2b 100644 --- a/packages/test-helpers/onscreendiv.js +++ b/packages/test-helpers/onscreendiv.js @@ -50,7 +50,7 @@ OnscreenDiv.prototype.kill = function() { if (self.div.parentNode) self.div.parentNode.removeChild(self.div); - Meteor.atFlush(function () { + Meteor._atFlush(function () { Spark.finalize(self.div); }); }; diff --git a/packages/test-helpers/reactivevar.js b/packages/test-helpers/reactivevar.js index 07f7881475..0c789663ef 100644 --- a/packages/test-helpers/reactivevar.js +++ b/packages/test-helpers/reactivevar.js @@ -19,7 +19,7 @@ var ReactiveVar = function(initialValue) { this._value = (typeof initialValue === "undefined" ? null : initialValue); - this._deps = new Meteor.deps.ContextSet; + this._deps = new Meteor.deps._ContextSet; }; ReactiveVar.prototype.get = function() { diff --git a/packages/test-helpers/wrappedfrag.js b/packages/test-helpers/wrappedfrag.js index e99ff653f5..f019bf2fea 100644 --- a/packages/test-helpers/wrappedfrag.js +++ b/packages/test-helpers/wrappedfrag.js @@ -31,7 +31,7 @@ WrappedFrag.prototype.release = function() { // decrement frag's GC protection reference count // Clean up on flush, if hits 0. Wait to decrement // so no one else cleans it up first. - Meteor.atFlush(function () { + Meteor._atFlush(function () { if (! --frag["_protect"]) { Spark.finalize(frag); } diff --git a/packages/test-in-browser/driver.js b/packages/test-in-browser/driver.js index 4c31723ba1..0a50601299 100644 --- a/packages/test-in-browser/driver.js +++ b/packages/test-in-browser/driver.js @@ -171,7 +171,7 @@ Template.event.is_debuggable = function() { var resultTree = []; -var resultDeps = new Meteor.deps.ContextSet; +var resultDeps = new Meteor.deps._ContextSet; var _resultsChanged = function() { resultDeps.invalidateAll();