Skip to content

Commit

Permalink
Broadcast watches before transaction when onDirty provided.
Browse files Browse the repository at this point in the history
When an options.onDirty callback is provided to cache.batch (#7819), we
want to call it with only the Cache.WatchOptions objects that were
directly affected by options.transaction, so it's important to broadcast
watches before the transaction, to flush out any pending watches waiting
to be broadcast. See provided tests for an example where this matters.
  • Loading branch information
benjamn committed Mar 26, 2021
1 parent 77a556a commit 49e8c9f
Show file tree
Hide file tree
Showing 2 changed files with 116 additions and 0 deletions.
106 changes: 106 additions & 0 deletions src/cache/inmemory/__tests__/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1502,6 +1502,112 @@ describe('Cache', () => {
abInfo.cancel();
bInfo.cancel();
});

it('does not pass previously invalidated queries to onDirty', () => {
const cache = new InMemoryCache;

const aQuery = gql`query { a }`;
const abQuery = gql`query { a b }`;
const bQuery = gql`query { b }`;

cache.writeQuery({
query: abQuery,
data: {
a: "ay",
b: "bee",
},
});

const aInfo = watch(cache, aQuery);
const abInfo = watch(cache, abQuery);
const bInfo = watch(cache, bQuery);

cache.writeQuery({
query: bQuery,
// Writing this data with broadcast:false queues this update for the
// next broadcast, whenever it happens. If that next broadcast is the
// one triggered by cache.batch, the bQuery broadcast could be
// accidentally intercepted by onDirty, even though the transaction
// does not touch the Query.b field. To solve this problem, the batch
// method calls cache.broadcastWatches() before the transaction, when
// options.onDirty is provided.
broadcast: false,
data: {
b: "beeeee",
},
});

const dirtied = new Map<Cache.WatchOptions, Cache.DiffResult<any>>();

cache.batch({
transaction(cache) {
cache.modify({
fields: {
a(value) {
expect(value).toBe("ay");
return "ayyyy";
},
},
});
},
optimistic: true,
onDirty(watch, diff) {
dirtied.set(watch, diff);
},
});

expect(dirtied.size).toBe(2);
expect(dirtied.has(aInfo.watch)).toBe(true);
expect(dirtied.has(abInfo.watch)).toBe(true);
expect(dirtied.has(bInfo.watch)).toBe(false);

expect(aInfo.diffs).toEqual([
// This diff resulted from the cache.modify call in the cache.batch
// transaction function.
{
complete: true,
result: {
a: "ayyyy",
},
}
]);

expect(abInfo.diffs).toEqual([
// This diff came from the broadcast of cache.writeQuery data before
// the cache.batch transaction, before any onDirty calls.
{
complete: true,
result: {
a: "ay",
b: "beeeee",
},
},
// This diff resulted from the cache.modify call in the cache.batch
// transaction function.
{
complete: true,
result: {
a: "ayyyy",
b: "beeeee",
},
},
]);

expect(bInfo.diffs).toEqual([
// This diff came from the broadcast of cache.writeQuery data before
// the cache.batch transaction, before any onDirty calls.
{
complete: true,
result: {
b: "beeeee",
},
},
]);

aInfo.cancel();
abInfo.cancel();
bInfo.cancel();
});
});

describe('performTransaction', () => {
Expand Down
10 changes: 10 additions & 0 deletions src/cache/inmemory/inMemoryCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,16 @@ export class InMemoryCache extends ApolloCache<NormalizedCacheObject> {
}
};

if (options.onDirty) {
// If an options.onDirty callback is provided, we want to call it with
// only the Cache.WatchOptions objects affected by options.transaction,
// so we broadcast watches first, to clear any pending watches waiting
// to be broadcast.
const { onDirty, ...rest } = options;
// Note that rest is just like options, except with onDirty removed.
this.broadcastWatches(rest);
}

if (typeof optimistic === 'string') {
// Note that there can be multiple layers with the same optimistic ID.
// When removeOptimistic(id) is called for that id, all matching layers
Expand Down

0 comments on commit 49e8c9f

Please sign in to comment.