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.
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
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).
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
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.
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.
38 function GetObservationStateJS() {
39 if (IS_UNDEFINED(observationState))
40 observationState = %GetObservationState();
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;
51 return observationState;
54 function GetWeakMapWrapper() {
55 function MapWrapper(map) {
59 MapWrapper.prototype = {
62 return %WeakCollectionGet(this.map_, key);
64 set: function(key, value) {
65 %WeakCollectionSet(this.map_, key, value);
68 return !IS_UNDEFINED(this.get(key));
77 function GetContextMaps() {
78 if (IS_UNDEFINED(contextMaps)) {
79 var map = GetWeakMapWrapper();
80 var observationState = GetObservationStateJS();
82 callbackInfoMap: new map(observationState.callbackInfoMap),
83 objectInfoMap: new map(observationState.objectInfoMap),
84 notifierObjectInfoMap: new map(observationState.notifierObjectInfoMap)
91 function GetCallbackInfoMap() {
92 return GetContextMaps().callbackInfoMap;
95 function GetObjectInfoMap() {
96 return GetContextMaps().objectInfoMap;
99 function GetNotifierObjectInfoMap() {
100 return GetContextMaps().notifierObjectInfoMap;
103 function GetPendingObservers() {
104 return GetObservationStateJS().pendingObservers;
107 function SetPendingObservers(pendingObservers) {
108 GetObservationStateJS().pendingObservers = pendingObservers;
111 function GetNextCallbackPriority() {
112 return GetObservationStateJS().nextCallbackPriority++;
115 function nullProtoObject() {
116 return { __proto__: null };
119 function TypeMapCreate() {
120 return nullProtoObject();
123 function TypeMapAddType(typeMap, type, ignoreDuplicate) {
124 typeMap[type] = ignoreDuplicate ? 1 : (typeMap[type] || 0) + 1;
127 function TypeMapRemoveType(typeMap, type) {
131 function TypeMapCreateFromList(typeList, length) {
132 var typeMap = TypeMapCreate();
133 for (var i = 0; i < length; i++) {
134 TypeMapAddType(typeMap, typeList[i], true);
139 function TypeMapHasType(typeMap, type) {
140 return !!typeMap[type];
143 function TypeMapIsDisjointFrom(typeMap1, typeMap2) {
144 if (!typeMap1 || !typeMap2)
147 for (var type in typeMap1) {
148 if (TypeMapHasType(typeMap1, type) && TypeMapHasType(typeMap2, type))
155 var defaultAcceptTypes = (function() {
164 return TypeMapCreateFromList(defaultTypes, defaultTypes.length);
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))
175 var observer = nullProtoObject();
176 observer.callback = callback;
177 observer.accept = acceptList;
181 function ObserverGetCallback(observer) {
182 return IS_SPEC_FUNCTION(observer) ? observer : observer.callback;
185 function ObserverGetAcceptTypes(observer) {
186 return IS_SPEC_FUNCTION(observer) ? defaultAcceptTypes : observer.accept;
189 function ObserverIsActive(observer, objectInfo) {
190 return TypeMapIsDisjointFrom(ObjectInfoGetPerformingTypes(objectInfo),
191 ObserverGetAcceptTypes(observer));
194 function ObjectInfoGetOrCreate(object) {
195 var objectInfo = ObjectInfoGet(object);
196 if (IS_UNDEFINED(objectInfo)) {
197 if (!%IsJSProxy(object))
198 %SetIsObserved(object);
202 changeObservers: null,
207 GetObjectInfoMap().set(object, objectInfo);
212 function ObjectInfoGet(object) {
213 return GetObjectInfoMap().get(object);
216 function ObjectInfoGetFromNotifier(notifier) {
217 return GetNotifierObjectInfoMap().get(notifier);
220 function ObjectInfoGetNotifier(objectInfo) {
221 if (IS_NULL(objectInfo.notifier)) {
222 objectInfo.notifier = { __proto__: notifierPrototype };
223 GetNotifierObjectInfoMap().set(objectInfo.notifier, objectInfo);
226 return objectInfo.notifier;
229 function ObjectInfoGetObject(objectInfo) {
230 return objectInfo.object;
233 function ChangeObserversIsOptimized(changeObservers) {
234 return typeof changeObservers === 'function' ||
235 typeof changeObservers.callback === 'function';
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;
253 function ObjectInfoAddObserver(objectInfo, callback, acceptList) {
254 var callbackInfo = CallbackInfoGetOrCreate(callback);
255 var observer = ObserverCreate(callback, acceptList);
257 if (!objectInfo.changeObservers) {
258 objectInfo.changeObservers = observer;
262 ObjectInfoNormalizeChangeObservers(objectInfo);
263 var priority = CallbackInfoGetPriority(callbackInfo);
264 objectInfo.changeObservers[priority] = observer;
267 function ObjectInfoRemoveObserver(objectInfo, callback) {
268 if (!objectInfo.changeObservers)
271 if (ChangeObserversIsOptimized(objectInfo.changeObservers)) {
272 if (callback === ObserverGetCallback(objectInfo.changeObservers))
273 objectInfo.changeObservers = null;
277 var callbackInfo = CallbackInfoGet(callback);
278 var priority = CallbackInfoGetPriority(callbackInfo);
279 objectInfo.changeObservers[priority] = null;
282 function ObjectInfoHasActiveObservers(objectInfo) {
283 if (IS_UNDEFINED(objectInfo) || !objectInfo.changeObservers)
286 if (ChangeObserversIsOptimized(objectInfo.changeObservers))
287 return ObserverIsActive(objectInfo.changeObservers, objectInfo);
289 for (var priority in objectInfo.changeObservers) {
290 var observer = objectInfo.changeObservers[priority];
291 if (!IS_NULL(observer) && ObserverIsActive(observer, objectInfo))
298 function ObjectInfoAddPerformingType(objectInfo, type) {
299 objectInfo.performing = objectInfo.performing || TypeMapCreate();
300 TypeMapAddType(objectInfo.performing, type);
301 objectInfo.performingCount++;
304 function ObjectInfoRemovePerformingType(objectInfo, type) {
305 objectInfo.performingCount--;
306 TypeMapRemoveType(objectInfo.performing, type);
309 function ObjectInfoGetPerformingTypes(objectInfo) {
310 return objectInfo.performingCount > 0 ? objectInfo.performing : null;
313 function ConvertAcceptListToTypeMap(arg) {
314 // We use undefined as a sentinel for the default accept list.
315 if (IS_UNDEFINED(arg))
318 if (!IS_SPEC_OBJECT(arg))
319 throw MakeTypeError("observe_accept_invalid");
321 var len = ToInteger(arg.length);
322 if (len < 0) len = 0;
324 return TypeMapCreateFromList(arg, len);
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);
334 function CallbackInfoGetOrCreate(callback) {
335 var callbackInfo = GetCallbackInfoMap().get(callback);
336 if (!IS_UNDEFINED(callbackInfo))
339 var priority = GetNextCallbackPriority();
340 GetCallbackInfoMap().set(callback, priority);
344 function CallbackInfoGetPriority(callbackInfo) {
345 if (IS_NUMBER(callbackInfo))
348 return callbackInfo.priority;
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);
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");
372 var objectObserveFn = %GetObjectContextObjectObserve(object);
373 return objectObserveFn(object, callback, acceptList);
376 function NativeObjectObserve(object, callback, acceptList) {
377 var objectInfo = ObjectInfoGetOrCreate(object);
378 var typeList = ConvertAcceptListToTypeMap(acceptList);
379 ObjectInfoAddObserver(objectInfo, callback, typeList);
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"]);
391 var objectInfo = ObjectInfoGet(object);
392 if (IS_UNDEFINED(objectInfo))
395 ObjectInfoRemoveObserver(objectInfo, callback);
399 function ArrayObserve(object, callback) {
400 return ObjectObserve(object, callback, ['add',
406 function ArrayUnobserve(object, callback) {
407 return ObjectUnobserve(object, callback);
410 function ObserverEnqueueIfActive(observer, objectInfo, changeRecord) {
411 if (!ObserverIsActive(observer, objectInfo) ||
412 !TypeMapHasType(ObserverGetAcceptTypes(observer), changeRecord.type)) {
416 var callback = ObserverGetCallback(observer);
417 if (!%ObserverObjectAndRecordHaveSameOrigin(callback, changeRecord.object,
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 });
433 %DebugAsyncTaskEvent({ type: "enqueue", id: id, name: name });
435 %EnqueueMicrotask(ObserveMicrotaskRunner);
438 GetPendingObservers()[callbackInfo.priority] = callback;
439 callbackInfo.push(changeRecord);
442 function ObjectInfoEnqueueExternalChangeRecord(objectInfo, changeRecord, type) {
443 if (!ObjectInfoHasActiveObservers(objectInfo))
446 var hasType = !IS_UNDEFINED(type);
447 var newRecord = hasType ?
448 { object: ObjectInfoGetObject(objectInfo), type: type } :
449 { object: ObjectInfoGetObject(objectInfo) };
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);
456 ObjectFreezeJS(newRecord);
458 ObjectInfoEnqueueInternalChangeRecord(objectInfo, newRecord);
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;
465 if (ChangeObserversIsOptimized(objectInfo.changeObservers)) {
466 var observer = objectInfo.changeObservers;
467 ObserverEnqueueIfActive(observer, objectInfo, changeRecord);
471 for (var priority in objectInfo.changeObservers) {
472 var observer = objectInfo.changeObservers[priority];
473 if (IS_NULL(observer))
475 ObserverEnqueueIfActive(observer, objectInfo, changeRecord);
479 function BeginPerformSplice(array) {
480 var objectInfo = ObjectInfoGet(array);
481 if (!IS_UNDEFINED(objectInfo))
482 ObjectInfoAddPerformingType(objectInfo, 'splice');
485 function EndPerformSplice(array) {
486 var objectInfo = ObjectInfoGet(array);
487 if (!IS_UNDEFINED(objectInfo))
488 ObjectInfoRemovePerformingType(objectInfo, 'splice');
491 function EnqueueSpliceRecord(array, index, removed, addedCount) {
492 var objectInfo = ObjectInfoGet(array);
493 if (!ObjectInfoHasActiveObservers(objectInfo))
501 addedCount: addedCount
504 ObjectFreezeJS(changeRecord);
505 ObjectFreezeJS(changeRecord.removed);
506 ObjectInfoEnqueueInternalChangeRecord(objectInfo, changeRecord);
509 function NotifyChange(type, object, name, oldValue) {
510 var objectInfo = ObjectInfoGet(object);
511 if (!ObjectInfoHasActiveObservers(objectInfo))
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 };
528 ObjectFreezeJS(changeRecord);
529 ObjectInfoEnqueueInternalChangeRecord(objectInfo, changeRecord);
532 var notifierPrototype = {};
534 function ObjectNotifierNotify(changeRecord) {
535 if (!IS_SPEC_OBJECT(this))
536 throw MakeTypeError("called_on_non_object", ["notify"]);
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");
544 ObjectInfoEnqueueExternalChangeRecord(objectInfo, changeRecord);
547 function ObjectNotifierPerformChange(changeType, changeFn) {
548 if (!IS_SPEC_OBJECT(this))
549 throw MakeTypeError("called_on_non_object", ["performChange"]);
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");
559 var performChangeFn = %GetObjectContextNotifierPerformChange(objectInfo);
560 performChangeFn(objectInfo, changeType, changeFn);
563 function NativeObjectNotifierPerformChange(objectInfo, changeType, changeFn) {
564 ObjectInfoAddPerformingType(objectInfo, changeType);
568 changeRecord = %_CallFunction(UNDEFINED, changeFn);
570 ObjectInfoRemovePerformingType(objectInfo, changeType);
573 if (IS_SPEC_OBJECT(changeRecord))
574 ObjectInfoEnqueueExternalChangeRecord(objectInfo, changeRecord, changeType);
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"]);
583 if (ObjectIsFrozen(object)) return null;
585 if (!%ObjectWasCreatedInCurrentOrigin(object)) return null;
587 var getNotifierFn = %GetObjectContextObjectGetNotifier(object);
588 return getNotifierFn(object);
591 function NativeObjectGetNotifier(object) {
592 var objectInfo = ObjectInfoGetOrCreate(object);
593 return ObjectInfoGetNotifier(objectInfo);
596 function CallbackDeliverPending(callback) {
597 var callbackInfo = GetCallbackInfoMap().get(callback);
598 if (IS_UNDEFINED(callbackInfo) || IS_NUMBER(callbackInfo))
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);
606 if (GetPendingObservers())
607 delete GetPendingObservers()[priority];
610 %MoveArrayContents(callbackInfo, delivered);
613 %_CallFunction(UNDEFINED, delivered, callback);
614 } catch (ex) {} // TODO(rossberg): perhaps log uncaught exceptions.
618 function ObjectDeliverChangeRecords(callback) {
619 if (!IS_SPEC_FUNCTION(callback))
620 throw MakeTypeError("observe_non_function", ["deliverChangeRecords"]);
622 while (CallbackDeliverPending(callback)) {}
625 function ObserveMicrotaskRunner() {
626 var pendingObservers = GetPendingObservers();
627 if (pendingObservers) {
628 SetPendingObservers(null);
629 for (var i in pendingObservers) {
630 CallbackDeliverPending(pendingObservers[i]);
635 function SetupObjectObserve() {
636 %CheckIsBootstrapping();
637 InstallFunctions($Object, DONT_ENUM, $Array(
638 "deliverChangeRecords", ObjectDeliverChangeRecords,
639 "getNotifier", ObjectGetNotifier,
640 "observe", ObjectObserve,
641 "unobserve", ObjectUnobserve
643 InstallFunctions($Array, DONT_ENUM, $Array(
644 "observe", ArrayObserve,
645 "unobserve", ArrayUnobserve
647 InstallFunctions(notifierPrototype, DONT_ENUM, $Array(
648 "notify", ObjectNotifierNotify,
649 "performChange", ObjectNotifierPerformChange
653 SetupObjectObserve();