Skip to content

Commit

Permalink
feat: relationshipRollback, serializePatch (#8824)
Browse files Browse the repository at this point in the history
* feat: relationshipRollback

* lint updates

* add all the tests

* remove unneeded assertion

* more tests

* fix ts
  • Loading branch information
runspired authored Sep 4, 2023
1 parent 222df18 commit feb6e5a
Show file tree
Hide file tree
Showing 23 changed files with 1,868 additions and 68 deletions.
2 changes: 0 additions & 2 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -255,8 +255,6 @@ module.exports = {
'tests/main/tests/integration/request-state-service-test.ts',
'tests/main/tests/integration/record-data/store-wrapper-test.ts',
'tests/main/tests/integration/record-data/record-data-test.ts',
'tests/main/tests/integration/record-data/record-data-state-test.ts',
'tests/main/tests/integration/record-data/record-data-errors-test.ts',
'tests/main/tests/integration/model-errors-test.ts',
'tests/main/tests/integration/identifiers/scenarios-test.ts',
'tests/main/tests/integration/identifiers/record-identifier-for-test.ts',
Expand Down
72 changes: 70 additions & 2 deletions ember-data-types/cache/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,21 @@ import { StableDocumentIdentifier } from './identifier';
import { Mutation } from './mutations';
import { Operation } from './operations';

export type RelationshipDiff =
| {
kind: 'collection';
remoteState: StableRecordIdentifier[];
additions: Set<StableRecordIdentifier>;
removals: Set<StableRecordIdentifier>;
localState: StableRecordIdentifier[];
reordered: boolean;
}
| {
kind: 'resource';
remoteState: StableRecordIdentifier | null;
localState: StableRecordIdentifier | null;
};

/**
* The interface for EmberData Caches.
*
Expand Down Expand Up @@ -365,13 +380,66 @@ export interface Cache {
*/
rollbackAttrs(identifier: StableRecordIdentifier): string[];

/**
* Query the cache for the changes to relationships of a resource.
*
* Returns a map of relationship names to RelationshipDiff objects.
*
* ```ts
* type RelationshipDiff =
| {
kind: 'collection';
remoteState: StableRecordIdentifier[];
additions: Set<StableRecordIdentifier>;
removals: Set<StableRecordIdentifier>;
localState: StableRecordIdentifier[];
reordered: boolean;
}
| {
kind: 'resource';
remoteState: StableRecordIdentifier | null;
localState: StableRecordIdentifier | null;
};
```
*
* @method changedRelationships
* @public
* @param {StableRecordIdentifier} identifier
* @returns {Map<string, RelationshipDiff>}
*/
changedRelationships(identifier: StableRecordIdentifier): Map<string, RelationshipDiff>;

/**
* Query the cache for whether any mutated attributes exist
*
* @method hasChangedRelationships
* @public
* @param {StableRecordIdentifier} identifier
* @returns {boolean}
*/
hasChangedRelationships(identifier: StableRecordIdentifier): boolean;

/**
* Tell the cache to discard any uncommitted mutations to relationships.
*
* This will also discard the change on any appropriate inverses.
*
* This method is a candidate to become a mutation
*
* @method rollbackRelationships
* @public
* @param {StableRecordIdentifier} identifier
* @returns {string[]} the names of relationships that were restored
*/
rollbackRelationships(identifier: StableRecordIdentifier): string[];

/**
* Query the cache for the current state of a relationship property
*
* @method getRelationship
* @public
* @param identifier
* @param field
* @param {StableRecordIdentifier} identifier
* @param {string} field
* @returns resource relationship object
*/
getRelationship(
Expand Down
35 changes: 35 additions & 0 deletions packages/graph/src/-private/-diff.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,13 @@ import { DEPRECATE_NON_UNIQUE_PAYLOADS } from '@ember-data/deprecations';
import { DEBUG } from '@ember-data/env';
import { StableRecordIdentifier } from '@ember-data/types/q/identifier';

import { isBelongsTo } from './-utils';
import { assertPolymorphicType } from './debug/assert-polymorphic-type';
import type { CollectionEdge } from './edges/collection';
import { ResourceEdge } from './edges/resource';
import { Graph } from './graph';
import replaceRelatedRecord from './operations/replace-related-record';
import replaceRelatedRecords from './operations/replace-related-records';

function _deprecatedCompare<T>(
newState: T[],
Expand Down Expand Up @@ -369,3 +373,34 @@ export function _removeRemote(relationship: CollectionEdge, value: StableRecordI

return true;
}

export function rollbackRelationship(
graph: Graph,
identifier: StableRecordIdentifier,
field: string,
relationship: CollectionEdge | ResourceEdge
): void {
if (isBelongsTo(relationship)) {
replaceRelatedRecord(
graph,
{
op: 'replaceRelatedRecord',
record: identifier,
field,
value: relationship.remoteState,
},
false
);
} else {
replaceRelatedRecords(
graph,
{
op: 'replaceRelatedRecords',
record: identifier,
field,
value: relationship.remoteState,
},
false
);
}
}
121 changes: 120 additions & 1 deletion packages/graph/src/-private/graph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ import { assert } from '@ember/debug';

import { LOG_GRAPH } from '@ember-data/debugging';
import { DEBUG } from '@ember-data/env';
import type { RelationshipDiff } from '@ember-data/types/cache/cache';
import type { CollectionRelationship, ResourceRelationship } from '@ember-data/types/cache/relationship';
import { MergeOperation } from '@ember-data/types/q/cache';
import type { CacheCapabilitiesManager } from '@ember-data/types/q/cache-store-wrapper';
import type { StableRecordIdentifier } from '@ember-data/types/q/identifier';

import { rollbackRelationship } from './-diff';
import type { EdgeCache, UpgradedMeta } from './-edge-definition';
import { isLHS, upgradeDefinition } from './-edge-definition';
import type {
Expand Down Expand Up @@ -293,11 +295,92 @@ export class Graph {
} else if (isHasMany(relationship)) {
const hasAdditions = relationship.additions !== null && relationship.additions.size > 0;
const hasRemovals = relationship.removals !== null && relationship.removals.size > 0;
return hasAdditions || hasRemovals;
return hasAdditions || hasRemovals || isReordered(relationship);
}
return false;
}

getChanged(identifier: StableRecordIdentifier): Map<string, RelationshipDiff> {
const relationships = this.identifiers.get(identifier);
const changed = new Map<string, RelationshipDiff>();

if (!relationships) {
return changed;
}

const keys = Object.keys(relationships);
for (let i = 0; i < keys.length; i++) {
const field = keys[i];
const relationship = relationships[field];
if (!relationship) {
continue;
}
if (isBelongsTo(relationship)) {
if (relationship.localState !== relationship.remoteState) {
changed.set(field, {
kind: 'resource',
remoteState: relationship.remoteState,
localState: relationship.localState,
});
}
} else if (isHasMany(relationship)) {
const hasAdditions = relationship.additions !== null && relationship.additions.size > 0;
const hasRemovals = relationship.removals !== null && relationship.removals.size > 0;
const reordered = isReordered(relationship);

if (hasAdditions || hasRemovals || reordered) {
changed.set(field, {
kind: 'collection',
additions: new Set(relationship.additions) || new Set(),
removals: new Set(relationship.removals) || new Set(),
remoteState: relationship.remoteState,
localState: legacyGetCollectionRelationshipData(relationship).data || [],
reordered,
});
}
}
}

return changed;
}

hasChanged(identifier: StableRecordIdentifier): boolean {
const relationships = this.identifiers.get(identifier);
if (!relationships) {
return false;
}
const keys = Object.keys(relationships);
for (let i = 0; i < keys.length; i++) {
if (this._isDirty(identifier, keys[i])) {
return true;
}
}
return false;
}

rollback(identifier: StableRecordIdentifier): string[] {
const relationships = this.identifiers.get(identifier);
const changed: string[] = [];
if (!relationships) {
return changed;
}
const keys = Object.keys(relationships);
for (let i = 0; i < keys.length; i++) {
const field = keys[i];
const relationship = relationships[field];
if (!relationship) {
continue;
}

if (this._isDirty(identifier, field)) {
rollbackRelationship(this, identifier, field, relationship as CollectionEdge | ResourceEdge);
changed.push(field);
}
}

return changed;
}

remove(identifier: StableRecordIdentifier) {
if (LOG_GRAPH) {
// eslint-disable-next-line no-console
Expand Down Expand Up @@ -695,3 +778,39 @@ function addPending(
}
arr.push(op);
}

function isReordered(relationship: CollectionEdge): boolean {
// if we are dirty we are never re-ordered because accessing
// the state would flush away any reordering.
if (relationship.isDirty) {
return false;
}

const { remoteState, localState, additions, removals } = relationship;
assert(`Expected localSate`, localState);

for (let i = 0, j = 0; i < remoteState.length; i++) {
const member = remoteState[i];
const localMember = localState[j];

if (member !== localMember) {
if (removals && removals.has(member)) {
// dont increment j because we want to skip this
continue;
}
if (additions && additions.has(localMember)) {
// increment j to skip this localMember
// decrement i to repeat this remoteMember
j++;
i--;
continue;
}
return true;
}

// if we made it here, increment j
j++;
}

return false;
}
80 changes: 56 additions & 24 deletions packages/graph/src/-private/operations/replace-related-records.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,41 +85,73 @@ function replaceRelatedRecordsLocal(graph: Graph, op: ReplaceRelatedRecordsOpera
const wasDirty = relationship.isDirty;
relationship.isDirty = false;

const diff = diffCollection(
identifiers,
relationship,
(identifier) => {
// Since we are diffing against the remote state, we check
// if our previous local state did not contain this identifier
if (removals?.has(identifier) || !additions?.has(identifier)) {
if (type !== identifier.type) {
if (DEBUG) {
assertPolymorphicType(relationship.identifier, relationship.definition, identifier, graph.store);
}
graph.registerPolymorphicType(type, identifier.type);
const onAdd = (identifier: StableRecordIdentifier) => {
// Since we are diffing against the remote state, we check
// if our previous local state did not contain this identifier
const removalsHas = removals?.has(identifier);
if (removalsHas || !additions?.has(identifier)) {
if (type !== identifier.type) {
if (DEBUG) {
assertPolymorphicType(relationship.identifier, relationship.definition, identifier, graph.store);
}
graph.registerPolymorphicType(type, identifier.type);
}

relationship.isDirty = true;
addToInverse(graph, identifier, inverseKey, op.record, isRemote);
relationship.isDirty = true;
addToInverse(graph, identifier, inverseKey, op.record, isRemote);

if (removalsHas) {
removals!.delete(identifier);
}
},
(identifier) => {
// Since we are diffing against the remote state, we check
// if our previous local state had contained this identifier
if (additions?.has(identifier) || !removals?.has(identifier)) {
relationship.isDirty = true;
removeFromInverse(graph, identifier, inverseKey, record, isRemote);
}
};

const onRemove = (identifier: StableRecordIdentifier) => {
// Since we are diffing against the remote state, we check
// if our previous local state had contained this identifier
const additionsHas = additions?.has(identifier);
if (additionsHas || !removals?.has(identifier)) {
relationship.isDirty = true;
removeFromInverse(graph, identifier, inverseKey, record, isRemote);

if (additionsHas) {
additions!.delete(identifier);
}
}
);
};

const diff = diffCollection(identifiers, relationship, onAdd, onRemove);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
let becameDirty = relationship.isDirty || diff.changed;

// any additions no longer in the local state
// need to be removed from the inverse
if (additions && additions.size > 0) {
additions.forEach((identifier) => {
if (!diff.add.has(identifier)) {
becameDirty = true;
onRemove(identifier);
}
});
}

// any removals no longer in the local state
// need to be added back to the inverse
if (removals && removals.size > 0) {
removals.forEach((identifier) => {
if (!diff.del.has(identifier)) {
becameDirty = true;
onAdd(identifier);
}
});
}

const becameDirty = relationship.isDirty || diff.changed;
relationship.additions = diff.add;
relationship.removals = diff.del;
relationship.localState = diff.finalState;
relationship.isDirty = wasDirty;

if (!wasDirty && becameDirty) {
if (!wasDirty /*&& becameDirty // TODO to guard like this we need to detect reorder when diffing local */) {
notifyChange(graph, op.record, op.field);
}
}
Expand Down
Loading

0 comments on commit feb6e5a

Please sign in to comment.