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[0]); | |
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 var eomObj = { pingPong: true }; | |
515 var eomRunScheduled = false; | |
516 | |
517 Object.observe(eomObj, function() { | |
518 runEOMTasks(); | |
519 eomRunScheduled = false; | |
520 }); | |
521 | |
522 return function(fn) { | |
523 eomTasks.push(fn); | |
524 if (!eomRunScheduled) { | |
525 eomRunScheduled = true; | |
526 eomObj.pingPong = !eomObj.pingPong; | |
527 } | |
528 }; | |
529 })() : | |
530 (function() { | |
531 return function(fn) { | |
532 eomTasks.push(fn); | |
533 }; | |
534 })(); | |
535 | |
536 var observedObjectCache = []; | |
537 | |
538 function newObservedObject() { | |
539 var observer; | |
540 var object; | |
541 var discardRecords = false; | |
542 var first = true; | |
543 | |
544 function callback(records) { | |
545 if (observer && observer.state_ === OPENED && !discardRecords) | |
546 observer.check_(records); | |
547 } | |
548 | |
549 return { | |
550 open: function(obs) { | |
551 if (observer) | |
552 throw Error('ObservedObject in use'); | |
553 | |
554 if (!first) | |
555 Object.deliverChangeRecords(callback); | |
556 | |
557 observer = obs; | |
558 first = false; | |
559 }, | |
560 observe: function(obj, arrayObserve) { | |
561 object = obj; | |
562 if (arrayObserve) | |
563 Array.observe(object, callback); | |
564 else | |
565 Object.observe(object, callback); | |
566 }, | |
567 deliver: function(discard) { | |
568 discardRecords = discard; | |
569 Object.deliverChangeRecords(callback); | |
570 discardRecords = false; | |
571 }, | |
572 close: function() { | |
573 observer = undefined; | |
574 Object.unobserve(object, callback); | |
575 observedObjectCache.push(this); | |
576 } | |
577 }; | |
578 } | |
579 | |
580 /* | |
581 * The observedSet abstraction is a perf optimization which reduces the total | |
582 * number of Object.observe observations of a set of objects. The idea is that | |
583 * groups of Observers will have some object dependencies in common and this | |
584 * observed set ensures that each object in the transitive closure of | |
585 * dependencies is only observed once. The observedSet acts as a write barrier | |
586 * such that whenever any change comes through, all Observers are checked for | |
587 * changed values. | |
588 * | |
589 * Note that this optimization is explicitly moving work from setup-time to | |
590 * change-time. | |
591 * | |
592 * TODO(rafaelw): Implement "garbage collection". In order to move work off | |
593 * the critical path, when Observers are closed, their observed objects are | |
594 * not Object.unobserve(d). As a result, it's possible that if the observedSet | |
595 * is kept open, but some Observers have been closed, it could cause "leaks" | |
596 * (prevent otherwise collectable objects from being collected). At some | |
597 * point, we should implement incremental "gc" which keeps a list of | |
598 * observedSets which may need clean-up and does small amounts of cleanup on a | |
599 * timeout until all is clean. | |
600 */ | |
601 | |
602 function getObservedObject(observer, object, arrayObserve) { | |
603 var dir = observedObjectCache.pop() || newObservedObject(); | |
604 dir.open(observer); | |
605 dir.observe(object, arrayObserve); | |
606 return dir; | |
607 } | |
608 | |
609 var observedSetCache = []; | |
610 | |
611 function newObservedSet() { | |
612 var observerCount = 0; | |
613 var observers = []; | |
614 var objects = []; | |
615 var rootObj; | |
616 var rootObjProps; | |
617 | |
618 function observe(obj, prop) { | |
619 if (!obj) | |
620 return; | |
621 | |
622 if (obj === rootObj) | |
623 rootObjProps[prop] = true; | |
624 | |
625 if (objects.indexOf(obj) < 0) { | |
626 objects.push(obj); | |
627 Object.observe(obj, callback); | |
628 } | |
629 | |
630 observe(Object.getPrototypeOf(obj), prop); | |
631 } | |
632 | |
633 function allRootObjNonObservedProps(recs) { | |
634 for (var i = 0; i < recs.length; i++) { | |
635 var rec = recs[i]; | |
636 if (rec.object !== rootObj || | |
637 rootObjProps[rec.name] || | |
638 rec.type === 'setPrototype') { | |
639 return false; | |
640 } | |
641 } | |
642 return true; | |
643 } | |
644 | |
645 function callback(recs) { | |
646 if (allRootObjNonObservedProps(recs)) | |
647 return; | |
648 | |
649 var observer; | |
650 for (var i = 0; i < observers.length; i++) { | |
651 observer = observers[i]; | |
652 if (observer.state_ == OPENED) { | |
653 observer.iterateObjects_(observe); | |
654 } | |
655 } | |
656 | |
657 for (var i = 0; i < observers.length; i++) { | |
658 observer = observers[i]; | |
659 if (observer.state_ == OPENED) { | |
660 observer.check_(); | |
661 } | |
662 } | |
663 } | |
664 | |
665 var record = { | |
666 object: undefined, | |
667 objects: objects, | |
668 open: function(obs, object) { | |
669 if (!rootObj) { | |
670 rootObj = object; | |
671 rootObjProps = {}; | |
672 } | |
673 | |
674 observers.push(obs); | |
675 observerCount++; | |
676 obs.iterateObjects_(observe); | |
677 }, | |
678 close: function(obs) { | |
679 observerCount--; | |
680 if (observerCount > 0) { | |
681 return; | |
682 } | |
683 | |
684 for (var i = 0; i < objects.length; i++) { | |
685 Object.unobserve(objects[i], callback); | |
686 Observer.unobservedCount++; | |
687 } | |
688 | |
689 observers.length = 0; | |
690 objects.length = 0; | |
691 rootObj = undefined; | |
692 rootObjProps = undefined; | |
693 observedSetCache.push(this); | |
694 } | |
695 }; | |
696 | |
697 return record; | |
698 } | |
699 | |
700 var lastObservedSet; | |
701 | |
702 function getObservedSet(observer, obj) { | |
703 if (!lastObservedSet || lastObservedSet.object !== obj) { | |
704 lastObservedSet = observedSetCache.pop() || newObservedSet(); | |
705 lastObservedSet.object = obj; | |
706 } | |
707 lastObservedSet.open(observer, obj); | |
708 return lastObservedSet; | |
709 } | |
710 | |
711 var UNOPENED = 0; | |
712 var OPENED = 1; | |
713 var CLOSED = 2; | |
714 var RESETTING = 3; | |
715 | |
716 var nextObserverId = 1; | |
717 | |
718 function Observer() { | |
719 this.state_ = UNOPENED; | |
720 this.callback_ = undefined; | |
721 this.target_ = undefined; // TODO(rafaelw): Should be WeakRef | |
722 this.directObserver_ = undefined; | |
723 this.value_ = undefined; | |
724 this.id_ = nextObserverId++; | |
725 } | |
726 | |
727 Observer.prototype = { | |
728 open: function(callback, target) { | |
729 if (this.state_ != UNOPENED) | |
730 throw Error('Observer has already been opened.'); | |
731 | |
732 addToAll(this); | |
733 this.callback_ = callback; | |
734 this.target_ = target; | |
735 this.connect_(); | |
736 this.state_ = OPENED; | |
737 return this.value_; | |
738 }, | |
739 | |
740 close: function() { | |
741 if (this.state_ != OPENED) | |
742 return; | |
743 | |
744 removeFromAll(this); | |
745 this.disconnect_(); | |
746 this.value_ = undefined; | |
747 this.callback_ = undefined; | |
748 this.target_ = undefined; | |
749 this.state_ = CLOSED; | |
750 }, | |
751 | |
752 deliver: function() { | |
753 if (this.state_ != OPENED) | |
754 return; | |
755 | |
756 dirtyCheck(this); | |
757 }, | |
758 | |
759 report_: function(changes) { | |
760 try { | |
761 this.callback_.apply(this.target_, changes); | |
762 } catch (ex) { | |
763 Observer._errorThrownDuringCallback = true; | |
764 console.error('Exception caught during observer callback: ' + | |
765 (ex.stack || ex)); | |
766 } | |
767 }, | |
768 | |
769 discardChanges: function() { | |
770 this.check_(undefined, true); | |
771 return this.value_; | |
772 } | |
773 } | |
774 | |
775 var collectObservers = !hasObserve; | |
776 var allObservers; | |
777 Observer._allObserversCount = 0; | |
778 | |
779 if (collectObservers) { | |
780 allObservers = []; | |
781 } | |
782 | |
783 function addToAll(observer) { | |
784 Observer._allObserversCount++; | |
785 if (!collectObservers) | |
786 return; | |
787 | |
788 allObservers.push(observer); | |
789 } | |
790 | |
791 function removeFromAll(observer) { | |
792 Observer._allObserversCount--; | |
793 } | |
794 | |
795 var runningMicrotaskCheckpoint = false; | |
796 | |
797 global.Platform = global.Platform || {}; | |
798 | |
799 global.Platform.performMicrotaskCheckpoint = function() { | |
800 if (runningMicrotaskCheckpoint) | |
801 return; | |
802 | |
803 if (!collectObservers) | |
804 return; | |
805 | |
806 runningMicrotaskCheckpoint = true; | |
807 | |
808 var cycles = 0; | |
809 var anyChanged, toCheck; | |
810 | |
811 do { | |
812 cycles++; | |
813 toCheck = allObservers; | |
814 allObservers = []; | |
815 anyChanged = false; | |
816 | |
817 for (var i = 0; i < toCheck.length; i++) { | |
818 var observer = toCheck[i]; | |
819 if (observer.state_ != OPENED) | |
820 continue; | |
821 | |
822 if (observer.check_()) | |
823 anyChanged = true; | |
824 | |
825 allObservers.push(observer); | |
826 } | |
827 if (runEOMTasks()) | |
828 anyChanged = true; | |
829 } while (cycles < MAX_DIRTY_CHECK_CYCLES && anyChanged); | |
830 | |
831 if (testingExposeCycleCount) | |
832 global.dirtyCheckCycleCount = cycles; | |
833 | |
834 runningMicrotaskCheckpoint = false; | |
835 }; | |
836 | |
837 if (collectObservers) { | |
838 global.Platform.clearObservers = function() { | |
839 allObservers = []; | |
840 }; | |
841 } | |
842 | |
843 function ObjectObserver(object) { | |
844 Observer.call(this); | |
845 this.value_ = object; | |
846 this.oldObject_ = undefined; | |
847 } | |
848 | |
849 ObjectObserver.prototype = createObject({ | |
850 __proto__: Observer.prototype, | |
851 | |
852 arrayObserve: false, | |
853 | |
854 connect_: function(callback, target) { | |
855 if (hasObserve) { | |
856 this.directObserver_ = getObservedObject(this, this.value_, | |
857 this.arrayObserve); | |
858 } else { | |
859 this.oldObject_ = this.copyObject(this.value_); | |
860 } | |
861 | |
862 }, | |
863 | |
864 copyObject: function(object) { | |
865 var copy = Array.isArray(object) ? [] : {}; | |
866 for (var prop in object) { | |
867 copy[prop] = object[prop]; | |
868 }; | |
869 if (Array.isArray(object)) | |
870 copy.length = object.length; | |
871 return copy; | |
872 }, | |
873 | |
874 check_: function(changeRecords, skipChanges) { | |
875 var diff; | |
876 var oldValues; | |
877 if (hasObserve) { | |
878 if (!changeRecords) | |
879 return false; | |
880 | |
881 oldValues = {}; | |
882 diff = diffObjectFromChangeRecords(this.value_, changeRecords, | |
883 oldValues); | |
884 } else { | |
885 oldValues = this.oldObject_; | |
886 diff = diffObjectFromOldObject(this.value_, this.oldObject_); | |
887 } | |
888 | |
889 if (diffIsEmpty(diff)) | |
890 return false; | |
891 | |
892 if (!hasObserve) | |
893 this.oldObject_ = this.copyObject(this.value_); | |
894 | |
895 this.report_([ | |
896 diff.added || {}, | |
897 diff.removed || {}, | |
898 diff.changed || {}, | |
899 function(property) { | |
900 return oldValues[property]; | |
901 } | |
902 ]); | |
903 | |
904 return true; | |
905 }, | |
906 | |
907 disconnect_: function() { | |
908 if (hasObserve) { | |
909 this.directObserver_.close(); | |
910 this.directObserver_ = undefined; | |
911 } else { | |
912 this.oldObject_ = undefined; | |
913 } | |
914 }, | |
915 | |
916 deliver: function() { | |
917 if (this.state_ != OPENED) | |
918 return; | |
919 | |
920 if (hasObserve) | |
921 this.directObserver_.deliver(false); | |
922 else | |
923 dirtyCheck(this); | |
924 }, | |
925 | |
926 discardChanges: function() { | |
927 if (this.directObserver_) | |
928 this.directObserver_.deliver(true); | |
929 else | |
930 this.oldObject_ = this.copyObject(this.value_); | |
931 | |
932 return this.value_; | |
933 } | |
934 }); | |
935 | |
936 function ArrayObserver(array) { | |
937 if (!Array.isArray(array)) | |
938 throw Error('Provided object is not an Array'); | |
939 ObjectObserver.call(this, array); | |
940 } | |
941 | |
942 ArrayObserver.prototype = createObject({ | |
943 | |
944 __proto__: ObjectObserver.prototype, | |
945 | |
946 arrayObserve: true, | |
947 | |
948 copyObject: function(arr) { | |
949 return arr.slice(); | |
950 }, | |
951 | |
952 check_: function(changeRecords) { | |
953 var splices; | |
954 if (hasObserve) { | |
955 if (!changeRecords) | |
956 return false; | |
957 splices = projectArraySplices(this.value_, changeRecords); | |
958 } else { | |
959 splices = calcSplices(this.value_, 0, this.value_.length, | |
960 this.oldObject_, 0, this.oldObject_.length); | |
961 } | |
962 | |
963 if (!splices || !splices.length) | |
964 return false; | |
965 | |
966 if (!hasObserve) | |
967 this.oldObject_ = this.copyObject(this.value_); | |
968 | |
969 this.report_([splices]); | |
970 return true; | |
971 } | |
972 }); | |
973 | |
974 ArrayObserver.applySplices = function(previous, current, splices) { | |
975 splices.forEach(function(splice) { | |
976 var spliceArgs = [splice.index, splice.removed.length]; | |
977 var addIndex = splice.index; | |
978 while (addIndex < splice.index + splice.addedCount) { | |
979 spliceArgs.push(current[addIndex]); | |
980 addIndex++; | |
981 } | |
982 | |
983 Array.prototype.splice.apply(previous, spliceArgs); | |
984 }); | |
985 }; | |
986 | |
987 function PathObserver(object, path) { | |
988 Observer.call(this); | |
989 | |
990 this.object_ = object; | |
991 this.path_ = getPath(path); | |
992 this.directObserver_ = undefined; | |
993 } | |
994 | |
995 PathObserver.prototype = createObject({ | |
996 __proto__: Observer.prototype, | |
997 | |
998 get path() { | |
999 return this.path_; | |
1000 }, | |
1001 | |
1002 connect_: function() { | |
1003 if (hasObserve) | |
1004 this.directObserver_ = getObservedSet(this, this.object_); | |
1005 | |
1006 this.check_(undefined, true); | |
1007 }, | |
1008 | |
1009 disconnect_: function() { | |
1010 this.value_ = undefined; | |
1011 | |
1012 if (this.directObserver_) { | |
1013 this.directObserver_.close(this); | |
1014 this.directObserver_ = undefined; | |
1015 } | |
1016 }, | |
1017 | |
1018 iterateObjects_: function(observe) { | |
1019 this.path_.iterateObjects(this.object_, observe); | |
1020 }, | |
1021 | |
1022 check_: function(changeRecords, skipChanges) { | |
1023 var oldValue = this.value_; | |
1024 this.value_ = this.path_.getValueFrom(this.object_); | |
1025 if (skipChanges || areSameValue(this.value_, oldValue)) | |
1026 return false; | |
1027 | |
1028 this.report_([this.value_, oldValue, this]); | |
1029 return true; | |
1030 }, | |
1031 | |
1032 setValue: function(newValue) { | |
1033 if (this.path_) | |
1034 this.path_.setValueFrom(this.object_, newValue); | |
1035 } | |
1036 }); | |
1037 | |
1038 function CompoundObserver(reportChangesOnOpen) { | |
1039 Observer.call(this); | |
1040 | |
1041 this.reportChangesOnOpen_ = reportChangesOnOpen; | |
1042 this.value_ = []; | |
1043 this.directObserver_ = undefined; | |
1044 this.observed_ = []; | |
1045 } | |
1046 | |
1047 var observerSentinel = {}; | |
1048 | |
1049 CompoundObserver.prototype = createObject({ | |
1050 __proto__: Observer.prototype, | |
1051 | |
1052 connect_: function() { | |
1053 if (hasObserve) { | |
1054 var object; | |
1055 var needsDirectObserver = false; | |
1056 for (var i = 0; i < this.observed_.length; i += 2) { | |
1057 object = this.observed_[i] | |
1058 if (object !== observerSentinel) { | |
1059 needsDirectObserver = true; | |
1060 break; | |
1061 } | |
1062 } | |
1063 | |
1064 if (needsDirectObserver) | |
1065 this.directObserver_ = getObservedSet(this, object); | |
1066 } | |
1067 | |
1068 this.check_(undefined, !this.reportChangesOnOpen_); | |
1069 }, | |
1070 | |
1071 disconnect_: function() { | |
1072 for (var i = 0; i < this.observed_.length; i += 2) { | |
1073 if (this.observed_[i] === observerSentinel) | |
1074 this.observed_[i + 1].close(); | |
1075 } | |
1076 this.observed_.length = 0; | |
1077 this.value_.length = 0; | |
1078 | |
1079 if (this.directObserver_) { | |
1080 this.directObserver_.close(this); | |
1081 this.directObserver_ = undefined; | |
1082 } | |
1083 }, | |
1084 | |
1085 addPath: function(object, path) { | |
1086 if (this.state_ != UNOPENED && this.state_ != RESETTING) | |
1087 throw Error('Cannot add paths once started.'); | |
1088 | |
1089 var path = getPath(path); | |
1090 this.observed_.push(object, path); | |
1091 if (!this.reportChangesOnOpen_) | |
1092 return; | |
1093 var index = this.observed_.length / 2 - 1; | |
1094 this.value_[index] = path.getValueFrom(object); | |
1095 }, | |
1096 | |
1097 addObserver: function(observer) { | |
1098 if (this.state_ != UNOPENED && this.state_ != RESETTING) | |
1099 throw Error('Cannot add observers once started.'); | |
1100 | |
1101 this.observed_.push(observerSentinel, observer); | |
1102 if (!this.reportChangesOnOpen_) | |
1103 return; | |
1104 var index = this.observed_.length / 2 - 1; | |
1105 this.value_[index] = observer.open(this.deliver, this); | |
1106 }, | |
1107 | |
1108 startReset: function() { | |
1109 if (this.state_ != OPENED) | |
1110 throw Error('Can only reset while open'); | |
1111 | |
1112 this.state_ = RESETTING; | |
1113 this.disconnect_(); | |
1114 }, | |
1115 | |
1116 finishReset: function() { | |
1117 if (this.state_ != RESETTING) | |
1118 throw Error('Can only finishReset after startReset'); | |
1119 this.state_ = OPENED; | |
1120 this.connect_(); | |
1121 | |
1122 return this.value_; | |
1123 }, | |
1124 | |
1125 iterateObjects_: function(observe) { | |
1126 var object; | |
1127 for (var i = 0; i < this.observed_.length; i += 2) { | |
1128 object = this.observed_[i] | |
1129 if (object !== observerSentinel) | |
1130 this.observed_[i + 1].iterateObjects(object, observe) | |
1131 } | |
1132 }, | |
1133 | |
1134 check_: function(changeRecords, skipChanges) { | |
1135 var oldValues; | |
1136 for (var i = 0; i < this.observed_.length; i += 2) { | |
1137 var object = this.observed_[i]; | |
1138 var path = this.observed_[i+1]; | |
1139 var value; | |
1140 if (object === observerSentinel) { | |
1141 var observable = path; | |
1142 value = this.state_ === UNOPENED ? | |
1143 observable.open(this.deliver, this) : | |
1144 observable.discardChanges(); | |
1145 } else { | |
1146 value = path.getValueFrom(object); | |
1147 } | |
1148 | |
1149 if (skipChanges) { | |
1150 this.value_[i / 2] = value; | |
1151 continue; | |
1152 } | |
1153 | |
1154 if (areSameValue(value, this.value_[i / 2])) | |
1155 continue; | |
1156 | |
1157 oldValues = oldValues || []; | |
1158 oldValues[i / 2] = this.value_[i / 2]; | |
1159 this.value_[i / 2] = value; | |
1160 } | |
1161 | |
1162 if (!oldValues) | |
1163 return false; | |
1164 | |
1165 // TODO(rafaelw): Having observed_ as the third callback arg here is | |
1166 // pretty lame API. Fix. | |
1167 this.report_([this.value_, oldValues, this.observed_]); | |
1168 return true; | |
1169 } | |
1170 }); | |
1171 | |
1172 function identFn(value) { return value; } | |
1173 | |
1174 function ObserverTransform(observable, getValueFn, setValueFn, | |
1175 dontPassThroughSet) { | |
1176 this.callback_ = undefined; | |
1177 this.target_ = undefined; | |
1178 this.value_ = undefined; | |
1179 this.observable_ = observable; | |
1180 this.getValueFn_ = getValueFn || identFn; | |
1181 this.setValueFn_ = setValueFn || identFn; | |
1182 // TODO(rafaelw): This is a temporary hack. PolymerExpressions needs this | |
1183 // at the moment because of a bug in it's dependency tracking. | |
1184 this.dontPassThroughSet_ = dontPassThroughSet; | |
1185 } | |
1186 | |
1187 ObserverTransform.prototype = { | |
1188 open: function(callback, target) { | |
1189 this.callback_ = callback; | |
1190 this.target_ = target; | |
1191 this.value_ = | |
1192 this.getValueFn_(this.observable_.open(this.observedCallback_, this)); | |
1193 return this.value_; | |
1194 }, | |
1195 | |
1196 observedCallback_: function(value) { | |
1197 value = this.getValueFn_(value); | |
1198 if (areSameValue(value, this.value_)) | |
1199 return; | |
1200 var oldValue = this.value_; | |
1201 this.value_ = value; | |
1202 this.callback_.call(this.target_, this.value_, oldValue); | |
1203 }, | |
1204 | |
1205 discardChanges: function() { | |
1206 this.value_ = this.getValueFn_(this.observable_.discardChanges()); | |
1207 return this.value_; | |
1208 }, | |
1209 | |
1210 deliver: function() { | |
1211 return this.observable_.deliver(); | |
1212 }, | |
1213 | |
1214 setValue: function(value) { | |
1215 value = this.setValueFn_(value); | |
1216 if (!this.dontPassThroughSet_ && this.observable_.setValue) | |
1217 return this.observable_.setValue(value); | |
1218 }, | |
1219 | |
1220 close: function() { | |
1221 if (this.observable_) | |
1222 this.observable_.close(); | |
1223 this.callback_ = undefined; | |
1224 this.target_ = undefined; | |
1225 this.observable_ = undefined; | |
1226 this.value_ = undefined; | |
1227 this.getValueFn_ = undefined; | |
1228 this.setValueFn_ = undefined; | |
1229 } | |
1230 } | |
1231 | |
1232 var expectedRecordTypes = { | |
1233 add: true, | |
1234 update: true, | |
1235 delete: true | |
1236 }; | |
1237 | |
1238 function diffObjectFromChangeRecords(object, changeRecords, oldValues) { | |
1239 var added = {}; | |
1240 var removed = {}; | |
1241 | |
1242 for (var i = 0; i < changeRecords.length; i++) { | |
1243 var record = changeRecords[i]; | |
1244 if (!expectedRecordTypes[record.type]) { | |
1245 console.error('Unknown changeRecord type: ' + record.type); | |
1246 console.error(record); | |
1247 continue; | |
1248 } | |
1249 | |
1250 if (!(record.name in oldValues)) | |
1251 oldValues[record.name] = record.oldValue; | |
1252 | |
1253 if (record.type == 'update') | |
1254 continue; | |
1255 | |
1256 if (record.type == 'add') { | |
1257 if (record.name in removed) | |
1258 delete removed[record.name]; | |
1259 else | |
1260 added[record.name] = true; | |
1261 | |
1262 continue; | |
1263 } | |
1264 | |
1265 // type = 'delete' | |
1266 if (record.name in added) { | |
1267 delete added[record.name]; | |
1268 delete oldValues[record.name]; | |
1269 } else { | |
1270 removed[record.name] = true; | |
1271 } | |
1272 } | |
1273 | |
1274 for (var prop in added) | |
1275 added[prop] = object[prop]; | |
1276 | |
1277 for (var prop in removed) | |
1278 removed[prop] = undefined; | |
1279 | |
1280 var changed = {}; | |
1281 for (var prop in oldValues) { | |
1282 if (prop in added || prop in removed) | |
1283 continue; | |
1284 | |
1285 var newValue = object[prop]; | |
1286 if (oldValues[prop] !== newValue) | |
1287 changed[prop] = newValue; | |
1288 } | |
1289 | |
1290 return { | |
1291 added: added, | |
1292 removed: removed, | |
1293 changed: changed | |
1294 }; | |
1295 } | |
1296 | |
1297 function newSplice(index, removed, addedCount) { | |
1298 return { | |
1299 index: index, | |
1300 removed: removed, | |
1301 addedCount: addedCount | |
1302 }; | |
1303 } | |
1304 | |
1305 var EDIT_LEAVE = 0; | |
1306 var EDIT_UPDATE = 1; | |
1307 var EDIT_ADD = 2; | |
1308 var EDIT_DELETE = 3; | |
1309 | |
1310 function ArraySplice() {} | |
1311 | |
1312 ArraySplice.prototype = { | |
1313 | |
1314 // Note: This function is *based* on the computation of the Levenshtein | |
1315 // "edit" distance. The one change is that "updates" are treated as two | |
1316 // edits - not one. With Array splices, an update is really a delete | |
1317 // followed by an add. By retaining this, we optimize for "keeping" the | |
1318 // maximum array items in the original array. For example: | |
1319 // | |
1320 // 'xxxx123' -> '123yyyy' | |
1321 // | |
1322 // With 1-edit updates, the shortest path would be just to update all seven | |
1323 // characters. With 2-edit updates, we delete 4, leave 3, and add 4. This | |
1324 // leaves the substring '123' intact. | |
1325 calcEditDistances: function(current, currentStart, currentEnd, | |
1326 old, oldStart, oldEnd) { | |
1327 // "Deletion" columns | |
1328 var rowCount = oldEnd - oldStart + 1; | |
1329 var columnCount = currentEnd - currentStart + 1; | |
1330 var distances = new Array(rowCount); | |
1331 | |
1332 // "Addition" rows. Initialize null column. | |
1333 for (var i = 0; i < rowCount; i++) { | |
1334 distances[i] = new Array(columnCount); | |
1335 distances[i][0] = i; | |
1336 } | |
1337 | |
1338 // Initialize null row | |
1339 for (var j = 0; j < columnCount; j++) | |
1340 distances[0][j] = j; | |
1341 | |
1342 for (var i = 1; i < rowCount; i++) { | |
1343 for (var j = 1; j < columnCount; j++) { | |
1344 if (this.equals(current[currentStart + j - 1], old[oldStart + i - 1])) | |
1345 distances[i][j] = distances[i - 1][j - 1]; | |
1346 else { | |
1347 var north = distances[i - 1][j] + 1; | |
1348 var west = distances[i][j - 1] + 1; | |
1349 distances[i][j] = north < west ? north : west; | |
1350 } | |
1351 } | |
1352 } | |
1353 | |
1354 return distances; | |
1355 }, | |
1356 | |
1357 // This starts at the final weight, and walks "backward" by finding | |
1358 // the minimum previous weight recursively until the origin of the weight | |
1359 // matrix. | |
1360 spliceOperationsFromEditDistances: function(distances) { | |
1361 var i = distances.length - 1; | |
1362 var j = distances[0].length - 1; | |
1363 var current = distances[i][j]; | |
1364 var edits = []; | |
1365 while (i > 0 || j > 0) { | |
1366 if (i == 0) { | |
1367 edits.push(EDIT_ADD); | |
1368 j--; | |
1369 continue; | |
1370 } | |
1371 if (j == 0) { | |
1372 edits.push(EDIT_DELETE); | |
1373 i--; | |
1374 continue; | |
1375 } | |
1376 var northWest = distances[i - 1][j - 1]; | |
1377 var west = distances[i - 1][j]; | |
1378 var north = distances[i][j - 1]; | |
1379 | |
1380 var min; | |
1381 if (west < north) | |
1382 min = west < northWest ? west : northWest; | |
1383 else | |
1384 min = north < northWest ? north : northWest; | |
1385 | |
1386 if (min == northWest) { | |
1387 if (northWest == current) { | |
1388 edits.push(EDIT_LEAVE); | |
1389 } else { | |
1390 edits.push(EDIT_UPDATE); | |
1391 current = northWest; | |
1392 } | |
1393 i--; | |
1394 j--; | |
1395 } else if (min == west) { | |
1396 edits.push(EDIT_DELETE); | |
1397 i--; | |
1398 current = west; | |
1399 } else { | |
1400 edits.push(EDIT_ADD); | |
1401 j--; | |
1402 current = north; | |
1403 } | |
1404 } | |
1405 | |
1406 edits.reverse(); | |
1407 return edits; | |
1408 }, | |
1409 | |
1410 /** | |
1411 * Splice Projection functions: | |
1412 * | |
1413 * A splice map is a representation of how a previous array of items | |
1414 * was transformed into a new array of items. Conceptually it is a list of | |
1415 * tuples of | |
1416 * | |
1417 * <index, removed, addedCount> | |
1418 * | |
1419 * which are kept in ascending index order of. The tuple represents that at | |
1420 * the |index|, |removed| sequence of items were removed, and counting forwa
rd | |
1421 * from |index|, |addedCount| items were added. | |
1422 */ | |
1423 | |
1424 /** | |
1425 * Lacking individual splice mutation information, the minimal set of | |
1426 * splices can be synthesized given the previous state and final state of an | |
1427 * array. The basic approach is to calculate the edit distance matrix and | |
1428 * choose the shortest path through it. | |
1429 * | |
1430 * Complexity: O(l * p) | |
1431 * l: The length of the current array | |
1432 * p: The length of the old array | |
1433 */ | |
1434 calcSplices: function(current, currentStart, currentEnd, | |
1435 old, oldStart, oldEnd) { | |
1436 var prefixCount = 0; | |
1437 var suffixCount = 0; | |
1438 | |
1439 var minLength = Math.min(currentEnd - currentStart, oldEnd - oldStart); | |
1440 if (currentStart == 0 && oldStart == 0) | |
1441 prefixCount = this.sharedPrefix(current, old, minLength); | |
1442 | |
1443 if (currentEnd == current.length && oldEnd == old.length) | |
1444 suffixCount = this.sharedSuffix(current, old, minLength - prefixCount); | |
1445 | |
1446 currentStart += prefixCount; | |
1447 oldStart += prefixCount; | |
1448 currentEnd -= suffixCount; | |
1449 oldEnd -= suffixCount; | |
1450 | |
1451 if (currentEnd - currentStart == 0 && oldEnd - oldStart == 0) | |
1452 return []; | |
1453 | |
1454 if (currentStart == currentEnd) { | |
1455 var splice = newSplice(currentStart, [], 0); | |
1456 while (oldStart < oldEnd) | |
1457 splice.removed.push(old[oldStart++]); | |
1458 | |
1459 return [ splice ]; | |
1460 } else if (oldStart == oldEnd) | |
1461 return [ newSplice(currentStart, [], currentEnd - currentStart) ]; | |
1462 | |
1463 var ops = this.spliceOperationsFromEditDistances( | |
1464 this.calcEditDistances(current, currentStart, currentEnd, | |
1465 old, oldStart, oldEnd)); | |
1466 | |
1467 var splice = undefined; | |
1468 var splices = []; | |
1469 var index = currentStart; | |
1470 var oldIndex = oldStart; | |
1471 for (var i = 0; i < ops.length; i++) { | |
1472 switch(ops[i]) { | |
1473 case EDIT_LEAVE: | |
1474 if (splice) { | |
1475 splices.push(splice); | |
1476 splice = undefined; | |
1477 } | |
1478 | |
1479 index++; | |
1480 oldIndex++; | |
1481 break; | |
1482 case EDIT_UPDATE: | |
1483 if (!splice) | |
1484 splice = newSplice(index, [], 0); | |
1485 | |
1486 splice.addedCount++; | |
1487 index++; | |
1488 | |
1489 splice.removed.push(old[oldIndex]); | |
1490 oldIndex++; | |
1491 break; | |
1492 case EDIT_ADD: | |
1493 if (!splice) | |
1494 splice = newSplice(index, [], 0); | |
1495 | |
1496 splice.addedCount++; | |
1497 index++; | |
1498 break; | |
1499 case EDIT_DELETE: | |
1500 if (!splice) | |
1501 splice = newSplice(index, [], 0); | |
1502 | |
1503 splice.removed.push(old[oldIndex]); | |
1504 oldIndex++; | |
1505 break; | |
1506 } | |
1507 } | |
1508 | |
1509 if (splice) { | |
1510 splices.push(splice); | |
1511 } | |
1512 return splices; | |
1513 }, | |
1514 | |
1515 sharedPrefix: function(current, old, searchLength) { | |
1516 for (var i = 0; i < searchLength; i++) | |
1517 if (!this.equals(current[i], old[i])) | |
1518 return i; | |
1519 return searchLength; | |
1520 }, | |
1521 | |
1522 sharedSuffix: function(current, old, searchLength) { | |
1523 var index1 = current.length; | |
1524 var index2 = old.length; | |
1525 var count = 0; | |
1526 while (count < searchLength && this.equals(current[--index1], old[--index2
])) | |
1527 count++; | |
1528 | |
1529 return count; | |
1530 }, | |
1531 | |
1532 calculateSplices: function(current, previous) { | |
1533 return this.calcSplices(current, 0, current.length, previous, 0, | |
1534 previous.length); | |
1535 }, | |
1536 | |
1537 equals: function(currentValue, previousValue) { | |
1538 return currentValue === previousValue; | |
1539 } | |
1540 }; | |
1541 | |
1542 var arraySplice = new ArraySplice(); | |
1543 | |
1544 function calcSplices(current, currentStart, currentEnd, | |
1545 old, oldStart, oldEnd) { | |
1546 return arraySplice.calcSplices(current, currentStart, currentEnd, | |
1547 old, oldStart, oldEnd); | |
1548 } | |
1549 | |
1550 function intersect(start1, end1, start2, end2) { | |
1551 // Disjoint | |
1552 if (end1 < start2 || end2 < start1) | |
1553 return -1; | |
1554 | |
1555 // Adjacent | |
1556 if (end1 == start2 || end2 == start1) | |
1557 return 0; | |
1558 | |
1559 // Non-zero intersect, span1 first | |
1560 if (start1 < start2) { | |
1561 if (end1 < end2) | |
1562 return end1 - start2; // Overlap | |
1563 else | |
1564 return end2 - start2; // Contained | |
1565 } else { | |
1566 // Non-zero intersect, span2 first | |
1567 if (end2 < end1) | |
1568 return end2 - start1; // Overlap | |
1569 else | |
1570 return end1 - start1; // Contained | |
1571 } | |
1572 } | |
1573 | |
1574 function mergeSplice(splices, index, removed, addedCount) { | |
1575 | |
1576 var splice = newSplice(index, removed, addedCount); | |
1577 | |
1578 var inserted = false; | |
1579 var insertionOffset = 0; | |
1580 | |
1581 for (var i = 0; i < splices.length; i++) { | |
1582 var current = splices[i]; | |
1583 current.index += insertionOffset; | |
1584 | |
1585 if (inserted) | |
1586 continue; | |
1587 | |
1588 var intersectCount = intersect(splice.index, | |
1589 splice.index + splice.removed.length, | |
1590 current.index, | |
1591 current.index + current.addedCount); | |
1592 | |
1593 if (intersectCount >= 0) { | |
1594 // Merge the two splices | |
1595 | |
1596 splices.splice(i, 1); | |
1597 i--; | |
1598 | |
1599 insertionOffset -= current.addedCount - current.removed.length; | |
1600 | |
1601 splice.addedCount += current.addedCount - intersectCount; | |
1602 var deleteCount = splice.removed.length + | |
1603 current.removed.length - intersectCount; | |
1604 | |
1605 if (!splice.addedCount && !deleteCount) { | |
1606 // merged splice is a noop. discard. | |
1607 inserted = true; | |
1608 } else { | |
1609 var removed = current.removed; | |
1610 | |
1611 if (splice.index < current.index) { | |
1612 // some prefix of splice.removed is prepended to current.removed. | |
1613 var prepend = splice.removed.slice(0, current.index - splice.index); | |
1614 Array.prototype.push.apply(prepend, removed); | |
1615 removed = prepend; | |
1616 } | |
1617 | |
1618 if (splice.index + splice.removed.length > current.index + current.add
edCount) { | |
1619 // some suffix of splice.removed is appended to current.removed. | |
1620 var append = splice.removed.slice(current.index + current.addedCount
- splice.index); | |
1621 Array.prototype.push.apply(removed, append); | |
1622 } | |
1623 | |
1624 splice.removed = removed; | |
1625 if (current.index < splice.index) { | |
1626 splice.index = current.index; | |
1627 } | |
1628 } | |
1629 } else if (splice.index < current.index) { | |
1630 // Insert splice here. | |
1631 | |
1632 inserted = true; | |
1633 | |
1634 splices.splice(i, 0, splice); | |
1635 i++; | |
1636 | |
1637 var offset = splice.addedCount - splice.removed.length | |
1638 current.index += offset; | |
1639 insertionOffset += offset; | |
1640 } | |
1641 } | |
1642 | |
1643 if (!inserted) | |
1644 splices.push(splice); | |
1645 } | |
1646 | |
1647 function createInitialSplices(array, changeRecords) { | |
1648 var splices = []; | |
1649 | |
1650 for (var i = 0; i < changeRecords.length; i++) { | |
1651 var record = changeRecords[i]; | |
1652 switch(record.type) { | |
1653 case 'splice': | |
1654 mergeSplice(splices, record.index, record.removed.slice(), record.adde
dCount); | |
1655 break; | |
1656 case 'add': | |
1657 case 'update': | |
1658 case 'delete': | |
1659 if (!isIndex(record.name)) | |
1660 continue; | |
1661 var index = toNumber(record.name); | |
1662 if (index < 0) | |
1663 continue; | |
1664 mergeSplice(splices, index, [record.oldValue], 1); | |
1665 break; | |
1666 default: | |
1667 console.error('Unexpected record type: ' + JSON.stringify(record)); | |
1668 break; | |
1669 } | |
1670 } | |
1671 | |
1672 return splices; | |
1673 } | |
1674 | |
1675 function projectArraySplices(array, changeRecords) { | |
1676 var splices = []; | |
1677 | |
1678 createInitialSplices(array, changeRecords).forEach(function(splice) { | |
1679 if (splice.addedCount == 1 && splice.removed.length == 1) { | |
1680 if (splice.removed[0] !== array[splice.index]) | |
1681 splices.push(splice); | |
1682 | |
1683 return | |
1684 }; | |
1685 | |
1686 splices = splices.concat(calcSplices(array, splice.index, splice.index + s
plice.addedCount, | |
1687 splice.removed, 0, splice.removed.len
gth)); | |
1688 }); | |
1689 | |
1690 return splices; | |
1691 } | |
1692 | |
1693 global.Observer = Observer; | |
1694 global.Observer.runEOM_ = runEOM; | |
1695 global.Observer.observerSentinel_ = observerSentinel; // for testing. | |
1696 global.Observer.hasObjectObserve = hasObserve; | |
1697 global.ArrayObserver = ArrayObserver; | |
1698 global.ArrayObserver.calculateSplices = function(current, previous) { | |
1699 return arraySplice.calculateSplices(current, previous); | |
1700 }; | |
1701 | |
1702 global.ArraySplice = ArraySplice; | |
1703 global.ObjectObserver = ObjectObserver; | |
1704 global.PathObserver = PathObserver; | |
1705 global.CompoundObserver = CompoundObserver; | |
1706 global.Path = Path; | |
1707 global.ObserverTransform = ObserverTransform; | |
1708 })(typeof global !== 'undefined' && global && typeof module !== 'undefined' && m
odule ? global : this || window); | |
OLD | NEW |