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 |