diff --git a/packages/ember-metal/lib/meta.js b/packages/ember-metal/lib/meta.js index 94a7e00b62c..0952d03965d 100644 --- a/packages/ember-metal/lib/meta.js +++ b/packages/ember-metal/lib/meta.js @@ -33,6 +33,7 @@ import EmptyObject from 'ember-metal/empty_object'; */ let members = { cache: ownMap, + weak: ownMap, watching: inheritedMap, mixins: inheritedMap, bindings: inheritedMap, @@ -53,6 +54,7 @@ function Meta(obj, parentMeta) { this._deps = undefined; this._chainWatchers = undefined; this._chains = undefined; + this._weak = undefined; // used only internally this.source = obj; @@ -148,6 +150,18 @@ Meta.prototype._getInherited = function(key) { } }; +Meta.prototype.getWeak = function(symbol) { + var weak = this.readableWeak(); + if (weak) { + return weak[symbol]; + } +}; + +Meta.prototype.setWeak = function(symbol, value) { + var weak = this.writableWeak(); + return weak[symbol] = value; +}; + Meta.prototype._findInherited = function(key, subkey) { let pointer = this; while (pointer !== undefined) { diff --git a/packages/ember-metal/lib/weak_map.js b/packages/ember-metal/lib/weak_map.js new file mode 100644 index 00000000000..682686ff159 --- /dev/null +++ b/packages/ember-metal/lib/weak_map.js @@ -0,0 +1,74 @@ +import { assert } from 'ember-metal/debug'; +import { GUID_KEY } from 'ember-metal/utils'; +import { meta } from 'ember-metal/meta'; + +var id = 0; +function UNDEFINED() {} + +/* + * @public + * @class Ember.WeakMap + * + * Weak relationship from Map -> Key, but not Key to Map. + * + * Key must be a non null object + */ +export default function WeakMap() { + this._id = GUID_KEY + (id++); +} + +/* + * @method get + * @param key {Object} + * @return {*} stored value + */ +WeakMap.prototype.get = function(obj) { + var map = meta(obj).readableWeak(); + if (map) { + if (map[this._id] === UNDEFINED) { + return undefined; + } + + return map[this._id]; + } +}; + +/* + * @method set + * @param key {Object} + * @param value {Any} + * @return {Any} stored value + */ +WeakMap.prototype.set = function(obj, value) { + assert('Uncaught TypeError: Invalid value used as weak map key', obj && (typeof obj === 'object' || typeof obj === 'function')); + + if (value === undefined) { + value = UNDEFINED; + } + + meta(obj).writableWeak()[this._id] = value; + return this; +}; + +/* + * @method has + * @param key {Object} + * @return {Boolean} if the key exists + */ +WeakMap.prototype.has = function(obj) { + var map = meta(obj).readableWeak(); + + return (map && map[this._id] !== undefined); +}; + +/* + * @method delete + * @param key {Object} + */ +WeakMap.prototype.delete = function(obj) { + if (this.has(obj)) { + delete meta(obj).writableWeak()[this._id]; + } + + return this; +}; diff --git a/packages/ember-metal/tests/weak_map_test.js b/packages/ember-metal/tests/weak_map_test.js new file mode 100644 index 00000000000..c55b015d285 --- /dev/null +++ b/packages/ember-metal/tests/weak_map_test.js @@ -0,0 +1,90 @@ +import WeakMap from 'ember-metal/weak_map'; + +QUnit.module('Ember.WeakMap'); + +QUnit.test('has weakMap like qualities', function(assert) { + var map = new WeakMap(); + var map2 = new WeakMap(); + + var a = {}; + var b = {}; + var c = {}; + + equal(map.get(a), undefined); + equal(map.get(b), undefined); + equal(map.get(c), undefined); + + equal(map2.get(a), undefined); + equal(map2.get(b), undefined); + equal(map2.get(c), undefined); + + equal(map.set(a, 1), map, 'map.set should return itself'); + equal(map.get(a), 1); + equal(map.set(b, undefined), map); + equal(map.set(a, 2), map); + equal(map.get(a), 2); + equal(map.set(b, undefined), map); + + equal(map2.get(a), undefined); + equal(map2.get(b), undefined); + equal(map2.get(c), undefined); + + equal(map.set(c, 1), map); + equal(map.get(c), 1); + equal(map.get(a), 2); + equal(map.get(b), undefined); + + equal(map2.set(a, 3), map2); + equal(map2.set(b, 4), map2); + equal(map2.set(c, 5), map2); + + equal(map2.get(a), 3); + equal(map2.get(b), 4); + equal(map2.get(c), 5); + + equal(map.get(c), 1); + equal(map.get(a), 2); + equal(map.get(b), undefined); +}); + +QUnit.test('that error is thrown when using a primitive key', function(assert) { + var map = new WeakMap(); + + expectAssertion(function() { + map.set('a', 1); + }, /Uncaught TypeError: Invalid value used as weak map key/); + + expectAssertion(function() { + map.set(1, 1); + }, /Uncaught TypeError: Invalid value used as weak map key/); + + expectAssertion(function() { + map.set(true, 1); + }, /Uncaught TypeError: Invalid value used as weak map key/); + + expectAssertion(function() { + map.set(null, 1); + }, /Uncaught TypeError: Invalid value used as weak map key/); + + expectAssertion(function() { + map.set(undefined, 1); + }, /Uncaught TypeError: Invalid value used as weak map key/); +}); + +QUnit.test('that .has and .delete work as expected', function(assert) { + var map = new WeakMap(); + var a = {}; + var b = {}; + var foo = { id: 1, name: 'My file', progress: 0 }; + + deepEqual(map.set(a, foo), map); + deepEqual(map.get(a), foo); + ok(map.has(a)); + ok(!map.has(b)); + + deepEqual(map.delete(a), map); + ok(!map.has(a)); + + map.set(a, undefined); + ok(map.has(a)); +});