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