From 9469dcb8a20f58ee25266d85c5bef0ecd5f98fdf Mon Sep 17 00:00:00 2001 From: Chris Thoburn Date: Fri, 15 Apr 2022 14:50:19 -0700 Subject: [PATCH 1/7] port more types work over --- .../record-data/record-data-errors-test.ts | 5 +- .../record-data/record-data-state-test.ts | 5 +- .../record-data/record-data-test.ts | 9 +- .../custom-class-model-test.ts | 9 +- packages/model/addon/-private/errors.ts | 2 +- .../system/{many-array.js => many-array.ts} | 101 +++-- .../-private/system/promise-belongs-to.js | 41 -- .../-private/system/promise-belongs-to.ts | 68 +++ .../-private/system/promise-many-array.ts | 22 +- .../record-data/addon/-private/record-data.ts | 4 +- .../integration/graph/edge-removal/setup.ts | 3 +- .../types/@ember/polyfills/index.d.ts | 13 +- .../store/addon/-private/system/core-store.ts | 29 +- .../-private/system/model/internal-model.ts | 407 +++++++++++------- .../addon/-private/system/record-data-for.ts | 2 + .../-private/system/references/belongs-to.ts | 5 +- .../-private/system/references/has-many.ts | 6 +- .../system/store/internal-model-factory.ts | 13 +- .../system/store/record-data-store-wrapper.ts | 3 +- .../addon/-private/ts-interfaces/ds-model.ts | 22 +- .../-private/ts-interfaces/record-data.ts | 5 +- .../-private/ts-interfaces/record-instance.ts | 4 +- .../addon/-private/ts-interfaces/store.ts | 1 + .../@ember/array/-private/enumerable.d.ts | 2 +- packages/store/types/@ember/array/index.d.ts | 2 +- .../@ember/object/promise-proxy-mixin.d.ts | 34 ++ packages/store/types/@ember/object/proxy.d.ts | 35 ++ 27 files changed, 561 insertions(+), 291 deletions(-) rename packages/model/addon/-private/system/{many-array.js => many-array.ts} (76%) delete mode 100644 packages/model/addon/-private/system/promise-belongs-to.js create mode 100644 packages/model/addon/-private/system/promise-belongs-to.ts create mode 100755 packages/store/types/@ember/object/promise-proxy-mixin.d.ts create mode 100755 packages/store/types/@ember/object/proxy.d.ts diff --git a/packages/-ember-data/tests/integration/record-data/record-data-errors-test.ts b/packages/-ember-data/tests/integration/record-data/record-data-errors-test.ts index 4b141a7d1fe..85e128f1a49 100644 --- a/packages/-ember-data/tests/integration/record-data/record-data-errors-test.ts +++ b/packages/-ember-data/tests/integration/record-data/record-data-errors-test.ts @@ -42,7 +42,10 @@ class TestRecordData implements RecordData { // Use correct interface once imports have been fix _storeWrapper: any; - pushData(data, calculateChange?: boolean) {} + pushData(data: object, calculateChange: true): string[]; + pushData(data: object, calculateChange?: false): void; + pushData(data: object, calculateChange?: boolean): string[] | void {} + clientDidCreate() {} willCommit() {} diff --git a/packages/-ember-data/tests/integration/record-data/record-data-state-test.ts b/packages/-ember-data/tests/integration/record-data/record-data-state-test.ts index 7cae5e9a9b2..f19c73dc377 100644 --- a/packages/-ember-data/tests/integration/record-data/record-data-state-test.ts +++ b/packages/-ember-data/tests/integration/record-data/record-data-state-test.ts @@ -41,7 +41,10 @@ class TestRecordData implements RecordData { // Use correct interface once imports have been fix _storeWrapper: any; - pushData(data, calculateChange?: boolean) {} + pushData(data: object, calculateChange: true): string[]; + pushData(data: object, calculateChange?: false): void; + pushData(data: object, calculateChange?: boolean): string[] | void {} + clientDidCreate() {} willCommit() {} diff --git a/packages/-ember-data/tests/integration/record-data/record-data-test.ts b/packages/-ember-data/tests/integration/record-data/record-data-test.ts index 3b046b4cad5..543f3676245 100644 --- a/packages/-ember-data/tests/integration/record-data/record-data-test.ts +++ b/packages/-ember-data/tests/integration/record-data/record-data-test.ts @@ -35,7 +35,10 @@ class TestRecordData { // Use correct interface once imports have been fix _storeWrapper: any; - pushData(data, calculateChange?: boolean) {} + pushData(data: object, calculateChange: true): string[]; + pushData(data: object, calculateChange?: false): void; + pushData(data: object, calculateChange?: boolean): string[] | void {} + clientDidCreate() {} willCommit() {} @@ -208,7 +211,9 @@ module('integration/record-data - Custom RecordData Implementations', function ( let isNew = false; class LifecycleRecordData extends TestRecordData { - pushData(data, calculateChange?: boolean) { + pushData(data: object, calculateChange: true): string[]; + pushData(data: object, calculateChange?: false): void; + pushData(data: object, calculateChange?: boolean): string[] | void { calledPush++; } diff --git a/packages/-ember-data/tests/unit/custom-class-support/custom-class-model-test.ts b/packages/-ember-data/tests/unit/custom-class-support/custom-class-model-test.ts index 13c2e66259d..cbb37e4421d 100644 --- a/packages/-ember-data/tests/unit/custom-class-support/custom-class-model-test.ts +++ b/packages/-ember-data/tests/unit/custom-class-support/custom-class-model-test.ts @@ -17,6 +17,7 @@ import type { AttributesSchema, RelationshipsSchema, } from '@ember-data/store/-private/ts-interfaces/record-data-schemas'; +import { RecordInstance } from '@ember-data/store/-private/ts-interfaces/record-instance'; import type { SchemaDefinitionService } from '@ember-data/store/-private/ts-interfaces/schema-definition-service'; module('unit/model - Custom Class Model', function (hooks) { @@ -25,8 +26,10 @@ module('unit/model - Custom Class Model', function (hooks) { constructor(public store: Store) { this.store = store; } - save() { - return this.store.saveRecord(this); + // these types aren't correct but we don't have a registry to help + // make them correct yet + save(): Promise { + return this.store.saveRecord(this as unknown as RecordInstance); } } @@ -287,7 +290,7 @@ module('unit/model - Custom Class Model', function (hooks) { }; store.registerSchemaDefinitionService(schema); let person = store.createRecord('person', { name: 'chris' }); - await person.save(); + await (person as unknown as Person).save(); }); test('hasModelFor with custom schema definition', async function (assert) { diff --git a/packages/model/addon/-private/errors.ts b/packages/model/addon/-private/errors.ts index cbfa57c547f..90d8791122d 100644 --- a/packages/model/addon/-private/errors.ts +++ b/packages/model/addon/-private/errors.ts @@ -9,7 +9,7 @@ type ValidationError = { message: string; }; /** - @module @ember-data/store + @module @ember-data/model */ interface ArrayProxyWithCustomOverrides extends Omit, 'clear' | 'content'> { // Omit causes `content` to be merged with the class def for ArrayProxy diff --git a/packages/model/addon/-private/system/many-array.js b/packages/model/addon/-private/system/many-array.ts similarity index 76% rename from packages/model/addon/-private/system/many-array.js rename to packages/model/addon/-private/system/many-array.ts index 0e7fa2d2e24..2c3e0c949ba 100644 --- a/packages/model/addon/-private/system/many-array.js +++ b/packages/model/addon/-private/system/many-array.ts @@ -8,10 +8,36 @@ import EmberObject, { get } from '@ember/object'; import { all } from 'rsvp'; +import type { RelationshipRecordData } from '@ember-data/record-data/-private/ts-interfaces/relationship-record-data'; +import type { InternalModel } from '@ember-data/store/-private'; import { PromiseArray, recordDataFor } from '@ember-data/store/-private'; +import type CoreStore from '@ember-data/store/-private/system/core-store'; +import type { CreateRecordProperties } from '@ember-data/store/-private/system/core-store'; +import ShimModelClass from '@ember-data/store/-private/system/model/shim-model-class'; +import type { DSModelSchema } from '@ember-data/store/-private/ts-interfaces/ds-model'; +import type { Links, PaginationLinks } from '@ember-data/store/-private/ts-interfaces/ember-data-json-api'; +import type { RecordInstance } from '@ember-data/store/-private/ts-interfaces/record-instance'; +import type { Dict } from '@ember-data/store/-private/ts-interfaces/utils'; import diffArray from './diff-array'; +interface MutableArrayWithObject extends EmberObject, MutableArray {} +const MutableArrayWithObject = EmberObject.extend(MutableArray) as unknown as new < + T, + M = T +>() => MutableArrayWithObject; + +export interface ManyArrayCreateArgs { + store: CoreStore; + type: ShimModelClass; + recordData: RelationshipRecordData; + key: string; + isPolymorphic: boolean; + isAsync: boolean; + _inverseIsAsync: boolean; + internalModel: InternalModel; + isLoaded: boolean; +} /** A `ManyArray` is a `MutableArray` that represents the contents of a has-many relationship. @@ -57,12 +83,27 @@ import diffArray from './diff-array'; @extends Ember.EmberObject @uses Ember.MutableArray */ -export default EmberObject.extend(MutableArray, { - isAsync: false, - isLoaded: false, +export default class ManyArray extends MutableArrayWithObject { + declare isAsync: boolean; + declare isLoaded: boolean; + declare isPolymorphic: boolean; + declare _isDirty: boolean; + declare _isUpdating: boolean; + declare _hasNotified: boolean; + declare __hasArrayObservers: boolean; + declare hasArrayObservers: boolean; // override the base declaration + declare _length: number; + declare _meta: Dict | null; + declare _links: Links | PaginationLinks | null; + declare currentState: InternalModel[]; + declare recordData: RelationshipRecordData; + declare internalModel: InternalModel; + declare store: CoreStore; + declare key: string; + declare type: DSModelSchema; init() { - this._super(...arguments); + super.init(); /** The loading state of this array @@ -71,6 +112,7 @@ export default EmberObject.extend(MutableArray, { @public */ this.isLoaded = this.isLoaded || false; + this.isAsync = this.isAsync || false; this._length = 0; @@ -158,12 +200,13 @@ export default EmberObject.extend(MutableArray, { // make sure we initialize to the correct state // since the user has already accessed this.retrieveLatest(); - }, + } // TODO refactor away _hasArrayObservers for tests get _hasArrayObservers() { + // cast necessary because hasArrayObservers is typed as a ComputedProperty vs a boolean; return this.hasArrayObservers || this.__hasArrayObservers; - }, + } notify() { this._isDirty = true; @@ -175,7 +218,7 @@ export default EmberObject.extend(MutableArray, { this.notifyPropertyChange('firstObject'); this.notifyPropertyChange('lastObject'); } - }, + } get length() { if (this._isDirty) { @@ -185,11 +228,11 @@ export default EmberObject.extend(MutableArray, { get(this, '[]'); return this._length; - }, + } set length(value) { this._length = value; - }, + } get links() { get(this, '[]'); @@ -197,10 +240,10 @@ export default EmberObject.extend(MutableArray, { this.retrieveLatest(); } return this._links; - }, + } set links(v) { this._links = v; - }, + } get meta() { get(this, '[]'); @@ -208,12 +251,12 @@ export default EmberObject.extend(MutableArray, { this.retrieveLatest(); } return this._meta; - }, + } set meta(v) { this._meta = v; - }, + } - objectAt(index) { + objectAt(index: number): RecordInstance | undefined { if (this._isDirty) { this.retrieveLatest(); } @@ -223,12 +266,12 @@ export default EmberObject.extend(MutableArray, { } return internalModel.getRecord(); - }, + } - replace(idx, amt, objects) { + replace(idx: number, amt: number, objects?: RecordInstance[]) { assert(`Cannot push mutations to the cache while updating the relationship from cache`, !this._isUpdating); this.store._backburner.join(() => { - let internalModels; + let internalModels: InternalModel[]; if (amt > 0) { internalModels = this.currentState.slice(idx, idx + amt); this.recordData.removeFromHasMany( @@ -243,13 +286,13 @@ export default EmberObject.extend(MutableArray, { ); this.recordData.addToHasMany( this.key, - objects.map((obj) => recordDataFor(obj)), + objects.map((obj: RecordInstance) => recordDataFor(obj)), idx ); } this.notify(); }); - }, + } retrieveLatest() { // It’s possible the parent side of the relationship may have been destroyed by this point @@ -260,7 +303,7 @@ export default EmberObject.extend(MutableArray, { this._isUpdating = true; let jsonApi = this.recordData.getHasMany(this.key); - let internalModels = []; + let internalModels: InternalModel[] = []; if (jsonApi.data) { for (let i = 0; i < jsonApi.data.length; i++) { let im = this.store._internalModelForResource(jsonApi.data[i]); @@ -298,7 +341,7 @@ export default EmberObject.extend(MutableArray, { } this._isUpdating = false; - }, + } /** Reloads all of the records in the manyArray. If the manyArray @@ -324,8 +367,8 @@ export default EmberObject.extend(MutableArray, { */ reload(options) { // TODO this is odd, we don't ask the store for anything else like this? - return this.store.reloadManyArray(this, this.internalModel, this.key, options); - }, + return this.internalModel.reloadHasMany(this.key, options); + } /** Saves all of the records in the `ManyArray`. @@ -347,7 +390,7 @@ export default EmberObject.extend(MutableArray, { */ save() { let manyArray = this; - let promiseLabel = 'DS: ManyArray#save ' + this.type; + let promiseLabel = 'DS: ManyArray#save ' + this.type.modelName; let promise = all(this.invoke('save'), promiseLabel).then( () => manyArray, null, @@ -356,7 +399,7 @@ export default EmberObject.extend(MutableArray, { // TODO deprecate returning a promiseArray here return PromiseArray.create({ promise }); - }, + } /** Create a child record within the owner @@ -366,12 +409,12 @@ export default EmberObject.extend(MutableArray, { @param {Object} hash @return {Model} record */ - createRecord(hash) { + createRecord(hash: CreateRecordProperties): RecordInstance { const { store, type } = this; - let record = store.createRecord(type.modelName, hash); + const record = store.createRecord(type.modelName, hash); this.pushObject(record); return record; - }, -}); + } +} diff --git a/packages/model/addon/-private/system/promise-belongs-to.js b/packages/model/addon/-private/system/promise-belongs-to.js deleted file mode 100644 index b1eade8688f..00000000000 --- a/packages/model/addon/-private/system/promise-belongs-to.js +++ /dev/null @@ -1,41 +0,0 @@ -import { assert } from '@ember/debug'; -import { computed } from '@ember/object'; - -import { PromiseObject } from '@ember-data/store/-private'; - -/** - @module @ember-data/model - */ - -/** - A PromiseBelongsTo is a PromiseObject that also proxies certain method calls - to the underlying belongsTo model. - Right now we proxy: - - * `reload()` - - @class PromiseBelongsTo - @extends PromiseObject - @private -*/ -const PromiseBelongsTo = PromiseObject.extend({ - // we don't proxy meta because we would need to proxy it to the relationship state container - // however, meta on relationships does not trigger change notifications. - // if you need relationship meta, you should do `record.belongsTo(relationshipName).meta()` - meta: computed(function () { - assert( - 'You attempted to access meta on the promise for the async belongsTo relationship ' + - `${this.get('_belongsToState').modelName}:${this.get('_belongsToState').key}'.` + - '\nUse `record.belongsTo(relationshipName).meta()` instead.', - false - ); - }), - - reload(options) { - assert('You are trying to reload an async belongsTo before it has been created', this.get('content') !== undefined); - let { key, store, originatingInternalModel } = this._belongsToState; - return store.reloadBelongsTo(this, originatingInternalModel, key, options).then(() => this); - }, -}); - -export default PromiseBelongsTo; diff --git a/packages/model/addon/-private/system/promise-belongs-to.ts b/packages/model/addon/-private/system/promise-belongs-to.ts new file mode 100644 index 00000000000..b05c6a0863d --- /dev/null +++ b/packages/model/addon/-private/system/promise-belongs-to.ts @@ -0,0 +1,68 @@ +import { assert } from '@ember/debug'; +import { computed } from '@ember/object'; +import type PromiseProxyMixin from '@ember/object/promise-proxy-mixin'; +import type ObjectProxy from '@ember/object/proxy'; + +import type { InternalModel } from '@ember-data/store/-private'; +import { PromiseObject } from '@ember-data/store/-private'; +import type CoreStore from '@ember-data/store/-private/system/core-store'; +import type { RecordInstance } from '@ember-data/store/-private/ts-interfaces/record-instance'; +import type { Dict } from '@ember-data/store/-private/ts-interfaces/utils'; + +export interface BelongsToProxyMeta { + key: string; + store: CoreStore; + originatingInternalModel: InternalModel; + modelName: string; +} +export interface BelongsToProxyCreateArgs { + promise: Promise; + content?: RecordInstance | null; + _belongsToState: BelongsToProxyMeta; +} + +interface PromiseObjectType extends PromiseProxyMixin, ObjectProxy { + new (...args: unknown[]): PromiseObjectType; +} +// eslint-disable-next-line @typescript-eslint/no-unused-vars +declare class PromiseObjectType {} + +const Extended: PromiseObjectType = PromiseObject as unknown as PromiseObjectType; + +/** + @module @ember-data/model + */ + +/** + A PromiseBelongsTo is a PromiseObject that also proxies certain method calls + to the underlying belongsTo model. + Right now we proxy: + * `reload()` + @class PromiseBelongsTo + @extends PromiseObject + @private +*/ +class PromiseBelongsTo extends Extended { + declare _belongsToState: BelongsToProxyMeta; + // we don't proxy meta because we would need to proxy it to the relationship state container + // however, meta on relationships does not trigger change notifications. + // if you need relationship meta, you should do `record.belongsTo(relationshipName).meta()` + @computed() + get meta(): void { + return assert( + 'You attempted to access meta on the promise for the async belongsTo relationship ' + + `${this.get('_belongsToState').modelName}:${this.get('_belongsToState').key}'.` + + '\nUse `record.belongsTo(relationshipName).meta()` instead.', + false + ); + } + + async reload(options: Dict): Promise { + assert('You are trying to reload an async belongsTo before it has been created', this.content !== undefined); + let { key, store, originatingInternalModel } = this._belongsToState; + await store.reloadBelongsTo(this, originatingInternalModel, key, options); + return this; + } +} + +export default PromiseBelongsTo; diff --git a/packages/model/addon/-private/system/promise-many-array.ts b/packages/model/addon/-private/system/promise-many-array.ts index c2831738cf6..54452d6e20c 100644 --- a/packages/model/addon/-private/system/promise-many-array.ts +++ b/packages/model/addon/-private/system/promise-many-array.ts @@ -1,4 +1,5 @@ import ArrayMixin from '@ember/array'; +import type ArrayProxy from '@ember/array/proxy'; import { assert } from '@ember/debug'; import { dependentKeyCompat } from '@ember/object/compat'; import { tracked } from '@glimmer/tracking'; @@ -6,6 +7,16 @@ import Ember from 'ember'; import { resolve } from 'rsvp'; +import type { ManyArray } from 'ember-data/-private'; + +import type { InternalModel } from '@ember-data/store/-private'; +import type { RecordInstance } from '@ember-data/store/-private/ts-interfaces/record-instance'; + +export interface HasManyProxyCreateArgs { + promise: Promise; + content?: ManyArray; +} + /** @module @ember-data/model */ @@ -31,12 +42,13 @@ import { resolve } from 'rsvp'; @class PromiseManyArray @public */ +export default interface PromiseManyArray extends Omit, 'destroy'> {} export default class PromiseManyArray { - declare promise: Promise | null; + declare promise: Promise | null; declare isDestroyed: boolean; declare isDestroying: boolean; - constructor(promise, content) { + constructor(promise: Promise, content?: ManyArray) { this._update(promise, content); this.isDestroyed = false; this.isDestroying = false; @@ -204,7 +216,7 @@ export default class PromiseManyArray { //---- Our own stuff - _update(promise, content) { + _update(promise: Promise, content?: ManyArray) { if (content !== undefined) { this.content = content; } @@ -212,7 +224,7 @@ export default class PromiseManyArray { this.promise = tapPromise(this, promise); } - static create({ promise, content }) { + static create({ promise, content }: HasManyProxyCreateArgs): PromiseManyArray { return new this(promise, content); } @@ -233,7 +245,7 @@ export default class PromiseManyArray { } } -function tapPromise(proxy, promise) { +function tapPromise(proxy: PromiseManyArray, promise: Promise) { proxy.isPending = true; proxy.isSettled = false; proxy.isFulfilled = false; diff --git a/packages/record-data/addon/-private/record-data.ts b/packages/record-data/addon/-private/record-data.ts index 648d148db75..eb4d3ae6d35 100644 --- a/packages/record-data/addon/-private/record-data.ts +++ b/packages/record-data/addon/-private/record-data.ts @@ -84,7 +84,9 @@ export default class RecordDataDefault implements RelationshipRecordData { return this.identifier; } - pushData(data: JsonApiResource, calculateChange: boolean) { + pushData(data: JsonApiResource, calculateChange: true): string[]; + pushData(data: JsonApiResource, calculateChange?: false): void; + pushData(data: JsonApiResource, calculateChange?: boolean): string[] | void { let changedKeys; if (this._isNew) { diff --git a/packages/record-data/tests/integration/graph/edge-removal/setup.ts b/packages/record-data/tests/integration/graph/edge-removal/setup.ts index 6990d996473..7a5c7f4954a 100644 --- a/packages/record-data/tests/integration/graph/edge-removal/setup.ts +++ b/packages/record-data/tests/integration/graph/edge-removal/setup.ts @@ -16,6 +16,7 @@ import type { SingleResourceDocument, } from '@ember-data/store/-private/ts-interfaces/ember-data-json-api'; import type { StableRecordIdentifier } from '@ember-data/store/-private/ts-interfaces/identifier'; +import { RecordInstance } from '@ember-data/store/-private/ts-interfaces/record-instance'; import type { Dict } from '@ember-data/store/-private/ts-interfaces/utils'; class AbstractMap { @@ -144,7 +145,7 @@ export interface Context { owner: any; } -interface TestStore extends CoreStore { +interface TestStore extends CoreStore { push(data: EmptyResourceDocument): null; push(data: SingleResourceDocument): T; push(data: CollectionResourceDocument): T[]; diff --git a/packages/record-data/types/@ember/polyfills/index.d.ts b/packages/record-data/types/@ember/polyfills/index.d.ts index 318e000b48a..cac10f55811 100644 --- a/packages/record-data/types/@ember/polyfills/index.d.ts +++ b/packages/record-data/types/@ember/polyfills/index.d.ts @@ -2,15 +2,4 @@ * Copy properties from a source object to a target object. * https://github.com/DefinitelyTyped/DefinitelyTyped/issues/38681 */ -export function assign(target: T, source: U): Mix; -export function assign( - target: T, - source1: U, - source2: V -): Mix3; -export function assign( - target: T, - source1: U, - source2: V, - source3: W -): Mix4; +export const assign = Object.assign; diff --git a/packages/store/addon/-private/system/core-store.ts b/packages/store/addon/-private/system/core-store.ts index b15b616b1f9..efcab4c9df0 100644 --- a/packages/store/addon/-private/system/core-store.ts +++ b/packages/store/addon/-private/system/core-store.ts @@ -17,11 +17,7 @@ import { importSync } from '@embroider/macros'; import { all, default as RSVP, Promise, resolve } from 'rsvp'; import { HAS_RECORD_DATA_PACKAGE } from '@ember-data/private-build-infra'; -import type { - BelongsToRelationship, - ManyRelationship, - RecordData as RecordDataClass, -} from '@ember-data/record-data/-private'; +import type { ManyRelationship, RecordData as RecordDataClass } from '@ember-data/record-data/-private'; import type { RelationshipState } from '@ember-data/record-data/-private/graph/-state'; import { IdentifierCache } from '../identifiers/cache'; @@ -97,6 +93,11 @@ function freeze(obj: T): T { return obj; } +export interface CreateRecordProperties { + id?: string | null; + [key: string]: unknown; +} + /** The store contains all of the data for records loaded from the server. It is also responsible for creating instances of `Model` that wrap @@ -490,7 +491,7 @@ abstract class CoreStore extends Service { newly created record. @return {Model} record */ - createRecord(modelName, inputProperties) { + createRecord(modelName: string, inputProperties: CreateRecordProperties): RecordInstance { if (DEBUG) { assertDestroyingStore(this, 'createRecord'); } @@ -1365,7 +1366,7 @@ abstract class CoreStore extends Service { @param options optional to include adapterOptions @return {Promise} promise */ - _reloadRecord(internalModel, options): RSVP.Promise { + _reloadRecord(internalModel: InternalModel, options: FindOptions): RSVP.Promise { options.isReloading = true; let { id, modelName } = internalModel; let adapter = this.adapterFor(modelName); @@ -1503,12 +1504,12 @@ abstract class CoreStore extends Service { _findHasManyByJsonApiResource( resource, parentInternalModel: InternalModel, - relationship: ManyRelationship | BelongsToRelationship, - options: any - ): RSVP.Promise { + relationship: ManyRelationship, + options?: Dict + ): Promise { if (HAS_RECORD_DATA_PACKAGE) { if (!resource) { - return resolve([]); + return resolve(); } const { definition, state } = relationship; let adapter = this.adapterFor(definition.type); @@ -1553,7 +1554,7 @@ abstract class CoreStore extends Service { // we were explicitly told we have no data and no links. // TODO if the relationshipIsStale, should we hit the adapter anyway? - return resolve([]); + return resolve(); } assert(`hasMany only works with the @ember-data/record-data package`); } @@ -2818,10 +2819,6 @@ abstract class CoreStore extends Service { serializer.pushPayload(this, payload); } - reloadManyArray(manyArray, internalModel, key, options) { - return internalModel.reloadHasMany(key, options); - } - reloadBelongsTo(belongsToProxy, internalModel, key, options) { return internalModel.reloadBelongsTo(key, options); } diff --git a/packages/store/addon/-private/system/model/internal-model.ts b/packages/store/addon/-private/system/model/internal-model.ts index 7d7606c53c5..d68ff4251b2 100644 --- a/packages/store/addon/-private/system/model/internal-model.ts +++ b/packages/store/addon/-private/system/model/internal-model.ts @@ -6,8 +6,18 @@ import { _backburner as emberBackburner, cancel, run } from '@ember/runloop'; import { DEBUG } from '@glimmer/env'; import { importSync } from '@embroider/macros'; -import RSVP, { Promise } from 'rsvp'; +import RSVP, { resolve } from 'rsvp'; +import type { ManyArray } from '@ember-data/model/-private'; +import RecordState from '@ember-data/model/-private/record-state'; +import type { ManyArrayCreateArgs } from '@ember-data/model/-private/system/many-array'; +import type { + BelongsToProxyCreateArgs, + BelongsToProxyMeta, +} from '@ember-data/model/-private/system/promise-belongs-to'; +import type PromiseBelongsTo from '@ember-data/model/-private/system/promise-belongs-to'; +import type { HasManyProxyCreateArgs } from '@ember-data/model/-private/system/promise-many-array'; +import type PromiseManyArray from '@ember-data/model/-private/system/promise-many-array'; import { HAS_MODEL_PACKAGE, HAS_RECORD_DATA_PACKAGE } from '@ember-data/private-build-infra'; import type { BelongsToRelationship, @@ -15,16 +25,21 @@ import type { RecordData as DefaultRecordData, } from '@ember-data/record-data/-private'; import type { UpgradedMeta } from '@ember-data/record-data/-private/graph/-edge-definition'; +import type { + DefaultSingleResourceRelationship, + RelationshipRecordData, +} from '@ember-data/record-data/-private/ts-interfaces/relationship-record-data'; -import { DSModel } from '../../ts-interfaces/ds-model'; +import type { DSModel } from '../../ts-interfaces/ds-model'; import type { StableRecordIdentifier } from '../../ts-interfaces/identifier'; -import type { RecordData } from '../../ts-interfaces/record-data'; +import type { ChangedAttributesHash, RecordData } from '../../ts-interfaces/record-data'; import type { JsonApiResource, JsonApiValidationError } from '../../ts-interfaces/record-data-json-api'; +import type { RelationshipSchema } from '../../ts-interfaces/record-data-schemas'; import type { RecordInstance } from '../../ts-interfaces/record-instance'; import type { FindOptions } from '../../ts-interfaces/store'; -import type { ConfidentDict } from '../../ts-interfaces/utils'; +import type { Dict } from '../../ts-interfaces/utils'; import type CoreStore from '../core-store'; -import type Store from '../ds-model-store'; +import type { CreateRecordProperties } from '../core-store'; import { errorsHashToArray } from '../errors-utils'; import recordDataFor from '../record-data-for'; import { BelongsToReference, HasManyReference, RecordReference } from '../references'; @@ -32,10 +47,11 @@ import Snapshot from '../snapshot'; import { internalModelFactoryFor } from '../store/internal-model-factory'; import RootState from './states'; -// move to TS hacks module that we can delete when this is no longer a necessary recast -type ManyArray = InstanceType; -type PromiseBelongsTo = InstanceType; -type PromiseManyArray = InstanceType; +type PrivateModelModule = { + ManyArray: { create(args: ManyArrayCreateArgs): ManyArray }; + PromiseBelongsTo: { create(args: BelongsToProxyCreateArgs): PromiseBelongsTo }; + PromiseManyArray: new (...args: unknown[]) => PromiseManyArray; +}; /** @module @ember-data/store @@ -43,18 +59,22 @@ type PromiseManyArray = InstanceType boolean; if (HAS_MODEL_PACKAGE) { _getModelPackage = function () { if (!_found) { - let modelPackage = importSync('@ember-data/model/-private') as typeof import('@ember-data/model/-private'); - ({ ManyArray, PromiseBelongsTo, PromiseManyArray: _PromiseManyArray } = modelPackage); - if (ManyArray && PromiseBelongsTo && _PromiseManyArray) { + let modelPackage = importSync('@ember-data/model/-private') as PrivateModelModule; + ({ + ManyArray: _ManyArray, + PromiseBelongsTo: _PromiseBelongsTo, + PromiseManyArray: _PromiseManyArray, + } = modelPackage); + if (_ManyArray && _PromiseBelongsTo && _PromiseManyArray) { _found = true; } } @@ -62,13 +82,6 @@ if (HAS_MODEL_PACKAGE) { }; } -interface BelongsToMetaWrapper { - key: string; - store: CoreStore; - originatingInternalModel: InternalModel; - modelName: string; -} - /* The TransitionChainMap caches the `state.enters`, `state.setups`, and final state reached when transitioning from one state to another, so that future transitions can replay the @@ -79,18 +92,32 @@ interface BelongsToMetaWrapper { and setups. It may also be faster to do a two level cache (from: { to }) instead of caching based on a key that adds the two together. */ +// TODO before deleting the state machine we should +// ensure all things in this map were properly accounted for. +// in the RecordState class. const TransitionChainMap = Object.create(null); const _extractPivotNameCache = Object.create(null); const _splitOnDotCache = Object.create(null); -function splitOnDot(name) { +function splitOnDot(name: string): string[] { return _splitOnDotCache[name] || (_splitOnDotCache[name] = name.split('.')); } -function extractPivotName(name) { +function extractPivotName(name: string): string { return _extractPivotNameCache[name] || (_extractPivotNameCache[name] = splitOnDot(name)[0]); } + +function isDSModel(record: RecordInstance | null): record is DSModel { + return ( + HAS_MODEL_PACKAGE && + !!record && + 'constructor' in record && + 'isModel' in record.constructor && + record.constructor.isModel === true + ); +} + export default class InternalModel { declare _id: string | null; declare modelName: string; @@ -107,25 +134,29 @@ export default class InternalModel { // Not typed yet declare _promiseProxy: any; - declare _record: any; + declare _record: RecordInstance | null; declare _scheduledDestroy: any; declare _modelClass: any; declare _deferredTriggers: any; declare __recordArrays: any; declare references: any; declare _recordReference: RecordReference; - declare _manyArrayCache: ConfidentDict; + declare _manyArrayCache: Dict; - declare _relationshipPromisesCache: ConfidentDict>; - declare _relationshipProxyCache: ConfidentDict; + declare _relationshipPromisesCache: Dict>; + declare _relationshipProxyCache: Dict; declare error: any; - declare currentState: any; + declare currentState: RecordState; declare _previousState: any; + declare store: CoreStore; + declare identifier: StableRecordIdentifier; - constructor(public store: CoreStore | Store, public identifier: StableRecordIdentifier) { + constructor(store: CoreStore, identifier: StableRecordIdentifier) { if (HAS_MODEL_PACKAGE) { _getModelPackage(); } + this.store = store; + this.identifier = identifier; this._id = identifier.id; this._isUpdatingId = false; this.modelName = identifier.type; @@ -257,15 +288,28 @@ export default class InternalModel { } } - getRecord(properties?): Object { - if (!this._record && !this._isDematerializing) { + getRecord(properties?: CreateRecordProperties): RecordInstance { + let record = this._record; + + if (this._isDematerializing) { + // TODO we should assert here instead of this return. + return null as unknown as RecordInstance; + } + + if (!record) { let { store } = this; - this._record = store._instantiateRecord(this, this.modelName, this._recordData, this.identifier, properties); + record = this._record = store._instantiateRecord( + this, + this.modelName, + this._recordData, + this.identifier, + properties + ); this._triggerDeferredTriggers(); } - return this._record; + return record; } dematerializeRecord() { @@ -289,9 +333,11 @@ export default class InternalModel { }); if (this._record) { - Object.keys(this._relationshipProxyCache).forEach((key) => { - if (this._relationshipProxyCache[key].destroy) { - this._relationshipProxyCache[key].destroy(); + let keys = Object.keys(this._relationshipProxyCache); + keys.forEach((key) => { + let proxy = this._relationshipProxyCache[key]!; + if (proxy.destroy) { + proxy.destroy(); } delete this._relationshipProxyCache[key]; }); @@ -325,9 +371,9 @@ export default class InternalModel { }); } - save(options): Promise { + save(options: FindOptions = {}): Promise { if (this._deletedRecordWasNew) { - return Promise.resolve(); + return resolve(); } let promiseLabel = 'DS: Model#save ' + this; let resolver = RSVP.defer(promiseLabel); @@ -336,22 +382,8 @@ export default class InternalModel { return this.store.scheduleSave(this, resolver, options) as Promise; } - reload(options) { - if (!options) { - options = {}; - } - let internalModel = this; - - return internalModel.store._reloadRecord(internalModel, options).then( - function () { - //TODO NOW seems like we shouldn't need to do this - return internalModel; - }, - function (error) { - throw error; - }, - 'DS: Model#reload complete, update flags' - ); + reload(options: Dict = {}): Promise { + return this.store._reloadRecord(this, options); } /* @@ -426,26 +458,32 @@ export default class InternalModel { } } - _findBelongsTo(key: string, resource, relationshipMeta, options): Promise { + _findBelongsTo( + key: string, + resource: DefaultSingleResourceRelationship, + relationshipMeta: RelationshipSchema, + options?: Dict + ): Promise { // TODO @runspired follow up if parent isNew then we should not be attempting load here + // TODO @runspired follow up on whether this should be in the relationship requests cache return this.store._findBelongsToByJsonApiResource(resource, this, relationshipMeta, options).then( - (internalModel) => handleCompletedRelationshipRequest(this, key, resource._relationship, internalModel, null), + (internalModel) => handleCompletedRelationshipRequest(this, key, resource._relationship, internalModel), (e) => handleCompletedRelationshipRequest(this, key, resource._relationship, null, e) ); } - getBelongsTo(key, options) { + getBelongsTo(key: string, options?: Dict): PromiseBelongsTo | RecordInstance | null { let resource = (this._recordData as DefaultRecordData).getBelongsTo(key); let identifier = resource && resource.data ? this.store.identifierCache.getOrCreateRecordIdentifier(resource.data) : null; let relationshipMeta = this.store._relationshipMetaFor(this.modelName, null, key); - if (!relationshipMeta) return; + assert(`Attempted to access a belongsTo relationship but no definition exists for it`, relationshipMeta); let store = this.store; let parentInternalModel = this; let async = relationshipMeta.options.async; let isAsync = typeof async === 'undefined' ? true : async; - let _belongsToState: BelongsToMetaWrapper = { + let _belongsToState: BelongsToProxyMeta = { key, store, originatingInternalModel: this, @@ -456,7 +494,7 @@ export default class InternalModel { let internalModel = identifier !== null ? store._internalModelForResource(identifier) : null; if (resource._relationship.state.hasFailedLoadAttempt) { - return this._relationshipProxyCache[key]; + return this._relationshipProxyCache[key] as PromiseBelongsTo; } let promise = this._findBelongsTo(key, resource, relationshipMeta, options); @@ -480,51 +518,49 @@ export default class InternalModel { "' with id " + parentInternalModel.id + ' but some of the associated records were not loaded. Either make sure they are all loaded together with the parent record, or specify that the relationship is async (`belongsTo({ async: true })`)', - toReturn === null || !(toReturn as DSModel).isEmpty + toReturn === null || !internalModel.currentState.isEmpty ); return toReturn; } } } - getManyArray(key: string, definition?: UpgradedMeta) { - if (HAS_RECORD_DATA_PACKAGE) { - let manyArray = this._manyArrayCache[key]; - if (!definition) { - const graphFor = ( - importSync('@ember-data/record-data/-private') as typeof import('@ember-data/record-data/-private') - ).graphFor; - definition = graphFor(this.store).get(this.identifier, key).definition as UpgradedMeta; - } - - if (!manyArray) { - manyArray = ManyArray.create({ - store: this.store, - type: this.store.modelFor(definition.type), - recordData: this._recordData, - key, - isPolymorphic: definition.isPolymorphic, - isAsync: definition.isAsync, - _inverseIsAsync: definition.inverseIsAsync, - internalModel: this, - isLoaded: !definition.isAsync, - }); - this._manyArrayCache[key] = manyArray; - } - - return manyArray; + getManyArray(key: string, definition?: UpgradedMeta): ManyArray { + assert('hasMany only works with the @ember-data/record-data package', HAS_RECORD_DATA_PACKAGE); + let manyArray: ManyArray | undefined = this._manyArrayCache[key]; + if (!definition) { + const graphFor = ( + importSync('@ember-data/record-data/-private') as typeof import('@ember-data/record-data/-private') + ).graphFor; + definition = graphFor(this.store).get(this.identifier, key).definition as UpgradedMeta; + } + + if (!manyArray) { + manyArray = _ManyArray.create({ + store: this.store, + type: this.store.modelFor(definition.type), + recordData: this._recordData as RelationshipRecordData, + key, + isPolymorphic: definition.isPolymorphic, + isAsync: definition.isAsync, + _inverseIsAsync: definition.inverseIsAsync, + internalModel: this, + isLoaded: !definition.isAsync, + }); + this._manyArrayCache[key] = manyArray; } - assert('hasMany only works with the @ember-data/record-data package'); + + return manyArray; } fetchAsyncHasMany( key: string, - relationship: ManyRelationship | BelongsToRelationship, - manyArray, - options - ): RSVP.Promise { + relationship: ManyRelationship, + manyArray: ManyArray, + options?: Dict + ): Promise { if (HAS_RECORD_DATA_PACKAGE) { - let loadingPromise = this._relationshipPromisesCache[key]; + let loadingPromise = this._relationshipPromisesCache[key] as Promise | undefined; if (loadingPromise) { return loadingPromise; } @@ -532,7 +568,7 @@ export default class InternalModel { const jsonApi = this._recordData.getHasMany(key); loadingPromise = this.store._findHasManyByJsonApiResource(jsonApi, this, relationship, options).then( - () => handleCompletedRelationshipRequest(this, key, relationship, manyArray, null), + () => handleCompletedRelationshipRequest(this, key, relationship, manyArray), (e) => handleCompletedRelationshipRequest(this, key, relationship, manyArray, e) ); this._relationshipPromisesCache[key] = loadingPromise; @@ -541,7 +577,7 @@ export default class InternalModel { assert('hasMany only works with the @ember-data/record-data package'); } - getHasMany(key: string, options?) { + getHasMany(key: string, options?): PromiseManyArray | ManyArray { if (HAS_RECORD_DATA_PACKAGE) { const graphFor = ( importSync('@ember-data/record-data/-private') as typeof import('@ember-data/record-data/-private') @@ -552,7 +588,7 @@ export default class InternalModel { if (definition.isAsync) { if (state.hasFailedLoadAttempt) { - return this._relationshipProxyCache[key]; + return this._relationshipProxyCache[key] as PromiseManyArray; } let promise = this.fetchAsyncHasMany(key, relationship, manyArray, options); @@ -570,40 +606,46 @@ export default class InternalModel { assert(`hasMany only works with the @ember-data/record-data package`); } + _updatePromiseProxyFor(kind: 'hasMany', key: string, args: HasManyProxyCreateArgs): PromiseManyArray; + _updatePromiseProxyFor(kind: 'belongsTo', key: string, args: BelongsToProxyCreateArgs): PromiseBelongsTo; + _updatePromiseProxyFor( + kind: 'belongsTo', + key: string, + args: { promise: Promise } + ): PromiseBelongsTo; _updatePromiseProxyFor( kind: 'hasMany' | 'belongsTo', key: string, - args: { - promise: RSVP.Promise; - content?: RecordInstance | ManyArray | null; - _belongsToState?: BelongsToMetaWrapper; - } - ) { + args: BelongsToProxyCreateArgs | HasManyProxyCreateArgs | { promise: Promise } + ): PromiseBelongsTo | PromiseManyArray { let promiseProxy = this._relationshipProxyCache[key]; if (kind === 'hasMany') { + const { promise, content } = args as HasManyProxyCreateArgs; if (promiseProxy) { - promiseProxy._update(args.promise, args.content); + assert(`Expected a PromiseManyArray`, '_update' in promiseProxy); + promiseProxy._update(promise, content); } else { - promiseProxy = this._relationshipProxyCache[key] = new _PromiseManyArray(args.promise, args.content); + promiseProxy = this._relationshipProxyCache[key] = new _PromiseManyArray(promise, 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 - promiseProxy.set('content', args.content as any); + const { promise, content } = args as BelongsToProxyCreateArgs; + assert(`Expected a PromiseBelongsTo`, '_belongsToState' in promiseProxy); + + if (content !== undefined) { + promiseProxy.set('content', content); } - promiseProxy.set('promise', args.promise); + promiseProxy.set('promise', promise); } else { - 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); + this._relationshipProxyCache[key] = promiseProxy = _PromiseBelongsTo.create(args as any); } - return this._relationshipProxyCache[key]; + return promiseProxy; } - reloadHasMany(key, options) { + reloadHasMany(key: string, options) { if (HAS_RECORD_DATA_PACKAGE) { let loadingPromise = this._relationshipPromisesCache[key]; if (loadingPromise) { @@ -629,8 +671,8 @@ export default class InternalModel { assert(`hasMany only works with the @ember-data/record-data package`); } - reloadBelongsTo(key, options) { - let loadingPromise = this._relationshipPromisesCache[key]; + reloadBelongsTo(key: string, options?: Dict): Promise { + let loadingPromise = this._relationshipPromisesCache[key] as Promise | undefined; if (loadingPromise) { return loadingPromise; } @@ -642,6 +684,7 @@ export default class InternalModel { resource._relationship.state.shouldForceReload = true; } let relationshipMeta = this.store._relationshipMetaFor(this.modelName, null, key); + assert(`Attempted to reload a belongsTo relationship but no definition exists for it`, relationshipMeta); let promise = this._findBelongsTo(key, resource, relationshipMeta, options); if (this._relationshipProxyCache[key]) { return this._updatePromiseProxyFor('belongsTo', key, { promise }); @@ -660,7 +703,7 @@ export default class InternalModel { destroy() { assert( 'Cannot destroy an internalModel while its record is materialized', - !this._record || this._record.get('isDestroyed') || this._record.get('isDestroying') + !this._record || this._record.isDestroyed || this._record.isDestroying ); this.isDestroying = true; if (this._recordReference) { @@ -669,13 +712,13 @@ export default class InternalModel { this._recordReference = null; let cache = this._manyArrayCache; Object.keys(cache).forEach((key) => { - cache[key].destroy(); + cache[key]!.destroy(); delete cache[key]; }); if (this.references) { cache = this.references; Object.keys(cache).forEach((key) => { - cache[key].destroy(); + cache[key]!.destroy(); delete cache[key]; }); } @@ -685,24 +728,35 @@ export default class InternalModel { } setupData(data) { - let changedKeys = this._recordData.pushData(data, this.hasRecord); - if (this.hasRecord) { - // TODO @runspired should this be going through the notification manager? - this._record._notifyProperties(changedKeys); + const hasRecord = this.hasRecord; + if (hasRecord) { + let changedKeys = this._recordData.pushData(data, true); + this.notifyAttributes(changedKeys); + } else { + this._recordData.pushData(data); } this.send('pushedData'); } - setDirtyHasMany(key, records) { + notifyAttributes(keys: string[]): void { + let manager = this.store._notificationManager; + let { identifier } = this; + + for (let i = 0; i < keys.length; i++) { + manager.notify(identifier, 'attributes', keys[i]); + } + } + + setDirtyHasMany(key: string, records) { assertRecordsPassedToHasMany(records); return this._recordData.setDirtyHasMany(key, extractRecordDatasFromRecords(records)); } - setDirtyBelongsTo(key, value) { + setDirtyBelongsTo(key: string, value) { return this._recordData.setDirtyBelongsTo(key, extractRecordDataFromRecord(value)); } - setDirtyAttribute(key, value) { + setDirtyAttribute(key: string, value: T): T { if (this.isDeleted()) { if (DEBUG) { throw new EmberError(`Attempted to set '${key}' to '${value}' on the deleted record ${this}`); @@ -724,11 +778,11 @@ export default class InternalModel { return value; } - get isDestroyed() { + get isDestroyed(): boolean { return this._isDestroyed; } - get hasRecord() { + get hasRecord(): boolean { return !!this._record; } @@ -736,7 +790,7 @@ export default class InternalModel { return new Snapshot(options, this.identifier, this.store); } - hasChangedAttributes() { + hasChangedAttributes(): boolean { if (!this.__recordData) { // no need to calculate changed attributes when calling `findRecord` return false; @@ -744,7 +798,7 @@ export default class InternalModel { return this._recordData.hasChangedAttributes(); } - changedAttributes() { + changedAttributes(): ChangedAttributesHash { if (!this.__recordData) { // no need to calculate changed attributes when calling `findRecord` return {}; @@ -752,16 +806,16 @@ export default class InternalModel { return this._recordData.changedAttributes(); } - adapterWillCommit() { + adapterWillCommit(): void { this._recordData.willCommit(); this.send('willCommit'); } - adapterDidDirty() { + adapterDidDirty(): void { this.send('becomeDirty'); } - send(name, context?) { + send(name: string, context?) { let currentState = this.currentState; if (!currentState[name]) { @@ -792,7 +846,7 @@ export default class InternalModel { } } - notifyPropertyChange(key) { + notifyPropertyChange(key: string) { if (this.hasRecord) { // TODO this should likely *mostly* be the `attributes` bucket // but it seems for local mutations we rely on computed updating @@ -802,7 +856,7 @@ export default class InternalModel { } } - notifyStateChange(key?) { + notifyStateChange(key?: string) { if (this.hasRecord) { this.store._notificationManager.notify(this.identifier, 'state'); } @@ -818,24 +872,24 @@ export default class InternalModel { rollbackAttributes() { this.store._backburner.join(() => { let dirtyKeys = this._recordData.rollbackAttributes(); - if (get(this, 'isError')) { + if (this.isError) { this.didCleanError(); } this.send('rolledBack'); - if (this._record && dirtyKeys && dirtyKeys.length > 0) { - this._record._notifyProperties(dirtyKeys); + if (this.hasRecord && dirtyKeys && dirtyKeys.length > 0) { + this.notifyAttributes(dirtyKeys); } }); } - transitionTo(name) { + transitionTo(name: string) { // POSSIBLE TODO: Remove this code and replace with // always having direct reference to state objects let pivotName = extractPivotName(name); - let state = this.currentState; + let state: any = this.currentState; let transitionMapId = `${state.stateName}->${name}`; do { @@ -880,13 +934,12 @@ export default class InternalModel { } this.currentState = state; - if (this.hasRecord && typeof this._record.notifyPropertyChange === 'function') { - // TODO refactor Model to have all flags pull from the notification manager - // and for currentState.stateName to be constructed from flag state. - // Probably just port this work from ember-m3 - // After that we can eliminate this. + + // isDSModel is the guard we want, but may be too restrictive if + // ember-m3 / ember-data-model-fragments were relying on this still. + if (this.hasRecord && isDSModel(this._record)) { + // TODO eliminate this. this.notifyStateChange('currentState'); - // this._record.notifyPropertyChange('currentState'); } for (i = 0, l = setups.length; i < l; i++) { @@ -894,7 +947,7 @@ export default class InternalModel { } } - _unhandledEvent(state, name, context) { + _unhandledEvent(state, name: string, context) { let errorMessage = 'Attempted to handle event `' + name + '` '; errorMessage += 'on ' + String(this) + ' while in state '; errorMessage += state.stateName + '. '; @@ -922,7 +975,7 @@ export default class InternalModel { return; } let triggers = this._deferredTriggers; - let record = this._record; + let record = this._record as DSModel; let trigger = record.trigger; // TODO Igor make nicer check if (trigger && typeof trigger === 'function') { @@ -1064,26 +1117,34 @@ export default class InternalModel { this.store._notificationManager.notify(this.identifier, 'attributes'); } - hasErrors() { + hasErrors(): boolean { + // TODO add assertion forcing consuming RecordData's to implement getErrors if (this._recordData.getErrors) { return this._recordData.getErrors(this.identifier).length > 0; } else { - let errors = (this.getRecord() as DSModel).errors; + // we can't have errors if we never tried loading + if (!this._record) { + return false; + } + let errors = (this._record as DSModel).errors; return errors.length > 0; } } // FOR USE DURING COMMIT PROCESS - adapterDidInvalidate(parsedErrors, error) { + adapterDidInvalidate(parsedErrors, error?) { // TODO @runspired this should be handled by RecordState // and errors should be dirtied but lazily fetch if at // all possible. We should only notify errors here. let attribute; if (error && parsedErrors) { + // TODO add assertion forcing consuming RecordData's to implement getErrors if (!this._recordData.getErrors) { + let record = this.getRecord() as DSModel; + let errors = record.errors; for (attribute in parsedErrors) { if (hasOwnProperty.call(parsedErrors, attribute)) { - (this.getRecord() as DSModel).errors._add(attribute, parsedErrors[attribute]); + errors._add(attribute, parsedErrors[attribute]); } } } @@ -1154,7 +1215,39 @@ export default class InternalModel { } } -function handleCompletedRelationshipRequest(internalModel, key, relationship, value, error) { +function handleCompletedRelationshipRequest( + internalModel: InternalModel, + key: string, + relationship: BelongsToRelationship, + value: InternalModel | null +): RecordInstance | null; +function handleCompletedRelationshipRequest( + internalModel: InternalModel, + key: string, + relationship: ManyRelationship, + value: ManyArray +): ManyArray; +function handleCompletedRelationshipRequest( + internalModel: InternalModel, + key: string, + relationship: BelongsToRelationship, + value: null, + error: Error +): never; +function handleCompletedRelationshipRequest( + internalModel: InternalModel, + key: string, + relationship: ManyRelationship, + value: ManyArray, + error: Error +): never; +function handleCompletedRelationshipRequest( + internalModel: InternalModel, + key: string, + relationship: BelongsToRelationship | ManyRelationship, + value: ManyArray | InternalModel | null, + error?: Error +): ManyArray | RecordInstance | null { delete internalModel._relationshipPromisesCache[key]; relationship.state.shouldForceReload = false; const isHasMany = relationship.definition.kind === 'hasMany'; @@ -1162,7 +1255,7 @@ function handleCompletedRelationshipRequest(internalModel, key, relationship, va if (isHasMany) { // we don't notify the record property here to avoid refetch // only the many array - value.notify(); + (value as ManyArray).notify(); } if (error) { @@ -1177,7 +1270,9 @@ function handleCompletedRelationshipRequest(internalModel, key, relationship, va // has never been accessed if (proxy && !isHasMany) { if (proxy.content && proxy.content.isDestroying) { - proxy.set('content', null); + // TODO @types/ember__object incorrectly disallows `null`, we should either + // override or fix upstream + (proxy as PromiseBelongsTo).set('content', null as unknown as undefined); } } @@ -1185,14 +1280,14 @@ function handleCompletedRelationshipRequest(internalModel, key, relationship, va } if (isHasMany) { - value.set('isLoaded', true); + (value as ManyArray).set('isLoaded', true); } relationship.state.hasFailedLoadAttempt = false; // only set to not stale if no error is thrown relationship.state.isStale = false; - return isHasMany || !value ? value : value.getRecord(); + return isHasMany || !value ? (value as ManyArray | null) : (value as InternalModel).getRecord(); } export function assertRecordsPassedToHasMany(records) { diff --git a/packages/store/addon/-private/system/record-data-for.ts b/packages/store/addon/-private/system/record-data-for.ts index 84ecb8a5451..eeca0097f6e 100644 --- a/packages/store/addon/-private/system/record-data-for.ts +++ b/packages/store/addon/-private/system/record-data-for.ts @@ -3,6 +3,7 @@ import { DEBUG } from '@glimmer/env'; import type { StableRecordIdentifier } from '../ts-interfaces/identifier'; import type { RecordData } from '../ts-interfaces/record-data'; +import type { RecordInstance } from '../ts-interfaces/record-instance'; import WeakCache from './weak-cache'; /* @@ -40,6 +41,7 @@ export function removeRecordDataFor(identifier: StableRecordIdentifier): void { export default function recordDataFor(instance: StableRecordIdentifier): RecordData | null; export default function recordDataFor(instance: Instance): RecordData; +export default function recordDataFor(instance: RecordInstance): RecordData; export default function recordDataFor(instance: object): null; export default function recordDataFor(instance: Instance | object): RecordData | null { if (RecordDataForIdentifierCache.has(instance as StableRecordIdentifier)) { diff --git a/packages/store/addon/-private/system/references/belongs-to.ts b/packages/store/addon/-private/system/references/belongs-to.ts index aabebb08a81..65ba1dc8352 100644 --- a/packages/store/addon/-private/system/references/belongs-to.ts +++ b/packages/store/addon/-private/system/references/belongs-to.ts @@ -8,6 +8,7 @@ import { assertPolymorphicType } from '@ember-data/store/-debug'; import { SingleResourceDocument } from '../../ts-interfaces/ember-data-json-api'; import { StableRecordIdentifier } from '../../ts-interfaces/identifier'; +import { RecordInstance } from '../../ts-interfaces/record-instance'; import CoreStore from '../core-store'; import { NotificationType, unsubscribe } from '../record-notification-manager'; import { internalModelFactoryFor, recordIdentifierFor } from '../store/internal-model-factory'; @@ -193,7 +194,7 @@ export default class BelongsToReference extends Reference { @param {Object|Promise} objectOrPromise a promise that resolves to a JSONAPI document object describing the new value of this relationship. @return {Promise} A promise that resolves with the new value in this belongs-to relationship. */ - async push(data: SingleResourceDocument | Promise): Promise { + async push(data: SingleResourceDocument | Promise): Promise { const jsonApiDoc = await resolve(data); let record = this.store.push(jsonApiDoc); @@ -267,7 +268,7 @@ export default class BelongsToReference extends Reference { @public @return {Model} the record in this relationship */ - value(): Object | null { + value(): RecordInstance | null { let resource = this._resource(); if (resource && resource.data) { let inverseInternalModel = this.store._internalModelForResource(resource.data); diff --git a/packages/store/addon/-private/system/references/has-many.ts b/packages/store/addon/-private/system/references/has-many.ts index 064ecb3efb2..2539f7b4e64 100644 --- a/packages/store/addon/-private/system/references/has-many.ts +++ b/packages/store/addon/-private/system/references/has-many.ts @@ -4,6 +4,8 @@ import { cached, tracked } from '@glimmer/tracking'; import { resolve } from 'rsvp'; +import { ManyArray } from 'ember-data/-private'; + import type { ManyRelationship } from '@ember-data/record-data/-private'; import { assertPolymorphicType } from '@ember-data/store/-debug'; @@ -250,7 +252,7 @@ export default class HasManyReference extends Reference { */ async push( objectOrPromise: ExistingResourceObject[] | CollectionResourceDocument | { data: SingleResourceDocument[] } - ): Promise { + ): Promise { const payload = await resolve(objectOrPromise); let array: Array; @@ -291,7 +293,7 @@ export default class HasManyReference extends Reference { }); // TODO IGOR it seems wrong that we were returning the many array here - return internalModel.getHasMany(this.key); + return internalModel.getHasMany(this.key) as Promise | ManyArray; // this cast is necessary because typescript does not work properly with custom thenables } _isLoaded() { diff --git a/packages/store/addon/-private/system/store/internal-model-factory.ts b/packages/store/addon/-private/system/store/internal-model-factory.ts index e5c02178630..3ed86ee39e5 100644 --- a/packages/store/addon/-private/system/store/internal-model-factory.ts +++ b/packages/store/addon/-private/system/store/internal-model-factory.ts @@ -9,6 +9,7 @@ import type { ResourceIdentifierObject, } from '../../ts-interfaces/ember-data-json-api'; import type { StableRecordIdentifier } from '../../ts-interfaces/identifier'; +import type { RecordData } from '../../ts-interfaces/record-data'; import type { RecordInstance } from '../../ts-interfaces/record-instance'; import constructResource from '../../utils/construct-resource'; import type CoreStore from '../core-store'; @@ -20,19 +21,19 @@ import WeakCache from '../weak-cache'; /** @module @ember-data/store */ - const FactoryCache = new WeakCache(DEBUG ? 'internal-model-factory' : ''); FactoryCache._generator = (store: CoreStore) => { return new InternalModelFactory(store); }; type NewResourceInfo = { type: string; id: string | null }; -const RecordCache = new WeakCache(DEBUG ? 'identifier' : ''); +const RecordCache = new WeakCache(DEBUG ? 'identifier' : ''); if (DEBUG) { - RecordCache._expectMsg = (key: RecordInstance) => `${key} is not a record instantiated by @ember-data/store`; + RecordCache._expectMsg = (key: RecordInstance | RecordData) => + `${String(key)} is not a record instantiated by @ember-data/store`; } -export function peekRecordIdentifier(record: any): StableRecordIdentifier | undefined { +export function peekRecordIdentifier(record: RecordInstance | RecordData): StableRecordIdentifier | undefined { return RecordCache.get(record); } @@ -60,11 +61,11 @@ export function peekRecordIdentifier(record: any): StableRecordIdentifier | unde @param {Object} record a record instance previously obstained from the store. @returns {StableRecordIdentifier} */ -export function recordIdentifierFor(record: RecordInstance): StableRecordIdentifier { +export function recordIdentifierFor(record: RecordInstance | RecordData): StableRecordIdentifier { return RecordCache.getWithError(record); } -export function setRecordIdentifier(record: RecordInstance, identifier: StableRecordIdentifier): void { +export function setRecordIdentifier(record: RecordInstance | RecordData, identifier: StableRecordIdentifier): void { if (DEBUG && RecordCache.has(record)) { throw new Error(`${record} was already assigned an identifier`); } diff --git a/packages/store/addon/-private/system/store/record-data-store-wrapper.ts b/packages/store/addon/-private/system/store/record-data-store-wrapper.ts index fd06beec41f..a82e5b60683 100644 --- a/packages/store/addon/-private/system/store/record-data-store-wrapper.ts +++ b/packages/store/addon/-private/system/store/record-data-store-wrapper.ts @@ -12,6 +12,7 @@ import type { RelationshipsSchema, } from '../../ts-interfaces/record-data-schemas'; import type { RecordDataStoreWrapper as StoreWrapper } from '../../ts-interfaces/record-data-store-wrapper'; +import { RecordInstance } from '../../ts-interfaces/record-instance'; import constructResource from '../../utils/construct-resource'; import type CoreStore from '../core-store'; import { internalModelFactoryFor } from './internal-model-factory'; @@ -224,7 +225,7 @@ export default class RecordDataStoreWrapper implements StoreWrapper { return false; } - const record = internalModel._record; + const record = internalModel._record as RecordInstance; return record && !(record.isDestroyed || record.isDestroying); } diff --git a/packages/store/addon/-private/ts-interfaces/ds-model.ts b/packages/store/addon/-private/ts-interfaces/ds-model.ts index 6a1a83572db..89739b291af 100644 --- a/packages/store/addon/-private/ts-interfaces/ds-model.ts +++ b/packages/store/addon/-private/ts-interfaces/ds-model.ts @@ -1,15 +1,20 @@ -import EmberObject from '@ember/object'; +import type EmberObject from '@ember/object'; -import RSVP from 'rsvp'; +import type { Errors } from '@ember-data/model/-private'; +import type CoreStore from '../system/core-store'; +import type InternalModel from '../system/model/internal-model'; import type { JsonApiValidationError } from './record-data-json-api'; -import type { AttributeSchema, RelationshipSchema } from './record-data-schemas'; -import { RecordInstance } from './record-instance'; +import type { AttributeSchema, RelationshipSchema, RelationshipsSchema } from './record-data-schemas'; // Placeholder until model.js is typed -export interface DSModel extends RecordInstance, EmberObject { +export interface DSModel extends EmberObject { + constructor: DSModelSchema; + store: CoreStore; + errors: Errors; + _internalModel: InternalModel; toString(): string; - save(): RSVP.Promise; + save(): Promise; eachRelationship(callback: (this: T, key: string, meta: RelationshipSchema) => void, binding?: T): void; eachAttribute(callback: (this: T, key: string, meta: AttributeSchema) => void, binding?: T): void; invalidErrorsChanged(errors: JsonApiValidationError[]): void; @@ -17,7 +22,7 @@ export interface DSModel extends RecordInstance, EmberObject { isDeleted: boolean; deleteRecord(): void; unloadRecord(): void; - errors: any; + _notifyProperties(keys: string[]): void; } // Implemented by both ShimModelClass and DSModel @@ -39,4 +44,7 @@ export interface ModelSchema { // once we can type it. export interface DSModelSchema extends ModelSchema { isModel: true; + relationshipsObject: RelationshipsSchema; + extend(...mixins: unknown[]): DSModelSchema; + reopenClass(...mixins: unknown[]): void; } diff --git a/packages/store/addon/-private/ts-interfaces/record-data.ts b/packages/store/addon/-private/ts-interfaces/record-data.ts index 177debfc62e..b2a19d302d8 100644 --- a/packages/store/addon/-private/ts-interfaces/record-data.ts +++ b/packages/store/addon/-private/ts-interfaces/record-data.ts @@ -12,7 +12,10 @@ export interface ChangedAttributesHash { export interface RecordData { getResourceIdentifier(): RecordIdentifier | undefined; - pushData(data: JsonApiResource, calculateChange?: boolean): void; + + pushData(data: JsonApiResource, calculateChange: true): string[]; + pushData(data: JsonApiResource, calculateChange?: false): void; + pushData(data: JsonApiResource, calculateChange?: boolean): string[] | void; clientDidCreate(): void; willCommit(): void; diff --git a/packages/store/addon/-private/ts-interfaces/record-instance.ts b/packages/store/addon/-private/ts-interfaces/record-instance.ts index cfd4fdaa78c..3acef75cbd3 100644 --- a/packages/store/addon/-private/ts-interfaces/record-instance.ts +++ b/packages/store/addon/-private/ts-interfaces/record-instance.ts @@ -1,3 +1,5 @@ +import type { DSModel } from './ds-model'; +import type { Dict } from './utils'; /** @module @ember-data/store */ @@ -13,4 +15,4 @@ The type belows allows for anything extending object. */ -export type RecordInstance = Object; +export type RecordInstance = DSModel | Dict; diff --git a/packages/store/addon/-private/ts-interfaces/store.ts b/packages/store/addon/-private/ts-interfaces/store.ts index a2c01ab4b29..c3bddf9ba61 100644 --- a/packages/store/addon/-private/ts-interfaces/store.ts +++ b/packages/store/addon/-private/ts-interfaces/store.ts @@ -6,4 +6,5 @@ export interface FindOptions { include?: string; adapterOptions?: Dict; preload?: Dict; + isReloading?: boolean; } diff --git a/packages/store/types/@ember/array/-private/enumerable.d.ts b/packages/store/types/@ember/array/-private/enumerable.d.ts index b5449cf6894..9952c756467 100644 --- a/packages/store/types/@ember/array/-private/enumerable.d.ts +++ b/packages/store/types/@ember/array/-private/enumerable.d.ts @@ -119,7 +119,7 @@ interface Enumerable { * implements it. This method corresponds to the implementation in * Prototype 1.6. */ - invoke(methodName: keyof T, ...args: unknown[]): unknown[]; + invoke(methodName: keyof I, ...args: unknown[]): unknown[]; /** * Simply converts the enumerable into a genuine array. The order is not * guaranteed. Corresponds to the method implemented by Prototype. diff --git a/packages/store/types/@ember/array/index.d.ts b/packages/store/types/@ember/array/index.d.ts index 135f0c77b74..489da26f171 100644 --- a/packages/store/types/@ember/array/index.d.ts +++ b/packages/store/types/@ember/array/index.d.ts @@ -72,7 +72,7 @@ interface Array extends Enumerable { * Becomes true whenever the array currently has observers watching changes * on the array. */ - hasArrayObservers: ComputedProperty; + hasArrayObservers: boolean | ComputedProperty; /** * If you are implementing an object that supports `Ember.Array`, call this * method just before the array content changes to notify any observers and diff --git a/packages/store/types/@ember/object/promise-proxy-mixin.d.ts b/packages/store/types/@ember/object/promise-proxy-mixin.d.ts new file mode 100755 index 00000000000..388f25c9919 --- /dev/null +++ b/packages/store/types/@ember/object/promise-proxy-mixin.d.ts @@ -0,0 +1,34 @@ +import Mixin from '@ember/object/mixin'; + +/** + * A low level mixin making ObjectProxy promise-aware. + */ +interface PromiseProxyMixin extends Promise { + /** + * If the proxied promise is rejected this will contain the reason + * provided. + */ + reason: string | Error; + /** + * Once the proxied promise has settled this will become `false`. + */ + isPending: boolean; + /** + * Once the proxied promise has settled this will become `true`. + */ + isSettled: boolean; + /** + * Will become `true` if the proxied promise is rejected. + */ + isRejected: boolean; + /** + * Will become `true` if the proxied promise is fulfilled. + */ + isFulfilled: boolean; + /** + * The promise whose fulfillment value is being proxied by this object. + */ + promise: Promise; +} +declare const PromiseProxyMixin: Mixin>; +export default PromiseProxyMixin; diff --git a/packages/store/types/@ember/object/proxy.d.ts b/packages/store/types/@ember/object/proxy.d.ts new file mode 100755 index 00000000000..47b8a8ea402 --- /dev/null +++ b/packages/store/types/@ember/object/proxy.d.ts @@ -0,0 +1,35 @@ +import EmberObject from '@ember/object'; +import { + UnwrapComputedPropertyGetter, + UnwrapComputedPropertyGetters, + UnwrapComputedPropertySetters, +} from '@ember/object/-private/types'; + +/** + * `Ember.ObjectProxy` forwards all properties not defined by the proxy itself + * to a proxied `content` object. + */ +export default class ObjectProxy extends EmberObject { + /** + * The object whose properties will be forwarded. + */ + content: T | null | undefined; + + get(key: K): UnwrapComputedPropertyGetter; + get(key: K): UnwrapComputedPropertyGetter | undefined; + + getProperties(list: K[]): Pick, K>; + getProperties(...list: K[]): Pick, K>; + getProperties(list: K[]): Pick>, K>; + getProperties(...list: K[]): Pick>, K>; + + set( + key: K, + value: UnwrapComputedPropertySetters[K] + ): UnwrapComputedPropertySetters[K]; + set(key: K, value: UnwrapComputedPropertySetters[K]): UnwrapComputedPropertySetters[K]; + + setProperties( + hash: Pick, K> + ): Pick, K>; +} From 4f7ea0aebd485c3315e41e4e39576f7180941a94 Mon Sep 17 00:00:00 2001 From: Chris Thoburn Date: Fri, 15 Apr 2022 15:20:36 -0700 Subject: [PATCH 2/7] fix types --- .../node-tests/fixtures/expected.js | 33 +++---- packages/adapter/addon/index.ts | 24 +++-- packages/adapter/addon/json-api.ts | 5 +- packages/adapter/addon/rest.ts | 34 ++++--- .../addon/-private/system/fetch-manager.ts | 1 + .../system/store/serializer-response.ts | 88 +++++++++---------- .../minimum-adapter-interface.ts | 39 ++++---- .../minimum-serializer-interface.ts | 15 +++- 8 files changed, 134 insertions(+), 105 deletions(-) diff --git a/packages/-ember-data/node-tests/fixtures/expected.js b/packages/-ember-data/node-tests/fixtures/expected.js index 6a9a3d7435a..bf831fc9ddc 100644 --- a/packages/-ember-data/node-tests/fixtures/expected.js +++ b/packages/-ember-data/node-tests/fixtures/expected.js @@ -66,14 +66,14 @@ module.exports = { '(private) @ember-data/serializer/rest RESTSerializer#_normalizeArray', '(private) @ember-data/serializer/rest RESTSerializer#_normalizeResponse', '(private) @ember-data/store AdapterPopulatedRecordArray#_setIdentifiers', - '(private) @ember-data/store Errors#_add', - '(private) @ember-data/store Errors#_clear', - '(private) @ember-data/store Errors#_findOrCreateMessages', - '(private) @ember-data/store Errors#_registerHandlers', - '(private) @ember-data/store Errors#_remove', - '(private) @ember-data/store Errors#content', - '(private) @ember-data/store Errors#errorsByAttributeName', - '(private) @ember-data/store Errors#unknownProperty', + '(private) @ember-data/model Errors#_add', + '(private) @ember-data/model Errors#_clear', + '(private) @ember-data/model Errors#_findOrCreateMessages', + '(private) @ember-data/model Errors#_registerHandlers', + '(private) @ember-data/model Errors#_remove', + '(private) @ember-data/model Errors#content', + '(private) @ember-data/model Errors#errorsByAttributeName', + '(private) @ember-data/model Errors#unknownProperty', '(private) @ember-data/store IdentifierCache#__configureMerge', '(private) @ember-data/store IdentifierCache#_getRecordIdentifier', '(private) @ember-data/store IdentifierCache#_mergeRecordIdentifiers', @@ -228,6 +228,7 @@ module.exports = { '(public) @ember-data/serializer MinimumSerializerInterface#pushPayload [OPTIONAL]', '(public) @ember-data/serializer MinimumSerializerInterface#serialize', '(public) @ember-data/serializer MinimumSerializerInterface#serializeIntoHash [OPTIONAL]', + '(public) @ember-data/serializer MinimumSerializerInterface#destroy [OPTIONAL]', '(public) @ember-data/serializer Serializer#normalize', '(public) @ember-data/serializer Serializer#normalizeResponse', '(public) @ember-data/serializer Serializer#serialize', @@ -301,14 +302,14 @@ module.exports = { '(public) @ember-data/store BelongsToReference#push', '(public) @ember-data/store BelongsToReference#reload', '(public) @ember-data/store BelongsToReference#value', - '(public) @ember-data/store Errors#add', - '(public) @ember-data/store Errors#clear', - '(public) @ember-data/store Errors#errorsFor', - '(public) @ember-data/store Errors#has', - '(public) @ember-data/store Errors#isEmpty', - '(public) @ember-data/store Errors#length', - '(public) @ember-data/store Errors#messages', - '(public) @ember-data/store Errors#remove', + '(public) @ember-data/model Errors#add', + '(public) @ember-data/model Errors#clear', + '(public) @ember-data/model Errors#errorsFor', + '(public) @ember-data/model Errors#has', + '(public) @ember-data/model Errors#isEmpty', + '(public) @ember-data/model Errors#length', + '(public) @ember-data/model Errors#messages', + '(public) @ember-data/model Errors#remove', '(public) @ember-data/store HasManyReference#ids', '(public) @ember-data/store HasManyReference#load', '(public) @ember-data/store HasManyReference#push', diff --git a/packages/adapter/addon/index.ts b/packages/adapter/addon/index.ts index a86ab8d9aec..60ee934fb42 100644 --- a/packages/adapter/addon/index.ts +++ b/packages/adapter/addon/index.ts @@ -144,7 +144,10 @@ import type { Snapshot } from '@ember-data/store/-private'; import type Store from '@ember-data/store/-private/system/core-store'; import type ShimModelClass from '@ember-data/store/-private/system/model/shim-model-class'; import type SnapshotRecordArray from '@ember-data/store/-private/system/snapshot-record-array'; -import type MinimumAdapterInterface from '@ember-data/store/-private/ts-interfaces/minimum-adapter-interface'; +import type { + AdapterPayload, + MinimumAdapterInterface, +} from '@ember-data/store/-private/ts-interfaces/minimum-adapter-interface'; import type { Dict } from '@ember-data/store/-private/ts-interfaces/utils'; /** @@ -240,7 +243,7 @@ export default class Adapter extends EmberObject implements MinimumAdapterInterf @return {Promise} promise @public */ - findRecord(store: Store, type: ShimModelClass, id: string, snapshot: Snapshot): Promise { + findRecord(store: Store, type: ShimModelClass, id: string, snapshot: Snapshot): Promise { if (DEBUG) { throw new Error('You subclassed the Adapter class but missing a findRecord override'); } @@ -279,7 +282,12 @@ export default class Adapter extends EmberObject implements MinimumAdapterInterf @return {Promise} promise @public */ - findAll(store: Store, type: ShimModelClass, neverSet, snapshotRecordArray: SnapshotRecordArray): Promise { + findAll( + store: Store, + type: ShimModelClass, + neverSet, + snapshotRecordArray: SnapshotRecordArray + ): Promise { if (DEBUG) { throw new Error('You subclassed the Adapter class but missing a findAll override'); } @@ -319,7 +327,7 @@ export default class Adapter extends EmberObject implements MinimumAdapterInterf @return {Promise} promise @public */ - query(store: Store, type: ShimModelClass, query): Promise { + query(store: Store, type: ShimModelClass, query): Promise { if (DEBUG) { throw new Error('You subclassed the Adapter class but missing a query override'); } @@ -365,7 +373,7 @@ export default class Adapter extends EmberObject implements MinimumAdapterInterf @return {Promise} promise @public */ - queryRecord(store: Store, type: ShimModelClass, query, adapterOptions): Promise { + queryRecord(store: Store, type: ShimModelClass, query, adapterOptions): Promise { if (DEBUG) { throw new Error('You subclassed the Adapter class but missing a queryRecord override'); } @@ -477,7 +485,7 @@ export default class Adapter extends EmberObject implements MinimumAdapterInterf @return {Promise} promise @public */ - createRecord(store: Store, type: ShimModelClass, snapshot: Snapshot): Promise { + createRecord(store: Store, type: ShimModelClass, snapshot: Snapshot): Promise { if (DEBUG) { throw new Error('You subclassed the Adapter class but missing a createRecord override'); } @@ -536,7 +544,7 @@ export default class Adapter extends EmberObject implements MinimumAdapterInterf @return {Promise} promise @public */ - updateRecord(store: Store, type: ShimModelClass, snapshot: Snapshot): Promise { + updateRecord(store: Store, type: ShimModelClass, snapshot: Snapshot): Promise { if (DEBUG) { throw new Error('You subclassed the Adapter class but missing a updateRecord override'); } @@ -587,7 +595,7 @@ export default class Adapter extends EmberObject implements MinimumAdapterInterf @return {Promise} promise @public */ - deleteRecord(store: Store, type: ShimModelClass, snapshot: Snapshot): Promise { + deleteRecord(store: Store, type: ShimModelClass, snapshot: Snapshot): Promise { if (DEBUG) { throw new Error('You subclassed the Adapter class but missing a deleteRecord override'); } diff --git a/packages/adapter/addon/json-api.ts b/packages/adapter/addon/json-api.ts index df7ff2441cf..5def84b16ec 100644 --- a/packages/adapter/addon/json-api.ts +++ b/packages/adapter/addon/json-api.ts @@ -9,6 +9,7 @@ import { pluralize } from 'ember-inflector'; import type Store from '@ember-data/store'; import type ShimModelClass from '@ember-data/store/-private/system/model/shim-model-class'; import type Snapshot from '@ember-data/store/-private/system/snapshot'; +import { AdapterPayload } from '@ember-data/store/-private/ts-interfaces/minimum-adapter-interface'; import { serializeIntoHash } from './-private'; import type { FetchRequestInit, JQueryRequestInit } from './rest'; @@ -242,7 +243,7 @@ class JSONAPIAdapter extends RESTAdapter { this._coalesceFindRequests = value; } - findMany(store: Store, type: ShimModelClass, ids: string[], snapshots: Snapshot[]): Promise { + findMany(store: Store, type: ShimModelClass, ids: string[], snapshots: Snapshot[]): Promise { let url = this.buildURL(type.modelName, ids, snapshots, 'findMany'); return this.ajax(url, 'GET', { data: { filter: { id: ids.join(',') } } }); } @@ -252,7 +253,7 @@ class JSONAPIAdapter extends RESTAdapter { return pluralize(dasherized); } - updateRecord(store: Store, schema: ShimModelClass, snapshot: Snapshot): Promise { + updateRecord(store: Store, schema: ShimModelClass, snapshot: Snapshot): Promise { const data = serializeIntoHash(store, schema, snapshot); const type = snapshot.modelName; const id = snapshot.id; diff --git a/packages/adapter/addon/rest.ts b/packages/adapter/addon/rest.ts index e88e5db7b5f..441f535c897 100644 --- a/packages/adapter/addon/rest.ts +++ b/packages/adapter/addon/rest.ts @@ -13,6 +13,7 @@ import type Store from '@ember-data/store'; import type ShimModelClass from '@ember-data/store/-private/system/model/shim-model-class'; import type Snapshot from '@ember-data/store/-private/system/snapshot'; import type SnapshotRecordArray from '@ember-data/store/-private/system/snapshot-record-array'; +import { AdapterPayload } from '@ember-data/store/-private/ts-interfaces/minimum-adapter-interface'; import type { Dict } from '@ember-data/store/-private/ts-interfaces/utils'; import { determineBodyPromise, fetch, parseResponseHeaders, serializeIntoHash, serializeQueryParams } from './-private'; @@ -562,7 +563,7 @@ class RESTAdapter extends Adapter.extend(BuildURLMixin) { @param {Snapshot} snapshot @return {Promise} promise */ - findRecord(store: Store, type: ShimModelClass, id: string, snapshot: Snapshot): Promise { + findRecord(store: Store, type: ShimModelClass, id: string, snapshot: Snapshot): Promise { let url = this.buildURL(type.modelName, id, snapshot, 'findRecord'); let query: QueryState = this.buildQuery(snapshot); @@ -584,7 +585,12 @@ class RESTAdapter extends Adapter.extend(BuildURLMixin) { @param {SnapshotRecordArray} snapshotRecordArray @return {Promise} promise */ - findAll(store: Store, type: ShimModelClass, sinceToken, snapshotRecordArray: SnapshotRecordArray): Promise { + findAll( + store: Store, + type: ShimModelClass, + sinceToken, + snapshotRecordArray: SnapshotRecordArray + ): Promise { let query: QueryState = this.buildQuery(snapshotRecordArray); let url = this.buildURL(type.modelName, null, snapshotRecordArray, 'findAll'); @@ -615,7 +621,7 @@ class RESTAdapter extends Adapter.extend(BuildURLMixin) { @param {Object} adapterOptions @return {Promise} promise */ - query(store: Store, type: ShimModelClass, query): Promise { + query(store: Store, type: ShimModelClass, query): Promise { let url = this.buildURL(type.modelName, null, null, 'query', query); if (this.sortQueryParams) { @@ -650,7 +656,7 @@ class RESTAdapter extends Adapter.extend(BuildURLMixin) { type: ShimModelClass, query: Dict, adapterOptions: Dict - ): Promise { + ): Promise { let url = this.buildURL(type.modelName, null, null, 'queryRecord', query); if (this.sortQueryParams) { @@ -694,7 +700,7 @@ class RESTAdapter extends Adapter.extend(BuildURLMixin) { @param {Array} snapshots @return {Promise} promise */ - findMany(store: Store, type: ShimModelClass, ids: string[], snapshots: Snapshot[]): Promise { + findMany(store: Store, type: ShimModelClass, ids: string[], snapshots: Snapshot[]): Promise { let url = this.buildURL(type.modelName, ids, snapshots, 'findMany'); return this.ajax(url, 'GET', { data: { ids: ids } }); } @@ -736,7 +742,7 @@ class RESTAdapter extends Adapter.extend(BuildURLMixin) { @param {Object} relationship meta object describing the relationship @return {Promise} promise */ - findHasMany(store: Store, snapshot: Snapshot, url: string, relationship: Dict): Promise { + findHasMany(store: Store, snapshot: Snapshot, url: string, relationship: Dict): Promise { let id = snapshot.id; let type = snapshot.modelName; @@ -786,7 +792,7 @@ class RESTAdapter extends Adapter.extend(BuildURLMixin) { @param {Object} relationship meta object describing the relationship @return {Promise} promise */ - findBelongsTo(store: Store, snapshot: Snapshot, url: string, relationship): Promise { + findBelongsTo(store: Store, snapshot: Snapshot, url: string, relationship): Promise { let id = snapshot.id; let type = snapshot.modelName; @@ -815,7 +821,7 @@ class RESTAdapter extends Adapter.extend(BuildURLMixin) { @param {Snapshot} snapshot @return {Promise} promise */ - createRecord(store: Store, type: ShimModelClass, snapshot: Snapshot): Promise { + createRecord(store: Store, type: ShimModelClass, snapshot: Snapshot): Promise { let url = this.buildURL(type.modelName, null, snapshot, 'createRecord'); const data = serializeIntoHash(store, type, snapshot); @@ -840,7 +846,7 @@ class RESTAdapter extends Adapter.extend(BuildURLMixin) { @param {Snapshot} snapshot @return {Promise} promise */ - updateRecord(store: Store, schema: ShimModelClass, snapshot: Snapshot): Promise { + updateRecord(store: Store, schema: ShimModelClass, snapshot: Snapshot): Promise { const data = serializeIntoHash(store, schema, snapshot, {}); const type = snapshot.modelName; const id = snapshot.id; @@ -862,7 +868,7 @@ class RESTAdapter extends Adapter.extend(BuildURLMixin) { @param {Snapshot} snapshot @return {Promise} promise */ - deleteRecord(store: Store, schema: ShimModelClass, snapshot: Snapshot): Promise { + deleteRecord(store: Store, schema: ShimModelClass, snapshot: Snapshot): Promise { const type = snapshot.modelName; const id = snapshot.id; assert(`Attempted to delete the ${type} record, but the record has no id`, typeof id === 'string' && id.length > 0); @@ -1088,7 +1094,7 @@ class RESTAdapter extends Adapter.extend(BuildURLMixin) { @param {Object} options @return {Promise} promise */ - async ajax(url: string, type: string, options: JQueryAjaxSettings | RequestInit = {}): Promise { + async ajax(url: string, type: string, options: JQueryAjaxSettings | RequestInit = {}): Promise { let adapter = this; let requestData: RequestData = { @@ -1332,7 +1338,7 @@ function ajaxSuccess( payload: Payload, requestData: RequestData, responseData: ResponseData -): Promise { +): Promise { let response; try { response = adapter.handleResponse(responseData.status, responseData.headers, payload, requestData); @@ -1400,7 +1406,7 @@ function fetchSuccessHandler( payload: Payload, response: Response, requestData: RequestData -): Promise { +): Promise { let responseData = fetchResponseData(response); return ajaxSuccess(adapter, payload, requestData, responseData); } @@ -1431,7 +1437,7 @@ function ajaxSuccessHandler( payload: Payload, jqXHR: JQuery.jqXHR, requestData: RequestData -): Promise { +): Promise { let responseData = ajaxResponseData(jqXHR); return ajaxSuccess(adapter, payload, requestData, responseData); } diff --git a/packages/store/addon/-private/system/fetch-manager.ts b/packages/store/addon/-private/system/fetch-manager.ts index 14516dcdb8e..d57fe224d3f 100644 --- a/packages/store/addon/-private/system/fetch-manager.ts +++ b/packages/store/addon/-private/system/fetch-manager.ts @@ -180,6 +180,7 @@ export default class FetchManager { if (error && error.isAdapterError === true && error.code === 'InvalidError') { let parsedErrors = error.errors; + // TODO deprecate extractErrors being called and/or make it part of the public interface if (serializer && typeof serializer.extractErrors === 'function') { parsedErrors = serializer.extractErrors(store, modelClass, error, snapshot.id); } else { diff --git a/packages/store/addon/-private/system/store/serializer-response.ts b/packages/store/addon/-private/system/store/serializer-response.ts index e7b77c4edba..74361887fbe 100644 --- a/packages/store/addon/-private/system/store/serializer-response.ts +++ b/packages/store/addon/-private/system/store/serializer-response.ts @@ -2,6 +2,7 @@ import { assert } from '@ember/debug'; import { DEBUG } from '@glimmer/env'; import { JsonApiDocument } from '../../ts-interfaces/ember-data-json-api'; +import { AdapterPayload } from '../../ts-interfaces/minimum-adapter-interface'; import { MinimumSerializerInterface, RequestType } from '../../ts-interfaces/minimum-serializer-interface'; import CoreStore from '../core-store'; import ShimModelClass from '../model/shim-model-class'; @@ -14,72 +15,71 @@ import ShimModelClass from '../model/shim-model-class'; @internal */ -export function validateDocumentStructure(doc: JsonApiDocument) { - let errors: string[] = []; - if (!doc || typeof doc !== 'object') { - errors.push('Top level of a JSON API document must be an object'); - } else { - if (!('data' in doc) && !('errors' in doc) && !('meta' in doc)) { - errors.push('One or more of the following keys must be present: "data", "errors", "meta".'); +function validateDocumentStructure(doc?: AdapterPayload | JsonApiDocument): asserts doc is JsonApiDocument { + if (DEBUG) { + let errors: string[] = []; + if (!doc || typeof doc !== 'object') { + errors.push('Top level of a JSON API document must be an object'); } else { - if ('data' in doc && 'errors' in doc) { - errors.push('Top level keys "errors" and "data" cannot both be present in a JSON API document'); + if (!('data' in doc) && !('errors' in doc) && !('meta' in doc)) { + errors.push('One or more of the following keys must be present: "data", "errors", "meta".'); + } else { + if ('data' in doc && 'errors' in doc) { + errors.push('Top level keys "errors" and "data" cannot both be present in a JSON API document'); + } } - } - if ('data' in doc) { - if (!(doc.data === null || Array.isArray(doc.data) || typeof doc.data === 'object')) { - errors.push('data must be null, an object, or an array'); + if ('data' in doc) { + if (!(doc.data === null || Array.isArray(doc.data) || typeof doc.data === 'object')) { + errors.push('data must be null, an object, or an array'); + } } - } - if ('meta' in doc) { - if (typeof doc.meta !== 'object') { - errors.push('meta must be an object'); + if ('meta' in doc) { + if (typeof doc.meta !== 'object') { + errors.push('meta must be an object'); + } } - } - if ('errors' in doc) { - if (!Array.isArray(doc.errors)) { - errors.push('errors must be an array'); + if ('errors' in doc) { + if (!Array.isArray(doc.errors)) { + errors.push('errors must be an array'); + } } - } - if ('links' in doc) { - if (typeof doc.links !== 'object') { - errors.push('links must be an object'); + if ('links' in doc) { + if (typeof doc.links !== 'object') { + errors.push('links must be an object'); + } } - } - if ('jsonapi' in doc) { - if (typeof doc.jsonapi !== 'object') { - errors.push('jsonapi must be an object'); + if ('jsonapi' in doc) { + if (typeof doc.jsonapi !== 'object') { + errors.push('jsonapi must be an object'); + } } - } - if ('included' in doc) { - if (typeof doc.included !== 'object') { - errors.push('included must be an array'); + if ('included' in doc) { + if (typeof doc.included !== 'object') { + errors.push('included must be an array'); + } } } - } - return errors; + assert( + `Response must be normalized to a valid JSON API document:\n\t* ${errors.join('\n\t* ')}`, + errors.length === 0 + ); + } } export function normalizeResponseHelper( serializer: MinimumSerializerInterface | null, store: CoreStore, modelClass: ShimModelClass, - payload: unknown, + payload: AdapterPayload, id: string | null, requestType: RequestType ): JsonApiDocument { let normalizedResponse = serializer ? serializer.normalizeResponse(store, modelClass, payload, id, requestType) - : (payload as JsonApiDocument); // we validate this cast below + : payload; - if (DEBUG) { - let validationErrors = validateDocumentStructure(normalizedResponse); - assert( - `Response must be normalized to a valid JSON API document:\n\t* ${validationErrors.join('\n\t* ')}`, - validationErrors.length === 0 - ); - } + validateDocumentStructure(normalizedResponse); return normalizedResponse; } diff --git a/packages/store/addon/-private/ts-interfaces/minimum-adapter-interface.ts b/packages/store/addon/-private/ts-interfaces/minimum-adapter-interface.ts index bfc5c56a47c..6b7ce3f8927 100644 --- a/packages/store/addon/-private/ts-interfaces/minimum-adapter-interface.ts +++ b/packages/store/addon/-private/ts-interfaces/minimum-adapter-interface.ts @@ -1,7 +1,3 @@ -// the above eslint rule checks return types. This is an interface -// and we intend Promise whether it is Native or polyfilled is of -// no consequence. - import type Store from '../system/core-store'; import type AdapterPopulatedRecordArray from '../system/record-arrays/adapter-populated-record-array'; import type Snapshot from '../system/snapshot'; @@ -11,7 +7,12 @@ import type { RelationshipSchema } from './record-data-schemas'; import type { Dict } from './utils'; type Group = Snapshot[]; - +// TODO this should probably just alias unknown +// since in theory a user could pass a blob or a string +// however those deserialization cases are handled +// far easier in the adapter itself and are unlikely +// to be passed to the serializer today. +export type AdapterPayload = Dict | unknown[]; /** * @module @ember-data/adapter */ @@ -26,7 +27,7 @@ type Group = Snapshot[]; @class MinimumAdapterInterface @public */ -interface Adapter { +export interface MinimumAdapterInterface { /** * `adapter.findRecord` takes a request for a resource of a given `type` and `id` combination * and should return a `Promise` which fulfills with data for a single resource matching that @@ -54,7 +55,7 @@ interface Adapter { * @param {Snapshot} snapshot * @return {Promise} a promise resolving with resource data to feed to the associated serializer */ - findRecord(store: Store, schema: ModelSchema, id: string, snapshot: Snapshot): Promise; + findRecord(store: Store, schema: ModelSchema, id: string, snapshot: Snapshot): Promise; /** * `adapter.findAll` takes a request for resources of a given `type` and should return @@ -91,7 +92,7 @@ interface Adapter { schema: ModelSchema, sinceToken: null, snapshotRecordArray: SnapshotRecordArray - ): Promise; + ): Promise; /** * `adapter.query` takes a request for resources of a given `type` and should return @@ -127,10 +128,10 @@ interface Adapter { query( store: Store, schema: ModelSchema, - query: Dict, + query: Dict, recordArray: AdapterPopulatedRecordArray, options: { adapterOptions?: unknown } - ): Promise; + ): Promise; /** * `adapter.queryRecord` takes a request for resource of a given `type` and should return @@ -158,9 +159,9 @@ interface Adapter { queryRecord( store: Store, schema: ModelSchema, - query: Dict, + query: Dict, options: { adapterOptions?: unknown } - ): Promise; + ): Promise; /** * `adapter.createRecord` takes a request to create a resource of a given `type` and should @@ -213,7 +214,7 @@ interface Adapter { * @param {Snapshot} snapshot * @return {Promise} a promise resolving with resource data to feed to the associated serializer */ - createRecord(store: Store, schema: ModelSchema, snapshot: Snapshot): Promise; + createRecord(store: Store, schema: ModelSchema, snapshot: Snapshot): Promise; /** * `adapter.updateRecord` takes a request to update a resource of a given `type` and should @@ -265,7 +266,7 @@ interface Adapter { * the type, attributes and relationships of the primary type associated with the request. * @param {Snapshot} snapshot */ - updateRecord(store: Store, schema: ModelSchema, snapshot: Snapshot): Promise; + updateRecord(store: Store, schema: ModelSchema, snapshot: Snapshot): Promise; /** * `adapter.deleteRecord` takes a request to delete a resource of a given `type` and @@ -293,7 +294,7 @@ interface Adapter { * @param {Snapshot} snapshot A Snapshot containing the record's current data * @return */ - deleteRecord(store: Store, schema: ModelSchema, snapshot: Snapshot): Promise; + deleteRecord(store: Store, schema: ModelSchema, snapshot: Snapshot): Promise; /** * `adapter.findBelongsTo` takes a request to fetch a related resource located at a @@ -330,7 +331,7 @@ interface Adapter { snapshot: Snapshot, relatedLink: string, relationship: RelationshipSchema - ): Promise; + ): Promise; /** * `adapter.findHasMany` takes a request to fetch a related resource collection located @@ -368,7 +369,7 @@ interface Adapter { snapshot: Snapshot, relatedLink: string, relationship: RelationshipSchema - ): Promise; + ): Promise; /** * ⚠️ This Method is only called if `coalesceFindRequests` is `true`. The array passed to it is determined @@ -399,7 +400,7 @@ interface Adapter { * @param {Array} snapshots An array of snapshots of the available data for the resources to fetch * @return {Promise} a promise resolving with resource data to feed to the associated serializer */ - findMany?(store: Store, schema: ModelSchema, ids: string[], snapshots: Snapshot[]): Promise; + findMany?(store: Store, schema: ModelSchema, ids: string[], snapshots: Snapshot[]): Promise; /** * This method provides the ability to generate an ID to assign to a new record whenever `store.createRecord` @@ -581,5 +582,3 @@ interface Adapter { */ destroy?(): void; } - -export default Adapter; diff --git a/packages/store/addon/-private/ts-interfaces/minimum-serializer-interface.ts b/packages/store/addon/-private/ts-interfaces/minimum-serializer-interface.ts index feaceb4e8ee..1d1006466cb 100644 --- a/packages/store/addon/-private/ts-interfaces/minimum-serializer-interface.ts +++ b/packages/store/addon/-private/ts-interfaces/minimum-serializer-interface.ts @@ -4,6 +4,7 @@ import type Store from '../system/core-store'; import type Snapshot from '../system/snapshot'; import type { ModelSchema } from './ds-model'; import type { JsonApiDocument, SingleResourceDocument } from './ember-data-json-api'; +import type { AdapterPayload } from './minimum-adapter-interface'; import type { Dict } from './utils'; export type OptionsHash = Dict; @@ -66,7 +67,7 @@ export interface MinimumSerializerInterface { normalizeResponse( store: Store, schema: ModelSchema, - rawPayload: unknown, + rawPayload: AdapterPayload, id: string | null, requestType: | 'findRecord' @@ -241,4 +242,16 @@ export interface MinimumSerializerInterface { * @returns {void} */ pushPayload?(store: Store, rawPayload: JSONObject): void; + + /** + * In some situations the serializer may need to perform cleanup when destroyed, + * that cleanup can be done in `destroy`. + * + * If not implemented, the store does not inform the serializer of destruction. + * + * @method destroy [OPTIONAL] + * @public + * @optional + */ + destroy?(): void; } From 2ca672ec1d5e2d73998cf99bf1dbb7416ae5ffb3 Mon Sep 17 00:00:00 2001 From: Chris Thoburn Date: Fri, 15 Apr 2022 17:25:17 -0700 Subject: [PATCH 3/7] progress, needs fixes --- .../types/@ember/runloop/index.d.ts | 5 - ...{promise-proxies.js => promise-proxies.ts} | 45 ++- .../-private/system/promise-proxy-base.d.ts | 65 ++++ .../-private/system/promise-proxy-base.js | 7 + ...ray-manager.js => record-array-manager.ts} | 130 ++++---- .../adapter-populated-record-array.js | 95 ------ .../adapter-populated-record-array.ts | 132 ++++++++ .../{record-array.js => record-array.ts} | 169 ++++++----- .../@ember/object/promise-proxy-mixin.d.ts | 4 +- .../@ember/runloop/-private/backburner.d.ts | 31 -- .../store/types/@ember/runloop/index.d.ts | 283 +++++++++++++++++- 11 files changed, 690 insertions(+), 276 deletions(-) delete mode 100644 packages/record-data/types/@ember/runloop/index.d.ts rename packages/store/addon/-private/system/{promise-proxies.js => promise-proxies.ts} (68%) create mode 100644 packages/store/addon/-private/system/promise-proxy-base.d.ts create mode 100644 packages/store/addon/-private/system/promise-proxy-base.js rename packages/store/addon/-private/system/{record-array-manager.js => record-array-manager.ts} (68%) delete mode 100644 packages/store/addon/-private/system/record-arrays/adapter-populated-record-array.js create mode 100644 packages/store/addon/-private/system/record-arrays/adapter-populated-record-array.ts rename packages/store/addon/-private/system/record-arrays/{record-array.js => record-array.ts} (61%) delete mode 100644 packages/store/types/@ember/runloop/-private/backburner.d.ts diff --git a/packages/record-data/types/@ember/runloop/index.d.ts b/packages/record-data/types/@ember/runloop/index.d.ts deleted file mode 100644 index 690db3672cd..00000000000 --- a/packages/record-data/types/@ember/runloop/index.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -// necessary because our "run" is run.backburner -// which we use to avoid autorun triggering for Ember <= 3.4 -// we can drop this and use run directly ~11/1/2019 -export const run: any; -export const _backburner: any; diff --git a/packages/store/addon/-private/system/promise-proxies.js b/packages/store/addon/-private/system/promise-proxies.ts similarity index 68% rename from packages/store/addon/-private/system/promise-proxies.js rename to packages/store/addon/-private/system/promise-proxies.ts index 19cb636d796..5b9fe1d6a74 100644 --- a/packages/store/addon/-private/system/promise-proxies.js +++ b/packages/store/addon/-private/system/promise-proxies.ts @@ -1,10 +1,13 @@ -import ArrayProxy from '@ember/array/proxy'; +import type NativeArray from '@ember/array/-private/native-array'; import { deprecate } from '@ember/debug'; +import type ComputedProperty from '@ember/object/computed'; import { reads } from '@ember/object/computed'; -import PromiseProxyMixin from '@ember/object/promise-proxy-mixin'; -import ObjectProxy from '@ember/object/proxy'; -import { Promise } from 'rsvp'; +import { resolve } from 'rsvp'; + +import { RecordInstance } from '../ts-interfaces/record-instance'; +import type { Dict } from '../ts-interfaces/utils'; +import { PromiseArrayProxy, PromiseObjectProxy } from './promise-proxy-base'; /** @module @ember-data/store @@ -41,9 +44,20 @@ import { Promise } from 'rsvp'; @extends Ember.ArrayProxy @uses Ember.PromiseProxyMixin */ -export const PromiseArray = ArrayProxy.extend(PromiseProxyMixin, { - meta: reads('content.meta'), -}); +interface EmberNativeArrayLike { + length: number | ComputedProperty; + objectAt(idx: number): T | undefined; +} +interface EmberArrayProxyLike { + length: number | ComputedProperty; + objectAtContent(idx: number): T | undefined; +} +type EmberArrayLike = EmberNativeArrayLike | EmberArrayProxyLike; + +export class PromiseArray = NativeArray> extends PromiseArrayProxy { + @reads('content.meta') + declare meta?: Dict; +} /** A `PromiseObject` is an object that acts like both an `EmberObject` @@ -76,18 +90,21 @@ export const PromiseArray = ArrayProxy.extend(PromiseProxyMixin, { @extends Ember.ObjectProxy @uses Ember.PromiseProxyMixin */ -export let PromiseObject = ObjectProxy.extend(PromiseProxyMixin); +export const PromiseObject = PromiseObjectProxy; -export function promiseObject(promise, label) { - return PromiseObject.create({ - promise: Promise.resolve(promise, label), +export function promiseObject(promise: Promise, label: string) { + return PromiseObjectProxy.create({ + promise: resolve(promise, label), }); } -export function promiseArray(promise, label) { +export function promiseArray = NativeArray>( + promise: Promise, + label?: string +): PromiseArray { return PromiseArray.create({ - promise: Promise.resolve(promise, label), - }); + promise: resolve(promise, label), + }) as unknown as PromiseArray; } // constructor is accessed in some internals but not including it in the copyright for the deprecation diff --git a/packages/store/addon/-private/system/promise-proxy-base.d.ts b/packages/store/addon/-private/system/promise-proxy-base.d.ts new file mode 100644 index 00000000000..b08a536de64 --- /dev/null +++ b/packages/store/addon/-private/system/promise-proxy-base.d.ts @@ -0,0 +1,65 @@ +import ArrayProxy from '@ember/array/proxy'; +import ObjectProxy from '@ember/object/proxy'; + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export interface PromiseArrayProxy extends Promise {} +export class PromiseArrayProxy extends ArrayProxy { + declare content: T; + + /** + * If the proxied promise is rejected this will contain the reason + * provided. + */ + reason: string | Error; + /** + * Once the proxied promise has settled this will become `false`. + */ + isPending: boolean; + /** + * Once the proxied promise has settled this will become `true`. + */ + isSettled: boolean; + /** + * Will become `true` if the proxied promise is rejected. + */ + isRejected: boolean; + /** + * Will become `true` if the proxied promise is fulfilled. + */ + isFulfilled: boolean; + /** + * The promise whose fulfillment value is being proxied by this object. + */ + promise: Promise; +} + +export interface PromiseObjectProxy extends Promise {} +export class PromiseObjectProxy extends ObjectProxy { + declare content?: T | null; + + /** + * If the proxied promise is rejected this will contain the reason + * provided. + */ + reason: string | Error; + /** + * Once the proxied promise has settled this will become `false`. + */ + isPending: boolean; + /** + * Once the proxied promise has settled this will become `true`. + */ + isSettled: boolean; + /** + * Will become `true` if the proxied promise is rejected. + */ + isRejected: boolean; + /** + * Will become `true` if the proxied promise is fulfilled. + */ + isFulfilled: boolean; + /** + * The promise whose fulfillment value is being proxied by this object. + */ + promise: Promise; +} diff --git a/packages/store/addon/-private/system/promise-proxy-base.js b/packages/store/addon/-private/system/promise-proxy-base.js new file mode 100644 index 00000000000..04c15dc2b55 --- /dev/null +++ b/packages/store/addon/-private/system/promise-proxy-base.js @@ -0,0 +1,7 @@ +import ArrayProxy from '@ember/array/proxy'; +import PromiseProxyMixin from '@ember/object/promise-proxy-mixin'; +import ObjectProxy from '@ember/object/proxy'; + +export const PromiseArrayProxy = ArrayProxy.extend(PromiseProxyMixin); + +export const PromiseObjectProxy = ObjectProxy.extend(PromiseProxyMixin); diff --git a/packages/store/addon/-private/system/record-array-manager.js b/packages/store/addon/-private/system/record-array-manager.ts similarity index 68% rename from packages/store/addon/-private/system/record-array-manager.js rename to packages/store/addon/-private/system/record-array-manager.ts index e7f6c6a189b..4ef25a5d6dc 100644 --- a/packages/store/addon/-private/system/record-array-manager.js +++ b/packages/store/addon/-private/system/record-array-manager.ts @@ -4,29 +4,35 @@ import { A } from '@ember/array'; import { assert } from '@ember/debug'; -import { get, set } from '@ember/object'; +import { set } from '@ember/object'; import { _backburner as emberBackburner } from '@ember/runloop'; import { DEBUG } from '@glimmer/env'; +import type { InternalModel } from 'ember-data/-private'; + import isStableIdentifier from '../identifiers/is-stable-identifier'; +import type { CollectionResourceDocument, Meta } from '../ts-interfaces/ember-data-json-api'; +import type { StableRecordIdentifier } from '../ts-interfaces/identifier'; +import type { Dict } from '../ts-interfaces/utils'; +import type CoreStore from './core-store'; import { AdapterPopulatedRecordArray, RecordArray } from './record-arrays'; import { internalModelFactoryFor } from './store/internal-model-factory'; import WeakCache from './weak-cache'; -const RecordArraysCache = new WeakCache(DEBUG ? 'record-arrays' : ''); +const RecordArraysCache = new WeakCache>(DEBUG ? 'record-arrays' : ''); RecordArraysCache._generator = () => new Set(); -export function recordArraysForIdentifier(identifierOrInternalModel) { - return RecordArraysCache.lookup(identifierOrInternalModel); +export function recordArraysForIdentifier(identifier: StableRecordIdentifier): Set { + return RecordArraysCache.lookup(identifier); } -const pendingForIdentifier = new Set([]); +const pendingForIdentifier: Set = new Set([]); -function getIdentifier(identifierOrInternalModel) { +function getIdentifier(identifierOrInternalModel: StableRecordIdentifier | InternalModel): StableRecordIdentifier { let i = identifierOrInternalModel; if (!isStableIdentifier(identifierOrInternalModel)) { // identifier may actually be an internalModel // but during materialization we will get an identifier that - // has already been removed from the identifiers cache yet + // has already been removed from the identifiers cache // so it will not behave as if stable. This is a bug we should fix. i = identifierOrInternalModel.identifier || i; } @@ -34,7 +40,7 @@ function getIdentifier(identifierOrInternalModel) { return i; } -function shouldIncludeInRecordArrays(store, identifier) { +function shouldIncludeInRecordArrays(store: CoreStore, identifier: StableRecordIdentifier): boolean { const cache = internalModelFactoryFor(store); const internalModel = cache.peek(identifier); @@ -49,12 +55,19 @@ function shouldIncludeInRecordArrays(store, identifier) { @internal */ class RecordArrayManager { - constructor(options) { + declare store: CoreStore; + declare isDestroying: boolean; + declare isDestroyed: boolean; + declare _liveRecordArrays: Dict; + declare _pendingIdentifiers: Dict; + declare _adapterPopulatedRecordArrays: RecordArray[]; + + constructor(options: { store: CoreStore }) { this.store = options.store; this.isDestroying = false; this.isDestroyed = false; - this._liveRecordArrays = Object.create(null); - this._pendingIdentifiers = Object.create(null); + this._liveRecordArrays = Object.create(null) as Dict; + this._pendingIdentifiers = Object.create(null) as Dict; this._adapterPopulatedRecordArrays = []; } @@ -64,15 +77,15 @@ class RecordArrayManager { * @param {StableIdentifier} param * @return {RecordArray} array */ - getRecordArraysForIdentifier(identifier) { + getRecordArraysForIdentifier(identifier: StableRecordIdentifier): Set { return recordArraysForIdentifier(identifier); } - _flushPendingIdentifiersForModelName(modelName, identifiers) { + _flushPendingIdentifiersForModelName(modelName: string, identifiers: StableRecordIdentifier[]): void { if (this.isDestroying || this.isDestroyed) { return; } - let modelsToRemove = []; + let identifiersToRemove: StableRecordIdentifier[] = []; for (let j = 0; j < identifiers.length; j++) { let i = identifiers[j]; @@ -82,7 +95,7 @@ class RecordArrayManager { // build up a set of models to ensure we have purged correctly; let isIncluded = shouldIncludeInRecordArrays(this.store, i); if (!isIncluded) { - modelsToRemove.push(i); + identifiersToRemove.push(i); } } @@ -94,30 +107,33 @@ class RecordArrayManager { } // process adapterPopulatedRecordArrays - if (modelsToRemove.length > 0) { - removeFromAdapterPopulatedRecordArrays(this.store, modelsToRemove); + if (identifiersToRemove.length > 0) { + removeFromAdapterPopulatedRecordArrays(this.store, identifiersToRemove); } } _flush() { let pending = this._pendingIdentifiers; - this._pendingIdentifiers = Object.create(null); + this._pendingIdentifiers = Object.create(null) as Dict; for (let modelName in pending) { - this._flushPendingIdentifiersForModelName(modelName, pending[modelName]); + this._flushPendingIdentifiersForModelName(modelName, pending[modelName]!); } } - _syncLiveRecordArray(array, modelName) { + _syncLiveRecordArray(array: RecordArray, modelName: string) { assert( `recordArrayManger.syncLiveRecordArray expects modelName not modelClass as the second param`, typeof modelName === 'string' ); let pending = this._pendingIdentifiers[modelName]; - let hasPendingChanges = Array.isArray(pending); - let hasNoPotentialDeletions = !hasPendingChanges || pending.length === 0; + + if (!Array.isArray(pending)) { + return; + } + let hasNoPotentialDeletions = pending.length === 0; let map = internalModelFactoryFor(this.store).modelMapFor(modelName); - let hasNoInsertionsOrRemovals = get(map, 'length') === get(array, 'length'); + let hasNoInsertionsOrRemovals = map.length === array.length; /* Ideally the recordArrayManager has knowledge of the changes to be applied to @@ -129,28 +145,26 @@ class RecordArrayManager { return; } - if (hasPendingChanges) { - this._flushPendingIdentifiersForModelName(modelName, pending); - delete this._pendingIdentifiers[modelName]; - } + this._flushPendingIdentifiersForModelName(modelName, pending); + delete this._pendingIdentifiers[modelName]; let identifiers = this._visibleIdentifiersByType(modelName); - let modelsToAdd = []; + let identifiersToAdd: StableRecordIdentifier[] = []; for (let i = 0; i < identifiers.length; i++) { let identifier = identifiers[i]; let recordArrays = recordArraysForIdentifier(identifier); if (recordArrays.has(array) === false) { recordArrays.add(array); - modelsToAdd.push(identifier); + identifiersToAdd.push(identifier); } } - if (modelsToAdd.length) { - array._pushIdentifiers(modelsToAdd); + if (identifiersToAdd.length) { + array._pushIdentifiers(identifiersToAdd); } } - _didUpdateAll(modelName) { + _didUpdateAll(modelName: string): void { let recordArray = this._liveRecordArrays[modelName]; if (recordArray) { set(recordArray, 'isUpdating', false); @@ -166,7 +180,7 @@ class RecordArrayManager { @param {String} modelName @return {RecordArray} */ - liveRecordArrayFor(modelName) { + liveRecordArrayFor(modelName: string): RecordArray { assert( `recordArrayManger.liveRecordArrayFor expects modelName not modelClass as the param`, typeof modelName === 'string' @@ -188,9 +202,9 @@ class RecordArrayManager { return array; } - _visibleIdentifiersByType(modelName) { + _visibleIdentifiersByType(modelName: string) { let all = internalModelFactoryFor(this.store).modelMapFor(modelName).recordIdentifiers; - let visible = []; + let visible: StableRecordIdentifier[] = []; for (let i = 0; i < all.length; i++) { let identifier = all[i]; let shouldInclude = shouldIncludeInRecordArrays(this.store, identifier); @@ -211,7 +225,7 @@ class RecordArrayManager { @param {Array} [identifiers] @return {RecordArray} */ - createRecordArray(modelName, identifiers) { + createRecordArray(modelName: string, identifiers: StableRecordIdentifier[] = []): RecordArray { assert( `recordArrayManger.createRecordArray expects modelName not modelClass as the param`, typeof modelName === 'string' @@ -241,13 +255,18 @@ class RecordArrayManager { @param {Object} query @return {AdapterPopulatedRecordArray} */ - createAdapterPopulatedRecordArray(modelName, query, identifiers, payload) { + createAdapterPopulatedRecordArray( + modelName: string, + query: Dict | undefined, + identifiers: StableRecordIdentifier[], + payload?: CollectionResourceDocument + ): AdapterPopulatedRecordArray { assert( `recordArrayManger.createAdapterPopulatedRecordArray expects modelName not modelClass as the first param, received ${modelName}`, typeof modelName === 'string' ); - let array; + let array: AdapterPopulatedRecordArray; if (Array.isArray(identifiers)) { array = AdapterPopulatedRecordArray.create({ modelName, @@ -257,8 +276,11 @@ class RecordArrayManager { manager: this, isLoaded: true, isUpdating: false, - meta: { ...payload.meta }, - links: { ...payload.links }, + // TODO this assign kills the root reference but a deep-copy would be required + // for both meta and links to actually not be by-ref. We whould likely change + // this to a dev-only deep-freeze. + meta: Object.assign({} as Meta, payload!.meta), + links: Object.assign({}, payload!.links), }); this._associateWithRecordArray(identifiers, array); @@ -285,7 +307,7 @@ class RecordArrayManager { @internal @param {RecordArray} array */ - unregisterRecordArray(array) { + unregisterRecordArray(array: RecordArray): void { let modelName = array.modelName; // remove from adapter populated record array @@ -308,7 +330,7 @@ class RecordArrayManager { * @param {StableIdentifier} identifiers * @param {RecordArray} array */ - _associateWithRecordArray(identifiers, array) { + _associateWithRecordArray(identifiers: StableRecordIdentifier[], array: RecordArray): void { for (let i = 0, l = identifiers.length; i < l; i++) { let identifier = identifiers[i]; identifier = getIdentifier(identifier); @@ -321,7 +343,7 @@ class RecordArrayManager { @method recordDidChange @internal */ - recordDidChange(identifier) { + recordDidChange(identifier: StableRecordIdentifier): void { if (this.isDestroying || this.isDestroyed) { return; } @@ -340,22 +362,26 @@ class RecordArrayManager { return; } + // TODO do we still need this schedule? + // eslint-disable-next-line @typescript-eslint/unbound-method emberBackburner.schedule('actions', this, this._flush); } willDestroy() { - Object.keys(this._liveRecordArrays).forEach((modelName) => this._liveRecordArrays[modelName].destroy()); + Object.keys(this._liveRecordArrays).forEach((modelName) => this._liveRecordArrays[modelName]!.destroy()); this._adapterPopulatedRecordArrays.forEach((entry) => entry.destroy()); this.isDestroyed = true; } destroy() { this.isDestroying = true; + // TODO do we still need this schedule? + // eslint-disable-next-line @typescript-eslint/unbound-method emberBackburner.schedule('actions', this, this.willDestroy); } } -function removeFromArray(array, item) { +function removeFromArray(array: RecordArray[], item: RecordArray): boolean { let index = array.indexOf(item); if (index !== -1) { @@ -366,9 +392,13 @@ function removeFromArray(array, item) { return false; } -function updateLiveRecordArray(store, recordArray, identifiers) { - let identifiersToAdd = []; - let identifiersToRemove = []; +function updateLiveRecordArray( + store: CoreStore, + recordArray: RecordArray, + identifiers: StableRecordIdentifier[] +): void { + let identifiersToAdd: StableRecordIdentifier[] = []; + let identifiersToRemove: StableRecordIdentifier[] = []; for (let i = 0; i < identifiers.length; i++) { let identifier = identifiers[i]; @@ -396,13 +426,13 @@ function updateLiveRecordArray(store, recordArray, identifiers) { } } -function removeFromAdapterPopulatedRecordArrays(store, identifiers) { +function removeFromAdapterPopulatedRecordArrays(store: CoreStore, identifiers: StableRecordIdentifier[]): void { for (let i = 0; i < identifiers.length; i++) { removeFromAll(store, identifiers[i]); } } -function removeFromAll(store, identifier) { +function removeFromAll(store: CoreStore, identifier: StableRecordIdentifier): void { identifier = getIdentifier(identifier); const recordArrays = recordArraysForIdentifier(identifier); diff --git a/packages/store/addon/-private/system/record-arrays/adapter-populated-record-array.js b/packages/store/addon/-private/system/record-arrays/adapter-populated-record-array.js deleted file mode 100644 index 0cf7dc0c83a..00000000000 --- a/packages/store/addon/-private/system/record-arrays/adapter-populated-record-array.js +++ /dev/null @@ -1,95 +0,0 @@ -import { A } from '@ember/array'; -import { get } from '@ember/object'; - -import RecordArray from './record-array'; - -/** - @module @ember-data/store -*/ - -/** - Represents an ordered list of records whose order and membership is - determined by the adapter. For example, a query sent to the adapter - may trigger a search on the server, whose results would be loaded - into an instance of the `AdapterPopulatedRecordArray`. - - This class should not be imported and instantiated by consuming applications. - - --- - - If you want to update the array and get the latest records from the - adapter, you can invoke [`update()`](AdapterPopulatedRecordArray/methods/update?anchor=update): - - Example - - ```javascript - // GET /users?isAdmin=true - store.query('user', { isAdmin: true }).then(function(admins) { - - admins.then(function() { - console.log(admins.get("length")); // 42 - }); - - // somewhere later in the app code, when new admins have been created - // in the meantime - // - // GET /users?isAdmin=true - admins.update().then(function() { - admins.get('isUpdating'); // false - console.log(admins.get("length")); // 123 - }); - - admins.get('isUpdating'); // true - } - ``` - - @class AdapterPopulatedRecordArray - @public - @extends RecordArray -*/ -export default RecordArray.extend({ - init() { - this.set('content', this.get('content') || A()); - - this._super(...arguments); - this.query = this.query || null; - this.links = this.links || null; - }, - - replace() { - throw new Error(`The result of a server query (on ${this.modelName}) is immutable.`); - }, - - _update() { - let store = get(this, 'store'); - let query = get(this, 'query'); - - return store._query(this.modelName, query, this); - }, - - _setObjects(identifiersOrInternalModels, payload) { - // TODO: initial load should not cause change events at all, only - // subsequent. This requires changing the public api of adapter.query, but - // hopefully we can do that soon. - this.get('content').setObjects(identifiersOrInternalModels); - - this.setProperties({ - isLoaded: true, - isUpdating: false, - meta: { ...payload.meta }, - links: { ...payload.links }, - }); - - this.manager._associateWithRecordArray(identifiersOrInternalModels, this); - }, - - /** - @method _setIdentifiers - @param {StableRecordIdentifier[]} identifiers - @param {Object} payload normalized payload - @private - */ - _setIdentifiers(identifiers, payload) { - this._setObjects(identifiers, payload); - }, -}); diff --git a/packages/store/addon/-private/system/record-arrays/adapter-populated-record-array.ts b/packages/store/addon/-private/system/record-arrays/adapter-populated-record-array.ts new file mode 100644 index 00000000000..9c045fb70c0 --- /dev/null +++ b/packages/store/addon/-private/system/record-arrays/adapter-populated-record-array.ts @@ -0,0 +1,132 @@ +import { A } from '@ember/array'; +import type NativeArray from '@ember/array/-private/native-array'; +import { assert } from '@ember/debug'; + +import type { PromiseArray, RecordArrayManager } from 'ember-data/-private'; + +import type { CollectionResourceDocument, Links, Meta, PaginationLinks } from '../../ts-interfaces/ember-data-json-api'; +import type { StableRecordIdentifier } from '../../ts-interfaces/identifier'; +import type { RecordInstance } from '../../ts-interfaces/record-instance'; +import type { FindOptions } from '../../ts-interfaces/store'; +import type { Dict } from '../../ts-interfaces/utils'; +import type CoreStore from '../core-store'; +import { promiseArray } from '../promise-proxies'; +import SnapshotRecordArray from '../snapshot-record-array'; +import RecordArray from './record-array'; + +export interface AdapterPopulatedRecordArrayCreateArgs { + modelName: string; + store: CoreStore; + manager: RecordArrayManager; + content: NativeArray; + isLoaded?: boolean; + query?: Dict; + meta?: Meta; + links?: Links | PaginationLinks | null; +} + +/** + @module @ember-data/store +*/ + +/** + Represents an ordered list of records whose order and membership is + determined by the adapter. For example, a query sent to the adapter + may trigger a search on the server, whose results would be loaded + into an instance of the `AdapterPopulatedRecordArray`. + + This class should not be imported and instantiated by consuming applications. + + --- + + If you want to update the array and get the latest records from the + adapter, you can invoke [`update()`](AdapterPopulatedRecordArray/methods/update?anchor=update): + + Example + + ```javascript + // GET /users?isAdmin=true + store.query('user', { isAdmin: true }).then(function(admins) { + + admins.then(function() { + console.log(admins.get("length")); // 42 + }); + + // somewhere later in the app code, when new admins have been created + // in the meantime + // + // GET /users?isAdmin=true + admins.update().then(function() { + admins.get('isUpdating'); // false + console.log(admins.get("length")); // 123 + }); + + admins.get('isUpdating'); // true + } + ``` + + @class AdapterPopulatedRecordArray + @public + @extends RecordArray +*/ +export default class AdapterPopulatedRecordArray extends RecordArray { + declare links?: Links | PaginationLinks | null; + declare meta?: Dict; + declare query: Dict | null; + + init(props?: AdapterPopulatedRecordArrayCreateArgs) { + assert(`Cannot initialize AdapterPopulatedRecordArray with isUpdating`, !props || !('isUpdating' in props)); + super.init(); + this.set('content', this.get('content') || A()); + + super.init(); + this.query = this.query || null; + this.links = this.links || null; + } + + replace() { + throw new Error(`The result of a server query (on ${this.modelName}) is immutable.`); + } + + _update(): PromiseArray { + const { store, query } = this; + + // TODO save options from initial request? + return promiseArray(store._query(this.modelName, query, this, {})); + } + + _setObjects(identifiers: StableRecordIdentifier[], payload: CollectionResourceDocument) { + // TODO: initial load should not cause change events at all, only + // subsequent. This requires changing the public api of adapter.query, but + // hopefully we can do that soon. + this.content.setObjects(identifiers); + + this.setProperties({ + isLoaded: true, + isUpdating: false, + // TODO this assign kills the root reference but a deep-copy would be required + // for both meta and links to actually not be by-ref. We whould likely change + // this to a dev-only deep-freeze. + meta: Object.assign({}, payload.meta), + links: Object.assign({}, payload.links), + }); + + this.manager._associateWithRecordArray(identifiers, this); + } + + _createSnapshot(options: FindOptions) { + // this is private for users, but public for ember-data internals + // meta will only be present for an AdapterPopulatedRecordArray + return new SnapshotRecordArray(this, this.meta, options); + } + + /** + @method _setIdentifiers + @param {StableRecordIdentifier[]} identifiers + @param {Object} payload normalized payload + @private + */ + _setIdentifiers(identifiers: StableRecordIdentifier[], payload: CollectionResourceDocument): void { + this._setObjects(identifiers, payload); + } +} diff --git a/packages/store/addon/-private/system/record-arrays/record-array.js b/packages/store/addon/-private/system/record-arrays/record-array.ts similarity index 61% rename from packages/store/addon/-private/system/record-arrays/record-array.js rename to packages/store/addon/-private/system/record-arrays/record-array.ts index 2e1f09f344e..c98d3941379 100644 --- a/packages/store/addon/-private/system/record-arrays/record-array.js +++ b/packages/store/addon/-private/system/record-arrays/record-array.ts @@ -1,19 +1,37 @@ /** @module @ember-data/store */ +import type NativeArray from '@ember/array/-private/native-array'; import ArrayProxy from '@ember/array/proxy'; +import { assert } from '@ember/debug'; import { computed, get, set } from '@ember/object'; +import { tracked } from '@glimmer/tracking'; import { Promise } from 'rsvp'; -import { PromiseArray } from '../promise-proxies'; +import type { RecordArrayManager, Snapshot } from 'ember-data/-private'; + +import type { StableRecordIdentifier } from '../../ts-interfaces/identifier'; +import type { RecordInstance } from '../../ts-interfaces/record-instance'; +import type { FindOptions } from '../../ts-interfaces/store'; +import type CoreStore from '../core-store'; +import type { PromiseArray } from '../promise-proxies'; +import { promiseArray } from '../promise-proxies'; import SnapshotRecordArray from '../snapshot-record-array'; import { internalModelFactoryFor } from '../store/internal-model-factory'; -function recordForIdentifier(store, identifier) { +function recordForIdentifier(store: CoreStore, identifier: StableRecordIdentifier): RecordInstance { return internalModelFactoryFor(store).lookup(identifier).getRecord(); } +export interface RecordArrayCreateArgs { + modelName: string; + store: CoreStore; + manager: RecordArrayManager; + content: NativeArray; + isLoaded: boolean; +} + /** A record array is an array that contains records of a certain modelName. The record array materializes records as needed when they are retrieved for the first @@ -27,24 +45,21 @@ function recordForIdentifier(store, identifier) { @public @extends Ember.ArrayProxy */ +export default class RecordArray extends ArrayProxy { + /** + The array of client ids backing the record array. When a + record is requested from the record array, the record + for the client id at the same index is materialized, if + necessary, by the store. -let RecordArray = ArrayProxy.extend({ - init(args) { - this._super(args); - - /** - The array of client ids backing the record array. When a - record is requested from the record array, the record - for the client id at the same index is materialized, if - necessary, by the store. - - @property content - @private - @type Ember.Array - */ - this.set('content', this.content || null); - - /** + @property content + @private + @type Ember.Array + */ + declare content: NativeArray; + declare _getDeprecatedEventedInfo: () => string; + declare modelName: string; + /** The flag to signal a `RecordArray` is finished loading data. Example @@ -57,42 +72,49 @@ let RecordArray = ArrayProxy.extend({ @property isLoaded @public @type Boolean + */ + declare isLoaded: boolean; + /** + The store that created this record array. + + @property store + @private + @type Store */ - this.isLoaded = this.isLoaded || false; - /** - The flag to signal a `RecordArray` is currently loading data. + declare store: CoreStore; + declare _updatingPromise: PromiseArray | null; + declare manager: RecordArrayManager; + /** + The flag to signal a `RecordArray` is currently loading data. Example - ```javascript let people = store.peekAll('person'); people.get('isUpdating'); // false people.update(); people.get('isUpdating'); // true ``` - @property isUpdating @public @type Boolean - */ - this.isUpdating = false; + */ + @tracked isUpdating: boolean = false; - /** - The store that created this record array. + init(props?: RecordArrayCreateArgs) { + assert(`Cannot initialize RecordArray with isUpdating`, !props || !('isUpdating' in props)); + assert(`Cannot initialize RecordArray with isUpdating`, !props || !('_updatingPromise' in props)); + super.init(); - @property store - @private - @type Store - */ - this.store = this.store || null; + // TODO can we get rid of this? + this.set('content', this.content || null); this._updatingPromise = null; - }, + } replace() { throw new Error( `The result of a server query (for all ${this.modelName} types) is immutable. To modify contents, use toArray()` ); - }, + } /** The modelClass represented by this record array. @@ -101,12 +123,13 @@ let RecordArray = ArrayProxy.extend({ @public @type {subclass of Model} */ - type: computed('modelName', function () { + @computed('modelName') + get type() { if (!this.modelName) { return null; } return this.store.modelFor(this.modelName); - }).readOnly(), + } /** Retrieves an object from the content by index. @@ -116,10 +139,10 @@ let RecordArray = ArrayProxy.extend({ @param {Number} index @return {Model} record */ - objectAtContent(index) { + objectAtContent(index: number): RecordInstance | undefined { let identifier = get(this, 'content').objectAt(index); return identifier ? recordForIdentifier(this.store, identifier) : undefined; - }, + } /** Used to get the latest version of all of the records in this array @@ -141,33 +164,34 @@ let RecordArray = ArrayProxy.extend({ @method update @public */ - update() { - if (get(this, 'isUpdating')) { + update(): PromiseArray { + if (this.isUpdating) { return this._updatingPromise; } - this.set('isUpdating', true); + this.isUpdating = true; - let updatingPromise = this._update().finally(() => { + let updatingPromise = this._update(); + updatingPromise.finally(() => { this._updatingPromise = null; - if (this.get('isDestroying') || this.get('isDestroyed')) { + if (this.isDestroying || this.isDestroyed) { return; } - this.set('isUpdating', false); + this.isUpdating = false; }); this._updatingPromise = updatingPromise; return updatingPromise; - }, + } /* Update this RecordArray and return a promise which resolves once the update is finished. */ - _update() { + _update(): PromiseArray { return this.store.findAll(this.modelName, { reload: true }); - }, + } /** Saves all of the records in the `RecordArray`. @@ -186,7 +210,7 @@ let RecordArray = ArrayProxy.extend({ @public @return {PromiseArray} promise */ - save() { + save(): PromiseArray { let promiseLabel = `DS: RecordArray#save ${this.modelName}`; let promise = Promise.all(this.invoke('save'), promiseLabel).then( () => this, @@ -194,8 +218,8 @@ let RecordArray = ArrayProxy.extend({ 'DS: RecordArray#save return RecordArray' ); - return PromiseArray.create({ promise }); - }, + return promiseArray(promise); + } /** @method _unregisterFromManager @@ -203,7 +227,7 @@ let RecordArray = ArrayProxy.extend({ */ _unregisterFromManager() { this.manager.unregisterRecordArray(this); - }, + } willDestroy() { this._unregisterFromManager(); @@ -215,33 +239,34 @@ let RecordArray = ArrayProxy.extend({ // * the exception being: if an dominator has a reference to this object, // and must be informed to release e.g. e.g. removing itself from th // recordArrayMananger - set(this, 'content', null); + set(this, 'content', null as unknown as NativeArray); set(this, 'length', 0); - this._super(...arguments); - }, + super.willDestroy(); + } /** @method _createSnapshot @private */ - _createSnapshot(options) { + _createSnapshot(options: FindOptions) { // this is private for users, but public for ember-data internals - return new SnapshotRecordArray(this, this.get('meta'), options); - }, + // meta will only be present for an AdapterPopulatedRecordArray + return new SnapshotRecordArray(this, undefined, options); + } /** @method _dissociateFromOwnRecords @internal */ _dissociateFromOwnRecords() { - this.get('content').forEach((identifier) => { + this.content.forEach((identifier) => { let recordArrays = this.manager.getRecordArraysForIdentifier(identifier); if (recordArrays) { recordArrays.delete(this); } }); - }, + } /** Adds identifiers to the `RecordArray` without duplicates @@ -250,9 +275,9 @@ let RecordArray = ArrayProxy.extend({ @internal @param {StableRecordIdentifier[]} identifiers */ - _pushIdentifiers(identifiers) { - get(this, 'content').pushObjects(identifiers); - }, + _pushIdentifiers(identifiers: StableRecordIdentifier[]): void { + this.content.pushObjects(identifiers); + } /** Removes identifiers from the `RecordArray`. @@ -261,19 +286,15 @@ let RecordArray = ArrayProxy.extend({ @internal @param {StableRecordIdentifier[]} identifiers */ - _removeIdentifiers(identifiers) { - get(this, 'content').removeObjects(identifiers); - }, + _removeIdentifiers(identifiers: StableRecordIdentifier[]): void { + this.content.removeObjects(identifiers); + } /** @method _takeSnapshot @internal */ - _takeSnapshot() { - return get(this, 'content').map((identifier) => - internalModelFactoryFor(this.store).lookup(identifier).createSnapshot() - ); - }, -}); - -export default RecordArray; + _takeSnapshot(): Snapshot[] { + return this.content.map((identifier) => internalModelFactoryFor(this.store).lookup(identifier).createSnapshot()); + } +} diff --git a/packages/store/types/@ember/object/promise-proxy-mixin.d.ts b/packages/store/types/@ember/object/promise-proxy-mixin.d.ts index 388f25c9919..ebf65239796 100755 --- a/packages/store/types/@ember/object/promise-proxy-mixin.d.ts +++ b/packages/store/types/@ember/object/promise-proxy-mixin.d.ts @@ -1,5 +1,3 @@ -import Mixin from '@ember/object/mixin'; - /** * A low level mixin making ObjectProxy promise-aware. */ @@ -30,5 +28,5 @@ interface PromiseProxyMixin extends Promise { */ promise: Promise; } -declare const PromiseProxyMixin: Mixin>; +declare class PromiseProxyMixin extends Promise {} export default PromiseProxyMixin; diff --git a/packages/store/types/@ember/runloop/-private/backburner.d.ts b/packages/store/types/@ember/runloop/-private/backburner.d.ts deleted file mode 100644 index 61c608a1373..00000000000 --- a/packages/store/types/@ember/runloop/-private/backburner.d.ts +++ /dev/null @@ -1,31 +0,0 @@ -export interface QueueItem { - method: string; - target: object; - args: object[]; - stack: string | undefined; -} - -export interface DeferredActionQueues { - [index: string]: any; - queues: object; - schedule(queueName: string, target: any, method: any, args: any, onceFlag: boolean, stack: any): any; - flush(fromAutorun: boolean): any; -} - -export interface DebugInfo { - autorun: Error | undefined | null; - counters: object; - timers: QueueItem[]; - instanceStack: DeferredActionQueues[]; -} - -export interface Backburner { - join(...args: any[]): void; - on(...args: any[]): void; - run(...args: any[]): void; - scheduleOnce(...args: any[]): void; - schedule(queueName: string, target: object | null, method: () => void | string): void; - ensureInstance(): void; - DEBUG: boolean; - getDebugInfo(): DebugInfo; -} diff --git a/packages/store/types/@ember/runloop/index.d.ts b/packages/store/types/@ember/runloop/index.d.ts index c79b9ec1dc7..fb9d8f5aa8e 100644 --- a/packages/store/types/@ember/runloop/index.d.ts +++ b/packages/store/types/@ember/runloop/index.d.ts @@ -1,7 +1,282 @@ +// Type definitions for non-npm package @ember/runloop 3.16 +// Project: https://emberjs.com/api/ember/3.16/modules/@ember%2Frunloop +// Definitions by: Mike North +// Steve Calvert +// Chris Krycho +// Dan Freeman +// James C. Davis +// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped +// TypeScript Version: 3.7 + +import type { Backburner } from '@ember/runloop/-private/backburner'; +import type { EmberRunQueues, RunMethod } from '@ember/runloop/-private/types'; +import type { EmberRunTimer } from '@ember/runloop/types'; + +export interface RunNamespace { + /** + * Runs the passed target and method inside of a RunLoop, ensuring any + * deferred actions including bindings and views updates are flushed at the + * end. + */ + (method: (...args: unknown[]) => Ret): Ret; + (target: Target, method: RunMethod, ...args: unknown[]): Ret; + /** + * If no run-loop is present, it creates a new one. If a run loop is + * present it will queue itself to run on the existing run-loops action + * queue. + */ + join(method: (...args: unknown[]) => Ret, ...args: unknown[]): Ret | undefined; + join(target: Target, method: RunMethod, ...args: unknown[]): Ret | undefined; + /** + * Allows you to specify which context to call the specified function in while + * adding the execution of that function to the Ember run loop. This ability + * makes this method a great way to asynchronously integrate third-party libraries + * into your Ember application. + */ + bind(target: Target, method: RunMethod, ...args: unknown[]): (...args: unknown[]) => Ret; + /** + * Begins a new RunLoop. Any deferred actions invoked after the begin will + * be buffered until you invoke a matching call to `run.end()`. This is + * a lower-level way to use a RunLoop instead of using `run()`. + */ + begin(): void; + /** + * Ends a RunLoop. This must be called sometime after you call + * `run.begin()` to flush any deferred actions. This is a lower-level way + * to use a RunLoop instead of using `run()`. + */ + end(): void; + /** + * Adds the passed target/method and any optional arguments to the named + * queue to be executed at the end of the RunLoop. If you have not already + * started a RunLoop when calling this method one will be started for you + * automatically. + */ + schedule(queue: EmberRunQueues, target: Target, method: RunMethod, ...args: unknown[]): EmberRunTimer; + schedule(queue: EmberRunQueues, method: (args: unknown[]) => unknown, ...args: unknown[]): EmberRunTimer; + /** + * Invokes the passed target/method and optional arguments after a specified + * period of time. The last parameter of this method must always be a number + * of milliseconds. + */ + later(method: (...args: unknown[]) => unknown, wait: number): EmberRunTimer; + later(target: Target, method: RunMethod, wait: number): EmberRunTimer; + later(target: Target, method: RunMethod, arg0: unknown, wait: number): EmberRunTimer; + later(target: Target, method: RunMethod, arg0: unknown, arg1: unknown, wait: number): EmberRunTimer; + later( + target: Target, + method: RunMethod, + arg0: unknown, + arg1: unknown, + arg2: unknown, + wait: number + ): EmberRunTimer; + later( + target: Target, + method: RunMethod, + arg0: unknown, + arg1: unknown, + arg2: unknown, + arg3: unknown, + wait: number + ): EmberRunTimer; + later( + target: Target, + method: RunMethod, + arg0: unknown, + arg1: unknown, + arg2: unknown, + arg3: unknown, + arg4: unknown, + wait: number + ): EmberRunTimer; + later( + target: Target, + method: RunMethod, + arg0: unknown, + arg1: unknown, + arg2: unknown, + arg3: unknown, + arg4: unknown, + arg5: unknown, + wait: number + ): EmberRunTimer; + /** + * Schedule a function to run one time during the current RunLoop. This is equivalent + * to calling `scheduleOnce` with the "actions" queue. + */ + once(target: Target, method: RunMethod, ...args: unknown[]): EmberRunTimer; + /** + * Schedules a function to run one time in a given queue of the current RunLoop. + * Calling this method with the same queue/target/method combination will have + * no effect (past the initial call). + */ + scheduleOnce( + queue: EmberRunQueues, + target: Target, + method: RunMethod, + ...args: unknown[] + ): EmberRunTimer; + /** + * Schedules an item to run from within a separate run loop, after + * control has been returned to the system. This is equivalent to calling + * `run.later` with a wait time of 1ms. + */ + next(target: Target, method: RunMethod, ...args: unknown[]): EmberRunTimer; + next(method: () => void, ...args: unknown[]): EmberRunTimer; + + /** + * Cancels a scheduled item. Must be a value returned by `run.later()`, + * `run.once()`, `run.scheduleOnce()`, `run.next()`, `run.debounce()`, or + * `run.throttle()`. + */ + cancel(timer: EmberRunTimer): boolean; + /** + * Delay calling the target method until the debounce period has elapsed + * with no additional debounce calls. If `debounce` is called again before + * the specified time has elapsed, the timer is reset and the entire period + * must pass again before the target method is called. + */ + debounce(method: (...args: unknown[]) => unknown, wait: number, immediate?: boolean): EmberRunTimer; + debounce(target: Target, method: RunMethod, wait: number, immediate?: boolean): EmberRunTimer; + debounce( + target: Target, + method: RunMethod, + arg0: unknown, + wait: number, + immediate?: boolean + ): EmberRunTimer; + debounce( + target: Target, + method: RunMethod, + arg0: unknown, + arg1: unknown, + wait: number, + immediate?: boolean + ): EmberRunTimer; + debounce( + target: Target, + method: RunMethod, + arg0: unknown, + arg1: unknown, + arg2: unknown, + wait: number, + immediate?: boolean + ): EmberRunTimer; + debounce( + target: Target, + method: RunMethod, + arg0: unknown, + arg1: unknown, + arg2: unknown, + arg3: unknown, + wait: number, + immediate?: boolean + ): EmberRunTimer; + debounce( + target: Target, + method: RunMethod, + arg0: unknown, + arg1: unknown, + arg2: unknown, + arg3: unknown, + arg4: unknown, + wait: number, + immediate?: boolean + ): EmberRunTimer; + debounce( + target: Target, + method: RunMethod, + arg0: unknown, + arg1: unknown, + arg2: unknown, + arg3: unknown, + arg4: unknown, + arg5: unknown, + wait: number, + immediate?: boolean + ): EmberRunTimer; + /** + * Ensure that the target method is never called more frequently than + * the specified spacing period. The target method is called immediately. + */ + throttle(method: (...args: unknown[]) => unknown, spacing: number, immediate?: boolean): EmberRunTimer; + throttle(target: Target, method: RunMethod, spacing: number, immediate?: boolean): EmberRunTimer; + throttle( + target: Target, + method: RunMethod, + arg0: unknown, + spacing: number, + immediate?: boolean + ): EmberRunTimer; + throttle( + target: Target, + method: RunMethod, + arg0: unknown, + arg1: unknown, + spacing: number, + immediate?: boolean + ): EmberRunTimer; + throttle( + target: Target, + method: RunMethod, + arg0: unknown, + arg1: unknown, + arg2: unknown, + spacing: number, + immediate?: boolean + ): EmberRunTimer; + throttle( + target: Target, + method: RunMethod, + arg0: unknown, + arg1: unknown, + arg2: unknown, + arg3: unknown, + spacing: number, + immediate?: boolean + ): EmberRunTimer; + throttle( + target: Target, + method: RunMethod, + arg0: unknown, + arg1: unknown, + arg2: unknown, + arg3: unknown, + arg4: unknown, + spacing: number, + immediate?: boolean + ): EmberRunTimer; + throttle( + target: Target, + method: RunMethod, + arg0: unknown, + arg1: unknown, + arg2: unknown, + arg3: unknown, + arg4: unknown, + arg5: unknown, + spacing: number, + immediate?: boolean + ): EmberRunTimer; + + queues: EmberRunQueues[]; +} + // necessary because our "run" is run.backburner // which we use to avoid autorun triggering for Ember <= 3.4 // we can drop this and use run directly ~11/1/2019 -export const run: any; -export const _backburner: any; -export const join: any; -export const cancel: any; +export const _backburner: Backburner; +export const run: RunNamespace; +export const begin: typeof run.begin; +export const bind: typeof run.bind; +export const cancel: typeof run.cancel; +export const debounce: typeof run.debounce; +export const end: typeof run.end; +export const join: typeof run.join; +export const later: typeof run.later; +export const next: typeof run.next; +export const once: typeof run.once; +export const schedule: typeof run.schedule; +export const scheduleOnce: typeof run.scheduleOnce; +export const throttle: typeof run.throttle; From a325fa075cf2ab4c0013b072ffe9668aca076555 Mon Sep 17 00:00:00 2001 From: Chris Thoburn Date: Sat, 16 Apr 2022 00:49:54 -0700 Subject: [PATCH 4/7] get it working --- .../integration/request-state-service-test.ts | 5 +- .../adapter-populated-record-array-test.js | 21 +-- .../unit/record-arrays/record-array-test.js | 3 +- .../store/addon/-private/system/core-store.ts | 125 ++++++++++-------- .../addon/-private/system/fetch-manager.ts | 33 ++--- .../addon/-private/system/promise-proxies.ts | 25 ++-- .../-private/system/promise-proxy-base.d.ts | 24 ++-- .../-private/system/record-array-manager.ts | 25 ++-- .../adapter-populated-record-array.ts | 11 +- .../system/record-arrays/record-array.ts | 4 +- .../-private/system/snapshot-record-array.ts | 23 ++-- .../addon/-private/utils/promise-record.ts | 6 +- .../@ember/runloop/-private/backburner.d.ts | 31 +++++ .../store/types/@ember/runloop/index.d.ts | 1 + tsconfig.json | 1 - 15 files changed, 185 insertions(+), 153 deletions(-) create mode 100644 packages/store/types/@ember/runloop/-private/backburner.d.ts diff --git a/packages/-ember-data/tests/integration/request-state-service-test.ts b/packages/-ember-data/tests/integration/request-state-service-test.ts index 6324683702b..a8baa6a493f 100644 --- a/packages/-ember-data/tests/integration/request-state-service-test.ts +++ b/packages/-ember-data/tests/integration/request-state-service-test.ts @@ -8,6 +8,7 @@ import { setupTest } from 'ember-qunit'; import Model, { attr } from '@ember-data/model'; import JSONSerializer from '@ember-data/serializer/json'; import type Store from '@ember-data/store'; +import { DSModel } from '@ember-data/store/-private/ts-interfaces/ds-model'; import type { RequestStateEnum } from '@ember-data/store/-private/ts-interfaces/fetch-manager'; class Person extends Model { @@ -93,7 +94,7 @@ module('integration/request-state-service - Request State Service', function (ho }; assert.deepEqual(request.request.data[0], requestOp, 'request op is correct'); - let person = await promise; + let person = (await promise) as DSModel; let lastRequest = requestService.getLastRequestForRecord(identifier); let requestStateResult = { type: 'query' as const, @@ -216,7 +217,7 @@ module('integration/request-state-service - Request State Service', function (ho count++; }); - let person = await store.findRecord('person', '1'); + let person = (await store.findRecord('person', '1')) as DSModel; await person.save(); assert.strictEqual(count, 4, 'callback called four times'); }); diff --git a/packages/-ember-data/tests/unit/record-arrays/adapter-populated-record-array-test.js b/packages/-ember-data/tests/unit/record-arrays/adapter-populated-record-array-test.js index 68672c71c30..79eb2111c34 100644 --- a/packages/-ember-data/tests/unit/record-arrays/adapter-populated-record-array-test.js +++ b/packages/-ember-data/tests/unit/record-arrays/adapter-populated-record-array-test.js @@ -22,7 +22,12 @@ module('unit/record-arrays/adapter-populated-record-array - DS.AdapterPopulatedR setupTest(hooks); test('default initial state', async function (assert) { - let recordArray = AdapterPopulatedRecordArray.create({ modelName: 'recordType' }); + let recordArray = AdapterPopulatedRecordArray.create({ + modelName: 'recordType', + isLoaded: false, + content: A(), + store: null, + }); assert.false(recordArray.get('isLoaded'), 'expected isLoaded to be false'); assert.strictEqual(recordArray.get('modelName'), 'recordType', 'has modelName'); @@ -38,7 +43,6 @@ module('unit/record-arrays/adapter-populated-record-array - DS.AdapterPopulatedR let recordArray = AdapterPopulatedRecordArray.create({ modelName: 'apple', isLoaded: true, - isUpdating: true, content, store, query: 'some-query', @@ -83,6 +87,8 @@ module('unit/record-arrays/adapter-populated-record-array - DS.AdapterPopulatedR let recordArray = AdapterPopulatedRecordArray.create({ modelName: 'recordType', store, + content: A(), + isLoaded: true, query: 'some-query', }); @@ -93,15 +99,14 @@ module('unit/record-arrays/adapter-populated-record-array - DS.AdapterPopulatedR let updateResult = recordArray.update(); assert.strictEqual(queryCalled, 1); - - deferred.resolve('return value'); + const expectedResult = A(); + deferred.resolve(expectedResult); assert.true(recordArray.get('isUpdating'), 'should be updating'); - return updateResult.then((result) => { - assert.strictEqual(result, 'return value'); - assert.false(recordArray.get('isUpdating'), 'should no longer be updating'); - }); + const result = await updateResult; + assert.strictEqual(result, expectedResult); + assert.false(recordArray.get('isUpdating'), 'should no longer be updating'); }); // TODO: is this method required, i suspect store._query should be refactor so this is not needed diff --git a/packages/-ember-data/tests/unit/record-arrays/record-array-test.js b/packages/-ember-data/tests/unit/record-arrays/record-array-test.js index caaf354a323..2a3f46bb892 100644 --- a/packages/-ember-data/tests/unit/record-arrays/record-array-test.js +++ b/packages/-ember-data/tests/unit/record-arrays/record-array-test.js @@ -22,7 +22,7 @@ module('unit/record-arrays/record-array - DS.RecordArray', function (hooks) { setupTest(hooks); test('default initial state', async function (assert) { - let recordArray = RecordArray.create({ modelName: 'recordType' }); + let recordArray = RecordArray.create({ modelName: 'recordType', isLoaded: false, store: null }); assert.false(get(recordArray, 'isLoaded'), 'record is not loaded'); assert.false(get(recordArray, 'isUpdating'), 'record is not updating'); @@ -37,7 +37,6 @@ module('unit/record-arrays/record-array - DS.RecordArray', function (hooks) { let recordArray = RecordArray.create({ modelName: 'apple', isLoaded: true, - isUpdating: true, content, store, }); diff --git a/packages/store/addon/-private/system/core-store.ts b/packages/store/addon/-private/system/core-store.ts index efcab4c9df0..30bbec57687 100644 --- a/packages/store/addon/-private/system/core-store.ts +++ b/packages/store/addon/-private/system/core-store.ts @@ -14,14 +14,13 @@ import { DEBUG } from '@glimmer/env'; import Ember from 'ember'; import { importSync } from '@embroider/macros'; -import { all, default as RSVP, Promise, resolve } from 'rsvp'; +import { all, default as RSVP, resolve } from 'rsvp'; import { HAS_RECORD_DATA_PACKAGE } from '@ember-data/private-build-infra'; import type { ManyRelationship, RecordData as RecordDataClass } from '@ember-data/record-data/-private'; import type { RelationshipState } from '@ember-data/record-data/-private/graph/-state'; import { IdentifierCache } from '../identifiers/cache'; -import type { DSModel } from '../ts-interfaces/ds-model'; import type { CollectionResourceDocument, EmptyResourceDocument, @@ -35,8 +34,8 @@ import type { StableExistingRecordIdentifier, StableRecordIdentifier, } from '../ts-interfaces/identifier'; +import { MinimumAdapterInterface } from '../ts-interfaces/minimum-adapter-interface'; import type { MinimumSerializerInterface } from '../ts-interfaces/minimum-serializer-interface'; -import type { PromiseProxy } from '../ts-interfaces/promise-proxies'; import type { RecordData } from '../ts-interfaces/record-data'; import type { JsonApiRelationship } from '../ts-interfaces/record-data-json-api'; import type { RecordDataRecordWrapper } from '../ts-interfaces/record-data-record-wrapper'; @@ -59,8 +58,10 @@ import { import type ShimModelClass from './model/shim-model-class'; import { getShimClass } from './model/shim-model-class'; import normalizeModelName from './normalize-model-name'; +import type { PromiseArray, PromiseObject } from './promise-proxies'; import { promiseArray, promiseObject } from './promise-proxies'; import RecordArrayManager from './record-array-manager'; +import { AdapterPopulatedRecordArray, RecordArray } from './record-arrays'; import { setRecordDataFor } from './record-data-for'; import NotificationManager from './record-notification-manager'; import type { BelongsToReference, HasManyReference } from './references'; @@ -81,7 +82,6 @@ let _RecordData: RecordDataConstruct | undefined; const { ENV } = Ember; type AsyncTrackingToken = Readonly<{ label: string; trace: Error | string }>; -type PromiseArray = Promise; const RECORD_REFERENCES = new WeakCache(DEBUG ? 'reference' : ''); @@ -178,14 +178,14 @@ abstract class CoreStore extends Service { * @property _backburner * @private */ - public _backburner: Backburner = edBackburner; - public recordArrayManager: RecordArrayManager = new RecordArrayManager({ store: this }); + declare _backburner: Backburner; + declare recordArrayManager: RecordArrayManager; declare _notificationManager: NotificationManager; declare identifierCache: IdentifierCache; - private _adapterCache = Object.create(null); - private _serializerCache = Object.create(null); - public _storeWrapper = new RecordDataStoreWrapper(this); + declare _adapterCache: Dict; + declare _serializerCache: Dict; + declare _storeWrapper: RecordDataStoreWrapper; /* Ember Data uses several specialized micro-queues for organizing @@ -195,15 +195,15 @@ abstract class CoreStore extends Service { ember-data's custom backburner instance. */ // used for coalescing internal model updates - private _updatedInternalModels: InternalModel[] = []; + declare _updatedInternalModels: InternalModel[]; declare _fetchManager: FetchManager; declare _schemaDefinitionService: SchemaDefinitionService; // DEBUG-only properties declare _trackedAsyncRequests: AsyncTrackingToken[]; - shouldTrackAsyncRequests: boolean = false; - generateStackTracesForTrackedRequests: boolean = false; + declare shouldTrackAsyncRequests: boolean; + declare generateStackTracesForTrackedRequests: boolean; declare _trackAsyncRequestStart: (str: string) => void; declare _trackAsyncRequestEnd: (token: AsyncTrackingToken) => void; declare __asyncWaiter: () => boolean; @@ -254,6 +254,12 @@ abstract class CoreStore extends Service { */ constructor() { super(...arguments); + this._adapterCache = Object.create(null); + this._serializerCache = Object.create(null); + this._storeWrapper = new RecordDataStoreWrapper(this); + this._backburner = edBackburner; + this.recordArrayManager = new RecordArrayManager({ store: this }); + this._updatedInternalModels = []; RECORD_REFERENCES._generator = (identifier) => { return new RecordReference(this, identifier); @@ -545,7 +551,7 @@ abstract class CoreStore extends Service { @param {Object} properties from the new record @return {String} if the adapter can generate one, an ID */ - _generateId(modelName, properties) { + _generateId(modelName: string, properties: CreateRecordProperties): string | null { let adapter = this.adapterFor(modelName); if (adapter && adapter.generateIdForRecord) { @@ -576,7 +582,7 @@ abstract class CoreStore extends Service { @public @param {Model} record */ - deleteRecord(record) { + deleteRecord(record: RecordInstance): void { if (DEBUG) { assertDestroyingStore(this, 'deleteRecord'); } @@ -607,7 +613,7 @@ abstract class CoreStore extends Service { @public @param {Model} record */ - unloadRecord(record) { + unloadRecord(record: RecordInstance): void { if (DEBUG) { assertDestroyingStore(this, 'unloadRecord'); } @@ -632,7 +638,7 @@ abstract class CoreStore extends Service { @return {Promise} promise @private */ - find(modelName, id, options) { + find(modelName: string, id: string | number, options?): PromiseObject { if (DEBUG) { assertDestroyingStore(this, 'find'); } @@ -1031,13 +1037,13 @@ abstract class CoreStore extends Service { @param {Object} [options] - if the first param is a string this will be the optional options for the request. See examples for available options. @return {Promise} promise */ - findRecord(resource: string, id: string | number, options?: FindOptions): PromiseProxy; - findRecord(resource: ResourceIdentifierObject, id?: FindOptions): PromiseProxy; + findRecord(resource: string, id: string | number, options?: FindOptions): PromiseObject; + findRecord(resource: ResourceIdentifierObject, id?: FindOptions): PromiseObject; findRecord( resource: string | ResourceIdentifierObject, id?: string | number | FindOptions, options?: FindOptions - ): PromiseProxy { + ): PromiseObject { if (DEBUG) { assertDestroyingStore(this, 'findRecord'); } @@ -1073,7 +1079,7 @@ abstract class CoreStore extends Service { return promiseRecord(fetchedInternalModel, `DS: Store#findRecord ${internalModel.identifier}`); } - _findRecord(internalModel: InternalModel, options: FindOptions) { + _findRecord(internalModel: InternalModel, options: FindOptions): Promise { // Refetch if the reload option is passed if (options.reload) { return this._scheduleFetch(internalModel, options); @@ -1092,7 +1098,7 @@ abstract class CoreStore extends Service { } if (options.backgroundReload === false) { - return Promise.resolve(internalModel); + return resolve(internalModel); } // Trigger the background refetch if backgroundReload option is passed @@ -1105,7 +1111,7 @@ abstract class CoreStore extends Service { } // Return the cached record - return Promise.resolve(internalModel); + return resolve(internalModel); } _findByInternalModel(internalModel: InternalModel, options: FindOptions = {}): Promise { @@ -1126,12 +1132,12 @@ abstract class CoreStore extends Service { if (internalModel.currentState.isLoading) { let pendingRequest = this._fetchManager.getPendingFetch(internalModel.identifier, options); if (pendingRequest) { - return pendingRequest.then(() => Promise.resolve(internalModel)); + return pendingRequest.then(() => resolve(internalModel)); } return this._scheduleFetch(internalModel, options); } - return Promise.resolve(internalModel); + return resolve(internalModel); } /** @@ -1172,10 +1178,10 @@ abstract class CoreStore extends Service { fetches[i] = this._scheduleFetch(internalModels[i], options); } - return Promise.all(fetches); + return all(fetches); } - _scheduleFetch(internalModel: InternalModel, options = {}): RSVP.Promise { + _scheduleFetch(internalModel: InternalModel, options = {}): Promise { let generateStackTrace = this.generateStackTracesForTrackedRequests; // TODO remove this once we don't rely on state machine internalModel.send('loadingData'); @@ -1366,7 +1372,7 @@ abstract class CoreStore extends Service { @param options optional to include adapterOptions @return {Promise} promise */ - _reloadRecord(internalModel: InternalModel, options: FindOptions): RSVP.Promise { + _reloadRecord(internalModel: InternalModel, options: FindOptions): Promise { options.isReloading = true; let { id, modelName } = internalModel; let adapter = this.adapterFor(modelName); @@ -1375,7 +1381,7 @@ abstract class CoreStore extends Service { assert(`You tried to reload a record but you have no adapter (for ${modelName})`, adapter); assert( `You tried to reload a record but your adapter does not implement 'findRecord'`, - typeof adapter.findRecord === 'function' || typeof adapter.find === 'function' + typeof adapter.findRecord === 'function' ); return this._scheduleFetch(internalModel, options); @@ -1462,7 +1468,7 @@ abstract class CoreStore extends Service { finds[i] = this._findEmptyInternalModel(internalModels[i], options); } - return Promise.all(finds); + return all(finds); } /** @@ -1719,7 +1725,7 @@ abstract class CoreStore extends Service { @param {Object} options optional, may include `adapterOptions` hash which will be passed to adapter.query @return {Promise} promise */ - query(modelName: string, query, options): PromiseArray { + query(modelName: string, query, options): PromiseArray { if (DEBUG) { assertDestroyingStore(this, 'query'); } @@ -1737,10 +1743,10 @@ abstract class CoreStore extends Service { } let normalizedModelName = normalizeModelName(modelName); - return this._query(normalizedModelName, query, null, adapterOptionsWrapper); + return promiseArray(this._query(normalizedModelName, query, null, adapterOptionsWrapper)); } - _query(modelName: string, query, array, options): PromiseArray { + _query(modelName: string, query, array, options): Promise { assert(`You need to pass a model name to the store's query method`, isPresent(modelName)); assert(`You need to pass a query hash to the store's query method`, query); assert( @@ -1756,7 +1762,7 @@ abstract class CoreStore extends Service { typeof adapter.query === 'function' ); - return promiseArray(_query(adapter, this, modelName, query, array, options)); + return _query(adapter, this, modelName, query, array, options) as unknown as Promise; } /** @@ -1857,7 +1863,7 @@ abstract class CoreStore extends Service { @param {Object} options optional, may include `adapterOptions` hash which will be passed to adapter.queryRecord @return {Promise} promise which resolves with the found record or `null` */ - queryRecord(modelName, query, options) { + queryRecord(modelName: string, query, options): PromiseObject { if (DEBUG) { assertDestroyingStore(this, 'queryRecord'); } @@ -1882,15 +1888,17 @@ abstract class CoreStore extends Service { typeof adapter.queryRecord === 'function' ); - return promiseObject( - _queryRecord(adapter, this, normalizedModelName, query, adapterOptionsWrapper).then((internalModel) => { - // the promise returned by store.queryRecord is expected to resolve with - // an instance of Model - if (internalModel) { - return internalModel.getRecord(); - } + const promise: Promise = _queryRecord( + adapter, + this, + normalizedModelName, + query, + adapterOptionsWrapper + ) as Promise; - return null; + return promiseObject( + promise.then((internalModel: InternalModel | null) => { + return internalModel ? internalModel.getRecord() : null; }) ); } @@ -2083,7 +2091,10 @@ abstract class CoreStore extends Service { @param {Object} options @return {Promise} promise */ - findAll(modelName, options) { + findAll( + modelName: string, + options: { reload?: boolean; backgroundReload?: boolean } = {} + ): PromiseArray { if (DEBUG) { assertDestroyingStore(this, 'findAll'); } @@ -2096,7 +2107,7 @@ abstract class CoreStore extends Service { let normalizedModelName = normalizeModelName(modelName); let fetch = this._fetchAll(normalizedModelName, this.peekAll(normalizedModelName), options); - return fetch; + return promiseArray(fetch); } /** @@ -2106,7 +2117,11 @@ abstract class CoreStore extends Service { @param {RecordArray} array @return {Promise} promise */ - _fetchAll(modelName, array, options: { reload?: boolean; backgroundReload?: boolean } = {}) { + _fetchAll( + modelName: string, + array: RecordArray, + options: { reload?: boolean; backgroundReload?: boolean } + ): Promise { let adapter = this.adapterFor(modelName); assert(`You tried to load all records but you have no adapter (for ${modelName})`, adapter); @@ -2117,7 +2132,7 @@ abstract class CoreStore extends Service { if (options.reload) { set(array, 'isUpdating', true); - return promiseArray(_findAll(adapter, this, modelName, options)); + return _findAll(adapter, this, modelName, options); } let snapshotArray = array._createSnapshot(options); @@ -2128,12 +2143,12 @@ abstract class CoreStore extends Service { (!adapter.shouldReloadAll && snapshotArray.length === 0) ) { set(array, 'isUpdating', true); - return promiseArray(_findAll(adapter, this, modelName, options)); + return _findAll(adapter, this, modelName, options); } } if (options.backgroundReload === false) { - return promiseArray(Promise.resolve(array)); + return resolve(array); } if ( @@ -2145,7 +2160,7 @@ abstract class CoreStore extends Service { _findAll(adapter, this, modelName, options); } - return promiseArray(Promise.resolve(array)); + return resolve(array); } /** @@ -2153,7 +2168,7 @@ abstract class CoreStore extends Service { @param {String} modelName @private */ - _didUpdateAll(modelName) { + _didUpdateAll(modelName: string): void { this.recordArrayManager._didUpdateAll(modelName); } @@ -2256,7 +2271,7 @@ abstract class CoreStore extends Service { internalModel: InternalModel, resolver: RSVP.Deferred, options: FindOptions - ): void | RSVP.Promise { + ): void | Promise { if (internalModel._isRecordFullyDeleted()) { resolver.resolve(); return resolver.promise; @@ -2845,13 +2860,13 @@ abstract class CoreStore extends Service { return internalModel!.createSnapshot(options).serialize(options); } - saveRecord(record: RecordInstance, options?: Dict): RSVP.Promise { + saveRecord(record: RecordInstance, options?: Dict): Promise { let identifier = recordIdentifierFor(record); let internalModel = internalModelFactoryFor(this).peek(identifier); // TODO we used to check if the record was destroyed here // Casting can be removed once REQUEST_SERVICE ff is turned on // because a `Record` is provided there will always be a matching internalModel - return (internalModel!.save(options) as RSVP.Promise).then(() => record); + return (internalModel!.save(options) as Promise).then(() => record); } relationshipReferenceFor(identifier: RecordIdentifier, key: string): BelongsToReference | HasManyReference { @@ -3133,14 +3148,14 @@ abstract class CoreStore extends Service { destroy() { // enqueue destruction of any adapters/serializers we have created for (let adapterName in this._adapterCache) { - let adapter = this._adapterCache[adapterName]; + let adapter = this._adapterCache[adapterName]!; if (typeof adapter.destroy === 'function') { adapter.destroy(); } } for (let serializerName in this._serializerCache) { - let serializer = this._serializerCache[serializerName]; + let serializer = this._serializerCache[serializerName]!; if (typeof serializer.destroy === 'function') { serializer.destroy(); } diff --git a/packages/store/addon/-private/system/fetch-manager.ts b/packages/store/addon/-private/system/fetch-manager.ts index d57fe224d3f..8b6d2a2b2fc 100644 --- a/packages/store/addon/-private/system/fetch-manager.ts +++ b/packages/store/addon/-private/system/fetch-manager.ts @@ -6,7 +6,7 @@ import { assert, deprecate, warn } from '@ember/debug'; import { _backburner as emberBackburner } from '@ember/runloop'; import { DEBUG } from '@glimmer/env'; -import { default as RSVP, Promise } from 'rsvp'; +import { default as RSVP, resolve } from 'rsvp'; import { DEPRECATE_RSVP_PROMISE } from '@ember-data/private-build-infra/deprecations'; @@ -90,10 +90,7 @@ export default class FetchManager { @internal */ - scheduleSave( - identifier: RecordIdentifier, - options: FetchMutationOptions - ): RSVP.Promise { + scheduleSave(identifier: RecordIdentifier, options: FetchMutationOptions): Promise { let promiseLabel = 'DS: Model#save ' + this; let resolver = RSVP.defer(promiseLabel); let query: SaveRecordMutation = { @@ -140,7 +137,7 @@ export default class FetchManager { typeof adapter[operation] === 'function' ); - let promise = Promise.resolve().then(() => adapter[operation](store, modelClass, snapshot)); + let promise = resolve().then(() => adapter[operation](store, modelClass, snapshot)); let serializer: SerializerWithParseErrors | null = store.serializerFor(modelName); let label = `DS: Extract and notify about ${operation} completion of ${internalModel}`; @@ -149,10 +146,7 @@ export default class FetchManager { promise !== undefined ); - promise = guardDestroyedStore(promise, store, label); - promise = _guard(promise, _bind(_objectIsAlive, internalModel)); - - promise = promise.then( + promise = _guard(guardDestroyedStore(promise, store, label), _bind(_objectIsAlive, internalModel)).then( (adapterPayload) => { if (!_objectIsAlive(internalModel)) { if (DEPRECATE_RSVP_PROMISE) { @@ -213,7 +207,7 @@ export default class FetchManager { } } - scheduleFetch(identifier: ExistingRecordIdentifier, options: any, shouldTrace: boolean): RSVP.Promise { + scheduleFetch(identifier: ExistingRecordIdentifier, options: any, shouldTrace: boolean): Promise { // TODO Probably the store should pass in the query object let query: FindRecordQuery = { @@ -296,17 +290,16 @@ export default class FetchManager { let snapshot = new Snapshot(fetchItem.options, identifier, this._store); let klass = this._store.modelFor(identifier.type); - - let promise = Promise.resolve().then(() => { - return adapter.findRecord(this._store, klass, identifier.id, snapshot); - }); - let id = identifier.id; - let label = `DS: Handle Adapter#findRecord of '${modelName}' with id: '${id}'`; - promise = guardDestroyedStore(promise, this._store, label); - promise = promise.then( + let promise = guardDestroyedStore( + resolve().then(() => { + return adapter.findRecord(this._store, klass, identifier.id, snapshot); + }), + this._store, + label + ).then( (adapterPayload) => { assert( `You made a 'findRecord' request for a '${modelName}' with id '${id}', but the adapter's response did not have any data`, @@ -521,7 +514,7 @@ export default class FetchManager { let groups: Snapshot[][]; if (adapter.groupRecordsForFindMany) { - groups = adapter.groupRecordsForFindMany(this, snapshots); + groups = adapter.groupRecordsForFindMany(this._store, snapshots); } else { groups = [snapshots]; } diff --git a/packages/store/addon/-private/system/promise-proxies.ts b/packages/store/addon/-private/system/promise-proxies.ts index 5b9fe1d6a74..14d48e78e5f 100644 --- a/packages/store/addon/-private/system/promise-proxies.ts +++ b/packages/store/addon/-private/system/promise-proxies.ts @@ -1,11 +1,9 @@ -import type NativeArray from '@ember/array/-private/native-array'; import { deprecate } from '@ember/debug'; import type ComputedProperty from '@ember/object/computed'; import { reads } from '@ember/object/computed'; import { resolve } from 'rsvp'; -import { RecordInstance } from '../ts-interfaces/record-instance'; import type { Dict } from '../ts-interfaces/utils'; import { PromiseArrayProxy, PromiseObjectProxy } from './promise-proxy-base'; @@ -54,7 +52,7 @@ interface EmberArrayProxyLike { } type EmberArrayLike = EmberNativeArrayLike | EmberArrayProxyLike; -export class PromiseArray = NativeArray> extends PromiseArrayProxy { +export class PromiseArray> extends PromiseArrayProxy { @reads('content.meta') declare meta?: Dict; } @@ -90,18 +88,15 @@ export class PromiseArray = NativeArray> exten @extends Ember.ObjectProxy @uses Ember.PromiseProxyMixin */ -export const PromiseObject = PromiseObjectProxy; +export { PromiseObjectProxy as PromiseObject }; -export function promiseObject(promise: Promise, label: string) { +export function promiseObject(promise: Promise, label?: string): PromiseObjectProxy { return PromiseObjectProxy.create({ promise: resolve(promise, label), - }); + }) as PromiseObjectProxy; } -export function promiseArray = NativeArray>( - promise: Promise, - label?: string -): PromiseArray { +export function promiseArray>(promise: Promise, label?: string): PromiseArray { return PromiseArray.create({ promise: resolve(promise, label), }) as unknown as PromiseArray; @@ -110,9 +105,9 @@ export function promiseArray = NativeArray>( // constructor is accessed in some internals but not including it in the copyright for the deprecation const ALLOWABLE_METHODS = ['constructor', 'then', 'catch', 'finally']; -export function deprecatedPromiseObject(promise) { +export function deprecatedPromiseObject(promise: PromiseObjectProxy): PromiseObjectProxy { const handler = { - get(target, prop) { + get(target: object, prop: string, receiver?: object): unknown { if (!ALLOWABLE_METHODS.includes(prop)) { deprecate( `Accessing ${prop} is deprecated. Only available methods to access on a promise returned from model.save() are .then, .catch and .finally`, @@ -129,11 +124,9 @@ export function deprecatedPromiseObject(promise) { ); } - /* global Reflect */ - return Reflect.get(...arguments).bind(target); + return (Reflect.get(target, prop, receiver) as Function).bind(target); }, }; - /* global Proxy */ - return new Proxy(promise, handler); + return new Proxy(promise, handler) as PromiseObjectProxy; } diff --git a/packages/store/addon/-private/system/promise-proxy-base.d.ts b/packages/store/addon/-private/system/promise-proxy-base.d.ts index b08a536de64..e6b9c1934ef 100644 --- a/packages/store/addon/-private/system/promise-proxy-base.d.ts +++ b/packages/store/addon/-private/system/promise-proxy-base.d.ts @@ -6,28 +6,28 @@ export interface PromiseArrayProxy extends Promise {} export class PromiseArrayProxy extends ArrayProxy { declare content: T; - /** + /* * If the proxied promise is rejected this will contain the reason * provided. */ reason: string | Error; - /** + /* * Once the proxied promise has settled this will become `false`. */ isPending: boolean; - /** + /* * Once the proxied promise has settled this will become `true`. */ isSettled: boolean; - /** + /* * Will become `true` if the proxied promise is rejected. */ isRejected: boolean; - /** + /* * Will become `true` if the proxied promise is fulfilled. */ isFulfilled: boolean; - /** + /* * The promise whose fulfillment value is being proxied by this object. */ promise: Promise; @@ -37,28 +37,28 @@ export interface PromiseObjectProxy extends Promise {} export class PromiseObjectProxy extends ObjectProxy { declare content?: T | null; - /** + /* * If the proxied promise is rejected this will contain the reason * provided. */ reason: string | Error; - /** + /* * Once the proxied promise has settled this will become `false`. */ isPending: boolean; - /** + /* * Once the proxied promise has settled this will become `true`. */ isSettled: boolean; - /** + /* * Will become `true` if the proxied promise is rejected. */ isRejected: boolean; - /** + /* * Will become `true` if the proxied promise is fulfilled. */ isFulfilled: boolean; - /** + /* * The promise whose fulfillment value is being proxied by this object. */ promise: Promise; diff --git a/packages/store/addon/-private/system/record-array-manager.ts b/packages/store/addon/-private/system/record-array-manager.ts index 4ef25a5d6dc..eaeb5d423e5 100644 --- a/packages/store/addon/-private/system/record-array-manager.ts +++ b/packages/store/addon/-private/system/record-array-manager.ts @@ -8,9 +8,7 @@ import { set } from '@ember/object'; import { _backburner as emberBackburner } from '@ember/runloop'; import { DEBUG } from '@glimmer/env'; -import type { InternalModel } from 'ember-data/-private'; - -import isStableIdentifier from '../identifiers/is-stable-identifier'; +// import isStableIdentifier from '../identifiers/is-stable-identifier'; import type { CollectionResourceDocument, Meta } from '../ts-interfaces/ember-data-json-api'; import type { StableRecordIdentifier } from '../ts-interfaces/identifier'; import type { Dict } from '../ts-interfaces/utils'; @@ -27,17 +25,15 @@ export function recordArraysForIdentifier(identifier: StableRecordIdentifier): S const pendingForIdentifier: Set = new Set([]); -function getIdentifier(identifierOrInternalModel: StableRecordIdentifier | InternalModel): StableRecordIdentifier { - let i = identifierOrInternalModel; - if (!isStableIdentifier(identifierOrInternalModel)) { - // identifier may actually be an internalModel - // but during materialization we will get an identifier that - // has already been removed from the identifiers cache - // so it will not behave as if stable. This is a bug we should fix. - i = identifierOrInternalModel.identifier || i; - } +function getIdentifier(identifier: StableRecordIdentifier): StableRecordIdentifier { + // during dematerialization we will get an identifier that + // has already been removed from the identifiers cache + // so it will not behave as if stable. This is a bug we should fix. + // if (!isStableIdentifier(identifierOrInternalModel)) { + // console.log({ unstable: i }); + // } - return i; + return identifier; } function shouldIncludeInRecordArrays(store: CoreStore, identifier: StableRecordIdentifier): boolean { @@ -288,7 +284,8 @@ class RecordArrayManager { array = AdapterPopulatedRecordArray.create({ modelName, query: query, - content: A(), + content: A(), + isLoaded: false, store: this.store, manager: this, }); diff --git a/packages/store/addon/-private/system/record-arrays/adapter-populated-record-array.ts b/packages/store/addon/-private/system/record-arrays/adapter-populated-record-array.ts index 9c045fb70c0..cdbeba1a17e 100644 --- a/packages/store/addon/-private/system/record-arrays/adapter-populated-record-array.ts +++ b/packages/store/addon/-private/system/record-arrays/adapter-populated-record-array.ts @@ -1,4 +1,3 @@ -import { A } from '@ember/array'; import type NativeArray from '@ember/array/-private/native-array'; import { assert } from '@ember/debug'; @@ -19,7 +18,7 @@ export interface AdapterPopulatedRecordArrayCreateArgs { store: CoreStore; manager: RecordArrayManager; content: NativeArray; - isLoaded?: boolean; + isLoaded: boolean; query?: Dict; meta?: Meta; links?: Links | PaginationLinks | null; @@ -70,18 +69,16 @@ export interface AdapterPopulatedRecordArrayCreateArgs { @extends RecordArray */ export default class AdapterPopulatedRecordArray extends RecordArray { - declare links?: Links | PaginationLinks | null; - declare meta?: Dict; + declare links: Links | PaginationLinks | null; + declare meta: Dict | null; declare query: Dict | null; init(props?: AdapterPopulatedRecordArrayCreateArgs) { assert(`Cannot initialize AdapterPopulatedRecordArray with isUpdating`, !props || !('isUpdating' in props)); - super.init(); - this.set('content', this.get('content') || A()); - super.init(); this.query = this.query || null; this.links = this.links || null; + this.meta = this.meta || null; } replace() { diff --git a/packages/store/addon/-private/system/record-arrays/record-array.ts b/packages/store/addon/-private/system/record-arrays/record-array.ts index c98d3941379..8f5c8f09e14 100644 --- a/packages/store/addon/-private/system/record-arrays/record-array.ts +++ b/packages/store/addon/-private/system/record-arrays/record-array.ts @@ -166,7 +166,7 @@ export default class RecordArray extends ArrayProxy { if (this.isUpdating) { - return this._updatingPromise; + return this._updatingPromise!; } this.isUpdating = true; @@ -251,7 +251,7 @@ export default class RecordArray extends ArrayProxy; - public adapterOptions: Dict; - public include?: string; + declare length: number; + declare meta: Dict | null; + declare adapterOptions?: Dict; + declare include?: string; /** SnapshotRecordArray is not directly instantiable. Instances are provided to consuming application's - adapters and serializers for certain requests. + adapters and serializers for certain requests. @method constructor @private @constructor @param {RecordArray} recordArray @param {Object} meta - @param options + @param options */ - constructor(recordArray: RecordArray, meta?: Dict, options: Dict = {}) { + constructor(recordArray: RecordArray, meta: Dict | null, options: FindOptions = {}) { /** An array of snapshots @private diff --git a/packages/store/addon/-private/utils/promise-record.ts b/packages/store/addon/-private/utils/promise-record.ts index badbc0b9999..759d7984531 100644 --- a/packages/store/addon/-private/utils/promise-record.ts +++ b/packages/store/addon/-private/utils/promise-record.ts @@ -1,7 +1,7 @@ import type InternalModel from '../system/model/internal-model'; +import type { PromiseObject } from '../system/promise-proxies'; import { promiseObject } from '../system/promise-proxies'; -import type { DSModel } from '../ts-interfaces/ds-model'; -import type { PromiseProxy } from '../ts-interfaces/promise-proxies'; +import type { RecordInstance } from '../ts-interfaces/record-instance'; /** @module @ember-data/store @@ -18,7 +18,7 @@ import type { PromiseProxy } from '../ts-interfaces/promise-proxies'; export default function promiseRecord( internalModelPromise: Promise, label: string -): PromiseProxy { +): PromiseObject { let toReturn = internalModelPromise.then((internalModel) => internalModel.getRecord()); return promiseObject(toReturn, label); diff --git a/packages/store/types/@ember/runloop/-private/backburner.d.ts b/packages/store/types/@ember/runloop/-private/backburner.d.ts new file mode 100644 index 00000000000..7b3f3331284 --- /dev/null +++ b/packages/store/types/@ember/runloop/-private/backburner.d.ts @@ -0,0 +1,31 @@ +export interface QueueItem { + method: string; + target: object; + args: object[]; + stack: string | undefined; +} + +export interface DeferredActionQueues { + [index: string]: any; + queues: object; + schedule(queueName: string, target: any, method: any, args: any, onceFlag: boolean, stack: any): any; + flush(fromAutorun: boolean): any; +} + +export interface DebugInfo { + autorun: Error | undefined | null; + counters: object; + timers: QueueItem[]; + instanceStack: DeferredActionQueues[]; +} + +export interface Backburner { + join(fn: () => T): T; + on(...args: any[]): void; + scheduleOnce(...args: any[]): void; + run(fn: () => T): T; + schedule(queueName: string, target: object | null, method: (() => void) | string): void; + ensureInstance(): void; + DEBUG: boolean; + getDebugInfo(): DebugInfo; +} diff --git a/packages/store/types/@ember/runloop/index.d.ts b/packages/store/types/@ember/runloop/index.d.ts index fb9d8f5aa8e..7d4aef9e129 100644 --- a/packages/store/types/@ember/runloop/index.d.ts +++ b/packages/store/types/@ember/runloop/index.d.ts @@ -52,6 +52,7 @@ export interface RunNamespace { * started a RunLoop when calling this method one will be started for you * automatically. */ + schedule(queue: EmberRunQueues, target: Target, method: keyof Target, ...args: unknown[]): EmberRunTimer; schedule(queue: EmberRunQueues, target: Target, method: RunMethod, ...args: unknown[]): EmberRunTimer; schedule(queue: EmberRunQueues, method: (args: unknown[]) => unknown, ...args: unknown[]): EmberRunTimer; /** diff --git a/tsconfig.json b/tsconfig.json index 532325d962f..5936d79031e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -84,7 +84,6 @@ "packages/serializer/tests/dummy/app/config/environment.d.ts", "packages/serializer/tests/dummy/app/app.ts", "packages/serializer/addon/index.ts", - "packages/record-data/types/@ember/runloop/index.d.ts", "packages/record-data/types/@ember/polyfills/index.d.ts", "packages/record-data/tests/integration/graph/polymorphism/implicit-keys-test.ts", "packages/record-data/tests/integration/graph/graph-test.ts", From 28305a3da151a38b58a729d333726fbba2e383b3 Mon Sep 17 00:00:00 2001 From: Chris Thoburn Date: Sat, 16 Apr 2022 01:14:11 -0700 Subject: [PATCH 5/7] fix code stripping --- packages/-ember-data/ember-cli-build.js | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/packages/-ember-data/ember-cli-build.js b/packages/-ember-data/ember-cli-build.js index 1c29f48d99c..2306586d5d3 100644 --- a/packages/-ember-data/ember-cli-build.js +++ b/packages/-ember-data/ember-cli-build.js @@ -9,6 +9,27 @@ module.exports = function (defaults) { const terserSettings = { exclude: ['assets/dummy.js', 'assets/tests.js', 'assets/test-support.js', 'dist/docs/*', 'docs/*'], + terser: { + compress: { + ecma: 2016, // probably can be higher + passes: 6, // slow, but worth it + negate_iife: false, + sequences: 30, + defaults: true, + arguments: true, + keep_fargs: false, + toplevel: true, + unsafe: true, + unsafe_comps: true, + unsafe_math: true, + unsafe_symbols: true, + unsafe_proto: true, + unsafe_undefined: true, + }, + toplevel: true, + sourceMap: false, + ecma: 2016, + }, }; if (isTest && isProd) { From 2a6d6d1485d9783a1d0426ddd3b4dfc70d5ca20e Mon Sep 17 00:00:00 2001 From: Chris Thoburn Date: Sat, 16 Apr 2022 01:18:52 -0700 Subject: [PATCH 6/7] fix assert --- .../-private/system/promise-belongs-to.ts | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/packages/model/addon/-private/system/promise-belongs-to.ts b/packages/model/addon/-private/system/promise-belongs-to.ts index b05c6a0863d..48d4f089910 100644 --- a/packages/model/addon/-private/system/promise-belongs-to.ts +++ b/packages/model/addon/-private/system/promise-belongs-to.ts @@ -48,13 +48,17 @@ class PromiseBelongsTo extends Extended { // however, meta on relationships does not trigger change notifications. // if you need relationship meta, you should do `record.belongsTo(relationshipName).meta()` @computed() - get meta(): void { - return assert( - 'You attempted to access meta on the promise for the async belongsTo relationship ' + - `${this.get('_belongsToState').modelName}:${this.get('_belongsToState').key}'.` + - '\nUse `record.belongsTo(relationshipName).meta()` instead.', - false - ); + get meta() { + // eslint-disable-next-line no-constant-condition + if (1) { + assert( + 'You attempted to access meta on the promise for the async belongsTo relationship ' + + `${this.get('_belongsToState').modelName}:${this.get('_belongsToState').key}'.` + + '\nUse `record.belongsTo(relationshipName).meta()` instead.', + false + ); + } + return; } async reload(options: Dict): Promise { From a59fee72ff8b566a1e4be678a554866d99ab8851 Mon Sep 17 00:00:00 2001 From: Chris Thoburn Date: Sat, 16 Apr 2022 01:42:16 -0700 Subject: [PATCH 7/7] end demo --- .../private-build-infra/src/utilities/rollup-private-module.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/private-build-infra/src/utilities/rollup-private-module.js b/packages/private-build-infra/src/utilities/rollup-private-module.js index df45bda9c74..f7d11bd07ed 100644 --- a/packages/private-build-infra/src/utilities/rollup-private-module.js +++ b/packages/private-build-infra/src/utilities/rollup-private-module.js @@ -71,8 +71,11 @@ module.exports = function rollupPrivateModule(tree, options) { format: options.babelCompiler.shouldCompileModules() ? 'amd' : 'esm', amd: { id: `${packageName}/-private` }, exports: 'named', + generatedCode: 'es2015', + minifyInternalExports: true, }, ], + treeshake: true, external: externalDependencies, onwarn: onWarn, // cache: true|false Defaults to true