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