diff --git a/rpg-docs/client/observe/observe.js b/rpg-docs/client/observe/observe.js deleted file mode 100644 index e4a10490..00000000 --- a/rpg-docs/client/observe/observe.js +++ /dev/null @@ -1,1711 +0,0 @@ -/* - * Copyright (c) 2014 The Polymer Project Authors. All rights reserved. - * This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt - * The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt - * The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt - * Code distributed by Google as part of the polymer project is also - * subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt - */ - -(function(global) { - 'use strict'; - - var testingExposeCycleCount = global.testingExposeCycleCount; - - // Detect and do basic sanity checking on Object/Array.observe. - function detectObjectObserve() { - if (typeof Object.observe !== 'function' || - typeof Array.observe !== 'function') { - return false; - } - - var records = []; - - function callback(recs) { - records = recs; - } - - var test = {}; - var arr = []; - Object.observe(test, callback); - Array.observe(arr, callback); - test.id = 1; - test.id = 2; - delete test.id; - arr.push(1, 2); - arr.length = 0; - - Object.deliverChangeRecords(callback); - if (records.length !== 5) - return false; - - if (records[0].type != 'add' || - records[1].type != 'update' || - records[2].type != 'delete' || - records[3].type != 'splice' || - records[4].type != 'splice') { - return false; - } - - Object.unobserve(test, callback); - Array.unobserve(arr, callback); - - return true; - } - - var hasObserve = detectObjectObserve(); - - function detectEval() { - // Don't test for eval if we're running in a Chrome App environment. - // We check for APIs set that only exist in a Chrome App context. - if (typeof chrome !== 'undefined' && chrome.app && chrome.app.runtime) { - return false; - } - - // Firefox OS Apps do not allow eval. This feature detection is very hacky - // but even if some other platform adds support for this function this code - // will continue to work. - if (typeof navigator != 'undefined' && navigator.getDeviceStorage) { - return false; - } - - try { - var f = new Function('', 'return true;'); - return f(); - } catch (ex) { - return false; - } - } - - var hasEval = detectEval(); - - function isIndex(s) { - return +s === s >>> 0 && s !== ''; - } - - function toNumber(s) { - return +s; - } - - function isObject(obj) { - return obj === Object(obj); - } - - var numberIsNaN = global.Number.isNaN || function(value) { - return typeof value === 'number' && global.isNaN(value); - } - - function areSameValue(left, right) { - if (left === right) - return left !== 0 || 1 / left === 1 / right; - if (numberIsNaN(left) && numberIsNaN(right)) - return true; - - return left !== left && right !== right; - } - - var createObject = ('__proto__' in {}) ? - function(obj) { return obj; } : - function(obj) { - var proto = obj.__proto__; - if (!proto) - return obj; - var newObject = Object.create(proto); - Object.getOwnPropertyNames(obj).forEach(function(name) { - Object.defineProperty(newObject, name, - Object.getOwnPropertyDescriptor(obj, name)); - }); - return newObject; - }; - - var identStart = '[\$_a-zA-Z]'; - var identPart = '[\$_a-zA-Z0-9]'; - var identRegExp = new RegExp('^' + identStart + '+' + identPart + '*' + '$'); - - function getPathCharType(char) { - if (char === undefined) - return 'eof'; - - var code = char.charCodeAt(0); - - switch(code) { - case 0x5B: // [ - case 0x5D: // ] - case 0x2E: // . - case 0x22: // " - case 0x27: // ' - case 0x30: // 0 - return char; - - case 0x5F: // _ - case 0x24: // $ - return 'ident'; - - case 0x20: // Space - case 0x09: // Tab - case 0x0A: // Newline - case 0x0D: // Return - case 0xA0: // No-break space - case 0xFEFF: // Byte Order Mark - case 0x2028: // Line Separator - case 0x2029: // Paragraph Separator - return 'ws'; - } - - // a-z, A-Z - if ((0x61 <= code && code <= 0x7A) || (0x41 <= code && code <= 0x5A)) - return 'ident'; - - // 1-9 - if (0x31 <= code && code <= 0x39) - return 'number'; - - return 'else'; - } - - var pathStateMachine = { - 'beforePath': { - 'ws': ['beforePath'], - 'ident': ['inIdent', 'append'], - '[': ['beforeElement'], - 'eof': ['afterPath'] - }, - - 'inPath': { - 'ws': ['inPath'], - '.': ['beforeIdent'], - '[': ['beforeElement'], - 'eof': ['afterPath'] - }, - - 'beforeIdent': { - 'ws': ['beforeIdent'], - 'ident': ['inIdent', 'append'] - }, - - 'inIdent': { - 'ident': ['inIdent', 'append'], - '0': ['inIdent', 'append'], - 'number': ['inIdent', 'append'], - 'ws': ['inPath', 'push'], - '.': ['beforeIdent', 'push'], - '[': ['beforeElement', 'push'], - 'eof': ['afterPath', 'push'] - }, - - 'beforeElement': { - 'ws': ['beforeElement'], - '0': ['afterZero', 'append'], - 'number': ['inIndex', 'append'], - "'": ['inSingleQuote', 'append', ''], - '"': ['inDoubleQuote', 'append', ''] - }, - - 'afterZero': { - 'ws': ['afterElement', 'push'], - ']': ['inPath', 'push'] - }, - - 'inIndex': { - '0': ['inIndex', 'append'], - 'number': ['inIndex', 'append'], - 'ws': ['afterElement'], - ']': ['inPath', 'push'] - }, - - 'inSingleQuote': { - "'": ['afterElement'], - 'eof': ['error'], - 'else': ['inSingleQuote', 'append'] - }, - - 'inDoubleQuote': { - '"': ['afterElement'], - 'eof': ['error'], - 'else': ['inDoubleQuote', 'append'] - }, - - 'afterElement': { - 'ws': ['afterElement'], - ']': ['inPath', 'push'] - } - } - - function noop() {} - - function parsePath(path) { - var keys = []; - var index = -1; - var c, newChar, key, type, transition, action, typeMap, mode = 'beforePath'; - - var actions = { - push: function() { - if (key === undefined) - return; - - keys.push(key); - key = undefined; - }, - - append: function() { - if (key === undefined) - key = newChar - else - key += newChar; - } - }; - - function maybeUnescapeQuote() { - if (index >= path.length) - return; - - var nextChar = path[index + 1]; - if ((mode == 'inSingleQuote' && nextChar == "'") || - (mode == 'inDoubleQuote' && nextChar == '"')) { - index++; - newChar = nextChar; - actions.append(); - return true; - } - } - - while (mode) { - index++; - c = path[index]; - - if (c == '\\' && maybeUnescapeQuote(mode)) - continue; - - type = getPathCharType(c); - typeMap = pathStateMachine[mode]; - transition = typeMap[type] || typeMap['else'] || 'error'; - - if (transition == 'error') - return; // parse error; - - mode = transition[0]; - action = actions[transition[1]] || noop; - newChar = transition[2] === undefined ? c : transition[2]; - action(); - - if (mode === 'afterPath') { - return keys; - } - } - - return; // parse error - } - - function isIdent(s) { - return identRegExp.test(s); - } - - var constructorIsPrivate = {}; - - function Path(parts, privateToken) { - if (privateToken !== constructorIsPrivate) - throw Error('Use Path.get to retrieve path objects'); - - for (var i = 0; i < parts.length; i++) { - this.push(String(parts[i])); - } - - if (hasEval && this.length) { - this.getValueFrom = this.compiledGetValueFromFn(); - } - } - - // TODO(rafaelw): Make simple LRU cache - var pathCache = {}; - - function getPath(pathString) { - if (pathString instanceof Path) - return pathString; - - if (pathString == null || pathString.length == 0) - pathString = ''; - - if (typeof pathString != 'string') { - if (isIndex(pathString.length)) { - // Constructed with array-like (pre-parsed) keys - return new Path(pathString, constructorIsPrivate); - } - - pathString = String(pathString); - } - - var path = pathCache[pathString]; - if (path) - return path; - - var parts = parsePath(pathString); - if (!parts) - return invalidPath; - - var path = new Path(parts, constructorIsPrivate); - pathCache[pathString] = path; - return path; - } - - Path.get = getPath; - - function formatAccessor(key) { - if (isIndex(key)) { - return '[' + key + ']'; - } else { - return '["' + key.replace(/"/g, '\\"') + '"]'; - } - } - - Path.prototype = createObject({ - __proto__: [], - valid: true, - - toString: function() { - var pathString = ''; - for (var i = 0; i < this.length; i++) { - var key = this[i]; - if (isIdent(key)) { - pathString += i ? '.' + key : key; - } else { - pathString += formatAccessor(key); - } - } - - return pathString; - }, - - getValueFrom: function(obj, directObserver) { - for (var i = 0; i < this.length; i++) { - if (obj == null) - return; - obj = obj[this[i]]; - } - return obj; - }, - - iterateObjects: function(obj, observe) { - for (var i = 0; i < this.length; i++) { - if (i) - obj = obj[this[i - 1]]; - if (!isObject(obj)) - return; - observe(obj, this[i]); - } - }, - - compiledGetValueFromFn: function() { - var str = ''; - var pathString = 'obj'; - str += 'if (obj != null'; - var i = 0; - var key; - for (; i < (this.length - 1); i++) { - key = this[i]; - pathString += isIdent(key) ? '.' + key : formatAccessor(key); - str += ' &&\n ' + pathString + ' != null'; - } - str += ')\n'; - - var key = this[i]; - pathString += isIdent(key) ? '.' + key : formatAccessor(key); - - str += ' return ' + pathString + ';\nelse\n return undefined;'; - return new Function('obj', str); - }, - - setValueFrom: function(obj, value) { - if (!this.length) - return false; - - for (var i = 0; i < this.length - 1; i++) { - if (!isObject(obj)) - return false; - obj = obj[this[i]]; - } - - if (!isObject(obj)) - return false; - - obj[this[i]] = value; - return true; - } - }); - - var invalidPath = new Path('', constructorIsPrivate); - invalidPath.valid = false; - invalidPath.getValueFrom = invalidPath.setValueFrom = function() {}; - - var MAX_DIRTY_CHECK_CYCLES = 1000; - - function dirtyCheck(observer) { - var cycles = 0; - while (cycles < MAX_DIRTY_CHECK_CYCLES && observer.check_()) { - cycles++; - } - if (testingExposeCycleCount) - global.dirtyCheckCycleCount = cycles; - - return cycles > 0; - } - - function objectIsEmpty(object) { - for (var prop in object) - return false; - return true; - } - - function diffIsEmpty(diff) { - return objectIsEmpty(diff.added) && - objectIsEmpty(diff.removed) && - objectIsEmpty(diff.changed); - } - - function diffObjectFromOldObject(object, oldObject) { - var added = {}; - var removed = {}; - var changed = {}; - - for (var prop in oldObject) { - var newValue = object[prop]; - - if (newValue !== undefined && newValue === oldObject[prop]) - continue; - - if (!(prop in object)) { - removed[prop] = undefined; - continue; - } - - if (newValue !== oldObject[prop]) - changed[prop] = newValue; - } - - for (var prop in object) { - if (prop in oldObject) - continue; - - added[prop] = object[prop]; - } - - if (Array.isArray(object) && object.length !== oldObject.length) - changed.length = object.length; - - return { - added: added, - removed: removed, - changed: changed - }; - } - - var eomTasks = []; - function runEOMTasks() { - if (!eomTasks.length) - return false; - - for (var i = 0; i < eomTasks.length; i++) { - eomTasks[i](); - } - eomTasks.length = 0; - return true; - } - - var runEOM = hasObserve ? (function(){ - return function(fn) { - return Promise.resolve().then(fn); - } - })() : - (function() { - return function(fn) { - eomTasks.push(fn); - }; - })(); - - var observedObjectCache = []; - - function newObservedObject() { - var observer; - var object; - var discardRecords = false; - var first = true; - - function callback(records) { - if (observer && observer.state_ === OPENED && !discardRecords) - observer.check_(records); - } - - return { - open: function(obs) { - if (observer) - throw Error('ObservedObject in use'); - - if (!first) - Object.deliverChangeRecords(callback); - - observer = obs; - first = false; - }, - observe: function(obj, arrayObserve) { - object = obj; - if (arrayObserve) - Array.observe(object, callback); - else - Object.observe(object, callback); - }, - deliver: function(discard) { - discardRecords = discard; - Object.deliverChangeRecords(callback); - discardRecords = false; - }, - close: function() { - observer = undefined; - Object.unobserve(object, callback); - observedObjectCache.push(this); - } - }; - } - - /* - * The observedSet abstraction is a perf optimization which reduces the total - * number of Object.observe observations of a set of objects. The idea is that - * groups of Observers will have some object dependencies in common and this - * observed set ensures that each object in the transitive closure of - * dependencies is only observed once. The observedSet acts as a write barrier - * such that whenever any change comes through, all Observers are checked for - * changed values. - * - * Note that this optimization is explicitly moving work from setup-time to - * change-time. - * - * TODO(rafaelw): Implement "garbage collection". In order to move work off - * the critical path, when Observers are closed, their observed objects are - * not Object.unobserve(d). As a result, it's possible that if the observedSet - * is kept open, but some Observers have been closed, it could cause "leaks" - * (prevent otherwise collectable objects from being collected). At some - * point, we should implement incremental "gc" which keeps a list of - * observedSets which may need clean-up and does small amounts of cleanup on a - * timeout until all is clean. - */ - - function getObservedObject(observer, object, arrayObserve) { - var dir = observedObjectCache.pop() || newObservedObject(); - dir.open(observer); - dir.observe(object, arrayObserve); - return dir; - } - - var observedSetCache = []; - - function newObservedSet() { - var observerCount = 0; - var observers = []; - var objects = []; - var rootObj; - var rootObjProps; - - function observe(obj, prop) { - if (!obj) - return; - - if (obj === rootObj) - rootObjProps[prop] = true; - - if (objects.indexOf(obj) < 0) { - objects.push(obj); - Object.observe(obj, callback); - } - - observe(Object.getPrototypeOf(obj), prop); - } - - function allRootObjNonObservedProps(recs) { - for (var i = 0; i < recs.length; i++) { - var rec = recs[i]; - if (rec.object !== rootObj || - rootObjProps[rec.name] || - rec.type === 'setPrototype') { - return false; - } - } - return true; - } - - function callback(recs) { - if (allRootObjNonObservedProps(recs)) - return; - - var observer; - for (var i = 0; i < observers.length; i++) { - observer = observers[i]; - if (observer.state_ == OPENED) { - observer.iterateObjects_(observe); - } - } - - for (var i = 0; i < observers.length; i++) { - observer = observers[i]; - if (observer.state_ == OPENED) { - observer.check_(); - } - } - } - - var record = { - objects: objects, - get rootObject() { return rootObj; }, - set rootObject(value) { - rootObj = value; - rootObjProps = {}; - }, - open: function(obs, object) { - observers.push(obs); - observerCount++; - obs.iterateObjects_(observe); - }, - close: function(obs) { - observerCount--; - if (observerCount > 0) { - return; - } - - for (var i = 0; i < objects.length; i++) { - Object.unobserve(objects[i], callback); - Observer.unobservedCount++; - } - - observers.length = 0; - objects.length = 0; - rootObj = undefined; - rootObjProps = undefined; - observedSetCache.push(this); - if (lastObservedSet === this) - lastObservedSet = null; - }, - }; - - return record; - } - - var lastObservedSet; - - function getObservedSet(observer, obj) { - if (!lastObservedSet || lastObservedSet.rootObject !== obj) { - lastObservedSet = observedSetCache.pop() || newObservedSet(); - lastObservedSet.rootObject = obj; - } - lastObservedSet.open(observer, obj); - return lastObservedSet; - } - - var UNOPENED = 0; - var OPENED = 1; - var CLOSED = 2; - var RESETTING = 3; - - var nextObserverId = 1; - - function Observer() { - this.state_ = UNOPENED; - this.callback_ = undefined; - this.target_ = undefined; // TODO(rafaelw): Should be WeakRef - this.directObserver_ = undefined; - this.value_ = undefined; - this.id_ = nextObserverId++; - } - - Observer.prototype = { - open: function(callback, target) { - if (this.state_ != UNOPENED) - throw Error('Observer has already been opened.'); - - addToAll(this); - this.callback_ = callback; - this.target_ = target; - this.connect_(); - this.state_ = OPENED; - return this.value_; - }, - - close: function() { - if (this.state_ != OPENED) - return; - - removeFromAll(this); - this.disconnect_(); - this.value_ = undefined; - this.callback_ = undefined; - this.target_ = undefined; - this.state_ = CLOSED; - }, - - deliver: function() { - if (this.state_ != OPENED) - return; - - dirtyCheck(this); - }, - - report_: function(changes) { - try { - this.callback_.apply(this.target_, changes); - } catch (ex) { - Observer._errorThrownDuringCallback = true; - console.error('Exception caught during observer callback: ' + - (ex.stack || ex)); - } - }, - - discardChanges: function() { - this.check_(undefined, true); - return this.value_; - } - } - - var collectObservers = !hasObserve; - var allObservers; - Observer._allObserversCount = 0; - - if (collectObservers) { - allObservers = []; - } - - function addToAll(observer) { - Observer._allObserversCount++; - if (!collectObservers) - return; - - allObservers.push(observer); - } - - function removeFromAll(observer) { - Observer._allObserversCount--; - } - - var runningMicrotaskCheckpoint = false; - - global.Platform = global.Platform || {}; - - global.Platform.performMicrotaskCheckpoint = function() { - if (runningMicrotaskCheckpoint) - return; - - if (!collectObservers) - return; - - runningMicrotaskCheckpoint = true; - - var cycles = 0; - var anyChanged, toCheck; - - do { - cycles++; - toCheck = allObservers; - allObservers = []; - anyChanged = false; - - for (var i = 0; i < toCheck.length; i++) { - var observer = toCheck[i]; - if (observer.state_ != OPENED) - continue; - - if (observer.check_()) - anyChanged = true; - - allObservers.push(observer); - } - if (runEOMTasks()) - anyChanged = true; - } while (cycles < MAX_DIRTY_CHECK_CYCLES && anyChanged); - - if (testingExposeCycleCount) - global.dirtyCheckCycleCount = cycles; - - runningMicrotaskCheckpoint = false; - }; - - if (collectObservers) { - global.Platform.clearObservers = function() { - allObservers = []; - }; - } - - function ObjectObserver(object) { - Observer.call(this); - this.value_ = object; - this.oldObject_ = undefined; - } - - ObjectObserver.prototype = createObject({ - __proto__: Observer.prototype, - - arrayObserve: false, - - connect_: function(callback, target) { - if (hasObserve) { - this.directObserver_ = getObservedObject(this, this.value_, - this.arrayObserve); - } else { - this.oldObject_ = this.copyObject(this.value_); - } - - }, - - copyObject: function(object) { - var copy = Array.isArray(object) ? [] : {}; - for (var prop in object) { - copy[prop] = object[prop]; - }; - if (Array.isArray(object)) - copy.length = object.length; - return copy; - }, - - check_: function(changeRecords, skipChanges) { - var diff; - var oldValues; - if (hasObserve) { - if (!changeRecords) - return false; - - oldValues = {}; - diff = diffObjectFromChangeRecords(this.value_, changeRecords, - oldValues); - } else { - oldValues = this.oldObject_; - diff = diffObjectFromOldObject(this.value_, this.oldObject_); - } - - if (diffIsEmpty(diff)) - return false; - - if (!hasObserve) - this.oldObject_ = this.copyObject(this.value_); - - this.report_([ - diff.added || {}, - diff.removed || {}, - diff.changed || {}, - function(property) { - return oldValues[property]; - } - ]); - - return true; - }, - - disconnect_: function() { - if (hasObserve) { - this.directObserver_.close(); - this.directObserver_ = undefined; - } else { - this.oldObject_ = undefined; - } - }, - - deliver: function() { - if (this.state_ != OPENED) - return; - - if (hasObserve) - this.directObserver_.deliver(false); - else - dirtyCheck(this); - }, - - discardChanges: function() { - if (this.directObserver_) - this.directObserver_.deliver(true); - else - this.oldObject_ = this.copyObject(this.value_); - - return this.value_; - } - }); - - function ArrayObserver(array) { - if (!Array.isArray(array)) - throw Error('Provided object is not an Array'); - ObjectObserver.call(this, array); - } - - ArrayObserver.prototype = createObject({ - - __proto__: ObjectObserver.prototype, - - arrayObserve: true, - - copyObject: function(arr) { - return arr.slice(); - }, - - check_: function(changeRecords) { - var splices; - if (hasObserve) { - if (!changeRecords) - return false; - splices = projectArraySplices(this.value_, changeRecords); - } else { - splices = calcSplices(this.value_, 0, this.value_.length, - this.oldObject_, 0, this.oldObject_.length); - } - - if (!splices || !splices.length) - return false; - - if (!hasObserve) - this.oldObject_ = this.copyObject(this.value_); - - this.report_([splices]); - return true; - } - }); - - ArrayObserver.applySplices = function(previous, current, splices) { - splices.forEach(function(splice) { - var spliceArgs = [splice.index, splice.removed.length]; - var addIndex = splice.index; - while (addIndex < splice.index + splice.addedCount) { - spliceArgs.push(current[addIndex]); - addIndex++; - } - - Array.prototype.splice.apply(previous, spliceArgs); - }); - }; - - function PathObserver(object, path) { - Observer.call(this); - - this.object_ = object; - this.path_ = getPath(path); - this.directObserver_ = undefined; - } - - PathObserver.prototype = createObject({ - __proto__: Observer.prototype, - - get path() { - return this.path_; - }, - - connect_: function() { - if (hasObserve) - this.directObserver_ = getObservedSet(this, this.object_); - - this.check_(undefined, true); - }, - - disconnect_: function() { - this.value_ = undefined; - - if (this.directObserver_) { - this.directObserver_.close(this); - this.directObserver_ = undefined; - } - }, - - iterateObjects_: function(observe) { - this.path_.iterateObjects(this.object_, observe); - }, - - check_: function(changeRecords, skipChanges) { - var oldValue = this.value_; - this.value_ = this.path_.getValueFrom(this.object_); - if (skipChanges || areSameValue(this.value_, oldValue)) - return false; - - this.report_([this.value_, oldValue, this]); - return true; - }, - - setValue: function(newValue) { - if (this.path_) - this.path_.setValueFrom(this.object_, newValue); - } - }); - - function CompoundObserver(reportChangesOnOpen) { - Observer.call(this); - - this.reportChangesOnOpen_ = reportChangesOnOpen; - this.value_ = []; - this.directObserver_ = undefined; - this.observed_ = []; - } - - var observerSentinel = {}; - - CompoundObserver.prototype = createObject({ - __proto__: Observer.prototype, - - connect_: function() { - if (hasObserve) { - var object; - var needsDirectObserver = false; - for (var i = 0; i < this.observed_.length; i += 2) { - object = this.observed_[i] - if (object !== observerSentinel) { - needsDirectObserver = true; - break; - } - } - - if (needsDirectObserver) - this.directObserver_ = getObservedSet(this, object); - } - - this.check_(undefined, !this.reportChangesOnOpen_); - }, - - disconnect_: function() { - for (var i = 0; i < this.observed_.length; i += 2) { - if (this.observed_[i] === observerSentinel) - this.observed_[i + 1].close(); - } - this.observed_.length = 0; - this.value_.length = 0; - - if (this.directObserver_) { - this.directObserver_.close(this); - this.directObserver_ = undefined; - } - }, - - addPath: function(object, path) { - if (this.state_ != UNOPENED && this.state_ != RESETTING) - throw Error('Cannot add paths once started.'); - - var path = getPath(path); - this.observed_.push(object, path); - if (!this.reportChangesOnOpen_) - return; - var index = this.observed_.length / 2 - 1; - this.value_[index] = path.getValueFrom(object); - }, - - addObserver: function(observer) { - if (this.state_ != UNOPENED && this.state_ != RESETTING) - throw Error('Cannot add observers once started.'); - - this.observed_.push(observerSentinel, observer); - if (!this.reportChangesOnOpen_) - return; - var index = this.observed_.length / 2 - 1; - this.value_[index] = observer.open(this.deliver, this); - }, - - startReset: function() { - if (this.state_ != OPENED) - throw Error('Can only reset while open'); - - this.state_ = RESETTING; - this.disconnect_(); - }, - - finishReset: function() { - if (this.state_ != RESETTING) - throw Error('Can only finishReset after startReset'); - this.state_ = OPENED; - this.connect_(); - - return this.value_; - }, - - iterateObjects_: function(observe) { - var object; - for (var i = 0; i < this.observed_.length; i += 2) { - object = this.observed_[i] - if (object !== observerSentinel) - this.observed_[i + 1].iterateObjects(object, observe) - } - }, - - check_: function(changeRecords, skipChanges) { - var oldValues; - for (var i = 0; i < this.observed_.length; i += 2) { - var object = this.observed_[i]; - var path = this.observed_[i+1]; - var value; - if (object === observerSentinel) { - var observable = path; - value = this.state_ === UNOPENED ? - observable.open(this.deliver, this) : - observable.discardChanges(); - } else { - value = path.getValueFrom(object); - } - - if (skipChanges) { - this.value_[i / 2] = value; - continue; - } - - if (areSameValue(value, this.value_[i / 2])) - continue; - - oldValues = oldValues || []; - oldValues[i / 2] = this.value_[i / 2]; - this.value_[i / 2] = value; - } - - if (!oldValues) - return false; - - // TODO(rafaelw): Having observed_ as the third callback arg here is - // pretty lame API. Fix. - this.report_([this.value_, oldValues, this.observed_]); - return true; - } - }); - - function identFn(value) { return value; } - - function ObserverTransform(observable, getValueFn, setValueFn, - dontPassThroughSet) { - this.callback_ = undefined; - this.target_ = undefined; - this.value_ = undefined; - this.observable_ = observable; - this.getValueFn_ = getValueFn || identFn; - this.setValueFn_ = setValueFn || identFn; - // TODO(rafaelw): This is a temporary hack. PolymerExpressions needs this - // at the moment because of a bug in it's dependency tracking. - this.dontPassThroughSet_ = dontPassThroughSet; - } - - ObserverTransform.prototype = { - open: function(callback, target) { - this.callback_ = callback; - this.target_ = target; - this.value_ = - this.getValueFn_(this.observable_.open(this.observedCallback_, this)); - return this.value_; - }, - - observedCallback_: function(value) { - value = this.getValueFn_(value); - if (areSameValue(value, this.value_)) - return; - var oldValue = this.value_; - this.value_ = value; - this.callback_.call(this.target_, this.value_, oldValue); - }, - - discardChanges: function() { - this.value_ = this.getValueFn_(this.observable_.discardChanges()); - return this.value_; - }, - - deliver: function() { - return this.observable_.deliver(); - }, - - setValue: function(value) { - value = this.setValueFn_(value); - if (!this.dontPassThroughSet_ && this.observable_.setValue) - return this.observable_.setValue(value); - }, - - close: function() { - if (this.observable_) - this.observable_.close(); - this.callback_ = undefined; - this.target_ = undefined; - this.observable_ = undefined; - this.value_ = undefined; - this.getValueFn_ = undefined; - this.setValueFn_ = undefined; - } - } - - var expectedRecordTypes = { - add: true, - update: true, - delete: true - }; - - function diffObjectFromChangeRecords(object, changeRecords, oldValues) { - var added = {}; - var removed = {}; - - for (var i = 0; i < changeRecords.length; i++) { - var record = changeRecords[i]; - if (!expectedRecordTypes[record.type]) { - console.error('Unknown changeRecord type: ' + record.type); - console.error(record); - continue; - } - - if (!(record.name in oldValues)) - oldValues[record.name] = record.oldValue; - - if (record.type == 'update') - continue; - - if (record.type == 'add') { - if (record.name in removed) - delete removed[record.name]; - else - added[record.name] = true; - - continue; - } - - // type = 'delete' - if (record.name in added) { - delete added[record.name]; - delete oldValues[record.name]; - } else { - removed[record.name] = true; - } - } - - for (var prop in added) - added[prop] = object[prop]; - - for (var prop in removed) - removed[prop] = undefined; - - var changed = {}; - for (var prop in oldValues) { - if (prop in added || prop in removed) - continue; - - var newValue = object[prop]; - if (oldValues[prop] !== newValue) - changed[prop] = newValue; - } - - return { - added: added, - removed: removed, - changed: changed - }; - } - - function newSplice(index, removed, addedCount) { - return { - index: index, - removed: removed, - addedCount: addedCount - }; - } - - var EDIT_LEAVE = 0; - var EDIT_UPDATE = 1; - var EDIT_ADD = 2; - var EDIT_DELETE = 3; - - function ArraySplice() {} - - ArraySplice.prototype = { - - // Note: This function is *based* on the computation of the Levenshtein - // "edit" distance. The one change is that "updates" are treated as two - // edits - not one. With Array splices, an update is really a delete - // followed by an add. By retaining this, we optimize for "keeping" the - // maximum array items in the original array. For example: - // - // 'xxxx123' -> '123yyyy' - // - // With 1-edit updates, the shortest path would be just to update all seven - // characters. With 2-edit updates, we delete 4, leave 3, and add 4. This - // leaves the substring '123' intact. - calcEditDistances: function(current, currentStart, currentEnd, - old, oldStart, oldEnd) { - // "Deletion" columns - var rowCount = oldEnd - oldStart + 1; - var columnCount = currentEnd - currentStart + 1; - var distances = new Array(rowCount); - - // "Addition" rows. Initialize null column. - for (var i = 0; i < rowCount; i++) { - distances[i] = new Array(columnCount); - distances[i][0] = i; - } - - // Initialize null row - for (var j = 0; j < columnCount; j++) - distances[0][j] = j; - - for (var i = 1; i < rowCount; i++) { - for (var j = 1; j < columnCount; j++) { - if (this.equals(current[currentStart + j - 1], old[oldStart + i - 1])) - distances[i][j] = distances[i - 1][j - 1]; - else { - var north = distances[i - 1][j] + 1; - var west = distances[i][j - 1] + 1; - distances[i][j] = north < west ? north : west; - } - } - } - - return distances; - }, - - // This starts at the final weight, and walks "backward" by finding - // the minimum previous weight recursively until the origin of the weight - // matrix. - spliceOperationsFromEditDistances: function(distances) { - var i = distances.length - 1; - var j = distances[0].length - 1; - var current = distances[i][j]; - var edits = []; - while (i > 0 || j > 0) { - if (i == 0) { - edits.push(EDIT_ADD); - j--; - continue; - } - if (j == 0) { - edits.push(EDIT_DELETE); - i--; - continue; - } - var northWest = distances[i - 1][j - 1]; - var west = distances[i - 1][j]; - var north = distances[i][j - 1]; - - var min; - if (west < north) - min = west < northWest ? west : northWest; - else - min = north < northWest ? north : northWest; - - if (min == northWest) { - if (northWest == current) { - edits.push(EDIT_LEAVE); - } else { - edits.push(EDIT_UPDATE); - current = northWest; - } - i--; - j--; - } else if (min == west) { - edits.push(EDIT_DELETE); - i--; - current = west; - } else { - edits.push(EDIT_ADD); - j--; - current = north; - } - } - - edits.reverse(); - return edits; - }, - - /** - * Splice Projection functions: - * - * A splice map is a representation of how a previous array of items - * was transformed into a new array of items. Conceptually it is a list of - * tuples of - * - * - * - * which are kept in ascending index order of. The tuple represents that at - * the |index|, |removed| sequence of items were removed, and counting forward - * from |index|, |addedCount| items were added. - */ - - /** - * Lacking individual splice mutation information, the minimal set of - * splices can be synthesized given the previous state and final state of an - * array. The basic approach is to calculate the edit distance matrix and - * choose the shortest path through it. - * - * Complexity: O(l * p) - * l: The length of the current array - * p: The length of the old array - */ - calcSplices: function(current, currentStart, currentEnd, - old, oldStart, oldEnd) { - var prefixCount = 0; - var suffixCount = 0; - - var minLength = Math.min(currentEnd - currentStart, oldEnd - oldStart); - if (currentStart == 0 && oldStart == 0) - prefixCount = this.sharedPrefix(current, old, minLength); - - if (currentEnd == current.length && oldEnd == old.length) - suffixCount = this.sharedSuffix(current, old, minLength - prefixCount); - - currentStart += prefixCount; - oldStart += prefixCount; - currentEnd -= suffixCount; - oldEnd -= suffixCount; - - if (currentEnd - currentStart == 0 && oldEnd - oldStart == 0) - return []; - - if (currentStart == currentEnd) { - var splice = newSplice(currentStart, [], 0); - while (oldStart < oldEnd) - splice.removed.push(old[oldStart++]); - - return [ splice ]; - } else if (oldStart == oldEnd) - return [ newSplice(currentStart, [], currentEnd - currentStart) ]; - - var ops = this.spliceOperationsFromEditDistances( - this.calcEditDistances(current, currentStart, currentEnd, - old, oldStart, oldEnd)); - - var splice = undefined; - var splices = []; - var index = currentStart; - var oldIndex = oldStart; - for (var i = 0; i < ops.length; i++) { - switch(ops[i]) { - case EDIT_LEAVE: - if (splice) { - splices.push(splice); - splice = undefined; - } - - index++; - oldIndex++; - break; - case EDIT_UPDATE: - if (!splice) - splice = newSplice(index, [], 0); - - splice.addedCount++; - index++; - - splice.removed.push(old[oldIndex]); - oldIndex++; - break; - case EDIT_ADD: - if (!splice) - splice = newSplice(index, [], 0); - - splice.addedCount++; - index++; - break; - case EDIT_DELETE: - if (!splice) - splice = newSplice(index, [], 0); - - splice.removed.push(old[oldIndex]); - oldIndex++; - break; - } - } - - if (splice) { - splices.push(splice); - } - return splices; - }, - - sharedPrefix: function(current, old, searchLength) { - for (var i = 0; i < searchLength; i++) - if (!this.equals(current[i], old[i])) - return i; - return searchLength; - }, - - sharedSuffix: function(current, old, searchLength) { - var index1 = current.length; - var index2 = old.length; - var count = 0; - while (count < searchLength && this.equals(current[--index1], old[--index2])) - count++; - - return count; - }, - - calculateSplices: function(current, previous) { - return this.calcSplices(current, 0, current.length, previous, 0, - previous.length); - }, - - equals: function(currentValue, previousValue) { - return currentValue === previousValue; - } - }; - - var arraySplice = new ArraySplice(); - - function calcSplices(current, currentStart, currentEnd, - old, oldStart, oldEnd) { - return arraySplice.calcSplices(current, currentStart, currentEnd, - old, oldStart, oldEnd); - } - - function intersect(start1, end1, start2, end2) { - // Disjoint - if (end1 < start2 || end2 < start1) - return -1; - - // Adjacent - if (end1 == start2 || end2 == start1) - return 0; - - // Non-zero intersect, span1 first - if (start1 < start2) { - if (end1 < end2) - return end1 - start2; // Overlap - else - return end2 - start2; // Contained - } else { - // Non-zero intersect, span2 first - if (end2 < end1) - return end2 - start1; // Overlap - else - return end1 - start1; // Contained - } - } - - function mergeSplice(splices, index, removed, addedCount) { - - var splice = newSplice(index, removed, addedCount); - - var inserted = false; - var insertionOffset = 0; - - for (var i = 0; i < splices.length; i++) { - var current = splices[i]; - current.index += insertionOffset; - - if (inserted) - continue; - - var intersectCount = intersect(splice.index, - splice.index + splice.removed.length, - current.index, - current.index + current.addedCount); - - if (intersectCount >= 0) { - // Merge the two splices - - splices.splice(i, 1); - i--; - - insertionOffset -= current.addedCount - current.removed.length; - - splice.addedCount += current.addedCount - intersectCount; - var deleteCount = splice.removed.length + - current.removed.length - intersectCount; - - if (!splice.addedCount && !deleteCount) { - // merged splice is a noop. discard. - inserted = true; - } else { - var removed = current.removed; - - if (splice.index < current.index) { - // some prefix of splice.removed is prepended to current.removed. - var prepend = splice.removed.slice(0, current.index - splice.index); - Array.prototype.push.apply(prepend, removed); - removed = prepend; - } - - if (splice.index + splice.removed.length > current.index + current.addedCount) { - // some suffix of splice.removed is appended to current.removed. - var append = splice.removed.slice(current.index + current.addedCount - splice.index); - Array.prototype.push.apply(removed, append); - } - - splice.removed = removed; - if (current.index < splice.index) { - splice.index = current.index; - } - } - } else if (splice.index < current.index) { - // Insert splice here. - - inserted = true; - - splices.splice(i, 0, splice); - i++; - - var offset = splice.addedCount - splice.removed.length - current.index += offset; - insertionOffset += offset; - } - } - - if (!inserted) - splices.push(splice); - } - - function createInitialSplices(array, changeRecords) { - var splices = []; - - for (var i = 0; i < changeRecords.length; i++) { - var record = changeRecords[i]; - switch(record.type) { - case 'splice': - mergeSplice(splices, record.index, record.removed.slice(), record.addedCount); - break; - case 'add': - case 'update': - case 'delete': - if (!isIndex(record.name)) - continue; - var index = toNumber(record.name); - if (index < 0) - continue; - mergeSplice(splices, index, [record.oldValue], 1); - break; - default: - console.error('Unexpected record type: ' + JSON.stringify(record)); - break; - } - } - - return splices; - } - - function projectArraySplices(array, changeRecords) { - var splices = []; - - createInitialSplices(array, changeRecords).forEach(function(splice) { - if (splice.addedCount == 1 && splice.removed.length == 1) { - if (splice.removed[0] !== array[splice.index]) - splices.push(splice); - - return - }; - - splices = splices.concat(calcSplices(array, splice.index, splice.index + splice.addedCount, - splice.removed, 0, splice.removed.length)); - }); - - return splices; - } - - // Export the observe-js object for **Node.js**, with - // backwards-compatibility for the old `require()` API. If we're in - // the browser, export as a global object. - - var expose = global; - - if (typeof exports !== 'undefined') { - if (typeof module !== 'undefined' && module.exports) { - expose = exports = module.exports; - } - expose = exports; - } - - expose.Observer = Observer; - expose.Observer.runEOM_ = runEOM; - expose.Observer.observerSentinel_ = observerSentinel; // for testing. - expose.Observer.hasObjectObserve = hasObserve; - expose.ArrayObserver = ArrayObserver; - expose.ArrayObserver.calculateSplices = function(current, previous) { - return arraySplice.calculateSplices(current, previous); - }; - - expose.ArraySplice = ArraySplice; - expose.ObjectObserver = ObjectObserver; - expose.PathObserver = PathObserver; - expose.CompoundObserver = CompoundObserver; - expose.Path = Path; - expose.ObserverTransform = ObserverTransform; - -})(typeof global !== 'undefined' && global && typeof module !== 'undefined' && module ? global : this || window); \ No newline at end of file