mirror of
https://github.com/meteor/meteor.git
synced 2026-05-02 03:01:46 -04:00
348 lines
12 KiB
JavaScript
348 lines
12 KiB
JavaScript
var warn = function () {
|
|
if (ObserveSequence._suppressWarnings) {
|
|
ObserveSequence._suppressWarnings--;
|
|
} else {
|
|
if (typeof console !== 'undefined' && console.warn)
|
|
console.warn.apply(console, arguments);
|
|
|
|
ObserveSequence._loggedWarnings++;
|
|
}
|
|
};
|
|
|
|
var idStringify = LocalCollection._idStringify;
|
|
var idParse = LocalCollection._idParse;
|
|
|
|
ObserveSequence = {
|
|
_suppressWarnings: 0,
|
|
_loggedWarnings: 0,
|
|
|
|
// A mechanism similar to cursor.observe which receives a reactive
|
|
// function returning a sequence type and firing appropriate callbacks
|
|
// when the value changes.
|
|
//
|
|
// @param sequenceFunc {Function} a reactive function returning a
|
|
// sequence type. The currently supported sequence types are:
|
|
// 'null', arrays and cursors.
|
|
//
|
|
// @param callbacks {Object} similar to a specific subset of
|
|
// callbacks passed to `cursor.observe`
|
|
// (http://docs.meteor.com/#observe), with minor variations to
|
|
// support the fact that not all sequences contain objects with
|
|
// _id fields. Specifically:
|
|
//
|
|
// * addedAt(id, item, atIndex, beforeId)
|
|
// * changedAt(id, newItem, oldItem, atIndex)
|
|
// * removedAt(id, oldItem, atIndex)
|
|
// * movedTo(id, item, fromIndex, toIndex, beforeId)
|
|
//
|
|
// @returns {Object(stop: Function)} call 'stop' on the return value
|
|
// to stop observing this sequence function.
|
|
//
|
|
// We don't make any assumptions about our ability to compare sequence
|
|
// elements (ie, we don't assume EJSON.equals works; maybe there is extra
|
|
// state/random methods on the objects) so unlike cursor.observe, we may
|
|
// sometimes call changedAt() when nothing actually changed.
|
|
// XXX consider if we *can* make the stronger assumption and avoid
|
|
// no-op changedAt calls (in some cases?)
|
|
//
|
|
// XXX currently only supports the callbacks used by our
|
|
// implementation of {{#each}}, but this can be expanded.
|
|
//
|
|
// XXX #each doesn't use the indices (though we'll eventually need
|
|
// a way to get them when we support `@index`), but calling
|
|
// `cursor.observe` causes the index to be calculated on every
|
|
// callback using a linear scan (unless you turn it off by passing
|
|
// `_no_indices`). Any way to avoid calculating indices on a pure
|
|
// cursor observe like we used to?
|
|
observe: function (sequenceFunc, callbacks) {
|
|
var lastSeq = null;
|
|
var activeObserveHandle = null;
|
|
|
|
// 'lastSeqArray' contains the previous value of the sequence
|
|
// we're observing. It is an array of objects with '_id' and
|
|
// 'item' fields. 'item' is the element in the array, or the
|
|
// document in the cursor.
|
|
//
|
|
// '_id' is whichever of the following is relevant, unless it has
|
|
// already appeared -- in which case it's randomly generated.
|
|
//
|
|
// * if 'item' is an object:
|
|
// * an '_id' field, if present
|
|
// * otherwise, the index in the array
|
|
//
|
|
// * if 'item' is a number or string, use that value
|
|
//
|
|
// XXX this can be generalized by allowing {{#each}} to accept a
|
|
// general 'key' argument which could be a function, a dotted
|
|
// field name, or the special @index value.
|
|
var lastSeqArray = []; // elements are objects of form {_id, item}
|
|
var computation = Tracker.autorun(function () {
|
|
var seq = sequenceFunc();
|
|
|
|
Tracker.nonreactive(function () {
|
|
var seqArray; // same structure as `lastSeqArray` above.
|
|
|
|
if (activeObserveHandle) {
|
|
// If we were previously observing a cursor, replace lastSeqArray with
|
|
// more up-to-date information. Then stop the old observe.
|
|
lastSeqArray = _.map(lastSeq.fetch(), function (doc) {
|
|
return {_id: doc._id, item: doc};
|
|
});
|
|
activeObserveHandle.stop();
|
|
activeObserveHandle = null;
|
|
}
|
|
|
|
if (!seq) {
|
|
seqArray = seqChangedToEmpty(lastSeqArray, callbacks);
|
|
} else if (seq instanceof Array) {
|
|
seqArray = seqChangedToArray(lastSeqArray, seq, callbacks);
|
|
} else if (isStoreCursor(seq)) {
|
|
var result /* [seqArray, activeObserveHandle] */ =
|
|
seqChangedToCursor(lastSeqArray, seq, callbacks);
|
|
seqArray = result[0];
|
|
activeObserveHandle = result[1];
|
|
} else {
|
|
throw badSequenceError();
|
|
}
|
|
|
|
diffArray(lastSeqArray, seqArray, callbacks);
|
|
lastSeq = seq;
|
|
lastSeqArray = seqArray;
|
|
});
|
|
});
|
|
|
|
return {
|
|
stop: function () {
|
|
computation.stop();
|
|
if (activeObserveHandle)
|
|
activeObserveHandle.stop();
|
|
}
|
|
};
|
|
},
|
|
|
|
// Fetch the items of `seq` into an array, where `seq` is of one of the
|
|
// sequence types accepted by `observe`. If `seq` is a cursor, a
|
|
// dependency is established.
|
|
fetch: function (seq) {
|
|
if (!seq) {
|
|
return [];
|
|
} else if (seq instanceof Array) {
|
|
return seq;
|
|
} else if (isStoreCursor(seq)) {
|
|
return seq.fetch();
|
|
} else {
|
|
throw badSequenceError();
|
|
}
|
|
}
|
|
};
|
|
|
|
var badSequenceError = function () {
|
|
return new Error("{{#each}} currently only accepts " +
|
|
"arrays, cursors or falsey values.");
|
|
};
|
|
|
|
var isStoreCursor = function (cursor) {
|
|
return cursor && _.isObject(cursor) &&
|
|
_.isFunction(cursor.observe) && _.isFunction(cursor.fetch);
|
|
};
|
|
|
|
// Calculates the differences between `lastSeqArray` and
|
|
// `seqArray` and calls appropriate functions from `callbacks`.
|
|
// Reuses Minimongo's diff algorithm implementation.
|
|
var diffArray = function (lastSeqArray, seqArray, callbacks) {
|
|
var diffFn = Package.minimongo.LocalCollection._diffQueryOrderedChanges;
|
|
var oldIdObjects = [];
|
|
var newIdObjects = [];
|
|
var posOld = {}; // maps from idStringify'd ids
|
|
var posNew = {}; // ditto
|
|
var posCur = {};
|
|
var lengthCur = lastSeqArray.length;
|
|
|
|
_.each(seqArray, function (doc, i) {
|
|
newIdObjects.push({_id: doc._id});
|
|
posNew[idStringify(doc._id)] = i;
|
|
});
|
|
_.each(lastSeqArray, function (doc, i) {
|
|
oldIdObjects.push({_id: doc._id});
|
|
posOld[idStringify(doc._id)] = i;
|
|
posCur[idStringify(doc._id)] = i;
|
|
});
|
|
|
|
// Arrays can contain arbitrary objects. We don't diff the
|
|
// objects. Instead we always fire 'changedAt' callback on every
|
|
// object. The consumer of `observe-sequence` should deal with
|
|
// it appropriately.
|
|
diffFn(oldIdObjects, newIdObjects, {
|
|
addedBefore: function (id, doc, before) {
|
|
var position = before ? posCur[idStringify(before)] : lengthCur;
|
|
|
|
if (before) {
|
|
// If not adding at the end, we need to update indexes.
|
|
// XXX this can still be improved greatly!
|
|
_.each(posCur, function (pos, id) {
|
|
if (pos >= position)
|
|
posCur[id]++;
|
|
});
|
|
}
|
|
|
|
lengthCur++;
|
|
posCur[idStringify(id)] = position;
|
|
|
|
callbacks.addedAt(
|
|
id,
|
|
seqArray[posNew[idStringify(id)]].item,
|
|
position,
|
|
before);
|
|
},
|
|
movedBefore: function (id, before) {
|
|
if (id === before)
|
|
return;
|
|
|
|
var oldPosition = posCur[idStringify(id)];
|
|
var newPosition = before ? posCur[idStringify(before)] : lengthCur;
|
|
|
|
// Moving the item forward. The new element is losing one position as it
|
|
// was removed from the old position before being inserted at the new
|
|
// position.
|
|
// Ex.: 0 *1* 2 3 4
|
|
// 0 2 3 *1* 4
|
|
// The original issued callback is "1" before "4".
|
|
// The position of "1" is 1, the position of "4" is 4.
|
|
// The generated move is (1) -> (3)
|
|
if (newPosition > oldPosition) {
|
|
newPosition--;
|
|
}
|
|
|
|
// Fix up the positions of elements between the old and the new positions
|
|
// of the moved element.
|
|
//
|
|
// There are two cases:
|
|
// 1. The element is moved forward. Then all the positions in between
|
|
// are moved back.
|
|
// 2. The element is moved back. Then the positions in between *and* the
|
|
// element that is currently standing on the moved element's future
|
|
// position are moved forward.
|
|
_.each(posCur, function (elCurPosition, id) {
|
|
if (oldPosition < elCurPosition && elCurPosition < newPosition)
|
|
posCur[id]--;
|
|
else if (newPosition <= elCurPosition && elCurPosition < oldPosition)
|
|
posCur[id]++;
|
|
});
|
|
|
|
// Finally, update the position of the moved element.
|
|
posCur[idStringify(id)] = newPosition;
|
|
|
|
callbacks.movedTo(
|
|
id,
|
|
seqArray[posNew[idStringify(id)]].item,
|
|
oldPosition,
|
|
newPosition,
|
|
before);
|
|
},
|
|
removed: function (id) {
|
|
var prevPosition = posCur[idStringify(id)];
|
|
|
|
_.each(posCur, function (pos, id) {
|
|
if (pos >= prevPosition)
|
|
posCur[id]--;
|
|
});
|
|
|
|
delete posCur[idStringify(id)];
|
|
lengthCur--;
|
|
|
|
callbacks.removedAt(
|
|
id,
|
|
lastSeqArray[posOld[idStringify(id)]].item,
|
|
prevPosition);
|
|
}
|
|
});
|
|
|
|
_.each(posNew, function (pos, idString) {
|
|
var id = idParse(idString);
|
|
if (_.has(posOld, idString)) {
|
|
// specifically for primitive types, compare equality before
|
|
// firing the 'changedAt' callback. otherwise, always fire it
|
|
// because doing a deep EJSON comparison is not guaranteed to
|
|
// work (an array can contain arbitrary objects, and 'transform'
|
|
// can be used on cursors). also, deep diffing is not
|
|
// necessarily the most efficient (if only a specific subfield
|
|
// of the object is later accessed).
|
|
var newItem = seqArray[pos].item;
|
|
var oldItem = lastSeqArray[posOld[idString]].item;
|
|
|
|
if (typeof newItem === 'object' || newItem !== oldItem)
|
|
callbacks.changedAt(id, newItem, oldItem, pos);
|
|
}
|
|
});
|
|
};
|
|
|
|
seqChangedToEmpty = function (lastSeqArray, callbacks) {
|
|
return [];
|
|
};
|
|
|
|
seqChangedToArray = function (lastSeqArray, array, callbacks) {
|
|
var idsUsed = {};
|
|
var seqArray = _.map(array, function (item, index) {
|
|
var id;
|
|
if (typeof item === 'string') {
|
|
// ensure not empty, since other layers (eg DomRange) assume this as well
|
|
id = "-" + item;
|
|
} else if (typeof item === 'number' ||
|
|
typeof item === 'boolean' ||
|
|
item === undefined) {
|
|
id = item;
|
|
} else if (typeof item === 'object') {
|
|
id = (item && item._id) || index;
|
|
} else {
|
|
throw new Error("{{#each}} doesn't support arrays with " +
|
|
"elements of type " + typeof item);
|
|
}
|
|
|
|
var idString = idStringify(id);
|
|
if (idsUsed[idString]) {
|
|
if (typeof item === 'object' && '_id' in item)
|
|
warn("duplicate id " + id + " in", array);
|
|
id = Random.id();
|
|
} else {
|
|
idsUsed[idString] = true;
|
|
}
|
|
|
|
return { _id: id, item: item };
|
|
});
|
|
|
|
return seqArray;
|
|
};
|
|
|
|
seqChangedToCursor = function (lastSeqArray, cursor, callbacks) {
|
|
var initial = true; // are we observing initial data from cursor?
|
|
var seqArray = [];
|
|
|
|
var observeHandle = cursor.observe({
|
|
addedAt: function (document, atIndex, before) {
|
|
if (initial) {
|
|
// keep track of initial data so that we can diff once
|
|
// we exit `observe`.
|
|
if (before !== null)
|
|
throw new Error("Expected initial data from observe in order");
|
|
seqArray.push({ _id: document._id, item: document });
|
|
} else {
|
|
callbacks.addedAt(document._id, document, atIndex, before);
|
|
}
|
|
},
|
|
changedAt: function (newDocument, oldDocument, atIndex) {
|
|
callbacks.changedAt(newDocument._id, newDocument, oldDocument,
|
|
atIndex);
|
|
},
|
|
removedAt: function (oldDocument, atIndex) {
|
|
callbacks.removedAt(oldDocument._id, oldDocument, atIndex);
|
|
},
|
|
movedTo: function (document, fromIndex, toIndex, before) {
|
|
callbacks.movedTo(
|
|
document._id, document, fromIndex, toIndex, before);
|
|
}
|
|
});
|
|
initial = false;
|
|
|
|
return [seqArray, observeHandle];
|
|
};
|