// Copyright 2012 Kap IT (http://www.kapit.fr/) // // Licensed under the Apache License, Version 2.0 (the 'License'); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an 'AS IS' BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Author : François de Campredon (http://francois.de-campredon.fr/), // Object.observe Shim // =================== // *See [The harmony proposal page](http://wiki.ecmascript.org/doku.php?id=harmony:observe)* (function (global) { 'use strict'; if (typeof Object.observe === 'function') { return; } // Utilities // --------- // setImmediate shim used to deliver changes records asynchronously // use setImmediate if available var setImmediate = global.setImmediate || global.msSetImmediate, clearImmediate = global.clearImmediate || global.msClearImmediate; if (!setImmediate) { // fallback on setTimeout if not setImmediate = function (func, args) { return setTimeout(func, 0, args); }; clearImmediate = function (id) { clearTimeout(id); }; } // WeakMap // ------- var PrivateMap; if (typeof WeakMap !== 'undefined') { //use weakmap if defined PrivateMap = WeakMap; } else { //else use ses like shim of WeakMap var HIDDEN_PREFIX = '__weakmap:' + (Math.random() * 1e9 >>> 0), counter = new Date().getTime() % 1e9, mascot = {}; PrivateMap = function () { this.name = HIDDEN_PREFIX + (Math.random() * 1e9 >>> 0) + (counter++ + '__'); }; PrivateMap.prototype = { has: function (key) { return key && key.hasOwnProperty(this.name); }, get: function (key) { var value = key && key[this.name]; return value === mascot ? undefined : value; }, set: function (key, value) { Object.defineProperty(key, this.name, { value : typeof value === 'undefined' ? mascot : value, enumerable: false, writable : true, configurable: true }); }, 'delete': function (key) { return delete key[this.name]; } }; var getOwnPropertyName = Object.getOwnPropertyNames; Object.defineProperty(Object, 'getOwnPropertyNames', { value: function fakeGetOwnPropertyNames(obj) { return getOwnPropertyName(obj).filter(function (name) { return name.substr(0, HIDDEN_PREFIX.length) !== HIDDEN_PREFIX; }); }, writable: true, enumerable: false, configurable: true }); } // Internal Properties // ------------------- // An ordered list used to provide a deterministic ordering in which callbacks are called. // [Corresponding Section in ECMAScript wiki](http://wiki.ecmascript.org/doku.php?id=harmony:observe_internals#observercallbacks) var observerCallbacks = []; // This object is used as the prototype of all the notifiers that are returned by Object.getNotifier(O). // [Corresponding Section in ECMAScript wiki](http://wiki.ecmascript.org/doku.php?id=harmony:observe_internals#notifierprototype) var NotifierPrototype = Object.create(Object.prototype); // Used to store immediate uid reference var changeDeliveryImmediateUid; // Used to schedule a call to _deliverAllChangeRecords function setUpChangesDelivery() { clearImmediate(changeDeliveryImmediateUid); changeDeliveryImmediateUid = setImmediate(_deliverAllChangeRecords); } Object.defineProperty(NotifierPrototype, 'notify', { value: function notify(changeRecord) { var notifier = this; if (Object(notifier) !== notifier) { throw new TypeError('this must be an Object, given ' + notifier); } if (!notifier.__target) { return; } if (Object(changeRecord) !== changeRecord) { throw new TypeError('changeRecord must be an Object, given ' + changeRecord); } var type = changeRecord.type; if (typeof type !== 'string') { throw new TypeError('changeRecord.type must be a string, given ' + type); } var changeObservers = changeObserversMap.get(notifier); if (!changeObservers || changeObservers.length === 0) { return; } var target = notifier.__target, newRecord = Object.create(Object.prototype, { 'object': { value: target, writable : false, enumerable : true, configurable: false } }); for (var prop in changeRecord) { if (prop !== 'object') { var value = changeRecord[prop]; Object.defineProperty(newRecord, prop, { value: value, writable : false, enumerable : true, configurable: false }); } } Object.preventExtensions(newRecord); _enqueueChangeRecord(notifier.__target, newRecord); }, writable: true, enumerable: false, configurable : true }); Object.defineProperty(NotifierPrototype, 'performChange', { value: function performChange(changeType, changeFn) { var notifier = this; if (Object(notifier) !== notifier) { throw new TypeError('this must be an Object, given ' + notifier); } if (!notifier.__target) { return; } if (typeof changeType !== 'string') { throw new TypeError('changeType must be a string given ' + notifier); } if (typeof changeFn !== 'function') { throw new TypeError('changeFn must be a function, given ' + changeFn); } _beginChange(notifier.__target, changeType); var error, changeRecord; try { changeRecord = changeFn.call(undefined); } catch (e) { error = e; } _endChange(notifier.__target, changeType); if (typeof error !== 'undefined') { throw error; } var changeObservers = changeObserversMap.get(notifier); if (changeObservers.length === 0) { return; } var target = notifier.__target, newRecord = Object.create(Object.prototype, { 'object': { value: target, writable : false, enumerable : true, configurable: false }, 'type': { value: changeType, writable : false, enumerable : true, configurable: false } }); if (typeof changeRecord !== 'undefined') { for (var prop in changeRecord) { if (prop !== 'object' && prop !== 'type') { var value = changeRecord[prop]; Object.defineProperty(newRecord, prop, { value: value, writable : false, enumerable : true, configurable: false }); } } } Object.preventExtensions(newRecord); _enqueueChangeRecord(notifier.__target, newRecord); }, writable: true, enumerable: false, configurable : true }); // Implementation of the internal algorithm 'BeginChange' // described in the proposal. // [Corresponding Section in ECMAScript wiki](http://wiki.ecmascript.org/doku.php?id=harmony:observe_internals#beginchange) function _beginChange(object, changeType) { var notifier = Object.getNotifier(object), activeChanges = activeChangesMap.get(notifier), changeCount = activeChangesMap.get(notifier)[changeType]; activeChanges[changeType] = typeof changeCount === 'undefined' ? 1 : changeCount + 1; } // Implementation of the internal algorithm 'EndChange' // described in the proposal. // [Corresponding Section in ECMAScript wiki](http://wiki.ecmascript.org/doku.php?id=harmony:observe_internals#endchange) function _endChange(object, changeType) { var notifier = Object.getNotifier(object), activeChanges = activeChangesMap.get(notifier), changeCount = activeChangesMap.get(notifier)[changeType]; activeChanges[changeType] = changeCount > 0 ? changeCount - 1 : 0; } // Implementation of the internal algorithm 'ShouldDeliverToObserver' // described in the proposal. // [Corresponding Section in ECMAScript wiki](http://wiki.ecmascript.org/doku.php?id=harmony:observe_internals#shoulddelivertoobserver) function _shouldDeliverToObserver(activeChanges, acceptList, changeType) { var doesAccept = false; if (acceptList) { for (var i = 0, l = acceptList.length; i < l; i++) { var accept = acceptList[i]; if (activeChanges[accept] > 0) { return false; } if (accept === changeType) { doesAccept = true; } } } return doesAccept; } // Map used to store corresponding notifier to an object var notifierMap = new PrivateMap(), changeObserversMap = new PrivateMap(), activeChangesMap = new PrivateMap(); // Implementation of the internal algorithm 'GetNotifier' // described in the proposal. // [Corresponding Section in ECMAScript wiki](http://wiki.ecmascript.org/doku.php?id=harmony:observe_internals#getnotifier) function _getNotifier(target) { if (!notifierMap.has(target)) { var notifier = Object.create(NotifierPrototype); // we does not really need to hide this, since anyway the host object is accessible from outside of the // implementation. we just make it unwritable Object.defineProperty(notifier, '__target', { value : target }); changeObserversMap.set(notifier, []); activeChangesMap.set(notifier, {}); notifierMap.set(target, notifier); } return notifierMap.get(target); } // map used to store reference to a list of pending changeRecords // in observer callback. var pendingChangesMap = new PrivateMap(); // Implementation of the internal algorithm 'EnqueueChangeRecord' // described in the proposal. // [Corresponding Section in ECMAScript wiki](http://wiki.ecmascript.org/doku.php?id=harmony:observe_internals#enqueuechangerecord) function _enqueueChangeRecord(object, changeRecord) { var notifier = Object.getNotifier(object), changeType = changeRecord.type, activeChanges = activeChangesMap.get(notifier), changeObservers = changeObserversMap.get(notifier); for (var i = 0, l = changeObservers.length; i < l; i++) { var observerRecord = changeObservers[i], acceptList = observerRecord.accept; if (_shouldDeliverToObserver(activeChanges, acceptList, changeType)) { var observer = observerRecord.callback, pendingChangeRecords = []; if (!pendingChangesMap.has(observer)) { pendingChangesMap.set(observer, pendingChangeRecords); } else { pendingChangeRecords = pendingChangesMap.get(observer); } pendingChangeRecords.push(changeRecord); } } setUpChangesDelivery(); } // map used to store a count of associated notifier to a function var attachedNotifierCountMap = new PrivateMap(); // Remove reference all reference to an observer callback, // if this one is not used anymore. // In the proposal the ObserverCallBack has a weak reference over observers, // Without this possibility we need to clean this list to avoid memory leak function _cleanObserver(observer) { if (!attachedNotifierCountMap.get(observer) && !pendingChangesMap.has(observer)) { attachedNotifierCountMap.delete(observer); var index = observerCallbacks.indexOf(observer); if (index !== -1) { observerCallbacks.splice(index, 1); } } } // Implementation of the internal algorithm 'DeliverChangeRecords' // described in the proposal. // [Corresponding Section in ECMAScript wiki](http://wiki.ecmascript.org/doku.php?id=harmony:observe_internals#deliverchangerecords) function _deliverChangeRecords(observer) { var pendingChangeRecords = pendingChangesMap.get(observer); pendingChangesMap.delete(observer); if (!pendingChangeRecords || pendingChangeRecords.length === 0) { return false; } try { observer.call(undefined, pendingChangeRecords); } catch (e) { } _cleanObserver(observer); return true; } // Implementation of the internal algorithm 'DeliverAllChangeRecords' // described in the proposal. // [Corresponding Section in ECMAScript wiki](http://wiki.ecmascript.org/doku.php?id=harmony:observe_internals#deliverallchangerecords) function _deliverAllChangeRecords() { var observers = observerCallbacks.slice(); var anyWorkDone = false; for (var i = 0, l = observers.length; i < l; i++) { var observer = observers[i]; if (_deliverChangeRecords(observer)) { anyWorkDone = true; } } return anyWorkDone; } Object.defineProperties(Object, { // Implementation of the public api 'Object.observe' // described in the proposal. // [Corresponding Section in ECMAScript wiki](http://wiki.ecmascript.org/doku.php?id=harmony:observe_public_api#object.observe) 'observe': { value: function observe(target, callback, accept) { if (Object(target) !== target) { throw new TypeError('target must be an Object, given ' + target); } if (typeof callback !== 'function') { throw new TypeError('observer must be a function, given ' + callback); } if (Object.isFrozen(callback)) { throw new TypeError('observer cannot be frozen'); } var acceptList; if (typeof accept === 'undefined') { acceptList = ['add', 'update', 'delete', 'reconfigure', 'setPrototype', 'preventExtensions']; } else { if (Object(accept) !== accept) { throw new TypeError('accept must be an object, given ' + accept); } var len = accept.length; if (typeof len !== 'number' || len >>> 0 !== len || len < 1) { throw new TypeError('the \'length\' property of accept must be a positive integer, given ' + len); } var nextIndex = 0; acceptList = []; while (nextIndex < len) { var next = accept[nextIndex]; if (typeof next !== 'string') { throw new TypeError('accept must contains only string, given' + next); } acceptList.push(next); nextIndex++; } } var notifier = _getNotifier(target), changeObservers = changeObserversMap.get(notifier); for (var i = 0, l = changeObservers.length; i < l; i++) { if (changeObservers[i].callback === callback) { changeObservers[i].accept = acceptList; return target; } } changeObservers.push({ callback: callback, accept: acceptList }); if (observerCallbacks.indexOf(callback) === -1) { observerCallbacks.push(callback); } if (!attachedNotifierCountMap.has(callback)) { attachedNotifierCountMap.set(callback, 1); } else { attachedNotifierCountMap.set(callback, attachedNotifierCountMap.get(callback) + 1); } return target; }, writable: true, configurable: true }, // Implementation of the public api 'Object.unobseve' // described in the proposal. // [Corresponding Section in ECMAScript wiki](http://wiki.ecmascript.org/doku.php?id=harmony:observe_public_api#object.unobseve) 'unobserve': { value: function unobserve(target, callback) { if (Object(target) !== target) { throw new TypeError('target must be an Object, given ' + target); } if (typeof callback !== 'function') { throw new TypeError('observer must be a function, given ' + callback); } var notifier = _getNotifier(target), changeObservers = changeObserversMap.get(notifier); for (var i = 0, l = changeObservers.length; i < l; i++) { if (changeObservers[i].callback === callback) { changeObservers.splice(i, 1); attachedNotifierCountMap.set(callback, attachedNotifierCountMap.get(callback) - 1); _cleanObserver(callback); break; } } return target; }, writable: true, configurable: true }, // Implementation of the public api 'Object.deliverChangeRecords' // described in the proposal. // [Corresponding Section in ECMAScript wiki](http://wiki.ecmascript.org/doku.php?id=harmony:observe_public_api#object.deliverchangerecords) 'deliverChangeRecords': { value: function deliverChangeRecords(observer) { if (typeof observer !== 'function') { throw new TypeError('callback must be a function, given ' + observer); } while (_deliverChangeRecords(observer)) {} }, writable: true, configurable: true }, // Implementation of the public api 'Object.getNotifier' // described in the proposal. // [Corresponding Section in ECMAScript wiki](http://wiki.ecmascript.org/doku.php?id=harmony:observe_public_api#object.getnotifier) 'getNotifier': { value: function getNotifier(target) { if (Object(target) !== target) { throw new TypeError('target must be an Object, given ' + target); } if (Object.isFrozen(target)) { return null; } return _getNotifier(target); }, writable: true, configurable: true } }); })(typeof global !== 'undefined' ? global : this);