@ember-data/store 3.28.9 → 3.28.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2500,6 +2500,7 @@ abstract class CoreStore extends Service {
2500
2500
  const factory = internalModelFactoryFor(this);
2501
2501
 
2502
2502
  if (modelName === undefined) {
2503
+ this._notificationManager.destroy();
2503
2504
  factory.clear();
2504
2505
  } else {
2505
2506
  let normalizedModelName = normalizeModelName(modelName);
@@ -28,6 +28,8 @@ import Snapshot from '../snapshot';
28
28
  import { internalModelFactoryFor, setRecordIdentifier } from '../store/internal-model-factory';
29
29
  import RootState from './states';
30
30
 
31
+ type DSModel = import('../../ts-interfaces/ds-model').DSModel;
32
+
31
33
  type BelongsToRelationship = import('@ember-data/record-data/-private').BelongsToRelationship;
32
34
  type ManyRelationship = import('@ember-data/record-data/-private').ManyRelationship;
33
35
 
@@ -130,7 +132,7 @@ export default class InternalModel {
130
132
  declare _deferredTriggers: any;
131
133
  declare __recordArrays: any;
132
134
  declare references: any;
133
- declare _recordReference: any;
135
+ declare _recordReference: RecordReference;
134
136
  declare _manyArrayCache: ConfidentDict<ManyArray>;
135
137
 
136
138
  declare _relationshipPromisesCache: ConfidentDict<RSVP.Promise<any>>;
@@ -202,7 +204,7 @@ export default class InternalModel {
202
204
  }
203
205
  }
204
206
 
205
- get recordReference() {
207
+ get recordReference(): RecordReference {
206
208
  if (this._recordReference === null) {
207
209
  this._recordReference = new RecordReference(this.store, this.identifier);
208
210
  }
@@ -294,7 +296,7 @@ export default class InternalModel {
294
296
  }
295
297
  }
296
298
 
297
- getRecord(properties?) {
299
+ getRecord(properties?): Object {
298
300
  if (!this._record && !this._isDematerializing) {
299
301
  let { store } = this;
300
302
 
@@ -616,7 +618,7 @@ export default class InternalModel {
616
618
  "' with id " +
617
619
  parentInternalModel.id +
618
620
  ' 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 })`)',
619
- toReturn === null || !toReturn.get('isEmpty')
621
+ toReturn === null || !(toReturn as DSModel).isEmpty
620
622
  );
621
623
  return toReturn;
622
624
  }
@@ -675,7 +677,7 @@ export default class InternalModel {
675
677
  assert(`hasMany only works with the @ember-data/record-data package`);
676
678
  }
677
679
 
678
- getHasMany(key: string, options) {
680
+ getHasMany(key: string, options?) {
679
681
  if (HAS_RECORD_DATA_PACKAGE) {
680
682
  const graphFor = require('@ember-data/record-data/-private').graphFor;
681
683
  const relationship = graphFor(this.store).get(this.identifier, key);
@@ -793,11 +795,22 @@ export default class InternalModel {
793
795
  !this._record || this._record.get('isDestroyed') || this._record.get('isDestroying')
794
796
  );
795
797
  this.isDestroying = true;
798
+ if (this._recordReference) {
799
+ this._recordReference.destroy();
800
+ }
801
+ this._recordReference = null;
796
802
  let cache = this._manyArrayCache;
797
803
  Object.keys(cache).forEach((key) => {
798
804
  cache[key].destroy();
799
805
  delete cache[key];
800
806
  });
807
+ if (this.references) {
808
+ cache = this.references;
809
+ Object.keys(cache).forEach((key) => {
810
+ cache[key].destroy();
811
+ delete cache[key];
812
+ });
813
+ }
801
814
 
802
815
  internalModelFactoryFor(this.store).remove(this);
803
816
  this._isDestroyed = true;
@@ -806,6 +819,7 @@ export default class InternalModel {
806
819
  setupData(data) {
807
820
  let changedKeys = this._recordData.pushData(data, this.hasRecord);
808
821
  if (this.hasRecord) {
822
+ // TODO @runspired should this be going through the notification manager?
809
823
  this._record._notifyProperties(changedKeys);
810
824
  }
811
825
  this.send('pushedData');
@@ -966,10 +980,10 @@ export default class InternalModel {
966
980
  this.store._notificationManager.notify(this.identifier, 'state');
967
981
  } else {
968
982
  if (!key || key === 'isNew') {
969
- this.getRecord().notifyPropertyChange('isNew');
983
+ (this.getRecord() as DSModel).notifyPropertyChange('isNew');
970
984
  }
971
985
  if (!key || key === 'isDeleted') {
972
- this.getRecord().notifyPropertyChange('isDeleted');
986
+ (this.getRecord() as DSModel).notifyPropertyChange('isDeleted');
973
987
  }
974
988
  }
975
989
  }
@@ -1273,12 +1287,12 @@ export default class InternalModel {
1273
1287
  if (this._recordData.getErrors) {
1274
1288
  return this._recordData.getErrors(this.identifier).length > 0;
1275
1289
  } else {
1276
- let errors = get(this.getRecord(), 'errors');
1277
- return errors.get('length') > 0;
1290
+ let errors = (this.getRecord() as DSModel).errors;
1291
+ return errors.length > 0;
1278
1292
  }
1279
1293
  } else {
1280
- let errors = get(this.getRecord(), 'errors');
1281
- return errors.get('length') > 0;
1294
+ let errors = (this.getRecord() as DSModel).errors;
1295
+ return errors.length > 0;
1282
1296
  }
1283
1297
  }
1284
1298
 
@@ -1293,7 +1307,7 @@ export default class InternalModel {
1293
1307
  if (!this._recordData.getErrors) {
1294
1308
  for (attribute in parsedErrors) {
1295
1309
  if (hasOwnProperty.call(parsedErrors, attribute)) {
1296
- this.getRecord().errors._add(attribute, parsedErrors[attribute]);
1310
+ (this.getRecord() as DSModel).errors._add(attribute, parsedErrors[attribute]);
1297
1311
  }
1298
1312
  }
1299
1313
  }
@@ -1313,7 +1327,7 @@ export default class InternalModel {
1313
1327
 
1314
1328
  for (attribute in parsedErrors) {
1315
1329
  if (hasOwnProperty.call(parsedErrors, attribute)) {
1316
- this.getRecord().errors._add(attribute, parsedErrors[attribute]);
1330
+ (this.getRecord() as DSModel).errors._add(attribute, parsedErrors[attribute]);
1317
1331
  }
1318
1332
  }
1319
1333
 
@@ -3,7 +3,7 @@
3
3
  */
4
4
  import { assert } from '@ember/debug';
5
5
 
6
- import { REQUEST_SERVICE } from '@ember-data/canary-features';
6
+ import { CUSTOM_MODEL_CLASS, REQUEST_SERVICE } from '@ember-data/canary-features';
7
7
  /*
8
8
  This file encapsulates the various states that a record can transition
9
9
  through during its lifecycle.
@@ -238,6 +238,7 @@ const DirtyState = {
238
238
  //TODO(Igor) reloading now triggers a
239
239
  //loadingData event, though it seems fine?
240
240
  loadingData() {},
241
+ becameValid() {},
241
242
 
242
243
  propertyWasReset(internalModel, name) {
243
244
  if (!internalModel.hasChangedAttributes()) {
@@ -431,6 +432,12 @@ createdState.uncommitted.rollback = function (internalModel) {
431
432
  };
432
433
 
433
434
  createdState.uncommitted.pushedData = function (internalModel) {
435
+ // TODO @runspired consider where to do this once we kill off state machine
436
+ if (CUSTOM_MODEL_CLASS) {
437
+ internalModel.store._notificationManager.notify(internalModel.identifier, 'identity');
438
+ } else {
439
+ internalModel.notifyPropertyChange('id');
440
+ }
434
441
  internalModel.transitionTo('loaded.updated.uncommitted');
435
442
  internalModel.triggerLater('didLoad');
436
443
  };
@@ -27,7 +27,7 @@ export function recordArraysForIdentifier(identifierOrInternalModel) {
27
27
  return RecordArraysCache.get(identifierOrInternalModel);
28
28
  }
29
29
 
30
- const pendingForIdentifier = new Set([]);
30
+ const pendingForIdentifier = new Set();
31
31
  const IMDematerializing = new WeakMap();
32
32
 
33
33
  const getIdentifier = function getIdentifier(identifierOrInternalModel) {
@@ -1,13 +1,16 @@
1
- import { identifierCacheFor } from '../identifiers/cache';
1
+ import { assert } from '@ember/debug';
2
+ import { DEBUG } from '@glimmer/env';
3
+
4
+ import isStableIdentifier from '../identifiers/is-stable-identifier';
2
5
 
3
6
  type CoreStore = import('./core-store').default;
4
- type RecordIdentifier = import('../ts-interfaces/identifier').RecordIdentifier;
5
7
  type StableRecordIdentifier = import('../ts-interfaces/identifier').StableRecordIdentifier;
6
8
 
7
- type UnsubscribeToken = Object;
9
+ type UnsubscribeToken = object;
10
+ let tokenId = 0;
8
11
 
9
- const Cache = new WeakMap<StableRecordIdentifier, NotificationCallback>();
10
- const Tokens = new WeakMap<UnsubscribeToken, StableRecordIdentifier>();
12
+ const Cache = new Map<StableRecordIdentifier, Map<UnsubscribeToken, NotificationCallback>>();
13
+ const Tokens = new Map<UnsubscribeToken, StableRecordIdentifier>();
11
14
 
12
15
  export type NotificationType =
13
16
  | 'attributes'
@@ -20,42 +23,73 @@ export type NotificationType =
20
23
  | 'property'; // 'property' is an internal EmberData only transition period concept.
21
24
 
22
25
  export interface NotificationCallback {
23
- (identifier: RecordIdentifier, notificationType: 'attributes' | 'relationships' | 'property', key?: string): void;
24
- (identifier: RecordIdentifier, notificationType: 'errors' | 'meta' | 'identity' | 'unload' | 'state'): void;
26
+ (identifier: StableRecordIdentifier, notificationType: 'attributes' | 'relationships', key?: string): void;
27
+ (identifier: StableRecordIdentifier, notificationType: 'errors' | 'meta' | 'identity' | 'state'): void;
25
28
  (identifier: StableRecordIdentifier, notificationType: NotificationType, key?: string): void;
26
29
  }
27
30
 
31
+ // TODO this isn't importable anyway, remove and use a map on the manager?
28
32
  export function unsubscribe(token: UnsubscribeToken) {
29
33
  let identifier = Tokens.get(token);
30
- if (!identifier) {
31
- throw new Error('Passed unknown unsubscribe token to unsubscribe');
34
+
35
+ if (identifier) {
36
+ Tokens.delete(token);
37
+ const map = Cache.get(identifier);
38
+ map?.delete(token);
32
39
  }
33
- Tokens.delete(token);
34
- Cache.delete(identifier);
35
40
  }
36
41
  /*
37
42
  Currently only support a single callback per identifier
38
43
  */
39
44
  export default class NotificationManager {
40
- constructor(private store: CoreStore) {}
45
+ declare store: CoreStore;
46
+ constructor(store: CoreStore) {
47
+ this.store = store;
48
+ }
49
+
50
+ subscribe(identifier: StableRecordIdentifier, callback: NotificationCallback): UnsubscribeToken {
51
+ assert(`Expected to receive a stable Identifier to subscribe to`, isStableIdentifier(identifier));
52
+ let map = Cache.get(identifier);
41
53
 
42
- subscribe(identifier: RecordIdentifier, callback: NotificationCallback): UnsubscribeToken {
43
- let stableIdentifier = identifierCacheFor(this.store).getOrCreateRecordIdentifier(identifier);
44
- Cache.set(stableIdentifier, callback);
45
- let unsubToken = {};
46
- Tokens.set(unsubToken, stableIdentifier);
54
+ if (!map) {
55
+ map = new Map();
56
+ Cache.set(identifier, map);
57
+ }
58
+
59
+ let unsubToken = DEBUG ? { _tokenRef: tokenId++ } : {};
60
+ map.set(unsubToken, callback);
61
+ Tokens.set(unsubToken, identifier);
47
62
  return unsubToken;
48
63
  }
49
64
 
50
- notify(identifier: RecordIdentifier, value: 'attributes' | 'relationships' | 'property', key?: string): boolean;
51
- notify(identifier: RecordIdentifier, value: 'errors' | 'meta' | 'identity' | 'unload' | 'state'): boolean;
52
- notify(identifier: RecordIdentifier, value: NotificationType, key?: string): boolean {
53
- let stableIdentifier = identifierCacheFor(this.store).getOrCreateRecordIdentifier(identifier);
54
- let callback = Cache.get(stableIdentifier);
55
- if (!callback) {
65
+ unsubscribe(token: UnsubscribeToken) {
66
+ unsubscribe(token);
67
+ }
68
+
69
+ // deactivated type signature overloads because pass-through was failing to match any. Bring back if possible.
70
+ // notify(identifier: StableRecordIdentifier, value: 'attributes' | 'relationships', key?: string): boolean;
71
+ // notify(identifier: StableRecordIdentifier, value: 'errors' | 'meta' | 'identity' | 'state'): boolean;
72
+ notify(identifier: StableRecordIdentifier, value: NotificationType, key?: string): boolean {
73
+ assert(
74
+ `Notify does not accept a key argument for the namespace '${value}'. Received key '${key}'.`,
75
+ !key || value === 'attributes' || value === 'relationships'
76
+ );
77
+ if (!isStableIdentifier(identifier)) {
78
+ return false;
79
+ }
80
+
81
+ let callbackMap = Cache.get(identifier);
82
+ if (!callbackMap || !callbackMap.size) {
56
83
  return false;
57
84
  }
58
- callback(stableIdentifier, value, key);
85
+ callbackMap.forEach((cb) => {
86
+ cb(identifier, value, key);
87
+ });
59
88
  return true;
60
89
  }
90
+
91
+ destroy() {
92
+ Tokens.clear();
93
+ Cache.clear();
94
+ }
61
95
  }
@@ -1,13 +1,24 @@
1
1
  import { deprecate } from '@ember/debug';
2
+ import { dependentKeyCompat } from '@ember/object/compat';
3
+ import { cached, tracked } from '@glimmer/tracking';
2
4
 
3
5
  import { resolve } from 'rsvp';
4
6
 
7
+ import { CUSTOM_MODEL_CLASS } from '@ember-data/canary-features';
5
8
  import { DEPRECATE_BELONGS_TO_REFERENCE_PUSH } from '@ember-data/private-build-infra/deprecations';
6
9
  import { assertPolymorphicType } from '@ember-data/store/-debug';
7
10
 
11
+ import { unsubscribe } from '../record-notification-manager';
8
12
  import { internalModelFactoryFor, peekRecordIdentifier, recordIdentifierFor } from '../store/internal-model-factory';
9
13
  import Reference from './reference';
10
14
 
15
+ type RecordReference = import('./record').default;
16
+ type NotificationType = import('../record-notification-manager').NotificationType;
17
+ type CoreStore = import('../core-store').default;
18
+ type StableRecordIdentifier = import('../../ts-interfaces/identifier').StableRecordIdentifier;
19
+ type SingleResourceDocument = import('../../ts-interfaces/ember-data-json-api').SingleResourceDocument;
20
+
21
+ type BelongsToRelationship = import('@ember-data/record-data/-private').BelongsToRelationship;
11
22
  /**
12
23
  @module @ember-data/store
13
24
  */
@@ -22,17 +33,84 @@ import Reference from './reference';
22
33
  @extends Reference
23
34
  */
24
35
  export default class BelongsToReference extends Reference {
25
- constructor(store, parentIMOrIdentifier, belongsToRelationship, key) {
26
- super(store, parentIMOrIdentifier);
36
+ declare key: string;
37
+ declare belongsToRelationship: BelongsToRelationship;
38
+ declare type: string;
39
+ declare parent: RecordReference;
40
+ declare parentIdentifier: StableRecordIdentifier;
41
+
42
+ // unsubscribe tokens given to us by the notification manager
43
+ #token!: object;
44
+ #relatedToken: object | null = null;
45
+
46
+ @tracked _ref = 0;
47
+
48
+ constructor(
49
+ store: CoreStore,
50
+ parentIdentifier: StableRecordIdentifier,
51
+ belongsToRelationship: BelongsToRelationship,
52
+ key: string
53
+ ) {
54
+ super(store, parentIdentifier);
27
55
  this.key = key;
28
56
  this.belongsToRelationship = belongsToRelationship;
29
57
  this.type = belongsToRelationship.definition.type;
30
- this.parent = internalModelFactoryFor(store).peek(parentIMOrIdentifier).recordReference;
31
- this.parentIdentifier = parentIMOrIdentifier;
58
+ const parent = internalModelFactoryFor(store).peek(parentIdentifier);
59
+ this.parent = parent!.recordReference;
60
+ this.parentIdentifier = parentIdentifier;
61
+
62
+ if (CUSTOM_MODEL_CLASS) {
63
+ this.#token = store._notificationManager.subscribe(
64
+ parentIdentifier,
65
+ (_: StableRecordIdentifier, bucket: NotificationType, notifiedKey?: string) => {
66
+ if ((bucket === 'relationships' || bucket === 'property') && notifiedKey === key) {
67
+ this._ref++;
68
+ }
69
+ }
70
+ );
71
+ }
32
72
 
33
73
  // TODO inverse
34
74
  }
35
75
 
76
+ destroy() {
77
+ if (CUSTOM_MODEL_CLASS) {
78
+ unsubscribe(this.#token);
79
+ this.#token = null as unknown as object;
80
+ if (this.#relatedToken) {
81
+ unsubscribe(this.#relatedToken);
82
+ this.#relatedToken = null;
83
+ }
84
+ }
85
+ }
86
+
87
+ @cached
88
+ @dependentKeyCompat
89
+ get _relatedIdentifier(): StableRecordIdentifier | null {
90
+ this._ref; // consume the tracked prop
91
+ if (this.#relatedToken) {
92
+ unsubscribe(this.#relatedToken);
93
+ this.#relatedToken = null;
94
+ }
95
+
96
+ let resource = this._resource();
97
+ if (resource && resource.data) {
98
+ const identifier = this.store.identifierCache.getOrCreateRecordIdentifier(resource.data);
99
+ this.#relatedToken = this.store._notificationManager.subscribe(
100
+ identifier,
101
+ (_: StableRecordIdentifier, bucket: NotificationType, notifiedKey?: string) => {
102
+ if (bucket === 'identity' || ((bucket === 'attributes' || bucket === 'property') && notifiedKey === 'id')) {
103
+ this._ref++;
104
+ }
105
+ }
106
+ );
107
+
108
+ return identifier;
109
+ }
110
+
111
+ return null;
112
+ }
113
+
36
114
  /**
37
115
  The `id` of the record that this reference refers to. Together, the
38
116
  `type()` and `id()` methods form a composite key for the identity
@@ -73,13 +151,18 @@ export default class BelongsToReference extends Reference {
73
151
  @public
74
152
  @return {String} The id of the record in this belongsTo relationship.
75
153
  */
76
- id() {
77
- let id = null;
154
+ id(): string | null {
155
+ if (CUSTOM_MODEL_CLASS) {
156
+ return this._relatedIdentifier?.id || null;
157
+ }
78
158
  let resource = this._resource();
79
159
  if (resource && resource.data) {
80
- id = resource.data.id;
160
+ const identifier = this.store.identifierCache.getOrCreateRecordIdentifier(resource.data);
161
+
162
+ return identifier.id;
81
163
  }
82
- return id;
164
+
165
+ return null;
83
166
  }
84
167
 
85
168
  _resource() {
@@ -132,10 +215,10 @@ export default class BelongsToReference extends Reference {
132
215
  @param {Object|Promise} objectOrPromise a promise that resolves to a JSONAPI document object describing the new value of this relationship.
133
216
  @return {Promise<record>} A promise that resolves with the new value in this belongs-to relationship.
134
217
  */
135
- push(objectOrPromise) {
218
+ async push(objectOrPromise: Object | SingleResourceDocument): Promise<Object> {
136
219
  // TODO deprecate thenable support
137
220
  return resolve(objectOrPromise).then((data) => {
138
- let record;
221
+ let record: Object;
139
222
 
140
223
  if (DEPRECATE_BELONGS_TO_REFERENCE_PUSH && peekRecordIdentifier(data)) {
141
224
  deprecate('Pushing a record into a BelongsToReference is deprecated', false, {
@@ -147,15 +230,16 @@ export default class BelongsToReference extends Reference {
147
230
  enabled: '3.16',
148
231
  },
149
232
  });
150
- record = data;
233
+ record = data as Object;
151
234
  } else {
152
- record = this.store.push(data);
235
+ record = this.store.push(data as SingleResourceDocument);
153
236
  }
154
237
 
238
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call
155
239
  assertPolymorphicType(
156
240
  this.belongsToRelationship.identifier,
157
241
  this.belongsToRelationship.definition,
158
- record._internalModel.identifier,
242
+ recordIdentifierFor(record),
159
243
  this.store
160
244
  );
161
245
 
@@ -223,7 +307,7 @@ export default class BelongsToReference extends Reference {
223
307
  @public
224
308
  @return {Model} the record in this relationship
225
309
  */
226
- value() {
310
+ value(): Object | null {
227
311
  let resource = this._resource();
228
312
  if (resource && resource.data) {
229
313
  let inverseInternalModel = this.store._internalModelForResource(resource.data);
@@ -299,7 +383,7 @@ export default class BelongsToReference extends Reference {
299
383
  */
300
384
  load(options) {
301
385
  let parentInternalModel = internalModelFactoryFor(this.store).peek(this.parentIdentifier);
302
- return parentInternalModel.getBelongsTo(this.key, options);
386
+ return parentInternalModel!.getBelongsTo(this.key, options);
303
387
  }
304
388
 
305
389
  /**
@@ -354,7 +438,7 @@ export default class BelongsToReference extends Reference {
354
438
  */
355
439
  reload(options) {
356
440
  let parentInternalModel = internalModelFactoryFor(this.store).peek(this.parentIdentifier);
357
- return parentInternalModel.reloadBelongsTo(this.key, options).then((internalModel) => {
441
+ return parentInternalModel!.reloadBelongsTo(this.key, options).then((internalModel) => {
358
442
  return this.value();
359
443
  });
360
444
  }
@@ -1,12 +1,26 @@
1
+ import { dependentKeyCompat } from '@ember/object/compat';
1
2
  import { DEBUG } from '@glimmer/env';
3
+ import { cached, tracked } from '@glimmer/tracking';
2
4
 
3
5
  import { resolve } from 'rsvp';
4
6
 
7
+ import { CUSTOM_MODEL_CLASS } from '@ember-data/canary-features';
5
8
  import { assertPolymorphicType } from '@ember-data/store/-debug';
6
9
 
10
+ import { unsubscribe } from '../record-notification-manager';
7
11
  import { internalModelFactoryFor, recordIdentifierFor } from '../store/internal-model-factory';
8
12
  import Reference, { internalModelForReference } from './reference';
9
13
 
14
+ type RecordReference = import('./record').default;
15
+ type NotificationType = import('../record-notification-manager').NotificationType;
16
+ type CoreStore = import('../core-store').default;
17
+ type StableRecordIdentifier = import('../../ts-interfaces/identifier').StableRecordIdentifier;
18
+ type CollectionResourceDocument = import('../../ts-interfaces/ember-data-json-api').CollectionResourceDocument;
19
+ type ExistingResourceObject = import('../../ts-interfaces/ember-data-json-api').ExistingResourceObject;
20
+ type SingleResourceDocument = import('../../ts-interfaces/ember-data-json-api').SingleResourceDocument;
21
+
22
+ type ManyRelationship = import('@ember-data/record-data/-private').ManyRelationship;
23
+
10
24
  /**
11
25
  @module @ember-data/store
12
26
  */
@@ -20,17 +34,102 @@ import Reference, { internalModelForReference } from './reference';
20
34
  @extends Reference
21
35
  */
22
36
  export default class HasManyReference extends Reference {
23
- constructor(store, parentIMOrIdentifier, hasManyRelationship, key) {
24
- super(store, parentIMOrIdentifier);
37
+ declare key: string;
38
+ declare hasManyRelationship: ManyRelationship;
39
+ declare type: string;
40
+ declare parent: RecordReference;
41
+ declare parentIdentifier: StableRecordIdentifier;
42
+
43
+ // unsubscribe tokens given to us by the notification manager
44
+ #token!: Object;
45
+ #relatedTokenMap!: Map<StableRecordIdentifier, Object>;
46
+
47
+ @tracked _ref = 0;
48
+
49
+ constructor(
50
+ store: CoreStore,
51
+ parentIdentifier: StableRecordIdentifier,
52
+ hasManyRelationship: ManyRelationship,
53
+ key: string
54
+ ) {
55
+ super(store, parentIdentifier);
25
56
  this.key = key;
26
57
  this.hasManyRelationship = hasManyRelationship;
27
58
  this.type = hasManyRelationship.definition.type;
28
59
 
29
- this.parent = internalModelFactoryFor(store).peek(parentIMOrIdentifier).recordReference;
60
+ this.parent = internalModelFactoryFor(store).peek(parentIdentifier)!.recordReference;
30
61
 
62
+ if (CUSTOM_MODEL_CLASS) {
63
+ this.#token = store._notificationManager.subscribe(
64
+ parentIdentifier,
65
+ (_: StableRecordIdentifier, bucket: NotificationType, notifiedKey?: string) => {
66
+ if ((bucket === 'relationships' || bucket === 'property') && notifiedKey === key) {
67
+ this._ref++;
68
+ }
69
+ }
70
+ );
71
+ this.#relatedTokenMap = new Map();
72
+ }
31
73
  // TODO inverse
32
74
  }
33
75
 
76
+ destroy() {
77
+ if (CUSTOM_MODEL_CLASS) {
78
+ unsubscribe(this.#token);
79
+ this.#token = null as unknown as object;
80
+ this.#relatedTokenMap.forEach((token) => {
81
+ unsubscribe(token);
82
+ });
83
+ this.#relatedTokenMap.clear();
84
+ }
85
+ }
86
+
87
+ @cached
88
+ @dependentKeyCompat
89
+ get _relatedIdentifiers(): StableRecordIdentifier[] {
90
+ this._ref; // consume the tracked prop
91
+
92
+ let resource = this._resource();
93
+
94
+ let map = this.#relatedTokenMap;
95
+ this.#relatedTokenMap = new Map();
96
+
97
+ if (resource && resource.data) {
98
+ return resource.data.map((resourceIdentifier) => {
99
+ const identifier = this.store.identifierCache.getOrCreateRecordIdentifier(resourceIdentifier);
100
+
101
+ let token = map.get(identifier);
102
+
103
+ if (token) {
104
+ map.delete(identifier);
105
+ } else {
106
+ token = this.store._notificationManager.subscribe(
107
+ identifier,
108
+ (_: StableRecordIdentifier, bucket: NotificationType, notifiedKey?: string) => {
109
+ if (
110
+ bucket === 'identity' ||
111
+ ((bucket === 'attributes' || bucket === 'property') && notifiedKey === 'id')
112
+ ) {
113
+ this._ref++;
114
+ }
115
+ }
116
+ );
117
+ }
118
+
119
+ this.#relatedTokenMap.set(identifier, token);
120
+
121
+ return identifier;
122
+ });
123
+ }
124
+
125
+ map.forEach((token) => {
126
+ this.store._notificationManager.unsubscribe(token);
127
+ });
128
+ map.clear();
129
+
130
+ return [];
131
+ }
132
+
34
133
  _resource() {
35
134
  return this.recordData.getHasMany(this.key);
36
135
  }
@@ -77,7 +176,7 @@ export default class HasManyReference extends Reference {
77
176
  @public
78
177
  @return {String} The name of the remote type. This should either be `link` or `ids`
79
178
  */
80
- remoteType() {
179
+ remoteType(): 'link' | 'ids' {
81
180
  let value = this._resource();
82
181
  if (value && value.links && value.links.related) {
83
182
  return 'link';
@@ -121,15 +220,22 @@ export default class HasManyReference extends Reference {
121
220
  @public
122
221
  @return {Array} The ids in this has-many relationship
123
222
  */
124
- ids() {
223
+ ids(): Array<string | null> {
224
+ if (CUSTOM_MODEL_CLASS) {
225
+ return this._relatedIdentifiers.map((identifier) => identifier.id);
226
+ }
227
+
125
228
  let resource = this._resource();
126
229
 
127
- let ids = [];
128
- if (resource.data) {
129
- ids = resource.data.map((data) => data.id);
230
+ if (resource && resource.data) {
231
+ return resource.data.map((resourceIdentifier) => {
232
+ const identifier = this.store.identifierCache.getOrCreateRecordIdentifier(resourceIdentifier);
233
+
234
+ return identifier.id;
235
+ });
130
236
  }
131
237
 
132
- return ids;
238
+ return [];
133
239
  }
134
240
 
135
241
  /**
@@ -177,45 +283,50 @@ export default class HasManyReference extends Reference {
177
283
  @param {Array|Promise} objectOrPromise a promise that resolves to a JSONAPI document object describing the new value of this relationship.
178
284
  @return {ManyArray}
179
285
  */
180
- push(objectOrPromise) {
181
- return resolve(objectOrPromise).then((payload) => {
182
- let array = payload;
183
-
184
- if (typeof payload === 'object' && payload.data) {
185
- array = payload.data;
186
- }
286
+ async push(
287
+ objectOrPromise: ExistingResourceObject[] | CollectionResourceDocument | { data: SingleResourceDocument[] }
288
+ ): Promise<any> {
289
+ const payload = await resolve(objectOrPromise);
290
+ let array: Array<ExistingResourceObject | SingleResourceDocument>;
291
+
292
+ if (!Array.isArray(payload) && typeof payload === 'object' && Array.isArray(payload.data)) {
293
+ array = payload.data;
294
+ } else {
295
+ array = payload as ExistingResourceObject[];
296
+ }
187
297
 
188
- let internalModel = internalModelForReference(this);
298
+ const internalModel = internalModelForReference(this)!;
299
+ const { store } = this;
189
300
 
190
- let identifiers = array.map((obj) => {
191
- let record = this.store.push(obj);
301
+ let identifiers = array.map((obj) => {
302
+ let record;
303
+ if ('data' in obj) {
304
+ // TODO deprecate pushing non-valid JSON:API here
305
+ record = store.push(obj);
306
+ } else {
307
+ record = store.push({ data: obj });
308
+ }
192
309
 
193
- if (DEBUG) {
194
- let relationshipMeta = this.hasManyRelationship.definition;
195
- assertPolymorphicType(
196
- internalModel.identifier,
197
- relationshipMeta,
198
- record._internalModel.identifier,
199
- this.store
200
- );
201
- }
202
- return recordIdentifierFor(record);
203
- });
310
+ if (DEBUG) {
311
+ let relationshipMeta = this.hasManyRelationship.definition;
312
+ let identifier = this.hasManyRelationship.identifier;
313
+ assertPolymorphicType(identifier, relationshipMeta, recordIdentifierFor(record), store);
314
+ }
315
+ return recordIdentifierFor(record);
316
+ });
204
317
 
205
- const { graph, identifier } = this.hasManyRelationship;
206
- this.store._backburner.join(() => {
207
- graph.push({
208
- op: 'replaceRelatedRecords',
209
- record: identifier,
210
- field: this.key,
211
- value: identifiers,
212
- });
318
+ const { graph, identifier } = this.hasManyRelationship;
319
+ store._backburner.join(() => {
320
+ graph.push({
321
+ op: 'replaceRelatedRecords',
322
+ record: identifier,
323
+ field: this.key,
324
+ value: identifiers,
213
325
  });
214
-
215
- return internalModel.getHasMany(this.key);
216
- // TODO IGOR it seems wrong that we were returning the many array here
217
- //return this.hasManyRelationship.manyArray;
218
326
  });
327
+
328
+ // TODO IGOR it seems wrong that we were returning the many array here
329
+ return internalModel.getHasMany(this.key);
219
330
  }
220
331
 
221
332
  _isLoaded() {
@@ -275,7 +386,7 @@ export default class HasManyReference extends Reference {
275
386
  @return {ManyArray}
276
387
  */
277
388
  value() {
278
- let internalModel = internalModelForReference(this);
389
+ let internalModel = internalModelForReference(this)!;
279
390
  if (this._isLoaded()) {
280
391
  return internalModel.getManyArray(this.key);
281
392
  }
@@ -348,7 +459,7 @@ export default class HasManyReference extends Reference {
348
459
  this has-many relationship.
349
460
  */
350
461
  load(options) {
351
- let internalModel = internalModelForReference(this);
462
+ let internalModel = internalModelForReference(this)!;
352
463
  return internalModel.getHasMany(this.key, options);
353
464
  }
354
465
 
@@ -403,7 +514,7 @@ export default class HasManyReference extends Reference {
403
514
  @return {Promise} a promise that resolves with the ManyArray in this has-many relationship.
404
515
  */
405
516
  reload(options) {
406
- let internalModel = internalModelForReference(this);
517
+ let internalModel = internalModelForReference(this)!;
407
518
  return internalModel.reloadHasMany(this.key, options);
408
519
  }
409
520
  }
@@ -1,7 +1,16 @@
1
+ import { dependentKeyCompat } from '@ember/object/compat';
2
+ import { cached, tracked } from '@glimmer/tracking';
3
+
1
4
  import RSVP, { resolve } from 'rsvp';
2
5
 
6
+ import { CUSTOM_MODEL_CLASS } from '@ember-data/canary-features';
7
+
8
+ import { unsubscribe } from '../record-notification-manager';
3
9
  import Reference, { internalModelForReference, REFERENCE_CACHE } from './reference';
4
10
 
11
+ type NotificationType = import('../record-notification-manager').NotificationType;
12
+
13
+ type CoreStore = import('../core-store').default;
5
14
  type SingleResourceDocument = import('../../ts-interfaces/ember-data-json-api').SingleResourceDocument;
6
15
  type RecordInstance = import('../../ts-interfaces/record-instance').RecordInstance;
7
16
  type StableRecordIdentifier = import('../../ts-interfaces/identifier').StableRecordIdentifier;
@@ -19,11 +28,39 @@ type StableRecordIdentifier = import('../../ts-interfaces/identifier').StableRec
19
28
  @extends Reference
20
29
  */
21
30
  export default class RecordReference extends Reference {
31
+ // unsubscribe token given to us by the notification manager
32
+ #token!: Object;
33
+
34
+ @tracked _ref = 0;
35
+
36
+ constructor(public store: CoreStore, identifier: StableRecordIdentifier) {
37
+ super(store, identifier);
38
+ if (CUSTOM_MODEL_CLASS) {
39
+ this.#token = store._notificationManager.subscribe(
40
+ identifier,
41
+ (_: StableRecordIdentifier, bucket: NotificationType, notifiedKey?: string) => {
42
+ if (bucket === 'identity' || ((bucket === 'attributes' || bucket === 'property') && notifiedKey === 'id')) {
43
+ this._ref++;
44
+ }
45
+ }
46
+ );
47
+ }
48
+ }
49
+
50
+ destroy() {
51
+ if (CUSTOM_MODEL_CLASS) {
52
+ unsubscribe(this.#token);
53
+ }
54
+ }
55
+
22
56
  public get type(): string {
23
57
  return this.identifier().type;
24
58
  }
25
59
 
60
+ @cached
61
+ @dependentKeyCompat
26
62
  private get _id(): string | null {
63
+ this._ref; // consume the tracked prop
27
64
  let identifier = this.identifier();
28
65
  if (identifier) {
29
66
  return identifier.id;
@@ -51,7 +88,15 @@ export default class RecordReference extends Reference {
51
88
  @return {String} The id of the record.
52
89
  */
53
90
  id() {
54
- return this._id;
91
+ if (CUSTOM_MODEL_CLASS) {
92
+ return this._id;
93
+ }
94
+ let identifier = this.identifier();
95
+ if (identifier) {
96
+ return identifier.id;
97
+ }
98
+
99
+ return null;
55
100
  }
56
101
 
57
102
  /**
@@ -93,7 +138,7 @@ export default class RecordReference extends Reference {
93
138
  @public
94
139
  @return {String} 'identity'
95
140
  */
96
- remoteType(): 'link' | 'id' | 'identity' {
141
+ remoteType(): 'identity' {
97
142
  return 'identity';
98
143
  }
99
144
 
@@ -160,7 +205,7 @@ export default class RecordReference extends Reference {
160
205
  @return {Model} the record for this RecordReference
161
206
  */
162
207
  value(): RecordInstance | null {
163
- if (this._id !== null) {
208
+ if (this.id() !== null) {
164
209
  let internalModel = internalModelForReference(this);
165
210
  if (internalModel && internalModel.currentState.isLoaded) {
166
211
  return internalModel.getRecord();
@@ -187,8 +232,9 @@ export default class RecordReference extends Reference {
187
232
  @return {Promise<record>} the record for this RecordReference
188
233
  */
189
234
  load() {
190
- if (this._id !== null) {
191
- return this.store.findRecord(this.type, this._id);
235
+ const id = this.id();
236
+ if (id !== null) {
237
+ return this.store.findRecord(this.type, id);
192
238
  }
193
239
  throw new Error(`Unable to fetch record of type ${this.type} without an id`);
194
240
  }
@@ -211,8 +257,9 @@ export default class RecordReference extends Reference {
211
257
  @return {Promise<record>} the record for this RecordReference
212
258
  */
213
259
  reload() {
214
- if (this._id !== null) {
215
- return this.store.findRecord(this.type, this._id, { reload: true });
260
+ const id = this.id();
261
+ if (id !== null) {
262
+ return this.store.findRecord(this.type, id, { reload: true });
216
263
  }
217
264
  throw new Error(`Unable to fetch record of type ${this.type} without an id`);
218
265
  }
@@ -100,7 +100,7 @@ abstract class Reference {
100
100
  @public
101
101
  @return {String} The name of the remote type. This should either be "link" or "ids"
102
102
  */
103
- remoteType(): 'link' | 'id' | 'identity' {
103
+ remoteType(): 'link' | 'id' | 'ids' | 'identity' {
104
104
  let value = this._resource();
105
105
  if (isResourceIdentiferWithRelatedLinks(value)) {
106
106
  return 'link';
@@ -19,6 +19,7 @@ export interface DSModel extends RecordInstance, EmberObject {
19
19
  isDeleted: boolean;
20
20
  deleteRecord(): void;
21
21
  unloadRecord(): void;
22
+ errors: any;
22
23
  }
23
24
 
24
25
  // Implemented by both ShimModelClass and DSModel
package/index.js CHANGED
@@ -10,6 +10,8 @@ module.exports = Object.assign({}, addonBaseConfig, {
10
10
  shouldRollupPrivate: true,
11
11
  externalDependenciesForPrivateModule() {
12
12
  return [
13
+ 'ember-cached-decorator-polyfill',
14
+
13
15
  '@ember-data/canary-features',
14
16
  '@ember-data/store/-debug',
15
17
 
@@ -23,6 +25,7 @@ module.exports = Object.assign({}, addonBaseConfig, {
23
25
  '@ember/object/evented',
24
26
  '@ember/object/internals',
25
27
  '@ember/object/mixin',
28
+ '@ember/object/compat',
26
29
  '@ember/object/promise-proxy-mixin',
27
30
  '@ember/object/proxy',
28
31
  '@ember/polyfills',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ember-data/store",
3
- "version": "3.28.9",
3
+ "version": "3.28.12",
4
4
  "description": "The default blueprint for ember-cli addons.",
5
5
  "keywords": [
6
6
  "ember-addon"
@@ -17,16 +17,17 @@
17
17
  "start": "ember serve"
18
18
  },
19
19
  "dependencies": {
20
- "@ember-data/canary-features": "3.28.9",
21
- "@ember-data/private-build-infra": "3.28.9",
20
+ "@ember-data/canary-features": "3.28.12",
21
+ "@ember-data/private-build-infra": "3.28.12",
22
22
  "@ember/string": "^3.0.0",
23
23
  "@glimmer/tracking": "^1.0.4",
24
+ "ember-cached-decorator-polyfill": "^0.1.4",
24
25
  "ember-cli-babel": "^7.26.6",
25
26
  "ember-cli-path-utils": "^1.0.0",
26
27
  "ember-cli-typescript": "^4.1.0"
27
28
  },
28
29
  "devDependencies": {
29
- "@ember-data/unpublished-test-infra": "3.28.9",
30
+ "@ember-data/unpublished-test-infra": "3.28.12",
30
31
  "@ember/optional-features": "^2.0.0",
31
32
  "@ember/test-helpers": "^2.2.5",
32
33
  "@types/ember": "^3.16.5",