1 // Copyright 2012 the V8 project authors. All rights reserved.
2 // Redistribution and use in source and binary forms, with or without
3 // modification, are permitted provided that the following conditions are
6 // * Redistributions of source code must retain the above copyright
7 // notice, this list of conditions and the following disclaimer.
8 // * Redistributions in binary form must reproduce the above
9 // copyright notice, this list of conditions and the following
10 // disclaimer in the documentation and/or other materials provided
11 // with the distribution.
12 // * Neither the name of Google Inc. nor the names of its
13 // contributors may be used to endorse or promote products derived
14 // from this software without specific prior written permission.
16 // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17 // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18 // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19 // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20 // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21 // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22 // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23 // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24 // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25 // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26 // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
32 // This file contains all of the routing and accounting for Object.observe.
33 // User code will interact with these mechanisms via the Object.observe APIs
34 // and, as a side effect of mutation objects which are observed. The V8 runtime
35 // (both C++ and JS) will interact with these mechanisms primarily by enqueuing
36 // proper change records for objects which were mutated. The Object.observe
37 // routing and accounting consists primarily of three participants
39 // 1) ObjectInfo. This represents the observed state of a given object. It
40 // records what callbacks are observing the object, with what options, and
41 // what "change types" are in progress on the object (i.e. via
42 // notifier.performChange).
44 // 2) CallbackInfo. This represents a callback used for observation. It holds
45 // the records which must be delivered to the callback, as well as the global
46 // priority of the callback (which determines delivery order between
49 // 3) observationState.pendingObservers. This is the set of observers which
50 // have change records which must be delivered. During "normal" delivery
51 // (i.e. not Object.deliverChangeRecords), this is the mechanism by which
52 // callbacks are invoked in the proper order until there are no more
53 // change records pending to a callback.
55 // Note that in order to reduce allocation and processing costs, the
56 // implementation of (1) and (2) have "optimized" states which represent
57 // common cases which can be handled more efficiently.
61 function GetObservationState() {
62 if (IS_UNDEFINED(observationState))
63 observationState = %GetObservationState();
65 if (IS_UNDEFINED(observationState.callbackInfoMap)) {
66 observationState.callbackInfoMap = %ObservationWeakMapCreate();
67 observationState.objectInfoMap = %ObservationWeakMapCreate();
68 observationState.notifierObjectInfoMap = %ObservationWeakMapCreate();
69 observationState.pendingObservers = null;
70 observationState.nextCallbackPriority = 0;
73 return observationState;
76 function GetWeakMapWrapper() {
77 function MapWrapper(map) {
81 MapWrapper.prototype = {
83 key = %UnwrapGlobalProxy(key);
84 if (!IS_SPEC_OBJECT(key)) return UNDEFINED;
85 return %WeakCollectionGet(this.map_, key);
87 set: function(key, value) {
88 key = %UnwrapGlobalProxy(key);
89 if (!IS_SPEC_OBJECT(key)) return UNDEFINED;
90 %WeakCollectionSet(this.map_, key, value);
93 return !IS_UNDEFINED(this.get(key));
102 function GetContextMaps() {
103 if (IS_UNDEFINED(contextMaps)) {
104 var map = GetWeakMapWrapper();
105 var observationState = GetObservationState();
107 callbackInfoMap: new map(observationState.callbackInfoMap),
108 objectInfoMap: new map(observationState.objectInfoMap),
109 notifierObjectInfoMap: new map(observationState.notifierObjectInfoMap)
116 function GetCallbackInfoMap() {
117 return GetContextMaps().callbackInfoMap;
120 function GetObjectInfoMap() {
121 return GetContextMaps().objectInfoMap;
124 function GetNotifierObjectInfoMap() {
125 return GetContextMaps().notifierObjectInfoMap;
128 function GetPendingObservers() {
129 return GetObservationState().pendingObservers;
132 function SetPendingObservers(pendingObservers) {
133 GetObservationState().pendingObservers = pendingObservers;
136 function GetNextCallbackPriority() {
137 return GetObservationState().nextCallbackPriority++;
140 function nullProtoObject() {
141 return { __proto__: null };
144 function TypeMapCreate() {
145 return nullProtoObject();
148 function TypeMapAddType(typeMap, type, ignoreDuplicate) {
149 typeMap[type] = ignoreDuplicate ? 1 : (typeMap[type] || 0) + 1;
152 function TypeMapRemoveType(typeMap, type) {
156 function TypeMapCreateFromList(typeList) {
157 var typeMap = TypeMapCreate();
158 for (var i = 0; i < typeList.length; i++) {
159 TypeMapAddType(typeMap, typeList[i], true);
164 function TypeMapHasType(typeMap, type) {
165 return !!typeMap[type];
168 function TypeMapIsDisjointFrom(typeMap1, typeMap2) {
169 if (!typeMap1 || !typeMap2)
172 for (var type in typeMap1) {
173 if (TypeMapHasType(typeMap1, type) && TypeMapHasType(typeMap2, type))
180 var defaultAcceptTypes = TypeMapCreateFromList([
189 // An Observer is a registration to observe an object by a callback with
190 // a given set of accept types. If the set of accept types is the default
191 // set for Object.observe, the observer is represented as a direct reference
192 // to the callback. An observer never changes its accept types and thus never
193 // needs to "normalize".
194 function ObserverCreate(callback, acceptList) {
195 if (IS_UNDEFINED(acceptList))
197 var observer = nullProtoObject();
198 observer.callback = callback;
199 observer.accept = TypeMapCreateFromList(acceptList);
203 function ObserverGetCallback(observer) {
204 return IS_SPEC_FUNCTION(observer) ? observer : observer.callback;
207 function ObserverGetAcceptTypes(observer) {
208 return IS_SPEC_FUNCTION(observer) ? defaultAcceptTypes : observer.accept;
211 function ObserverIsActive(observer, objectInfo) {
212 return TypeMapIsDisjointFrom(ObjectInfoGetPerformingTypes(objectInfo),
213 ObserverGetAcceptTypes(observer));
216 function ObjectInfoGetOrCreate(object) {
217 var objectInfo = ObjectInfoGet(object);
218 if (IS_UNDEFINED(objectInfo)) {
219 if (!%IsJSProxy(object))
220 %SetIsObserved(object);
224 changeObservers: null,
229 GetObjectInfoMap().set(object, objectInfo);
234 function ObjectInfoGet(object) {
235 return GetObjectInfoMap().get(object);
238 function ObjectInfoGetFromNotifier(notifier) {
239 return GetNotifierObjectInfoMap().get(notifier);
242 function ObjectInfoGetNotifier(objectInfo) {
243 if (IS_NULL(objectInfo.notifier)) {
244 objectInfo.notifier = { __proto__: notifierPrototype };
245 GetNotifierObjectInfoMap().set(objectInfo.notifier, objectInfo);
248 return objectInfo.notifier;
251 function ObjectInfoGetObject(objectInfo) {
252 return objectInfo.object;
255 function ChangeObserversIsOptimized(changeObservers) {
256 return typeof changeObservers === 'function' ||
257 typeof changeObservers.callback === 'function';
260 // The set of observers on an object is called 'changeObservers'. The first
261 // observer is referenced directly via objectInfo.changeObservers. When a second
262 // is added, changeObservers "normalizes" to become a mapping of callback
263 // priority -> observer and is then stored on objectInfo.changeObservers.
264 function ObjectInfoNormalizeChangeObservers(objectInfo) {
265 if (ChangeObserversIsOptimized(objectInfo.changeObservers)) {
266 var observer = objectInfo.changeObservers;
267 var callback = ObserverGetCallback(observer);
268 var callbackInfo = CallbackInfoGet(callback);
269 var priority = CallbackInfoGetPriority(callbackInfo);
270 objectInfo.changeObservers = nullProtoObject();
271 objectInfo.changeObservers[priority] = observer;
275 function ObjectInfoAddObserver(objectInfo, callback, acceptList) {
276 var callbackInfo = CallbackInfoGetOrCreate(callback);
277 var observer = ObserverCreate(callback, acceptList);
279 if (!objectInfo.changeObservers) {
280 objectInfo.changeObservers = observer;
284 ObjectInfoNormalizeChangeObservers(objectInfo);
285 var priority = CallbackInfoGetPriority(callbackInfo);
286 objectInfo.changeObservers[priority] = observer;
289 function ObjectInfoRemoveObserver(objectInfo, callback) {
290 if (!objectInfo.changeObservers)
293 if (ChangeObserversIsOptimized(objectInfo.changeObservers)) {
294 if (callback === ObserverGetCallback(objectInfo.changeObservers))
295 objectInfo.changeObservers = null;
299 var callbackInfo = CallbackInfoGet(callback);
300 var priority = CallbackInfoGetPriority(callbackInfo);
301 objectInfo.changeObservers[priority] = null;
304 function ObjectInfoHasActiveObservers(objectInfo) {
305 if (IS_UNDEFINED(objectInfo) || !objectInfo.changeObservers)
308 if (ChangeObserversIsOptimized(objectInfo.changeObservers))
309 return ObserverIsActive(objectInfo.changeObservers, objectInfo);
311 for (var priority in objectInfo.changeObservers) {
312 var observer = objectInfo.changeObservers[priority];
313 if (!IS_NULL(observer) && ObserverIsActive(observer, objectInfo))
320 function ObjectInfoAddPerformingType(objectInfo, type) {
321 objectInfo.performing = objectInfo.performing || TypeMapCreate();
322 TypeMapAddType(objectInfo.performing, type);
323 objectInfo.performingCount++;
326 function ObjectInfoRemovePerformingType(objectInfo, type) {
327 objectInfo.performingCount--;
328 TypeMapRemoveType(objectInfo.performing, type);
331 function ObjectInfoGetPerformingTypes(objectInfo) {
332 return objectInfo.performingCount > 0 ? objectInfo.performing : null;
335 function AcceptArgIsValid(arg) {
336 if (IS_UNDEFINED(arg))
339 if (!IS_SPEC_OBJECT(arg) ||
340 !IS_NUMBER(arg.length) ||
347 // CallbackInfo's optimized state is just a number which represents its global
348 // priority. When a change record must be enqueued for the callback, it
349 // normalizes. When delivery clears any pending change records, it re-optimizes.
350 function CallbackInfoGet(callback) {
351 return GetCallbackInfoMap().get(callback);
354 function CallbackInfoGetOrCreate(callback) {
355 var callbackInfo = GetCallbackInfoMap().get(callback);
356 if (!IS_UNDEFINED(callbackInfo))
359 var priority = GetNextCallbackPriority();
360 GetCallbackInfoMap().set(callback, priority);
364 function CallbackInfoGetPriority(callbackInfo) {
365 if (IS_NUMBER(callbackInfo))
368 return callbackInfo.priority;
371 function CallbackInfoNormalize(callback) {
372 var callbackInfo = GetCallbackInfoMap().get(callback);
373 if (IS_NUMBER(callbackInfo)) {
374 var priority = callbackInfo;
375 callbackInfo = new InternalArray;
376 callbackInfo.priority = priority;
377 GetCallbackInfoMap().set(callback, callbackInfo);
382 function ObjectObserve(object, callback, acceptList) {
383 if (!IS_SPEC_OBJECT(object))
384 throw MakeTypeError("observe_non_object", ["observe"]);
385 if (!IS_SPEC_FUNCTION(callback))
386 throw MakeTypeError("observe_non_function", ["observe"]);
387 if (ObjectIsFrozen(callback))
388 throw MakeTypeError("observe_callback_frozen");
389 if (!AcceptArgIsValid(acceptList))
390 throw MakeTypeError("observe_accept_invalid");
392 var objectInfo = ObjectInfoGetOrCreate(object);
393 ObjectInfoAddObserver(objectInfo, callback, acceptList);
397 function ObjectUnobserve(object, callback) {
398 if (!IS_SPEC_OBJECT(object))
399 throw MakeTypeError("observe_non_object", ["unobserve"]);
400 if (!IS_SPEC_FUNCTION(callback))
401 throw MakeTypeError("observe_non_function", ["unobserve"]);
403 var objectInfo = ObjectInfoGet(object);
404 if (IS_UNDEFINED(objectInfo))
407 ObjectInfoRemoveObserver(objectInfo, callback);
411 function ArrayObserve(object, callback) {
412 return ObjectObserve(object, callback, ['add',
418 function ArrayUnobserve(object, callback) {
419 return ObjectUnobserve(object, callback);
422 function ObserverEnqueueIfActive(observer, objectInfo, changeRecord,
424 if (!ObserverIsActive(observer, objectInfo) ||
425 !TypeMapHasType(ObserverGetAcceptTypes(observer), changeRecord.type)) {
429 var callback = ObserverGetCallback(observer);
430 if (needsAccessCheck &&
431 // Drop all splice records on the floor for access-checked objects
432 (changeRecord.type == 'splice' ||
433 !%IsAccessAllowedForObserver(
434 callback, changeRecord.object, changeRecord.name))) {
438 var callbackInfo = CallbackInfoNormalize(callback);
439 if (IS_NULL(GetPendingObservers())) {
440 SetPendingObservers(nullProtoObject())
441 GetMicrotaskQueue().push(ObserveMicrotaskRunner);
442 %SetMicrotaskPending(true);
444 GetPendingObservers()[callbackInfo.priority] = callback;
445 callbackInfo.push(changeRecord);
448 function ObjectInfoEnqueueExternalChangeRecord(objectInfo, changeRecord, type) {
449 if (!ObjectInfoHasActiveObservers(objectInfo))
452 var hasType = !IS_UNDEFINED(type);
453 var newRecord = hasType ?
454 { object: ObjectInfoGetObject(objectInfo), type: type } :
455 { object: ObjectInfoGetObject(objectInfo) };
457 for (var prop in changeRecord) {
458 if (prop === 'object' || (hasType && prop === 'type')) continue;
459 %DefineOrRedefineDataProperty(newRecord, prop, changeRecord[prop],
460 READ_ONLY + DONT_DELETE);
462 ObjectFreeze(newRecord);
464 ObjectInfoEnqueueInternalChangeRecord(objectInfo, newRecord,
465 true /* skip access check */);
468 function ObjectInfoEnqueueInternalChangeRecord(objectInfo, changeRecord,
470 // TODO(rossberg): adjust once there is a story for symbols vs proxies.
471 if (IS_SYMBOL(changeRecord.name)) return;
473 var needsAccessCheck = !skipAccessCheck &&
474 %IsAccessCheckNeeded(changeRecord.object);
476 if (ChangeObserversIsOptimized(objectInfo.changeObservers)) {
477 var observer = objectInfo.changeObservers;
478 ObserverEnqueueIfActive(observer, objectInfo, changeRecord,
483 for (var priority in objectInfo.changeObservers) {
484 var observer = objectInfo.changeObservers[priority];
485 if (IS_NULL(observer))
487 ObserverEnqueueIfActive(observer, objectInfo, changeRecord,
492 function BeginPerformSplice(array) {
493 var objectInfo = ObjectInfoGet(array);
494 if (!IS_UNDEFINED(objectInfo))
495 ObjectInfoAddPerformingType(objectInfo, 'splice');
498 function EndPerformSplice(array) {
499 var objectInfo = ObjectInfoGet(array);
500 if (!IS_UNDEFINED(objectInfo))
501 ObjectInfoRemovePerformingType(objectInfo, 'splice');
504 function EnqueueSpliceRecord(array, index, removed, addedCount) {
505 var objectInfo = ObjectInfoGet(array);
506 if (!ObjectInfoHasActiveObservers(objectInfo))
514 addedCount: addedCount
517 ObjectFreeze(changeRecord);
518 ObjectFreeze(changeRecord.removed);
519 ObjectInfoEnqueueInternalChangeRecord(objectInfo, changeRecord);
522 function NotifyChange(type, object, name, oldValue) {
523 var objectInfo = ObjectInfoGet(object);
524 if (!ObjectInfoHasActiveObservers(objectInfo))
528 if (arguments.length == 2) {
529 changeRecord = { type: type, object: object };
530 } else if (arguments.length == 3) {
531 changeRecord = { type: type, object: object, name: name };
541 ObjectFreeze(changeRecord);
542 ObjectInfoEnqueueInternalChangeRecord(objectInfo, changeRecord);
545 var notifierPrototype = {};
547 function ObjectNotifierNotify(changeRecord) {
548 if (!IS_SPEC_OBJECT(this))
549 throw MakeTypeError("called_on_non_object", ["notify"]);
551 var objectInfo = ObjectInfoGetFromNotifier(this);
552 if (IS_UNDEFINED(objectInfo))
553 throw MakeTypeError("observe_notify_non_notifier");
554 if (!IS_STRING(changeRecord.type))
555 throw MakeTypeError("observe_type_non_string");
557 ObjectInfoEnqueueExternalChangeRecord(objectInfo, changeRecord);
560 function ObjectNotifierPerformChange(changeType, changeFn) {
561 if (!IS_SPEC_OBJECT(this))
562 throw MakeTypeError("called_on_non_object", ["performChange"]);
564 var objectInfo = ObjectInfoGetFromNotifier(this);
566 if (IS_UNDEFINED(objectInfo))
567 throw MakeTypeError("observe_notify_non_notifier");
568 if (!IS_STRING(changeType))
569 throw MakeTypeError("observe_perform_non_string");
570 if (!IS_SPEC_FUNCTION(changeFn))
571 throw MakeTypeError("observe_perform_non_function");
573 ObjectInfoAddPerformingType(objectInfo, changeType);
577 changeRecord = %_CallFunction(UNDEFINED, changeFn);
579 ObjectInfoRemovePerformingType(objectInfo, changeType);
582 if (IS_SPEC_OBJECT(changeRecord))
583 ObjectInfoEnqueueExternalChangeRecord(objectInfo, changeRecord, changeType);
586 function ObjectGetNotifier(object) {
587 if (!IS_SPEC_OBJECT(object))
588 throw MakeTypeError("observe_non_object", ["getNotifier"]);
590 if (ObjectIsFrozen(object)) return null;
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 // Disable Object.observe API for M35.
654 // SetupObjectObserve();