@ember-data/record-data
Version:
Provides the default resource cache (RecordData) implementation for ember-data
395 lines (341 loc) • 12.4 kB
text/typescript
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);
}