Upstream version 9.38.198.0
[platform/framework/web/crosswalk.git] / src / v8 / src / object-observe.js
1 // Copyright 2012 the V8 project authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 "use strict";
6
7 // Overview:
8 //
9 // This file contains all of the routing and accounting for Object.observe.
10 // User code will interact with these mechanisms via the Object.observe APIs
11 // and, as a side effect of mutation objects which are observed. The V8 runtime
12 // (both C++ and JS) will interact with these mechanisms primarily by enqueuing
13 // proper change records for objects which were mutated. The Object.observe
14 // routing and accounting consists primarily of three participants
15 //
16 // 1) ObjectInfo. This represents the observed state of a given object. It
17 //    records what callbacks are observing the object, with what options, and
18 //    what "change types" are in progress on the object (i.e. via
19 //    notifier.performChange).
20 //
21 // 2) CallbackInfo. This represents a callback used for observation. It holds
22 //    the records which must be delivered to the callback, as well as the global
23 //    priority of the callback (which determines delivery order between
24 //    callbacks).
25 //
26 // 3) observationState.pendingObservers. This is the set of observers which
27 //    have change records which must be delivered. During "normal" delivery
28 //    (i.e. not Object.deliverChangeRecords), this is the mechanism by which
29 //    callbacks are invoked in the proper order until there are no more
30 //    change records pending to a callback.
31 //
32 // Note that in order to reduce allocation and processing costs, the
33 // implementation of (1) and (2) have "optimized" states which represent
34 // common cases which can be handled more efficiently.
35
36 var observationState;
37
38 function GetObservationStateJS() {
39   if (IS_UNDEFINED(observationState))
40     observationState = %GetObservationState();
41
42   if (IS_UNDEFINED(observationState.callbackInfoMap)) {
43     observationState.callbackInfoMap = %ObservationWeakMapCreate();
44     observationState.objectInfoMap = %ObservationWeakMapCreate();
45     observationState.notifierObjectInfoMap = %ObservationWeakMapCreate();
46     observationState.pendingObservers = null;
47     observationState.nextCallbackPriority = 0;
48     observationState.lastMicrotaskId = 0;
49   }
50
51   return observationState;
52 }
53
54 function GetWeakMapWrapper() {
55   function MapWrapper(map) {
56     this.map_ = map;
57   };
58
59   MapWrapper.prototype = {
60     __proto__: null,
61     get: function(key) {
62       return %WeakCollectionGet(this.map_, key);
63     },
64     set: function(key, value) {
65       %WeakCollectionSet(this.map_, key, value);
66     },
67     has: function(key) {
68       return !IS_UNDEFINED(this.get(key));
69     }
70   };
71
72   return MapWrapper;
73 }
74
75 var contextMaps;
76
77 function GetContextMaps() {
78   if (IS_UNDEFINED(contextMaps)) {
79     var map = GetWeakMapWrapper();
80     var observationState = GetObservationStateJS();
81     contextMaps = {
82       callbackInfoMap: new map(observationState.callbackInfoMap),
83       objectInfoMap: new map(observationState.objectInfoMap),
84       notifierObjectInfoMap: new map(observationState.notifierObjectInfoMap)
85     };
86   }
87
88   return contextMaps;
89 }
90
91 function GetCallbackInfoMap() {
92   return GetContextMaps().callbackInfoMap;
93 }
94
95 function GetObjectInfoMap() {
96   return GetContextMaps().objectInfoMap;
97 }
98
99 function GetNotifierObjectInfoMap() {
100   return GetContextMaps().notifierObjectInfoMap;
101 }
102
103 function GetPendingObservers() {
104   return GetObservationStateJS().pendingObservers;
105 }
106
107 function SetPendingObservers(pendingObservers) {
108   GetObservationStateJS().pendingObservers = pendingObservers;
109 }
110
111 function GetNextCallbackPriority() {
112   return GetObservationStateJS().nextCallbackPriority++;
113 }
114
115 function nullProtoObject() {
116   return { __proto__: null };
117 }
118
119 function TypeMapCreate() {
120   return nullProtoObject();
121 }
122
123 function TypeMapAddType(typeMap, type, ignoreDuplicate) {
124   typeMap[type] = ignoreDuplicate ? 1 : (typeMap[type] || 0) + 1;
125 }
126
127 function TypeMapRemoveType(typeMap, type) {
128   typeMap[type]--;
129 }
130
131 function TypeMapCreateFromList(typeList, length) {
132   var typeMap = TypeMapCreate();
133   for (var i = 0; i < length; i++) {
134     TypeMapAddType(typeMap, typeList[i], true);
135   }
136   return typeMap;
137 }
138
139 function TypeMapHasType(typeMap, type) {
140   return !!typeMap[type];
141 }
142
143 function TypeMapIsDisjointFrom(typeMap1, typeMap2) {
144   if (!typeMap1 || !typeMap2)
145     return true;
146
147   for (var type in typeMap1) {
148     if (TypeMapHasType(typeMap1, type) && TypeMapHasType(typeMap2, type))
149       return false;
150   }
151
152   return true;
153 }
154
155 var defaultAcceptTypes = (function() {
156   var defaultTypes = [
157     'add',
158     'update',
159     'delete',
160     'setPrototype',
161     'reconfigure',
162     'preventExtensions'
163   ];
164   return TypeMapCreateFromList(defaultTypes, defaultTypes.length);
165 })();
166
167 // An Observer is a registration to observe an object by a callback with
168 // a given set of accept types. If the set of accept types is the default
169 // set for Object.observe, the observer is represented as a direct reference
170 // to the callback. An observer never changes its accept types and thus never
171 // needs to "normalize".
172 function ObserverCreate(callback, acceptList) {
173   if (IS_UNDEFINED(acceptList))
174     return callback;
175   var observer = nullProtoObject();
176   observer.callback = callback;
177   observer.accept = acceptList;
178   return observer;
179 }
180
181 function ObserverGetCallback(observer) {
182   return IS_SPEC_FUNCTION(observer) ? observer : observer.callback;
183 }
184
185 function ObserverGetAcceptTypes(observer) {
186   return IS_SPEC_FUNCTION(observer) ? defaultAcceptTypes : observer.accept;
187 }
188
189 function ObserverIsActive(observer, objectInfo) {
190   return TypeMapIsDisjointFrom(ObjectInfoGetPerformingTypes(objectInfo),
191                                ObserverGetAcceptTypes(observer));
192 }
193
194 function ObjectInfoGetOrCreate(object) {
195   var objectInfo = ObjectInfoGet(object);
196   if (IS_UNDEFINED(objectInfo)) {
197     if (!%IsJSProxy(object))
198       %SetIsObserved(object);
199
200     objectInfo = {
201       object: object,
202       changeObservers: null,
203       notifier: null,
204       performing: null,
205       performingCount: 0,
206     };
207     GetObjectInfoMap().set(object, objectInfo);
208   }
209   return objectInfo;
210 }
211
212 function ObjectInfoGet(object) {
213   return GetObjectInfoMap().get(object);
214 }
215
216 function ObjectInfoGetFromNotifier(notifier) {
217   return GetNotifierObjectInfoMap().get(notifier);
218 }
219
220 function ObjectInfoGetNotifier(objectInfo) {
221   if (IS_NULL(objectInfo.notifier)) {
222     objectInfo.notifier = { __proto__: notifierPrototype };
223     GetNotifierObjectInfoMap().set(objectInfo.notifier, objectInfo);
224   }
225
226   return objectInfo.notifier;
227 }
228
229 function ObjectInfoGetObject(objectInfo) {
230   return objectInfo.object;
231 }
232
233 function ChangeObserversIsOptimized(changeObservers) {
234   return typeof changeObservers === 'function' ||
235          typeof changeObservers.callback === 'function';
236 }
237
238 // The set of observers on an object is called 'changeObservers'. The first
239 // observer is referenced directly via objectInfo.changeObservers. When a second
240 // is added, changeObservers "normalizes" to become a mapping of callback
241 // priority -> observer and is then stored on objectInfo.changeObservers.
242 function ObjectInfoNormalizeChangeObservers(objectInfo) {
243   if (ChangeObserversIsOptimized(objectInfo.changeObservers)) {
244     var observer = objectInfo.changeObservers;
245     var callback = ObserverGetCallback(observer);
246     var callbackInfo = CallbackInfoGet(callback);
247     var priority = CallbackInfoGetPriority(callbackInfo);
248     objectInfo.changeObservers = nullProtoObject();
249     objectInfo.changeObservers[priority] = observer;
250   }
251 }
252
253 function ObjectInfoAddObserver(objectInfo, callback, acceptList) {
254   var callbackInfo = CallbackInfoGetOrCreate(callback);
255   var observer = ObserverCreate(callback, acceptList);
256
257   if (!objectInfo.changeObservers) {
258     objectInfo.changeObservers = observer;
259     return;
260   }
261
262   ObjectInfoNormalizeChangeObservers(objectInfo);
263   var priority = CallbackInfoGetPriority(callbackInfo);
264   objectInfo.changeObservers[priority] = observer;
265 }
266
267 function ObjectInfoRemoveObserver(objectInfo, callback) {
268   if (!objectInfo.changeObservers)
269     return;
270
271   if (ChangeObserversIsOptimized(objectInfo.changeObservers)) {
272     if (callback === ObserverGetCallback(objectInfo.changeObservers))
273       objectInfo.changeObservers = null;
274     return;
275   }
276
277   var callbackInfo = CallbackInfoGet(callback);
278   var priority = CallbackInfoGetPriority(callbackInfo);
279   objectInfo.changeObservers[priority] = null;
280 }
281
282 function ObjectInfoHasActiveObservers(objectInfo) {
283   if (IS_UNDEFINED(objectInfo) || !objectInfo.changeObservers)
284     return false;
285
286   if (ChangeObserversIsOptimized(objectInfo.changeObservers))
287     return ObserverIsActive(objectInfo.changeObservers, objectInfo);
288
289   for (var priority in objectInfo.changeObservers) {
290     var observer = objectInfo.changeObservers[priority];
291     if (!IS_NULL(observer) && ObserverIsActive(observer, objectInfo))
292       return true;
293   }
294
295   return false;
296 }
297
298 function ObjectInfoAddPerformingType(objectInfo, type) {
299   objectInfo.performing = objectInfo.performing || TypeMapCreate();
300   TypeMapAddType(objectInfo.performing, type);
301   objectInfo.performingCount++;
302 }
303
304 function ObjectInfoRemovePerformingType(objectInfo, type) {
305   objectInfo.performingCount--;
306   TypeMapRemoveType(objectInfo.performing, type);
307 }
308
309 function ObjectInfoGetPerformingTypes(objectInfo) {
310   return objectInfo.performingCount > 0 ? objectInfo.performing : null;
311 }
312
313 function ConvertAcceptListToTypeMap(arg) {
314   // We use undefined as a sentinel for the default accept list.
315   if (IS_UNDEFINED(arg))
316     return arg;
317
318   if (!IS_SPEC_OBJECT(arg))
319     throw MakeTypeError("observe_accept_invalid");
320
321   var len = ToInteger(arg.length);
322   if (len < 0) len = 0;
323
324   return TypeMapCreateFromList(arg, len);
325 }
326
327 // CallbackInfo's optimized state is just a number which represents its global
328 // priority. When a change record must be enqueued for the callback, it
329 // normalizes. When delivery clears any pending change records, it re-optimizes.
330 function CallbackInfoGet(callback) {
331   return GetCallbackInfoMap().get(callback);
332 }
333
334 function CallbackInfoGetOrCreate(callback) {
335   var callbackInfo = GetCallbackInfoMap().get(callback);
336   if (!IS_UNDEFINED(callbackInfo))
337     return callbackInfo;
338
339   var priority =  GetNextCallbackPriority();
340   GetCallbackInfoMap().set(callback, priority);
341   return priority;
342 }
343
344 function CallbackInfoGetPriority(callbackInfo) {
345   if (IS_NUMBER(callbackInfo))
346     return callbackInfo;
347   else
348     return callbackInfo.priority;
349 }
350
351 function CallbackInfoNormalize(callback) {
352   var callbackInfo = GetCallbackInfoMap().get(callback);
353   if (IS_NUMBER(callbackInfo)) {
354     var priority = callbackInfo;
355     callbackInfo = new InternalArray;
356     callbackInfo.priority = priority;
357     GetCallbackInfoMap().set(callback, callbackInfo);
358   }
359   return callbackInfo;
360 }
361
362 function ObjectObserve(object, callback, acceptList) {
363   if (!IS_SPEC_OBJECT(object))
364     throw MakeTypeError("observe_non_object", ["observe"]);
365   if (%IsJSGlobalProxy(object))
366     throw MakeTypeError("observe_global_proxy", ["observe"]);
367   if (!IS_SPEC_FUNCTION(callback))
368     throw MakeTypeError("observe_non_function", ["observe"]);
369   if (ObjectIsFrozen(callback))
370     throw MakeTypeError("observe_callback_frozen");
371
372   var objectObserveFn = %GetObjectContextObjectObserve(object);
373   return objectObserveFn(object, callback, acceptList);
374 }
375
376 function NativeObjectObserve(object, callback, acceptList) {
377   var objectInfo = ObjectInfoGetOrCreate(object);
378   var typeList = ConvertAcceptListToTypeMap(acceptList);
379   ObjectInfoAddObserver(objectInfo, callback, typeList);
380   return object;
381 }
382
383 function ObjectUnobserve(object, callback) {
384   if (!IS_SPEC_OBJECT(object))
385     throw MakeTypeError("observe_non_object", ["unobserve"]);
386   if (%IsJSGlobalProxy(object))
387     throw MakeTypeError("observe_global_proxy", ["unobserve"]);
388   if (!IS_SPEC_FUNCTION(callback))
389     throw MakeTypeError("observe_non_function", ["unobserve"]);
390
391   var objectInfo = ObjectInfoGet(object);
392   if (IS_UNDEFINED(objectInfo))
393     return object;
394
395   ObjectInfoRemoveObserver(objectInfo, callback);
396   return object;
397 }
398
399 function ArrayObserve(object, callback) {
400   return ObjectObserve(object, callback, ['add',
401                                           'update',
402                                           'delete',
403                                           'splice']);
404 }
405
406 function ArrayUnobserve(object, callback) {
407   return ObjectUnobserve(object, callback);
408 }
409
410 function ObserverEnqueueIfActive(observer, objectInfo, changeRecord) {
411   if (!ObserverIsActive(observer, objectInfo) ||
412       !TypeMapHasType(ObserverGetAcceptTypes(observer), changeRecord.type)) {
413     return;
414   }
415
416   var callback = ObserverGetCallback(observer);
417   if (!%ObserverObjectAndRecordHaveSameOrigin(callback, changeRecord.object,
418                                               changeRecord)) {
419     return;
420   }
421
422   var callbackInfo = CallbackInfoNormalize(callback);
423   if (IS_NULL(GetPendingObservers())) {
424     SetPendingObservers(nullProtoObject());
425     if (DEBUG_IS_ACTIVE) {
426       var id = ++GetObservationStateJS().lastMicrotaskId;
427       var name = "Object.observe";
428       %EnqueueMicrotask(function() {
429         %DebugAsyncTaskEvent({ type: "willHandle", id: id, name: name });
430         ObserveMicrotaskRunner();
431         %DebugAsyncTaskEvent({ type: "didHandle", id: id, name: name });
432       });
433       %DebugAsyncTaskEvent({ type: "enqueue", id: id, name: name });
434     } else {
435       %EnqueueMicrotask(ObserveMicrotaskRunner);
436     }
437   }
438   GetPendingObservers()[callbackInfo.priority] = callback;
439   callbackInfo.push(changeRecord);
440 }
441
442 function ObjectInfoEnqueueExternalChangeRecord(objectInfo, changeRecord, type) {
443   if (!ObjectInfoHasActiveObservers(objectInfo))
444     return;
445
446   var hasType = !IS_UNDEFINED(type);
447   var newRecord = hasType ?
448       { object: ObjectInfoGetObject(objectInfo), type: type } :
449       { object: ObjectInfoGetObject(objectInfo) };
450
451   for (var prop in changeRecord) {
452     if (prop === 'object' || (hasType && prop === 'type')) continue;
453     %DefineDataPropertyUnchecked(
454         newRecord, prop, changeRecord[prop], READ_ONLY + DONT_DELETE);
455   }
456   ObjectFreezeJS(newRecord);
457
458   ObjectInfoEnqueueInternalChangeRecord(objectInfo, newRecord);
459 }
460
461 function ObjectInfoEnqueueInternalChangeRecord(objectInfo, changeRecord) {
462   // TODO(rossberg): adjust once there is a story for symbols vs proxies.
463   if (IS_SYMBOL(changeRecord.name)) return;
464
465   if (ChangeObserversIsOptimized(objectInfo.changeObservers)) {
466     var observer = objectInfo.changeObservers;
467     ObserverEnqueueIfActive(observer, objectInfo, changeRecord);
468     return;
469   }
470
471   for (var priority in objectInfo.changeObservers) {
472     var observer = objectInfo.changeObservers[priority];
473     if (IS_NULL(observer))
474       continue;
475     ObserverEnqueueIfActive(observer, objectInfo, changeRecord);
476   }
477 }
478
479 function BeginPerformSplice(array) {
480   var objectInfo = ObjectInfoGet(array);
481   if (!IS_UNDEFINED(objectInfo))
482     ObjectInfoAddPerformingType(objectInfo, 'splice');
483 }
484
485 function EndPerformSplice(array) {
486   var objectInfo = ObjectInfoGet(array);
487   if (!IS_UNDEFINED(objectInfo))
488     ObjectInfoRemovePerformingType(objectInfo, 'splice');
489 }
490
491 function EnqueueSpliceRecord(array, index, removed, addedCount) {
492   var objectInfo = ObjectInfoGet(array);
493   if (!ObjectInfoHasActiveObservers(objectInfo))
494     return;
495
496   var changeRecord = {
497     type: 'splice',
498     object: array,
499     index: index,
500     removed: removed,
501     addedCount: addedCount
502   };
503
504   ObjectFreezeJS(changeRecord);
505   ObjectFreezeJS(changeRecord.removed);
506   ObjectInfoEnqueueInternalChangeRecord(objectInfo, changeRecord);
507 }
508
509 function NotifyChange(type, object, name, oldValue) {
510   var objectInfo = ObjectInfoGet(object);
511   if (!ObjectInfoHasActiveObservers(objectInfo))
512     return;
513
514   var changeRecord;
515   if (arguments.length == 2) {
516     changeRecord = { type: type, object: object };
517   } else if (arguments.length == 3) {
518     changeRecord = { type: type, object: object, name: name };
519   } else {
520     changeRecord = {
521       type: type,
522       object: object,
523       name: name,
524       oldValue: oldValue
525     };
526   }
527
528   ObjectFreezeJS(changeRecord);
529   ObjectInfoEnqueueInternalChangeRecord(objectInfo, changeRecord);
530 }
531
532 var notifierPrototype = {};
533
534 function ObjectNotifierNotify(changeRecord) {
535   if (!IS_SPEC_OBJECT(this))
536     throw MakeTypeError("called_on_non_object", ["notify"]);
537
538   var objectInfo = ObjectInfoGetFromNotifier(this);
539   if (IS_UNDEFINED(objectInfo))
540     throw MakeTypeError("observe_notify_non_notifier");
541   if (!IS_STRING(changeRecord.type))
542     throw MakeTypeError("observe_type_non_string");
543
544   ObjectInfoEnqueueExternalChangeRecord(objectInfo, changeRecord);
545 }
546
547 function ObjectNotifierPerformChange(changeType, changeFn) {
548   if (!IS_SPEC_OBJECT(this))
549     throw MakeTypeError("called_on_non_object", ["performChange"]);
550
551   var objectInfo = ObjectInfoGetFromNotifier(this);
552   if (IS_UNDEFINED(objectInfo))
553     throw MakeTypeError("observe_notify_non_notifier");
554   if (!IS_STRING(changeType))
555     throw MakeTypeError("observe_perform_non_string");
556   if (!IS_SPEC_FUNCTION(changeFn))
557     throw MakeTypeError("observe_perform_non_function");
558
559   var performChangeFn = %GetObjectContextNotifierPerformChange(objectInfo);
560   performChangeFn(objectInfo, changeType, changeFn);
561 }
562
563 function NativeObjectNotifierPerformChange(objectInfo, changeType, changeFn) {
564   ObjectInfoAddPerformingType(objectInfo, changeType);
565
566   var changeRecord;
567   try {
568     changeRecord = %_CallFunction(UNDEFINED, changeFn);
569   } finally {
570     ObjectInfoRemovePerformingType(objectInfo, changeType);
571   }
572
573   if (IS_SPEC_OBJECT(changeRecord))
574     ObjectInfoEnqueueExternalChangeRecord(objectInfo, changeRecord, changeType);
575 }
576
577 function ObjectGetNotifier(object) {
578   if (!IS_SPEC_OBJECT(object))
579     throw MakeTypeError("observe_non_object", ["getNotifier"]);
580   if (%IsJSGlobalProxy(object))
581     throw MakeTypeError("observe_global_proxy", ["getNotifier"]);
582
583   if (ObjectIsFrozen(object)) return null;
584
585   if (!%ObjectWasCreatedInCurrentOrigin(object)) return null;
586
587   var getNotifierFn = %GetObjectContextObjectGetNotifier(object);
588   return getNotifierFn(object);
589 }
590
591 function NativeObjectGetNotifier(object) {
592   var objectInfo = ObjectInfoGetOrCreate(object);
593   return ObjectInfoGetNotifier(objectInfo);
594 }
595
596 function CallbackDeliverPending(callback) {
597   var callbackInfo = GetCallbackInfoMap().get(callback);
598   if (IS_UNDEFINED(callbackInfo) || IS_NUMBER(callbackInfo))
599     return false;
600
601   // Clear the pending change records from callback and return it to its
602   // "optimized" state.
603   var priority = callbackInfo.priority;
604   GetCallbackInfoMap().set(callback, priority);
605
606   if (GetPendingObservers())
607     delete GetPendingObservers()[priority];
608
609   var delivered = [];
610   %MoveArrayContents(callbackInfo, delivered);
611
612   try {
613     %_CallFunction(UNDEFINED, delivered, callback);
614   } catch (ex) {}  // TODO(rossberg): perhaps log uncaught exceptions.
615   return true;
616 }
617
618 function ObjectDeliverChangeRecords(callback) {
619   if (!IS_SPEC_FUNCTION(callback))
620     throw MakeTypeError("observe_non_function", ["deliverChangeRecords"]);
621
622   while (CallbackDeliverPending(callback)) {}
623 }
624
625 function ObserveMicrotaskRunner() {
626   var pendingObservers = GetPendingObservers();
627   if (pendingObservers) {
628     SetPendingObservers(null);
629     for (var i in pendingObservers) {
630       CallbackDeliverPending(pendingObservers[i]);
631     }
632   }
633 }
634
635 function SetupObjectObserve() {
636   %CheckIsBootstrapping();
637   InstallFunctions($Object, DONT_ENUM, $Array(
638     "deliverChangeRecords", ObjectDeliverChangeRecords,
639     "getNotifier", ObjectGetNotifier,
640     "observe", ObjectObserve,
641     "unobserve", ObjectUnobserve
642   ));
643   InstallFunctions($Array, DONT_ENUM, $Array(
644     "observe", ArrayObserve,
645     "unobserve", ArrayUnobserve
646   ));
647   InstallFunctions(notifierPrototype, DONT_ENUM, $Array(
648     "notify", ObjectNotifierNotify,
649     "performChange", ObjectNotifierPerformChange
650   ));
651 }
652
653 SetupObjectObserve();