diff --git a/lib/Model/ref.js b/lib/Model/ref.js index d0d5e7787..8502d1558 100644 --- a/lib/Model/ref.js +++ b/lib/Model/ref.js @@ -48,11 +48,13 @@ function addIndexListeners(model) { onIndexChange(segments, patchMove); }); function onIndexChange(segments, patch) { - var fromMap = model._refs.fromMap; - for (var from in fromMap) { - var ref = fromMap[from]; + var toPathMap = model._refs.toPathMap; + var refs = toPathMap.get(segments) || []; + + for(var i = 0, len = refs.length; i < len; i++) { + var ref = refs[i]; + var from = ref.from; if (!(ref.updateIndices && - util.contains(segments, ref.toSegments) && ref.toSegments.length > segments.length)) continue; var index = +ref.toSegments[segments.length]; var patched = patch(index); @@ -111,15 +113,15 @@ function addListener(model, type, fn) { // Find cases where an event is emitted on a path where a reference // is pointing. All original mutations happen on the fully dereferenced // location, so this detection only needs to happen in one direction - var toMap = model._refs.toMap; + var toPathMap = model._refs.toPathMap; var subpath; for (var i = 0, len = segments.length; i < len; i++) { subpath = (subpath) ? subpath + '.' + segments[i] : segments[i]; // If a ref is found pointing to a matching subpath, re-emit on the // place where the reference is coming from as if the mutation also // occured at that path - var refs = toMap[subpath]; - if (!refs) continue; + var refs = toPathMap.get(subpath.split('.'), true); + if (!refs.length) continue; var remaining = segments.slice(i + 1); for (var refIndex = 0, numRefs = refs.length; refIndex < numRefs; refIndex++) { var ref = refs[refIndex]; @@ -138,9 +140,9 @@ function addListener(model, type, fn) { } // If a ref points to a child of a matching subpath, get the value in // case it has changed and set if different - var parentToMap = model._refs.parentToMap; - var refs = parentToMap[subpath]; - if (!refs) return; + var parentToPathMap = model._refs.parentToPathMap; + var refs = parentToPathMap.get(subpath.split('.'), true); + if (!refs.length) return; for (var refIndex = 0, numRefs = refs.length; refIndex < numRefs; refIndex++) { var ref = refs[refIndex]; var value = model._get(ref.toSegments); @@ -208,9 +210,16 @@ Model.prototype.removeAllRefs = function(subpath) { this._removeAllRefs(segments); }; Model.prototype._removeAllRefs = function(segments) { - this._removeMapRefs(segments, this.root._refs.fromMap); + this._removePathMapRefs(segments, this.root._refs.fromPathMap); this._removeMapRefs(segments, this.root._refLists.fromMap); }; +Model.prototype._removePathMapRefs = function(segments, map) { + var refs = map.getList(segments); + for(var i = 0, len = refs.length; i < len; i++) { + var ref = refs[i]; + this._removeRef(ref.fromSegments, ref.from); + } +}; Model.prototype._removeMapRefs = function(segments, map) { for (var from in map) { var fromSegments = map[from].fromSegments; @@ -227,7 +236,7 @@ Model.prototype.dereference = function(subpath) { Model.prototype._dereference = function(segments, forArrayMutator, ignore) { if (segments.length === 0) return segments; - var refs = this.root._refs.fromMap; + var refs = this.root._refs.fromPathMap; var refLists = this.root._refLists.fromMap; var doAgain; do { @@ -236,7 +245,7 @@ Model.prototype._dereference = function(segments, forArrayMutator, ignore) { for (var i = 0, len = segments.length; i < len; i++) { subpath = (subpath) ? subpath + '.' + segments[i] : segments[i]; - var ref = refs[subpath]; + var ref = refs.get(subpath.split('.')); if (ref) { var remaining = segments.slice(i + 1); segments = ref.toSegments.concat(remaining); @@ -278,53 +287,214 @@ function Ref(model, from, to, options) { } this.updateIndices = options && options.updateIndices; } -function FromMap() {} -function ToMap() {} function Refs() { - this.fromMap = new FromMap(); - this.toMap = new ToMap(); - this.parentToMap = new ToMap(); + this.parentToPathMap = new PathListMap(); + this.toPathMap = new PathListMap(); + this.fromPathMap = new PathMap(); } Refs.prototype.add = function(ref) { - this.fromMap[ref.from] = ref; - listMapAdd(this.toMap, ref.to, ref); + this.fromPathMap.add(ref.fromSegments, ref); + this.toPathMap.add(ref.toSegments, ref); for (var i = 0, len = ref.parentTos.length; i < len; i++) { - listMapAdd(this.parentToMap, ref.parentTos[i], ref); + this.parentToPathMap.add(ref.parentTos[i].split('.'), ref); } }; Refs.prototype.remove = function(from) { - var ref = this.fromMap[from]; + var ref = this.fromPathMap.get((from || '').split('.')); if (!ref) return; - delete this.fromMap[from]; - listMapRemove(this.toMap, ref.to, ref); + this.fromPathMap.delete(ref.fromSegments); + this.toPathMap.delete(ref.toSegments, ref); for (var i = 0, len = ref.parentTos.length; i < len; i++) { - listMapRemove(this.parentToMap, ref.parentTos[i], ref); + this.parentToPathMap.delete(ref.parentTos[i].split('.'), ref); } return ref; }; Refs.prototype.toJSON = function() { var out = []; - for (var from in this.fromMap) { - var ref = this.fromMap[from]; + var refs = this.fromPathMap.getList([]); + + for(var i = 0, len = refs.length; i < len; i++) { + var ref = refs[i]; out.push([ref.from, ref.to]); } return out; }; -function listMapAdd(map, name, item) { - map[name] || (map[name] = []); - map[name].push(item); +function PathMap() { + this.map = {}; +} + +PathMap.prototype.add = function (segments, item) { + var map = this.map; + + for(var i = 0, len = segments.length - 1; i < len; i++) { + map[segments[i]] = map[segments[i]] || {}; + map = map[segments[i]]; + } + + map[segments[segments.length - 1]] = {"$item": item}; +}; + +PathMap.prototype.get = function (segments) { + var val = this._get(segments); + + return (val && val['$item']) ? val['$item'] : void 0; +}; + +PathMap.prototype._get = function (segments) { + var val = this.map; + + for(var i = 0, len = segments.length; i < len; i++) { + val = val[segments[i]]; + if(!val) return; + } + + return val; +}; + +PathMap.prototype.getList = function (segments) { + var obj = this._get(segments); + + return flattenObj(obj); +}; + +function flattenObj(obj) { + if(!obj) return []; + + var arr = []; + var keys = Object.keys(obj); + if(obj['$item']) arr.push(obj['$item']); + + for(var i = 0, len = keys.length; i < len; i++) { + if(keys[i] === '$item') continue; + + arr = arr.concat(flattenObj(obj[keys[i]])); + } + + return arr; +}; + +PathMap.prototype.delete = function (segments) { + del(this.map, segments.slice(0), true); +}; + +function del(map, segments, safe) { + var segment = segments.shift(); + + if(!segments.length) { + if(safe) { + delete map[segment]; + return false; + } else { + return true; + } + } + + var nextMap = map[segment]; + if(!nextMap) return true; + + var nextSafe = (Object.keys(nextMap).length > 1); + var remove = del(nextMap, segments, nextSafe); + + if(remove) { + if(safe) { + delete map[segment]; + return false; + } else { + return true; + } + } +} + +function PathListMap() { + this.map = {}; +} + +PathListMap.prototype.add = function (segments, item) { + var map = this.map; + + for(var i = 0, len = segments.length - 1; i < len; i++) { + map[segments[i]] = map[segments[i]] || {"$items": []}; + map = map[segments[i]]; + } + + var segment = segments[segments.length - 1]; + + map[segment] = map[segment] || {"$items": []}; + map[segment]['$items'].push(item); +}; + +PathListMap.prototype.get = function (segments, onlyAtLevel) { + var val = this.map; + + for(var i = 0, len = segments.length; i < len; i++) { + val = val[segments[i]]; + if(!val) return []; + } + + if(onlyAtLevel) return (val['$items'] || []); + + return flatten(val); +}; + +function flatten(obj) { + var arr = obj['$items'] || []; + var keys = Object.keys(obj); + + for(var i = 0, len = keys.length; i < len; i++) { + if(keys[i] === '$items') continue; + + arr.concat(flatten(obj[i])); + } + + return arr; } -function listMapRemove(map, name, item) { - var items = map[name]; - if (!items) return; - var index = items.indexOf(item); - if (index === -1) return; - items.splice(index, 1); - if (!items.length) delete map[name]; +PathListMap.prototype.delete = function (segments, item) { + delList(this.map, segments.slice(0), item, true); +}; + +function delList(map, segments, item, safe) { + var segment = segments.shift(); + + if(!segments.length) { + if(!map[segment] || !map[segment]['$items']) return true; + + var items = map[segment]['$items']; + var keys = Object.keys(map[segment]); + + if(items.length < 2 && keys.length < 2) { + if(safe) { + delete map[segment]; + return false; + } else { + return true; + } + } else { + var i = items.indexOf(item); + + if(i > -1) items.splice(i, 1); + + return false; + } + } + + var nextMap = map[segment]; + if(!nextMap) return true; + + var nextSafe = (Object.keys(nextMap).length > 2 || nextMap['$items'].length); + var remove = delList(nextMap, segments, item, nextSafe); + + if(remove) { + if(safe) { + delete map[segment]; + return false; + } else { + return true; + } + } }