Merge branch 'deps-utils-private' into devel

This adds three deps utilities, which are currently undocumented and internal:
Meteor._ContextSet, Meteor._autorun, and Meteor._atFlush.
This commit is contained in:
David Glasser
2012-10-06 11:06:58 -07:00
12 changed files with 241 additions and 229 deletions

View File

@@ -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

View File

@@ -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.onInvalidate(rerun);
};
rerun();
return {
stop: function () {
slain = true;
ctx.invalidate();
}
};
};
Template.d3Demo.left = function () {
return { group: "left" };
};
@@ -233,7 +207,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; });

118
packages/deps/deps-utils.js Normal file
View File

@@ -0,0 +1,118 @@
(function () {
// XXX Document, test, and remove the leading underscore from everything.
////////// Meteor.deps._ContextSet
// 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 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)) {
self._contextsById[ctx.id] = ctx;
ctx.onInvalidate(function () {
delete self._contextsById[ctx.id];
});
return true;
}
return false;
};
// 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;
if (! context)
return false;
return self.add(context);
};
// 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)
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;
////////// 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.onInvalidate(rerun);
};
rerun();
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.onInvalidate(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();
}
};
})();

View File

@@ -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
});

View File

@@ -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);
});

View File

@@ -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
keyDeps: {}, // key -> _ContextSet
keyValueDeps: {}, // key -> value -> _ContextSet
set: function (key, value) {
var self = this;
@@ -36,35 +14,26 @@ Session = _.extend({}, {
value !== null && value !== undefined)
throw new Error("Session.set: value can't be an object");
var old_value = self.keys[key];
if (value === old_value)
var oldValue = self.keys[key];
if (value === oldValue)
return;
self.keys[key] = value;
var invalidate = function (map) {
if (map)
for (var id in map)
map[id].invalidate();
var invalidateAll = function (cset) {
cset && cset.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.keyDeps[key]);
if (self.keyValueDeps[key]) {
invalidateAll(self.keyValueDeps[key][oldValue]);
invalidateAll(self.keyValueDeps[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.onInvalidate(function () {
delete self.key_deps[key][context.id];
});
}
self.keyDeps[key].addCurrentContext();
return self.keys[key];
},
@@ -75,24 +44,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] = {};
if (!(context.id in self.key_value_deps[key][value])) {
self.key_value_deps[key][value][context.id] = context;
if (!(value in self.keyValueDeps[key]))
self.keyValueDeps[key][value] = new Meteor.deps._ContextSet;
var isNew = self.keyValueDeps[key][value].add(context);
if (isNew) {
context.onInvalidate(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.keyValueDeps[key][value].isEmpty())
delete self.keyValueDeps[key][value];
});
}
}
@@ -102,9 +70,9 @@ Session = _.extend({}, {
_ensureKey: function (key) {
var self = this;
if (!(key in self.key_deps)) {
self.key_deps[key] = {};
self.key_value_deps[key] = {};
if (!(key in self.keyDeps)) {
self.keyDeps[key] = new Meteor.deps._ContextSet;
self.keyValueDeps[key] = {};
}
}
});

View File

@@ -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() {
@@ -144,9 +144,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
@@ -156,20 +166,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 + "</$" + id + ">";
@@ -340,8 +345,7 @@ var scheduleOnscreenSetup = function (frag, landmarkRanges) {
finalized = true;
};
var ctx = new Meteor.deps.Context;
ctx.onInvalidate(function () {
Meteor._atFlush(function () {
if (finalized)
return;
@@ -395,8 +399,6 @@ var scheduleOnscreenSetup = function (frag, landmarkRanges) {
notifyWatchers(renderedRange.firstNode(), renderedRange.lastNode());
renderedRange.destroy();
});
ctx.invalidate();
};
Spark.render = function (htmlFunc) {
@@ -721,6 +723,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);
});
@@ -787,66 +792,40 @@ 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) {
if (! r) {
// 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();
};
}
});
ctx.onInvalidate(refresh);
};
firstRun = false;
} else {
Spark.renderToRange(range, htmlFunc);
}
});
ctx.onInvalidate(refresh);
});
return retHtml;
};
/******************************************************************************/
/* 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.onInvalidate(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 ''; };
@@ -874,8 +853,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
@@ -889,11 +868,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
}
}
@@ -903,13 +882,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
@@ -930,7 +911,7 @@ Spark.list = function (cursor, itemFunc, elseFunc) {
};
var later = function (f) {
atFlushTime(function () {
Meteor._atFlush(function () {
if (! stopped)
withEventGuard(f);
});
@@ -1124,6 +1105,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,
@@ -1140,9 +1127,6 @@ Spark.createLandmark = function (options, htmlFunc) {
landmark._range = range;
renderer.landmarkRanges.push(range);
}, function () {
// "annotation not used" callback
options.destroyed && options.destroyed.call(landmark);
});
};

View File

@@ -47,11 +47,10 @@ Meteor._Stream = function (url) {
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
@@ -120,13 +119,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.onInvalidate(function () {
delete self.status_listeners[context.id];
});
}
if (self.status_listeners)
self.status_listeners.addCurrentContext();
return self.current_status;
},

View File

@@ -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.onInvalidate(function() {
Meteor._atFlush(function () {
Spark.finalize(self.div);
});
cx.invalidate();
};
// remove the DIV from the document

View File

@@ -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.onInvalidate(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;
};

View File

@@ -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.onInvalidate(function() {
Meteor._atFlush(function () {
if (! --frag["_protect"]) {
Spark.finalize(frag);
}
});
cx.invalidate();
return this;
};

View File

@@ -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) {