From e316b48c01237d3a2dafc3703d9f00feb91f21ab Mon Sep 17 00:00:00 2001 From: Chris Thoburn Date: Sat, 8 May 2021 18:27:39 -0700 Subject: [PATCH] perf: custom PromiseManyArray --- .../node-tests/fixtures/expected.js | 14 + .../integration/adapter/store-adapter-test.js | 8 +- .../-ember-data/tests/unit/many-array-test.js | 22 +- .../unit/model/relationships/has-many-test.js | 40 +-- .../tests/unit/promise-proxies-test.js | 109 +++--- .../-private/system/promise-many-array.js | 57 ---- .../-private/system/promise-many-array.ts | 322 ++++++++++++++++++ .../-private/system/model/internal-model.ts | 16 +- .../app/routes/destroy.js | 13 +- .../app/routes/unload.js | 9 +- 10 files changed, 446 insertions(+), 164 deletions(-) delete mode 100644 packages/model/addon/-private/system/promise-many-array.js create mode 100644 packages/model/addon/-private/system/promise-many-array.ts diff --git a/packages/-ember-data/node-tests/fixtures/expected.js b/packages/-ember-data/node-tests/fixtures/expected.js index 8e305f0312c..4ea02cc3ef4 100644 --- a/packages/-ember-data/node-tests/fixtures/expected.js +++ b/packages/-ember-data/node-tests/fixtures/expected.js @@ -402,5 +402,19 @@ module.exports = { "(public) @ember-data/adapter/error @ember-data/adapter/error#errorsArrayToHash", "(public) @ember-data/adapter/error @ember-data/adapter/error#errorsHashToArray", "(public) @ember-data/store Store#identifierCache", + '(private) @ember-data/model PromiseManyArray#forEach', + '(public) @ember-data/model PromiseManyArray#isFulfilled', + '(public) @ember-data/model PromiseManyArray#isPending', + '(public) @ember-data/model PromiseManyArray#isRejected', + '(public) @ember-data/model PromiseManyArray#isSettled', + '(public) @ember-data/model PromiseManyArray#length', + '(public) @ember-data/model PromiseManyArray#links', + "(public) @ember-data/model PromiseManyArray#catch", + "(public) @ember-data/model PromiseManyArray#finally", + "(public) @ember-data/model PromiseManyArray#meta", + "(public) @ember-data/model PromiseManyArray#reload", + "(public) @ember-data/model PromiseManyArray#then", + '(public) @ember-data/store ManyArray#links', + '(public) @ember-data/store Store#identifierCache' ], }; diff --git a/packages/-ember-data/tests/integration/adapter/store-adapter-test.js b/packages/-ember-data/tests/integration/adapter/store-adapter-test.js index d0d63f12fc0..3c72770b0c0 100644 --- a/packages/-ember-data/tests/integration/adapter/store-adapter-test.js +++ b/packages/-ember-data/tests/integration/adapter/store-adapter-test.js @@ -12,7 +12,7 @@ import RESTAdapter from '@ember-data/adapter/rest'; import Model, { attr, belongsTo, hasMany } from '@ember-data/model'; import JSONAPISerializer from '@ember-data/serializer/json-api'; import RESTSerializer from '@ember-data/serializer/rest'; -import { PromiseArray, Snapshot } from '@ember-data/store/-private'; +import { Snapshot } from '@ember-data/store/-private'; import testInDebug from '@ember-data/unpublished-test-infra/test-support/test-in-debug'; function moveRecordOutOfInFlight(record) { @@ -1169,7 +1169,7 @@ module('integration/adapter/store-adapter - DS.Store and DS.Adapter integration }, }, }); - assert.ok(tom.get('dogs') instanceof PromiseArray, 'dogs is a promise'); + assert.ok(typeof tom.dogs.then === 'function', 'dogs is a thenable'); return tom.get('dogs'); }) .then((dogs) => { @@ -1205,12 +1205,12 @@ module('integration/adapter/store-adapter - DS.Store and DS.Adapter integration let tom = store.createRecord('person', { name: 'Tom Dale' }); run(() => { - assert.ok(tom.get('dogs') instanceof PromiseArray, 'dogs is a promise before save'); + assert.ok(typeof tom.dogs.then === 'function', 'dogs is a thenable before save'); }); return run(() => { return tom.save().then(() => { - assert.ok(tom.get('dogs') instanceof PromiseArray, 'dogs is a promise after save'); + assert.ok(typeof tom.dogs.then === 'function', 'dogs is a thenable after save'); }); }); }); diff --git a/packages/-ember-data/tests/unit/many-array-test.js b/packages/-ember-data/tests/unit/many-array-test.js index bca47de4b5e..418a9743144 100644 --- a/packages/-ember-data/tests/unit/many-array-test.js +++ b/packages/-ember-data/tests/unit/many-array-test.js @@ -101,17 +101,24 @@ module('unit/many_array - DS.ManyArray', function (hooks) { test('manyArray trigger arrayContentChange functions with the correct values', function (assert) { assert.expect(6); + const TestManyArray = DS.ManyArray.proto(); + let willChangeStartIdx; let willChangeRemoveAmt; let willChangeAddAmt; - DS.ManyArray.proto().__hasArrayObservers = true; - let originalArrayContentWillChange = DS.ManyArray.proto().arrayContentWillChange; - let originalArrayContentDidChange = DS.ManyArray.proto().arrayContentDidChange; + let originalArrayContentWillChange = TestManyArray.arrayContentWillChange; + let originalArrayContentDidChange = TestManyArray.arrayContentDidChange; + let originalInit = TestManyArray.init; // override DS.ManyArray temp (cleanup occures in afterTest); - DS.ManyArray.proto().arrayContentWillChange = function (startIdx, removeAmt, addAmt) { + TestManyArray.init = function (...args) { + this.__hasArrayObservers = true; + originalInit.call(this, ...args); + }; + + TestManyArray.arrayContentWillChange = function (startIdx, removeAmt, addAmt) { willChangeStartIdx = startIdx; willChangeRemoveAmt = removeAmt; willChangeAddAmt = addAmt; @@ -119,7 +126,7 @@ module('unit/many_array - DS.ManyArray', function (hooks) { return originalArrayContentWillChange.apply(this, arguments); }; - DS.ManyArray.proto().arrayContentDidChange = function (startIdx, removeAmt, addAmt) { + TestManyArray.arrayContentDidChange = function (startIdx, removeAmt, addAmt) { assert.equal(startIdx, willChangeStartIdx, 'WillChange and DidChange startIdx should match'); assert.equal(removeAmt, willChangeRemoveAmt, 'WillChange and DidChange removeAmt should match'); assert.equal(addAmt, willChangeAddAmt, 'WillChange and DidChange addAmt should match'); @@ -181,8 +188,9 @@ module('unit/many_array - DS.ManyArray', function (hooks) { }); }); } finally { - DS.ManyArray.proto().arrayContentWillChange = originalArrayContentWillChange; - DS.ManyArray.proto().arrayContentDidChange = originalArrayContentDidChange; + TestManyArray.arrayContentWillChange = originalArrayContentWillChange; + TestManyArray.arrayContentDidChange = originalArrayContentDidChange; + TestManyArray.init = originalInit; } }); }); diff --git a/packages/-ember-data/tests/unit/model/relationships/has-many-test.js b/packages/-ember-data/tests/unit/model/relationships/has-many-test.js index 38d1ff4a101..2fecd02697e 100644 --- a/packages/-ember-data/tests/unit/model/relationships/has-many-test.js +++ b/packages/-ember-data/tests/unit/model/relationships/has-many-test.js @@ -1,5 +1,6 @@ import { get, observer } from '@ember/object'; import { run } from '@ember/runloop'; +import settled from '@ember/test-helpers/settled'; import { module, test } from 'qunit'; import { hash, Promise as EmberPromise } from 'rsvp'; @@ -7,7 +8,6 @@ import { hash, Promise as EmberPromise } from 'rsvp'; import DS from 'ember-data'; import { setupTest } from 'ember-qunit'; -import { CUSTOM_MODEL_CLASS } from '@ember-data/canary-features'; import testInDebug from '@ember-data/unpublished-test-infra/test-support/test-in-debug'; import todo from '@ember-data/unpublished-test-infra/test-support/todo'; @@ -2327,7 +2327,7 @@ module('unit/model/relationships - DS.hasMany', function (hooks) { let store = this.owner.lookup('service:store'); let tag = store.createRecord('tag'); - assert.ok(tag.get('people') instanceof DS.PromiseArray, 'people should be an async relationship'); + assert.ok(tag.get('people') instanceof DS.PromiseManyArray, 'people should be an async relationship'); }); test('hasMany is stable', function (assert) { @@ -2379,22 +2379,13 @@ module('unit/model/relationships - DS.hasMany', function (hooks) { return peopleProxy.then((people) => { run(() => { - let isRecordDataBuild = people.recordData !== undefined; tag.unloadRecord(); // TODO Check all unloading behavior assert.false(people.isDestroying, 'people is NOT destroying sync after unloadRecord'); assert.false(people.isDestroyed, 'people is NOT destroyed sync after unloadRecord'); - // unload is not the same as destroy, and we may cancel - // prior to RecordData, this was coupled to the destroy - // of the relationship, which was async and possibly could - // be cancelled were an unload to be aborted. - assert.equal( - peopleProxy.isDestroying, - isRecordDataBuild, - 'peopleProxy is not destroying sync after unloadRecord' - ); - assert.false(peopleProxy.isDestroyed, 'peopleProxy is NOT YET destroyed sync after unloadRecord'); + assert.true(peopleProxy.isDestroying, 'peopleProxy is destroying sync after unloadRecord'); + assert.true(peopleProxy.isDestroyed, 'peopleProxy is destroyed sync after unloadRecord'); }); assert.true(peopleProxy.isDestroying, 'peopleProxy is destroying after the run post unloadRecord'); @@ -2402,9 +2393,9 @@ module('unit/model/relationships - DS.hasMany', function (hooks) { }); }); - test('DS.ManyArray is lazy', function (assert) { + test('DS.ManyArray is lazy', async function (assert) { let peopleDidChange = 0; - let expectedNumberOfChanges = CUSTOM_MODEL_CLASS ? 1 : 2; + let expectedNumberOfChanges = 1; const Tag = DS.Model.extend({ name: DS.attr('string'), people: DS.hasMany('person'), @@ -2428,11 +2419,11 @@ module('unit/model/relationships - DS.hasMany', function (hooks) { //assert.ok(!hasManyRelationship._manyArray); - run(() => { - assert.equal(peopleDidChange, 0, 'expect people hasMany to not emit a change event (before access)'); - tag.get('people'); - assert.equal(peopleDidChange, 0, 'expect people hasMany to not emit a change event (sync after access)'); - }); + assert.equal(peopleDidChange, 0, 'expect people hasMany to not emit a change event (before access)'); + tag.people; // access async relationship + assert.equal(peopleDidChange, 0, 'expect people hasMany to not emit a change event (sync after access)'); + + await settled(); assert.equal( peopleDidChange, @@ -2443,11 +2434,10 @@ module('unit/model/relationships - DS.hasMany', function (hooks) { let person = store.createRecord('person'); - run(() => { - assert.equal(peopleDidChange, 0, 'expect people hasMany to not emit a change event (before access)'); - tag.get('people').addObject(person); - assert.equal(peopleDidChange, expectedNumberOfChanges, 'expect people hasMany to have changed exactly once'); - }); + assert.equal(peopleDidChange, 0, 'expect people hasMany to not emit a change event (before access)'); + const people = await tag.people; + people.addObject(person); + assert.strictEqual(peopleDidChange, expectedNumberOfChanges, 'expect people hasMany to have changed exactly once'); }); test('fetch hasMany loads full relationship after a parent and child have been loaded', function (assert) { diff --git a/packages/-ember-data/tests/unit/promise-proxies-test.js b/packages/-ember-data/tests/unit/promise-proxies-test.js index 16a41494aa8..0dd4a00e63f 100644 --- a/packages/-ember-data/tests/unit/promise-proxies-test.js +++ b/packages/-ember-data/tests/unit/promise-proxies-test.js @@ -12,7 +12,7 @@ import JSONAPISerializer from '@ember-data/serializer/json-api'; module('PromiseManyArray', function () { test('.reload should NOT leak the internal promise, rather return another promiseArray', function (assert) { - assert.expect(2); + assert.expect(1); let content = A(); @@ -24,56 +24,57 @@ module('PromiseManyArray', function () { let reloaded = array.reload(); - assert.ok(reloaded instanceof DS.PromiseManyArray); - - return reloaded.then((value) => assert.equal(content, value)); + assert.ok(reloaded === array); }); - test('.reload should be stable', function (assert) { + test('.reload should be stable', async function (assert) { assert.expect(19); let content = A(); + let array; - content.reload = () => EmberPromise.resolve(content); + content.reload = () => { + let p = EmberPromise.resolve(content); + array._update(p); + return p; + }; let promise = EmberPromise.resolve(content); - let array = DS.PromiseManyArray.create({ + array = DS.PromiseManyArray.create({ promise, }); - assert.false(array.get('isRejected'), 'should NOT be rejected'); - assert.true(array.get('isPending'), 'should be pending'); - assert.false(array.get('isSettled'), 'should NOT be settled'); - assert.false(array.get('isFulfilled'), 'should NOT be fulfilled'); + assert.false(array.isRejected, 'should NOT be rejected'); + assert.true(array.isPending, 'should be pending'); + assert.false(array.isSettled, 'should NOT be settled'); + assert.false(array.isFulfilled, 'should NOT be fulfilled'); - return array.then(() => { - assert.false(array.get('isRejected'), 'should NOT be rejected'); - assert.false(array.get('isPending'), 'should NOT be pending'); - assert.true(array.get('isSettled'), 'should be settled'); - assert.true(array.get('isFulfilled'), 'should be fulfilled'); + await array; + assert.false(array.isRejected, 'should NOT be rejected'); + assert.false(array.isPending, 'should NOT be pending'); + assert.true(array.isSettled, 'should be settled'); + assert.true(array.isFulfilled, 'should be fulfilled'); - let reloaded = array.reload(); + let reloaded = array.reload(); - assert.false(array.get('isRejected'), 'should NOT be rejected'); - assert.true(array.get('isPending'), 'should be pending'); - assert.false(array.get('isSettled'), 'should NOT be settled'); - assert.false(array.get('isFulfilled'), 'should NOT be fulfilled'); + assert.false(array.isRejected, 'should NOT be rejected'); + assert.true(array.isPending, 'should be pending'); + assert.false(array.isSettled, 'should NOT be settled'); + assert.false(array.isFulfilled, 'should NOT be fulfilled'); - assert.ok(reloaded instanceof DS.PromiseManyArray); - assert.equal(reloaded, array); + assert.ok(reloaded instanceof DS.PromiseManyArray); + assert.equal(reloaded, array); - return reloaded.then((value) => { - assert.false(array.get('isRejected'), 'should NOT be rejected'); - assert.false(array.get('isPending'), 'should NOT be pending'); - assert.true(array.get('isSettled'), 'should be settled'); - assert.true(array.get('isFulfilled'), 'should be fulfilled'); + let value = await reloaded; + assert.false(array.isRejected, 'should NOT be rejected'); + assert.false(array.isPending, 'should NOT be pending'); + assert.true(array.isSettled, 'should be settled'); + assert.true(array.isFulfilled, 'should be fulfilled'); - assert.equal(content, value); - }); - }); + assert.equal(content, value); }); - test('.set to new promise should be like reload', function (assert) { + test('.set to new promise should be like reload', async function (assert) { assert.expect(18); let content = A([1, 2, 3]); @@ -84,35 +85,33 @@ module('PromiseManyArray', function () { promise, }); - assert.false(array.get('isRejected'), 'should NOT be rejected'); - assert.true(array.get('isPending'), 'should be pending'); - assert.false(array.get('isSettled'), 'should NOT be settled'); - assert.false(array.get('isFulfilled'), 'should NOT be fulfilled'); + assert.false(array.isRejected, 'should NOT be rejected'); + assert.true(array.isPending, 'should be pending'); + assert.false(array.isSettled, 'should NOT be settled'); + assert.false(array.isFulfilled, 'should NOT be fulfilled'); - return array.then(() => { - assert.false(array.get('isRejected'), 'should NOT be rejected'); - assert.false(array.get('isPending'), 'should NOT be pending'); - assert.true(array.get('isSettled'), 'should be settled'); - assert.true(array.get('isFulfilled'), 'should be fulfilled'); + await array; + assert.false(array.isRejected, 'should NOT be rejected'); + assert.false(array.isPending, 'should NOT be pending'); + assert.true(array.isSettled, 'should be settled'); + assert.true(array.isFulfilled, 'should be fulfilled'); - array.set('promise', EmberPromise.resolve(content)); + array._update(EmberPromise.resolve(content)); - assert.false(array.get('isRejected'), 'should NOT be rejected'); - assert.true(array.get('isPending'), 'should be pending'); - assert.false(array.get('isSettled'), 'should NOT be settled'); - assert.false(array.get('isFulfilled'), 'should NOT be fulfilled'); + assert.false(array.isRejected, 'should NOT be rejected'); + assert.true(array.isPending, 'should be pending'); + assert.false(array.isSettled, 'should NOT be settled'); + assert.false(array.isFulfilled, 'should NOT be fulfilled'); - assert.ok(array instanceof DS.PromiseManyArray); + assert.ok(array instanceof DS.PromiseManyArray); - return array.then((value) => { - assert.false(array.get('isRejected'), 'should NOT be rejected'); - assert.false(array.get('isPending'), 'should NOT be pending'); - assert.true(array.get('isSettled'), 'should be settled'); - assert.true(array.get('isFulfilled'), 'should be fulfilled'); + let value = await array; + assert.false(array.isRejected, 'should NOT be rejected'); + assert.false(array.isPending, 'should NOT be pending'); + assert.true(array.isSettled, 'should be settled'); + assert.true(array.isFulfilled, 'should be fulfilled'); - assert.equal(content, value); - }); - }); + assert.equal(content, value); }); }); diff --git a/packages/model/addon/-private/system/promise-many-array.js b/packages/model/addon/-private/system/promise-many-array.js deleted file mode 100644 index 823c5a4bd08..00000000000 --- a/packages/model/addon/-private/system/promise-many-array.js +++ /dev/null @@ -1,57 +0,0 @@ -import { assert } from '@ember/debug'; -import { get } from '@ember/object'; -import { reads } from '@ember/object/computed'; - -import { Promise } from 'rsvp'; - -import { PromiseArray } from '@ember-data/store/-private'; - -/** - @module @ember-data/model - */ - -/** - A PromiseManyArray is a PromiseArray that also proxies certain method calls - to the underlying manyArray. - Right now we proxy: - - * `reload()` - * `createRecord()` - * `on()` - * `one()` - * `trigger()` - * `off()` - * `has()` - - @class PromiseManyArray - @extends Ember.ArrayProxy - @private -*/ -const PromiseManyArray = PromiseArray.extend({ - links: reads('content.links'), - reload(options) { - assert('You are trying to reload an async manyArray before it has been created', get(this, 'content')); - this.set('promise', this.get('content').reload(options)); - return this; - }, - createRecord: proxyToContent('createRecord'), - on: proxyToContent('on'), - one: proxyToContent('one'), - trigger: proxyToContent('trigger'), - off: proxyToContent('off'), - has: proxyToContent('has'), -}); - -export default PromiseManyArray; - -export function promiseManyArray(promise, label) { - return PromiseManyArray.create({ - promise: Promise.resolve(promise, label), - }); -} - -function proxyToContent(method) { - return function () { - return get(this, 'content')[method](...arguments); - }; -} diff --git a/packages/model/addon/-private/system/promise-many-array.ts b/packages/model/addon/-private/system/promise-many-array.ts new file mode 100644 index 00000000000..ee77a5f416f --- /dev/null +++ b/packages/model/addon/-private/system/promise-many-array.ts @@ -0,0 +1,322 @@ +import { assert } from '@ember/debug'; +import { dependentKeyCompat } from '@ember/object/compat'; +import { tracked } from '@glimmer/tracking'; +import Ember from 'ember'; + +import { resolve } from 'rsvp'; + +import { DEPRECATE_EVENTED_API_USAGE } from '@ember-data/private-build-infra/deprecations'; + +/** + @module @ember-data/model + */ +/** + This class is returned as the result of accessing an async hasMany relationship + on an instance of a Model extending from `@ember-data/model`. + + A PromiseManyArray is an array-like proxy that also proxies certain method calls + to the underlying ManyArray in addition to being "promisified". + + Right now we proxy: + + * `reload()` + * `createRecord()` + + This promise-proxy behavior is primarily to ensure that async relationship interact + nicely with templates. In your JS code you should resolve the promise first. + + ```js + const comments = await post.comments; + ``` + + @class PromiseManyArray + @public +*/ +export default class PromiseManyArray { + declare promise: Promise | null; + declare isDestroyed: boolean; + declare isDestroying: boolean; + + constructor(promise, content) { + this._update(promise, content); + this.isDestroyed = false; + this.isDestroying = false; + } + + //---- Methods/Properties on ArrayProxy that we will keep as our API + + @tracked content: any | null = null; + + /** + * Retrieve the length of the content + * @property length + * @public + */ + @dependentKeyCompat + get length(): number { + return this.content ? this.content.length : 0; + } + + /** + * Iterate the proxied content. Called by the glimmer iterator in #each + * + * @method forEach + * @param cb + * @returns + * @private + */ + forEach(cb) { + if (this.content) { + this.content.forEach(cb); + } + } + + //---- Properties/Methods from the PromiseProxyMixin that we will keep as our API + + /** + * Whether the loading promise is still pending + * + * @property {boolean} isPending + * @public + */ + @tracked isPending: boolean = false; + /** + * Whether the loading promise rejected + * + * @property {boolean} isRejected + * @public + */ + @tracked isRejected: boolean = false; + /** + * Whether the loading promise succeeded + * + * @property {boolean} isFulfilled + * @public + */ + @tracked isFulfilled: boolean = false; + /** + * Whether the loading promise completed (resolved or rejected) + * + * @property {boolean} isSettled + * @public + */ + @tracked isSettled: boolean = false; + + /** + * chain this promise + * + * @method then + * @public + * @param success + * @param fail + * @returns Promise + */ + then(s, f) { + return this.promise!.then(s, f); + } + + /** + * catch errors thrown by this promise + * @method catch + * @public + * @param callback + * @returns Promise + */ + catch(cb) { + return this.promise!.catch(cb); + } + + /** + * run cleanup after this promise completes + * + * @method finally + * @public + * @param callback + * @returns Promise + */ + finally(cb) { + return this.promise!.finally(cb); + } + + //---- Methods on EmberObject that we should keep + + destroy() { + this.isDestroying = true; + this.isDestroyed = true; + this.content = null; + this.promise = null; + } + + //---- Methods/Properties on ManyArray that we own and proxy to + + /** + * Retrieve the links for this relationship + * @property links + * @public + */ + @dependentKeyCompat + get links() { + return this.content ? this.content.links : undefined; + } + + /** + * Retrieve the meta for this relationship + * @property meta + * @public + */ + @dependentKeyCompat + get meta() { + return this.content ? this.content.meta : undefined; + } + + /** + * Reload the relationship + * @method reload + * @public + * @param options + * @returns + */ + reload(options) { + assert('You are trying to reload an async manyArray before it has been created', this.content); + this.content.reload(options); + return this; + } + + //---- Our own stuff + + _update(promise, content) { + if (content !== undefined) { + this.content = content; + } + + this.promise = tapPromise(this, promise); + } + + static create({ promise, content }) { + return new this(promise, content); + } + + // Methods on ManyArray which people should resolve the relationship first before calling + createRecord(...args) { + assert('You are trying to createRecord on an async manyArray before it has been created', this.content); + return this.content.createRecord(...args); + } + + // Properties/Methods on ArrayProxy we should deprecate + + get firstObject() { + return this.content ? this.content.firstObject : undefined; + } + + get lastObject() { + return this.content ? this.content.lastObject : undefined; + } +} + +function tapPromise(proxy, promise) { + proxy.isPending = true; + proxy.isSettled = false; + proxy.isFulfilled = false; + proxy.isRejected = false; + return resolve(promise).then( + (content) => { + proxy.isPending = false; + proxy.isFulfilled = true; + proxy.isSettled = true; + proxy.content = content; + return content; + }, + (error) => { + proxy.isPending = false; + proxy.isFulfilled = false; + proxy.isRejected = true; + proxy.isSettled = true; + throw error; + } + ); +} + +const EmberObjectMethods = [ + 'addObserver', + 'cacheFor', + 'decrementProperty', + 'get', + 'getProperties', + 'incrementProperty', + 'notifyPropertyChange', + 'removeObserver', + 'set', + 'setProperties', + 'toggleProperty', +]; +EmberObjectMethods.forEach((method) => { + PromiseManyArray.prototype[method] = function delegatedMethod(...args) { + return Ember[method](this, ...args); + }; +}); + +const InheritedProxyMethods = [ + 'addArrayObserver', + 'addObject', + 'addObjects', + 'any', + 'arrayContentDidChange', + 'arrayContentWillChange', + 'clear', + 'compact', + 'every', + 'filter', + 'filterBy', + 'find', + 'findBy', + 'getEach', + 'includes', + 'indexOf', + 'insertAt', + 'invoke', + 'isAny', + 'isEvery', + 'lastIndexOf', + 'map', + 'mapBy', + 'objectAt', + 'objectsAt', + 'popObject', + 'pushObject', + 'pushObjects', + 'reduce', + 'reject', + 'rejectBy', + 'removeArrayObserver', + 'removeAt', + 'removeObject', + 'removeObjects', + 'replace', + 'reverseObjects', + 'setEach', + 'setObjects', + 'shiftObject', + 'slice', + 'sortBy', + 'toArray', + 'uniq', + 'uniqBy', + 'unshiftObject', + 'unshiftObjects', + 'without', +]; +InheritedProxyMethods.forEach((method) => { + PromiseManyArray.prototype[method] = function proxiedMethod(...args) { + assert(`Cannot call ${method} before content is assigned.`, this.content); + return this.content[method](...args); + }; +}); + +if (DEPRECATE_EVENTED_API_USAGE) { + ['on', 'has', 'trigger', 'off', 'one'].forEach((method) => { + PromiseManyArray.prototype[method] = function proxiedMethod(...args) { + assert(`Cannot call ${method} before content is assigned.`, this.content); + return this.content[method](...args); + }; + }); +} diff --git a/packages/store/addon/-private/system/model/internal-model.ts b/packages/store/addon/-private/system/model/internal-model.ts index 3830212c0c8..31bbac08270 100644 --- a/packages/store/addon/-private/system/model/internal-model.ts +++ b/packages/store/addon/-private/system/model/internal-model.ts @@ -63,7 +63,7 @@ const { hasOwnProperty } = Object.prototype; let ManyArray: ManyArray; let PromiseBelongsTo: PromiseBelongsTo; -let PromiseManyArray: PromiseManyArray; +let _PromiseManyArray: any; // TODO find a way to get the klass type here let _found = false; let _getModelPackage: () => boolean; @@ -71,8 +71,8 @@ if (HAS_MODEL_PACKAGE) { _getModelPackage = function () { if (!_found) { let modelPackage = require('@ember-data/model/-private'); - ({ ManyArray, PromiseBelongsTo, PromiseManyArray } = modelPackage); - if (ManyArray && PromiseBelongsTo && PromiseManyArray) { + ({ ManyArray, PromiseBelongsTo, PromiseManyArray: _PromiseManyArray } = modelPackage); + if (ManyArray && PromiseBelongsTo && _PromiseManyArray) { _found = true; } } @@ -739,6 +739,14 @@ export default class InternalModel { } ) { let promiseProxy = this._relationshipProxyCache[key]; + if (kind === 'hasMany') { + if (promiseProxy) { + promiseProxy._update(args.promise, args.content); + } else { + promiseProxy = this._relationshipProxyCache[key] = new _PromiseManyArray(args.promise, args.content); + } + return promiseProxy; + } if (promiseProxy) { if (args.content !== undefined) { // this usage of `any` can be removed when `@types/ember_object` proxy allows `null` for content @@ -746,7 +754,7 @@ export default class InternalModel { } promiseProxy.set('promise', args.promise); } else { - const klass = kind === 'hasMany' ? PromiseManyArray : PromiseBelongsTo; + const klass = PromiseBelongsTo; // this usage of `any` can be removed when `@types/ember_object` proxy allows `null` for content this._relationshipProxyCache[key] = klass.create(args as any); } diff --git a/packages/unpublished-relationship-performance-test-app/app/routes/destroy.js b/packages/unpublished-relationship-performance-test-app/app/routes/destroy.js index f5e1be60084..e23e9f5b41d 100644 --- a/packages/unpublished-relationship-performance-test-app/app/routes/destroy.js +++ b/packages/unpublished-relationship-performance-test-app/app/routes/destroy.js @@ -11,16 +11,15 @@ export default Route.extend({ performance.mark('start-push-payload'); const parent = this.store.push(payload); performance.mark('start-destroy-records'); + const children = await parent.children; + const childrenPromise = all( - parent - .get('children') - .toArray() - .map((child) => child.destroyRecord().then(() => run(() => child.unloadRecord()))) + children.map((child) => child.destroyRecord().then(() => run(() => child.unloadRecord()))) ); const parentPromise = parent.destroyRecord().then(() => run(() => parent.unloadRecord())); - return all([childrenPromise, parentPromise]).then(() => { - performance.mark('end-destroy-records'); - }); + await all([childrenPromise, parentPromise]); + + performance.mark('end-destroy-records'); }, }); diff --git a/packages/unpublished-relationship-performance-test-app/app/routes/unload.js b/packages/unpublished-relationship-performance-test-app/app/routes/unload.js index d14b64bdeea..da8ab75815a 100644 --- a/packages/unpublished-relationship-performance-test-app/app/routes/unload.js +++ b/packages/unpublished-relationship-performance-test-app/app/routes/unload.js @@ -8,12 +8,11 @@ export default Route.extend({ performance.mark('start-push-payload'); const parent = this.store.push(payload); performance.mark('start-unload-records'); + const children = await parent.children; + + // runloop to ensure destroy does not escape bounds of the test run(() => { - parent - .get('children') - .toArray() - .forEach((child) => child.unloadRecord()); - parent.unloadRecord(); + children.forEach((child) => child.unloadRecord()); }); performance.mark('end-unload-records'); },