diff --git a/packages/ember-meta/lib/meta.ts b/packages/ember-meta/lib/meta.ts index aad90b85a62..a2e707cc338 100644 --- a/packages/ember-meta/lib/meta.ts +++ b/packages/ember-meta/lib/meta.ts @@ -323,12 +323,12 @@ export class Meta { ); let ret = this._chains; if (ret === undefined) { - if (this.parent === null) { - ret = create(this.source); - } else { - ret = this.parent.writableChains(create).copy(this.source); + this._chains = ret = create(this.source); + + if (this.parent !== null) { + let parentChains = this.parent.writableChains(create); + parentChains.copyTo(ret); } - this._chains = ret; } return ret; } diff --git a/packages/ember-metal/lib/chains.ts b/packages/ember-metal/lib/chains.ts index d76e2065f5e..55a279f576d 100644 --- a/packages/ember-metal/lib/chains.ts +++ b/packages/ember-metal/lib/chains.ts @@ -208,18 +208,16 @@ class ChainNode { } // copies a top level object only - copy(obj: any) { - let ret = makeChainNode(obj); + copyTo(target: ChainNode) { let paths = this.paths; if (paths !== undefined) { let path; for (path in paths) { if (paths[path] > 0) { - ret.add(path); + target.add(path); } } } - return ret; } // called on the root node of a chain to setup watchers on the specified diff --git a/packages/ember-metal/tests/chains_test.js b/packages/ember-metal/tests/chains_test.js index 90bc2f3d040..91aa9bfc2ab 100644 --- a/packages/ember-metal/tests/chains_test.js +++ b/packages/ember-metal/tests/chains_test.js @@ -5,6 +5,7 @@ import { finishChains, defineProperty, computed, + alias, notifyPropertyChange, watch, unwatch, @@ -152,5 +153,55 @@ moduleFor( assert.equal(watcherCount(obj.a, 'b.c'), 0); assert.equal(watcherCount(obj.a.b, 'c'), 0); } + + ['@test writable chains is not defined more than once'](assert) { + assert.expect(0); + function didChange() {} + + let obj = { + foo: { + bar: { + baz: { + value: 123, + }, + }, + }, + }; + + // Setup object like a constructor, which delays initializing values in chains + let parentMeta = meta(obj); + parentMeta.proto = obj; + + // Define a standard computed property, which will eventually setup dependencies + defineProperty( + obj, + 'bar', + computed('foo.bar', { + get() { + return this.foo.bar; + }, + }) + ); + + // Define some aliases, which will proxy chains along + defineProperty(obj, 'baz', alias('bar.baz')); + defineProperty(obj, 'value', alias('baz.value')); + + // Define an observer, which will eagerly attempt to setup chains and watch + // their values. This follows the aliases eagerly, and forces the first + // computed to actually set up its values/dependencies for chains. If + // writableChains was not already defined, this results in multiple root + // chain nodes being defined on the same object meta. + addObserver(obj, 'value', null, didChange); + + let childObj = Object.create(obj); + + let childMeta = meta(childObj); + + finishChains(childMeta); + + // If we can unwatch the root computed, then chains was not overwritten + unwatch(childObj, 'foo.bar'); + } } );