UNPKG

@ember-data/record-data

Version:

Provides the default resource cache (RecordData) implementation for ember-data

395 lines (341 loc) 12.4 kB
import { assert } from '@ember/debug'; import { DEBUG } from '@glimmer/env'; import type { StableRecordIdentifier } from '@ember-data/types/q/identifier'; import { assertPolymorphicType } from '../../debug/assert-polymorphic-type'; import type ManyRelationship from '../../relationships/state/has-many'; import type { ReplaceRelatedRecordsOperation } from '../-operations'; import { isBelongsTo, isHasMany, isNew, notifyChange } from '../-utils'; import type { Graph } from '../graph'; /* case many:1 ======== In a bi-directional graph with Many:1 edges, adding a value results in up-to 3 discrete value transitions, while removing a value is only 2 transitions. For adding C to A If: A <<-> B, C <->> D is the initial state, and: B <->> A <<-> C, D is the final state then we would undergo the following transitions. add C to A remove C from D add A to C For removing B from A If: A <<-> B, C <->> D is the initial state, and: A, B, C <->> D is the final state then we would undergo the following transitions. remove B from A remove A from B case many:many =========== In a bi-directional graph with Many:Many edges, adding or removing a value requires only 2 value transitions. For Adding If: A<<->>B, C<<->>D is the initial state (double arrows representing the many side) And: D<<->>C<<->>A<<->>B is the final state Then we would undergo two transitions. add C to A. add A to C For Removing If: A<<->>B, C<<->>D is the initial state (double arrows representing the many side) And: A, B, C<<->>D is the final state Then we would undergo two transitions. remove B from A remove A from B case many:? ======== In a uni-directional graph with Many:? edges (modeled in EmberData with `inverse:null`) with artificial (implicit) inverses, replacing a value results in 2 discrete value transitions. This is because a Many:? relationship is effectively Many:Many. */ export default function replaceRelatedRecords(graph: Graph, op: ReplaceRelatedRecordsOperation, isRemote: boolean) { if (isRemote) { replaceRelatedRecordsRemote(graph, op, isRemote); } else { replaceRelatedRecordsLocal(graph, op, isRemote); } } function replaceRelatedRecordsLocal(graph: Graph, op: ReplaceRelatedRecordsOperation, isRemote: boolean) { const identifiers = op.value; const relationship = graph.get(op.record, op.field); assert(`expected hasMany relationship`, isHasMany(relationship)); relationship.state.hasReceivedData = true; // cache existing state const { localState, localMembers, definition } = relationship; const newValues = new Set(identifiers); const identifiersLength = identifiers.length; const newState = new Array(newValues.size); const newMembership = new Set<StableRecordIdentifier>(); // wipe existing state relationship.localMembers = newMembership; relationship.localState = newState; const { type } = relationship.definition; let changed = false; const currentLength = localState.length; const iterationLength = currentLength > identifiersLength ? currentLength : identifiersLength; const equalLength = currentLength === identifiersLength; for (let i = 0, j = 0; i < iterationLength; i++) { let adv = false; if (i < identifiersLength) { const identifier = identifiers[i]; // skip processing if we encounter a duplicate identifier in the array if (!newMembership.has(identifier)) { if (type !== identifier.type) { if (DEBUG) { assertPolymorphicType(relationship.identifier, relationship.definition, identifier, graph.store); } graph.registerPolymorphicType(type, identifier.type); } newState[j] = identifier; adv = true; newMembership.add(identifier); if (!localMembers.has(identifier)) { changed = true; addToInverse(graph, identifier, definition.inverseKey, op.record, isRemote); } } } if (i < currentLength) { const identifier = localState[i]; // detect reordering if (!newMembership.has(identifier)) { if (equalLength && newState[i] !== identifier) { changed = true; } if (!newValues.has(identifier)) { changed = true; removeFromInverse(graph, identifier, definition.inverseKey, op.record, isRemote); } } } if (adv) { j++; } } if (changed) { notifyChange(graph, relationship.identifier, relationship.definition.key); } } function replaceRelatedRecordsRemote(graph: Graph, op: ReplaceRelatedRecordsOperation, isRemote: boolean) { const identifiers = op.value; const relationship = graph.get(op.record, op.field); assert( `You can only '${op.op}' on a hasMany relationship. ${op.record.type}.${op.field} is a ${relationship.definition.kind}`, isHasMany(relationship) ); if (isRemote) { graph._addToTransaction(relationship); } relationship.state.hasReceivedData = true; // cache existing state const { remoteState, remoteMembers, definition } = relationship; const newValues = new Set(identifiers); const identifiersLength = identifiers.length; const newState = new Array(newValues.size); const newMembership = new Set<StableRecordIdentifier>(); // wipe existing state relationship.remoteMembers = newMembership; relationship.remoteState = newState; const { type } = relationship.definition; let changed = false; const canonicalLength = remoteState.length; const iterationLength = canonicalLength > identifiersLength ? canonicalLength : identifiersLength; const equalLength = canonicalLength === identifiersLength; for (let i = 0, j = 0; i < iterationLength; i++) { let adv = false; if (i < identifiersLength) { const identifier = identifiers[i]; if (!newMembership.has(identifier)) { if (type !== identifier.type) { if (DEBUG) { assertPolymorphicType(relationship.identifier, relationship.definition, identifier, graph.store); } graph.registerPolymorphicType(type, identifier.type); } newState[j] = identifier; newMembership.add(identifier); adv = true; if (!remoteMembers.has(identifier)) { changed = true; addToInverse(graph, identifier, definition.inverseKey, op.record, isRemote); } } } if (i < canonicalLength) { const identifier = remoteState[i]; if (!newMembership.has(identifier)) { // detect reordering if (equalLength && newState[j] !== identifier) { changed = true; } if (!newValues.has(identifier)) { changed = true; removeFromInverse(graph, identifier, definition.inverseKey, op.record, isRemote); } } } if (adv) { j++; } } if (changed) { flushCanonical(graph, relationship); /* replaceRelatedRecordsLocal( graph, { op: op.op, record: op.record, field: op.field, value: remoteState, }, false );*/ } else { // preserve legacy behavior we want to change but requires some sort // of deprecation. flushCanonical(graph, relationship); } } export function addToInverse( graph: Graph, identifier: StableRecordIdentifier, key: string, value: StableRecordIdentifier, isRemote: boolean ) { const relationship = graph.get(identifier, key); const { type } = relationship.definition; if (type !== value.type) { if (DEBUG) { assertPolymorphicType(relationship.identifier, relationship.definition, value, graph.store); } graph.registerPolymorphicType(type, value.type); } if (isBelongsTo(relationship)) { relationship.state.hasReceivedData = true; relationship.state.isEmpty = false; if (isRemote) { graph._addToTransaction(relationship); if (relationship.remoteState !== null) { removeFromInverse(graph, relationship.remoteState, relationship.definition.inverseKey, identifier, isRemote); } relationship.remoteState = value; } if (relationship.localState !== value) { if (!isRemote && relationship.localState) { removeFromInverse(graph, relationship.localState, relationship.definition.inverseKey, identifier, isRemote); } relationship.localState = value; notifyChange(graph, relationship.identifier, relationship.definition.key); } } else if (isHasMany(relationship)) { if (isRemote) { if (!relationship.remoteMembers.has(value)) { graph._addToTransaction(relationship); relationship.remoteState.push(value); relationship.remoteMembers.add(value); relationship.state.hasReceivedData = true; flushCanonical(graph, relationship); } } else { if (!relationship.localMembers.has(value)) { relationship.localState.push(value); relationship.localMembers.add(value); relationship.state.hasReceivedData = true; notifyChange(graph, relationship.identifier, relationship.definition.key); } } } else { if (isRemote) { if (!relationship.remoteMembers.has(value)) { relationship.remoteMembers.add(value); relationship.localMembers.add(value); } } else { if (!relationship.localMembers.has(value)) { relationship.localMembers.add(value); } } } } export function notifyInverseOfPotentialMaterialization( graph: Graph, identifier: StableRecordIdentifier, key: string, value: StableRecordIdentifier, isRemote: boolean ) { const relationship = graph.get(identifier, key); if (isHasMany(relationship) && isRemote && relationship.remoteMembers.has(value)) { notifyChange(graph, relationship.identifier, relationship.definition.key); } } export function removeFromInverse( graph: Graph, identifier: StableRecordIdentifier, key: string, value: StableRecordIdentifier, isRemote: boolean ) { const relationship = graph.get(identifier, key); if (isBelongsTo(relationship)) { relationship.state.isEmpty = true; if (isRemote) { graph._addToTransaction(relationship); relationship.remoteState = null; } if (relationship.localState === value) { relationship.localState = null; notifyChange(graph, identifier, key); } } else if (isHasMany(relationship)) { if (isRemote) { graph._addToTransaction(relationship); let index = relationship.remoteState.indexOf(value); if (index !== -1) { relationship.remoteMembers.delete(value); relationship.remoteState.splice(index, 1); } } let index = relationship.localState.indexOf(value); if (index !== -1) { relationship.localMembers.delete(value); relationship.localState.splice(index, 1); } notifyChange(graph, relationship.identifier, relationship.definition.key); } else { if (isRemote) { relationship.remoteMembers.delete(value); relationship.localMembers.delete(value); } else { if (value && relationship.localMembers.has(value)) { relationship.localMembers.delete(value); } } } } export function syncRemoteToLocal(graph: Graph, rel: ManyRelationship) { let toSet = rel.remoteState; let newRecordDatas = rel.localState.filter((recordData) => isNew(recordData) && toSet.indexOf(recordData) === -1); let existingState = rel.localState; rel.localState = toSet.concat(newRecordDatas); let localMembers = (rel.localMembers = new Set<StableRecordIdentifier>()); rel.remoteMembers.forEach((v) => localMembers.add(v)); for (let i = 0; i < newRecordDatas.length; i++) { localMembers.add(newRecordDatas[i]); } // TODO always notifying fails only one test and we should probably do away with it if (existingState.length !== rel.localState.length) { notifyChange(graph, rel.identifier, rel.definition.key); } else { for (let i = 0; i < existingState.length; i++) { if (existingState[i] !== rel.localState[i]) { notifyChange(graph, rel.identifier, rel.definition.key); break; } } } } function flushCanonical(graph: Graph, rel: ManyRelationship) { graph._scheduleLocalSync(rel); }