520 lines
20 KiB
JavaScript
520 lines
20 KiB
JavaScript
// 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);
|
|
|
|
|