| // Copyright 2012 the V8 project authors. All rights reserved. |
| // Redistribution and use in source and binary forms, with or without |
| // modification, are permitted provided that the following conditions are |
| // met: |
| // |
| // * Redistributions of source code must retain the above copyright |
| // notice, this list of conditions and the following disclaimer. |
| // * Redistributions in binary form must reproduce the above |
| // copyright notice, this list of conditions and the following |
| // disclaimer in the documentation and/or other materials provided |
| // with the distribution. |
| // * Neither the name of Google Inc. nor the names of its |
| // contributors may be used to endorse or promote products derived |
| // from this software without specific prior written permission. |
| // |
| // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
| // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
| // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
| // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
| // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
| // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
| // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
| // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
| // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| |
| "use strict"; |
| |
| var observationState = %GetObservationState(); |
| if (IS_UNDEFINED(observationState.callbackInfoMap)) { |
| observationState.callbackInfoMap = %ObservationWeakMapCreate(); |
| observationState.objectInfoMap = %ObservationWeakMapCreate(); |
| observationState.notifierTargetMap = %ObservationWeakMapCreate(); |
| observationState.pendingObservers = new InternalArray; |
| observationState.nextCallbackPriority = 0; |
| } |
| |
| function ObservationWeakMap(map) { |
| this.map_ = map; |
| } |
| |
| ObservationWeakMap.prototype = { |
| get: function(key) { |
| key = %UnwrapGlobalProxy(key); |
| if (!IS_SPEC_OBJECT(key)) return void 0; |
| return %WeakCollectionGet(this.map_, key); |
| }, |
| set: function(key, value) { |
| key = %UnwrapGlobalProxy(key); |
| if (!IS_SPEC_OBJECT(key)) return void 0; |
| %WeakCollectionSet(this.map_, key, value); |
| }, |
| has: function(key) { |
| return !IS_UNDEFINED(this.get(key)); |
| } |
| }; |
| |
| var callbackInfoMap = |
| new ObservationWeakMap(observationState.callbackInfoMap); |
| var objectInfoMap = new ObservationWeakMap(observationState.objectInfoMap); |
| var notifierTargetMap = |
| new ObservationWeakMap(observationState.notifierTargetMap); |
| |
| function CreateObjectInfo(object) { |
| var info = { |
| changeObservers: new InternalArray, |
| notifier: null, |
| inactiveObservers: new InternalArray, |
| performing: { __proto__: null }, |
| performingCount: 0, |
| }; |
| objectInfoMap.set(object, info); |
| return info; |
| } |
| |
| var defaultAcceptTypes = { |
| __proto__: null, |
| 'new': true, |
| 'updated': true, |
| 'deleted': true, |
| 'prototype': true, |
| 'reconfigured': true |
| }; |
| |
| function CreateObserver(callback, accept) { |
| var observer = { |
| __proto__: null, |
| callback: callback, |
| accept: defaultAcceptTypes |
| }; |
| |
| if (IS_UNDEFINED(accept)) |
| return observer; |
| |
| var acceptMap = { __proto__: null }; |
| for (var i = 0; i < accept.length; i++) |
| acceptMap[accept[i]] = true; |
| |
| observer.accept = acceptMap; |
| return observer; |
| } |
| |
| function ObserverIsActive(observer, objectInfo) { |
| if (objectInfo.performingCount === 0) |
| return true; |
| |
| var performing = objectInfo.performing; |
| for (var type in performing) { |
| if (performing[type] > 0 && observer.accept[type]) |
| return false; |
| } |
| |
| return true; |
| } |
| |
| function ObserverIsInactive(observer, objectInfo) { |
| return !ObserverIsActive(observer, objectInfo); |
| } |
| |
| function RemoveNullElements(from) { |
| var i = 0; |
| var j = 0; |
| for (; i < from.length; i++) { |
| if (from[i] === null) |
| continue; |
| if (j < i) |
| from[j] = from[i]; |
| j++; |
| } |
| |
| if (i !== j) |
| from.length = from.length - (i - j); |
| } |
| |
| function RepartitionObservers(conditionFn, from, to, objectInfo) { |
| var anyRemoved = false; |
| for (var i = 0; i < from.length; i++) { |
| var observer = from[i]; |
| if (conditionFn(observer, objectInfo)) { |
| anyRemoved = true; |
| from[i] = null; |
| to.push(observer); |
| } |
| } |
| |
| if (anyRemoved) |
| RemoveNullElements(from); |
| } |
| |
| function BeginPerformChange(objectInfo, type) { |
| objectInfo.performing[type] = (objectInfo.performing[type] || 0) + 1; |
| objectInfo.performingCount++; |
| RepartitionObservers(ObserverIsInactive, |
| objectInfo.changeObservers, |
| objectInfo.inactiveObservers, |
| objectInfo); |
| } |
| |
| function EndPerformChange(objectInfo, type) { |
| objectInfo.performing[type]--; |
| objectInfo.performingCount--; |
| RepartitionObservers(ObserverIsActive, |
| objectInfo.inactiveObservers, |
| objectInfo.changeObservers, |
| objectInfo); |
| } |
| |
| function EnsureObserverRemoved(objectInfo, callback) { |
| function remove(observerList) { |
| for (var i = 0; i < observerList.length; i++) { |
| if (observerList[i].callback === callback) { |
| observerList.splice(i, 1); |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| if (!remove(objectInfo.changeObservers)) |
| remove(objectInfo.inactiveObservers); |
| } |
| |
| function AcceptArgIsValid(arg) { |
| if (IS_UNDEFINED(arg)) |
| return true; |
| |
| if (!IS_SPEC_OBJECT(arg) || |
| !IS_NUMBER(arg.length) || |
| arg.length < 0) |
| return false; |
| |
| var length = arg.length; |
| for (var i = 0; i < length; i++) { |
| if (!IS_STRING(arg[i])) |
| return false; |
| } |
| return true; |
| } |
| |
| function EnsureCallbackPriority(callback) { |
| if (!callbackInfoMap.has(callback)) |
| callbackInfoMap.set(callback, observationState.nextCallbackPriority++); |
| } |
| |
| function NormalizeCallbackInfo(callback) { |
| var callbackInfo = callbackInfoMap.get(callback); |
| if (IS_NUMBER(callbackInfo)) { |
| var priority = callbackInfo; |
| callbackInfo = new InternalArray; |
| callbackInfo.priority = priority; |
| callbackInfoMap.set(callback, callbackInfo); |
| } |
| return callbackInfo; |
| } |
| |
| function ObjectObserve(object, callback, accept) { |
| if (!IS_SPEC_OBJECT(object)) |
| throw MakeTypeError("observe_non_object", ["observe"]); |
| if (!IS_SPEC_FUNCTION(callback)) |
| throw MakeTypeError("observe_non_function", ["observe"]); |
| if (ObjectIsFrozen(callback)) |
| throw MakeTypeError("observe_callback_frozen"); |
| if (!AcceptArgIsValid(accept)) |
| throw MakeTypeError("observe_accept_invalid"); |
| |
| EnsureCallbackPriority(callback); |
| |
| var objectInfo = objectInfoMap.get(object); |
| if (IS_UNDEFINED(objectInfo)) { |
| objectInfo = CreateObjectInfo(object); |
| %SetIsObserved(object); |
| } |
| |
| EnsureObserverRemoved(objectInfo, callback); |
| |
| var observer = CreateObserver(callback, accept); |
| if (ObserverIsActive(observer, objectInfo)) |
| objectInfo.changeObservers.push(observer); |
| else |
| objectInfo.inactiveObservers.push(observer); |
| |
| return object; |
| } |
| |
| function ObjectUnobserve(object, callback) { |
| if (!IS_SPEC_OBJECT(object)) |
| throw MakeTypeError("observe_non_object", ["unobserve"]); |
| if (!IS_SPEC_FUNCTION(callback)) |
| throw MakeTypeError("observe_non_function", ["unobserve"]); |
| |
| var objectInfo = objectInfoMap.get(object); |
| if (IS_UNDEFINED(objectInfo)) |
| return object; |
| |
| EnsureObserverRemoved(objectInfo, callback); |
| return object; |
| } |
| |
| function ArrayObserve(object, callback) { |
| return ObjectObserve(object, callback, ['new', |
| 'updated', |
| 'deleted', |
| 'splice']); |
| } |
| |
| function ArrayUnobserve(object, callback) { |
| return ObjectUnobserve(object, callback); |
| } |
| |
| function EnqueueToCallback(callback, changeRecord) { |
| var callbackInfo = NormalizeCallbackInfo(callback); |
| observationState.pendingObservers[callbackInfo.priority] = callback; |
| callbackInfo.push(changeRecord); |
| %SetObserverDeliveryPending(); |
| } |
| |
| function EnqueueChangeRecord(changeRecord, observers) { |
| // TODO(rossberg): adjust once there is a story for symbols vs proxies. |
| if (IS_SYMBOL(changeRecord.name)) return; |
| |
| for (var i = 0; i < observers.length; i++) { |
| var observer = observers[i]; |
| if (IS_UNDEFINED(observer.accept[changeRecord.type])) |
| continue; |
| |
| EnqueueToCallback(observer.callback, changeRecord); |
| } |
| } |
| |
| function BeginPerformSplice(array) { |
| var objectInfo = objectInfoMap.get(array); |
| if (!IS_UNDEFINED(objectInfo)) |
| BeginPerformChange(objectInfo, 'splice'); |
| } |
| |
| function EndPerformSplice(array) { |
| var objectInfo = objectInfoMap.get(array); |
| if (!IS_UNDEFINED(objectInfo)) |
| EndPerformChange(objectInfo, 'splice'); |
| } |
| |
| function EnqueueSpliceRecord(array, index, removed, addedCount) { |
| var objectInfo = objectInfoMap.get(array); |
| if (IS_UNDEFINED(objectInfo) || objectInfo.changeObservers.length === 0) |
| return; |
| |
| var changeRecord = { |
| type: 'splice', |
| object: array, |
| index: index, |
| removed: removed, |
| addedCount: addedCount |
| }; |
| |
| ObjectFreeze(changeRecord); |
| ObjectFreeze(changeRecord.removed); |
| EnqueueChangeRecord(changeRecord, objectInfo.changeObservers); |
| } |
| |
| function NotifyChange(type, object, name, oldValue) { |
| var objectInfo = objectInfoMap.get(object); |
| if (objectInfo.changeObservers.length === 0) |
| return; |
| |
| var changeRecord = (arguments.length < 4) ? |
| { type: type, object: object, name: name } : |
| { type: type, object: object, name: name, oldValue: oldValue }; |
| ObjectFreeze(changeRecord); |
| EnqueueChangeRecord(changeRecord, objectInfo.changeObservers); |
| } |
| |
| var notifierPrototype = {}; |
| |
| function ObjectNotifierNotify(changeRecord) { |
| if (!IS_SPEC_OBJECT(this)) |
| throw MakeTypeError("called_on_non_object", ["notify"]); |
| |
| var target = notifierTargetMap.get(this); |
| if (IS_UNDEFINED(target)) |
| throw MakeTypeError("observe_notify_non_notifier"); |
| if (!IS_STRING(changeRecord.type)) |
| throw MakeTypeError("observe_type_non_string"); |
| |
| var objectInfo = objectInfoMap.get(target); |
| if (IS_UNDEFINED(objectInfo) || objectInfo.changeObservers.length === 0) |
| return; |
| |
| var newRecord = { object: target }; |
| for (var prop in changeRecord) { |
| if (prop === 'object') continue; |
| %DefineOrRedefineDataProperty(newRecord, prop, changeRecord[prop], |
| READ_ONLY + DONT_DELETE); |
| } |
| ObjectFreeze(newRecord); |
| |
| EnqueueChangeRecord(newRecord, objectInfo.changeObservers); |
| } |
| |
| function ObjectNotifierPerformChange(changeType, changeFn, receiver) { |
| if (!IS_SPEC_OBJECT(this)) |
| throw MakeTypeError("called_on_non_object", ["performChange"]); |
| |
| var target = notifierTargetMap.get(this); |
| if (IS_UNDEFINED(target)) |
| throw MakeTypeError("observe_notify_non_notifier"); |
| if (!IS_STRING(changeType)) |
| throw MakeTypeError("observe_perform_non_string"); |
| if (!IS_SPEC_FUNCTION(changeFn)) |
| throw MakeTypeError("observe_perform_non_function"); |
| |
| if (IS_NULL_OR_UNDEFINED(receiver)) { |
| receiver = %GetDefaultReceiver(changeFn) || receiver; |
| } else if (!IS_SPEC_OBJECT(receiver) && %IsClassicModeFunction(changeFn)) { |
| receiver = ToObject(receiver); |
| } |
| |
| var objectInfo = objectInfoMap.get(target); |
| if (IS_UNDEFINED(objectInfo)) |
| return; |
| |
| BeginPerformChange(objectInfo, changeType); |
| try { |
| %_CallFunction(receiver, changeFn); |
| } finally { |
| EndPerformChange(objectInfo, changeType); |
| } |
| } |
| |
| function ObjectGetNotifier(object) { |
| if (!IS_SPEC_OBJECT(object)) |
| throw MakeTypeError("observe_non_object", ["getNotifier"]); |
| |
| if (ObjectIsFrozen(object)) return null; |
| |
| var objectInfo = objectInfoMap.get(object); |
| if (IS_UNDEFINED(objectInfo)) { |
| objectInfo = CreateObjectInfo(object); |
| %SetIsObserved(object); |
| } |
| |
| if (IS_NULL(objectInfo.notifier)) { |
| objectInfo.notifier = { __proto__: notifierPrototype }; |
| notifierTargetMap.set(objectInfo.notifier, object); |
| } |
| |
| return objectInfo.notifier; |
| } |
| |
| function CallbackDeliverPending(callback) { |
| var callbackInfo = callbackInfoMap.get(callback); |
| if (IS_UNDEFINED(callbackInfo) || IS_NUMBER(callbackInfo)) |
| return false; |
| |
| // Clear the pending change records from callback and return it to its |
| // "optimized" state. |
| var priority = callbackInfo.priority; |
| callbackInfoMap.set(callback, priority); |
| |
| delete observationState.pendingObservers[priority]; |
| var delivered = []; |
| %MoveArrayContents(callbackInfo, delivered); |
| |
| try { |
| %Call(void 0, delivered, callback); |
| } catch (ex) {} |
| return true; |
| } |
| |
| function ObjectDeliverChangeRecords(callback) { |
| if (!IS_SPEC_FUNCTION(callback)) |
| throw MakeTypeError("observe_non_function", ["deliverChangeRecords"]); |
| |
| while (CallbackDeliverPending(callback)) {} |
| } |
| |
| function DeliverChangeRecords() { |
| while (observationState.pendingObservers.length) { |
| var pendingObservers = observationState.pendingObservers; |
| observationState.pendingObservers = new InternalArray; |
| for (var i in pendingObservers) { |
| CallbackDeliverPending(pendingObservers[i]); |
| } |
| } |
| } |
| |
| function SetupObjectObserve() { |
| %CheckIsBootstrapping(); |
| InstallFunctions($Object, DONT_ENUM, $Array( |
| "deliverChangeRecords", ObjectDeliverChangeRecords, |
| "getNotifier", ObjectGetNotifier, |
| "observe", ObjectObserve, |
| "unobserve", ObjectUnobserve |
| )); |
| InstallFunctions($Array, DONT_ENUM, $Array( |
| "observe", ArrayObserve, |
| "unobserve", ArrayUnobserve |
| )); |
| InstallFunctions(notifierPrototype, DONT_ENUM, $Array( |
| "notify", ObjectNotifierNotify, |
| "performChange", ObjectNotifierPerformChange |
| )); |
| } |
| |
| SetupObjectObserve(); |