@ember-data/store 4.8.0-alpha.1 → 4.8.0-alpha.4

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.
Files changed (41) hide show
  1. package/addon/-debug/index.js +1 -1
  2. package/addon/-private/{identifier-cache.ts → caches/identifier-cache.ts} +114 -47
  3. package/addon/-private/caches/instance-cache.ts +702 -0
  4. package/addon/-private/{record-data-for.ts → caches/record-data-for.ts} +2 -2
  5. package/addon/-private/index.ts +38 -21
  6. package/addon/-private/{model → legacy-model-support}/record-reference.ts +15 -13
  7. package/addon/-private/{schema-definition-service.ts → legacy-model-support/schema-definition-service.ts} +13 -9
  8. package/addon/-private/{model → legacy-model-support}/shim-model-class.ts +8 -4
  9. package/addon/-private/{record-array-manager.ts → managers/record-array-manager.ts} +21 -44
  10. package/addon/-private/managers/record-data-manager.ts +830 -0
  11. package/addon/-private/managers/record-data-store-wrapper.ts +413 -0
  12. package/addon/-private/managers/record-notification-manager.ts +90 -0
  13. package/addon/-private/network/fetch-manager.ts +552 -0
  14. package/addon/-private/{finders.js → network/finders.js} +4 -12
  15. package/addon/-private/{request-cache.ts → network/request-cache.ts} +1 -1
  16. package/addon/-private/{snapshot-record-array.ts → network/snapshot-record-array.ts} +3 -3
  17. package/addon/-private/{snapshot.ts → network/snapshot.ts} +40 -49
  18. package/addon/-private/{promise-proxies.ts → proxies/promise-proxies.ts} +4 -4
  19. package/addon/-private/{promise-proxy-base.js → proxies/promise-proxy-base.js} +0 -0
  20. package/addon/-private/record-arrays/adapter-populated-record-array.ts +9 -11
  21. package/addon/-private/record-arrays/record-array.ts +25 -15
  22. package/addon/-private/{core-store.ts → store-service.ts} +412 -148
  23. package/addon/-private/{coerce-id.ts → utils/coerce-id.ts} +1 -1
  24. package/addon/-private/{common.js → utils/common.js} +1 -2
  25. package/addon/-private/utils/construct-resource.ts +2 -2
  26. package/addon/-private/{identifer-debug-consts.ts → utils/identifer-debug-consts.ts} +0 -0
  27. package/addon/-private/{normalize-model-name.ts → utils/normalize-model-name.ts} +1 -3
  28. package/addon/-private/utils/promise-record.ts +3 -3
  29. package/addon/-private/{serializer-response.ts → utils/serializer-response.ts} +2 -2
  30. package/addon/-private/utils/uuid-polyfill.ts +71 -0
  31. package/addon/-private/{weak-cache.ts → utils/weak-cache.ts} +0 -0
  32. package/package.json +11 -7
  33. package/addon/-private/errors-utils.js +0 -146
  34. package/addon/-private/fetch-manager.ts +0 -597
  35. package/addon/-private/identity-map.ts +0 -54
  36. package/addon/-private/instance-cache.ts +0 -387
  37. package/addon/-private/internal-model-factory.ts +0 -359
  38. package/addon/-private/internal-model-map.ts +0 -121
  39. package/addon/-private/model/internal-model.ts +0 -602
  40. package/addon/-private/record-data-store-wrapper.ts +0 -243
  41. package/addon/-private/record-notification-manager.ts +0 -73
@@ -0,0 +1,702 @@
1
+ import { assert, deprecate, warn } from '@ember/debug';
2
+ import { DEBUG } from '@glimmer/env';
3
+
4
+ import { importSync } from '@embroider/macros';
5
+ import { resolve } from 'rsvp';
6
+
7
+ import { V2CACHE_SINGLETON_MANAGER } from '@ember-data/canary-features';
8
+ import { HAS_RECORD_DATA_PACKAGE } from '@ember-data/private-build-infra';
9
+ import { LOG_INSTANCE_CACHE } from '@ember-data/private-build-infra/debugging';
10
+ import { DEPRECATE_V1CACHE_STORE_APIS } from '@ember-data/private-build-infra/deprecations';
11
+ import type { Graph, peekGraph } from '@ember-data/record-data/-private/graph/index';
12
+ import type {
13
+ ExistingResourceIdentifierObject,
14
+ ExistingResourceObject,
15
+ NewResourceIdentifierObject,
16
+ } from '@ember-data/types/q/ember-data-json-api';
17
+ import type {
18
+ RecordIdentifier,
19
+ StableExistingRecordIdentifier,
20
+ StableRecordIdentifier,
21
+ } from '@ember-data/types/q/identifier';
22
+ import type { RecordData } from '@ember-data/types/q/record-data';
23
+ import { JsonApiRelationship, JsonApiResource } from '@ember-data/types/q/record-data-json-api';
24
+ import { RelationshipSchema } from '@ember-data/types/q/record-data-schemas';
25
+ import type { RecordDataStoreWrapper as StoreWrapper } from '@ember-data/types/q/record-data-store-wrapper';
26
+ import type { RecordInstance } from '@ember-data/types/q/record-instance';
27
+ import type { FindOptions } from '@ember-data/types/q/store';
28
+ import { Dict } from '@ember-data/types/q/utils';
29
+
30
+ import RecordReference from '../legacy-model-support/record-reference';
31
+ import { NonSingletonRecordDataManager, SingletonRecordDataManager } from '../managers/record-data-manager';
32
+ import { RecordDataStoreWrapper } from '../managers/record-data-store-wrapper';
33
+ import Snapshot from '../network/snapshot';
34
+ import type { CreateRecordProperties } from '../store-service';
35
+ import type Store from '../store-service';
36
+ import { assertIdentifierHasId } from '../store-service';
37
+ import coerceId, { ensureStringId } from '../utils/coerce-id';
38
+ import constructResource from '../utils/construct-resource';
39
+ import normalizeModelName from '../utils/normalize-model-name';
40
+ import WeakCache, { DebugWeakCache } from '../utils/weak-cache';
41
+ import { removeRecordDataFor, setRecordDataFor } from './record-data-for';
42
+
43
+ let _peekGraph: peekGraph;
44
+ if (HAS_RECORD_DATA_PACKAGE) {
45
+ let __peekGraph: peekGraph;
46
+ _peekGraph = (wrapper: Store | StoreWrapper): Graph | undefined => {
47
+ let a = (importSync('@ember-data/record-data/-private') as { peekGraph: peekGraph }).peekGraph;
48
+ __peekGraph = __peekGraph || a;
49
+ return __peekGraph(wrapper);
50
+ };
51
+ }
52
+
53
+ /**
54
+ @module @ember-data/store
55
+ */
56
+
57
+ const RecordCache = new WeakCache<RecordInstance, StableRecordIdentifier>(DEBUG ? 'identifier' : '');
58
+ if (DEBUG) {
59
+ RecordCache._expectMsg = (key: RecordInstance) => `${String(key)} is not a record instantiated by @ember-data/store`;
60
+ }
61
+
62
+ export function peekRecordIdentifier(record: RecordInstance): StableRecordIdentifier | undefined {
63
+ return RecordCache.get(record);
64
+ }
65
+
66
+ /**
67
+ Retrieves the unique referentially-stable [RecordIdentifier](/ember-data/release/classes/StableRecordIdentifier)
68
+ assigned to the given record instance.
69
+ ```js
70
+ import { recordIdentifierFor } from "@ember-data/store";
71
+ // ... gain access to a record, for instance with peekRecord or findRecord
72
+ const record = store.peekRecord("user", "1");
73
+ // get the identifier for the record (see docs for StableRecordIdentifier)
74
+ const identifier = recordIdentifierFor(record);
75
+ // access the identifier's properties.
76
+ const { id, type, lid } = identifier;
77
+ ```
78
+ @method recordIdentifierFor
79
+ @public
80
+ @static
81
+ @for @ember-data/store
82
+ @param {Object} record a record instance previously obstained from the store.
83
+ @returns {StableRecordIdentifier}
84
+ */
85
+ export function recordIdentifierFor(record: RecordInstance): StableRecordIdentifier {
86
+ return RecordCache.getWithError(record);
87
+ }
88
+
89
+ export function setRecordIdentifier(record: RecordInstance, identifier: StableRecordIdentifier): void {
90
+ if (DEBUG && RecordCache.has(record) && RecordCache.get(record) !== identifier) {
91
+ throw new Error(`${String(record)} was already assigned an identifier`);
92
+ }
93
+
94
+ /*
95
+ It would be nice to do a reverse check here that an identifier has not
96
+ previously been assigned a record; however, unload + rematerialization
97
+ prevents us from having a great way of doing so when CustomRecordClasses
98
+ don't necessarily give us access to a `isDestroyed` for dematerialized
99
+ instance.
100
+ */
101
+
102
+ RecordCache.set(record, identifier);
103
+ }
104
+
105
+ export const StoreMap = new WeakCache<RecordInstance, Store>(DEBUG ? 'store' : '');
106
+
107
+ export function storeFor(record: RecordInstance): Store | undefined {
108
+ const store = StoreMap.get(record);
109
+
110
+ assert(
111
+ `A record in a disconnected state cannot utilize the store. This typically means the record has been destroyed, most commonly by unloading it.`,
112
+ store
113
+ );
114
+ return store;
115
+ }
116
+
117
+ type Caches = {
118
+ record: Map<StableRecordIdentifier, RecordInstance>;
119
+ recordData: Map<StableRecordIdentifier, RecordData>;
120
+ reference: DebugWeakCache<StableRecordIdentifier, RecordReference>;
121
+ };
122
+
123
+ export class InstanceCache {
124
+ declare store: Store;
125
+ declare _storeWrapper: RecordDataStoreWrapper;
126
+ declare peekList: Dict<Set<StableRecordIdentifier>>;
127
+ declare __recordDataFor: (resource: RecordIdentifier) => RecordData;
128
+
129
+ declare __cacheManager: NonSingletonRecordDataManager;
130
+ __instances: Caches = {
131
+ record: new Map<StableRecordIdentifier, RecordInstance>(),
132
+ recordData: new Map<StableRecordIdentifier, RecordData>(),
133
+ reference: new WeakCache<StableRecordIdentifier, RecordReference>(DEBUG ? 'reference' : ''),
134
+ };
135
+
136
+ recordIsLoaded(identifier: StableRecordIdentifier, filterDeleted: boolean = false) {
137
+ const recordData = this.peek({ identifier, bucket: 'recordData' });
138
+ if (!recordData) {
139
+ return false;
140
+ }
141
+ const isNew = recordData.isNew(identifier);
142
+ const isEmpty = recordData.isEmpty?.(identifier) || false;
143
+
144
+ // if we are new we must consider ourselves loaded
145
+ if (isNew) {
146
+ return true;
147
+ }
148
+ // even if we have a past request, if we are now empty we are not loaded
149
+ // typically this is true after an unloadRecord call
150
+
151
+ // if we are not empty, not new && we have a fulfilled request then we are loaded
152
+ // we should consider allowing for something to be loaded that is simply "not empty".
153
+ // which is how RecordState currently handles this case; however, RecordState is buggy
154
+ // in that it does not account for unloading.
155
+ // return !isEmpty;
156
+
157
+ const req = this.store.getRequestStateService();
158
+ const fulfilled = req.getLastRequestForRecord(identifier);
159
+ const isLoading =
160
+ fulfilled !== null && req.getPendingRequestsForRecord(identifier).some((req) => req.type === 'query');
161
+
162
+ if (isEmpty || (filterDeleted && recordData.isDeletionCommitted(identifier)) || isLoading) {
163
+ return false;
164
+ }
165
+
166
+ return true;
167
+ }
168
+
169
+ constructor(store: Store) {
170
+ this.store = store;
171
+ this.peekList = Object.create(null) as Dict<Set<StableRecordIdentifier>>;
172
+
173
+ this._storeWrapper = new RecordDataStoreWrapper(this.store);
174
+ this.__recordDataFor = (resource: RecordIdentifier) => {
175
+ // TODO enforce strict
176
+ const identifier = this.store.identifierCache.getOrCreateRecordIdentifier(resource);
177
+ return this.getRecordData(identifier);
178
+ };
179
+
180
+ this.__instances.reference._generator = (identifier) => {
181
+ return new RecordReference(this.store, identifier);
182
+ };
183
+
184
+ store.identifierCache.__configureMerge(
185
+ (identifier: StableRecordIdentifier, matchedIdentifier: StableRecordIdentifier, resourceData) => {
186
+ let intendedIdentifier = identifier;
187
+ if (identifier.id !== matchedIdentifier.id) {
188
+ intendedIdentifier =
189
+ 'id' in resourceData && identifier.id === resourceData.id ? identifier : matchedIdentifier;
190
+ } else if (identifier.type !== matchedIdentifier.type) {
191
+ intendedIdentifier =
192
+ 'type' in resourceData && identifier.type === resourceData.type ? identifier : matchedIdentifier;
193
+ }
194
+ let altIdentifier = identifier === intendedIdentifier ? matchedIdentifier : identifier;
195
+
196
+ // check for duplicate entities
197
+ let imHasRecord = this.__instances.record.has(intendedIdentifier);
198
+ let otherHasRecord = this.__instances.record.has(altIdentifier);
199
+ let imRecordData = this.__instances.recordData.get(intendedIdentifier) || null;
200
+ let otherRecordData = this.__instances.recordData.get(altIdentifier) || null;
201
+
202
+ // we cannot merge entities when both have records
203
+ // (this may not be strictly true, we could probably swap the recordData the record points at)
204
+ if (imHasRecord && otherHasRecord) {
205
+ // TODO we probably don't need to throw these errors anymore
206
+ // we can probably just "swap" what data source the abandoned
207
+ // record points at so long as
208
+ // it itself is not retained by the store in any way.
209
+ if ('id' in resourceData) {
210
+ throw new Error(
211
+ `Failed to update the 'id' for the RecordIdentifier '${identifier.type}:${String(identifier.id)} (${
212
+ identifier.lid
213
+ })' to '${String(resourceData.id)}', because that id is already in use by '${
214
+ matchedIdentifier.type
215
+ }:${String(matchedIdentifier.id)} (${matchedIdentifier.lid})'`
216
+ );
217
+ }
218
+ // TODO @runspired determine when this is even possible
219
+ assert(
220
+ `Failed to update the RecordIdentifier '${identifier.type}:${String(identifier.id)} (${
221
+ identifier.lid
222
+ })' to merge with the detected duplicate identifier '${matchedIdentifier.type}:${String(
223
+ matchedIdentifier.id
224
+ )} (${String(matchedIdentifier.lid)})'`
225
+ );
226
+ }
227
+
228
+ // remove "other" from cache
229
+ if (otherHasRecord) {
230
+ // TODO probably need to release other things
231
+ this.peekList[altIdentifier.type]?.delete(altIdentifier);
232
+ }
233
+
234
+ if (imRecordData === null && otherRecordData === null) {
235
+ // nothing more to do
236
+ return intendedIdentifier;
237
+
238
+ // only the other has a RecordData
239
+ // OR only the other has a Record
240
+ } else if (
241
+ (imRecordData === null && otherRecordData !== null) ||
242
+ (imRecordData && !imHasRecord && otherRecordData && otherHasRecord)
243
+ ) {
244
+ if (imRecordData) {
245
+ // TODO check if we are retained in any async relationships
246
+ // TODO probably need to release other things
247
+ this.peekList[intendedIdentifier.type]?.delete(intendedIdentifier);
248
+ // im.destroy();
249
+ }
250
+ imRecordData = otherRecordData!;
251
+ // TODO do we need to notify the id change?
252
+ // TODO swap recordIdentifierFor result?
253
+ this.peekList[intendedIdentifier.type] = this.peekList[intendedIdentifier.type] || new Set();
254
+ this.peekList[intendedIdentifier.type]!.add(intendedIdentifier);
255
+
256
+ // just use im
257
+ } else {
258
+ // otherIm.destroy();
259
+ }
260
+
261
+ /*
262
+ TODO @runspired consider adding this to make polymorphism even nicer
263
+ if (HAS_RECORD_DATA_PACKAGE) {
264
+ if (identifier.type !== matchedIdentifier.type) {
265
+ const graphFor = importSync('@ember-data/record-data/-private').graphFor;
266
+ graphFor(this).registerPolymorphicType(identifier.type, matchedIdentifier.type);
267
+ }
268
+ }
269
+ */
270
+
271
+ return intendedIdentifier;
272
+ }
273
+ );
274
+ }
275
+ peek({ identifier, bucket }: { identifier: StableRecordIdentifier; bucket: 'record' }): RecordInstance | undefined;
276
+ peek({ identifier, bucket }: { identifier: StableRecordIdentifier; bucket: 'recordData' }): RecordData | undefined;
277
+ peek({
278
+ identifier,
279
+ bucket,
280
+ }: {
281
+ identifier: StableRecordIdentifier;
282
+ bucket: 'record' | 'recordData';
283
+ }): RecordData | RecordInstance | undefined {
284
+ return this.__instances[bucket]?.get(identifier);
285
+ }
286
+
287
+ getRecord(identifier: StableRecordIdentifier, properties?: CreateRecordProperties): RecordInstance {
288
+ let record = this.peek({ identifier, bucket: 'record' });
289
+
290
+ if (!record) {
291
+ const recordData = this.getRecordData(identifier);
292
+
293
+ record = this.store.instantiateRecord(
294
+ identifier,
295
+ properties || {},
296
+ this.__recordDataFor,
297
+ this.store._notificationManager
298
+ );
299
+ setRecordIdentifier(record, identifier);
300
+ setRecordDataFor(record, recordData);
301
+ StoreMap.set(record, this.store);
302
+ this.__instances.record.set(identifier, record);
303
+
304
+ if (LOG_INSTANCE_CACHE) {
305
+ // eslint-disable-next-line no-console
306
+ console.log(`InstanceCache: created Record for ${String(identifier)}`, properties);
307
+ }
308
+ }
309
+
310
+ return record;
311
+ }
312
+
313
+ getRecordData(identifier: StableRecordIdentifier): RecordData {
314
+ let recordData = this.peek({ identifier, bucket: 'recordData' });
315
+
316
+ if (!recordData) {
317
+ if (DEPRECATE_V1CACHE_STORE_APIS && this.store.createRecordDataFor.length > 2) {
318
+ deprecate(
319
+ `Store.createRecordDataFor(<type>, <id>, <lid>, <storeWrapper>) has been deprecated in favor of Store.createRecordDataFor(<identifier>, <storeWrapper>)`,
320
+ false,
321
+ {
322
+ id: 'ember-data:deprecate-v1cache-store-apis',
323
+ for: 'ember-data',
324
+ until: '5.0',
325
+ since: { enabled: '4.8', available: '4.8' },
326
+ }
327
+ );
328
+ let recordDataInstance = this.store.createRecordDataFor(
329
+ identifier.type,
330
+ identifier.id,
331
+ // @ts-expect-error
332
+ identifier.lid,
333
+ this._storeWrapper
334
+ );
335
+ if (V2CACHE_SINGLETON_MANAGER) {
336
+ recordData = this.__cacheManager =
337
+ this.__cacheManager || new NonSingletonRecordDataManager(this.store, recordDataInstance, identifier);
338
+ } else {
339
+ recordData = new NonSingletonRecordDataManager(this.store, recordDataInstance, identifier);
340
+ }
341
+ } else {
342
+ let recordDataInstance = this.store.createRecordDataFor(identifier, this._storeWrapper);
343
+ if (V2CACHE_SINGLETON_MANAGER) {
344
+ if (DEBUG) {
345
+ recordData = this.__cacheManager = this.__cacheManager || new SingletonRecordDataManager(this.store);
346
+ (recordData as SingletonRecordDataManager)._addRecordData(identifier, recordDataInstance as RecordData);
347
+ } else {
348
+ recordData = recordDataInstance as RecordData;
349
+ }
350
+ } else {
351
+ recordData = new NonSingletonRecordDataManager(this.store, recordDataInstance, identifier);
352
+ }
353
+ }
354
+ setRecordDataFor(identifier, recordData);
355
+
356
+ this.__instances.recordData.set(identifier, recordData);
357
+ this.peekList[identifier.type] = this.peekList[identifier.type] || new Set();
358
+ this.peekList[identifier.type]!.add(identifier);
359
+ if (LOG_INSTANCE_CACHE) {
360
+ // eslint-disable-next-line no-console
361
+ console.log(`InstanceCache: created RecordData for ${String(identifier)}`);
362
+ }
363
+ }
364
+
365
+ return recordData;
366
+ }
367
+
368
+ getReference(identifier: StableRecordIdentifier) {
369
+ return this.__instances.reference.lookup(identifier);
370
+ }
371
+
372
+ createSnapshot(identifier: StableRecordIdentifier, options: FindOptions = {}): Snapshot {
373
+ return new Snapshot(options, identifier, this.store);
374
+ }
375
+
376
+ disconnect(identifier: StableRecordIdentifier) {
377
+ const record = this.__instances.record.get(identifier);
378
+ assert(
379
+ 'Cannot destroy record while it is still materialized',
380
+ !record || record.isDestroyed || record.isDestroying
381
+ );
382
+
383
+ if (HAS_RECORD_DATA_PACKAGE) {
384
+ let graph = _peekGraph(this.store);
385
+ if (graph) {
386
+ graph.remove(identifier);
387
+ }
388
+ }
389
+
390
+ this.store.identifierCache.forgetRecordIdentifier(identifier);
391
+ if (LOG_INSTANCE_CACHE) {
392
+ // eslint-disable-next-line no-console
393
+ console.log(`InstanceCache: disconnected ${String(identifier)}`);
394
+ }
395
+ }
396
+
397
+ unloadRecord(identifier: StableRecordIdentifier) {
398
+ if (DEBUG) {
399
+ const requests = this.store.getRequestStateService().getPendingRequestsForRecord(identifier);
400
+ if (
401
+ requests.some((req) => {
402
+ return req.type === 'mutation';
403
+ })
404
+ ) {
405
+ assert(`You can only unload a record which is not inFlight. '${String(identifier)}'`);
406
+ }
407
+ }
408
+ if (LOG_INSTANCE_CACHE) {
409
+ // eslint-disable-next-line no-console
410
+ console.groupCollapsed(`InstanceCache: unloading record for ${String(identifier)}`);
411
+ }
412
+
413
+ // TODO is this join still necessary?
414
+ this.store._backburner.join(() => {
415
+ const record = this.peek({ identifier, bucket: 'record' });
416
+ const recordData = this.peek({ identifier, bucket: 'recordData' });
417
+ this.peekList[identifier.type]?.delete(identifier);
418
+
419
+ if (record) {
420
+ this.store.teardownRecord(record);
421
+ this.__instances.record.delete(identifier);
422
+ StoreMap.delete(record);
423
+ RecordCache.delete(record);
424
+ removeRecordDataFor(record);
425
+
426
+ if (LOG_INSTANCE_CACHE) {
427
+ // eslint-disable-next-line no-console
428
+ console.log(`InstanceCache: destroyed record for ${String(identifier)}`);
429
+ }
430
+ }
431
+
432
+ if (recordData) {
433
+ recordData.unloadRecord(identifier);
434
+ this.__instances.recordData.delete(identifier);
435
+ removeRecordDataFor(identifier);
436
+ } else {
437
+ this.disconnect(identifier);
438
+ }
439
+
440
+ this.store._fetchManager.clearEntries(identifier);
441
+ this.store.recordArrayManager.recordDidChange(identifier);
442
+ if (LOG_INSTANCE_CACHE) {
443
+ // eslint-disable-next-line no-console
444
+ console.log(`InstanceCache: unloaded RecordData for ${String(identifier)}`);
445
+ // eslint-disable-next-line no-console
446
+ console.groupEnd();
447
+ }
448
+ });
449
+ }
450
+
451
+ clear(type?: string) {
452
+ if (type === undefined) {
453
+ let keys = Object.keys(this.peekList);
454
+ keys.forEach((key) => this.clear(key));
455
+ } else {
456
+ let identifiers = this.peekList[type];
457
+ if (identifiers) {
458
+ identifiers.forEach((identifier) => {
459
+ // TODO we rely on not removing the main cache
460
+ // and only removing the peekList cache apparently.
461
+ // we should figure out this duality and codify whatever
462
+ // signal it is actually trying to give us.
463
+ // this.cache.delete(identifier);
464
+ this.peekList[identifier.type]!.delete(identifier);
465
+ this.unloadRecord(identifier);
466
+ // TODO we don't remove the identifier, should we?
467
+ });
468
+ }
469
+ }
470
+ }
471
+
472
+ // TODO this should move into the network layer
473
+ _fetchDataIfNeededForIdentifier(
474
+ identifier: StableRecordIdentifier,
475
+ options: FindOptions = {}
476
+ ): Promise<StableRecordIdentifier> {
477
+ // pre-loading will change the isEmpty value
478
+ const isEmpty = _isEmpty(this, identifier);
479
+ const isLoading = _isLoading(this, identifier);
480
+
481
+ if (options.preload) {
482
+ this.store._backburner.join(() => {
483
+ preloadData(this.store, identifier, options.preload!);
484
+ });
485
+ }
486
+
487
+ let promise: Promise<StableRecordIdentifier>;
488
+ if (isEmpty) {
489
+ assertIdentifierHasId(identifier);
490
+
491
+ promise = this.store._fetchManager.scheduleFetch(identifier, options);
492
+ } else if (isLoading) {
493
+ promise = this.store._fetchManager.getPendingFetch(identifier, options)!;
494
+ assert(`Expected to find a pending request for a record in the loading state, but found none`, promise);
495
+ } else {
496
+ promise = resolve(identifier);
497
+ }
498
+
499
+ return promise;
500
+ }
501
+
502
+ // TODO this should move into something coordinating operations
503
+ setRecordId(identifier: StableRecordIdentifier, id: string) {
504
+ const { type, lid } = identifier;
505
+ let oldId = identifier.id;
506
+
507
+ // ID absolutely can't be missing if the oldID is empty (missing Id in response for a new record)
508
+ assert(
509
+ `'${type}' was saved to the server, but the response does not have an id and your record does not either.`,
510
+ !(id === null && oldId === null)
511
+ );
512
+
513
+ // ID absolutely can't be different than oldID if oldID is not null
514
+ // TODO this assertion and restriction may not strictly be needed in the identifiers world
515
+ assert(
516
+ `Cannot update the id for '${type}:${lid}' from '${String(oldId)}' to '${id}'.`,
517
+ !(oldId !== null && id !== oldId)
518
+ );
519
+
520
+ // ID can be null if oldID is not null (altered ID in response for a record)
521
+ // however, this is more than likely a developer error.
522
+ if (oldId !== null && id === null) {
523
+ warn(
524
+ `Your ${type} record was saved to the server, but the response does not have an id.`,
525
+ !(oldId !== null && id === null)
526
+ );
527
+ return;
528
+ }
529
+
530
+ if (LOG_INSTANCE_CACHE) {
531
+ // eslint-disable-next-line no-console
532
+ console.log(`InstanceCache: updating id to '${id}' for record ${String(identifier)}`);
533
+ }
534
+
535
+ let existingIdentifier = this.store.identifierCache.peekRecordIdentifier({ type, id });
536
+ assert(
537
+ `'${type}' was saved to the server, but the response returned the new id '${id}', which has already been used with another record.'`,
538
+ !existingIdentifier || existingIdentifier === identifier
539
+ );
540
+
541
+ if (identifier.id === null) {
542
+ // TODO potentially this needs to handle merged result
543
+ this.store.identifierCache.updateRecordIdentifier(identifier, { type, id });
544
+ }
545
+
546
+ // TODO update recordData if needed ?
547
+ // TODO handle consequences of identifier merge for notifications
548
+ this.store._notificationManager.notify(identifier, 'identity');
549
+ }
550
+
551
+ // TODO this should move into something coordinating operations
552
+ loadData(data: ExistingResourceObject): StableExistingRecordIdentifier {
553
+ let modelName = data.type;
554
+ assert(
555
+ `You must include an 'id' for ${modelName} in an object passed to 'push'`,
556
+ data.id !== null && data.id !== undefined && data.id !== ''
557
+ );
558
+ assert(
559
+ `You tried to push data with a type '${modelName}' but no model could be found with that name.`,
560
+ this.store.getSchemaDefinitionService().doesTypeExist(modelName)
561
+ );
562
+
563
+ const resource = constructResource(normalizeModelName(data.type), ensureStringId(data.id), coerceId(data.lid));
564
+ let identifier = this.store.identifierCache.peekRecordIdentifier(resource);
565
+ let isUpdate = false;
566
+
567
+ // store.push will be from empty
568
+ // findRecord will be from root.loading
569
+ // this cannot be loading state if we do not already have an identifier
570
+ // all else will be updates
571
+ if (identifier) {
572
+ const isLoading = _isLoading(this, identifier) || !this.recordIsLoaded(identifier);
573
+ isUpdate = !_isEmpty(this, identifier) && !isLoading;
574
+
575
+ // exclude store.push (root.empty) case
576
+ if (isUpdate || isLoading) {
577
+ identifier = this.store.identifierCache.updateRecordIdentifier(identifier, data);
578
+ }
579
+ } else {
580
+ identifier = this.store.identifierCache.getOrCreateRecordIdentifier(data);
581
+ }
582
+
583
+ const recordData = this.getRecordData(identifier);
584
+ if (recordData.isNew(identifier)) {
585
+ this.store._notificationManager.notify(identifier, 'identity');
586
+ }
587
+
588
+ const hasRecord = this.__instances.record.has(identifier);
589
+ recordData.pushData(identifier, data, hasRecord);
590
+
591
+ if (!isUpdate) {
592
+ this.store.recordArrayManager.recordDidChange(identifier);
593
+ }
594
+
595
+ return identifier as StableExistingRecordIdentifier;
596
+ }
597
+ }
598
+
599
+ function _recordDataIsFullDeleted(identifier: StableRecordIdentifier, recordData: RecordData): boolean {
600
+ return (
601
+ recordData.isDeletionCommitted(identifier) || (recordData.isNew(identifier) && recordData.isDeleted(identifier))
602
+ );
603
+ }
604
+
605
+ export function recordDataIsFullyDeleted(cache: InstanceCache, identifier: StableRecordIdentifier): boolean {
606
+ let recordData = cache.peek({ identifier, bucket: 'recordData' });
607
+ return !recordData || _recordDataIsFullDeleted(identifier, recordData);
608
+ }
609
+
610
+ /*
611
+ When a find request is triggered on the store, the user can optionally pass in
612
+ attributes and relationships to be preloaded. These are meant to behave as if they
613
+ came back from the server, except the user obtained them out of band and is informing
614
+ the store of their existence. The most common use case is for supporting client side
615
+ nested URLs, such as `/posts/1/comments/2` so the user can do
616
+ `store.findRecord('comment', 2, { preload: { post: 1 } })` without having to fetch the post.
617
+
618
+ Preloaded data can be attributes and relationships passed in either as IDs or as actual
619
+ models.
620
+ */
621
+ type PreloadRelationshipValue = RecordInstance | string;
622
+ function preloadData(store: Store, identifier: StableRecordIdentifier, preload: Dict<unknown>) {
623
+ let jsonPayload: JsonApiResource = {};
624
+ //TODO(Igor) consider the polymorphic case
625
+ const schemas = store.getSchemaDefinitionService();
626
+ const relationships = schemas.relationshipsDefinitionFor(identifier);
627
+ Object.keys(preload).forEach((key) => {
628
+ let preloadValue = preload[key];
629
+
630
+ let relationshipMeta = relationships[key];
631
+ if (relationshipMeta) {
632
+ if (!jsonPayload.relationships) {
633
+ jsonPayload.relationships = {};
634
+ }
635
+ jsonPayload.relationships[key] = preloadRelationship(
636
+ relationshipMeta,
637
+ preloadValue as PreloadRelationshipValue | null | Array<PreloadRelationshipValue>
638
+ );
639
+ } else {
640
+ if (!jsonPayload.attributes) {
641
+ jsonPayload.attributes = {};
642
+ }
643
+ jsonPayload.attributes[key] = preloadValue;
644
+ }
645
+ });
646
+ store._instanceCache.getRecordData(identifier).pushData(identifier, jsonPayload);
647
+ }
648
+
649
+ function preloadRelationship(
650
+ schema: RelationshipSchema,
651
+ preloadValue: PreloadRelationshipValue | null | Array<PreloadRelationshipValue>
652
+ ): JsonApiRelationship {
653
+ const relatedType = schema.type;
654
+
655
+ if (schema.kind === 'hasMany') {
656
+ assert('You need to pass in an array to set a hasMany property on a record', Array.isArray(preloadValue));
657
+ return { data: preloadValue.map((value) => _convertPreloadRelationshipToJSON(value, relatedType)) };
658
+ }
659
+
660
+ assert('You should not pass in an array to set a belongsTo property on a record', !Array.isArray(preloadValue));
661
+ return { data: preloadValue ? _convertPreloadRelationshipToJSON(preloadValue, relatedType) : null };
662
+ }
663
+
664
+ /*
665
+ findRecord('user', '1', { preload: { friends: ['1'] }});
666
+ findRecord('user', '1', { preload: { friends: [record] }});
667
+ */
668
+ function _convertPreloadRelationshipToJSON(
669
+ value: RecordInstance | string,
670
+ type: string
671
+ ): ExistingResourceIdentifierObject | NewResourceIdentifierObject {
672
+ if (typeof value === 'string' || typeof value === 'number') {
673
+ return { type, id: value };
674
+ }
675
+ // TODO if not a record instance assert it's an identifier
676
+ // and allow identifiers to be used
677
+ return recordIdentifierFor(value);
678
+ }
679
+
680
+ function _isEmpty(cache: InstanceCache, identifier: StableRecordIdentifier): boolean {
681
+ const recordData = cache.peek({ identifier: identifier, bucket: 'recordData' });
682
+ if (!recordData) {
683
+ return true;
684
+ }
685
+ const isNew = recordData.isNew(identifier);
686
+ const isDeleted = recordData.isDeleted(identifier);
687
+ const isEmpty = recordData.isEmpty?.(identifier) || false;
688
+
689
+ return (!isNew || isDeleted) && isEmpty;
690
+ }
691
+
692
+ function _isLoading(cache: InstanceCache, identifier: StableRecordIdentifier): boolean {
693
+ const req = cache.store.getRequestStateService();
694
+ // const fulfilled = req.getLastRequestForRecord(identifier);
695
+ const isLoaded = cache.recordIsLoaded(identifier);
696
+
697
+ return (
698
+ !isLoaded &&
699
+ // fulfilled === null &&
700
+ req.getPendingRequestsForRecord(identifier).some((req) => req.type === 'query')
701
+ );
702
+ }