Got rid of self (sic!).

This commit is contained in:
Radosław Miernik
2017-07-12 00:18:20 +02:00
parent d01c0bbf88
commit e1ef1e2984
4 changed files with 225 additions and 290 deletions

View File

@@ -4,39 +4,36 @@ import {LocalCollection} from './local_collection.js';
// a defined order, limit, and offset. creating a Cursor with LocalCollection.find(),
export class Cursor {
// don't call this ctor directly. use LocalCollection.find().
constructor (collection, selector, options) {
const self = this;
if (!options) options = {};
self.collection = collection;
self.sorter = null;
self.matcher = new Minimongo.Matcher(selector);
constructor (collection, selector, options = {}) {
this.collection = collection;
this.sorter = null;
this.matcher = new Minimongo.Matcher(selector);
if (LocalCollection._selectorIsId(selector)) {
// stash for fast path
self._selectorId = selector;
this._selectorId = selector;
} else if (LocalCollection._selectorIsIdPerhapsAsObject(selector)) {
// also do the fast path for { _id: idString }
self._selectorId = selector._id;
this._selectorId = selector._id;
} else {
self._selectorId = undefined;
if (self.matcher.hasGeoQuery() || options.sort) {
self.sorter = new Minimongo.Sorter(options.sort || [],
{ matcher: self.matcher });
this._selectorId = undefined;
if (this.matcher.hasGeoQuery() || options.sort) {
this.sorter = new Minimongo.Sorter(options.sort || [],
{ matcher: this.matcher });
}
}
self.skip = options.skip;
self.limit = options.limit;
self.fields = options.fields;
this.skip = options.skip;
this.limit = options.limit;
this.fields = options.fields;
self._projectionFn = LocalCollection._compileProjection(self.fields || {});
this._projectionFn = LocalCollection._compileProjection(this.fields || {});
self._transform = LocalCollection.wrapTransform(options.transform);
this._transform = LocalCollection.wrapTransform(options.transform);
// by default, queries register w/ Tracker when it is available.
if (typeof Tracker !== "undefined")
self.reactive = (options.reactive === undefined) ? true : options.reactive;
this.reactive = (options.reactive === undefined) ? true : options.reactive;
}
/**
@@ -48,13 +45,11 @@ export class Cursor {
* @returns {Number}
*/
count () {
const self = this;
if (self.reactive)
self._depend({added: true, removed: true},
if (this.reactive)
this._depend({added: true, removed: true},
true /* allow the observe to be unordered */);
return self._getRawObjects({ordered: true}).length;
return this._getRawObjects({ordered: true}).length;
}
/**
@@ -66,9 +61,8 @@ export class Cursor {
* @returns {Object[]}
*/
fetch () {
const self = this;
const res = [];
self.forEach(doc => {
this.forEach(doc => {
res.push(doc);
});
return res;
@@ -89,12 +83,10 @@ export class Cursor {
* @param {Any} [thisArg] An object which will be the value of `this` inside `callback`.
*/
forEach (callback, thisArg) {
const self = this;
const objects = this._getRawObjects({ordered: true});
const objects = self._getRawObjects({ordered: true});
if (self.reactive) {
self._depend({
if (this.reactive) {
this._depend({
addedBefore: true,
removed: true,
changed: true,
@@ -103,11 +95,11 @@ export class Cursor {
objects.forEach((elt, i) => {
// This doubles as a clone operation.
elt = self._projectionFn(elt);
elt = this._projectionFn(elt);
if (self._transform)
elt = self._transform(elt);
callback.call(thisArg, elt, i, self);
if (this._transform)
elt = this._transform(elt);
callback.call(thisArg, elt, i, this);
});
}
@@ -125,10 +117,9 @@ export class Cursor {
* @param {Any} [thisArg] An object which will be the value of `this` inside `callback`.
*/
map (callback, thisArg) {
const self = this;
const res = [];
self.forEach((doc, index) => {
res.push(callback.call(thisArg, doc, index, self));
this.forEach((doc, index) => {
res.push(callback.call(thisArg, doc, index, this));
});
return res;
}
@@ -162,8 +153,7 @@ export class Cursor {
* @param {Object} callbacks Functions to call to deliver the result set as it changes
*/
observe (options) {
const self = this;
return LocalCollection._observeFromObserveChanges(self, options);
return LocalCollection._observeFromObserveChanges(this, options);
}
/**
@@ -174,42 +164,40 @@ export class Cursor {
* @param {Object} callbacks Functions to call to deliver the result set as it changes
*/
observeChanges (options) {
const self = this;
const ordered = LocalCollection._observeChangesCallbacksAreOrdered(options);
// there are several places that assume you aren't combining skip/limit with
// unordered observe. eg, update's EJSON.clone, and the "there are several"
// comment in _modifyAndNotify
// XXX allow skip/limit with unordered observe
if (!options._allow_unordered && !ordered && (self.skip || self.limit))
if (!options._allow_unordered && !ordered && (this.skip || this.limit))
throw new Error("must use ordered observe (ie, 'addedBefore' instead of 'added') with skip or limit");
if (self.fields && (self.fields._id === 0 || self.fields._id === false))
if (this.fields && (this.fields._id === 0 || this.fields._id === false))
throw Error("You may not observe a cursor with {fields: {_id: 0}}");
const query = {
dirty: false,
matcher: self.matcher, // not fast pathed
sorter: ordered && self.sorter,
matcher: this.matcher, // not fast pathed
sorter: ordered && this.sorter,
distances: (
self.matcher.hasGeoQuery() && ordered && new LocalCollection._IdMap),
this.matcher.hasGeoQuery() && ordered && new LocalCollection._IdMap),
resultsSnapshot: null,
ordered,
cursor: self,
projectionFn: self._projectionFn
cursor: this,
projectionFn: this._projectionFn
};
let qid;
// Non-reactive queries call added[Before] and then never call anything
// else.
if (self.reactive) {
qid = self.collection.next_qid++;
self.collection.queries[qid] = query;
if (this.reactive) {
qid = this.collection.next_qid++;
this.collection.queries[qid] = query;
}
query.results = self._getRawObjects({
query.results = this._getRawObjects({
ordered, distances: query.distances});
if (self.collection.paused)
if (this.collection.paused)
query.resultsSnapshot = (ordered ? [] : new LocalCollection._IdMap);
// wrap callbacks we were passed. callbacks only fire when not paused and
@@ -222,15 +210,15 @@ export class Cursor {
const wrapCallback = f => {
if (!f)
return () => {};
const self = this;
return function (/*args*/) {
const context = this;
const args = arguments;
if (self.collection.paused)
return;
self.collection._observeQueue.queueTask(() => {
f.apply(context, args);
f.apply(this, args);
});
};
};
@@ -242,7 +230,7 @@ export class Cursor {
query.movedBefore = wrapCallback(options.movedBefore);
}
if (!options._suppress_initial && !self.collection.paused) {
if (!options._suppress_initial && !this.collection.paused) {
const results = query.results._map || query.results;
Object.keys(results).forEach(key => {
const doc = results[key];
@@ -250,21 +238,21 @@ export class Cursor {
delete fields._id;
if (ordered)
query.addedBefore(doc._id, self._projectionFn(fields), null);
query.added(doc._id, self._projectionFn(fields));
query.addedBefore(doc._id, this._projectionFn(fields), null);
query.added(doc._id, this._projectionFn(fields));
});
}
const handle = new LocalCollection.ObserveHandle;
Object.assign(handle, {
collection: self.collection,
stop() {
if (self.reactive)
delete self.collection.queries[qid];
collection: this.collection,
stop: () => {
if (this.reactive)
delete this.collection.queries[qid];
}
});
if (self.reactive && Tracker.active) {
if (this.reactive && Tracker.active) {
// XXX in many cases, the same observe will be recreated when
// the current autorun is rerun. we could save work by
// letting it linger across rerun and potentially get
@@ -276,7 +264,7 @@ export class Cursor {
}
// run the observe callbacks resulting from the initial contents
// before we leave the observe.
self.collection._observeQueue.drain();
this.collection._observeQueue.drain();
return handle;
}
@@ -290,8 +278,6 @@ export class Cursor {
// XXX Maybe we need a version of observe that just calls a callback if
// anything changed.
_depend (changers, _allow_unordered) {
const self = this;
if (Tracker.active) {
const v = new Tracker.Dependency;
v.depend();
@@ -307,13 +293,12 @@ export class Cursor {
});
// observeChanges will stop() when this computation is invalidated
self.observeChanges(options);
this.observeChanges(options);
}
}
_getCollectionName () {
const self = this;
return self.collection.name;
return this.collection.name;
}
// Returns a collection of matching objects, but doesn't deep copy them.
@@ -331,28 +316,25 @@ export class Cursor {
// argument, this function will clear it and use it for this purpose (otherwise
// it will just create its own _IdMap). The observeChanges implementation uses
// this to remember the distances after this function returns.
_getRawObjects (options) {
const self = this;
options = options || {};
_getRawObjects (options = {}) {
// XXX use OrderedDict instead of array, and make IdMap and OrderedDict
// compatible
const results = options.ordered ? [] : new LocalCollection._IdMap;
// fast path for single ID value
if (self._selectorId !== undefined) {
if (this._selectorId !== undefined) {
// If you have non-zero skip and ask for a single id, you get
// nothing. This is so it matches the behavior of the '{_id: foo}'
// path.
if (self.skip)
if (this.skip)
return results;
const selectedDoc = self.collection._docs.get(self._selectorId);
const selectedDoc = this.collection._docs.get(this._selectorId);
if (selectedDoc) {
if (options.ordered)
results.push(selectedDoc);
else
results.set(self._selectorId, selectedDoc);
results.set(this._selectorId, selectedDoc);
}
return results;
}
@@ -363,7 +345,7 @@ export class Cursor {
// live results set) object. in other cases, distances is only used inside
// this function.
let distances;
if (self.matcher.hasGeoQuery() && options.ordered) {
if (this.matcher.hasGeoQuery() && options.ordered) {
if (options.distances) {
distances = options.distances;
distances.clear();
@@ -372,8 +354,8 @@ export class Cursor {
}
}
self.collection._docs.forEach((doc, id) => {
const matchResult = self.matcher.documentMatches(doc);
this.collection._docs.forEach((doc, id) => {
const matchResult = this.matcher.documentMatches(doc);
if (matchResult.result) {
if (options.ordered) {
results.push(doc);
@@ -385,8 +367,8 @@ export class Cursor {
}
// Fast path for limited unsorted queries.
// XXX 'length' check here seems wrong for ordered
if (self.limit && !self.skip && !self.sorter &&
results.length === self.limit)
if (this.limit && !this.skip && !this.sorter &&
results.length === this.limit)
return false; // break
return true; // continue
});
@@ -394,27 +376,26 @@ export class Cursor {
if (!options.ordered)
return results;
if (self.sorter) {
const comparator = self.sorter.getComparator({distances});
if (this.sorter) {
const comparator = this.sorter.getComparator({distances});
results.sort(comparator);
}
const idx_start = self.skip || 0;
const idx_end = self.limit ? (self.limit + idx_start) : results.length;
const idx_start = this.skip || 0;
const idx_end = this.limit ? (this.limit + idx_start) : results.length;
return results.slice(idx_start, idx_end);
}
_publishCursor (sub) {
const self = this;
if (! self.collection.name)
if (! this.collection.name)
throw new Error("Can't publish a cursor from a collection without a name.");
const collection = self.collection.name;
const collection = this.collection.name;
// XXX minimongo should not depend on mongo-livedata!
if (! Package.mongo) {
throw new Error("Can't publish from Minimongo without the `mongo` package.");
}
return Package.mongo.Mongo.Collection._publishCursor(self, sub, collection);
return Package.mongo.Mongo.Collection._publishCursor(this, sub, collection);
}
}

View File

@@ -86,7 +86,6 @@ export class LocalCollection {
// XXX possibly enforce that 'undefined' does not appear (we assume
// this in our handling of null and $exists)
insert (doc, callback) {
const self = this;
doc = EJSON.clone(doc);
assertHasValidFieldNames(doc);
@@ -99,16 +98,16 @@ export class LocalCollection {
}
const id = doc._id;
if (self._docs.has(id))
if (this._docs.has(id))
throw MinimongoError(`Duplicate _id '${id}'`);
self._saveOriginal(id, undefined);
self._docs.set(id, doc);
this._saveOriginal(id, undefined);
this._docs.set(id, doc);
const queriesToRecompute = [];
// trigger live queries that match
for (let qid in self.queries) {
const query = self.queries[qid];
for (let qid in this.queries) {
const query = this.queries[qid];
if (query.dirty) continue;
const matchResult = query.matcher.documentMatches(doc);
if (matchResult.result) {
@@ -122,10 +121,10 @@ export class LocalCollection {
}
queriesToRecompute.forEach(qid => {
if (self.queries[qid])
self._recomputeResults(self.queries[qid]);
if (this.queries[qid])
this._recomputeResults(this.queries[qid]);
});
self._observeQueue.drain();
this._observeQueue.drain();
// Defer because the caller likely doesn't expect the callback to be run
// immediately.
@@ -155,16 +154,14 @@ export class LocalCollection {
}
remove (selector, callback) {
const self = this;
// Easy special case: if we're not calling observeChanges callbacks and we're
// not saving originals and we got asked to remove everything, then just empty
// everything directly.
if (self.paused && !self._savedOriginals && EJSON.equals(selector, {})) {
const result = self._docs.size();
self._docs.clear();
Object.keys(self.queries).forEach(qid => {
const query = self.queries[qid];
if (this.paused && !this._savedOriginals && EJSON.equals(selector, {})) {
const result = this._docs.size();
this._docs.clear();
Object.keys(this.queries).forEach(qid => {
const query = this.queries[qid];
if (query.ordered) {
query.results = [];
} else {
@@ -181,7 +178,7 @@ export class LocalCollection {
const matcher = new Minimongo.Matcher(selector);
const remove = [];
self._eachPossiblyMatchingDoc(selector, (doc, id) => {
this._eachPossiblyMatchingDoc(selector, (doc, id) => {
if (matcher.documentMatches(doc).result)
remove.push(id);
});
@@ -190,9 +187,9 @@ export class LocalCollection {
const queryRemove = [];
for (let i = 0; i < remove.length; i++) {
const removeId = remove[i];
const removeDoc = self._docs.get(removeId);
Object.keys(self.queries).forEach(qid => {
const query = self.queries[qid];
const removeDoc = this._docs.get(removeId);
Object.keys(this.queries).forEach(qid => {
const query = this.queries[qid];
if (query.dirty) return;
if (query.matcher.documentMatches(removeDoc).result) {
@@ -202,24 +199,24 @@ export class LocalCollection {
queryRemove.push({qid, doc: removeDoc});
}
});
self._saveOriginal(removeId, removeDoc);
self._docs.remove(removeId);
this._saveOriginal(removeId, removeDoc);
this._docs.remove(removeId);
}
// run live query callbacks _after_ we've removed the documents.
queryRemove.forEach(remove => {
const query = self.queries[remove.qid];
const query = this.queries[remove.qid];
if (query) {
query.distances && query.distances.remove(remove.doc._id);
LocalCollection._removeFromResults(query, remove.doc);
}
});
queriesToRecompute.forEach(qid => {
const query = self.queries[qid];
const query = this.queries[qid];
if (query)
self._recomputeResults(query);
this._recomputeResults(query);
});
self._observeQueue.drain();
this._observeQueue.drain();
const result = remove.length;
if (callback)
Meteor.defer(() => {
@@ -233,7 +230,6 @@ export class LocalCollection {
// database. Note that this is not just replaying all the changes that
// happened during the pause, it is a smarter 'coalesced' diff.
resumeObservers () {
const self = this;
// No-op if not paused.
if (!this.paused)
return;
@@ -243,11 +239,11 @@ export class LocalCollection {
this.paused = false;
for (let qid in this.queries) {
const query = self.queries[qid];
const query = this.queries[qid];
if (query.dirty) {
query.dirty = false;
// re-compute results will perform `LocalCollection._diffQueryChanges` automatically.
self._recomputeResults(query, query.resultsSnapshot);
this._recomputeResults(query, query.resultsSnapshot);
} else {
// Diff the current results against the snapshot and send to observers.
// pass the query object for its observer callbacks.
@@ -257,16 +253,15 @@ export class LocalCollection {
}
query.resultsSnapshot = null;
}
self._observeQueue.drain();
this._observeQueue.drain();
}
retrieveOriginals () {
const self = this;
if (!self._savedOriginals)
if (!this._savedOriginals)
throw new Error("Called retrieveOriginals without saveOriginals");
const originals = self._savedOriginals;
self._savedOriginals = null;
const originals = this._savedOriginals;
this._savedOriginals = null;
return originals;
}
@@ -278,16 +273,14 @@ export class LocalCollection {
// is the value.) You must alternate between calls to saveOriginals() and
// retrieveOriginals().
saveOriginals () {
const self = this;
if (self._savedOriginals)
if (this._savedOriginals)
throw new Error("Called saveOriginals twice without retrieveOriginals");
self._savedOriginals = new LocalCollection._IdMap;
this._savedOriginals = new LocalCollection._IdMap;
}
// XXX atomicity: if multi is true, and one modification fails, do
// we rollback the whole operation, or what?
update (selector, mod, options, callback) {
const self = this;
if (! callback && options instanceof Function) {
callback = options;
options = null;
@@ -306,9 +299,9 @@ export class LocalCollection {
const docMap = new LocalCollection._IdMap;
const idsMatchedBySelector = LocalCollection._idsMatchedBySelector(selector);
Object.keys(self.queries).forEach(qid => {
const query = self.queries[qid];
if ((query.cursor.skip || query.cursor.limit) && ! self.paused) {
Object.keys(this.queries).forEach(qid => {
const query = this.queries[qid];
if ((query.cursor.skip || query.cursor.limit) && ! this.paused) {
// Catch the case of a reactive `count()` on a cursor with skip
// or limit, which registers an unordered observe. This is a
// pretty rare case, so we just clone the entire result set with
@@ -351,12 +344,12 @@ export class LocalCollection {
let updateCount = 0;
self._eachPossiblyMatchingDoc(selector, (doc, id) => {
this._eachPossiblyMatchingDoc(selector, (doc, id) => {
const queryResult = matcher.documentMatches(doc);
if (queryResult.result) {
// XXX Should we save the original even if mod ends up being a no-op?
self._saveOriginal(id, doc);
self._modifyAndNotify(doc, mod, recomputeQids, queryResult.arrayIndices);
this._saveOriginal(id, doc);
this._modifyAndNotify(doc, mod, recomputeQids, queryResult.arrayIndices);
++updateCount;
if (!options.multi)
return false; // break
@@ -365,11 +358,11 @@ export class LocalCollection {
});
Object.keys(recomputeQids).forEach(qid => {
const query = self.queries[qid];
const query = this.queries[qid];
if (query)
self._recomputeResults(query, qidToOriginalResults[qid]);
this._recomputeResults(query, qidToOriginalResults[qid]);
});
self._observeQueue.drain();
this._observeQueue.drain();
// If we are doing an upsert, and we didn't modify any documents yet, then
// it's time to do an insert. Figure out what document we are inserting, and
@@ -396,7 +389,7 @@ export class LocalCollection {
if (! newDoc._id && options.insertedId)
newDoc._id = options.insertedId;
insertedId = self.insert(newDoc);
insertedId = this.insert(newDoc);
updateCount = 1;
}
@@ -425,12 +418,11 @@ export class LocalCollection {
// equivalent to LocalCollection.update(sel, mod, { upsert: true, _returnObject:
// true }).
upsert (selector, mod, options, callback) {
const self = this;
if (! callback && typeof options === "function") {
callback = options;
options = {};
}
return self.update(selector, mod, Object.assign({}, options, {
return this.update(selector, mod, Object.assign({}, options, {
upsert: true,
_returnObject: true
}), callback);
@@ -441,12 +433,11 @@ export class LocalCollection {
// specific _id's, it only looks at those. doc is *not* cloned: it is the
// same object that is in _docs.
_eachPossiblyMatchingDoc (selector, f) {
const self = this;
const specificIds = LocalCollection._idsMatchedBySelector(selector);
if (specificIds) {
for (let i = 0; i < specificIds.length; ++i) {
const id = specificIds[i];
const doc = self._docs.get(id);
const doc = this._docs.get(id);
if (doc) {
const breakIfFalse = f(doc, id);
if (breakIfFalse === false)
@@ -454,16 +445,14 @@ export class LocalCollection {
}
}
} else {
self._docs.forEach(f);
this._docs.forEach(f);
}
}
_modifyAndNotify (doc, mod, recomputeQids, arrayIndices) {
const self = this;
const matched_before = {};
for (let qid in self.queries) {
const query = self.queries[qid];
for (let qid in this.queries) {
const query = this.queries[qid];
if (query.dirty) continue;
if (query.ordered) {
@@ -479,8 +468,8 @@ export class LocalCollection {
LocalCollection._modify(doc, mod, {arrayIndices});
for (let qid in self.queries) {
const query = self.queries[qid];
for (let qid in this.queries) {
const query = this.queries[qid];
if (query.dirty) continue;
const before = matched_before[qid];
@@ -520,8 +509,7 @@ export class LocalCollection {
//
// oldResults is guaranteed to be ignored if the query is not paused.
_recomputeResults (query, oldResults) {
const self = this;
if (self.paused) {
if (this.paused) {
// There's no reason to recompute the results now as we're still paused.
// By flagging the query as "dirty", the recompute will be performed
// when resumeObservers is called.
@@ -529,14 +517,14 @@ export class LocalCollection {
return;
}
if (! self.paused && ! oldResults)
if (! this.paused && ! oldResults)
oldResults = query.results;
if (query.distances)
query.distances.clear();
query.results = query.cursor._getRawObjects({
ordered: query.ordered, distances: query.distances});
if (! self.paused) {
if (! this.paused) {
LocalCollection._diffQueryChanges(
query.ordered, oldResults, query.results, query,
{ projectionFn: query.projectionFn });
@@ -544,16 +532,15 @@ export class LocalCollection {
}
_saveOriginal (id, doc) {
const self = this;
// Are we even trying to save originals?
if (!self._savedOriginals)
if (!this._savedOriginals)
return;
// Have we previously mutated the original (and so 'doc' is not actually
// original)? (Note the 'has' check rather than truth: we store undefined
// here for inserted docs!)
if (self._savedOriginals.has(id))
if (this._savedOriginals.has(id))
return;
self._savedOriginals.set(id, EJSON.clone(doc));
this._savedOriginals.set(id, EJSON.clone(doc));
}
}
@@ -564,76 +551,73 @@ LocalCollection.ObserveHandle = ObserveHandle;
// XXX maybe move these into another ObserveHelpers package or something
// _CachingChangeObserver is an object which receives observeChanges callbacks
// and keeps a cache of the current cursor state up to date in self.docs. Users
// and keeps a cache of the current cursor state up to date in this.docs. Users
// of this class should read the docs field but not modify it. You should pass
// the "applyChange" field as the callbacks to the underlying observeChanges
// call. Optionally, you can specify your own observeChanges callbacks which are
// invoked immediately before the docs field is updated; this object is made
// available as `this` to those callbacks.
LocalCollection._CachingChangeObserver = class _CachingChangeObserver {
constructor (options) {
const self = this;
options = options || {};
constructor (options = {}) {
const orderedFromCallbacks = options.callbacks &&
LocalCollection._observeChangesCallbacksAreOrdered(options.callbacks);
if (options.hasOwnProperty('ordered')) {
self.ordered = options.ordered;
this.ordered = options.ordered;
if (options.callbacks && options.ordered !== orderedFromCallbacks)
throw Error("ordered option doesn't match callbacks");
} else if (options.callbacks) {
self.ordered = orderedFromCallbacks;
this.ordered = orderedFromCallbacks;
} else {
throw Error("must provide ordered or callbacks");
}
const callbacks = options.callbacks || {};
if (self.ordered) {
self.docs = new OrderedDict(MongoID.idStringify);
self.applyChange = {
addedBefore(id, fields, before) {
if (this.ordered) {
this.docs = new OrderedDict(MongoID.idStringify);
this.applyChange = {
addedBefore: (id, fields, before) => {
const doc = EJSON.clone(fields);
doc._id = id;
callbacks.addedBefore && callbacks.addedBefore.call(
self, id, fields, before);
this, id, fields, before);
// This line triggers if we provide added with movedBefore.
callbacks.added && callbacks.added.call(self, id, fields);
callbacks.added && callbacks.added.call(this, id, fields);
// XXX could `before` be a falsy ID? Technically
// idStringify seems to allow for them -- though
// OrderedDict won't call stringify on a falsy arg.
self.docs.putBefore(id, doc, before || null);
this.docs.putBefore(id, doc, before || null);
},
movedBefore(id, before) {
const doc = self.docs.get(id);
callbacks.movedBefore && callbacks.movedBefore.call(self, id, before);
self.docs.moveBefore(id, before || null);
movedBefore: (id, before) => {
const doc = this.docs.get(id);
callbacks.movedBefore && callbacks.movedBefore.call(this, id, before);
this.docs.moveBefore(id, before || null);
}
};
} else {
self.docs = new LocalCollection._IdMap;
self.applyChange = {
added(id, fields) {
this.docs = new LocalCollection._IdMap;
this.applyChange = {
added: (id, fields) => {
const doc = EJSON.clone(fields);
callbacks.added && callbacks.added.call(self, id, fields);
callbacks.added && callbacks.added.call(this, id, fields);
doc._id = id;
self.docs.set(id, doc);
this.docs.set(id, doc);
}
};
}
// The methods in _IdMap and OrderedDict used by these callbacks are
// identical.
self.applyChange.changed = (id, fields) => {
const doc = self.docs.get(id);
this.applyChange.changed = (id, fields) => {
const doc = this.docs.get(id);
if (!doc)
throw new Error(`Unknown id for changed: ${id}`);
callbacks.changed && callbacks.changed.call(
self, id, EJSON.clone(fields));
this, id, EJSON.clone(fields));
DiffSequence.applyChanges(doc, fields);
};
self.applyChange.removed = id => {
callbacks.removed && callbacks.removed.call(self, id);
self.docs.remove(id);
this.applyChange.removed = id => {
callbacks.removed && callbacks.removed.call(this, id);
this.docs.remove(id);
};
}
};
@@ -987,60 +971,56 @@ LocalCollection._observeFromObserveChanges = (cursor, observeCallbacks) => {
// relative order, transforms, and applyChanges -- without the speed hit.
const indices = !observeCallbacks._no_indices;
observeChangesCallbacks = {
addedBefore(id, fields, before) {
const self = this;
addedBefore (id, fields, before) {
if (suppressed || !(observeCallbacks.addedAt || observeCallbacks.added))
return;
const doc = transform(Object.assign(fields, {_id: id}));
if (observeCallbacks.addedAt) {
const index = indices
? (before ? self.docs.indexOf(before) : self.docs.size()) : -1;
? (before ? this.docs.indexOf(before) : this.docs.size()) : -1;
observeCallbacks.addedAt(doc, index, before);
} else {
observeCallbacks.added(doc);
}
},
changed(id, fields) {
const self = this;
changed (id, fields) {
if (!(observeCallbacks.changedAt || observeCallbacks.changed))
return;
let doc = EJSON.clone(self.docs.get(id));
let doc = EJSON.clone(this.docs.get(id));
if (!doc)
throw new Error(`Unknown id for changed: ${id}`);
const oldDoc = transform(EJSON.clone(doc));
DiffSequence.applyChanges(doc, fields);
doc = transform(doc);
if (observeCallbacks.changedAt) {
const index = indices ? self.docs.indexOf(id) : -1;
const index = indices ? this.docs.indexOf(id) : -1;
observeCallbacks.changedAt(doc, oldDoc, index);
} else {
observeCallbacks.changed(doc, oldDoc);
}
},
movedBefore(id, before) {
const self = this;
movedBefore (id, before) {
if (!observeCallbacks.movedTo)
return;
const from = indices ? self.docs.indexOf(id) : -1;
const from = indices ? this.docs.indexOf(id) : -1;
let to = indices
? (before ? self.docs.indexOf(before) : self.docs.size()) : -1;
? (before ? this.docs.indexOf(before) : this.docs.size()) : -1;
// When not moving backwards, adjust for the fact that removing the
// document slides everything back one slot.
if (to > from)
--to;
observeCallbacks.movedTo(transform(EJSON.clone(self.docs.get(id))),
observeCallbacks.movedTo(transform(EJSON.clone(this.docs.get(id))),
from, to, before || null);
},
removed(id) {
const self = this;
removed (id) {
if (!(observeCallbacks.removedAt || observeCallbacks.removed))
return;
// technically maybe there should be an EJSON.clone here, but it's about
// to be removed from self.docs!
const doc = transform(self.docs.get(id));
// to be removed from this.docs!
const doc = transform(this.docs.get(id));
if (observeCallbacks.removedAt) {
const index = indices ? self.docs.indexOf(id) : -1;
const index = indices ? this.docs.indexOf(id) : -1;
observeCallbacks.removedAt(doc, index);
} else {
observeCallbacks.removed(doc);
@@ -1049,26 +1029,24 @@ LocalCollection._observeFromObserveChanges = (cursor, observeCallbacks) => {
};
} else {
observeChangesCallbacks = {
added(id, fields) {
added (id, fields) {
if (!suppressed && observeCallbacks.added) {
const doc = Object.assign(fields, {_id: id});
observeCallbacks.added(transform(doc));
}
},
changed(id, fields) {
const self = this;
changed (id, fields) {
if (observeCallbacks.changed) {
const oldDoc = self.docs.get(id);
const oldDoc = this.docs.get(id);
const doc = EJSON.clone(oldDoc);
DiffSequence.applyChanges(doc, fields);
observeCallbacks.changed(transform(doc),
transform(EJSON.clone(oldDoc)));
}
},
removed(id) {
const self = this;
removed (id) {
if (observeCallbacks.removed) {
observeCallbacks.removed(transform(self.docs.get(id)));
observeCallbacks.removed(transform(this.docs.get(id)));
}
}
};

View File

@@ -7,7 +7,6 @@ import {
} from './common.js';
Minimongo._pathsElidingNumericKeys = function (paths) {
const self = this;
return paths.map(path => path.split('.').filter(part => !isNumericKey(part)).join('.'));
};
@@ -20,11 +19,10 @@ Minimongo._pathsElidingNumericKeys = function (paths) {
// - $unset
// - 'abc.d': 1
Minimongo.Matcher.prototype.affectedByModifier = function (modifier) {
const self = this;
// safe check for $set/$unset being objects
modifier = Object.assign({ $set: {}, $unset: {} }, modifier);
const modifiedPaths = Object.keys(modifier.$set).concat(Object.keys(modifier.$unset));
const meaningfulPaths = self._getPaths();
const meaningfulPaths = this._getPaths();
return modifiedPaths.some(path => {
const mod = path.split('.');
@@ -66,17 +64,16 @@ Minimongo.Matcher.prototype.affectedByModifier = function (modifier) {
// stay 'false'.
// Currently doesn't support $-operators and numeric indices precisely.
Minimongo.Matcher.prototype.canBecomeTrueByModifier = function (modifier) {
const self = this;
if (!this.affectedByModifier(modifier))
return false;
modifier = Object.assign({$set:{}, $unset:{}}, modifier);
const modifierPaths = Object.keys(modifier.$set).concat(Object.keys(modifier.$unset));
if (!self.isSimple())
if (!this.isSimple())
return true;
if (self._getPaths().some(pathHasNumericKeys) ||
if (this._getPaths().some(pathHasNumericKeys) ||
modifierPaths.some(pathHasNumericKeys))
return true;
@@ -85,8 +82,8 @@ Minimongo.Matcher.prototype.canBecomeTrueByModifier = function (modifier) {
// NOTE: it is correct since we allow only scalars in $-operators
// Example: for selector {'a.b': {$gt: 5}} the modifier {'a.b.c':7} would
// definitely set the result to false as 'a.b' appears to be an object.
const expectedScalarIsObject = Object.keys(self._selector).some(path => {
const sel = self._selector[path];
const expectedScalarIsObject = Object.keys(this._selector).some(path => {
const sel = this._selector[path];
if (! isOperatorObject(sel))
return false;
return modifierPaths.some(modifierPath => startsWith(modifierPath, `${path}.`));
@@ -98,7 +95,7 @@ Minimongo.Matcher.prototype.canBecomeTrueByModifier = function (modifier) {
// See if we can apply the modifier on the ideally matching object. If it
// still matches the selector, then the modifier could have turned the real
// object in the database into something matching.
const matchingDocument = EJSON.clone(self.matchingDocument());
const matchingDocument = EJSON.clone(this.matchingDocument());
// The selector is too complex, anything can happen.
if (matchingDocument === null)
@@ -122,15 +119,14 @@ Minimongo.Matcher.prototype.canBecomeTrueByModifier = function (modifier) {
throw e;
}
return self.documentMatches(matchingDocument).result;
return this.documentMatches(matchingDocument).result;
};
// Knows how to combine a mongo selector and a fields projection to a new fields
// projection taking into account active fields from the passed selector.
// @returns Object - projection object (same as fields option of mongo cursor)
Minimongo.Matcher.prototype.combineIntoProjection = function (projection) {
const self = this;
const selectorPaths = Minimongo._pathsElidingNumericKeys(self._getPaths());
const selectorPaths = Minimongo._pathsElidingNumericKeys(this._getPaths());
// Special case for $where operator in the selector - projection should depend
// on all fields of the document. getSelectorPaths returns a list of paths
@@ -147,18 +143,16 @@ Minimongo.Matcher.prototype.combineIntoProjection = function (projection) {
// { 'a.b': { ans: 42 }, 'foo.bar': null, 'foo.baz': "something" }
// => { a: { b: { ans: 42 } }, foo: { bar: null, baz: "something" } }
Minimongo.Matcher.prototype.matchingDocument = function () {
const self = this;
// check if it was computed before
if (self._matchingDocument !== undefined)
return self._matchingDocument;
if (this._matchingDocument !== undefined)
return this._matchingDocument;
// If the analysis of this selector is too hard for our implementation
// fallback to "YES"
let fallback = false;
self._matchingDocument = pathsToTree(self._getPaths(),
this._matchingDocument = pathsToTree(this._getPaths(),
path => {
const valueSelector = self._selector[path];
const valueSelector = this._selector[path];
if (isOperatorObject(valueSelector)) {
// if there is a strict equality, there is a good
// chance we can use one of those as "matching"
@@ -191,7 +185,7 @@ Minimongo.Matcher.prototype.matchingDocument = function () {
return middle;
} else if (onlyContainsKeys(valueSelector, ['$nin', '$ne'])) {
// Since self._isSimple makes sure $nin and $ne are not combined with
// Since this._isSimple makes sure $nin and $ne are not combined with
// objects or arrays, we can confidently return an empty object as it
// never matches any scalar.
return {};
@@ -199,26 +193,24 @@ Minimongo.Matcher.prototype.matchingDocument = function () {
fallback = true;
}
}
return self._selector[path];
return this._selector[path];
},
x => x);
if (fallback)
self._matchingDocument = null;
this._matchingDocument = null;
return self._matchingDocument;
return this._matchingDocument;
};
// Minimongo.Sorter gets a similar method, which delegates to a Matcher it made
// for this exact purpose.
Minimongo.Sorter.prototype.affectedByModifier = function (modifier) {
const self = this;
return self._selectorForAffectedByModifier.affectedByModifier(modifier);
return this._selectorForAffectedByModifier.affectedByModifier(modifier);
};
Minimongo.Sorter.prototype.combineIntoProjection = function (projection) {
const self = this;
const specPaths = Minimongo._pathsElidingNumericKeys(self._getPaths());
const specPaths = Minimongo._pathsElidingNumericKeys(this._getPaths());
return combineImportantPathsIntoProjection(specPaths, projection);
};

View File

@@ -21,19 +21,16 @@ import {
// first, or 0 if neither object comes before the other.
export class Sorter {
constructor (spec, options) {
const self = this;
options = options || {};
self._sortSpecParts = [];
self._sortFunction = null;
constructor (spec, options = {}) {
this._sortSpecParts = [];
this._sortFunction = null;
const addSpecPart = (path, ascending) => {
if (!path)
throw Error("sort keys must be non-empty");
if (path.charAt(0) === '$')
throw Error(`unsupported sort key: ${path}`);
self._sortSpecParts.push({
this._sortSpecParts.push({
path,
lookup: makeLookupFunction(path, {forSort: true}),
ascending
@@ -54,46 +51,44 @@ export class Sorter {
addSpecPart(key, value >= 0);
});
} else if (typeof spec === "function") {
self._sortFunction = spec;
this._sortFunction = spec;
} else {
throw Error(`Bad sort specification: ${JSON.stringify(spec)}`);
}
// If a function is specified for sorting, we skip the rest.
if (self._sortFunction)
if (this._sortFunction)
return;
// To implement affectedByModifier, we piggy-back on top of Matcher's
// affectedByModifier code; we create a selector that is affected by the same
// modifiers as this sort order. This is only implemented on the server.
if (self.affectedByModifier) {
if (this.affectedByModifier) {
const selector = {};
self._sortSpecParts.forEach(spec => {
this._sortSpecParts.forEach(spec => {
selector[spec.path] = 1;
});
self._selectorForAffectedByModifier = new Minimongo.Matcher(selector);
this._selectorForAffectedByModifier = new Minimongo.Matcher(selector);
}
self._keyComparator = composeComparators(
self._sortSpecParts.map((spec, i) => self._keyFieldComparator(i)));
this._keyComparator = composeComparators(
this._sortSpecParts.map((spec, i) => this._keyFieldComparator(i)));
// If you specify a matcher for this Sorter, _keyFilter may be set to a
// function which selects whether or not a given "sort key" (tuple of values
// for the different sort spec fields) is compatible with the selector.
self._keyFilter = null;
options.matcher && self._useWithMatcher(options.matcher);
this._keyFilter = null;
options.matcher && this._useWithMatcher(options.matcher);
}
getComparator (options) {
const self = this;
// If sort is specified or have no distances, just use the comparator from
// the source specification (which defaults to "everything is equal".
// issue #3599
// https://docs.mongodb.com/manual/reference/operator/query/near/#sort-operation
// sort effectively overrides $near
if (self._sortSpecParts.length || !options || !options.distances) {
return self._getBaseComparator();
if (this._sortSpecParts.length || !options || !options.distances) {
return this._getBaseComparator();
}
const distances = options.distances;
@@ -112,21 +107,18 @@ export class Sorter {
// parts. Returns negative, 0, or positive based on using the sort spec to
// compare fields.
_compareKeys (key1, key2) {
const self = this;
if (key1.length !== self._sortSpecParts.length ||
key2.length !== self._sortSpecParts.length) {
if (key1.length !== this._sortSpecParts.length ||
key2.length !== this._sortSpecParts.length) {
throw Error("Key has wrong length");
}
return self._keyComparator(key1, key2);
return this._keyComparator(key1, key2);
}
// Iterates over each possible "key" from doc (ie, over each branch), calling
// 'cb' with the key.
_generateKeysFromDoc (doc, cb) {
const self = this;
if (self._sortSpecParts.length === 0)
if (this._sortSpecParts.length === 0)
throw new Error("can't generate keys without a spec");
// maps index -> ({'' -> value} or {path -> value})
@@ -136,7 +128,7 @@ export class Sorter {
let knownPaths = null;
self._sortSpecParts.forEach((spec, whichField) => {
this._sortSpecParts.forEach((spec, whichField) => {
// Expand any leaf arrays that we find, and ignore those arrays
// themselves. (We never sort based on an array itself.)
let branches = expandArraysInBranches(spec.lookup(doc), true);
@@ -221,21 +213,19 @@ export class Sorter {
// Returns a comparator that represents the sort specification (but not
// including a possible geoquery distance tie-breaker).
_getBaseComparator () {
const self = this;
if (self._sortFunction)
return self._sortFunction;
if (this._sortFunction)
return this._sortFunction;
// If we're only sorting on geoquery distance and no specs, just say
// everything is equal.
if (!self._sortSpecParts.length) {
if (!this._sortSpecParts.length) {
return (doc1, doc2) => 0;
}
return (doc1, doc2) => {
const key1 = self._getMinKeyFromDoc(doc1);
const key2 = self._getMinKeyFromDoc(doc2);
return self._compareKeys(key1, key2);
const key1 = this._getMinKeyFromDoc(doc1);
const key2 = this._getMinKeyFromDoc(doc2);
return this._compareKeys(key1, key2);
};
}
@@ -250,18 +240,17 @@ export class Sorter {
// 1, y: 3}]} with sort spec {'a.x': 1, 'a.y': 1}, the only keys are [0,5] and
// [1,3], and the minimum key is [0,5]; notably, [0,3] is NOT a key.
_getMinKeyFromDoc (doc) {
const self = this;
let minKey = null;
self._generateKeysFromDoc(doc, key => {
if (!self._keyCompatibleWithSelector(key))
this._generateKeysFromDoc(doc, key => {
if (!this._keyCompatibleWithSelector(key))
return;
if (minKey === null) {
minKey = key;
return;
}
if (self._compareKeys(key, minKey) < 0) {
if (this._compareKeys(key, minKey) < 0) {
minKey = key;
}
});
@@ -274,20 +263,17 @@ export class Sorter {
}
_getPaths () {
const self = this;
return self._sortSpecParts.map(part => part.path);
return this._sortSpecParts.map(part => part.path);
}
_keyCompatibleWithSelector (key) {
const self = this;
return !self._keyFilter || self._keyFilter(key);
return !this._keyFilter || this._keyFilter(key);
}
// Given an index 'i', returns a comparator that compares two key arrays based
// on field 'i'.
_keyFieldComparator (i) {
const self = this;
const invert = !self._sortSpecParts[i].ascending;
const invert = !this._sortSpecParts[i].ascending;
return (key1, key2) => {
let compare = LocalCollection._f._cmp(key1[i], key2[i]);
if (invert)
@@ -316,15 +302,13 @@ export class Sorter {
// subtle and undocumented; we've gotten as close as we can figure out based
// on our understanding of Mongo's behavior.
_useWithMatcher (matcher) {
const self = this;
if (self._keyFilter)
if (this._keyFilter)
throw Error("called _useWithMatcher twice?");
// If we are only sorting by distance, then we're not going to bother to
// build a key filter.
// XXX figure out how geoqueries interact with this stuff
if (!self._sortSpecParts.length)
if (!this._sortSpecParts.length)
return;
const selector = matcher._selector;
@@ -335,7 +319,7 @@ export class Sorter {
return;
const constraintsByPath = {};
self._sortSpecParts.forEach((spec, i) => {
this._sortSpecParts.forEach((spec, i) => {
constraintsByPath[spec.path] = [];
});
@@ -394,10 +378,10 @@ export class Sorter {
// others; we shouldn't create a key filter unless the first sort field is
// restricted, though after that point we can restrict the other sort fields
// or not as we wish.
if (!constraintsByPath[self._sortSpecParts[0].path].length)
if (!constraintsByPath[this._sortSpecParts[0].path].length)
return;
self._keyFilter = key => self._sortSpecParts.every((specPart, index) => constraintsByPath[specPart.path].every(f => f(key[index])));
this._keyFilter = key => this._sortSpecParts.every((specPart, index) => constraintsByPath[specPart.path].every(f => f(key[index])));
}
}