Skip to content

Commit

Permalink
Allow manually associating caches with reactive variables. (#7350)
Browse files Browse the repository at this point in the history
In the context of a larger cache read operation, accessing a reactive
variable automatically associates the variable with the cache, so future
updates to the variable can be broadcast to the cache.

Outside this context, it may not be possible for the variable to discover
the currently active cache. For example, there is no currently active
cache in asynchronous ApolloLink code (#7338).

Since we recently added a `reactiveVar.forgetCache(cache)` in #7279, I
think it makes sense to have a manual way to do the opposite, which I have
decided to call `reactiveVar.attachCache(cache)`.

Although this attachment is manual at the moment, in a hypothetical future
where the client/cache manage the persistence of reactive variables, it
should be easy to attach the appropriate cache to those variables when
they are first initialized, which would enable reactive updates from
literally anywhere, with no additional effort by the developer.
  • Loading branch information
benjamn committed Nov 20, 2020
1 parent d1f0b96 commit 8a5b8c8
Show file tree
Hide file tree
Showing 4 changed files with 155 additions and 13 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@
- Cancel `queryInfo.notifyTimeout` in `QueryInfo#markResult` to prevent unnecessary network requests when using a `FetchPolicy` of `cache-and-network` or `network-only` in a React component with multiple `useQuery` calls. <br/>
[@benjamn](https://github.com/benjamn) in [#7347](https://github.com/apollographql/apollo-client/pull/7347)

- `ApolloCache` objects (including `InMemoryCache`) may now be associated with or disassociated from individual reactive variables by calling `reactiveVar.attachCache(cache)` and/or `reactiveVar.forgetCache(cache)`. <br/>
[@benjamn](https://github.com/benjamn) in [#7350](https://github.com/apollographql/apollo-client/pull/7350)

## Apollo Client 3.2.7

## Bug Fixes
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
{
"name": "apollo-client",
"path": "./dist/apollo-client.cjs.min.js",
"maxSize": "25.4 kB"
"maxSize": "25.5 kB"
}
],
"peerDependencies": {
Expand Down
128 changes: 127 additions & 1 deletion src/cache/inmemory/__tests__/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2056,7 +2056,6 @@ describe("InMemoryCache#modify", () => {
cache.modify({
fields: {
comments(comments: Reference[], { readField }) {
debugger;
expect(Object.isFrozen(comments)).toBe(true);
expect(comments.length).toBe(3);
const filtered = comments.filter(comment => {
Expand Down Expand Up @@ -2757,6 +2756,133 @@ describe("ReactiveVar and makeVar", () => {
},
]);
});

it('should broadcast to manually added caches', () => {
const rv = makeVar(0);
const cache = new InMemoryCache;
const query = gql`query { value }`;
const diffs: Cache.DiffResult<any>[] = [];
const watch: Cache.WatchOptions = {
query,
optimistic: true,
callback(diff) {
diffs.push(diff);
},
};

cache.writeQuery({
query,
data: {
value: "oyez",
},
});

const cancel = cache.watch(watch);

// This should not trigger a broadcast, since we haven't associated
// this cache with rv yet.
rv(rv() + 1);
expect(diffs).toEqual([]);

// The rv.attachCache method returns rv, for chaining.
rv.attachCache(cache)(rv() + 1);

expect(diffs).toEqual([
{
complete: true,
result: {
value: "oyez",
},
},
]);

cache.writeQuery({
query,
broadcast: false,
data: {
value: "oyez, oyez",
},
});

expect(diffs).toEqual([
{
complete: true,
result: {
value: "oyez",
},
},
]);

rv(rv() + 1);
expect(diffs).toEqual([
{
complete: true,
result: {
value: "oyez",
},
},
{
complete: true,
result: {
value: "oyez, oyez",
},
},
]);

expect(rv.forgetCache(cache)).toBe(true);

cache.writeQuery({
query,
broadcast: false,
data: {
value: "oyez, oyez, oyez",
},
});

// Since we called rv.forgetCache(cache) above, updating rv here
// should not trigger a broadcast.
rv(rv() + 1);
expect(diffs).toEqual([
{
complete: true,
result: {
value: "oyez",
},
},
{
complete: true,
result: {
value: "oyez, oyez",
},
},
]);

cache["broadcastWatches"]();
expect(diffs).toEqual([
{
complete: true,
result: {
value: "oyez",
},
},
{
complete: true,
result: {
value: "oyez, oyez",
},
},
{
complete: true,
result: {
value: "oyez, oyez, oyez",
},
},
]);

cancel();

expect(rv()).toBe(4);
});
});

describe('TypedDocumentNode<Data, Variables>', () => {
Expand Down
35 changes: 24 additions & 11 deletions src/cache/inmemory/reactiveVars.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { ApolloCache } from '../../core';
export interface ReactiveVar<T> {
(newValue?: T): T;
onNextChange(listener: ReactiveListener<T>): () => void;
attachCache(cache: ApolloCache<any>): this;
forgetCache(cache: ApolloCache<any>): boolean;
}

Expand All @@ -22,10 +23,12 @@ export const cacheSlot = new Slot<ApolloCache<any>>();
// to the original elements of the set before we begin iterating. See
// iterateObserversSafely for another example of this pattern.
function consumeAndIterate<T>(set: Set<T>, callback: (item: T) => any) {
const items: T[] = [];
set.forEach(item => items.push(item));
set.clear();
items.forEach(callback);
if (set.size) {
const items: T[] = [];
set.forEach(item => items.push(item));
set.clear();
items.forEach(callback);
}
}

const varsByCache = new WeakMap<ApolloCache<any>, Set<ReactiveVar<any>>>();
Expand Down Expand Up @@ -61,12 +64,7 @@ export function makeVar<T>(value: T): ReactiveVar<T> {
// context via cacheSlot. This isn't entirely foolproof, but it's
// the same system that powers varDep.
const cache = cacheSlot.getValue();
if (cache) {
caches.add(cache);
let vars = varsByCache.get(cache)!;
if (!vars) varsByCache.set(cache, vars = new Set);
vars.add(rv);
}
if (cache) attach(cache);
varDep(rv);
}

Expand All @@ -80,7 +78,22 @@ export function makeVar<T>(value: T): ReactiveVar<T> {
};
};

rv.forgetCache = cache => caches.delete(cache);
const attach = rv.attachCache = cache => {
caches.add(cache);
let vars = varsByCache.get(cache)!;
if (!vars) varsByCache.set(cache, vars = new Set);
vars.add(rv);
return rv;
};

rv.forgetCache = cache => {
const deleted = caches.delete(cache);
if (deleted) {
const vars = varsByCache.get(cache);
if (vars) vars.delete(rv);
}
return deleted;
};

return rv;
}
Expand Down

0 comments on commit 8a5b8c8

Please sign in to comment.