@ember-data/store 5.3.0-alpha.9 → 5.4.0-alpha.0

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.
@@ -1,6 +1,6 @@
1
1
  import { macroCondition, getOwnConfig } from '@embroider/macros';
2
2
  import { getOwner } from '@ember/application';
3
- import { assert, warn } from '@ember/debug';
3
+ import { assert, deprecate, warn } from '@ember/debug';
4
4
  import EmberObject from '@ember/object';
5
5
  import { _backburner } from '@ember/runloop';
6
6
  import { tracked } from '@glimmer/tracking';
@@ -354,50 +354,63 @@ function copyDocumentProperties(target, source) {
354
354
  }
355
355
  }
356
356
 
357
- /**
358
- @module @ember-data/store
359
- */
360
-
361
357
  // Used by the store to normalize IDs entering the store. Despite the fact
362
358
  // that developers may provide IDs as numbers (e.g., `store.findRecord('person', 1)`),
363
359
  // it is important that internally we use strings, since IDs may be serialized
364
360
  // and lose type information. For example, Ember's router may put a record's
365
361
  // ID into the URL, and if we later try to deserialize that URL and find the
366
362
  // corresponding record, we will not know if it is a string or a number.
367
-
368
363
  function coerceId(id) {
369
- if (id === null || id === undefined || id === '') {
370
- return null;
371
- }
372
- if (typeof id === 'string') {
373
- return id;
374
- }
375
- if (typeof id === 'symbol') {
376
- return id.toString();
364
+ if (macroCondition(getOwnConfig().deprecations.DEPRECATE_NON_STRICT_ID)) {
365
+ let normalized;
366
+ if (id === null || id === undefined || id === '') {
367
+ normalized = null;
368
+ } else {
369
+ normalized = String(id);
370
+ }
371
+ deprecate(`The resource id '<${typeof id}> ${String(id)} ' is not normalized. Update your application code to use '${JSON.stringify(normalized)}' instead.`, normalized === id, {
372
+ id: 'ember-data:deprecate-non-strict-id',
373
+ until: '6.0',
374
+ for: 'ember-data',
375
+ since: {
376
+ available: '5.3',
377
+ enabled: '5.3'
378
+ }
379
+ });
380
+ return normalized;
377
381
  }
378
- return '' + id;
382
+ assert(`Resource IDs must be a non-empty string or null. Received '${String(id)}'.`, id === null || typeof id === 'string' && id.length > 0);
383
+ return id;
379
384
  }
380
385
  function ensureStringId(id) {
381
386
  let normalized = null;
382
387
  if (typeof id === 'string') {
383
388
  normalized = id.length > 0 ? id : null;
384
389
  } else if (typeof id === 'number' && !isNaN(id)) {
385
- normalized = '' + id;
386
- }
387
- if (normalized === null) {
388
- throw new Error(`Expected id to be a string or number, received ${String(id)}`);
390
+ normalized = String(id);
389
391
  }
392
+ assert(`Expected id to be a string or number, received ${String(id)}`, normalized !== null);
390
393
  return normalized;
391
394
  }
392
395
 
393
396
  // provided for additional debuggability
394
397
  const DEBUG_CLIENT_ORIGINATED = Symbol('record-originated-on-client');
395
398
  const DEBUG_IDENTIFIER_BUCKET = Symbol('identifier-bucket');
396
- function isNonEmptyString(str) {
397
- return str && typeof str === 'string';
398
- }
399
- function normalizeModelName(modelName) {
400
- return dasherize(modelName);
399
+ function normalizeModelName(type) {
400
+ if (macroCondition(getOwnConfig().deprecations.DEPRECATE_NON_STRICT_TYPES)) {
401
+ const result = dasherize(type);
402
+ deprecate(`The resource type '${type}' is not normalized. Update your application code to use '${result}' instead of '${type}'.`, result === type, {
403
+ id: 'ember-data:deprecate-non-strict-types',
404
+ until: '6.0',
405
+ for: 'ember-data',
406
+ since: {
407
+ available: '5.3',
408
+ enabled: '5.3'
409
+ }
410
+ });
411
+ return result;
412
+ }
413
+ return type;
401
414
  }
402
415
 
403
416
  /**
@@ -441,6 +454,21 @@ function installPolyfill() {
441
454
  };
442
455
  }
443
456
  }
457
+ function isResource(resource) {
458
+ return Boolean(resource && typeof resource === 'object');
459
+ }
460
+ function hasProp(resource, prop) {
461
+ return Boolean(isResource(resource) && prop in resource && typeof resource[prop] === 'string' && resource[prop].length);
462
+ }
463
+ function hasLid(resource) {
464
+ return hasProp(resource, 'lid');
465
+ }
466
+ function hasId(resource) {
467
+ return hasProp(resource, 'id') || Boolean(isResource(resource) && 'id' in resource && typeof resource.id === 'number');
468
+ }
469
+ function hasType(resource) {
470
+ return hasProp(resource, 'type');
471
+ }
444
472
 
445
473
  /**
446
474
  @module @ember-data/store
@@ -459,7 +487,7 @@ if (macroCondition(getOwnConfig().polyfillUUID)) {
459
487
  installPolyfill();
460
488
  }
461
489
  function uuidv4() {
462
- assert('crypto.randomUUID needs to be avaliable. Some browsers incorrectly disallow it in insecure contexts. You maybe want to enable the polyfill: https://github.com/emberjs/data#randomuuid-polyfill', _crypto.randomUUID);
490
+ assert('crypto.randomUUID needs to be avaliable. Some browsers incorrectly disallow it in insecure contexts. You maybe want to enable the polyfill: https://github.com/emberjs/data#randomuuid-polyfill', typeof _crypto.randomUUID === 'function');
463
491
  return _crypto.randomUUID();
464
492
  }
465
493
  function freeze(obj) {
@@ -468,6 +496,9 @@ function freeze(obj) {
468
496
  }
469
497
  return obj;
470
498
  }
499
+
500
+ // type IdentifierTypeLookup = { all: Set<StableRecordIdentifier>; id: Map<string, StableRecordIdentifier> };
501
+ // type IdentifiersByType = Map<string, IdentifierTypeLookup>;
471
502
  let configuredForgetMethod;
472
503
  let configuredGenerationMethod;
473
504
  let configuredResetMethod;
@@ -484,20 +515,46 @@ function setIdentifierForgetMethod(method) {
484
515
  function setIdentifierResetMethod(method) {
485
516
  configuredResetMethod = method;
486
517
  }
518
+
519
+ // Map<type, Map<id, lid>>
520
+
521
+ const NEW_IDENTIFIERS = new Map();
522
+ function updateTypeIdMapping(typeMap, identifier, id) {
523
+ let idMap = typeMap.get(identifier.type);
524
+ if (!idMap) {
525
+ idMap = new Map();
526
+ typeMap.set(identifier.type, idMap);
527
+ }
528
+ idMap.set(id, identifier.lid);
529
+ }
530
+ function defaultUpdateMethod(identifier, data, bucket) {
531
+ if (bucket === 'record') {
532
+ assert(`Expected identifier to be a StableRecordIdentifier`, isStableIdentifier(identifier));
533
+ if (!identifier.id && hasId(data)) {
534
+ updateTypeIdMapping(NEW_IDENTIFIERS, identifier, data.id);
535
+ }
536
+ }
537
+ }
538
+ function defaultKeyInfoMethod(resource, known) {
539
+ // TODO RFC something to make this configurable
540
+ const id = hasId(resource) ? coerceId(resource.id) : null;
541
+ const type = hasType(resource) ? normalizeModelName(resource.type) : known ? known.type : null;
542
+ assert(`Expected keyInfoForResource to provide a type for the resource`, type);
543
+ return {
544
+ type,
545
+ id
546
+ };
547
+ }
487
548
  function defaultGenerationMethod(data, bucket) {
488
549
  if (bucket === 'record') {
489
- if (isNonEmptyString(data.lid)) {
550
+ if (hasLid(data)) {
490
551
  return data.lid;
491
552
  }
492
- if (data.id !== undefined) {
493
- let {
494
- type,
495
- id
496
- } = data;
497
- // TODO: add test for id not a string
498
- if (isNonEmptyString(coerceId(id))) {
499
- return `@lid:${normalizeModelName(type)}-${id}`;
500
- }
553
+ assert(`Cannot generate an identifier for a resource without a type`, hasType(data));
554
+ if (hasId(data)) {
555
+ const type = normalizeModelName(data.type);
556
+ const lid = NEW_IDENTIFIERS.get(type)?.get(data.id);
557
+ return lid || `@lid:${type}-${data.id}`;
501
558
  }
502
559
  return uuidv4();
503
560
  } else if (bucket === 'document') {
@@ -512,6 +569,9 @@ function defaultGenerationMethod(data, bucket) {
512
569
  assert(`Unknown bucket ${bucket}`, false);
513
570
  }
514
571
  function defaultEmptyCallback(...args) {}
572
+ function defaultMergeMethod(a, _b, _c) {
573
+ return a;
574
+ }
515
575
  let DEBUG_MAP;
516
576
  if (macroCondition(getOwnConfig().env.DEBUG)) {
517
577
  DEBUG_MAP = new WeakMap();
@@ -532,19 +592,20 @@ if (macroCondition(getOwnConfig().env.DEBUG)) {
532
592
  */
533
593
  class IdentifierCache {
534
594
  constructor() {
535
- this._cache = {
536
- lids: new Map(),
537
- types: Object.create(null),
538
- documents: new Map()
539
- };
540
595
  // we cache the user configuredGenerationMethod at init because it must
541
596
  // be configured prior and is not allowed to be changed
542
597
  this._generate = configuredGenerationMethod || defaultGenerationMethod;
543
- this._update = configuredUpdateMethod || defaultEmptyCallback;
598
+ this._update = configuredUpdateMethod || defaultUpdateMethod;
544
599
  this._forget = configuredForgetMethod || defaultEmptyCallback;
545
600
  this._reset = configuredResetMethod || defaultEmptyCallback;
546
- this._merge = defaultEmptyCallback;
601
+ this._merge = defaultMergeMethod;
602
+ this._keyInfoForResource = defaultKeyInfoMethod;
547
603
  this._isDefaultConfig = !configuredGenerationMethod;
604
+ this._cache = {
605
+ resources: new Map(),
606
+ resourcesByType: Object.create(null),
607
+ documents: new Map()
608
+ };
548
609
  }
549
610
 
550
611
  /**
@@ -557,7 +618,10 @@ class IdentifierCache {
557
618
  * @private
558
619
  */
559
620
  __configureMerge(method) {
560
- this._merge = method || defaultEmptyCallback;
621
+ this._merge = method || defaultMergeMethod;
622
+ }
623
+ upgradeIdentifier(resource) {
624
+ return this._getRecordIdentifier(resource, 2);
561
625
  }
562
626
 
563
627
  /**
@@ -565,130 +629,62 @@ class IdentifierCache {
565
629
  * @private
566
630
  */
567
631
 
568
- _getRecordIdentifier(resource, shouldGenerate = false) {
632
+ _getRecordIdentifier(resource, shouldGenerate) {
633
+ if (macroCondition(getOwnConfig().debug.LOG_IDENTIFIERS)) {
634
+ // eslint-disable-next-line no-console
635
+ console.groupCollapsed(`Identifiers: ${shouldGenerate ? 'Generating' : 'Peeking'} Identifier`, resource);
636
+ }
569
637
  // short circuit if we're already the stable version
570
638
  if (isStableIdentifier(resource)) {
571
639
  if (macroCondition(getOwnConfig().env.DEBUG)) {
572
640
  // TODO should we instead just treat this case as a new generation skipping the short circuit?
573
- if (!this._cache.lids.has(resource.lid) || this._cache.lids.get(resource.lid) !== resource) {
574
- throw new Error(`The supplied identifier ${resource} does not belong to this store instance`);
641
+ if (!this._cache.resources.has(resource.lid) || this._cache.resources.get(resource.lid) !== resource) {
642
+ throw new Error(`The supplied identifier ${JSON.stringify(resource)} does not belong to this store instance`);
575
643
  }
576
644
  }
577
645
  if (macroCondition(getOwnConfig().debug.LOG_IDENTIFIERS)) {
578
646
  // eslint-disable-next-line no-console
579
- console.log(`Identifiers: Peeked Identifier was already Stable ${String(resource)}`);
647
+ console.log(`Identifiers: cache HIT - Stable ${resource.lid}`);
648
+ // eslint-disable-next-line no-console
649
+ console.groupEnd();
580
650
  }
581
651
  return resource;
582
652
  }
583
- let lid = coerceId(resource.lid);
584
- let identifier = lid !== null ? this._cache.lids.get(lid) : undefined;
653
+
654
+ // the resource is unknown, ask the application to identify this data for us
655
+ const lid = this._generate(resource, 'record');
656
+ if (macroCondition(getOwnConfig().debug.LOG_IDENTIFIERS)) {
657
+ // eslint-disable-next-line no-console
658
+ console.log(`Identifiers: ${lid ? 'no ' : ''}lid ${lid ? lid + ' ' : ''}determined for resource`, resource);
659
+ }
660
+ let identifier = /*#__NOINLINE__*/getIdentifierFromLid(this._cache, lid, resource);
585
661
  if (identifier !== undefined) {
586
662
  if (macroCondition(getOwnConfig().debug.LOG_IDENTIFIERS)) {
587
663
  // eslint-disable-next-line no-console
588
- console.log(`Identifiers: cache HIT ${identifier}`, resource);
664
+ console.groupEnd();
589
665
  }
590
666
  return identifier;
591
667
  }
592
- if (macroCondition(getOwnConfig().debug.LOG_IDENTIFIERS)) {
593
- // eslint-disable-next-line no-console
594
- console.groupCollapsed(`Identifiers: ${shouldGenerate ? 'Generating' : 'Peeking'} Identifier`, resource);
595
- }
596
- if (shouldGenerate === false) {
597
- if (!resource.type || !resource.id) {
598
- return;
599
- }
600
- }
601
-
602
- // `type` must always be present
603
- assert('resource.type needs to be a string', 'type' in resource && isNonEmptyString(resource.type));
604
- let type = resource.type && normalizeModelName(resource.type);
605
- let id = coerceId(resource.id);
606
- let keyOptions = getTypeIndex(this._cache.types, type);
607
-
608
- // go straight for the stable RecordIdentifier key'd to `lid`
609
- if (lid !== null) {
610
- identifier = keyOptions.lid.get(lid);
611
- }
612
-
613
- // we may have not seen this resource before
614
- // but just in case we check our own secondary lookup (`id`)
615
- if (identifier === undefined && id !== null) {
616
- identifier = keyOptions.id.get(id);
617
- }
618
- if (identifier === undefined) {
619
- // we have definitely not seen this resource before
620
- // so we allow the user configured `GenerationMethod` to tell us
621
- let newLid = this._generate(resource, 'record');
668
+ if (shouldGenerate === 0) {
622
669
  if (macroCondition(getOwnConfig().debug.LOG_IDENTIFIERS)) {
623
670
  // eslint-disable-next-line no-console
624
- console.log(`Identifiers: lid ${newLid} determined for resource`, resource);
625
- }
626
-
627
- // we do this _even_ when `lid` is present because secondary lookups
628
- // may need to be populated, but we enforce not giving us something
629
- // different than expected
630
- if (lid !== null && newLid !== lid) {
631
- throw new Error(`You should not change the <lid> of a RecordIdentifier`);
632
- } else if (lid === null && !this._isDefaultConfig) {
633
- // allow configuration to tell us that we have
634
- // seen this `lid` before. E.g. a secondary lookup
635
- // connects this resource to a previously seen
636
- // resource.
637
- identifier = keyOptions.lid.get(newLid);
638
- }
639
- if (shouldGenerate === true) {
640
- if (identifier === undefined) {
641
- // if we still don't have an identifier, time to generate one
642
- identifier = makeStableRecordIdentifier(id, type, newLid, 'record', false);
643
-
644
- // populate our unique table
645
- if (macroCondition(getOwnConfig().env.DEBUG)) {
646
- // realistically if you hit this it means you changed `type` :/
647
- // TODO consider how to handle type change assertions more gracefully
648
- if (this._cache.lids.has(identifier.lid)) {
649
- throw new Error(`You should not change the <type> of a RecordIdentifier`);
650
- }
651
- }
652
- this._cache.lids.set(identifier.lid, identifier);
653
-
654
- // populate our primary lookup table
655
- // TODO consider having the `lid` cache be
656
- // one level up
657
- keyOptions.lid.set(identifier.lid, identifier);
658
- if (macroCondition(getOwnConfig().debug.LOG_IDENTIFIERS)) {
659
- if (shouldGenerate) {
660
- // eslint-disable-next-line no-console
661
- console.log(`Identifiers: generated ${String(identifier)} for`, resource);
662
- if (resource[DEBUG_IDENTIFIER_BUCKET]) {
663
- // eslint-disable-next-line no-console
664
- console.trace(`[WARNING] Identifiers: generated a new identifier from a previously used identifier. This is likely a bug.`);
665
- }
666
- }
667
- }
668
- }
669
-
670
- // populate our own secondary lookup table
671
- // even for the "successful" secondary lookup
672
- // by `_generate()`, since we missed the cache
673
- // previously
674
- // we use identifier.id instead of id here
675
- // because they may not match and we prefer
676
- // what we've set via resource data
677
- if (identifier.id !== null) {
678
- keyOptions.id.set(identifier.id, identifier);
679
-
680
- // TODO allow filling out of `id` here
681
- // for the `username` non-client created
682
- // case.
683
- }
671
+ console.groupEnd();
684
672
  }
673
+ return;
685
674
  }
686
675
 
676
+ // if we still don't have an identifier, time to generate one
677
+ if (shouldGenerate === 2) {
678
+ resource.lid = lid;
679
+ identifier = /*#__NOINLINE__*/makeStableRecordIdentifier(resource, 'record', false);
680
+ } else {
681
+ // we lie a bit here as a memory optimization
682
+ const keyInfo = this._keyInfoForResource(resource, null);
683
+ keyInfo.lid = lid;
684
+ identifier = /*#__NOINLINE__*/makeStableRecordIdentifier(keyInfo, 'record', false);
685
+ }
686
+ addResourceToCache(this._cache, identifier);
687
687
  if (macroCondition(getOwnConfig().debug.LOG_IDENTIFIERS)) {
688
- if (!identifier && !shouldGenerate) {
689
- // eslint-disable-next-line no-console
690
- console.log(`Identifiers: cache MISS`, resource);
691
- }
692
688
  // eslint-disable-next-line no-console
693
689
  console.groupEnd();
694
690
  }
@@ -706,7 +702,7 @@ class IdentifierCache {
706
702
  * @private
707
703
  */
708
704
  peekRecordIdentifier(resource) {
709
- return this._getRecordIdentifier(resource, false);
705
+ return this._getRecordIdentifier(resource, 0);
710
706
  }
711
707
 
712
708
  /**
@@ -751,9 +747,8 @@ class IdentifierCache {
751
747
  @returns {StableRecordIdentifier}
752
748
  @public
753
749
  */
754
-
755
750
  getOrCreateRecordIdentifier(resource) {
756
- return this._getRecordIdentifier(resource, true);
751
+ return this._getRecordIdentifier(resource, 1);
757
752
  }
758
753
 
759
754
  /**
@@ -769,22 +764,21 @@ class IdentifierCache {
769
764
  */
770
765
  createIdentifierForNewRecord(data) {
771
766
  let newLid = this._generate(data, 'record');
772
- let identifier = makeStableRecordIdentifier(data.id || null, data.type, newLid, 'record', true);
773
- let keyOptions = getTypeIndex(this._cache.types, data.type);
767
+ let identifier = /*#__NOINLINE__*/makeStableRecordIdentifier({
768
+ id: data.id || null,
769
+ type: data.type,
770
+ lid: newLid
771
+ }, 'record', true);
774
772
 
775
773
  // populate our unique table
776
774
  if (macroCondition(getOwnConfig().env.DEBUG)) {
777
- if (this._cache.lids.has(identifier.lid)) {
775
+ if (this._cache.resources.has(identifier.lid)) {
778
776
  throw new Error(`The lid generated for the new record is not unique as it matches an existing identifier`);
779
777
  }
780
778
  }
781
- this._cache.lids.set(identifier.lid, identifier);
782
779
 
783
- // populate the type+lid cache
784
- keyOptions.lid.set(newLid, identifier);
785
- if (data.id) {
786
- keyOptions.id.set(data.id, identifier);
787
- }
780
+ /*#__NOINLINE__*/
781
+ addResourceToCache(this._cache, identifier);
788
782
  if (macroCondition(getOwnConfig().debug.LOG_IDENTIFIERS)) {
789
783
  // eslint-disable-next-line no-console
790
784
  console.log(`Identifiers: created identifier ${String(identifier)} for newly generated resource`, data);
@@ -812,43 +806,49 @@ class IdentifierCache {
812
806
  */
813
807
  updateRecordIdentifier(identifierObject, data) {
814
808
  let identifier = this.getOrCreateRecordIdentifier(identifierObject);
815
- let newId = data.id !== undefined ? coerceId(data.id) : null;
816
- let existingIdentifier = detectMerge(this._cache.types, identifier, data, newId, this._cache.lids);
809
+ const keyInfo = this._keyInfoForResource(data, identifier);
810
+ let existingIdentifier = /*#__NOINLINE__*/detectMerge(this._cache, keyInfo, identifier, data);
811
+ const hadLid = hasLid(data);
817
812
  if (!existingIdentifier) {
818
813
  // If the incoming type does not match the identifier type, we need to create an identifier for the incoming
819
814
  // data so we can merge the incoming data with the existing identifier, see #7325 and #7363
820
- if (data.type && identifier.type !== normalizeModelName(data.type)) {
821
- let incomingDataResource = {
822
- ...data
823
- };
824
- // Need to strip the lid from the incomingData in order force a new identifier creation
825
- delete incomingDataResource.lid;
826
- existingIdentifier = this.getOrCreateRecordIdentifier(incomingDataResource);
815
+ if (identifier.type !== keyInfo.type) {
816
+ if (hadLid) {
817
+ // Strip the lid to ensure we force a new identifier creation
818
+ delete data.lid;
819
+ }
820
+ existingIdentifier = this.getOrCreateRecordIdentifier(data);
827
821
  }
828
822
  }
829
823
  if (existingIdentifier) {
830
- let keyOptions = getTypeIndex(this._cache.types, identifier.type);
831
824
  let generatedIdentifier = identifier;
832
- identifier = this._mergeRecordIdentifiers(keyOptions, generatedIdentifier, existingIdentifier, data, newId);
825
+ identifier = this._mergeRecordIdentifiers(keyInfo, generatedIdentifier, existingIdentifier, data);
826
+
827
+ // make sure that the `lid` on the data we are processing matches the lid we kept
828
+ if (hadLid) {
829
+ data.lid = identifier.lid;
830
+ }
833
831
  if (macroCondition(getOwnConfig().debug.LOG_IDENTIFIERS)) {
834
832
  // eslint-disable-next-line no-console
835
833
  console.log(`Identifiers: merged identifiers ${generatedIdentifier.lid} and ${existingIdentifier.lid} for resource into ${identifier.lid}`, data);
836
834
  }
837
835
  }
838
836
  let id = identifier.id;
839
- performRecordIdentifierUpdate(identifier, data, this._update);
840
- newId = identifier.id;
837
+ /*#__NOINLINE__*/
838
+ performRecordIdentifierUpdate(identifier, keyInfo, data, this._update);
839
+ const newId = identifier.id;
841
840
 
842
841
  // add to our own secondary lookup table
843
842
  if (id !== newId && newId !== null) {
844
843
  if (macroCondition(getOwnConfig().debug.LOG_IDENTIFIERS)) {
845
844
  // eslint-disable-next-line no-console
846
- console.log(`Identifiers: updated id for identifier ${identifier.lid} from '${id}' to '${newId}' for resource`, data);
845
+ console.log(`Identifiers: updated id for identifier ${identifier.lid} from '${String(id)}' to '${String(newId)}' for resource`, data);
847
846
  }
848
- let keyOptions = getTypeIndex(this._cache.types, identifier.type);
849
- keyOptions.id.set(newId, identifier);
847
+ const typeSet = this._cache.resourcesByType[identifier.type];
848
+ assert(`Expected to find a typeSet for ${identifier.type}`, typeSet);
849
+ typeSet.id.set(newId, identifier);
850
850
  if (id !== null) {
851
- keyOptions.id.delete(id);
851
+ typeSet.id.delete(id);
852
852
  }
853
853
  } else if (macroCondition(getOwnConfig().debug.LOG_IDENTIFIERS)) {
854
854
  // eslint-disable-next-line no-console
@@ -861,22 +861,22 @@ class IdentifierCache {
861
861
  * @method _mergeRecordIdentifiers
862
862
  * @private
863
863
  */
864
- _mergeRecordIdentifiers(keyOptions, identifier, existingIdentifier, data, newId) {
864
+ _mergeRecordIdentifiers(keyInfo, identifier, existingIdentifier, data) {
865
+ assert(`Expected keyInfo to contain an id`, hasId(keyInfo));
865
866
  // delegate determining which identifier to keep to the configured MergeMethod
866
- let kept = this._merge(identifier, existingIdentifier, data);
867
- let abandoned = kept === identifier ? existingIdentifier : identifier;
867
+ const kept = this._merge(identifier, existingIdentifier, data);
868
+ const abandoned = kept === identifier ? existingIdentifier : identifier;
868
869
 
869
870
  // cleanup the identifier we no longer need
870
871
  this.forgetRecordIdentifier(abandoned);
871
872
 
872
873
  // ensure a secondary cache entry for this id for the identifier we do keep
873
- keyOptions.id.set(newId, kept);
874
+ // keyOptions.id.set(newId, kept);
875
+
874
876
  // ensure a secondary cache entry for this id for the abandoned identifier's type we do keep
875
- let baseKeyOptions = getTypeIndex(this._cache.types, existingIdentifier.type);
876
- baseKeyOptions.id.set(newId, kept);
877
+ // let baseKeyOptions = getTypeIndex(this._cache.resourcesByType, existingIdentifier.type);
878
+ // baseKeyOptions.id.set(newId, kept);
877
879
 
878
- // make sure that the `lid` on the data we are processing matches the lid we kept
879
- data.lid = kept.lid;
880
880
  return kept;
881
881
  }
882
882
 
@@ -891,14 +891,15 @@ class IdentifierCache {
891
891
  @public
892
892
  */
893
893
  forgetRecordIdentifier(identifierObject) {
894
- let identifier = this.getOrCreateRecordIdentifier(identifierObject);
895
- let keyOptions = getTypeIndex(this._cache.types, identifier.type);
894
+ const identifier = this.getOrCreateRecordIdentifier(identifierObject);
895
+ const typeSet = this._cache.resourcesByType[identifier.type];
896
+ assert(`Expected to find a typeSet for ${identifier.type}`, typeSet);
896
897
  if (identifier.id !== null) {
897
- keyOptions.id.delete(identifier.id);
898
+ typeSet.id.delete(identifier.id);
898
899
  }
899
- this._cache.lids.delete(identifier.lid);
900
- keyOptions.lid.delete(identifier.lid);
901
- IDENTIFIERS.delete(identifierObject);
900
+ this._cache.resources.delete(identifier.lid);
901
+ typeSet.lid.delete(identifier.lid);
902
+ IDENTIFIERS.delete(identifier);
902
903
  this._forget(identifier, 'record');
903
904
  if (macroCondition(getOwnConfig().debug.LOG_IDENTIFIERS)) {
904
905
  // eslint-disable-next-line no-console
@@ -906,29 +907,14 @@ class IdentifierCache {
906
907
  }
907
908
  }
908
909
  destroy() {
910
+ NEW_IDENTIFIERS.clear();
909
911
  this._cache.documents.forEach(identifier => {
910
912
  DOCUMENTS.delete(identifier);
911
913
  });
912
914
  this._reset();
913
915
  }
914
916
  }
915
- function getTypeIndex(typeMap, type) {
916
- let typeIndex = typeMap[type];
917
- if (typeIndex === undefined) {
918
- typeIndex = {
919
- lid: new Map(),
920
- id: new Map()
921
- };
922
- typeMap[type] = typeIndex;
923
- }
924
- return typeIndex;
925
- }
926
- function makeStableRecordIdentifier(id, type, lid, bucket, clientOriginated = false) {
927
- let recordIdentifier = {
928
- lid,
929
- id,
930
- type
931
- };
917
+ function makeStableRecordIdentifier(recordIdentifier, bucket, clientOriginated) {
932
918
  IDENTIFIERS.add(recordIdentifier);
933
919
  if (macroCondition(getOwnConfig().env.DEBUG)) {
934
920
  // we enforce immutability in dev
@@ -944,15 +930,15 @@ function makeStableRecordIdentifier(id, type, lid, bucket, clientOriginated = fa
944
930
  return recordIdentifier.type;
945
931
  },
946
932
  toString() {
947
- let {
933
+ const {
948
934
  type,
949
935
  id,
950
936
  lid
951
937
  } = recordIdentifier;
952
- return `${clientOriginated ? '[CLIENT_ORIGINATED] ' : ''}${type}:${id} (${lid})`;
938
+ return `${clientOriginated ? '[CLIENT_ORIGINATED] ' : ''}${String(type)}:${String(id)} (${lid})`;
953
939
  },
954
940
  toJSON() {
955
- let {
941
+ const {
956
942
  type,
957
943
  id,
958
944
  lid
@@ -973,37 +959,33 @@ function makeStableRecordIdentifier(id, type, lid, bucket, clientOriginated = fa
973
959
  }
974
960
  return recordIdentifier;
975
961
  }
976
- function performRecordIdentifierUpdate(identifier, data, updateFn) {
962
+ function performRecordIdentifierUpdate(identifier, keyInfo, data, updateFn) {
977
963
  if (macroCondition(getOwnConfig().env.DEBUG)) {
978
- let {
979
- lid
980
- } = data;
981
- let id = 'id' in data ? data.id : undefined;
982
- let type = 'type' in data && data.type && normalizeModelName(data.type);
964
+ const {
965
+ id,
966
+ type
967
+ } = keyInfo;
983
968
 
984
969
  // get the mutable instance behind our proxy wrapper
985
970
  let wrapper = identifier;
986
971
  identifier = DEBUG_MAP.get(wrapper);
987
- if (lid !== undefined) {
988
- let newLid = coerceId(lid);
989
- if (newLid !== identifier.lid) {
990
- throw new Error(`The 'lid' for a RecordIdentifier cannot be updated once it has been created. Attempted to set lid for '${wrapper}' to '${lid}'.`);
972
+ if (hasLid(data)) {
973
+ const lid = data.lid;
974
+ if (lid !== identifier.lid) {
975
+ throw new Error(`The 'lid' for a RecordIdentifier cannot be updated once it has been created. Attempted to set lid for '${wrapper.lid}' to '${lid}'.`);
991
976
  }
992
977
  }
993
- if (id !== undefined) {
994
- let newId = coerceId(id);
995
- if (identifier.id !== null && identifier.id !== newId) {
996
- // here we warn and ignore, as this may be a mistake, but we allow the user
997
- // to have multiple cache-keys pointing at a single lid so we cannot error
998
- warn(`The 'id' for a RecordIdentifier should not be updated once it has been set. Attempted to set id for '${wrapper}' to '${newId}'.`, false, {
999
- id: 'ember-data:multiple-ids-for-identifier'
1000
- });
1001
- }
978
+ if (id && identifier.id !== null && identifier.id !== id) {
979
+ // here we warn and ignore, as this may be a mistake, but we allow the user
980
+ // to have multiple cache-keys pointing at a single lid so we cannot error
981
+ warn(`The 'id' for a RecordIdentifier should not be updated once it has been set. Attempted to set id for '${wrapper.lid}' to '${id}'.`, false, {
982
+ id: 'ember-data:multiple-ids-for-identifier'
983
+ });
1002
984
  }
1003
985
 
1004
986
  // TODO consider just ignoring here to allow flexible polymorphic support
1005
987
  if (type && type !== identifier.type) {
1006
- throw new Error(`The 'type' for a RecordIdentifier cannot be updated once it has been set. Attempted to set type for '${wrapper}' to '${type}'.`);
988
+ throw new Error(`The 'type' for a RecordIdentifier cannot be updated once it has been set. Attempted to set type for '${wrapper.lid}' to '${type}'.`);
1007
989
  }
1008
990
  updateFn(wrapper, data, 'record');
1009
991
  } else {
@@ -1018,32 +1000,62 @@ function performRecordIdentifierUpdate(identifier, data, updateFn) {
1018
1000
  identifier.id = coerceId(data.id);
1019
1001
  }
1020
1002
  }
1021
- function detectMerge(typesCache, identifier, data, newId, lids) {
1003
+ function detectMerge(cache, keyInfo, identifier, data) {
1004
+ const newId = keyInfo.id;
1022
1005
  const {
1023
1006
  id,
1024
1007
  type,
1025
1008
  lid
1026
1009
  } = identifier;
1010
+ const typeSet = cache.resourcesByType[identifier.type];
1011
+
1012
+ // if the IDs are present but do not match
1013
+ // then check if we have an existing identifier
1014
+ // for the newer ID.
1027
1015
  if (id !== null && id !== newId && newId !== null) {
1028
- let keyOptions = getTypeIndex(typesCache, identifier.type);
1029
- let existingIdentifier = keyOptions.id.get(newId);
1016
+ const existingIdentifier = typeSet && typeSet.id.get(newId);
1030
1017
  return existingIdentifier !== undefined ? existingIdentifier : false;
1031
1018
  } else {
1032
- let newType = data.type && normalizeModelName(data.type);
1019
+ const newType = keyInfo.type;
1033
1020
 
1034
1021
  // If the ids and type are the same but lid is not the same, we should trigger a merge of the identifiers
1035
- if (id !== null && id === newId && newType === type && data.lid && data.lid !== lid) {
1036
- let existingIdentifier = lids.get(data.lid);
1037
- return existingIdentifier !== undefined ? existingIdentifier : false;
1022
+ // we trigger a merge of the identifiers
1023
+ // though probably we should just throw an error here
1024
+ if (id !== null && id === newId && newType === type && hasLid(data) && data.lid !== lid) {
1025
+ return cache.resources.get(data.lid) || false;
1026
+
1038
1027
  // If the lids are the same, and ids are the same, but types are different we should trigger a merge of the identifiers
1039
- } else if (id !== null && id === newId && newType && newType !== type && data.lid && data.lid === lid) {
1040
- let keyOptions = getTypeIndex(typesCache, newType);
1041
- let existingIdentifier = keyOptions.id.get(id);
1028
+ } else if (id !== null && id === newId && newType && newType !== type && hasLid(data) && data.lid === lid) {
1029
+ const newTypeSet = cache.resourcesByType[newType];
1030
+ const existingIdentifier = newTypeSet && newTypeSet.id.get(newId);
1042
1031
  return existingIdentifier !== undefined ? existingIdentifier : false;
1043
1032
  }
1044
1033
  }
1045
1034
  return false;
1046
1035
  }
1036
+ function getIdentifierFromLid(cache, lid, resource) {
1037
+ const identifier = cache.resources.get(lid);
1038
+ if (macroCondition(getOwnConfig().debug.LOG_IDENTIFIERS)) {
1039
+ // eslint-disable-next-line no-console
1040
+ console.log(`Identifiers: cache ${identifier ? 'HIT' : 'MISS'} - Non-Stable ${lid}`, resource);
1041
+ }
1042
+ return identifier;
1043
+ }
1044
+ function addResourceToCache(cache, identifier) {
1045
+ cache.resources.set(identifier.lid, identifier);
1046
+ let typeSet = cache.resourcesByType[identifier.type];
1047
+ if (!typeSet) {
1048
+ typeSet = {
1049
+ lid: new Map(),
1050
+ id: new Map()
1051
+ };
1052
+ cache.resourcesByType[identifier.type] = typeSet;
1053
+ }
1054
+ typeSet.lid.set(identifier.lid, identifier);
1055
+ if (identifier.id) {
1056
+ typeSet.id.set(identifier.id, identifier);
1057
+ }
1058
+ }
1047
1059
  var _class$1, _descriptor$1;
1048
1060
 
1049
1061
  /**
@@ -1402,9 +1414,12 @@ class InstanceCache {
1402
1414
  store.identifierCache.__configureMerge((identifier, matchedIdentifier, resourceData) => {
1403
1415
  let keptIdentifier = identifier;
1404
1416
  if (identifier.id !== matchedIdentifier.id) {
1417
+ // @ts-expect-error TODO this needs to be fixed
1405
1418
  keptIdentifier = 'id' in resourceData && identifier.id === resourceData.id ? identifier : matchedIdentifier;
1406
1419
  } else if (identifier.type !== matchedIdentifier.type) {
1407
- keptIdentifier = 'type' in resourceData && identifier.type === resourceData.type ? identifier : matchedIdentifier;
1420
+ keptIdentifier =
1421
+ // @ts-expect-error TODO this needs to be fixed
1422
+ 'type' in resourceData && identifier.type === resourceData.type ? identifier : matchedIdentifier;
1408
1423
  }
1409
1424
  let staleIdentifier = identifier === keptIdentifier ? matchedIdentifier : identifier;
1410
1425
 
@@ -1419,6 +1434,7 @@ class InstanceCache {
1419
1434
  // we can probably just "swap" what data source the abandoned
1420
1435
  // record points at so long as
1421
1436
  // it itself is not retained by the store in any way.
1437
+ // @ts-expect-error TODO this needs to be fixed
1422
1438
  if ('id' in resourceData) {
1423
1439
  throw new Error(`Failed to update the 'id' for the RecordIdentifier '${identifier.type}:${String(identifier.id)} (${identifier.lid})' to '${String(resourceData.id)}', because that id is already in use by '${matchedIdentifier.type}:${String(matchedIdentifier.id)} (${matchedIdentifier.lid})'`);
1424
1440
  }
@@ -1557,11 +1573,11 @@ class InstanceCache {
1557
1573
  if (type === undefined) {
1558
1574
  // it would be cool if we could just de-ref cache here
1559
1575
  // but probably would require WeakRef models to do so.
1560
- cache.lids.forEach(identifier => {
1576
+ cache.resources.forEach(identifier => {
1561
1577
  this.unloadRecord(identifier);
1562
1578
  });
1563
1579
  } else {
1564
- const typeCache = cache.types;
1580
+ const typeCache = cache.resourcesByType;
1565
1581
  let identifiers = typeCache[type]?.lid;
1566
1582
  if (identifiers) {
1567
1583
  identifiers.forEach(identifier => {
@@ -1683,7 +1699,7 @@ function _convertPreloadRelationshipToJSON(value, type) {
1683
1699
  if (typeof value === 'string' || typeof value === 'number') {
1684
1700
  return {
1685
1701
  type,
1686
- id: value
1702
+ id: ensureStringId(value)
1687
1703
  };
1688
1704
  }
1689
1705
  // TODO if not a record instance assert it's an identifier
@@ -1712,7 +1728,7 @@ function getShimClass(store, modelName) {
1712
1728
  return shim;
1713
1729
  }
1714
1730
  function mapFromHash(hash) {
1715
- let map = new Map();
1731
+ const map = new Map();
1716
1732
  for (let i in hash) {
1717
1733
  if (Object.prototype.hasOwnProperty.call(hash, i)) {
1718
1734
  map.set(i, hash[i]);
@@ -1721,7 +1737,7 @@ function mapFromHash(hash) {
1721
1737
  return map;
1722
1738
  }
1723
1739
 
1724
- // Mimics the static apis of DSModel
1740
+ // Mimics the static apis of @ember-data/model
1725
1741
  class ShimModelClass {
1726
1742
  constructor(store, modelName) {
1727
1743
  this.__store = store;
@@ -2561,13 +2577,13 @@ let IdentifierArray = (_class3 = class IdentifierArray {
2561
2577
  @type Store
2562
2578
  */
2563
2579
 
2564
- destroy() {
2565
- this.isDestroying = true;
2580
+ destroy(clear) {
2581
+ this.isDestroying = !clear;
2566
2582
  // changing the reference breaks the Proxy
2567
2583
  // this[SOURCE] = [];
2568
2584
  this[SOURCE].length = 0;
2569
2585
  this[NOTIFY]();
2570
- this.isDestroyed = true;
2586
+ this.isDestroyed = !clear;
2571
2587
  }
2572
2588
 
2573
2589
  // length must be on self for proxied methods to work properly
@@ -2872,8 +2888,8 @@ class Collection extends IdentifierArray {
2872
2888
  });
2873
2889
  return promise;
2874
2890
  }
2875
- destroy() {
2876
- super.destroy();
2891
+ destroy(clear) {
2892
+ super.destroy(clear);
2877
2893
  this._manager._managed.delete(this);
2878
2894
  this._manager._pending.delete(this);
2879
2895
  }
@@ -2971,10 +2987,14 @@ class RecordArrayManager {
2971
2987
  this._staged = new Map();
2972
2988
  this._keyedArrays = new Map();
2973
2989
  this._identifiers = new Map();
2990
+ this._set = new Map();
2991
+ this._visibilitySet = new Map();
2974
2992
  this._subscription = this.store.notifications.subscribe('resource', (identifier, type) => {
2975
2993
  if (type === 'added') {
2994
+ this._visibilitySet.set(identifier, true);
2976
2995
  this.identifierAdded(identifier);
2977
2996
  } else if (type === 'removed') {
2997
+ this._visibilitySet.set(identifier, false);
2978
2998
  this.identifierRemoved(identifier);
2979
2999
  } else if (type === 'state') {
2980
3000
  this.identifierChanged(identifier);
@@ -2986,7 +3006,7 @@ class RecordArrayManager {
2986
3006
  if (!pending || this.isDestroying || this.isDestroyed) {
2987
3007
  return;
2988
3008
  }
2989
- sync(array, pending);
3009
+ sync(array, pending, this._set.get(array));
2990
3010
  this._pending.delete(array);
2991
3011
  }
2992
3012
 
@@ -3019,6 +3039,7 @@ class RecordArrayManager {
3019
3039
  manager: this
3020
3040
  });
3021
3041
  this._live.set(type, array);
3042
+ this._set.set(array, new Set(identifiers));
3022
3043
  }
3023
3044
  return array;
3024
3045
  }
@@ -3036,6 +3057,7 @@ class RecordArrayManager {
3036
3057
  };
3037
3058
  let array = new Collection(options);
3038
3059
  this._managed.add(array);
3060
+ this._set.set(array, new Set(options.identifiers || []));
3039
3061
  if (config.identifiers) {
3040
3062
  associate(this._identifiers, array, config.identifiers);
3041
3063
  }
@@ -3107,6 +3129,7 @@ class RecordArrayManager {
3107
3129
  const old = source.slice();
3108
3130
  source.length = 0;
3109
3131
  fastPush(source, identifiers);
3132
+ this._set.set(array, new Set(identifiers));
3110
3133
  notifyArray(array);
3111
3134
  array.meta = payload.meta || null;
3112
3135
  array.links = payload.links || null;
@@ -3144,21 +3167,30 @@ class RecordArrayManager {
3144
3167
  }
3145
3168
  identifierChanged(identifier) {
3146
3169
  let newState = this.store._instanceCache.recordIsLoaded(identifier, true);
3170
+
3171
+ // if the change matches the most recent direct added/removed
3172
+ // state, then we can ignore it
3173
+ if (this._visibilitySet.get(identifier) === newState) {
3174
+ return;
3175
+ }
3147
3176
  if (newState) {
3148
3177
  this.identifierAdded(identifier);
3149
3178
  } else {
3150
3179
  this.identifierRemoved(identifier);
3151
3180
  }
3152
3181
  }
3153
- clear() {
3154
- this._live.forEach(array => array.destroy());
3155
- this._managed.forEach(array => array.destroy());
3182
+ clear(isClear = true) {
3183
+ this._live.forEach(array => array.destroy(isClear));
3184
+ this._managed.forEach(array => array.destroy(isClear));
3156
3185
  this._managed.clear();
3157
3186
  this._identifiers.clear();
3187
+ this._pending.clear();
3188
+ this._set.forEach(set => set.clear());
3189
+ this._visibilitySet.clear();
3158
3190
  }
3159
3191
  destroy() {
3160
3192
  this.isDestroying = true;
3161
- this.clear();
3193
+ this.clear(false);
3162
3194
  this._live.clear();
3163
3195
  this.isDestroyed = true;
3164
3196
  // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
@@ -3187,19 +3219,20 @@ function disassociateIdentifier(ArraysCache, array, identifier) {
3187
3219
  cache.delete(array);
3188
3220
  }
3189
3221
  }
3190
- function sync(array, changes) {
3222
+ function sync(array, changes, arraySet) {
3191
3223
  let state = array[SOURCE];
3192
3224
  const adds = [];
3193
3225
  const removes = [];
3194
3226
  changes.forEach((value, key) => {
3195
3227
  if (value === 'add') {
3196
3228
  // likely we want to keep a Set along-side
3197
- if (state.includes(key)) {
3229
+ if (arraySet.has(key)) {
3198
3230
  return;
3199
3231
  }
3200
3232
  adds.push(key);
3233
+ arraySet.add(key);
3201
3234
  } else {
3202
- if (state.includes(key)) {
3235
+ if (arraySet.has(key)) {
3203
3236
  removes.push(key);
3204
3237
  }
3205
3238
  }
@@ -3207,6 +3240,7 @@ function sync(array, changes) {
3207
3240
  if (removes.length) {
3208
3241
  if (removes.length === state.length) {
3209
3242
  state.length = 0;
3243
+ arraySet.clear();
3210
3244
  // changing the reference breaks the Proxy
3211
3245
  // state = array[SOURCE] = [];
3212
3246
  } else {
@@ -3214,6 +3248,7 @@ function sync(array, changes) {
3214
3248
  const index = state.indexOf(i);
3215
3249
  if (index !== -1) {
3216
3250
  state.splice(index, 1);
3251
+ arraySet.delete(i);
3217
3252
  }
3218
3253
  });
3219
3254
  }
@@ -3232,11 +3267,9 @@ function sync(array, changes) {
3232
3267
  }
3233
3268
  }
3234
3269
 
3235
- /**
3236
- * @module @ember-data/store
3237
- */
3238
3270
  const Touching = Symbol('touching');
3239
3271
  const RequestPromise = Symbol('promise');
3272
+ const EMPTY_ARR = macroCondition(getOwnConfig().env.DEBUG) ? Object.freeze([]) : [];
3240
3273
  function hasRecordIdentifier(op) {
3241
3274
  return 'recordIdentifier' in op;
3242
3275
  }
@@ -3250,9 +3283,9 @@ function hasRecordIdentifier(op) {
3250
3283
  */
3251
3284
  class RequestStateService {
3252
3285
  constructor(store) {
3253
- this._pending = Object.create(null);
3286
+ this._pending = new Map();
3254
3287
  this._done = new Map();
3255
- this._subscriptions = Object.create(null);
3288
+ this._subscriptions = new Map();
3256
3289
  this._toFlush = [];
3257
3290
  this._store = void 0;
3258
3291
  this._store = store;
@@ -3263,10 +3296,10 @@ class RequestStateService {
3263
3296
  _enqueue(promise, queryRequest) {
3264
3297
  let query = queryRequest.data[0];
3265
3298
  if (hasRecordIdentifier(query)) {
3266
- let lid = query.recordIdentifier.lid;
3299
+ const identifier = query.recordIdentifier;
3267
3300
  let type = query.op === 'saveRecord' ? 'mutation' : 'query';
3268
- if (!this._pending[lid]) {
3269
- this._pending[lid] = [];
3301
+ if (!this._pending.has(identifier)) {
3302
+ this._pending.set(identifier, []);
3270
3303
  }
3271
3304
  let request = {
3272
3305
  state: 'pending',
@@ -3275,10 +3308,10 @@ class RequestStateService {
3275
3308
  };
3276
3309
  request[Touching] = [query.recordIdentifier];
3277
3310
  request[RequestPromise] = promise;
3278
- this._pending[lid].push(request);
3311
+ this._pending.get(identifier).push(request);
3279
3312
  this._triggerSubscriptions(request);
3280
3313
  return promise.then(result => {
3281
- this._dequeue(lid, request);
3314
+ this._dequeue(identifier, request);
3282
3315
  let finalizedRequest = {
3283
3316
  state: 'fulfilled',
3284
3317
  request: queryRequest,
@@ -3292,7 +3325,7 @@ class RequestStateService {
3292
3325
  this._triggerSubscriptions(finalizedRequest);
3293
3326
  return result;
3294
3327
  }, error => {
3295
- this._dequeue(lid, request);
3328
+ this._dequeue(identifier, request);
3296
3329
  let finalizedRequest = {
3297
3330
  state: 'rejected',
3298
3331
  request: queryRequest,
@@ -3329,13 +3362,15 @@ class RequestStateService {
3329
3362
  }
3330
3363
  _flushRequest(req) {
3331
3364
  req[Touching].forEach(identifier => {
3332
- if (this._subscriptions[identifier.lid]) {
3333
- this._subscriptions[identifier.lid].forEach(callback => callback(req));
3365
+ const subscriptions = this._subscriptions.get(identifier);
3366
+ if (subscriptions) {
3367
+ subscriptions.forEach(callback => callback(req));
3334
3368
  }
3335
3369
  });
3336
3370
  }
3337
- _dequeue(lid, request) {
3338
- this._pending[lid] = this._pending[lid].filter(req => req !== request);
3371
+ _dequeue(identifier, request) {
3372
+ const pending = this._pending.get(identifier);
3373
+ this._pending.set(identifier, pending.filter(req => req !== request));
3339
3374
  }
3340
3375
  _addDone(request) {
3341
3376
  request[Touching].forEach(identifier => {
@@ -3346,7 +3381,7 @@ class RequestStateService {
3346
3381
  requests = requests.filter(req => {
3347
3382
  // TODO add support for multiple
3348
3383
  let data;
3349
- if (req.request.data instanceof Array) {
3384
+ if (Array.isArray(req.request.data)) {
3350
3385
  data = req.request.data[0];
3351
3386
  } else {
3352
3387
  data = req.request.data;
@@ -3390,10 +3425,12 @@ class RequestStateService {
3390
3425
  * @param {(state: RequestState) => void} callback
3391
3426
  */
3392
3427
  subscribeForRecord(identifier, callback) {
3393
- if (!this._subscriptions[identifier.lid]) {
3394
- this._subscriptions[identifier.lid] = [];
3428
+ let subscriptions = this._subscriptions.get(identifier);
3429
+ if (!subscriptions) {
3430
+ subscriptions = [];
3431
+ this._subscriptions.set(identifier, subscriptions);
3395
3432
  }
3396
- this._subscriptions[identifier.lid].push(callback);
3433
+ subscriptions.push(callback);
3397
3434
  }
3398
3435
 
3399
3436
  /**
@@ -3405,10 +3442,7 @@ class RequestStateService {
3405
3442
  * @returns {RequestState[]} an array of request states for any pending requests for the given identifier
3406
3443
  */
3407
3444
  getPendingRequestsForRecord(identifier) {
3408
- if (this._pending[identifier.lid]) {
3409
- return this._pending[identifier.lid];
3410
- }
3411
- return [];
3445
+ return this._pending.get(identifier) || EMPTY_ARR;
3412
3446
  }
3413
3447
 
3414
3448
  /**
@@ -3427,6 +3461,9 @@ class RequestStateService {
3427
3461
  return null;
3428
3462
  }
3429
3463
  }
3464
+ function isNonEmptyString(str) {
3465
+ return Boolean(str && typeof str === 'string');
3466
+ }
3430
3467
  function constructResource(type, id, lid) {
3431
3468
  if (typeof type === 'object' && type !== null) {
3432
3469
  let resource = type;
@@ -3623,17 +3660,34 @@ class Store extends EmberObject {
3623
3660
  _run(cb) {
3624
3661
  assert(`EmberData should never encounter a nested run`, !this._cbs);
3625
3662
  const _cbs = this._cbs = {};
3626
- cb();
3627
- if (_cbs.coalesce) {
3628
- _cbs.coalesce();
3629
- }
3630
- if (_cbs.sync) {
3631
- _cbs.sync();
3632
- }
3633
- if (_cbs.notify) {
3634
- _cbs.notify();
3663
+ if (macroCondition(getOwnConfig().env.DEBUG)) {
3664
+ try {
3665
+ cb();
3666
+ if (_cbs.coalesce) {
3667
+ _cbs.coalesce();
3668
+ }
3669
+ if (_cbs.sync) {
3670
+ _cbs.sync();
3671
+ }
3672
+ if (_cbs.notify) {
3673
+ _cbs.notify();
3674
+ }
3675
+ } finally {
3676
+ this._cbs = null;
3677
+ }
3678
+ } else {
3679
+ cb();
3680
+ if (_cbs.coalesce) {
3681
+ _cbs.coalesce();
3682
+ }
3683
+ if (_cbs.sync) {
3684
+ _cbs.sync();
3685
+ }
3686
+ if (_cbs.notify) {
3687
+ _cbs.notify();
3688
+ }
3689
+ this._cbs = null;
3635
3690
  }
3636
- this._cbs = null;
3637
3691
  }
3638
3692
  _join(cb) {
3639
3693
  if (this._cbs) {
@@ -3666,9 +3720,8 @@ class Store extends EmberObject {
3666
3720
  if (macroCondition(getOwnConfig().env.TESTING)) {
3667
3721
  const all = [];
3668
3722
  const pending = this._requestCache._pending;
3669
- const lids = Object.keys(pending);
3670
- lids.forEach(lid => {
3671
- all.push(...pending[lid].map(v => v[RequestPromise]));
3723
+ pending.forEach(requests => {
3724
+ all.push(...requests.map(v => v[RequestPromise]));
3672
3725
  });
3673
3726
  this.requestManager._pending.forEach(v => all.push(v));
3674
3727
  const promise = Promise.allSettled(all);
@@ -3994,7 +4047,7 @@ class Store extends EmberObject {
3994
4047
  };
3995
4048
  if (resource.id) {
3996
4049
  const identifier = this.identifierCache.peekRecordIdentifier(resource);
3997
- assert(`The id ${properties.id} has already been used with another '${normalizedModelName}' record.`, !identifier);
4050
+ assert(`The id ${String(properties.id)} has already been used with another '${normalizedModelName}' record.`, !identifier);
3998
4051
  }
3999
4052
  const identifier = this.identifierCache.createIdentifierForNewRecord(resource);
4000
4053
  const cache = this.cache;
@@ -4476,7 +4529,7 @@ class Store extends EmberObject {
4476
4529
  assertDestroyingStore(this, 'peekRecord');
4477
4530
  }
4478
4531
  assert(`You need to pass a model name to the store's peekRecord method`, identifier);
4479
- assert(`Passing classes to store methods has been removed. Please pass a dasherized string instead of ${identifier}`, typeof identifier === 'string');
4532
+ assert(`Passing classes to store methods has been removed. Please pass a dasherized string instead of ${String(identifier)}`, typeof identifier === 'string');
4480
4533
  const type = normalizeModelName(identifier);
4481
4534
  const normalizedId = ensureStringId(id);
4482
4535
  const resource = {
@@ -4864,7 +4917,7 @@ class Store extends EmberObject {
4864
4917
  if (macroCondition(getOwnConfig().env.DEBUG)) {
4865
4918
  assertDestroyedStoreOnly(this, 'unloadAll');
4866
4919
  }
4867
- assert(`Passing classes to store methods has been removed. Please pass a dasherized string instead of ${modelName}`, !modelName || typeof modelName === 'string');
4920
+ assert(`Passing classes to store methods has been removed. Please pass a dasherized string instead of ${String(modelName)}`, !modelName || typeof modelName === 'string');
4868
4921
  this._join(() => {
4869
4922
  if (modelName === undefined) {
4870
4923
  // destroy the graph before unloadAll
@@ -5032,7 +5085,7 @@ class Store extends EmberObject {
5032
5085
  @method _push
5033
5086
  @private
5034
5087
  @param {Object} jsonApiDoc
5035
- @return {StableRecordIdentifier|Array<StableRecordIdentifier>} identifiers for the primary records that had data loaded
5088
+ @return {StableRecordIdentifier|Array<StableRecordIdentifier>|null} identifiers for the primary records that had data loaded
5036
5089
  */
5037
5090
  _push(jsonApiDoc, asyncFlush) {
5038
5091
  if (macroCondition(getOwnConfig().env.DEBUG)) {
@@ -5058,7 +5111,7 @@ class Store extends EmberObject {
5058
5111
  });
5059
5112
  });
5060
5113
  this._enableAsyncFlush = null;
5061
- return ret.data;
5114
+ return 'data' in ret ? ret.data : null;
5062
5115
  }
5063
5116
 
5064
5117
  /**
@@ -5109,19 +5162,10 @@ class Store extends EmberObject {
5109
5162
  if (macroCondition(getOwnConfig().env.DEBUG)) {
5110
5163
  assertDestroyingStore(this, 'pushPayload');
5111
5164
  }
5112
- let serializer;
5113
- let payload;
5114
- if (!inputPayload) {
5115
- payload = modelName;
5116
- serializer = this.serializerFor('application');
5117
- assert(`You cannot use 'store#pushPayload' without a modelName unless your default serializer defines 'pushPayload'`, typeof serializer.pushPayload === 'function');
5118
- } else {
5119
- payload = inputPayload;
5120
- assert(`Passing classes to store methods has been removed. Please pass a dasherized string instead of ${modelName}`, typeof modelName === 'string');
5121
- let normalizedModelName = normalizeModelName(modelName);
5122
- serializer = this.serializerFor(normalizedModelName);
5123
- }
5124
- assert(`You must define a pushPayload method in your serializer in order to call store.pushPayload`, serializer.pushPayload);
5165
+ const payload = inputPayload || modelName;
5166
+ const normalizedModelName = inputPayload ? normalizeModelName(modelName) : 'application';
5167
+ const serializer = this.serializerFor(normalizedModelName);
5168
+ assert(`You cannot use 'store.pushPayload(<type>, <payload>)' unless the serializer for '${normalizedModelName}' defines 'pushPayload'`, serializer && typeof serializer.pushPayload === 'function');
5125
5169
  serializer.pushPayload(this, payload);
5126
5170
  }
5127
5171
 
@@ -5147,7 +5191,7 @@ class Store extends EmberObject {
5147
5191
  return Promise.reject(`Record Is Disconnected`);
5148
5192
  }
5149
5193
  // TODO we used to check if the record was destroyed here
5150
- assert(`Cannot initiate a save request for an unloaded record: ${identifier}`, this._instanceCache.recordIsLoaded(identifier));
5194
+ assert(`Cannot initiate a save request for an unloaded record: ${identifier.lid}`, this._instanceCache.recordIsLoaded(identifier));
5151
5195
  if (resourceIsFullyDeleted(this._instanceCache, identifier)) {
5152
5196
  return Promise.resolve(record);
5153
5197
  }
@@ -5235,11 +5279,11 @@ class Store extends EmberObject {
5235
5279
  }
5236
5280
  assert(`You need to pass a model name to the store's normalize method`, modelName);
5237
5281
  assert(`Passing classes to store methods has been removed. Please pass a dasherized string instead of ${typeof modelName}`, typeof modelName === 'string');
5238
- let normalizedModelName = normalizeModelName(modelName);
5239
- let serializer = this.serializerFor(normalizedModelName);
5240
- let model = this.modelFor(normalizedModelName);
5241
- assert(`You must define a normalize method in your serializer in order to call store.normalize`, serializer?.normalize);
5242
- return serializer.normalize(model, payload);
5282
+ const normalizedModelName = normalizeModelName(modelName);
5283
+ const serializer = this.serializerFor(normalizedModelName);
5284
+ const schema = this.modelFor(normalizedModelName);
5285
+ assert(`You must define a normalize method in your serializer in order to call store.normalize`, typeof serializer?.normalize === 'function');
5286
+ return serializer.normalize(schema, payload);
5243
5287
  }
5244
5288
 
5245
5289
  /**
@@ -5268,7 +5312,7 @@ class Store extends EmberObject {
5268
5312
  if (adapter) {
5269
5313
  return adapter;
5270
5314
  }
5271
- let owner = getOwner(this);
5315
+ const owner = getOwner(this);
5272
5316
 
5273
5317
  // name specific adapter
5274
5318
  adapter = owner.lookup(`adapter:${normalizedModelName}`);
@@ -5315,9 +5359,9 @@ class Store extends EmberObject {
5315
5359
  if (serializer) {
5316
5360
  return serializer;
5317
5361
  }
5318
- let owner = getOwner(this);
5319
5362
 
5320
5363
  // by name
5364
+ const owner = getOwner(this);
5321
5365
  serializer = owner.lookup(`serializer:${normalizedModelName}`);
5322
5366
  if (serializer !== undefined) {
5323
5367
  _serializerCache[normalizedModelName] = serializer;
@@ -5369,9 +5413,11 @@ class Store extends EmberObject {
5369
5413
  let assertDestroyingStore;
5370
5414
  let assertDestroyedStoreOnly;
5371
5415
  if (macroCondition(getOwnConfig().env.DEBUG)) {
5372
- assertDestroyingStore = function assertDestroyedStore(store, method) {
5416
+ // eslint-disable-next-line @typescript-eslint/no-shadow
5417
+ assertDestroyingStore = function assertDestroyingStore(store, method) {
5373
5418
  assert(`Attempted to call store.${method}(), but the store instance has already been destroyed.`, !(store.isDestroying || store.isDestroyed));
5374
5419
  };
5420
+ // eslint-disable-next-line @typescript-eslint/no-shadow
5375
5421
  assertDestroyedStoreOnly = function assertDestroyedStoreOnly(store, method) {
5376
5422
  assert(`Attempted to call store.${method}(), but the store instance has already been destroyed.`, !store.isDestroyed);
5377
5423
  };