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
36var observationState;
37
38function 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
54function 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
75var contextMaps;
76
77function 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
91function GetCallbackInfoMap() {
92  return GetContextMaps().callbackInfoMap;
93}
94
95function GetObjectInfoMap() {
96  return GetContextMaps().objectInfoMap;
97}
98
99function GetNotifierObjectInfoMap() {
100  return GetContextMaps().notifierObjectInfoMap;
101}
102
103function GetPendingObservers() {
104  return GetObservationStateJS().pendingObservers;
105}
106
107function SetPendingObservers(pendingObservers) {
108  GetObservationStateJS().pendingObservers = pendingObservers;
109}
110
111function GetNextCallbackPriority() {
112  return GetObservationStateJS().nextCallbackPriority++;
113}
114
115function nullProtoObject() {
116  return { __proto__: null };
117}
118
119function TypeMapCreate() {
120  return nullProtoObject();
121}
122
123function TypeMapAddType(typeMap, type, ignoreDuplicate) {
124  typeMap[type] = ignoreDuplicate ? 1 : (typeMap[type] || 0) + 1;
125}
126
127function TypeMapRemoveType(typeMap, type) {
128  typeMap[type]--;
129}
130
131function 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
139function TypeMapHasType(typeMap, type) {
140  return !!typeMap[type];
141}
142
143function 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
155var 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".
172function 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
181function ObserverGetCallback(observer) {
182  return IS_SPEC_FUNCTION(observer) ? observer : observer.callback;
183}
184
185function ObserverGetAcceptTypes(observer) {
186  return IS_SPEC_FUNCTION(observer) ? defaultAcceptTypes : observer.accept;
187}
188
189function ObserverIsActive(observer, objectInfo) {
190  return TypeMapIsDisjointFrom(ObjectInfoGetPerformingTypes(objectInfo),
191                               ObserverGetAcceptTypes(observer));
192}
193
194function 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
212function ObjectInfoGet(object) {
213  return GetObjectInfoMap().get(object);
214}
215
216function ObjectInfoGetFromNotifier(notifier) {
217  return GetNotifierObjectInfoMap().get(notifier);
218}
219
220function 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
229function ObjectInfoGetObject(objectInfo) {
230  return objectInfo.object;
231}
232
233function 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.
242function 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
253function 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
267function 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
282function 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
298function ObjectInfoAddPerformingType(objectInfo, type) {
299  objectInfo.performing = objectInfo.performing || TypeMapCreate();
300  TypeMapAddType(objectInfo.performing, type);
301  objectInfo.performingCount++;
302}
303
304function ObjectInfoRemovePerformingType(objectInfo, type) {
305  objectInfo.performingCount--;
306  TypeMapRemoveType(objectInfo.performing, type);
307}
308
309function ObjectInfoGetPerformingTypes(objectInfo) {
310  return objectInfo.performingCount > 0 ? objectInfo.performing : null;
311}
312
313function 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.
330function CallbackInfoGet(callback) {
331  return GetCallbackInfoMap().get(callback);
332}
333
334function 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
344function CallbackInfoGetPriority(callbackInfo) {
345  if (IS_NUMBER(callbackInfo))
346    return callbackInfo;
347  else
348    return callbackInfo.priority;
349}
350
351function 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
362function 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
376function NativeObjectObserve(object, callback, acceptList) {
377  var objectInfo = ObjectInfoGetOrCreate(object);
378  var typeList = ConvertAcceptListToTypeMap(acceptList);
379  ObjectInfoAddObserver(objectInfo, callback, typeList);
380  return object;
381}
382
383function 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
399function ArrayObserve(object, callback) {
400  return ObjectObserve(object, callback, ['add',
401                                          'update',
402                                          'delete',
403                                          'splice']);
404}
405
406function ArrayUnobserve(object, callback) {
407  return ObjectUnobserve(object, callback);
408}
409
410function 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
442function 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
461function 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
479function BeginPerformSplice(array) {
480  var objectInfo = ObjectInfoGet(array);
481  if (!IS_UNDEFINED(objectInfo))
482    ObjectInfoAddPerformingType(objectInfo, 'splice');
483}
484
485function EndPerformSplice(array) {
486  var objectInfo = ObjectInfoGet(array);
487  if (!IS_UNDEFINED(objectInfo))
488    ObjectInfoRemovePerformingType(objectInfo, 'splice');
489}
490
491function 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
509function 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
532var notifierPrototype = {};
533
534function 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
547function 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
563function 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
577function 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
591function NativeObjectGetNotifier(object) {
592  var objectInfo = ObjectInfoGetOrCreate(object);
593  return ObjectInfoGetNotifier(objectInfo);
594}
595
596function 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
618function ObjectDeliverChangeRecords(callback) {
619  if (!IS_SPEC_FUNCTION(callback))
620    throw MakeTypeError("observe_non_function", ["deliverChangeRecords"]);
621
622  while (CallbackDeliverPending(callback)) {}
623}
624
625function 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
635function 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
653SetupObjectObserve();
654