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