Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

(graphcache) - Implement RFC for optimistic behavior enhancements #750

Merged
merged 20 commits into from
Apr 24, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
ad5e66e
Replace data structure for dependency sets
kitten Apr 24, 2020
a60b2cb
Block refetches for operations that overlap with optimistic updates
kitten Apr 24, 2020
d9d3599
Add cache-and-network for previously blocked ops in executePendingOpe…
kitten Apr 24, 2020
6102cbf
Update Operations Set<number> type in cacheExchange
kitten Apr 24, 2020
22b4bef
Update tests for new Dependencies data structure
kitten Apr 24, 2020
6907ac0
Prevent non-C&N requests from being added to requestedRefetch
kitten Apr 24, 2020
693cba3
Implement buffered prior-optimistic mutation result flushing
kitten Apr 24, 2020
b4f8e63
Fix linting errors
kitten Apr 24, 2020
4c7cc93
Fix starting condition for mutation result flushing
kitten Apr 24, 2020
46edd04
Add changesets
kitten Apr 24, 2020
bc20f8a
Split up optimistic result flush logic to two streams
kitten Apr 24, 2020
e373675
Simplify blocked dependencies
kitten Apr 24, 2020
398254f
Prevent flushed results from clearing non-optimistic results
kitten Apr 24, 2020
78eb449
Ensure consistent return type in isBlockedByOptimisticUpdate
kitten Apr 24, 2020
79ead92
Add test for blocking cache-and-network behaviour
kitten Apr 24, 2020
4bb6f4e
Add test for mutation result batching
kitten Apr 24, 2020
9c4e03b
Add additional cleanup for ops to prepareForwardedOperation
kitten Apr 24, 2020
47fea7d
Batch all pending dependent operations for optimistic mutation result…
kitten Apr 24, 2020
ef2534b
Upgrade husky and fix linting
kitten Apr 24, 2020
2648544
Block forwarded cache-miss operations as well
kitten Apr 24, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/little-chicken-ring.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@urql/exchange-graphcache': minor
---

Implement refetch blocking for queries that are affected by optimistic update. When a query would normally be refetched, either because it was partial or a cache-and-network operation, we now wait if it touched optimistic data for that optimistic mutation to complete. This prevents optimistic update data from unexpectedly disappearing
5 changes: 5 additions & 0 deletions .changeset/seven-onions-check.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@urql/exchange-graphcache': minor
---

Implement optimistic mutation result flushing. Mutation results for mutation that have had optimistic updates will now wait for all optimistic mutations to complete at the same time before being applied to the cache. This sometimes does delay cache updates to until after multiple mutations have completed, but it does prevent optimistic data from being accidentally committed permanently, which is more intuitive.
202 changes: 200 additions & 2 deletions exchanges/graphcache/src/cacheExchange.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -696,6 +696,204 @@ describe('optimistic updates', () => {
expect(result).toHaveBeenCalledTimes(4);
});

it('batches optimistic mutation result application', () => {
jest.useFakeTimers();

const mutation = gql`
mutation {
concealAuthor {
id
name
}
}
`;

const optimisticMutationData = {
__typename: 'Mutation',
concealAuthor: {
__typename: 'Author',
id: '123',
name: '[REDACTED OFFLINE]',
},
};

const mutationData = {
__typename: 'Mutation',
concealAuthor: {
__typename: 'Author',
id: '123',
name: '[REDACTED ONLINE]',
},
};

const client = createClient({ url: 'http://0.0.0.0' });
const { source: ops$, next } = makeSubject<Operation>();

const reexec = jest
.spyOn(client, 'reexecuteOperation')
.mockImplementation(next);

const opOne = client.createRequestOperation('query', {
key: 1,
query: queryOne,
});

const opMutationOne = client.createRequestOperation('mutation', {
key: 2,
query: mutation,
});

const opMutationTwo = client.createRequestOperation('mutation', {
key: 3,
query: mutation,
});

const response = jest.fn(
(forwardOp: Operation): OperationResult => {
if (forwardOp.key === 1) {
return { operation: opOne, data: queryOneData };
} else if (forwardOp.key === 2) {
return { operation: opMutationOne, data: mutationData };
} else if (forwardOp.key === 3) {
return { operation: opMutationTwo, data: mutationData };
}

return undefined as any;
}
);

const result = jest.fn();
const forward: ExchangeIO = ops$ => pipe(ops$, delay(3), map(response));

const optimistic = {
concealAuthor: jest.fn(() => optimisticMutationData.concealAuthor) as any,
};

pipe(
cacheExchange({ optimistic })({ forward, client, dispatchDebug })(ops$),
filter(x => x.operation.operationName === 'mutation'),
tap(result),
publish
);

next(opOne);
jest.runAllTimers();
expect(response).toHaveBeenCalledTimes(1);
expect(result).toHaveBeenCalledTimes(0);

next(opMutationOne);
jest.advanceTimersByTime(1);
next(opMutationTwo);

expect(response).toHaveBeenCalledTimes(1);
expect(optimistic.concealAuthor).toHaveBeenCalledTimes(2);
expect(reexec).toHaveBeenCalledTimes(2);
expect(result).toHaveBeenCalledTimes(0);

jest.advanceTimersByTime(2);
expect(response).toHaveBeenCalledTimes(2);
expect(result).toHaveBeenCalledTimes(0);

jest.runAllTimers();
expect(response).toHaveBeenCalledTimes(3);
expect(result).toHaveBeenCalledTimes(2);
});

it('blocks refetches of overlapping queries', () => {
jest.useFakeTimers();

const mutation = gql`
mutation {
concealAuthor {
id
name
}
}
`;

const optimisticMutationData = {
__typename: 'Mutation',
concealAuthor: {
__typename: 'Author',
id: '123',
name: '[REDACTED OFFLINE]',
},
};

const client = createClient({ url: 'http://0.0.0.0' });
const { source: ops$, next } = makeSubject<Operation>();

const reexec = jest
.spyOn(client, 'reexecuteOperation')
.mockImplementation(next);

const opOne = client.createRequestOperation(
'query',
{
key: 1,
query: queryOne,
},
{
requestPolicy: 'cache-and-network',
}
);

const opMutation = client.createRequestOperation('mutation', {
key: 2,
query: mutation,
});

const response = jest.fn(
(forwardOp: Operation): OperationResult => {
if (forwardOp.key === 1) {
return { operation: opOne, data: queryOneData };
}

return undefined as any;
}
);

const result = jest.fn();
const forward: ExchangeIO = ops$ =>
pipe(
ops$,
delay(1),
filter(x => x.operationName !== 'mutation'),
map(response)
);

const optimistic = {
concealAuthor: jest.fn(() => optimisticMutationData.concealAuthor) as any,
};

pipe(
cacheExchange({ optimistic })({ forward, client, dispatchDebug })(ops$),
tap(result),
publish
);

next(opOne);
jest.runAllTimers();
expect(response).toHaveBeenCalledTimes(1);

next(opMutation);
expect(response).toHaveBeenCalledTimes(1);
expect(optimistic.concealAuthor).toHaveBeenCalledTimes(1);
expect(reexec).toHaveBeenCalledTimes(1);

expect(reexec.mock.calls[0][0]).toHaveProperty(
'context.requestPolicy',
'cache-first'
);

jest.runAllTimers();
expect(response).toHaveBeenCalledTimes(1);

next(opOne);
expect(response).toHaveBeenCalledTimes(1);
expect(reexec).toHaveBeenCalledTimes(1);
});

it('correctly clears on error', () => {
jest.useFakeTimers();

Expand Down Expand Up @@ -1529,7 +1727,7 @@ describe('commutativity', () => {
},
});

expect(reexec).toHaveBeenCalledTimes(1);
expect(reexec).toHaveBeenCalledTimes(3);
expect(data).toHaveProperty('node.name', 'mutation');

nextRes({
Expand All @@ -1544,7 +1742,7 @@ describe('commutativity', () => {
},
});

expect(reexec).toHaveBeenCalledTimes(2);
expect(reexec).toHaveBeenCalledTimes(4);
expect(data).toHaveProperty('node.name', 'mutation');
});

Expand Down
Loading