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

Solve issue #7491 by supporting watchQueryOptions.refetchWritePolicy. #7810

Merged
merged 10 commits into from
Mar 25, 2021
41 changes: 21 additions & 20 deletions src/cache/core/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,27 +167,28 @@ export abstract class ApolloCache<TSerialized> implements DataProxy {
});
}

public writeQuery<TData = any, TVariables = any>(
options: Cache.WriteQueryOptions<TData, TVariables>,
): Reference | undefined {
return this.write({
dataId: options.id || 'ROOT_QUERY',
result: options.data,
query: options.query,
variables: options.variables,
broadcast: options.broadcast,
});
public writeQuery<TData = any, TVariables = any>({
id,
data,
...options
}: Cache.WriteQueryOptions<TData, TVariables>): Reference | undefined {
return this.write(Object.assign(options, {
dataId: id || 'ROOT_QUERY',
result: data,
}));
}

public writeFragment<TData = any, TVariables = any>(
options: Cache.WriteFragmentOptions<TData, TVariables>,
): Reference | undefined {
return this.write({
dataId: options.id,
result: options.data,
variables: options.variables,
query: this.getFragmentDoc(options.fragment, options.fragmentName),
broadcast: options.broadcast,
});
public writeFragment<TData = any, TVariables = any>({
id,
data,
fragment,
fragmentName,
...options
}: Cache.WriteFragmentOptions<TData, TVariables>): Reference | undefined {
return this.write(Object.assign(options, {
query: this.getFragmentDoc(fragment, fragmentName),
dataId: id,
result: data,
}));
}
}
5 changes: 3 additions & 2 deletions src/cache/core/types/Cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,11 @@ export namespace Cache {
}

export interface WriteOptions<TResult = any, TVariables = any>
extends DataProxy.Query<TVariables, TResult> {
extends Omit<DataProxy.Query<TVariables, TResult>, "id">,
Omit<DataProxy.WriteOptions<TResult>, "data">
{
dataId?: string;
result: TResult;
broadcast?: boolean;
}

export interface DiffOptions extends ReadOptions {
Expand Down
22 changes: 10 additions & 12 deletions src/cache/core/types/DataProxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,7 @@ export namespace DataProxy {
optimistic?: boolean;
}

export interface WriteQueryOptions<TData, TVariables>
extends Query<TVariables, TData> {
export interface WriteOptions<TData> {
/**
* The data you will be writing to the store.
*/
Expand All @@ -94,20 +93,19 @@ export namespace DataProxy {
* Whether to notify query watchers (default: true).
*/
broadcast?: boolean;
}

export interface WriteFragmentOptions<TData, TVariables>
extends Fragment<TVariables, TData> {
/**
* The data you will be writing to the store.
*/
data: TData;
/**
* Whether to notify query watchers (default: true).
* When true, ignore existing field data rather than merging it with
* incoming data (default: false).
*/
broadcast?: boolean;
overwrite?: boolean;
}

export interface WriteQueryOptions<TData, TVariables>
extends Query<TVariables, TData>, WriteOptions<TData> {}

export interface WriteFragmentOptions<TData, TVariables>
extends Fragment<TVariables, TData>, WriteOptions<TData> {}

export type DiffResult<T> = {
result?: T;
complete?: boolean;
Expand Down
14 changes: 9 additions & 5 deletions src/cache/inmemory/__tests__/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import {
import { EntityStore } from "../entityStore";
import { InMemoryCache } from "../inMemoryCache";
import { StoreReader } from "../readFromStore";
import { StoreWriter, WriteToStoreOptions } from "../writeToStore";
import { StoreWriter } from "../writeToStore";
import { Cache } from "../../../core";

export function defaultNormalizedCacheFactory(
seed?: NormalizedCacheObject,
Expand All @@ -19,11 +20,10 @@ export function defaultNormalizedCacheFactory(
});
}

interface WriteQueryToStoreOptions
extends Omit<WriteToStoreOptions, "store"> {
interface WriteQueryToStoreOptions extends Cache.WriteOptions {
writer: StoreWriter;
store?: NormalizedCache;
}
};

export function readQueryFromStore<T = any>(
reader: StoreReader,
Expand All @@ -43,8 +43,12 @@ export function writeQueryToStore(
store = new EntityStore.Root({
policies: options.writer.cache.policies,
}),
...writeOptions
} = options;
options.writer.writeToStore({ ...options, dataId, store });
options.writer.writeToStore(store, {
...writeOptions,
dataId,
});
return store;
}

Expand Down
2 changes: 1 addition & 1 deletion src/cache/inmemory/__tests__/policies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -769,7 +769,7 @@ describe("type policies", function () {
},
};

function check<TData, TVars>(
function check<TData extends typeof data, TVars>(
query: DocumentNode | TypedDocumentNode<TData, TVars>,
variables?: TVars,
) {
Expand Down
8 changes: 1 addition & 7 deletions src/cache/inmemory/inMemoryCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,13 +148,7 @@ export class InMemoryCache extends ApolloCache<NormalizedCacheObject> {
public write(options: Cache.WriteOptions): Reference | undefined {
try {
++this.txCount;
return this.storeWriter.writeToStore({
store: this.data,
query: options.query,
result: options.result,
dataId: options.dataId,
variables: options.variables,
});
return this.storeWriter.writeToStore(this.data, options);
} finally {
if (!--this.txCount && options.broadcast !== false) {
this.broadcastWatches();
Expand Down
11 changes: 10 additions & 1 deletion src/cache/inmemory/policies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import {
ReadFieldOptions,
CanReadFunction,
} from '../core/types/common';
import { WriteContext } from './writeToStore';

export type TypePolicies = {
[__typename: string]: TypePolicy;
Expand Down Expand Up @@ -784,7 +785,7 @@ export class Policies {
existing: StoreValue,
incoming: StoreValue,
{ field, typename, merge }: MergeInfo,
context: ReadMergeModifyContext,
context: WriteContext,
storage?: StorageType,
) {
if (merge === mergeTrueFn) {
Expand All @@ -802,6 +803,14 @@ export class Policies {
return incoming;
}

// If cache.writeQuery or cache.writeFragment was called with
// options.overwrite set to true, we still call merge functions, but
// the existing data is always undefined, so the merge function will
// not attempt to combine the incoming data with the existing data.
if (context.overwrite) {
existing = void 0;
}

return merge(existing, incoming, makeFieldFunctionOptions(
this,
// Unlike options.readField for read functions, we do not fall
Expand Down
38 changes: 10 additions & 28 deletions src/cache/inmemory/writeToStore.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { SelectionSetNode, FieldNode, DocumentNode } from 'graphql';
import { SelectionSetNode, FieldNode } from 'graphql';
import { invariant, InvariantError } from 'ts-invariant';
import { equal } from '@wry/equality';

Expand Down Expand Up @@ -27,6 +27,7 @@ import { makeProcessedFieldsMerger, fieldNameFromStoreName, storeValueIsStoreObj
import { StoreReader } from './readFromStore';
import { InMemoryCache } from './inMemoryCache';
import { EntityStore } from './entityStore';
import { Cache } from '../../core';

export interface WriteContext extends ReadMergeModifyContext {
readonly written: {
Expand All @@ -35,6 +36,8 @@ export interface WriteContext extends ReadMergeModifyContext {
readonly fragmentMap?: FragmentMap;
// General-purpose deep-merge function for use during writes.
merge<T>(existing: T, incoming: T): T;
// If true, merge functions will be called with undefined existing data.
overwrite: boolean;
};

interface ProcessSelectionSetOptions {
Expand All @@ -45,41 +48,19 @@ interface ProcessSelectionSetOptions {
mergeTree: MergeTree;
}

export interface WriteToStoreOptions {
query: DocumentNode;
result: Object;
dataId?: string;
store: NormalizedCache;
variables?: Object;
}

export class StoreWriter {
constructor(
public readonly cache: InMemoryCache,
private reader?: StoreReader,
) {}

/**
* Writes the result of a query to the store.
*
* @param result The result object returned for the query document.
*
* @param query The query document whose result we are writing to the store.
*
* @param store The {@link NormalizedCache} used by Apollo for the `data` portion of the store.
*
* @param variables A map from the name of a variable to its value. These variables can be
* referenced by the query document.
*
* @return A `Reference` to the written object.
*/
public writeToStore({
public writeToStore(store: NormalizedCache, {
query,
result,
dataId,
store,
variables,
}: WriteToStoreOptions): Reference | undefined {
overwrite,
}: Cache.WriteOptions): Reference | undefined {
const operationDefinition = getOperationDefinition(query)!;
const merger = makeProcessedFieldsMerger();

Expand All @@ -102,6 +83,7 @@ export class StoreWriter {
variables,
varString: JSON.stringify(variables),
fragmentMap: createFragmentMap(getFragmentDefinitions(query)),
overwrite: !!overwrite,
},
});

Expand Down Expand Up @@ -286,7 +268,7 @@ export class StoreWriter {
incomingFields = this.applyMerges(mergeTree, entityRef, incomingFields, context);
}

if (process.env.NODE_ENV !== "production") {
if (process.env.NODE_ENV !== "production" && !context.overwrite) {
Comment on lines -289 to +271
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change means you won't see Cache data may be lost... warnings when writing to the cache with overwrite: true, since we can assume overwriting is the behavior you want.

const hasSelectionSet = (storeFieldName: string) =>
fieldsWithSelectionSets.has(fieldNameFromStoreName(storeFieldName));
const fieldsWithSelectionSets = new Set<string>();
Expand Down Expand Up @@ -360,7 +342,7 @@ export class StoreWriter {
mergeTree: MergeTree,
existing: StoreValue,
incoming: T,
context: ReadMergeModifyContext,
context: WriteContext,
getStorageArgs?: Parameters<EntityStore["getStorage"]>,
): T {
if (mergeTree.map.size && !isReference(incoming)) {
Expand Down
11 changes: 9 additions & 2 deletions src/core/QueryInfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ export type QueryStoreValue = Pick<QueryInfo,
| "graphQLErrors"
>;

export const enum CacheWriteBehavior {
FORBID,
OVERWRITE,
MERGE,
};

const destructiveMethodCounts = new (
canUseWeakMap ? WeakMap : Map
)<ApolloCache<any>, number>();
Expand Down Expand Up @@ -307,7 +313,7 @@ export class QueryInfo {
| "variables"
| "fetchPolicy"
| "errorPolicy">,
allowCacheWrite: boolean,
cacheWriteBehavior: CacheWriteBehavior,
) {
this.graphQLErrors = isNonEmptyArray(result.errors) ? result.errors : [];

Expand All @@ -318,7 +324,7 @@ export class QueryInfo {
if (options.fetchPolicy === 'no-cache') {
this.diff = { result: result.data, complete: true };

} else if (!this.stopped && allowCacheWrite) {
} else if (!this.stopped && cacheWriteBehavior !== CacheWriteBehavior.FORBID) {
if (shouldWriteResult(result, options.errorPolicy)) {
// Using a transaction here so we have a chance to read the result
// back from the cache before the watch callback fires as a result
Expand All @@ -330,6 +336,7 @@ export class QueryInfo {
query: this.document!,
data: result.data as T,
variables: options.variables,
overwrite: cacheWriteBehavior === CacheWriteBehavior.OVERWRITE,
});

this.lastWrite = {
Expand Down
Loading