| OLD | NEW |
| (Empty) |
| 1 /* | |
| 2 * Copyright (c) 2014 The Polymer Project Authors. All rights reserved. | |
| 3 * This code may only be used under the BSD style license found at http://polyme
r.github.io/LICENSE.txt | |
| 4 * The complete set of authors may be found at http://polymer.github.io/AUTHORS.
txt | |
| 5 * The complete set of contributors may be found at http://polymer.github.io/CON
TRIBUTORS.txt | |
| 6 * Code distributed by Google as part of the polymer project is also | |
| 7 * subject to an additional IP rights grant found at http://polymer.github.io/PA
TENTS.txt | |
| 8 */ | |
| 9 | |
| 10 (function(global) { | |
| 11 'use strict'; | |
| 12 | |
| 13 var testingExposeCycleCount = global.testingExposeCycleCount; | |
| 14 | |
| 15 // Detect and do basic sanity checking on Object/Array.observe. | |
| 16 function detectObjectObserve() { | |
| 17 if (typeof Object.observe !== 'function' || | |
| 18 typeof Array.observe !== 'function') { | |
| 19 return false; | |
| 20 } | |
| 21 | |
| 22 var records = []; | |
| 23 | |
| 24 function callback(recs) { | |
| 25 records = recs; | |
| 26 } | |
| 27 | |
| 28 var test = {}; | |
| 29 var arr = []; | |
| 30 Object.observe(test, callback); | |
| 31 Array.observe(arr, callback); | |
| 32 test.id = 1; | |
| 33 test.id = 2; | |
| 34 delete test.id; | |
| 35 arr.push(1, 2); | |
| 36 arr.length = 0; | |
| 37 | |
| 38 Object.deliverChangeRecords(callback); | |
| 39 if (records.length !== 5) | |
| 40 return false; | |
| 41 | |
| 42 if (records[0].type != 'add' || | |
| 43 records[1].type != 'update' || | |
| 44 records[2].type != 'delete' || | |
| 45 records[3].type != 'splice' || | |
| 46 records[4].type != 'splice') { | |
| 47 return false; | |
| 48 } | |
| 49 | |
| 50 Object.unobserve(test, callback); | |
| 51 Array.unobserve(arr, callback); | |
| 52 | |
| 53 return true; | |
| 54 } | |
| 55 | |
| 56 var hasObserve = detectObjectObserve(); | |
| 57 | |
| 58 function detectEval() { | |
| 59 // Don't test for eval if we're running in a Chrome App environment. | |
| 60 // We check for APIs set that only exist in a Chrome App context. | |
| 61 if (typeof chrome !== 'undefined' && chrome.app && chrome.app.runtime) { | |
| 62 return false; | |
| 63 } | |
| 64 | |
| 65 // Firefox OS Apps do not allow eval. This feature detection is very hacky | |
| 66 // but even if some other platform adds support for this function this code | |
| 67 // will continue to work. | |
| 68 if (typeof navigator != 'undefined' && navigator.getDeviceStorage) { | |
| 69 return false; | |
| 70 } | |
| 71 | |
| 72 try { | |
| 73 var f = new Function('', 'return true;'); | |
| 74 return f(); | |
| 75 } catch (ex) { | |
| 76 return false; | |
| 77 } | |
| 78 } | |
| 79 | |
| 80 var hasEval = detectEval(); | |
| 81 | |
| 82 function isIndex(s) { | |
| 83 return +s === s >>> 0 && s !== ''; | |
| 84 } | |
| 85 | |
| 86 function toNumber(s) { | |
| 87 return +s; | |
| 88 } | |
| 89 | |
| 90 function isObject(obj) { | |
| 91 return obj === Object(obj); | |
| 92 } | |
| 93 | |
| 94 var numberIsNaN = global.Number.isNaN || function(value) { | |
| 95 return typeof value === 'number' && global.isNaN(value); | |
| 96 } | |
| 97 | |
| 98 function areSameValue(left, right) { | |
| 99 if (left === right) | |
| 100 return left !== 0 || 1 / left === 1 / right; | |
| 101 if (numberIsNaN(left) && numberIsNaN(right)) | |
| 102 return true; | |
| 103 | |
| 104 return left !== left && right !== right; | |
| 105 } | |
| 106 | |
| 107 var createObject = ('__proto__' in {}) ? | |
| 108 function(obj) { return obj; } : | |
| 109 function(obj) { | |
| 110 var proto = obj.__proto__; | |
| 111 if (!proto) | |
| 112 return obj; | |
| 113 var newObject = Object.create(proto); | |
| 114 Object.getOwnPropertyNames(obj).forEach(function(name) { | |
| 115 Object.defineProperty(newObject, name, | |
| 116 Object.getOwnPropertyDescriptor(obj, name)); | |
| 117 }); | |
| 118 return newObject; | |
| 119 }; | |
| 120 | |
| 121 var identStart = '[\$_a-zA-Z]'; | |
| 122 var identPart = '[\$_a-zA-Z0-9]'; | |
| 123 var identRegExp = new RegExp('^' + identStart + '+' + identPart + '*' + '$'); | |
| 124 | |
| 125 function getPathCharType(char) { | |
| 126 if (char === undefined) | |
| 127 return 'eof'; | |
| 128 | |
| 129 var code = char.charCodeAt(0); | |
| 130 | |
| 131 switch(code) { | |
| 132 case 0x5B: // [ | |
| 133 case 0x5D: // ] | |
| 134 case 0x2E: // . | |
| 135 case 0x22: // " | |
| 136 case 0x27: // ' | |
| 137 case 0x30: // 0 | |
| 138 return char; | |
| 139 | |
| 140 case 0x5F: // _ | |
| 141 case 0x24: // $ | |
| 142 return 'ident'; | |
| 143 | |
| 144 case 0x20: // Space | |
| 145 case 0x09: // Tab | |
| 146 case 0x0A: // Newline | |
| 147 case 0x0D: // Return | |
| 148 case 0xA0: // No-break space | |
| 149 case 0xFEFF: // Byte Order Mark | |
| 150 case 0x2028: // Line Separator | |
| 151 case 0x2029: // Paragraph Separator | |
| 152 return 'ws'; | |
| 153 } | |
| 154 | |
| 155 // a-z, A-Z | |
| 156 if ((0x61 <= code && code <= 0x7A) || (0x41 <= code && code <= 0x5A)) | |
| 157 return 'ident'; | |
| 158 | |
| 159 // 1-9 | |
| 160 if (0x31 <= code && code <= 0x39) | |
| 161 return 'number'; | |
| 162 | |
| 163 return 'else'; | |
| 164 } | |
| 165 | |
| 166 var pathStateMachine = { | |
| 167 'beforePath': { | |
| 168 'ws': ['beforePath'], | |
| 169 'ident': ['inIdent', 'append'], | |
| 170 '[': ['beforeElement'], | |
| 171 'eof': ['afterPath'] | |
| 172 }, | |
| 173 | |
| 174 'inPath': { | |
| 175 'ws': ['inPath'], | |
| 176 '.': ['beforeIdent'], | |
| 177 '[': ['beforeElement'], | |
| 178 'eof': ['afterPath'] | |
| 179 }, | |
| 180 | |
| 181 'beforeIdent': { | |
| 182 'ws': ['beforeIdent'], | |
| 183 'ident': ['inIdent', 'append'] | |
| 184 }, | |
| 185 | |
| 186 'inIdent': { | |
| 187 'ident': ['inIdent', 'append'], | |
| 188 '0': ['inIdent', 'append'], | |
| 189 'number': ['inIdent', 'append'], | |
| 190 'ws': ['inPath', 'push'], | |
| 191 '.': ['beforeIdent', 'push'], | |
| 192 '[': ['beforeElement', 'push'], | |
| 193 'eof': ['afterPath', 'push'] | |
| 194 }, | |
| 195 | |
| 196 'beforeElement': { | |
| 197 'ws': ['beforeElement'], | |
| 198 '0': ['afterZero', 'append'], | |
| 199 'number': ['inIndex', 'append'], | |
| 200 "'": ['inSingleQuote', 'append', ''], | |
| 201 '"': ['inDoubleQuote', 'append', ''] | |
| 202 }, | |
| 203 | |
| 204 'afterZero': { | |
| 205 'ws': ['afterElement', 'push'], | |
| 206 ']': ['inPath', 'push'] | |
| 207 }, | |
| 208 | |
| 209 'inIndex': { | |
| 210 '0': ['inIndex', 'append'], | |
| 211 'number': ['inIndex', 'append'], | |
| 212 'ws': ['afterElement'], | |
| 213 ']': ['inPath', 'push'] | |
| 214 }, | |
| 215 | |
| 216 'inSingleQuote': { | |
| 217 "'": ['afterElement'], | |
| 218 'eof': ['error'], | |
| 219 'else': ['inSingleQuote', 'append'] | |
| 220 }, | |
| 221 | |
| 222 'inDoubleQuote': { | |
| 223 '"': ['afterElement'], | |
| 224 'eof': ['error'], | |
| 225 'else': ['inDoubleQuote', 'append'] | |
| 226 }, | |
| 227 | |
| 228 'afterElement': { | |
| 229 'ws': ['afterElement'], | |
| 230 ']': ['inPath', 'push'] | |
| 231 } | |
| 232 } | |
| 233 | |
| 234 function noop() {} | |
| 235 | |
| 236 function parsePath(path) { | |
| 237 var keys = []; | |
| 238 var index = -1; | |
| 239 var c, newChar, key, type, transition, action, typeMap, mode = 'beforePath'; | |
| 240 | |
| 241 var actions = { | |
| 242 push: function() { | |
| 243 if (key === undefined) | |
| 244 return; | |
| 245 | |
| 246 keys.push(key); | |
| 247 key = undefined; | |
| 248 }, | |
| 249 | |
| 250 append: function() { | |
| 251 if (key === undefined) | |
| 252 key = newChar | |
| 253 else | |
| 254 key += newChar; | |
| 255 } | |
| 256 }; | |
| 257 | |
| 258 function maybeUnescapeQuote() { | |
| 259 if (index >= path.length) | |
| 260 return; | |
| 261 | |
| 262 var nextChar = path[index + 1]; | |
| 263 if ((mode == 'inSingleQuote' && nextChar == "'") || | |
| 264 (mode == 'inDoubleQuote' && nextChar == '"')) { | |
| 265 index++; | |
| 266 newChar = nextChar; | |
| 267 actions.append(); | |
| 268 return true; | |
| 269 } | |
| 270 } | |
| 271 | |
| 272 while (mode) { | |
| 273 index++; | |
| 274 c = path[index]; | |
| 275 | |
| 276 if (c == '\\' && maybeUnescapeQuote(mode)) | |
| 277 continue; | |
| 278 | |
| 279 type = getPathCharType(c); | |
| 280 typeMap = pathStateMachine[mode]; | |
| 281 transition = typeMap[type] || typeMap['else'] || 'error'; | |
| 282 | |
| 283 if (transition == 'error') | |
| 284 return; // parse error; | |
| 285 | |
| 286 mode = transition[0]; | |
| 287 action = actions[transition[1]] || noop; | |
| 288 newChar = transition[2] === undefined ? c : transition[2]; | |
| 289 action(); | |
| 290 | |
| 291 if (mode === 'afterPath') { | |
| 292 return keys; | |
| 293 } | |
| 294 } | |
| 295 | |
| 296 return; // parse error | |
| 297 } | |
| 298 | |
| 299 function isIdent(s) { | |
| 300 return identRegExp.test(s); | |
| 301 } | |
| 302 | |
| 303 var constructorIsPrivate = {}; | |
| 304 | |
| 305 function Path(parts, privateToken) { | |
| 306 if (privateToken !== constructorIsPrivate) | |
| 307 throw Error('Use Path.get to retrieve path objects'); | |
| 308 | |
| 309 for (var i = 0; i < parts.length; i++) { | |
| 310 this.push(String(parts[i])); | |
| 311 } | |
| 312 | |
| 313 if (hasEval && this.length) { | |
| 314 this.getValueFrom = this.compiledGetValueFromFn(); | |
| 315 } | |
| 316 } | |
| 317 | |
| 318 // TODO(rafaelw): Make simple LRU cache | |
| 319 var pathCache = {}; | |
| 320 | |
| 321 function getPath(pathString) { | |
| 322 if (pathString instanceof Path) | |
| 323 return pathString; | |
| 324 | |
| 325 if (pathString == null || pathString.length == 0) | |
| 326 pathString = ''; | |
| 327 | |
| 328 if (typeof pathString != 'string') { | |
| 329 if (isIndex(pathString.length)) { | |
| 330 // Constructed with array-like (pre-parsed) keys | |
| 331 return new Path(pathString, constructorIsPrivate); | |
| 332 } | |
| 333 | |
| 334 pathString = String(pathString); | |
| 335 } | |
| 336 | |
| 337 var path = pathCache[pathString]; | |
| 338 if (path) | |
| 339 return path; | |
| 340 | |
| 341 var parts = parsePath(pathString); | |
| 342 if (!parts) | |
| 343 return invalidPath; | |
| 344 | |
| 345 var path = new Path(parts, constructorIsPrivate); | |
| 346 pathCache[pathString] = path; | |
| 347 return path; | |
| 348 } | |
| 349 | |
| 350 Path.get = getPath; | |
| 351 | |
| 352 function formatAccessor(key) { | |
| 353 if (isIndex(key)) { | |
| 354 return '[' + key + ']'; | |
| 355 } else { | |
| 356 return '["' + key.replace(/"/g, '\\"') + '"]'; | |
| 357 } | |
| 358 } | |
| 359 | |
| 360 Path.prototype = createObject({ | |
| 361 __proto__: [], | |
| 362 valid: true, | |
| 363 | |
| 364 toString: function() { | |
| 365 var pathString = ''; | |
| 366 for (var i = 0; i < this.length; i++) { | |
| 367 var key = this[i]; | |
| 368 if (isIdent(key)) { | |
| 369 pathString += i ? '.' + key : key; | |
| 370 } else { | |
| 371 pathString += formatAccessor(key); | |
| 372 } | |
| 373 } | |
| 374 | |
| 375 return pathString; | |
| 376 }, | |
| 377 | |
| 378 getValueFrom: function(obj, directObserver) { | |
| 379 for (var i = 0; i < this.length; i++) { | |
| 380 if (obj == null) | |
| 381 return; | |
| 382 obj = obj[this[i]]; | |
| 383 } | |
| 384 return obj; | |
| 385 }, | |
| 386 | |
| 387 iterateObjects: function(obj, observe) { | |
| 388 for (var i = 0; i < this.length; i++) { | |
| 389 if (i) | |
| 390 obj = obj[this[i - 1]]; | |
| 391 if (!isObject(obj)) | |
| 392 return; | |
| 393 observe(obj, this[i]); | |
| 394 } | |
| 395 }, | |
| 396 | |
| 397 compiledGetValueFromFn: function() { | |
| 398 var str = ''; | |
| 399 var pathString = 'obj'; | |
| 400 str += 'if (obj != null'; | |
| 401 var i = 0; | |
| 402 var key; | |
| 403 for (; i < (this.length - 1); i++) { | |
| 404 key = this[i]; | |
| 405 pathString += isIdent(key) ? '.' + key : formatAccessor(key); | |
| 406 str += ' &&\n ' + pathString + ' != null'; | |
| 407 } | |
| 408 str += ')\n'; | |
| 409 | |
| 410 var key = this[i]; | |
| 411 pathString += isIdent(key) ? '.' + key : formatAccessor(key); | |
| 412 | |
| 413 str += ' return ' + pathString + ';\nelse\n return undefined;'; | |
| 414 return new Function('obj', str); | |
| 415 }, | |
| 416 | |
| 417 setValueFrom: function(obj, value) { | |
| 418 if (!this.length) | |
| 419 return false; | |
| 420 | |
| 421 for (var i = 0; i < this.length - 1; i++) { | |
| 422 if (!isObject(obj)) | |
| 423 return false; | |
| 424 obj = obj[this[i]]; | |
| 425 } | |
| 426 | |
| 427 if (!isObject(obj)) | |
| 428 return false; | |
| 429 | |
| 430 obj[this[i]] = value; | |
| 431 return true; | |
| 432 } | |
| 433 }); | |
| 434 | |
| 435 var invalidPath = new Path('', constructorIsPrivate); | |
| 436 invalidPath.valid = false; | |
| 437 invalidPath.getValueFrom = invalidPath.setValueFrom = function() {}; | |
| 438 | |
| 439 var MAX_DIRTY_CHECK_CYCLES = 1000; | |
| 440 | |
| 441 function dirtyCheck(observer) { | |
| 442 var cycles = 0; | |
| 443 while (cycles < MAX_DIRTY_CHECK_CYCLES && observer.check_()) { | |
| 444 cycles++; | |
| 445 } | |
| 446 if (testingExposeCycleCount) | |
| 447 global.dirtyCheckCycleCount = cycles; | |
| 448 | |
| 449 return cycles > 0; | |
| 450 } | |
| 451 | |
| 452 function objectIsEmpty(object) { | |
| 453 for (var prop in object) | |
| 454 return false; | |
| 455 return true; | |
| 456 } | |
| 457 | |
| 458 function diffIsEmpty(diff) { | |
| 459 return objectIsEmpty(diff.added) && | |
| 460 objectIsEmpty(diff.removed) && | |
| 461 objectIsEmpty(diff.changed); | |
| 462 } | |
| 463 | |
| 464 function diffObjectFromOldObject(object, oldObject) { | |
| 465 var added = {}; | |
| 466 var removed = {}; | |
| 467 var changed = {}; | |
| 468 | |
| 469 for (var prop in oldObject) { | |
| 470 var newValue = object[prop]; | |
| 471 | |
| 472 if (newValue !== undefined && newValue === oldObject[prop]) | |
| 473 continue; | |
| 474 | |
| 475 if (!(prop in object)) { | |
| 476 removed[prop] = undefined; | |
| 477 continue; | |
| 478 } | |
| 479 | |
| 480 if (newValue !== oldObject[prop]) | |
| 481 changed[prop] = newValue; | |
| 482 } | |
| 483 | |
| 484 for (var prop in object) { | |
| 485 if (prop in oldObject) | |
| 486 continue; | |
| 487 | |
| 488 added[prop] = object[prop]; | |
| 489 } | |
| 490 | |
| 491 if (Array.isArray(object) && object.length !== oldObject.length) | |
| 492 changed.length = object.length; | |
| 493 | |
| 494 return { | |
| 495 added: added, | |
| 496 removed: removed, | |
| 497 changed: changed | |
| 498 }; | |
| 499 } | |
| 500 | |
| 501 var eomTasks = []; | |
| 502 function runEOMTasks() { | |
| 503 if (!eomTasks.length) | |
| 504 return false; | |
| 505 | |
| 506 for (var i = 0; i < eomTasks.length; i++) { | |
| 507 eomTasks[i](); | |
| 508 } | |
| 509 eomTasks.length = 0; | |
| 510 return true; | |
| 511 } | |
| 512 | |
| 513 var runEOM = hasObserve ? (function(){ | |
| 514 return function(fn) { | |
| 515 return Promise.resolve().then(fn); | |
| 516 } | |
| 517 })() : | |
| 518 (function() { | |
| 519 return function(fn) { | |
| 520 eomTasks.push(fn); | |
| 521 }; | |
| 522 })(); | |
| 523 | |
| 524 var observedObjectCache = []; | |
| 525 | |
| 526 function newObservedObject() { | |
| 527 var observer; | |
| 528 var object; | |
| 529 var discardRecords = false; | |
| 530 var first = true; | |
| 531 | |
| 532 function callback(records) { | |
| 533 if (observer && observer.state_ === OPENED && !discardRecords) | |
| 534 observer.check_(records); | |
| 535 } | |
| 536 | |
| 537 return { | |
| 538 open: function(obs) { | |
| 539 if (observer) | |
| 540 throw Error('ObservedObject in use'); | |
| 541 | |
| 542 if (!first) | |
| 543 Object.deliverChangeRecords(callback); | |
| 544 | |
| 545 observer = obs; | |
| 546 first = false; | |
| 547 }, | |
| 548 observe: function(obj, arrayObserve) { | |
| 549 object = obj; | |
| 550 if (arrayObserve) | |
| 551 Array.observe(object, callback); | |
| 552 else | |
| 553 Object.observe(object, callback); | |
| 554 }, | |
| 555 deliver: function(discard) { | |
| 556 discardRecords = discard; | |
| 557 Object.deliverChangeRecords(callback); | |
| 558 discardRecords = false; | |
| 559 }, | |
| 560 close: function() { | |
| 561 observer = undefined; | |
| 562 Object.unobserve(object, callback); | |
| 563 observedObjectCache.push(this); | |
| 564 } | |
| 565 }; | |
| 566 } | |
| 567 | |
| 568 /* | |
| 569 * The observedSet abstraction is a perf optimization which reduces the total | |
| 570 * number of Object.observe observations of a set of objects. The idea is that | |
| 571 * groups of Observers will have some object dependencies in common and this | |
| 572 * observed set ensures that each object in the transitive closure of | |
| 573 * dependencies is only observed once. The observedSet acts as a write barrier | |
| 574 * such that whenever any change comes through, all Observers are checked for | |
| 575 * changed values. | |
| 576 * | |
| 577 * Note that this optimization is explicitly moving work from setup-time to | |
| 578 * change-time. | |
| 579 * | |
| 580 * TODO(rafaelw): Implement "garbage collection". In order to move work off | |
| 581 * the critical path, when Observers are closed, their observed objects are | |
| 582 * not Object.unobserve(d). As a result, it's possible that if the observedSet | |
| 583 * is kept open, but some Observers have been closed, it could cause "leaks" | |
| 584 * (prevent otherwise collectable objects from being collected). At some | |
| 585 * point, we should implement incremental "gc" which keeps a list of | |
| 586 * observedSets which may need clean-up and does small amounts of cleanup on a | |
| 587 * timeout until all is clean. | |
| 588 */ | |
| 589 | |
| 590 function getObservedObject(observer, object, arrayObserve) { | |
| 591 var dir = observedObjectCache.pop() || newObservedObject(); | |
| 592 dir.open(observer); | |
| 593 dir.observe(object, arrayObserve); | |
| 594 return dir; | |
| 595 } | |
| 596 | |
| 597 var observedSetCache = []; | |
| 598 | |
| 599 function newObservedSet() { | |
| 600 var observerCount = 0; | |
| 601 var observers = []; | |
| 602 var objects = []; | |
| 603 var rootObj; | |
| 604 var rootObjProps; | |
| 605 | |
| 606 function observe(obj, prop) { | |
| 607 if (!obj) | |
| 608 return; | |
| 609 | |
| 610 if (obj === rootObj) | |
| 611 rootObjProps[prop] = true; | |
| 612 | |
| 613 if (objects.indexOf(obj) < 0) { | |
| 614 objects.push(obj); | |
| 615 Object.observe(obj, callback); | |
| 616 } | |
| 617 | |
| 618 observe(Object.getPrototypeOf(obj), prop); | |
| 619 } | |
| 620 | |
| 621 function allRootObjNonObservedProps(recs) { | |
| 622 for (var i = 0; i < recs.length; i++) { | |
| 623 var rec = recs[i]; | |
| 624 if (rec.object !== rootObj || | |
| 625 rootObjProps[rec.name] || | |
| 626 rec.type === 'setPrototype') { | |
| 627 return false; | |
| 628 } | |
| 629 } | |
| 630 return true; | |
| 631 } | |
| 632 | |
| 633 function callback(recs) { | |
| 634 if (allRootObjNonObservedProps(recs)) | |
| 635 return; | |
| 636 | |
| 637 var observer; | |
| 638 for (var i = 0; i < observers.length; i++) { | |
| 639 observer = observers[i]; | |
| 640 if (observer.state_ == OPENED) { | |
| 641 observer.iterateObjects_(observe); | |
| 642 } | |
| 643 } | |
| 644 | |
| 645 for (var i = 0; i < observers.length; i++) { | |
| 646 observer = observers[i]; | |
| 647 if (observer.state_ == OPENED) { | |
| 648 observer.check_(); | |
| 649 } | |
| 650 } | |
| 651 } | |
| 652 | |
| 653 var record = { | |
| 654 objects: objects, | |
| 655 get rootObject() { return rootObj; }, | |
| 656 set rootObject(value) { | |
| 657 rootObj = value; | |
| 658 rootObjProps = {}; | |
| 659 }, | |
| 660 open: function(obs, object) { | |
| 661 observers.push(obs); | |
| 662 observerCount++; | |
| 663 obs.iterateObjects_(observe); | |
| 664 }, | |
| 665 close: function(obs) { | |
| 666 observerCount--; | |
| 667 if (observerCount > 0) { | |
| 668 return; | |
| 669 } | |
| 670 | |
| 671 for (var i = 0; i < objects.length; i++) { | |
| 672 Object.unobserve(objects[i], callback); | |
| 673 Observer.unobservedCount++; | |
| 674 } | |
| 675 | |
| 676 observers.length = 0; | |
| 677 objects.length = 0; | |
| 678 rootObj = undefined; | |
| 679 rootObjProps = undefined; | |
| 680 observedSetCache.push(this); | |
| 681 if (lastObservedSet === this) | |
| 682 lastObservedSet = null; | |
| 683 }, | |
| 684 }; | |
| 685 | |
| 686 return record; | |
| 687 } | |
| 688 | |
| 689 var lastObservedSet; | |
| 690 | |
| 691 function getObservedSet(observer, obj) { | |
| 692 if (!lastObservedSet || lastObservedSet.rootObject !== obj) { | |
| 693 lastObservedSet = observedSetCache.pop() || newObservedSet(); | |
| 694 lastObservedSet.rootObject = obj; | |
| 695 } | |
| 696 lastObservedSet.open(observer, obj); | |
| 697 return lastObservedSet; | |
| 698 } | |
| 699 | |
| 700 var UNOPENED = 0; | |
| 701 var OPENED = 1; | |
| 702 var CLOSED = 2; | |
| 703 var RESETTING = 3; | |
| 704 | |
| 705 var nextObserverId = 1; | |
| 706 | |
| 707 function Observer() { | |
| 708 this.state_ = UNOPENED; | |
| 709 this.callback_ = undefined; | |
| 710 this.target_ = undefined; // TODO(rafaelw): Should be WeakRef | |
| 711 this.directObserver_ = undefined; | |
| 712 this.value_ = undefined; | |
| 713 this.id_ = nextObserverId++; | |
| 714 } | |
| 715 | |
| 716 Observer.prototype = { | |
| 717 open: function(callback, target) { | |
| 718 if (this.state_ != UNOPENED) | |
| 719 throw Error('Observer has already been opened.'); | |
| 720 | |
| 721 addToAll(this); | |
| 722 this.callback_ = callback; | |
| 723 this.target_ = target; | |
| 724 this.connect_(); | |
| 725 this.state_ = OPENED; | |
| 726 return this.value_; | |
| 727 }, | |
| 728 | |
| 729 close: function() { | |
| 730 if (this.state_ != OPENED) | |
| 731 return; | |
| 732 | |
| 733 removeFromAll(this); | |
| 734 this.disconnect_(); | |
| 735 this.value_ = undefined; | |
| 736 this.callback_ = undefined; | |
| 737 this.target_ = undefined; | |
| 738 this.state_ = CLOSED; | |
| 739 }, | |
| 740 | |
| 741 deliver: function() { | |
| 742 if (this.state_ != OPENED) | |
| 743 return; | |
| 744 | |
| 745 dirtyCheck(this); | |
| 746 }, | |
| 747 | |
| 748 report_: function(changes) { | |
| 749 try { | |
| 750 this.callback_.apply(this.target_, changes); | |
| 751 } catch (ex) { | |
| 752 Observer._errorThrownDuringCallback = true; | |
| 753 console.error('Exception caught during observer callback: ' + | |
| 754 (ex.stack || ex)); | |
| 755 } | |
| 756 }, | |
| 757 | |
| 758 discardChanges: function() { | |
| 759 this.check_(undefined, true); | |
| 760 return this.value_; | |
| 761 } | |
| 762 } | |
| 763 | |
| 764 var collectObservers = !hasObserve; | |
| 765 var allObservers; | |
| 766 Observer._allObserversCount = 0; | |
| 767 | |
| 768 if (collectObservers) { | |
| 769 allObservers = []; | |
| 770 } | |
| 771 | |
| 772 function addToAll(observer) { | |
| 773 Observer._allObserversCount++; | |
| 774 if (!collectObservers) | |
| 775 return; | |
| 776 | |
| 777 allObservers.push(observer); | |
| 778 } | |
| 779 | |
| 780 function removeFromAll(observer) { | |
| 781 Observer._allObserversCount--; | |
| 782 } | |
| 783 | |
| 784 var runningMicrotaskCheckpoint = false; | |
| 785 | |
| 786 global.Platform = global.Platform || {}; | |
| 787 | |
| 788 global.Platform.performMicrotaskCheckpoint = function() { | |
| 789 if (runningMicrotaskCheckpoint) | |
| 790 return; | |
| 791 | |
| 792 if (!collectObservers) | |
| 793 return; | |
| 794 | |
| 795 runningMicrotaskCheckpoint = true; | |
| 796 | |
| 797 var cycles = 0; | |
| 798 var anyChanged, toCheck; | |
| 799 | |
| 800 do { | |
| 801 cycles++; | |
| 802 toCheck = allObservers; | |
| 803 allObservers = []; | |
| 804 anyChanged = false; | |
| 805 | |
| 806 for (var i = 0; i < toCheck.length; i++) { | |
| 807 var observer = toCheck[i]; | |
| 808 if (observer.state_ != OPENED) | |
| 809 continue; | |
| 810 | |
| 811 if (observer.check_()) | |
| 812 anyChanged = true; | |
| 813 | |
| 814 allObservers.push(observer); | |
| 815 } | |
| 816 if (runEOMTasks()) | |
| 817 anyChanged = true; | |
| 818 } while (cycles < MAX_DIRTY_CHECK_CYCLES && anyChanged); | |
| 819 | |
| 820 if (testingExposeCycleCount) | |
| 821 global.dirtyCheckCycleCount = cycles; | |
| 822 | |
| 823 runningMicrotaskCheckpoint = false; | |
| 824 }; | |
| 825 | |
| 826 if (collectObservers) { | |
| 827 global.Platform.clearObservers = function() { | |
| 828 allObservers = []; | |
| 829 }; | |
| 830 } | |
| 831 | |
| 832 function ObjectObserver(object) { | |
| 833 Observer.call(this); | |
| 834 this.value_ = object; | |
| 835 this.oldObject_ = undefined; | |
| 836 } | |
| 837 | |
| 838 ObjectObserver.prototype = createObject({ | |
| 839 __proto__: Observer.prototype, | |
| 840 | |
| 841 arrayObserve: false, | |
| 842 | |
| 843 connect_: function(callback, target) { | |
| 844 if (hasObserve) { | |
| 845 this.directObserver_ = getObservedObject(this, this.value_, | |
| 846 this.arrayObserve); | |
| 847 } else { | |
| 848 this.oldObject_ = this.copyObject(this.value_); | |
| 849 } | |
| 850 | |
| 851 }, | |
| 852 | |
| 853 copyObject: function(object) { | |
| 854 var copy = Array.isArray(object) ? [] : {}; | |
| 855 for (var prop in object) { | |
| 856 copy[prop] = object[prop]; | |
| 857 }; | |
| 858 if (Array.isArray(object)) | |
| 859 copy.length = object.length; | |
| 860 return copy; | |
| 861 }, | |
| 862 | |
| 863 check_: function(changeRecords, skipChanges) { | |
| 864 var diff; | |
| 865 var oldValues; | |
| 866 if (hasObserve) { | |
| 867 if (!changeRecords) | |
| 868 return false; | |
| 869 | |
| 870 oldValues = {}; | |
| 871 diff = diffObjectFromChangeRecords(this.value_, changeRecords, | |
| 872 oldValues); | |
| 873 } else { | |
| 874 oldValues = this.oldObject_; | |
| 875 diff = diffObjectFromOldObject(this.value_, this.oldObject_); | |
| 876 } | |
| 877 | |
| 878 if (diffIsEmpty(diff)) | |
| 879 return false; | |
| 880 | |
| 881 if (!hasObserve) | |
| 882 this.oldObject_ = this.copyObject(this.value_); | |
| 883 | |
| 884 this.report_([ | |
| 885 diff.added || {}, | |
| 886 diff.removed || {}, | |
| 887 diff.changed || {}, | |
| 888 function(property) { | |
| 889 return oldValues[property]; | |
| 890 } | |
| 891 ]); | |
| 892 | |
| 893 return true; | |
| 894 }, | |
| 895 | |
| 896 disconnect_: function() { | |
| 897 if (hasObserve) { | |
| 898 this.directObserver_.close(); | |
| 899 this.directObserver_ = undefined; | |
| 900 } else { | |
| 901 this.oldObject_ = undefined; | |
| 902 } | |
| 903 }, | |
| 904 | |
| 905 deliver: function() { | |
| 906 if (this.state_ != OPENED) | |
| 907 return; | |
| 908 | |
| 909 if (hasObserve) | |
| 910 this.directObserver_.deliver(false); | |
| 911 else | |
| 912 dirtyCheck(this); | |
| 913 }, | |
| 914 | |
| 915 discardChanges: function() { | |
| 916 if (this.directObserver_) | |
| 917 this.directObserver_.deliver(true); | |
| 918 else | |
| 919 this.oldObject_ = this.copyObject(this.value_); | |
| 920 | |
| 921 return this.value_; | |
| 922 } | |
| 923 }); | |
| 924 | |
| 925 function ArrayObserver(array) { | |
| 926 if (!Array.isArray(array)) | |
| 927 throw Error('Provided object is not an Array'); | |
| 928 ObjectObserver.call(this, array); | |
| 929 } | |
| 930 | |
| 931 ArrayObserver.prototype = createObject({ | |
| 932 | |
| 933 __proto__: ObjectObserver.prototype, | |
| 934 | |
| 935 arrayObserve: true, | |
| 936 | |
| 937 copyObject: function(arr) { | |
| 938 return arr.slice(); | |
| 939 }, | |
| 940 | |
| 941 check_: function(changeRecords) { | |
| 942 var splices; | |
| 943 if (hasObserve) { | |
| 944 if (!changeRecords) | |
| 945 return false; | |
| 946 splices = projectArraySplices(this.value_, changeRecords); | |
| 947 } else { | |
| 948 splices = calcSplices(this.value_, 0, this.value_.length, | |
| 949 this.oldObject_, 0, this.oldObject_.length); | |
| 950 } | |
| 951 | |
| 952 if (!splices || !splices.length) | |
| 953 return false; | |
| 954 | |
| 955 if (!hasObserve) | |
| 956 this.oldObject_ = this.copyObject(this.value_); | |
| 957 | |
| 958 this.report_([splices]); | |
| 959 return true; | |
| 960 } | |
| 961 }); | |
| 962 | |
| 963 ArrayObserver.applySplices = function(previous, current, splices) { | |
| 964 splices.forEach(function(splice) { | |
| 965 var spliceArgs = [splice.index, splice.removed.length]; | |
| 966 var addIndex = splice.index; | |
| 967 while (addIndex < splice.index + splice.addedCount) { | |
| 968 spliceArgs.push(current[addIndex]); | |
| 969 addIndex++; | |
| 970 } | |
| 971 | |
| 972 Array.prototype.splice.apply(previous, spliceArgs); | |
| 973 }); | |
| 974 }; | |
| 975 | |
| 976 function PathObserver(object, path) { | |
| 977 Observer.call(this); | |
| 978 | |
| 979 this.object_ = object; | |
| 980 this.path_ = getPath(path); | |
| 981 this.directObserver_ = undefined; | |
| 982 } | |
| 983 | |
| 984 PathObserver.prototype = createObject({ | |
| 985 __proto__: Observer.prototype, | |
| 986 | |
| 987 get path() { | |
| 988 return this.path_; | |
| 989 }, | |
| 990 | |
| 991 connect_: function() { | |
| 992 if (hasObserve) | |
| 993 this.directObserver_ = getObservedSet(this, this.object_); | |
| 994 | |
| 995 this.check_(undefined, true); | |
| 996 }, | |
| 997 | |
| 998 disconnect_: function() { | |
| 999 this.value_ = undefined; | |
| 1000 | |
| 1001 if (this.directObserver_) { | |
| 1002 this.directObserver_.close(this); | |
| 1003 this.directObserver_ = undefined; | |
| 1004 } | |
| 1005 }, | |
| 1006 | |
| 1007 iterateObjects_: function(observe) { | |
| 1008 this.path_.iterateObjects(this.object_, observe); | |
| 1009 }, | |
| 1010 | |
| 1011 check_: function(changeRecords, skipChanges) { | |
| 1012 var oldValue = this.value_; | |
| 1013 this.value_ = this.path_.getValueFrom(this.object_); | |
| 1014 if (skipChanges || areSameValue(this.value_, oldValue)) | |
| 1015 return false; | |
| 1016 | |
| 1017 this.report_([this.value_, oldValue, this]); | |
| 1018 return true; | |
| 1019 }, | |
| 1020 | |
| 1021 setValue: function(newValue) { | |
| 1022 if (this.path_) | |
| 1023 this.path_.setValueFrom(this.object_, newValue); | |
| 1024 } | |
| 1025 }); | |
| 1026 | |
| 1027 function CompoundObserver(reportChangesOnOpen) { | |
| 1028 Observer.call(this); | |
| 1029 | |
| 1030 this.reportChangesOnOpen_ = reportChangesOnOpen; | |
| 1031 this.value_ = []; | |
| 1032 this.directObserver_ = undefined; | |
| 1033 this.observed_ = []; | |
| 1034 } | |
| 1035 | |
| 1036 var observerSentinel = {}; | |
| 1037 | |
| 1038 CompoundObserver.prototype = createObject({ | |
| 1039 __proto__: Observer.prototype, | |
| 1040 | |
| 1041 connect_: function() { | |
| 1042 if (hasObserve) { | |
| 1043 var object; | |
| 1044 var needsDirectObserver = false; | |
| 1045 for (var i = 0; i < this.observed_.length; i += 2) { | |
| 1046 object = this.observed_[i] | |
| 1047 if (object !== observerSentinel) { | |
| 1048 needsDirectObserver = true; | |
| 1049 break; | |
| 1050 } | |
| 1051 } | |
| 1052 | |
| 1053 if (needsDirectObserver) | |
| 1054 this.directObserver_ = getObservedSet(this, object); | |
| 1055 } | |
| 1056 | |
| 1057 this.check_(undefined, !this.reportChangesOnOpen_); | |
| 1058 }, | |
| 1059 | |
| 1060 disconnect_: function() { | |
| 1061 for (var i = 0; i < this.observed_.length; i += 2) { | |
| 1062 if (this.observed_[i] === observerSentinel) | |
| 1063 this.observed_[i + 1].close(); | |
| 1064 } | |
| 1065 this.observed_.length = 0; | |
| 1066 this.value_.length = 0; | |
| 1067 | |
| 1068 if (this.directObserver_) { | |
| 1069 this.directObserver_.close(this); | |
| 1070 this.directObserver_ = undefined; | |
| 1071 } | |
| 1072 }, | |
| 1073 | |
| 1074 addPath: function(object, path) { | |
| 1075 if (this.state_ != UNOPENED && this.state_ != RESETTING) | |
| 1076 throw Error('Cannot add paths once started.'); | |
| 1077 | |
| 1078 var path = getPath(path); | |
| 1079 this.observed_.push(object, path); | |
| 1080 if (!this.reportChangesOnOpen_) | |
| 1081 return; | |
| 1082 var index = this.observed_.length / 2 - 1; | |
| 1083 this.value_[index] = path.getValueFrom(object); | |
| 1084 }, | |
| 1085 | |
| 1086 addObserver: function(observer) { | |
| 1087 if (this.state_ != UNOPENED && this.state_ != RESETTING) | |
| 1088 throw Error('Cannot add observers once started.'); | |
| 1089 | |
| 1090 this.observed_.push(observerSentinel, observer); | |
| 1091 if (!this.reportChangesOnOpen_) | |
| 1092 return; | |
| 1093 var index = this.observed_.length / 2 - 1; | |
| 1094 this.value_[index] = observer.open(this.deliver, this); | |
| 1095 }, | |
| 1096 | |
| 1097 startReset: function() { | |
| 1098 if (this.state_ != OPENED) | |
| 1099 throw Error('Can only reset while open'); | |
| 1100 | |
| 1101 this.state_ = RESETTING; | |
| 1102 this.disconnect_(); | |
| 1103 }, | |
| 1104 | |
| 1105 finishReset: function() { | |
| 1106 if (this.state_ != RESETTING) | |
| 1107 throw Error('Can only finishReset after startReset'); | |
| 1108 this.state_ = OPENED; | |
| 1109 this.connect_(); | |
| 1110 | |
| 1111 return this.value_; | |
| 1112 }, | |
| 1113 | |
| 1114 iterateObjects_: function(observe) { | |
| 1115 var object; | |
| 1116 for (var i = 0; i < this.observed_.length; i += 2) { | |
| 1117 object = this.observed_[i] | |
| 1118 if (object !== observerSentinel) | |
| 1119 this.observed_[i + 1].iterateObjects(object, observe) | |
| 1120 } | |
| 1121 }, | |
| 1122 | |
| 1123 check_: function(changeRecords, skipChanges) { | |
| 1124 var oldValues; | |
| 1125 for (var i = 0; i < this.observed_.length; i += 2) { | |
| 1126 var object = this.observed_[i]; | |
| 1127 var path = this.observed_[i+1]; | |
| 1128 var value; | |
| 1129 if (object === observerSentinel) { | |
| 1130 var observable = path; | |
| 1131 value = this.state_ === UNOPENED ? | |
| 1132 observable.open(this.deliver, this) : | |
| 1133 observable.discardChanges(); | |
| 1134 } else { | |
| 1135 value = path.getValueFrom(object); | |
| 1136 } | |
| 1137 | |
| 1138 if (skipChanges) { | |
| 1139 this.value_[i / 2] = value; | |
| 1140 continue; | |
| 1141 } | |
| 1142 | |
| 1143 if (areSameValue(value, this.value_[i / 2])) | |
| 1144 continue; | |
| 1145 | |
| 1146 oldValues = oldValues || []; | |
| 1147 oldValues[i / 2] = this.value_[i / 2]; | |
| 1148 this.value_[i / 2] = value; | |
| 1149 } | |
| 1150 | |
| 1151 if (!oldValues) | |
| 1152 return false; | |
| 1153 | |
| 1154 // TODO(rafaelw): Having observed_ as the third callback arg here is | |
| 1155 // pretty lame API. Fix. | |
| 1156 this.report_([this.value_, oldValues, this.observed_]); | |
| 1157 return true; | |
| 1158 } | |
| 1159 }); | |
| 1160 | |
| 1161 function identFn(value) { return value; } | |
| 1162 | |
| 1163 function ObserverTransform(observable, getValueFn, setValueFn, | |
| 1164 dontPassThroughSet) { | |
| 1165 this.callback_ = undefined; | |
| 1166 this.target_ = undefined; | |
| 1167 this.value_ = undefined; | |
| 1168 this.observable_ = observable; | |
| 1169 this.getValueFn_ = getValueFn || identFn; | |
| 1170 this.setValueFn_ = setValueFn || identFn; | |
| 1171 // TODO(rafaelw): This is a temporary hack. PolymerExpressions needs this | |
| 1172 // at the moment because of a bug in it's dependency tracking. | |
| 1173 this.dontPassThroughSet_ = dontPassThroughSet; | |
| 1174 } | |
| 1175 | |
| 1176 ObserverTransform.prototype = { | |
| 1177 open: function(callback, target) { | |
| 1178 this.callback_ = callback; | |
| 1179 this.target_ = target; | |
| 1180 this.value_ = | |
| 1181 this.getValueFn_(this.observable_.open(this.observedCallback_, this)); | |
| 1182 return this.value_; | |
| 1183 }, | |
| 1184 | |
| 1185 observedCallback_: function(value) { | |
| 1186 value = this.getValueFn_(value); | |
| 1187 if (areSameValue(value, this.value_)) | |
| 1188 return; | |
| 1189 var oldValue = this.value_; | |
| 1190 this.value_ = value; | |
| 1191 this.callback_.call(this.target_, this.value_, oldValue); | |
| 1192 }, | |
| 1193 | |
| 1194 discardChanges: function() { | |
| 1195 this.value_ = this.getValueFn_(this.observable_.discardChanges()); | |
| 1196 return this.value_; | |
| 1197 }, | |
| 1198 | |
| 1199 deliver: function() { | |
| 1200 return this.observable_.deliver(); | |
| 1201 }, | |
| 1202 | |
| 1203 setValue: function(value) { | |
| 1204 value = this.setValueFn_(value); | |
| 1205 if (!this.dontPassThroughSet_ && this.observable_.setValue) | |
| 1206 return this.observable_.setValue(value); | |
| 1207 }, | |
| 1208 | |
| 1209 close: function() { | |
| 1210 if (this.observable_) | |
| 1211 this.observable_.close(); | |
| 1212 this.callback_ = undefined; | |
| 1213 this.target_ = undefined; | |
| 1214 this.observable_ = undefined; | |
| 1215 this.value_ = undefined; | |
| 1216 this.getValueFn_ = undefined; | |
| 1217 this.setValueFn_ = undefined; | |
| 1218 } | |
| 1219 } | |
| 1220 | |
| 1221 var expectedRecordTypes = { | |
| 1222 add: true, | |
| 1223 update: true, | |
| 1224 delete: true | |
| 1225 }; | |
| 1226 | |
| 1227 function diffObjectFromChangeRecords(object, changeRecords, oldValues) { | |
| 1228 var added = {}; | |
| 1229 var removed = {}; | |
| 1230 | |
| 1231 for (var i = 0; i < changeRecords.length; i++) { | |
| 1232 var record = changeRecords[i]; | |
| 1233 if (!expectedRecordTypes[record.type]) { | |
| 1234 console.error('Unknown changeRecord type: ' + record.type); | |
| 1235 console.error(record); | |
| 1236 continue; | |
| 1237 } | |
| 1238 | |
| 1239 if (!(record.name in oldValues)) | |
| 1240 oldValues[record.name] = record.oldValue; | |
| 1241 | |
| 1242 if (record.type == 'update') | |
| 1243 continue; | |
| 1244 | |
| 1245 if (record.type == 'add') { | |
| 1246 if (record.name in removed) | |
| 1247 delete removed[record.name]; | |
| 1248 else | |
| 1249 added[record.name] = true; | |
| 1250 | |
| 1251 continue; | |
| 1252 } | |
| 1253 | |
| 1254 // type = 'delete' | |
| 1255 if (record.name in added) { | |
| 1256 delete added[record.name]; | |
| 1257 delete oldValues[record.name]; | |
| 1258 } else { | |
| 1259 removed[record.name] = true; | |
| 1260 } | |
| 1261 } | |
| 1262 | |
| 1263 for (var prop in added) | |
| 1264 added[prop] = object[prop]; | |
| 1265 | |
| 1266 for (var prop in removed) | |
| 1267 removed[prop] = undefined; | |
| 1268 | |
| 1269 var changed = {}; | |
| 1270 for (var prop in oldValues) { | |
| 1271 if (prop in added || prop in removed) | |
| 1272 continue; | |
| 1273 | |
| 1274 var newValue = object[prop]; | |
| 1275 if (oldValues[prop] !== newValue) | |
| 1276 changed[prop] = newValue; | |
| 1277 } | |
| 1278 | |
| 1279 return { | |
| 1280 added: added, | |
| 1281 removed: removed, | |
| 1282 changed: changed | |
| 1283 }; | |
| 1284 } | |
| 1285 | |
| 1286 function newSplice(index, removed, addedCount) { | |
| 1287 return { | |
| 1288 index: index, | |
| 1289 removed: removed, | |
| 1290 addedCount: addedCount | |
| 1291 }; | |
| 1292 } | |
| 1293 | |
| 1294 var EDIT_LEAVE = 0; | |
| 1295 var EDIT_UPDATE = 1; | |
| 1296 var EDIT_ADD = 2; | |
| 1297 var EDIT_DELETE = 3; | |
| 1298 | |
| 1299 function ArraySplice() {} | |
| 1300 | |
| 1301 ArraySplice.prototype = { | |
| 1302 | |
| 1303 // Note: This function is *based* on the computation of the Levenshtein | |
| 1304 // "edit" distance. The one change is that "updates" are treated as two | |
| 1305 // edits - not one. With Array splices, an update is really a delete | |
| 1306 // followed by an add. By retaining this, we optimize for "keeping" the | |
| 1307 // maximum array items in the original array. For example: | |
| 1308 // | |
| 1309 // 'xxxx123' -> '123yyyy' | |
| 1310 // | |
| 1311 // With 1-edit updates, the shortest path would be just to update all seven | |
| 1312 // characters. With 2-edit updates, we delete 4, leave 3, and add 4. This | |
| 1313 // leaves the substring '123' intact. | |
| 1314 calcEditDistances: function(current, currentStart, currentEnd, | |
| 1315 old, oldStart, oldEnd) { | |
| 1316 // "Deletion" columns | |
| 1317 var rowCount = oldEnd - oldStart + 1; | |
| 1318 var columnCount = currentEnd - currentStart + 1; | |
| 1319 var distances = new Array(rowCount); | |
| 1320 | |
| 1321 // "Addition" rows. Initialize null column. | |
| 1322 for (var i = 0; i < rowCount; i++) { | |
| 1323 distances[i] = new Array(columnCount); | |
| 1324 distances[i][0] = i; | |
| 1325 } | |
| 1326 | |
| 1327 // Initialize null row | |
| 1328 for (var j = 0; j < columnCount; j++) | |
| 1329 distances[0][j] = j; | |
| 1330 | |
| 1331 for (var i = 1; i < rowCount; i++) { | |
| 1332 for (var j = 1; j < columnCount; j++) { | |
| 1333 if (this.equals(current[currentStart + j - 1], old[oldStart + i - 1])) | |
| 1334 distances[i][j] = distances[i - 1][j - 1]; | |
| 1335 else { | |
| 1336 var north = distances[i - 1][j] + 1; | |
| 1337 var west = distances[i][j - 1] + 1; | |
| 1338 distances[i][j] = north < west ? north : west; | |
| 1339 } | |
| 1340 } | |
| 1341 } | |
| 1342 | |
| 1343 return distances; | |
| 1344 }, | |
| 1345 | |
| 1346 // This starts at the final weight, and walks "backward" by finding | |
| 1347 // the minimum previous weight recursively until the origin of the weight | |
| 1348 // matrix. | |
| 1349 spliceOperationsFromEditDistances: function(distances) { | |
| 1350 var i = distances.length - 1; | |
| 1351 var j = distances[0].length - 1; | |
| 1352 var current = distances[i][j]; | |
| 1353 var edits = []; | |
| 1354 while (i > 0 || j > 0) { | |
| 1355 if (i == 0) { | |
| 1356 edits.push(EDIT_ADD); | |
| 1357 j--; | |
| 1358 continue; | |
| 1359 } | |
| 1360 if (j == 0) { | |
| 1361 edits.push(EDIT_DELETE); | |
| 1362 i--; | |
| 1363 continue; | |
| 1364 } | |
| 1365 var northWest = distances[i - 1][j - 1]; | |
| 1366 var west = distances[i - 1][j]; | |
| 1367 var north = distances[i][j - 1]; | |
| 1368 | |
| 1369 var min; | |
| 1370 if (west < north) | |
| 1371 min = west < northWest ? west : northWest; | |
| 1372 else | |
| 1373 min = north < northWest ? north : northWest; | |
| 1374 | |
| 1375 if (min == northWest) { | |
| 1376 if (northWest == current) { | |
| 1377 edits.push(EDIT_LEAVE); | |
| 1378 } else { | |
| 1379 edits.push(EDIT_UPDATE); | |
| 1380 current = northWest; | |
| 1381 } | |
| 1382 i--; | |
| 1383 j--; | |
| 1384 } else if (min == west) { | |
| 1385 edits.push(EDIT_DELETE); | |
| 1386 i--; | |
| 1387 current = west; | |
| 1388 } else { | |
| 1389 edits.push(EDIT_ADD); | |
| 1390 j--; | |
| 1391 current = north; | |
| 1392 } | |
| 1393 } | |
| 1394 | |
| 1395 edits.reverse(); | |
| 1396 return edits; | |
| 1397 }, | |
| 1398 | |
| 1399 /** | |
| 1400 * Splice Projection functions: | |
| 1401 * | |
| 1402 * A splice map is a representation of how a previous array of items | |
| 1403 * was transformed into a new array of items. Conceptually it is a list of | |
| 1404 * tuples of | |
| 1405 * | |
| 1406 * <index, removed, addedCount> | |
| 1407 * | |
| 1408 * which are kept in ascending index order of. The tuple represents that at | |
| 1409 * the |index|, |removed| sequence of items were removed, and counting forwa
rd | |
| 1410 * from |index|, |addedCount| items were added. | |
| 1411 */ | |
| 1412 | |
| 1413 /** | |
| 1414 * Lacking individual splice mutation information, the minimal set of | |
| 1415 * splices can be synthesized given the previous state and final state of an | |
| 1416 * array. The basic approach is to calculate the edit distance matrix and | |
| 1417 * choose the shortest path through it. | |
| 1418 * | |
| 1419 * Complexity: O(l * p) | |
| 1420 * l: The length of the current array | |
| 1421 * p: The length of the old array | |
| 1422 */ | |
| 1423 calcSplices: function(current, currentStart, currentEnd, | |
| 1424 old, oldStart, oldEnd) { | |
| 1425 var prefixCount = 0; | |
| 1426 var suffixCount = 0; | |
| 1427 | |
| 1428 var minLength = Math.min(currentEnd - currentStart, oldEnd - oldStart); | |
| 1429 if (currentStart == 0 && oldStart == 0) | |
| 1430 prefixCount = this.sharedPrefix(current, old, minLength); | |
| 1431 | |
| 1432 if (currentEnd == current.length && oldEnd == old.length) | |
| 1433 suffixCount = this.sharedSuffix(current, old, minLength - prefixCount); | |
| 1434 | |
| 1435 currentStart += prefixCount; | |
| 1436 oldStart += prefixCount; | |
| 1437 currentEnd -= suffixCount; | |
| 1438 oldEnd -= suffixCount; | |
| 1439 | |
| 1440 if (currentEnd - currentStart == 0 && oldEnd - oldStart == 0) | |
| 1441 return []; | |
| 1442 | |
| 1443 if (currentStart == currentEnd) { | |
| 1444 var splice = newSplice(currentStart, [], 0); | |
| 1445 while (oldStart < oldEnd) | |
| 1446 splice.removed.push(old[oldStart++]); | |
| 1447 | |
| 1448 return [ splice ]; | |
| 1449 } else if (oldStart == oldEnd) | |
| 1450 return [ newSplice(currentStart, [], currentEnd - currentStart) ]; | |
| 1451 | |
| 1452 var ops = this.spliceOperationsFromEditDistances( | |
| 1453 this.calcEditDistances(current, currentStart, currentEnd, | |
| 1454 old, oldStart, oldEnd)); | |
| 1455 | |
| 1456 var splice = undefined; | |
| 1457 var splices = []; | |
| 1458 var index = currentStart; | |
| 1459 var oldIndex = oldStart; | |
| 1460 for (var i = 0; i < ops.length; i++) { | |
| 1461 switch(ops[i]) { | |
| 1462 case EDIT_LEAVE: | |
| 1463 if (splice) { | |
| 1464 splices.push(splice); | |
| 1465 splice = undefined; | |
| 1466 } | |
| 1467 | |
| 1468 index++; | |
| 1469 oldIndex++; | |
| 1470 break; | |
| 1471 case EDIT_UPDATE: | |
| 1472 if (!splice) | |
| 1473 splice = newSplice(index, [], 0); | |
| 1474 | |
| 1475 splice.addedCount++; | |
| 1476 index++; | |
| 1477 | |
| 1478 splice.removed.push(old[oldIndex]); | |
| 1479 oldIndex++; | |
| 1480 break; | |
| 1481 case EDIT_ADD: | |
| 1482 if (!splice) | |
| 1483 splice = newSplice(index, [], 0); | |
| 1484 | |
| 1485 splice.addedCount++; | |
| 1486 index++; | |
| 1487 break; | |
| 1488 case EDIT_DELETE: | |
| 1489 if (!splice) | |
| 1490 splice = newSplice(index, [], 0); | |
| 1491 | |
| 1492 splice.removed.push(old[oldIndex]); | |
| 1493 oldIndex++; | |
| 1494 break; | |
| 1495 } | |
| 1496 } | |
| 1497 | |
| 1498 if (splice) { | |
| 1499 splices.push(splice); | |
| 1500 } | |
| 1501 return splices; | |
| 1502 }, | |
| 1503 | |
| 1504 sharedPrefix: function(current, old, searchLength) { | |
| 1505 for (var i = 0; i < searchLength; i++) | |
| 1506 if (!this.equals(current[i], old[i])) | |
| 1507 return i; | |
| 1508 return searchLength; | |
| 1509 }, | |
| 1510 | |
| 1511 sharedSuffix: function(current, old, searchLength) { | |
| 1512 var index1 = current.length; | |
| 1513 var index2 = old.length; | |
| 1514 var count = 0; | |
| 1515 while (count < searchLength && this.equals(current[--index1], old[--index2
])) | |
| 1516 count++; | |
| 1517 | |
| 1518 return count; | |
| 1519 }, | |
| 1520 | |
| 1521 calculateSplices: function(current, previous) { | |
| 1522 return this.calcSplices(current, 0, current.length, previous, 0, | |
| 1523 previous.length); | |
| 1524 }, | |
| 1525 | |
| 1526 equals: function(currentValue, previousValue) { | |
| 1527 return currentValue === previousValue; | |
| 1528 } | |
| 1529 }; | |
| 1530 | |
| 1531 var arraySplice = new ArraySplice(); | |
| 1532 | |
| 1533 function calcSplices(current, currentStart, currentEnd, | |
| 1534 old, oldStart, oldEnd) { | |
| 1535 return arraySplice.calcSplices(current, currentStart, currentEnd, | |
| 1536 old, oldStart, oldEnd); | |
| 1537 } | |
| 1538 | |
| 1539 function intersect(start1, end1, start2, end2) { | |
| 1540 // Disjoint | |
| 1541 if (end1 < start2 || end2 < start1) | |
| 1542 return -1; | |
| 1543 | |
| 1544 // Adjacent | |
| 1545 if (end1 == start2 || end2 == start1) | |
| 1546 return 0; | |
| 1547 | |
| 1548 // Non-zero intersect, span1 first | |
| 1549 if (start1 < start2) { | |
| 1550 if (end1 < end2) | |
| 1551 return end1 - start2; // Overlap | |
| 1552 else | |
| 1553 return end2 - start2; // Contained | |
| 1554 } else { | |
| 1555 // Non-zero intersect, span2 first | |
| 1556 if (end2 < end1) | |
| 1557 return end2 - start1; // Overlap | |
| 1558 else | |
| 1559 return end1 - start1; // Contained | |
| 1560 } | |
| 1561 } | |
| 1562 | |
| 1563 function mergeSplice(splices, index, removed, addedCount) { | |
| 1564 | |
| 1565 var splice = newSplice(index, removed, addedCount); | |
| 1566 | |
| 1567 var inserted = false; | |
| 1568 var insertionOffset = 0; | |
| 1569 | |
| 1570 for (var i = 0; i < splices.length; i++) { | |
| 1571 var current = splices[i]; | |
| 1572 current.index += insertionOffset; | |
| 1573 | |
| 1574 if (inserted) | |
| 1575 continue; | |
| 1576 | |
| 1577 var intersectCount = intersect(splice.index, | |
| 1578 splice.index + splice.removed.length, | |
| 1579 current.index, | |
| 1580 current.index + current.addedCount); | |
| 1581 | |
| 1582 if (intersectCount >= 0) { | |
| 1583 // Merge the two splices | |
| 1584 | |
| 1585 splices.splice(i, 1); | |
| 1586 i--; | |
| 1587 | |
| 1588 insertionOffset -= current.addedCount - current.removed.length; | |
| 1589 | |
| 1590 splice.addedCount += current.addedCount - intersectCount; | |
| 1591 var deleteCount = splice.removed.length + | |
| 1592 current.removed.length - intersectCount; | |
| 1593 | |
| 1594 if (!splice.addedCount && !deleteCount) { | |
| 1595 // merged splice is a noop. discard. | |
| 1596 inserted = true; | |
| 1597 } else { | |
| 1598 var removed = current.removed; | |
| 1599 | |
| 1600 if (splice.index < current.index) { | |
| 1601 // some prefix of splice.removed is prepended to current.removed. | |
| 1602 var prepend = splice.removed.slice(0, current.index - splice.index); | |
| 1603 Array.prototype.push.apply(prepend, removed); | |
| 1604 removed = prepend; | |
| 1605 } | |
| 1606 | |
| 1607 if (splice.index + splice.removed.length > current.index + current.add
edCount) { | |
| 1608 // some suffix of splice.removed is appended to current.removed. | |
| 1609 var append = splice.removed.slice(current.index + current.addedCount
- splice.index); | |
| 1610 Array.prototype.push.apply(removed, append); | |
| 1611 } | |
| 1612 | |
| 1613 splice.removed = removed; | |
| 1614 if (current.index < splice.index) { | |
| 1615 splice.index = current.index; | |
| 1616 } | |
| 1617 } | |
| 1618 } else if (splice.index < current.index) { | |
| 1619 // Insert splice here. | |
| 1620 | |
| 1621 inserted = true; | |
| 1622 | |
| 1623 splices.splice(i, 0, splice); | |
| 1624 i++; | |
| 1625 | |
| 1626 var offset = splice.addedCount - splice.removed.length | |
| 1627 current.index += offset; | |
| 1628 insertionOffset += offset; | |
| 1629 } | |
| 1630 } | |
| 1631 | |
| 1632 if (!inserted) | |
| 1633 splices.push(splice); | |
| 1634 } | |
| 1635 | |
| 1636 function createInitialSplices(array, changeRecords) { | |
| 1637 var splices = []; | |
| 1638 | |
| 1639 for (var i = 0; i < changeRecords.length; i++) { | |
| 1640 var record = changeRecords[i]; | |
| 1641 switch(record.type) { | |
| 1642 case 'splice': | |
| 1643 mergeSplice(splices, record.index, record.removed.slice(), record.adde
dCount); | |
| 1644 break; | |
| 1645 case 'add': | |
| 1646 case 'update': | |
| 1647 case 'delete': | |
| 1648 if (!isIndex(record.name)) | |
| 1649 continue; | |
| 1650 var index = toNumber(record.name); | |
| 1651 if (index < 0) | |
| 1652 continue; | |
| 1653 mergeSplice(splices, index, [record.oldValue], 1); | |
| 1654 break; | |
| 1655 default: | |
| 1656 console.error('Unexpected record type: ' + JSON.stringify(record)); | |
| 1657 break; | |
| 1658 } | |
| 1659 } | |
| 1660 | |
| 1661 return splices; | |
| 1662 } | |
| 1663 | |
| 1664 function projectArraySplices(array, changeRecords) { | |
| 1665 var splices = []; | |
| 1666 | |
| 1667 createInitialSplices(array, changeRecords).forEach(function(splice) { | |
| 1668 if (splice.addedCount == 1 && splice.removed.length == 1) { | |
| 1669 if (splice.removed[0] !== array[splice.index]) | |
| 1670 splices.push(splice); | |
| 1671 | |
| 1672 return | |
| 1673 }; | |
| 1674 | |
| 1675 splices = splices.concat(calcSplices(array, splice.index, splice.index + s
plice.addedCount, | |
| 1676 splice.removed, 0, splice.removed.len
gth)); | |
| 1677 }); | |
| 1678 | |
| 1679 return splices; | |
| 1680 } | |
| 1681 | |
| 1682 // Export the observe-js object for **Node.js**, with | |
| 1683 // backwards-compatibility for the old `require()` API. If we're in | |
| 1684 // the browser, export as a global object. | |
| 1685 | |
| 1686 var expose = global; | |
| 1687 | |
| 1688 if (typeof exports !== 'undefined') { | |
| 1689 if (typeof module !== 'undefined' && module.exports) { | |
| 1690 expose = exports = module.exports; | |
| 1691 } | |
| 1692 expose = exports; | |
| 1693 } | |
| 1694 | |
| 1695 expose.Observer = Observer; | |
| 1696 expose.Observer.runEOM_ = runEOM; | |
| 1697 expose.Observer.observerSentinel_ = observerSentinel; // for testing. | |
| 1698 expose.Observer.hasObjectObserve = hasObserve; | |
| 1699 expose.ArrayObserver = ArrayObserver; | |
| 1700 expose.ArrayObserver.calculateSplices = function(current, previous) { | |
| 1701 return arraySplice.calculateSplices(current, previous); | |
| 1702 }; | |
| 1703 | |
| 1704 expose.ArraySplice = ArraySplice; | |
| 1705 expose.ObjectObserver = ObjectObserver; | |
| 1706 expose.PathObserver = PathObserver; | |
| 1707 expose.CompoundObserver = CompoundObserver; | |
| 1708 expose.Path = Path; | |
| 1709 expose.ObserverTransform = ObserverTransform; | |
| 1710 | |
| 1711 })(typeof global !== 'undefined' && global && typeof module !== 'undefined' && m
odule ? global : this || window); | |
| OLD | NEW |