diff --git a/packages/deps/deps.js b/packages/deps/deps.js index 45597260da..852cc74f46 100644 --- a/packages/deps/deps.js +++ b/packages/deps/deps.js @@ -46,18 +46,24 @@ throw new Error( "Deps.Computation constructor is private; use Deps.run"); + this.stopped = false; + this.invalidated = false; + this.active = false; + this.firstRun = true; + this._id = nextId++; this._callbacks = { onInvalidate: [], afterInvalidate: [] }; - this.stopped = false; - this.invalidated = false; - this.active = false; this._parent = null; // set in Deps.run; for future use this._func = (f || function () {}); - this._run(); + try { + this._run(); + } finally { + this.firstRun = false; + } }; _.extend(Deps.Computation.prototype, { @@ -65,7 +71,7 @@ onInvalidate: function (f) { if (! this.active) throw new Error( - "Can only register callbacks with an active Computation"); + "Can only register callbacks on an active Computation"); this._callbacks.onInvalidate.push(f); }, @@ -73,31 +79,36 @@ afterInvalidate: function (f) { if (! this.active) throw new Error( - "Can only register callbacks with an active Computation"); + "Can only register callbacks on an active Computation"); this._callbacks.afterInvalidate.push(f); }, invalidate: function () { if (! this.invalidated) { - pendingComputations.push(this); - requireFlush(); + if (! this.active) + // an active computation is enqueued at + // end of _run instead. + this._enqueue(); this.invalidated = true; } }, stop: function () { if (! this.stopped) { - if (! this.invalidated) { - requireFlush(); - pendingComputations.push(this); - } - this.invalidated = true; + this.invalidate(); this.stopped = true; } }, + _enqueue: function () { + requireFlush(); + pendingComputations.push(this); + }, + _run: function () { + this.invalidated = false; + var previous = Deps.currentComputation; Deps.currentComputation = this; Deps.active = true; @@ -109,41 +120,43 @@ Deps.currentComputation = previous; Deps.active = !! Deps.currentComputation; } + + if (this.invalidated) + this._enqueue(); }, - _callCallbacks: function (which) { - var self = this; - var callbacks = self._callbacks; + _process: function () { + while (this.invalidated) { + var onInvalidateCallbacks = this._callbacks.onInvalidate; + this._callbacks.onInvalidate = []; + var afterInvalidateCallbacks = this._callbacks.afterInvalidate; + this._callbacks.afterInvalidate = []; - // call funcs in callbacks[which] in order, allowing - // for new ones that might come along during the loop. - while (callbacks[which].length) { - var funcs = callbacks[which]; - callbacks[which] = []; - - for(var i = 0, f; f = funcs[i]; i++) { + for(var i = 0, f; f = onInvalidateCallbacks[i]; i++) { try { - f(self); + f(this); } catch (e) { _debugFunc()("Exception from Deps invalidation callback:", e.stack); } } - } - }, - _process: function () { - while (this.invalidated) { - this._callCallbacks('onInvalidate'); if (! this.stopped) { try { this._run(); } catch (e) { _debugFunc()("Exception from Deps rerun:", e.stack); } - this.invalidated = false; } - this._callCallbacks('afterInvalidate'); + + for(var i = 0, f; f = afterInvalidateCallbacks[i]; i++) { + try { + f(this); + } catch (e) { + _debugFunc()("Exception from Deps invalidation callback:", + e.stack); + } + } if (this.stopped) break; @@ -207,20 +220,31 @@ // // https://app.asana.com/0/159908330244/385138233856 if (inFlush) { - _debugFunc()("Warning: Ignored nested Deps.flush"); + // note: consider removing this warning if it comes up + // in legit uses of flush and is annoying. + _debugFunc()("Warning: Ignored nested Deps.flush:", + (new Error).stack); return; } inFlush = true; willFlush = true; - while (pendingComputations.length) { - var comps = pendingComputations; - pendingComputations = []; + // It's possible for Computations to be active, + // if we are in an enclosing Deps.run in its + // first run (i.e. not called from flush). + // Keep one from being currentComputation. + Deps.nonreactive(function () { - for (var i = 0, comp; comp = comps[i]; i++) - comp._process(); - } + while (pendingComputations.length) { + var comps = pendingComputations; + pendingComputations = []; + + for (var i = 0, comp; comp = comps[i]; i++) + comp._process(); + } + + }); inFlush = false; willFlush = false; diff --git a/packages/deps/deps_tests.js b/packages/deps/deps_tests.js index 3f4325abff..43c65fc735 100644 --- a/packages/deps/deps_tests.js +++ b/packages/deps/deps_tests.js @@ -55,7 +55,7 @@ Tinytest.add("deps - nested run", function (test) { var buf = ""; - var c1 = new Deps.Computation(function () { + var c1 = Deps._newComputation(function () { Deps.depend(a); buf += 'a'; Deps.run(function () { @@ -64,7 +64,7 @@ Tinytest.add("deps - nested run", function (test) { Deps.run(function () { Deps.depend(c); buf += 'c'; - var c2 = new Deps.Computation(function () { + var c2 = Deps._newComputation(function () { Deps.depend(d); buf += 'd'; Deps.run(function () { @@ -149,3 +149,82 @@ Tinytest.add("deps - nested run", function (test) { test.isFalse(e.hasDependents()); test.isFalse(f.hasDependents()); }); + +Tinytest.add("deps - flush", function (test) { + + var buf = ""; + + var c1 = Deps.run(function (c) { + buf += 'a'; + // invalidate first time + if (c.firstRun) + c.invalidate(); + }); + + test.equal(buf, 'a'); + Deps.flush(); + test.equal(buf, 'aa'); + Deps.flush(); + test.equal(buf, 'aa'); + c1.stop(); + Deps.flush(); + test.equal(buf, 'aa'); + + ///// + // Can't cause rerun nested in run + + buf = ""; + + var c2 = Deps.run(function (c) { + buf += 'a'; + // invalidate first time + if (c.firstRun) + c.invalidate(); + + Deps.onInvalidate(function () { + buf += "<"; + }); + Deps.afterInvalidate(function () { + buf += ">"; + }); + + if (c.firstRun) + Meteor.flush(); + }); + + test.equal(buf, 'a'); + Deps.flush(); + test.equal(buf, 'a'); + c2.stop(); + Deps.flush(); + test.equal(buf, 'a<>'); + + ///// + // Can flush a diferent run from a run; + // no current computation in onInvalidate + + buf = ""; + + var c3 = Deps.run(function (c) { + buf += 'a'; + // invalidate first time + if (c.firstRun) + c.invalidate(); + Deps.onInvalidate(function () { + buf += (Deps.active ? "1" : "0"); + }); + }); + + var c4 = Deps.run(function (c) { + c4 = c; + buf += 'b'; + Meteor.flush(); + buf += 'b'; + }); + + test.equal(buf, 'ab0ab'); + c3.stop(); + c4.stop(); + Deps.flush(); + +}); \ No newline at end of file