/*
No rights, expressed or implied, whatsoever to this software are provided by Motorola Mobility, Inc. hereunder.
(c) Copyright 2011 Motorola Mobility, Inc. All Rights Reserved.
Assumes that an ES5 platform where, if {@code WeakMap} is
* already present, then it conforms to the anticipated ES6
* specification. To run this file on an ES5 or almost ES5
* implementation where the {@code WeakMap} specification does not
* quite conform, run repairES5.js
first.
*
* @author Mark S. Miller
* @requires ses
* @overrides WeakMap
*/
/**
@module montage/core/shim/weak-map
*/
/**
* This {@code WeakMap} emulation is observably equivalent to the
* ES-Harmony WeakMap, but with leakier garbage collection properties.
*
* As with true WeakMaps, in this emulation, a key does not
* retain maps indexed by that key and (crucially) a map does not
* retain the keys it indexes. A map by itself also does not retain
* the values associated with that map.
*
However, the values associated with a key in some map are * retained so long as that key is retained and those associations are * not overridden. For example, when used to support membranes, all * values exported from a given membrane will live for the lifetime * they would have had in the absence of an interposed membrane. Even * when the membrane is revoked, all objects that would have been * reachable in the absence of revocation will still be reachable, as * far as the GC can tell, even though they will no longer be relevant * to ongoing computation. * *
The API implemented here is approximately the API as implemented * in FF6.0a1 and agreed to by MarkM, Andreas Gal, and Dave Herman, * rather than the offially approved proposal page. TODO(erights): * upgrade the ecmascript WeakMap proposal page to explain this API * change and present to EcmaScript committee for their approval. *
The first difference between the emulation here and that in * FF6.0a1 is the presence of non enumerable {@code get___, has___, * set___, and delete___} methods on WeakMap instances to represent * what would be the hidden internal properties of a primitive * implementation. Whereas the FF6.0a1 WeakMap.prototype methods * require their {@code this} to be a genuine WeakMap instance (i.e., * an object of {@code [[Class]]} "WeakMap}), since there is nothing * unforgeable about the pseudo-internal method names used here, * nothing prevents these emulated prototype methods from being * applied to non-WeakMaps with pseudo-internal methods of the same * names. * *
Another difference is that our emulated {@code * WeakMap.prototype} is not itself a WeakMap. A problem with the * current FF6.0a1 API is that WeakMap.prototype is itself a WeakMap * providing ambient mutability and an ambient communications * channel. Thus, if a WeakMap is already present and has this * problem, repairES5.js wraps it in a safe wrappper in order to * prevent access to this channel. (See * PATCH_MUTABLE_FROZEN_WEAKMAP_PROTO in repairES5.js). @class module:montage/core/shim/weak-map.WeakMap */ var WeakMap; /** * If this is a full secureable ES5 platform and the ES-Harmony {@code WeakMap} is * absent, install an approximate emulation. * *
If this is almost a secureable ES5 platform, then WeakMap.js * should be run after repairES5.js. * *
See {@code WeakMap} for documentation of the garbage collection * properties of this WeakMap emulation. */ (function() { "use strict"; // if (typeof ses !== 'undefined' && ses.ok && !ses.ok()) { // // already too broken, so give up // return; // } if (typeof WeakMap === 'function') { // assumed fine, so we're done. return; } var hop = Object.prototype.hasOwnProperty; var gopn = Object.getOwnPropertyNames; var defProp = Object.defineProperty; /** * Holds the orginal static properties of the Object constructor, * after repairES5 fixes these if necessary to be a more complete * secureable ES5 environment, but before installing the following * WeakMap emulation overrides and before any untrusted code runs. */ var originalProps = {}; gopn(Object).forEach(function(name) { originalProps[name] = Object[name]; }); /** * Security depends on HIDDEN_NAME being both unguessable and * undiscoverable by untrusted code. * *
Given the known weaknesses of Math.random() on existing * browsers, it does not generate unguessability we can be confident * of. TODO(erights): Detect crypto.getRandomValues and if there, * use it instead. * *
It is the monkey patching logic in this file that is intended * to ensure undiscoverability. The basic idea is that there are * three fundamental means of discovering properties of an object: * The for/in loop, Object.keys(), and Object.getOwnPropertyNames(), * as well as some proposed ES6 extensions that appear on our * whitelist. The first two only discover enumerable properties, and * we only use HIDDEN_NAME to name a non-enumerable property, so the * only remaining threat should be getOwnPropertyNames and some * proposed ES6 extensions that appear on our whitelist. We monkey * patch them to remove HIDDEN_NAME from the list of properties they * returns. */ var HIDDEN_NAME = 'ident:' + Math.random() + '___'; /** * Monkey patch getOwnPropertyNames to avoid revealing the * HIDDEN_NAME. * *
The ES5.1 spec requires each name to appear only once, but as
* of this writing, this requirement is controversial for ES6, so we
* made this code robust against this case. If the resulting extra
* search turns out to be expensive, we can probably relax this once
* ES6 is adequately supported on all major browsers, iff no browser
* versions we support at that time have relaxed this constraint
* without providing built-in ES6 WeakMaps.
*/
defProp(Object, 'getOwnPropertyNames', {
value: function fakeGetOwnPropertyNames(obj) {
var result = gopn(obj);
var i = 0;
while ((i = result.indexOf(HIDDEN_NAME, i)) >= 0) {
result.splice(i, 1);
}
return result;
}
});
/**
getPropertyNames is not in ES5 but it is proposed for ES6 and
does appear in our whitelist, so we need to clean it too.
*/
if ('getPropertyNames' in Object) {
defProp(Object, 'getPropertyNames', {
value: function fakeGetPropertyNames(obj) {
var result = originalProps.getPropertyNames(obj);
var i = 0;
while ((i = result.indexOf(HIDDEN_NAME, i)) >= 0) {
result.splice(i, 1);
}
return result;
}
});
}
/**
*
To treat objects as identity-keys with reasonable efficiency * on ES5 by itself (i.e., without any object-keyed collections), we * need to add a hidden property to such key objects when we * can. This raises several issues: *
The monkey patched versions throw a TypeError if their
* argument is not an object, so it should only be done to functions
* that should throw a TypeError anyway if their argument is not an
* object.
*/
(function(){
var oldFreeze = Object.freeze;
defProp(Object, 'freeze', {
value: function identifyingFreeze(obj) {
getHiddenRecord(obj);
return oldFreeze(obj);
}
});
var oldSeal = Object.seal;
defProp(Object, 'seal', {
value: function identifyingSeal(obj) {
getHiddenRecord(obj);
return oldSeal(obj);
}
});
var oldPreventExtensions = Object.preventExtensions;
defProp(Object, 'preventExtensions', {
value: function identifyingPreventExtensions(obj) {
getHiddenRecord(obj);
return oldPreventExtensions(obj);
}
});
})();
function constFunc(func) {
Object.freeze(func.prototype);
return Object.freeze(func);
}
WeakMap = function() {
var keys = []; // brute force for prematurely non-extensible keys.
var vals = []; // brute force for corresponding values.
function get___(key, opt_default) {
var hr = getHiddenRecord(key);
var i, vs;
if (hr) {
i = hr.gets.indexOf(get___);
vs = hr.vals;
} else {
i = keys.indexOf(key);
vs = vals;
}
return (i >= 0) ? vs[i] : opt_default;
}
function has___(key) {
var hr = getHiddenRecord(key);
var i;
if (hr) {
i = hr.gets.indexOf(get___);
} else {
i = keys.indexOf(key);
}
return i >= 0;
}
function set___(key, value) {
var hr = getHiddenRecord(key);
var i;
if (hr) {
i = hr.gets.indexOf(get___);
if (i >= 0) {
hr.vals[i] = value;
} else {
hr.gets.push(get___);
hr.vals.push(value);
}
} else {
i = keys.indexOf(key);
if (i >= 0) {
vals[i] = value;
} else {
keys.push(key);
vals.push(value);
}
}
}
function delete___(key) {
var hr = getHiddenRecord(key);
var i;
if (hr) {
i = hr.gets.indexOf(get___);
if (i >= 0) {
hr.gets.splice(i, 1);
hr.vals.splice(i, 1);
}
} else {
i = keys.indexOf(key);
if (i >= 0) {
keys.splice(i, 1);
vals.splice(i, 1);
}
}
return true;
}
return Object.create(WeakMap.prototype, {
get___: { value: constFunc(get___) },
has___: { value: constFunc(has___) },
set___: { value: constFunc(set___) },
delete___: { value: constFunc(delete___) }
});
};
WeakMap.prototype = Object.create(Object.prototype, /** @lends module:montage/core/shim/weak-map.WeakMap# */ {
/**
* Returns the value most recently associated with key, or
* opt_default if none.
* @function
* @param {object} key
* @param {object} opt_default
*/
get: {
value: function get(key, opt_default) {
return this.get___(key, opt_default);
},
writable: true,
configurable: true
},
/**
* Returns true
if there is a value associated with the specified key in the WeakMap, otherwise returns false
* @function
* @param {object} key
*/
has: {
value: function has(key) {
return this.has___(key);
},
writable: true,
configurable: true
},
/**
* Associate value with key in this WeakMap, overwriting any
* previous association if present.
* @function
* @param {object} key
* @param {object} value
*/
set: {
value: function set(key, value) {
this.set___(key, value);
},
writable: true,
configurable: true
},
/**
* Remove any association for key in this WeakMap, returning
* whether there was one.
* @function
* @param {object} key
*
* FIXME
Note that the boolean return here does not work like the * delete operator. The operator returns * whether the deletion succeeds at bringing about a state in * which the deleted property is absent. The delete * operator therefore returns true if the property was already * absent, whereas this {@link delete} method returns false if * the association was already absent. */ 'delete': { value: function remove(key) { return this.delete___(key); }, writable: true, configurable: true } }); })(); exports.WeakMap = WeakMap;