diff --git a/.eslintrc.js b/.eslintrc.js index 2c69bc843f6..b75a77d66b9 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -230,8 +230,6 @@ module.exports = { 'packages/store/src/-private/store-service.ts', 'packages/store/src/-private/utils/coerce-id.ts', 'packages/store/src/-private/index.ts', - 'packages/store/src/-private/caches/identifier-cache.ts', - 'packages/serializer/src/index.ts', '@types/@ember/runloop/index.d.ts', '@types/@ember/polyfills/index.d.ts', 'tests/graph/tests/integration/graph/polymorphism/implicit-keys-test.ts', diff --git a/ember-data-types/q/identifier.ts b/ember-data-types/q/identifier.ts index 94e36aac0ea..b30e19c1278 100644 --- a/ember-data-types/q/identifier.ts +++ b/ember-data-types/q/identifier.ts @@ -191,7 +191,7 @@ export type StableRecordIdentifier = StableExistingRecordIdentifier | StableNewR */ export interface GenerationMethod { (data: ImmutableRequestInfo, bucket: 'document'): string | null; - (data: ResourceData | { type: string }, bucket: 'record'): string; + (data: unknown | { type: string }, bucket: 'record'): string; (data: unknown, bucket: IdentifierBucket): string | null; } diff --git a/packages/debug/addon/index.js b/packages/debug/addon/index.js index 2920fdc4691..ac986a40b01 100644 --- a/packages/debug/addon/index.js +++ b/packages/debug/addon/index.js @@ -103,7 +103,7 @@ export default class extends DataAdapter { }]; const discoveredTypes = typesMapFor(store); - Object.keys(store.identifierCache._cache.types).forEach((type) => { + Object.keys(store.identifierCache._cache.resourcesByType).forEach((type) => { discoveredTypes.set(type, false); }); diff --git a/packages/graph/src/-private/graph/-edge-definition.ts b/packages/graph/src/-private/graph/-edge-definition.ts index e889c96d948..6a86a0f4d0f 100644 --- a/packages/graph/src/-private/graph/-edge-definition.ts +++ b/packages/graph/src/-private/graph/-edge-definition.ts @@ -333,7 +333,7 @@ export function upgradeDefinition( const polymorphicLookup = graph._potentialPolymorphicTypes; const { type } = identifier; - let cached = expandingGet(cache, type, propertyName); + let cached = /*#__NOINLINE__*/ expandingGet(cache, type, propertyName); // CASE: We have a cached resolution (null if no relationship exists) if (cached !== undefined) { @@ -357,7 +357,7 @@ export function upgradeDefinition( for (let i = 0; i < altTypes.length; i++) { const _cached = expandingGet(cache, altTypes[i], propertyName); if (_cached) { - expandingSet(cache, type, propertyName, _cached); + /*#__NOINLINE__*/ expandingSet(cache, type, propertyName, _cached); _cached.rhs_modelNames.push(type); return _cached; } @@ -371,7 +371,7 @@ export function upgradeDefinition( cache[type]![propertyName] = null; return null; } - const definition = upgradeMeta(meta); + const definition = /*#__NOINLINE__*/ upgradeMeta(meta); let inverseDefinition; let inverseKey; @@ -383,7 +383,7 @@ export function upgradeDefinition( assert(`Expected the inverse model to exist`, getStore(storeWrapper).modelFor(inverseType)); inverseDefinition = null; } else { - inverseKey = inverseForRelationship(getStore(storeWrapper), identifier, propertyName); + inverseKey = /*#__NOINLINE__*/ inverseForRelationship(getStore(storeWrapper), identifier, propertyName); // CASE: If we are polymorphic, and we declared an inverse that is non-null // we must assume that the lack of inverseKey means that there is no @@ -423,7 +423,7 @@ export function upgradeDefinition( // CASE: We have no inverse if (!inverseDefinition) { // polish off meta - inverseKey = implicitKeyFor(type, propertyName); + inverseKey = /*#__NOINLINE__*/ implicitKeyFor(type, propertyName); inverseDefinition = { kind: 'implicit', key: inverseKey, diff --git a/packages/graph/src/-private/graph/graph.ts b/packages/graph/src/-private/graph/graph.ts index a7e03db1dcb..56ef92c55f8 100644 --- a/packages/graph/src/-private/graph/graph.ts +++ b/packages/graph/src/-private/graph/graph.ts @@ -113,7 +113,7 @@ export class Graph { let relationship = relationships[propertyName]; if (!relationship) { - const info = upgradeDefinition(this, identifier, propertyName); + const info = /*#__NOINLINE__*/ upgradeDefinition(this, identifier, propertyName); assert(`Could not determine relationship information for ${identifier.type}.${propertyName}`, info !== null); if (info.rhs_definition?.kind === 'implicit') { @@ -122,7 +122,9 @@ export class Graph { // this.registerPolymorphicType(info.rhs_baseModelName, identifier.type); } - const meta = isLHS(info, identifier.type, propertyName) ? info.lhs_definition : info.rhs_definition!; + const meta = /*#__NOINLINE__*/ isLHS(info, identifier.type, propertyName) + ? info.lhs_definition + : info.rhs_definition!; if (meta.kind !== 'implicit') { const Klass = meta.kind === 'hasMany' ? ManyRelationship : BelongsToRelationship; @@ -227,8 +229,8 @@ export class Graph { if (!rel) { return; } - destroyRelationship(this, rel, silenceNotifications); - if (isImplicit(rel)) { + /*#__NOINLINE__*/ destroyRelationship(this, rel, silenceNotifications); + if (/*#__NOINLINE__*/ isImplicit(rel)) { // @ts-expect-error relationships[key] = undefined; } @@ -293,7 +295,7 @@ export class Graph { case 'mergeIdentifiers': { const relationships = this.identifiers.get(op.record); if (relationships) { - mergeIdentifier(this, op, relationships); + /*#__NOINLINE__*/ mergeIdentifier(this, op, relationships); } break; } @@ -304,7 +306,7 @@ export class Graph { // TODO add deprecations/assertion here for duplicates assertValidRelationshipPayload(this, op); } - updateRelationshipOperation(this, op); + /*#__NOINLINE__*/ updateRelationshipOperation(this, op); break; case 'deleteRecord': { assert(`Can only perform the operation deleteRelationship on remote state`, isRemote); @@ -320,23 +322,23 @@ export class Graph { // works together with the has check // @ts-expect-error relationships[key] = undefined; - removeCompletelyFromInverse(this, rel); + /*#__NOINLINE__*/ removeCompletelyFromInverse(this, rel); }); this.identifiers.delete(identifier); } break; } case 'replaceRelatedRecord': - replaceRelatedRecord(this, op, isRemote); + /*#__NOINLINE__*/ replaceRelatedRecord(this, op, isRemote); break; case 'addToRelatedRecords': - addToRelatedRecords(this, op, isRemote); + /*#__NOINLINE__*/ addToRelatedRecords(this, op, isRemote); break; case 'removeFromRelatedRecords': - removeFromRelatedRecords(this, op, isRemote); + /*#__NOINLINE__*/ removeFromRelatedRecords(this, op, isRemote); break; case 'replaceRelatedRecords': - replaceRelatedRecords(this, op, isRemote); + /*#__NOINLINE__*/ replaceRelatedRecords(this, op, isRemote); break; default: assert(`No local relationship update operation exists for '${op.op}'`); @@ -410,7 +412,7 @@ export class Graph { this._willSyncLocal = false; let updated = this._updatedRelationships; this._updatedRelationships = new Set(); - updated.forEach((rel) => syncRemoteToLocal(this, rel)); + updated.forEach((rel) => /*#__NOINLINE__*/ syncRemoteToLocal(this, rel)); } destroy() { @@ -446,7 +448,7 @@ export class Graph { function destroyRelationship(graph: Graph, rel: RelationshipEdge, silenceNotifications?: boolean) { if (isImplicit(rel)) { if (graph.isReleasable(rel.identifier)) { - removeCompletelyFromInverse(graph, rel); + /*#__NOINLINE__*/ removeCompletelyFromInverse(graph, rel); } return; } @@ -455,14 +457,20 @@ function destroyRelationship(graph: Graph, rel: RelationshipEdge, silenceNotific const { inverseKey } = rel.definition; if (!rel.definition.inverseIsImplicit) { - forAllRelatedIdentifiers(rel, (inverseIdentifer: StableRecordIdentifier) => - notifyInverseOfDematerialization(graph, inverseIdentifer, inverseKey, identifier, silenceNotifications) + /*#__NOINLINE__*/ forAllRelatedIdentifiers(rel, (inverseIdentifer: StableRecordIdentifier) => + /*#__NOINLINE__*/ notifyInverseOfDematerialization( + graph, + inverseIdentifer, + inverseKey, + identifier, + silenceNotifications + ) ); } if (!rel.definition.inverseIsImplicit && !rel.definition.inverseIsAsync) { rel.state.isStale = true; - clearRelationship(rel); + /*#__NOINLINE__*/ clearRelationship(rel); // necessary to clear relationships in the ui from dematerialized records // hasMany is managed by Model which calls `retreiveLatest` after @@ -473,7 +481,7 @@ function destroyRelationship(graph: Graph, rel: RelationshipEdge, silenceNotific // leave the ui relationship populated since the record is destroyed and // internally we've fully cleaned up. if (!rel.definition.isAsync && !silenceNotifications) { - notifyChange(graph, rel.identifier, rel.definition.key); + /*#__NOINLINE__*/ notifyChange(graph, rel.identifier, rel.definition.key); } } } @@ -495,7 +503,7 @@ function notifyInverseOfDematerialization( // For remote members, it is possible that inverseRecordData has already been associated to // to another record. For such cases, do not dematerialize the inverseRecordData if (!isBelongsTo(relationship) || !relationship.localState || identifier === relationship.localState) { - removeDematerializedInverse( + /*#__NOINLINE__*/ removeDematerializedInverse( graph, relationship as BelongsToRelationship | ManyRelationship, identifier, @@ -558,7 +566,7 @@ function removeDematerializedInverse( // cache. // if the record being unloaded only exists on the client, we similarly // treat it as a client side delete - removeIdentifierCompletelyFromRelationship(graph, relationship, inverseIdentifier); + /*#__NOINLINE__*/ removeIdentifierCompletelyFromRelationship(graph, relationship, inverseIdentifier); } else { relationship.state.hasDematerializedInverse = true; } diff --git a/packages/json-api/src/-private/cache.ts b/packages/json-api/src/-private/cache.ts index 3e76ac1d262..2460108d364 100644 --- a/packages/json-api/src/-private/cache.ts +++ b/packages/json-api/src/-private/cache.ts @@ -8,6 +8,7 @@ import { LOG_MUTATIONS, LOG_OPERATIONS } from '@ember-data/debugging'; import { DEBUG } from '@ember-data/env'; import { graphFor, peekGraph } from '@ember-data/graph/-private'; import type { LocalRelationshipOperation } from '@ember-data/graph/-private/graph/-operations'; +import type { Graph } from '@ember-data/graph/-private/graph/graph'; import type { ImplicitRelationship } from '@ember-data/graph/-private/graph/index'; import type BelongsToRelationship from '@ember-data/graph/-private/relationships/state/belongs-to'; import type ManyRelationship from '@ember-data/graph/-private/relationships/state/has-many'; @@ -118,11 +119,13 @@ export default class JSONAPICache implements Cache { declare __cache: Map; declare __destroyedCache: Map; declare __documents: Map>; + declare __graph: Graph; constructor(storeWrapper: CacheCapabilitiesManager) { this.version = '2'; this.__storeWrapper = storeWrapper; this.__cache = new Map(); + this.__graph = graphFor(storeWrapper); this.__destroyedCache = new Map(); this.__documents = new Map(); } @@ -285,7 +288,7 @@ export default class JSONAPICache implements Cache { this.__cache.set(op.value, cache); this.__cache.delete(op.record); } - graphFor(this.__storeWrapper).update(op, true); + this.__graph.update(op, true); } } @@ -308,7 +311,7 @@ export default class JSONAPICache implements Cache { console.log(`EmberData | Mutation - update ${mutation.op}`, mutation); } } - graphFor(this.__storeWrapper).update(mutation, false); + this.__graph.update(mutation, false); } /** @@ -357,8 +360,7 @@ export default class JSONAPICache implements Cache { const attributes = Object.assign({}, peeked.remoteAttrs, peeked.inflightAttrs, peeked.localAttrs); const relationships = {}; - const graph = graphFor(this.__storeWrapper); - const rels = graph.identifiers.get(identifier); + const rels = this.__graph.identifiers.get(identifier); if (rels) { Object.keys(rels).forEach((key) => { const rel = rels[key]!; @@ -418,8 +420,8 @@ export default class JSONAPICache implements Cache { const existed = !!peeked; const cached = peeked || this._createCache(identifier); - const isLoading = _isLoading(peeked, this.__storeWrapper, identifier) || !recordIsLoaded(peeked); - let isUpdate = !_isEmpty(peeked) && !isLoading; + const isLoading = /*#__NOINLINE__*/ _isLoading(peeked, this.__storeWrapper, identifier) || !recordIsLoaded(peeked); + let isUpdate = /*#__NOINLINE__*/ !_isEmpty(peeked) && !isLoading; if (LOG_OPERATIONS) { try { @@ -458,7 +460,7 @@ export default class JSONAPICache implements Cache { } if (data.relationships) { - setupRelationships(this.__storeWrapper, identifier, data); + setupRelationships(this.__graph, this.__storeWrapper, identifier, data); } if (changedKeys && changedKeys.length) { @@ -612,7 +614,7 @@ export default class JSONAPICache implements Cache { const storeWrapper = this.__storeWrapper; let attributeDefs = storeWrapper.getSchemaDefinitionService().attributesDefinitionFor(identifier); let relationshipDefs = storeWrapper.getSchemaDefinitionService().relationshipsDefinitionFor(identifier); - const graph = graphFor(storeWrapper); + const graph = this.__graph; let propertyNames = Object.keys(options); for (let i = 0; i < propertyNames.length; i++) { @@ -713,7 +715,7 @@ export default class JSONAPICache implements Cache { const cached = this.__peek(identifier, false); if (cached.isDeleted) { - graphFor(this.__storeWrapper).push({ + this.__graph.push({ op: 'deleteRecord', record: identifier, isNew: false, @@ -750,7 +752,7 @@ export default class JSONAPICache implements Cache { ); if (data.relationships) { - setupRelationships(this.__storeWrapper, identifier, data); + setupRelationships(this.__graph, this.__storeWrapper, identifier, data); } newCanonicalAttributes = data.attributes; } @@ -995,7 +997,7 @@ export default class JSONAPICache implements Cache { } if (cached.isNew) { - graphFor(this.__storeWrapper).push({ + this.__graph.push({ op: 'deleteRecord', record: identifier, isNew: true, @@ -1033,7 +1035,7 @@ export default class JSONAPICache implements Cache { identifier: StableRecordIdentifier, field: string ): SingleResourceRelationship | CollectionResourceRelationship { - return (graphFor(this.__storeWrapper).get(identifier, field) as BelongsToRelationship | ManyRelationship).getData(); + return (this.__graph.get(identifier, field) as BelongsToRelationship | ManyRelationship).getData(); } // Resource State @@ -1055,7 +1057,7 @@ export default class JSONAPICache implements Cache { cached.isDeleted = isDeleted; if (cached.isNew) { // TODO can we delete this since we will do this in unload? - graphFor(this.__storeWrapper).push({ + this.__graph.push({ op: 'deleteRecord', record: identifier, isNew: true, @@ -1329,6 +1331,7 @@ function _isLoading( } function setupRelationships( + graph: Graph, storeWrapper: CacheCapabilitiesManager, identifier: StableRecordIdentifier, data: JsonApiResource @@ -1347,7 +1350,7 @@ function setupRelationships( continue; } - graphFor(storeWrapper).push({ + graph.push({ op: 'updateRelationship', record: identifier, field: relationshipName, diff --git a/packages/model/src/-private/legacy-relationships-support.ts b/packages/model/src/-private/legacy-relationships-support.ts index 758ecb5af73..4942a267b75 100644 --- a/packages/model/src/-private/legacy-relationships-support.ts +++ b/packages/model/src/-private/legacy-relationships-support.ts @@ -5,6 +5,7 @@ import { importSync } from '@embroider/macros'; import { DEBUG } from '@ember-data/env'; import type { UpgradedMeta } from '@ember-data/graph/-private/graph/-edge-definition'; import type { LocalRelationshipOperation } from '@ember-data/graph/-private/graph/-operations'; +import type { Graph } from '@ember-data/graph/-private/graph/graph'; import type { ImplicitRelationship } from '@ember-data/graph/-private/graph/index'; import type BelongsToRelationship from '@ember-data/graph/-private/relationships/state/belongs-to'; import type ManyRelationship from '@ember-data/graph/-private/relationships/state/has-many'; @@ -39,6 +40,7 @@ type PromiseBelongsToFactory = { create(args: BelongsToProxyCreateArgs): Promise export class LegacySupport { declare record: Model; declare store: Store; + declare graph: Graph; declare cache: Cache; declare references: Record; declare identifier: StableRecordIdentifier; @@ -56,6 +58,13 @@ export class LegacySupport { this.identifier = recordIdentifierFor(record); this.cache = peekCache(record); + if (HAS_JSON_API_PACKAGE) { + const graphFor = (importSync('@ember-data/graph/-private') as typeof import('@ember-data/graph/-private')) + .graphFor; + + this.graph = graphFor(this.store); + } + this._manyArrayCache = Object.create(null) as Record; this._relationshipPromisesCache = Object.create(null) as Record< string, @@ -113,8 +122,7 @@ export class LegacySupport { return loadingPromise; } - const graphFor = (importSync('@ember-data/graph/-private') as typeof import('@ember-data/graph/-private')).graphFor; - const relationship = graphFor(this.store).get(this.identifier, key); + const relationship = this.graph.get(this.identifier, key); assert(`Expected ${key} to be a belongs-to relationship`, isBelongsTo(relationship)); let resource = this.cache.getRelationship(this.identifier, key) as SingleResourceRelationship; @@ -135,8 +143,7 @@ export class LegacySupport { assert(`Expected a stable identifier`, !relatedIdentifier || isStableIdentifier(relatedIdentifier)); const store = this.store; - const graphFor = (importSync('@ember-data/graph/-private') as typeof import('@ember-data/graph/-private')).graphFor; - const relationship = graphFor(store).get(this.identifier, key); + const relationship = this.graph.get(this.identifier, key); assert(`Expected ${key} to be a belongs-to relationship`, isBelongsTo(relationship)); let isAsync = relationship.definition.isAsync; @@ -213,9 +220,7 @@ export class LegacySupport { if (HAS_JSON_API_PACKAGE) { let manyArray: RelatedCollection | undefined = this._manyArrayCache[key]; if (!definition) { - const graphFor = (importSync('@ember-data/graph/-private') as typeof import('@ember-data/graph/-private')) - .graphFor; - definition = graphFor(this.store).get(this.identifier, key).definition; + definition = this.graph.get(this.identifier, key).definition; } if (!manyArray) { @@ -281,9 +286,7 @@ export class LegacySupport { if (loadingPromise) { return loadingPromise; } - const graphFor = (importSync('@ember-data/graph/-private') as typeof import('@ember-data/graph/-private')) - .graphFor; - const relationship = graphFor(this.store).get(this.identifier, key) as ManyRelationship; + const relationship = this.graph.get(this.identifier, key) as ManyRelationship; const { definition, state } = relationship; state.hasFailedLoadAttempt = false; @@ -302,9 +305,7 @@ export class LegacySupport { getHasMany(key: string, options?: FindOptions): PromiseManyArray | RelatedCollection { if (HAS_JSON_API_PACKAGE) { - const graphFor = (importSync('@ember-data/graph/-private') as typeof import('@ember-data/graph/-private')) - .graphFor; - const relationship = graphFor(this.store).get(this.identifier, key) as ManyRelationship; + const relationship = this.graph.get(this.identifier, key) as ManyRelationship; const { definition, state } = relationship; let manyArray = this.getManyArray(key, definition); @@ -379,14 +380,12 @@ export class LegacySupport { // because of the intimate API access involved. This is something we will need to redesign. assert(`snapshot.belongsTo only supported for @ember-data/json-api`); } - const graphFor = (importSync('@ember-data/graph/-private') as typeof import('@ember-data/graph/-private')) - .graphFor; - const graph = graphFor(this.store); - const relationship = graph.get(this.identifier, name); + const { graph, identifier } = this; + const relationship = graph.get(identifier, name); if (DEBUG) { if (kind) { - let modelName = this.identifier.type; + let modelName = identifier.type; let actualRelationshipKind = relationship.definition.kind; assert( `You tried to get the '${name}' relationship on a '${modelName}' via record.${kind}('${name}'), but the relationship is of kind '${actualRelationshipKind}'. Use record.${actualRelationshipKind}('${name}') instead.`, @@ -398,15 +397,9 @@ export class LegacySupport { let relationshipKind = relationship.definition.kind; if (relationshipKind === 'belongsTo') { - reference = new BelongsToReference( - this.store, - graph, - this.identifier, - relationship as BelongsToRelationship, - name - ); + reference = new BelongsToReference(this.store, graph, identifier, relationship as BelongsToRelationship, name); } else if (relationshipKind === 'hasMany') { - reference = new HasManyReference(this.store, graph, this.identifier, relationship as ManyRelationship, name); + reference = new HasManyReference(this.store, graph, identifier, relationship as ManyRelationship, name); } this.references[name] = reference; diff --git a/packages/serializer/src/index.ts b/packages/serializer/src/index.ts index e43812a97d2..6c9ca7a6098 100644 --- a/packages/serializer/src/index.ts +++ b/packages/serializer/src/index.ts @@ -111,6 +111,7 @@ import EmberObject from '@ember/object'; import { inject as service } from '@ember/service'; import type Store from '@ember-data/store'; +import { ModelSchema } from '@ember-data/types/q/ds-model'; /** > ⚠️ CAUTION you likely want the docs for [ Serializer](/ember-data/release/classes/%3CInterface%3E%20Serializer) @@ -264,7 +265,7 @@ export default class extends EmberObject { @param {Object} hash @return {Object} */ - normalize(typeClass, hash) { + normalize(_typeClass: ModelSchema, hash: Record): Record { return hash; } } diff --git a/packages/store/src/-private/caches/identifier-cache.ts b/packages/store/src/-private/caches/identifier-cache.ts index 83946c5df75..5346fe52c89 100644 --- a/packages/store/src/-private/caches/identifier-cache.ts +++ b/packages/store/src/-private/caches/identifier-cache.ts @@ -18,16 +18,16 @@ import type { RecordIdentifier, ResetMethod, ResourceData, - StableExistingRecordIdentifier, + StableIdentifier, StableRecordIdentifier, UpdateMethod, } from '@ember-data/types/q/identifier'; -import coerceId, { ensureStringId } from '../utils/coerce-id'; +import coerceId from '../utils/coerce-id'; import { DEBUG_CLIENT_ORIGINATED, DEBUG_IDENTIFIER_BUCKET } from '../utils/identifier-debug-consts'; -import isNonEmptyString from '../utils/is-non-empty-string'; import normalizeModelName from '../utils/normalize-model-name'; import installPolyfill from '../utils/uuid-polyfill'; +import { hasId, hasLid, hasType } from './resource-utils'; const IDENTIFIERS = new Set(); const DOCUMENTS = new Set(); @@ -50,7 +50,7 @@ if (macroCondition(getOwnConfig<{ polyfillUUID: boolean }>().polyfillUUID)) { function uuidv4(): string { 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 + typeof _crypto.randomUUID === 'function' ); return _crypto.randomUUID(); } @@ -67,7 +67,22 @@ interface KeyOptions { id: IdentifierMap; } type TypeMap = { [key: string]: KeyOptions }; + +// type IdentifierTypeLookup = { all: Set; id: Map }; +// type IdentifiersByType = Map; type IdentifierMap = Map; +type KeyInfo = { + id: string | null; + type: string; +}; +type StableCache = { + resources: IdentifierMap; + documents: Map; + resourcesByType: TypeMap; +}; + +export type KeyInfoMethod = (resource: unknown, known: StableRecordIdentifier | null) => KeyInfo; + export type MergeMethod = ( targetIdentifier: StableRecordIdentifier, matchedIdentifier: StableRecordIdentifier, @@ -78,6 +93,7 @@ let configuredForgetMethod: ForgetMethod | null; let configuredGenerationMethod: GenerationMethod | null; let configuredResetMethod: ResetMethod | null; let configuredUpdateMethod: UpdateMethod | null; +let configuredKeyInfoMethod: KeyInfoMethod | null; export function setIdentifierGenerationMethod(method: GenerationMethod | null): void { configuredGenerationMethod = method; @@ -95,13 +111,52 @@ export function setIdentifierResetMethod(method: ResetMethod | null): void { configuredResetMethod = method; } -type WithLid = { lid: string }; -type WithId = { id: string | null; type: string }; +export function setKeyInfoForResource(method: KeyInfoMethod | null): void { + configuredKeyInfoMethod = method; +} function assertIsRequest(request: unknown): asserts request is ImmutableRequestInfo { return; } +// Map> +type TypeIdMap = Map>; +const NEW_IDENTIFIERS: TypeIdMap = new Map(); + +function updateTypeIdMapping(typeMap: TypeIdMap, identifier: StableRecordIdentifier, id: string): void { + let idMap = typeMap.get(identifier.type); + if (!idMap) { + idMap = new Map(); + typeMap.set(identifier.type, idMap); + } + idMap.set(id, identifier.lid); +} + +function defaultUpdateMethod(identifier: StableRecordIdentifier, data: unknown, bucket: 'record'): void; +function defaultUpdateMethod(identifier: StableIdentifier, newData: unknown, bucket: never): void; +function defaultUpdateMethod( + identifier: StableIdentifier | StableRecordIdentifier, + data: unknown, + bucket: 'record' +): void { + if (bucket === 'record') { + assert(`Expected identifier to be a StableRecordIdentifier`, isStableIdentifier(identifier)); + if (!identifier.id && hasId(data)) { + updateTypeIdMapping(NEW_IDENTIFIERS, identifier, data.id); + } + } +} + +function defaultKeyInfoMethod(resource: unknown, known: StableRecordIdentifier | null): KeyInfo { + // TODO RFC something to make this configurable + const id = hasId(resource) ? coerceId(resource.id) : null; + const type = hasType(resource) ? normalizeModelName(resource.type) : known ? known.type : null; + + assert(`Expected keyInfoForResource to provide a type for the resource`, type); + + return { type, id }; +} + function defaultGenerationMethod(data: ImmutableRequestInfo, bucket: 'document'): string | null; function defaultGenerationMethod(data: ResourceData | { type: string }, bucket: 'record'): string; function defaultGenerationMethod( @@ -109,16 +164,19 @@ function defaultGenerationMethod( bucket: IdentifierBucket ): string | null { if (bucket === 'record') { - if (isNonEmptyString((data as WithLid).lid)) { - return (data as WithLid).lid; + if (hasLid(data)) { + return data.lid; } - if ((data as WithId).id !== undefined) { - let { type, id } = data as WithId; - // TODO: add test for id not a string - if (isNonEmptyString(coerceId(id))) { - return `@lid:${normalizeModelName(type)}-${id}`; - } + + assert(`Cannot generate an identifier for a resource without a type`, hasType(data)); + + if (hasId(data)) { + const type = normalizeModelName(data.type); + const lid = NEW_IDENTIFIERS.get(type)?.get(data.id); + + return lid || `@lid:${type}-${data.id}`; } + return uuidv4(); } else if (bucket === 'document') { assertIsRequest(data); @@ -130,12 +188,19 @@ function defaultGenerationMethod( } return null; } - assert(`Unknown bucket ${bucket}`, false); + assert(`Unknown bucket ${bucket as string}`, false); } -function defaultEmptyCallback(...args: any[]): any {} +function defaultEmptyCallback(...args: unknown[]): void {} +function defaultMergeMethod( + a: StableRecordIdentifier, + _b: StableRecordIdentifier, + _c: unknown +): StableRecordIdentifier { + return a; +} -let DEBUG_MAP; +let DEBUG_MAP: WeakMap; if (DEBUG) { DEBUG_MAP = new WeakMap(); } @@ -154,27 +219,31 @@ if (DEBUG) { @public */ export class IdentifierCache { - _cache = { - lids: new Map(), - types: Object.create(null) as TypeMap, - documents: new Map(), - }; + declare _cache: StableCache; declare _generate: GenerationMethod; declare _update: UpdateMethod; declare _forget: ForgetMethod; declare _reset: ResetMethod; declare _merge: MergeMethod; + declare _keyInfoForResource: KeyInfoMethod; declare _isDefaultConfig: boolean; constructor() { // we cache the user configuredGenerationMethod at init because it must // be configured prior and is not allowed to be changed this._generate = configuredGenerationMethod || (defaultGenerationMethod as GenerationMethod); - this._update = configuredUpdateMethod || defaultEmptyCallback; + this._update = configuredUpdateMethod || defaultUpdateMethod; this._forget = configuredForgetMethod || defaultEmptyCallback; this._reset = configuredResetMethod || defaultEmptyCallback; - this._merge = defaultEmptyCallback; + this._merge = defaultMergeMethod; + this._keyInfoForResource = configuredKeyInfoMethod || defaultKeyInfoMethod; this._isDefaultConfig = !configuredGenerationMethod; + + this._cache = { + resources: new Map(), + resourcesByType: Object.create(null) as TypeMap, + documents: new Map(), + }; } /** @@ -187,153 +256,72 @@ export class IdentifierCache { * @private */ __configureMerge(method: MergeMethod | null) { - this._merge = method || defaultEmptyCallback; + this._merge = method || defaultMergeMethod; } /** * @method _getRecordIdentifier * @private */ - _getRecordIdentifier(resource: ResourceIdentifierObject, shouldGenerate: true): StableRecordIdentifier; - _getRecordIdentifier(resource: ResourceIdentifierObject, shouldGenerate: false): StableRecordIdentifier | undefined; - _getRecordIdentifier( - resource: ResourceIdentifierObject, - shouldGenerate: boolean = false - ): StableRecordIdentifier | undefined { + _getRecordIdentifier(resource: unknown, shouldGenerate: true): StableRecordIdentifier; + _getRecordIdentifier(resource: unknown, shouldGenerate: false): StableRecordIdentifier | undefined; + _getRecordIdentifier(resource: unknown, shouldGenerate: boolean): StableRecordIdentifier | undefined { + if (LOG_IDENTIFIERS) { + // eslint-disable-next-line no-console + console.groupCollapsed(`Identifiers: ${shouldGenerate ? 'Generating' : 'Peeking'} Identifier`, resource); + } // short circuit if we're already the stable version if (isStableIdentifier(resource)) { if (DEBUG) { // TODO should we instead just treat this case as a new generation skipping the short circuit? - if (!this._cache.lids.has(resource.lid) || this._cache.lids.get(resource.lid) !== resource) { - throw new Error(`The supplied identifier ${resource} does not belong to this store instance`); + if (!this._cache.resources.has(resource.lid) || this._cache.resources.get(resource.lid) !== resource) { + throw new Error(`The supplied identifier ${JSON.stringify(resource)} does not belong to this store instance`); } } if (LOG_IDENTIFIERS) { // eslint-disable-next-line no-console - console.log(`Identifiers: Peeked Identifier was already Stable ${String(resource)}`); + console.log(`Identifiers: cache HIT - Stable ${resource.lid}`); + // eslint-disable-next-line no-console + console.groupEnd(); } return resource; } - let lid = resource.lid || null; - let identifier: StableRecordIdentifier | undefined = lid !== null ? this._cache.lids.get(lid) : undefined; + // the resource is unknown, ask the application to identify this data for us + const lid = this._generate(resource, 'record'); + if (LOG_IDENTIFIERS) { + // eslint-disable-next-line no-console + console.log(`Identifiers: ${lid ? 'no ' : ''}lid ${lid ? lid + ' ' : ''}determined for resource`, resource); + } + let identifier: StableRecordIdentifier | undefined = /*#__NOINLINE__*/ getIdentifierFromLid( + this._cache, + lid, + resource + ); if (identifier !== undefined) { if (LOG_IDENTIFIERS) { // eslint-disable-next-line no-console - console.log(`Identifiers: cache HIT ${identifier}`, resource); + console.groupEnd(); } return identifier; } - if (LOG_IDENTIFIERS) { - // eslint-disable-next-line no-console - console.groupCollapsed(`Identifiers: ${shouldGenerate ? 'Generating' : 'Peeking'} Identifier`, resource); - } - if (shouldGenerate === false) { - if (!(resource as ExistingResourceObject).type || !(resource as ExistingResourceObject).id) { - return; - } - } - - // `type` must always be present - assert('resource.type needs to be a string', 'type' in resource && isNonEmptyString(resource.type)); - - let type = resource.type && normalizeModelName(resource.type); - let id = 'id' in resource ? coerceId(resource.id) : null; - - let keyOptions = getTypeIndex(this._cache.types, type); - - // go straight for the stable RecordIdentifier key'd to `lid` - if (lid !== null) { - identifier = keyOptions.lid.get(lid); - } - - // we may have not seen this resource before - // but just in case we check our own secondary lookup (`id`) - if (identifier === undefined && id !== null) { - identifier = keyOptions.id.get(id); - } - - if (identifier === undefined) { - // we have definitely not seen this resource before - // so we allow the user configured `GenerationMethod` to tell us - let newLid = this._generate(resource, 'record'); if (LOG_IDENTIFIERS) { // eslint-disable-next-line no-console - console.log(`Identifiers: lid ${newLid} determined for resource`, resource); - } - - // we do this _even_ when `lid` is present because secondary lookups - // may need to be populated, but we enforce not giving us something - // different than expected - if (lid !== null && newLid !== lid) { - throw new Error(`You should not change the of a RecordIdentifier`); - } else if (lid === null && !this._isDefaultConfig) { - // allow configuration to tell us that we have - // seen this `lid` before. E.g. a secondary lookup - // connects this resource to a previously seen - // resource. - identifier = keyOptions.lid.get(newLid); + console.groupEnd(); } + return; + } - if (shouldGenerate === true) { - if (identifier === undefined) { - // if we still don't have an identifier, time to generate one - identifier = makeStableRecordIdentifier(id, type, newLid, 'record', false); - - // populate our unique table - if (DEBUG) { - // realistically if you hit this it means you changed `type` :/ - // TODO consider how to handle type change assertions more gracefully - if (this._cache.lids.has(identifier.lid)) { - throw new Error(`You should not change the of a RecordIdentifier`); - } - } - this._cache.lids.set(identifier.lid, identifier); - - // populate our primary lookup table - // TODO consider having the `lid` cache be - // one level up - keyOptions.lid.set(identifier.lid, identifier); - - if (LOG_IDENTIFIERS) { - if (shouldGenerate) { - // eslint-disable-next-line no-console - console.log(`Identifiers: generated ${String(identifier)} for`, resource); - if (resource[DEBUG_IDENTIFIER_BUCKET]) { - // eslint-disable-next-line no-console - console.trace( - `[WARNING] Identifiers: generated a new identifier from a previously used identifier. This is likely a bug.` - ); - } - } - } - } + // if we still don't have an identifier, time to generate one + const keyInfo = this._keyInfoForResource(resource, null); + identifier = /*#__NOINLINE__*/ makeStableRecordIdentifier(keyInfo.id, keyInfo.type, lid, 'record', false); - // populate our own secondary lookup table - // even for the "successful" secondary lookup - // by `_generate()`, since we missed the cache - // previously - // we use identifier.id instead of id here - // because they may not match and we prefer - // what we've set via resource data - if (identifier.id !== null) { - keyOptions.id.set(identifier.id, identifier); - - // TODO allow filling out of `id` here - // for the `username` non-client created - // case. - } - } - } + addResourceToCache(this._cache, identifier); if (LOG_IDENTIFIERS) { - if (!identifier && !shouldGenerate) { - // eslint-disable-next-line no-console - console.log(`Identifiers: cache MISS`, resource); - } // eslint-disable-next-line no-console console.groupEnd(); } @@ -404,11 +392,7 @@ export class IdentifierCache { @returns {StableRecordIdentifier} @public */ - getOrCreateRecordIdentifier(resource: ExistingResourceObject): StableExistingRecordIdentifier; - getOrCreateRecordIdentifier( - resource: ResourceIdentifierObject | Identifier | StableRecordIdentifier - ): StableRecordIdentifier; - getOrCreateRecordIdentifier(resource: ResourceData | Identifier): StableRecordIdentifier { + getOrCreateRecordIdentifier(resource: unknown): StableRecordIdentifier { return this._getRecordIdentifier(resource, true); } @@ -427,22 +411,16 @@ export class IdentifierCache { */ createIdentifierForNewRecord(data: { type: string; id?: string | null }): StableRecordIdentifier { let newLid = this._generate(data, 'record'); - let identifier = makeStableRecordIdentifier(data.id || null, data.type, newLid, 'record', true); - let keyOptions = getTypeIndex(this._cache.types, data.type); + let identifier = /*#__NOINLINE__*/ makeStableRecordIdentifier(data.id || null, data.type, newLid, 'record', true); // populate our unique table if (DEBUG) { - if (this._cache.lids.has(identifier.lid)) { + if (this._cache.resources.has(identifier.lid)) { throw new Error(`The lid generated for the new record is not unique as it matches an existing identifier`); } } - this._cache.lids.set(identifier.lid, identifier); - // populate the type+lid cache - keyOptions.lid.set(newLid, identifier); - if (data.id) { - keyOptions.id.set(data.id, identifier); - } + /*#__NOINLINE__*/ addResourceToCache(this._cache, identifier); if (LOG_IDENTIFIERS) { // eslint-disable-next-line no-console @@ -477,35 +455,31 @@ export class IdentifierCache { updateRecordIdentifier(identifierObject: RecordIdentifier, data: unknown): StableRecordIdentifier { let identifier = this.getOrCreateRecordIdentifier(identifierObject); - let newId = - (data as ExistingResourceObject).id !== undefined ? coerceId((data as ExistingResourceObject).id) : null; - let existingIdentifier = detectMerge(this._cache.types, identifier, data, newId, this._cache.lids); + const keyInfo = this._keyInfoForResource(data, identifier); + let existingIdentifier = /*#__NOINLINE__*/ detectMerge(this._cache, keyInfo, identifier, data); + const hadLid = hasLid(data); if (!existingIdentifier) { // If the incoming type does not match the identifier type, we need to create an identifier for the incoming // data so we can merge the incoming data with the existing identifier, see #7325 and #7363 - if ( - (data as ExistingResourceObject).type && - identifier.type !== normalizeModelName((data as ExistingResourceObject).type) - ) { - // @ts-expect-error TODO this needs to be fixed - let incomingDataResource = { ...data }; - // Need to strip the lid from the incomingData in order force a new identifier creation - delete incomingDataResource.lid; - existingIdentifier = this.getOrCreateRecordIdentifier(incomingDataResource); + if (identifier.type !== keyInfo.type) { + if (hadLid) { + // Strip the lid to ensure we force a new identifier creation + delete (data as { lid?: string }).lid; + } + existingIdentifier = this.getOrCreateRecordIdentifier(data); } } if (existingIdentifier) { - let keyOptions = getTypeIndex(this._cache.types, identifier.type); let generatedIdentifier = identifier; - identifier = this._mergeRecordIdentifiers( - keyOptions, - generatedIdentifier, - existingIdentifier, - data, - newId as string - ); + identifier = this._mergeRecordIdentifiers(keyInfo, generatedIdentifier, existingIdentifier, data); + + // make sure that the `lid` on the data we are processing matches the lid we kept + if (hadLid) { + data.lid = identifier.lid; + } + if (LOG_IDENTIFIERS) { // eslint-disable-next-line no-console console.log( @@ -516,23 +490,27 @@ export class IdentifierCache { } let id = identifier.id; - performRecordIdentifierUpdate(identifier, data, this._update); - newId = identifier.id; + /*#__NOINLINE__*/ performRecordIdentifierUpdate(identifier, keyInfo, data, this._update); + const newId = identifier.id; // add to our own secondary lookup table if (id !== newId && newId !== null) { if (LOG_IDENTIFIERS) { // eslint-disable-next-line no-console console.log( - `Identifiers: updated id for identifier ${identifier.lid} from '${id}' to '${newId}' for resource`, + `Identifiers: updated id for identifier ${identifier.lid} from '${String(id)}' to '${String( + newId + )}' for resource`, data ); } - let keyOptions = getTypeIndex(this._cache.types, identifier.type); - keyOptions.id.set(newId, identifier); + + const typeSet = this._cache.resourcesByType[identifier.type]; + assert(`Expected to find a typeSet for ${identifier.type}`, typeSet); + typeSet.id.set(newId, identifier); if (id !== null) { - keyOptions.id.delete(id); + typeSet.id.delete(id); } } else if (LOG_IDENTIFIERS) { // eslint-disable-next-line no-console @@ -547,28 +525,25 @@ export class IdentifierCache { * @private */ _mergeRecordIdentifiers( - keyOptions: KeyOptions, + keyInfo: KeyInfo, identifier: StableRecordIdentifier, existingIdentifier: StableRecordIdentifier, - data: unknown, - newId: string + data: unknown ): StableRecordIdentifier { + assert(`Expected keyInfo to contain an id`, hasId(keyInfo)); // delegate determining which identifier to keep to the configured MergeMethod - let kept = this._merge(identifier, existingIdentifier, data); - let abandoned = kept === identifier ? existingIdentifier : identifier; + const kept = this._merge(identifier, existingIdentifier, data); + const abandoned = kept === identifier ? existingIdentifier : identifier; // cleanup the identifier we no longer need this.forgetRecordIdentifier(abandoned); // ensure a secondary cache entry for this id for the identifier we do keep - keyOptions.id.set(newId, kept); - // ensure a secondary cache entry for this id for the abandoned identifier's type we do keep - let baseKeyOptions = getTypeIndex(this._cache.types, existingIdentifier.type); - baseKeyOptions.id.set(newId, kept); + // keyOptions.id.set(newId, kept); - // make sure that the `lid` on the data we are processing matches the lid we kept - // @ts-expect-error TODO this needs to be fixed - data.lid = kept.lid; + // ensure a secondary cache entry for this id for the abandoned identifier's type we do keep + // let baseKeyOptions = getTypeIndex(this._cache.resourcesByType, existingIdentifier.type); + // baseKeyOptions.id.set(newId, kept); return kept; } @@ -586,15 +561,17 @@ export class IdentifierCache { @public */ forgetRecordIdentifier(identifierObject: RecordIdentifier): void { - let identifier = this.getOrCreateRecordIdentifier(identifierObject); - let keyOptions = getTypeIndex(this._cache.types, identifier.type); + const identifier = this.getOrCreateRecordIdentifier(identifierObject); + const typeSet = this._cache.resourcesByType[identifier.type]; + assert(`Expected to find a typeSet for ${identifier.type}`, typeSet); + if (identifier.id !== null) { - keyOptions.id.delete(identifier.id); + typeSet.id.delete(identifier.id); } - this._cache.lids.delete(identifier.lid); - keyOptions.lid.delete(identifier.lid); + this._cache.resources.delete(identifier.lid); + typeSet.lid.delete(identifier.lid); - IDENTIFIERS.delete(identifierObject); + IDENTIFIERS.delete(identifier); this._forget(identifier, 'record'); if (LOG_IDENTIFIERS) { // eslint-disable-next-line no-console @@ -603,6 +580,7 @@ export class IdentifierCache { } destroy() { + NEW_IDENTIFIERS.clear(); this._cache.documents.forEach((identifier) => { DOCUMENTS.delete(identifier); }); @@ -610,31 +588,17 @@ export class IdentifierCache { } } -function getTypeIndex(typeMap: TypeMap, type: string): KeyOptions { - let typeIndex: KeyOptions = typeMap[type]; - - if (typeIndex === undefined) { - typeIndex = { - lid: new Map(), - id: new Map(), - }; - typeMap[type] = typeIndex; - } - - return typeIndex; -} - function makeStableRecordIdentifier( - id: string | null, - type: string, - lid: string, + _id: string | null, + _type: string, + _lid: string, bucket: IdentifierBucket, - clientOriginated: boolean = false + clientOriginated: boolean ): Readonly { let recordIdentifier = { - lid, - id, - type, + lid: _lid, + id: _id, + type: _type, }; IDENTIFIERS.add(recordIdentifier); @@ -652,13 +616,11 @@ function makeStableRecordIdentifier( return recordIdentifier.type; }, toString() { - // eslint-disable-next-line @typescript-eslint/no-shadow - let { type, id, lid } = recordIdentifier; - return `${clientOriginated ? '[CLIENT_ORIGINATED] ' : ''}${type}:${id} (${lid})`; + const { type, id, lid } = recordIdentifier; + return `${clientOriginated ? '[CLIENT_ORIGINATED] ' : ''}${String(type)}:${String(id)} (${lid})`; }, toJSON() { - // eslint-disable-next-line @typescript-eslint/no-shadow - let { type, id, lid } = recordIdentifier; + const { type, id, lid } = recordIdentifier; return { type, id, lid }; }, }; @@ -673,47 +635,42 @@ function makeStableRecordIdentifier( return recordIdentifier; } -function performRecordIdentifierUpdate(identifier: StableRecordIdentifier, data: unknown, updateFn: UpdateMethod) { +function performRecordIdentifierUpdate( + identifier: StableRecordIdentifier, + keyInfo: KeyInfo, + data: unknown, + updateFn: UpdateMethod +) { if (DEBUG) { - // @ts-expect-error TODO this needs to be fixed - let { lid } = data; - // @ts-expect-error TODO this needs to be fixed - let id = 'id' in data ? data.id : undefined; - // @ts-expect-error TODO this needs to be fixed - let type = 'type' in data && data.type && normalizeModelName(data.type); + const { id, type } = keyInfo; // get the mutable instance behind our proxy wrapper let wrapper = identifier; - identifier = DEBUG_MAP.get(wrapper); + identifier = DEBUG_MAP.get(wrapper)!; - if (lid !== undefined) { - let newLid = ensureStringId(lid); - if (newLid !== identifier.lid) { + if (hasLid(data)) { + const lid = data.lid; + if (lid !== identifier.lid) { throw new Error( - `The 'lid' for a RecordIdentifier cannot be updated once it has been created. Attempted to set lid for '${wrapper}' to '${lid}'.` + `The 'lid' for a RecordIdentifier cannot be updated once it has been created. Attempted to set lid for '${wrapper.lid}' to '${lid}'.` ); } } - if (id !== undefined) { - // @ts-expect-error TODO this needs to be fixed - let newId = coerceId(id); - - if (identifier.id !== null && identifier.id !== newId) { - // here we warn and ignore, as this may be a mistake, but we allow the user - // to have multiple cache-keys pointing at a single lid so we cannot error - warn( - `The 'id' for a RecordIdentifier should not be updated once it has been set. Attempted to set id for '${wrapper}' to '${newId}'.`, - false, - { id: 'ember-data:multiple-ids-for-identifier' } - ); - } + if (id && identifier.id !== null && identifier.id !== id) { + // here we warn and ignore, as this may be a mistake, but we allow the user + // to have multiple cache-keys pointing at a single lid so we cannot error + 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, + { id: 'ember-data:multiple-ids-for-identifier' } + ); } // TODO consider just ignoring here to allow flexible polymorphic support if (type && type !== identifier.type) { throw new Error( - `The 'type' for a RecordIdentifier cannot be updated once it has been set. Attempted to set type for '${wrapper}' to '${type}'.` + `The 'type' for a RecordIdentifier cannot be updated once it has been set. Attempted to set type for '${wrapper.lid}' to '${type}'.` ); } @@ -732,35 +689,63 @@ function performRecordIdentifierUpdate(identifier: StableRecordIdentifier, data: } function detectMerge( - typesCache: { [key: string]: KeyOptions }, + cache: StableCache, + keyInfo: KeyInfo, identifier: StableRecordIdentifier, - data: unknown, - newId: string | null, - lids: IdentifierMap + data: unknown ): StableRecordIdentifier | false { + const newId = keyInfo.id; const { id, type, lid } = identifier; + const typeSet = cache.resourcesByType[identifier.type]; + + // if the IDs are present but do not match + // then check if we have an existing identifier + // for the newer ID. if (id !== null && id !== newId && newId !== null) { - let keyOptions = getTypeIndex(typesCache, identifier.type); - let existingIdentifier = keyOptions.id.get(newId); + const existingIdentifier = typeSet && typeSet.id.get(newId); return existingIdentifier !== undefined ? existingIdentifier : false; } else { - let newType = (data as ExistingResourceObject).type && normalizeModelName((data as ExistingResourceObject).type); + const newType = keyInfo.type; // If the ids and type are the same but lid is not the same, we should trigger a merge of the identifiers - // @ts-expect-error TODO this needs to be fixed - if (id !== null && id === newId && newType === type && data.lid && data.lid !== lid) { - // @ts-expect-error TODO this needs to be fixed - let existingIdentifier = lids.get(data.lid); - return existingIdentifier !== undefined ? existingIdentifier : false; + // we trigger a merge of the identifiers + // though probably we should just throw an error here + if (id !== null && id === newId && newType === type && hasLid(data) && data.lid !== lid) { + return cache.resources.get(data.lid) || false; + // If the lids are the same, and ids are the same, but types are different we should trigger a merge of the identifiers - // @ts-expect-error TODO this needs to be fixed - } else if (id !== null && id === newId && newType && newType !== type && data.lid && data.lid === lid) { - let keyOptions = getTypeIndex(typesCache, newType); - let existingIdentifier = keyOptions.id.get(id); + } else if (id !== null && id === newId && newType && newType !== type && hasLid(data) && data.lid === lid) { + const newTypeSet = cache.resourcesByType[newType]; + const existingIdentifier = newTypeSet && newTypeSet.id.get(newId); + return existingIdentifier !== undefined ? existingIdentifier : false; } } return false; } + +function getIdentifierFromLid(cache: StableCache, lid: string, resource: unknown): StableRecordIdentifier | undefined { + const identifier = cache.resources.get(lid); + if (LOG_IDENTIFIERS) { + // eslint-disable-next-line no-console + console.log(`Identifiers: cache ${identifier ? 'HIT' : 'MISS'} - Non-Stable ${lid}`, resource); + } + return identifier; +} + +function addResourceToCache(cache: StableCache, identifier: StableRecordIdentifier): void { + cache.resources.set(identifier.lid, identifier); + let typeSet = cache.resourcesByType[identifier.type]; + + if (!typeSet) { + typeSet = { lid: new Map(), id: new Map() }; + cache.resourcesByType[identifier.type] = typeSet; + } + + typeSet.lid.set(identifier.lid, identifier); + if (identifier.id) { + typeSet.id.set(identifier.id, identifier); + } +} diff --git a/packages/store/src/-private/caches/instance-cache.ts b/packages/store/src/-private/caches/instance-cache.ts index 5a223a02ca5..229d7958bb6 100644 --- a/packages/store/src/-private/caches/instance-cache.ts +++ b/packages/store/src/-private/caches/instance-cache.ts @@ -310,11 +310,11 @@ export class InstanceCache { if (type === undefined) { // it would be cool if we could just de-ref cache here // but probably would require WeakRef models to do so. - cache.lids.forEach((identifier) => { + cache.resources.forEach((identifier) => { this.unloadRecord(identifier); }); } else { - const typeCache = cache.types; + const typeCache = cache.resourcesByType; let identifiers = typeCache[type]?.lid; if (identifiers) { identifiers.forEach((identifier) => { diff --git a/packages/store/src/-private/caches/resource-utils.ts b/packages/store/src/-private/caches/resource-utils.ts new file mode 100644 index 00000000000..9ef9eee9b05 --- /dev/null +++ b/packages/store/src/-private/caches/resource-utils.ts @@ -0,0 +1,23 @@ +function isResource(resource: unknown): resource is Record { + return Boolean(resource && typeof resource === 'object'); +} + +function hasProp(resource: unknown, prop: T): resource is K { + return Boolean( + isResource(resource) && prop in resource && typeof resource[prop] === 'string' && (resource[prop] as string).length + ); +} + +export function hasLid(resource: unknown): resource is { lid: string } { + return hasProp(resource, 'lid'); +} + +export function hasId(resource: unknown): resource is { id: string } { + return ( + hasProp(resource, 'id') || Boolean(isResource(resource) && 'id' in resource && typeof resource.id === 'number') + ); +} + +export function hasType(resource: unknown): resource is { type: string } { + return hasProp(resource, 'type'); +} diff --git a/packages/store/src/-private/network/request-cache.ts b/packages/store/src/-private/network/request-cache.ts index fa8bf214516..7f56e868283 100644 --- a/packages/store/src/-private/network/request-cache.ts +++ b/packages/store/src/-private/network/request-cache.ts @@ -3,6 +3,7 @@ */ import { assert } from '@ember/debug'; +import { DEBUG } from '@ember-data/env'; import type { FindRecordQuery, Operation, @@ -16,6 +17,7 @@ import Store from '../store-service'; const Touching: unique symbol = Symbol('touching'); export const RequestPromise: unique symbol = Symbol('promise'); +const EMPTY_ARR: RequestState[] = DEBUG ? (Object.freeze([]) as unknown as RequestState[]) : []; interface InternalRequest extends RequestState { [Touching]: RecordIdentifier[]; @@ -36,7 +38,7 @@ function hasRecordIdentifier(op: Operation): op is RecordOperation { * @public */ export default class RequestStateService { - _pending: { [lid: string]: InternalRequest[] } = Object.create(null); + _pending: Map = new Map(); _done: Map = new Map(); _subscriptions: { [lid: string]: Function[] } = Object.create(null); _toFlush: InternalRequest[] = []; @@ -53,10 +55,10 @@ export default class RequestStateService { _enqueue(promise: Promise, queryRequest: Request): Promise { let query = queryRequest.data[0]; if (hasRecordIdentifier(query)) { - let lid = query.recordIdentifier.lid; + const identifier = query.recordIdentifier; let type = query.op === 'saveRecord' ? ('mutation' as const) : ('query' as const); - if (!this._pending[lid]) { - this._pending[lid] = []; + if (!this._pending.has(identifier)) { + this._pending.set(identifier, []); } let request: InternalRequest = { state: 'pending', @@ -65,11 +67,11 @@ export default class RequestStateService { } as InternalRequest; request[Touching] = [query.recordIdentifier]; request[RequestPromise] = promise; - this._pending[lid].push(request); + this._pending.get(identifier)!.push(request); this._triggerSubscriptions(request); return promise.then( (result) => { - this._dequeue(lid, request); + this._dequeue(identifier, request); let finalizedRequest = { state: 'fulfilled', request: queryRequest, @@ -82,7 +84,7 @@ export default class RequestStateService { return result; }, (error) => { - this._dequeue(lid, request); + this._dequeue(identifier, request); let finalizedRequest = { state: 'rejected', request: queryRequest, @@ -128,8 +130,12 @@ export default class RequestStateService { }); } - _dequeue(lid: string, request: InternalRequest) { - this._pending[lid] = this._pending[lid].filter((req) => req !== request); + _dequeue(identifier: StableRecordIdentifier, request: InternalRequest) { + const pending = this._pending.get(identifier)!; + this._pending.set( + identifier, + pending.filter((req) => req !== request) + ); } _addDone(request: InternalRequest) { @@ -200,11 +206,8 @@ export default class RequestStateService { * @param {StableRecordIdentifier} identifier * @returns {RequestState[]} an array of request states for any pending requests for the given identifier */ - getPendingRequestsForRecord(identifier: RecordIdentifier): RequestState[] { - if (this._pending[identifier.lid]) { - return this._pending[identifier.lid]; - } - return []; + getPendingRequestsForRecord(identifier: StableRecordIdentifier): RequestState[] { + return this._pending.get(identifier) || EMPTY_ARR; } /** diff --git a/packages/store/src/-private/store-service.ts b/packages/store/src/-private/store-service.ts index e63628591f6..2bca0e9146f 100644 --- a/packages/store/src/-private/store-service.ts +++ b/packages/store/src/-private/store-service.ts @@ -307,9 +307,9 @@ class Store extends EmberObject { if (TESTING) { const all: Promise[] = []; const pending = this._requestCache._pending; - const lids = Object.keys(pending); - lids.forEach((lid) => { - all.push(...pending[lid].map((v) => v[RequestPromise]!)); + + pending.forEach((requests) => { + all.push(...requests.map((v) => v[RequestPromise]!)); }); this.requestManager._pending.forEach((v) => all.push(v)); const promise: Promise & { length: number } = Promise.allSettled(all) as Promise & { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a7334f36411..ecf962a528d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2868,6 +2868,9 @@ importers: loader.js: specifier: ^4.7.0 version: 4.7.0 + terser-webpack-plugin: + specifier: ^5.3.9 + version: 5.3.9(webpack@5.88.2) zlib: specifier: 1.0.5 version: 1.0.5 diff --git a/tests/json-api/tests/integration/cache/collection-data-documents-test.ts b/tests/json-api/tests/integration/cache/collection-data-documents-test.ts index bc5112c1b0c..85031368145 100644 --- a/tests/json-api/tests/integration/cache/collection-data-documents-test.ts +++ b/tests/json-api/tests/integration/cache/collection-data-documents-test.ts @@ -9,7 +9,7 @@ import type { NotificationType } from '@ember-data/store/-private/managers/notif import type { CollectionResourceDataDocument, StructuredDocument } from '@ember-data/types/cache/document'; import type { CacheCapabilitiesManager } from '@ember-data/types/q/cache-store-wrapper'; import type { CollectionResourceDocument } from '@ember-data/types/q/ember-data-json-api'; -import type { StableRecordIdentifier } from '@ember-data/types/q/identifier'; +import type { StableExistingRecordIdentifier, StableRecordIdentifier } from '@ember-data/types/q/identifier'; import { JsonApiResource } from '@ember-data/types/q/record-data-json-api'; import { AttributesSchema, RelationshipsSchema } from '@ember-data/types/q/record-data-schemas'; @@ -103,8 +103,16 @@ module('Integration | @ember-data/json-api Cache.put()', ], }, } as StructuredDocument) as CollectionResourceDataDocument; - const identifier = store.identifierCache.getOrCreateRecordIdentifier({ type: 'user', id: '1' }); - const identifier2 = store.identifierCache.getOrCreateRecordIdentifier({ type: 'user', id: '2' }); + const identifier = store.identifierCache.getOrCreateRecordIdentifier({ + type: 'user', + id: '1', + }) as StableExistingRecordIdentifier; + const identifier2 = store.identifierCache.getOrCreateRecordIdentifier({ + type: 'user', + id: '2', + }) as StableExistingRecordIdentifier; + assert.strictEqual(identifier.id, '1', 'We were given the correct data back'); + assert.strictEqual(identifier2.id, '2', 'We were given the correct data back'); assert.deepEqual(responseDocument.data, [identifier, identifier2], 'We were given the correct data back'); diff --git a/tests/json-api/tests/integration/cache/meta-documents-test.ts b/tests/json-api/tests/integration/cache/meta-documents-test.ts index 0e4fe401914..2fa13a706c2 100644 --- a/tests/json-api/tests/integration/cache/meta-documents-test.ts +++ b/tests/json-api/tests/integration/cache/meta-documents-test.ts @@ -13,6 +13,7 @@ import type { import { StableDocumentIdentifier } from '@ember-data/types/cache/identifier'; import type { CacheCapabilitiesManager } from '@ember-data/types/q/cache-store-wrapper'; import type { CollectionResourceDocument } from '@ember-data/types/q/ember-data-json-api'; +import { StableExistingRecordIdentifier } from '@ember-data/types/q/identifier'; import { AttributesSchema, RelationshipsSchema } from '@ember-data/types/q/record-data-schemas'; class TestStore extends Store { @@ -215,7 +216,10 @@ module('Integration | @ember-data/json-api Cach.put()', function ( meta: { count: 4, last: 4 }, }, } as StructuredDocument) as CollectionResourceDataDocument; - const identifier = store.identifierCache.getOrCreateRecordIdentifier({ type: 'user', id: '1' }); + const identifier = store.identifierCache.getOrCreateRecordIdentifier({ + type: 'user', + id: '1', + }) as StableExistingRecordIdentifier; assert.deepEqual(responseDocument.data, [identifier], 'data is associated'); assert.deepEqual(responseDocument.meta, { count: 4, last: 4 }, 'meta is correct'); diff --git a/tests/json-api/tests/integration/cache/resource-data-documents-test.ts b/tests/json-api/tests/integration/cache/resource-data-documents-test.ts index d1bfb7ea1dc..5155a039580 100644 --- a/tests/json-api/tests/integration/cache/resource-data-documents-test.ts +++ b/tests/json-api/tests/integration/cache/resource-data-documents-test.ts @@ -10,7 +10,7 @@ import type { SingleResourceDataDocument, StructuredDocument } from '@ember-data import type { StableDocumentIdentifier } from '@ember-data/types/cache/identifier'; import type { CacheCapabilitiesManager } from '@ember-data/types/q/cache-store-wrapper'; import type { SingleResourceDocument } from '@ember-data/types/q/ember-data-json-api'; -import type { StableRecordIdentifier } from '@ember-data/types/q/identifier'; +import type { StableExistingRecordIdentifier, StableRecordIdentifier } from '@ember-data/types/q/identifier'; import type { JsonApiResource } from '@ember-data/types/q/record-data-json-api'; import type { AttributesSchema, RelationshipsSchema } from '@ember-data/types/q/record-data-schemas'; @@ -82,7 +82,10 @@ module('Integration | @ember-data/json-api Cache.put()', f data: { type: 'user', id: '1', attributes: { name: 'Chris' } }, }, } as StructuredDocument) as SingleResourceDataDocument; - const identifier = store.identifierCache.getOrCreateRecordIdentifier({ type: 'user', id: '1' }); + const identifier = store.identifierCache.getOrCreateRecordIdentifier({ + type: 'user', + id: '1', + }) as StableExistingRecordIdentifier; assert.strictEqual(responseDocument.data, identifier, 'We were given the correct data back'); }); @@ -97,7 +100,10 @@ module('Integration | @ember-data/json-api Cache.put()', f data: { type: 'user', id: '1', attributes: { name: 'Chris' } }, }, } as StructuredDocument) as SingleResourceDataDocument; - const identifier = store.identifierCache.getOrCreateRecordIdentifier({ type: 'user', id: '1' }); + const identifier = store.identifierCache.getOrCreateRecordIdentifier({ + type: 'user', + id: '1', + }) as StableExistingRecordIdentifier; assert.strictEqual(responseDocument.data, identifier, 'We were given the correct data back'); @@ -134,7 +140,10 @@ module('Integration | @ember-data/json-api Cache.put()', f data: { type: 'user', id: '1', attributes: { name: 'Chris' } }, }, } as StructuredDocument) as SingleResourceDataDocument; - const identifier = store.identifierCache.getOrCreateRecordIdentifier({ type: 'user', id: '1' }); + const identifier = store.identifierCache.getOrCreateRecordIdentifier({ + type: 'user', + id: '1', + }) as StableExistingRecordIdentifier; assert.strictEqual(responseDocument.data, identifier, 'We were given the correct data back'); diff --git a/tests/main/ember-cli-build.js b/tests/main/ember-cli-build.js index 93337d7735c..d55b963d018 100644 --- a/tests/main/ember-cli-build.js +++ b/tests/main/ember-cli-build.js @@ -13,7 +13,7 @@ module.exports = function (defaults) { terser: { compress: { - ecma: 2021, + ecma: 2022, passes: 6, // slow, but worth it negate_iife: false, sequences: 30, @@ -30,7 +30,7 @@ module.exports = function (defaults) { }, toplevel: false, sourceMap: false, - ecma: 2021, + ecma: 2022, }, }; diff --git a/tests/main/tests/integration/cache-handler/store-package-setup-test.ts b/tests/main/tests/integration/cache-handler/store-package-setup-test.ts index 044f68c7ba5..0a4359db56a 100644 --- a/tests/main/tests/integration/cache-handler/store-package-setup-test.ts +++ b/tests/main/tests/integration/cache-handler/store-package-setup-test.ts @@ -27,7 +27,7 @@ import type { import { StableDocumentIdentifier } from '@ember-data/types/cache/identifier'; import type { CacheCapabilitiesManager } from '@ember-data/types/q/cache-store-wrapper'; import type { ResourceIdentifierObject } from '@ember-data/types/q/ember-data-json-api'; -import type { StableRecordIdentifier } from '@ember-data/types/q/identifier'; +import type { StableExistingRecordIdentifier, StableRecordIdentifier } from '@ember-data/types/q/identifier'; import type { JsonApiResource } from '@ember-data/types/q/record-data-json-api'; import type { RecordInstance } from '@ember-data/types/q/record-instance'; @@ -889,7 +889,10 @@ module('Store | CacheHandler - @ember-data/store', function (hooks) { store, url: '/assets/users/1.json', }); - const identifier = store.identifierCache.getOrCreateRecordIdentifier({ type: 'user', id: '1' }); + const identifier = store.identifierCache.getOrCreateRecordIdentifier({ + type: 'user', + id: '1', + }) as StableExistingRecordIdentifier; const record = store.peekRecord(identifier) as FakeRecord | null; const data = userDocument.content.data!; @@ -948,7 +951,10 @@ module('Store | CacheHandler - @ember-data/store', function (hooks) { store.identifierCache.getOrCreateDocumentIdentifier({ url: '/assets/users/1.json' })! ) as unknown as StructuredDataDocument; const data3 = updatedUserDocument?.content?.data; - const identifier2 = store.identifierCache.getOrCreateRecordIdentifier({ type: 'user', id: '2' }); + const identifier2 = store.identifierCache.getOrCreateRecordIdentifier({ + type: 'user', + id: '2', + }) as StableExistingRecordIdentifier; assert.strictEqual(data3, identifier2, 'we get an identifier back as data'); assert.strictEqual(updatedUserDocument.content.lid, '/assets/users/1.json', 'we get back url as the cache key'); diff --git a/tests/main/tests/integration/identifiers/configuration-test.ts b/tests/main/tests/integration/identifiers/configuration-test.ts index 7f50f321399..fb4988274a8 100644 --- a/tests/main/tests/integration/identifiers/configuration-test.ts +++ b/tests/main/tests/integration/identifiers/configuration-test.ts @@ -410,6 +410,7 @@ module('Integration | Identifiers - configuration', function (hooks) { const userByIdPromise = store.findRecord('user', '1'); assert.strictEqual(generateLidCalls, 2, 'We generated two lids'); + assert.strictEqual(store.identifierCache._cache.resources.size, 2, 'We have 2 identifiers in the cache'); generateLidCalls = 0; const originalUserByUsernameIdentifier = store.identifierCache.getOrCreateRecordIdentifier({ @@ -421,7 +422,8 @@ module('Integration | Identifiers - configuration', function (hooks) { id: '1', }); - assert.strictEqual(generateLidCalls, 0, 'We generated no new lids when we looked up the originals'); + assert.strictEqual(generateLidCalls, 2, 'We generated no new lids when we looked up the originals'); + assert.strictEqual(store.identifierCache._cache.resources.size, 2, 'We still have 2 identifiers in the cache'); generateLidCalls = 0; // we expect that the username based identifier will be abandoned @@ -431,7 +433,8 @@ module('Integration | Identifiers - configuration', function (hooks) { const finalUserByUsernameIdentifier = recordIdentifierFor(userByUsername); const finalUserByIdIdentifier = recordIdentifierFor(userById); - assert.strictEqual(generateLidCalls, 0, 'We generated no new lids when we looked up the final by record'); + assert.strictEqual(generateLidCalls, 2, 'We generated no new lids when we looked up the originals'); + assert.strictEqual(store.identifierCache._cache.resources.size, 1, 'We now have only 1 identifier in the cache'); assert.strictEqual(forgetMethodCalls, 1, 'We abandoned an identifier'); assert.notStrictEqual( diff --git a/tests/main/tests/integration/identifiers/polymorphic-scenarios-test.ts b/tests/main/tests/integration/identifiers/polymorphic-scenarios-test.ts index e4c082ba3d3..7d5f6d5c163 100644 --- a/tests/main/tests/integration/identifiers/polymorphic-scenarios-test.ts +++ b/tests/main/tests/integration/identifiers/polymorphic-scenarios-test.ts @@ -7,6 +7,7 @@ import { setupTest } from 'ember-qunit'; import Adapter from '@ember-data/adapter'; import Model, { attr, belongsTo, hasMany } from '@ember-data/model'; +import { recordIdentifierFor } from '@ember-data/store'; type RID = { type: string; id: string }; @@ -86,9 +87,12 @@ module('Integration | Identifiers - single-table-inheritance polymorphic scenari const foundFerrari = await store.findRecord('car', '1'); assert.strictEqual(foundFerrari.constructor.modelName, 'ferrari', 'We found the right type'); + assert.strictEqual(recordIdentifierFor(foundFerrari).type, 'ferrari', 'We ended with the correct type'); const cachedFerrari = await store.peekRecord('ferrari', '1'); assert.strictEqual(cachedFerrari.constructor.modelName, 'ferrari', 'We cached the right type'); + assert.strictEqual(recordIdentifierFor(cachedFerrari).type, 'ferrari', 'We ended with the correct type'); + assert.strictEqual(foundFerrari, cachedFerrari, 'We have the same car'); }); test(`Identity of polymorphic relations can change type when in cache`, async function (assert) { diff --git a/tests/main/tests/integration/identifiers/scenarios-test.ts b/tests/main/tests/integration/identifiers/scenarios-test.ts index 692cffa4b49..f75f6929634 100644 --- a/tests/main/tests/integration/identifiers/scenarios-test.ts +++ b/tests/main/tests/integration/identifiers/scenarios-test.ts @@ -186,7 +186,7 @@ module('Integration | Identifiers - scenarios', function (hooks) { assert.strictEqual(calls.queryRecord, 1, 'We made one call to Adapter.queryRecord'); // ensure we truly are in a good state internally - const lidCache = store.identifierCache._cache.lids; + const lidCache = store.identifierCache._cache.resources; const lids = [...lidCache.values()]; assert.strictEqual( lidCache.size, @@ -207,7 +207,7 @@ module('Integration | Identifiers - scenarios', function (hooks) { assert.strictEqual(calls.queryRecord, 1, 'We made one call to Adapter.queryRecord'); // ensure we truly are in a good state internally - const lidCache = store.identifierCache._cache.lids; + const lidCache = store.identifierCache._cache.resources; const lids = [...lidCache.values()]; assert.strictEqual( lidCache.size, @@ -237,7 +237,7 @@ module('Integration | Identifiers - scenarios', function (hooks) { assert.strictEqual(calls.queryRecord, 2, 'We made two calls to Adapter.queryRecord'); // ensure we truly are in a good state internally - const lidCache = store.identifierCache._cache.lids; + const lidCache = store.identifierCache._cache.resources; const lids = [...lidCache.values()]; assert.strictEqual( lidCache.size, @@ -420,7 +420,7 @@ module('Integration | Identifiers - scenarios', function (hooks) { assert.strictEqual(identifierById.id, '1', 'The identifier id is correct'); // ensure we truly are in a good state internally - const lidCache = store.identifierCache._cache.lids; + const lidCache = store.identifierCache._cache.resources; const lids = [...lidCache.values()]; assert.strictEqual( lidCache.size, @@ -443,7 +443,7 @@ module('Integration | Identifiers - scenarios', function (hooks) { assert.strictEqual(identifierById.id, '1', 'The identifier id is correct'); // ensure we truly are in a good state internally - const lidCache = store.identifierCache._cache.lids; + const lidCache = store.identifierCache._cache.resources; const lids = [...lidCache.values()]; assert.strictEqual( lidCache.size, @@ -469,7 +469,7 @@ module('Integration | Identifiers - scenarios', function (hooks) { assert.strictEqual(identifierById.id, '1', 'The identifier id is correct'); // ensure we truly are in a good state internally - const lidCache = store.identifierCache._cache.lids; + const lidCache = store.identifierCache._cache.resources; const lids = [...lidCache.values()]; assert.strictEqual( lidCache.size, @@ -502,7 +502,7 @@ module('Integration | Identifiers - scenarios', function (hooks) { assert.strictEqual(identifierByUsername.id, '1', 'The identifier id is correct'); // ensure we truly are in a good state internally - const lidCache = store.identifierCache._cache.lids; + const lidCache = store.identifierCache._cache.resources; const lids = [...lidCache.values()]; assert.strictEqual( lidCache.size, @@ -554,7 +554,7 @@ module('Integration | Identifiers - scenarios', function (hooks) { assert.strictEqual(identifierByUsername.id, '1', 'The identifier id is correct'); // ensure we truly are in a good state internally - const lidCache = store.identifierCache._cache.lids; + const lidCache = store.identifierCache._cache.resources; const lids = [...lidCache.values()]; assert.strictEqual( lidCache.size, @@ -587,7 +587,7 @@ module('Integration | Identifiers - scenarios', function (hooks) { assert.strictEqual(identifierById.id, '1', 'The identifier id is correct'); // ensure we truly are in a good state internally - const lidCache = store.identifierCache._cache.lids; + const lidCache = store.identifierCache._cache.resources; const lids = [...lidCache.values()]; assert.strictEqual( lidCache.size, @@ -619,7 +619,7 @@ module('Integration | Identifiers - scenarios', function (hooks) { assert.strictEqual(identifierById.id, '1', 'The identifier id is correct'); // ensure we truly are in a good state internally - const lidCache = store.identifierCache._cache.lids; + const lidCache = store.identifierCache._cache.resources; const lids = [...lidCache.values()]; assert.strictEqual( lidCache.size, diff --git a/tests/main/tests/integration/record-array-test.js b/tests/main/tests/integration/record-array-test.js index 0ba6c569edf..5aa870de433 100644 --- a/tests/main/tests/integration/record-array-test.js +++ b/tests/main/tests/integration/record-array-test.js @@ -73,7 +73,7 @@ module('integration/record-array - RecordArray', function (hooks) { deprecatedTest( 'acts as a live query (normalized names)', { - count: 11, + count: 9, until: '6.0', id: 'ember-data:deprecate-non-strict-types', }, diff --git a/tests/main/tests/integration/record-data/record-data-state-test.ts b/tests/main/tests/integration/record-data/record-data-state-test.ts index 5c988b81c56..92c059fc3d7 100644 --- a/tests/main/tests/integration/record-data/record-data-state-test.ts +++ b/tests/main/tests/integration/record-data/record-data-state-test.ts @@ -32,11 +32,7 @@ import type { SingleResourceDocument, SingleResourceRelationship, } from '@ember-data/types/q/ember-data-json-api'; -import type { - RecordIdentifier, - StableExistingRecordIdentifier, - StableRecordIdentifier, -} from '@ember-data/types/q/identifier'; +import type { StableExistingRecordIdentifier, StableRecordIdentifier } from '@ember-data/types/q/identifier'; import type { JsonApiError, JsonApiResource } from '@ember-data/types/q/record-data-json-api'; class Person extends Model { @@ -70,15 +66,17 @@ class TestRecordData implements Cache { if ('content' in doc && !('error' in doc)) { if (Array.isArray(doc.content.data)) { const data = doc.content.data.map((resource) => { - const identifier = this._storeWrapper.identifierCache.getOrCreateRecordIdentifier(resource); + const identifier = this._storeWrapper.identifierCache.getOrCreateRecordIdentifier( + resource + ) as StableExistingRecordIdentifier; this.upsert(identifier, resource, this._storeWrapper.hasRecord(identifier)); return identifier; }); return { data }; } else { const identifier = this._storeWrapper.identifierCache.getOrCreateRecordIdentifier( - doc.content.data as RecordIdentifier - ); + doc.content.data + ) as StableExistingRecordIdentifier; this.upsert(identifier, doc.content.data as JsonApiResource, this._storeWrapper.hasRecord(identifier)); return { data: identifier } as SingleResourceDataDocument; } diff --git a/tests/main/tests/integration/record-data/record-data-test.ts b/tests/main/tests/integration/record-data/record-data-test.ts index 4b7ba22d730..64f6d2d3375 100644 --- a/tests/main/tests/integration/record-data/record-data-test.ts +++ b/tests/main/tests/integration/record-data/record-data-test.ts @@ -81,7 +81,9 @@ class TestRecordData implements Cache { if ('content' in doc && !('error' in doc)) { if (Array.isArray(doc.content.data)) { const data = doc.content.data.map((resource) => { - const identifier = this._storeWrapper.identifierCache.getOrCreateRecordIdentifier(resource); + const identifier = this._storeWrapper.identifierCache.getOrCreateRecordIdentifier( + resource + ) as StableExistingRecordIdentifier; this.upsert(identifier, resource, this._storeWrapper.hasRecord(identifier)); return identifier; }); diff --git a/tests/main/tests/integration/records/rematerialize-test.js b/tests/main/tests/integration/records/rematerialize-test.js index db2c5cadf77..40ebc9ef3f5 100644 --- a/tests/main/tests/integration/records/rematerialize-test.js +++ b/tests/main/tests/integration/records/rematerialize-test.js @@ -20,69 +20,58 @@ module('integration/unload - Rematerializing Unloaded Records', function (hooks) }); test('a sync belongs to relationship to an unloaded record can restore that record', function (assert) { - const Person = Model.extend({ - name: attr('string'), - cars: hasMany('car', { async: false, inverse: 'person' }), - toString: () => 'Person', - }); + class Person extends Model { + @attr('string') name; + @hasMany('car', { async: false, inverse: 'person' }) cars; + } - const Car = Model.extend({ - make: attr('string'), - model: attr('string'), - person: belongsTo('person', { async: false, inverse: 'cars' }), - toString: () => 'Car', - }); + class Car extends Model { + @attr('string') make; + @attr('string') model; + @belongsTo('person', { async: false, inverse: 'cars' }) person; + } this.owner.register('model:person', Person); this.owner.register('model:car', Car); - let store = this.owner.lookup('service:store'); - let adapter = store.adapterFor('application'); + const store = this.owner.lookup('service:store'); + const adapter = store.adapterFor('application'); // disable background reloading so we do not re-create the relationship. adapter.shouldBackgroundReloadRecord = () => false; - let adam = run(() => { - store.push({ - data: { - type: 'person', - id: '1', - attributes: { - name: 'Adam Sunderland', - }, - relationships: { - cars: { - data: [{ type: 'car', id: '1' }], - }, + let adam = store.push({ + data: { + type: 'person', + id: '1', + attributes: { + name: 'Adam Sunderland', + }, + relationships: { + cars: { + data: [{ type: 'car', id: '1' }], }, }, - }); - - return store.peekRecord('person', 1); + }, }); - let bob = run(() => { - store.push({ - data: { - type: 'car', - id: '1', - attributes: { - make: 'Lotus', - model: 'Exige', - }, - relationships: { - person: { - data: { type: 'person', id: '1' }, - }, + const lotus = store.push({ + data: { + type: 'car', + id: '1', + attributes: { + make: 'Lotus', + model: 'Exige', + }, + relationships: { + person: { + data: { type: 'person', id: '1' }, }, }, - }); - - return store.peekRecord('car', 1); + }, }); - let person = store.peekRecord('person', 1); - assert.strictEqual(person.cars.length, 1, 'The inital length of cars is correct'); + assert.strictEqual(adam.cars.length, 1, 'The inital length of cars is correct'); assert.notStrictEqual(store.peekRecord('person', '1'), null, 'The person is in the store'); assert.true( @@ -90,7 +79,7 @@ module('integration/unload - Rematerializing Unloaded Records', function (hooks) 'The person identifier is loaded' ); - run(() => person.unloadRecord()); + run(() => adam.unloadRecord()); assert.strictEqual(store.peekRecord('person', '1'), null, 'The person is unloaded'); assert.false( @@ -98,26 +87,25 @@ module('integration/unload - Rematerializing Unloaded Records', function (hooks) 'The person identifier is freed' ); - run(() => { - store.push({ - data: { - type: 'person', - id: '1', - attributes: { - name: 'Adam Sunderland', - }, - relationships: { - cars: { - data: [{ type: 'car', id: '1' }], - }, + const newAdam = store.push({ + data: { + type: 'person', + id: '1', + attributes: { + name: 'Adam Sunderland', + }, + relationships: { + cars: { + data: [{ type: 'car', id: '1' }], }, }, - }); + }, }); - let rematerializedPerson = bob.person; + let rematerializedPerson = lotus.person; assert.strictEqual(rematerializedPerson.id, '1'); assert.strictEqual(rematerializedPerson.name, 'Adam Sunderland'); + assert.strictEqual(rematerializedPerson, newAdam); // the person is rematerialized; the previous person is *not* re-used assert.notEqual(rematerializedPerson, adam, 'the person is rematerialized, not recycled'); }); diff --git a/tests/main/tests/integration/store-test.js b/tests/main/tests/integration/store-test.js index fb9f570f985..f290b3bbd59 100644 --- a/tests/main/tests/integration/store-test.js +++ b/tests/main/tests/integration/store-test.js @@ -351,8 +351,8 @@ module('integration/store - findRecord', function (hooks) { adapter.shouldBackgroundReloadRecord = () => true; adapter.findRecord = () => { - if (calls++ < 3) { - return resolve({ + if (calls++ < 4) { + return Promise.resolve({ data: { type: 'car', id: '1', @@ -371,6 +371,7 @@ module('integration/store - findRecord', function (hooks) { const car = await proxiedCar; // load 1 assert.strictEqual(car.model, 'Mini', 'car record is returned from cache'); + const proxiedCar2 = store.findRecord('car', '1'); // will trigger a backgroundReload const car2 = await proxiedCar2; @@ -387,7 +388,7 @@ module('integration/store - findRecord', function (hooks) { await store._getAllPending(); - assert.strictEqual(calls, 3, 'we triggered one background reload and one load'); + assert.strictEqual(calls, 3, 'we triggered two background reloads and one load'); }); test('multiple parallel calls to store#findRecord return the cached record without waiting for background requests', async function (assert) { diff --git a/tests/performance/ember-cli-build.js b/tests/performance/ember-cli-build.js index 85f34a68459..20d6b1e7c92 100644 --- a/tests/performance/ember-cli-build.js +++ b/tests/performance/ember-cli-build.js @@ -28,6 +28,7 @@ module.exports = function (defaults) { // along with the exports of each module as its value. const { Webpack } = require('@embroider/webpack'); + const TerserPlugin = require('terser-webpack-plugin'); return require('@embroider/compat').compatBuild(app, Webpack, { // @@ -37,9 +38,46 @@ module.exports = function (defaults) { // staticModifiers: true, // staticComponents: true, // splitAtRoutes: ['route.name'], // can also be a RegExp - // packagerOptions: { - // webpackConfig: { } - // } + packagerOptions: { + webpackConfig: { + optimization: { + minimize: true, + minimizer: [ + new TerserPlugin({ + terserOptions: { + compress: { + ecma: 2022, + passes: 6, // slow, but worth it + negate_iife: false, + sequences: 30, + defaults: true, + arguments: false, + keep_fargs: false, + toplevel: false, + unsafe: true, + unsafe_comps: true, + unsafe_math: true, + unsafe_symbols: true, + unsafe_proto: true, + unsafe_undefined: true, + inline: 5, + reduce_funcs: false, + }, + mangle: { + keep_classnames: true, + keep_fnames: true, + module: true, + }, + format: { beautify: true }, + toplevel: false, + sourceMap: false, + ecma: 2022, + }, + }), + ], + }, + }, + }, // extraPublicTrees: [], }); diff --git a/tests/performance/package.json b/tests/performance/package.json index f95b15ce20d..3cc5a96c309 100644 --- a/tests/performance/package.json +++ b/tests/performance/package.json @@ -47,6 +47,7 @@ "ember-cli-htmlbars": "^6.2.0", "ember-load-initializers": "^2.1.2", "ember-maybe-import-regenerator": "^1.0.0", + "terser-webpack-plugin": "^5.3.9", "ember-resolver": "^10.1.1", "ember-source": "~5.1.2", "loader.js": "^4.7.0", diff --git a/tsconfig.json b/tsconfig.json index 412733082bd..95e5d1041d6 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -42,8 +42,6 @@ "packages/store/src/-private/store-service.ts", "packages/store/src/-private/utils/coerce-id.ts", "packages/store/src/-private/index.ts", - "packages/store/src/-private/caches/identifier-cache.ts", - "packages/serializer/src/index.ts", "tests/graph/tests/integration/graph/polymorphism/implicit-keys-test.ts", "tests/graph/tests/integration/graph/graph-test.ts", "tests/graph/tests/integration/graph/operations-test.ts",