Skip to content

Commit

Permalink
fix(graphcache): Re-enable offlineExchange issuing non-cache request …
Browse files Browse the repository at this point in the history
…policies (#3308)
  • Loading branch information
kitten authored Jul 19, 2023
1 parent 76ad619 commit 7ddccc1
Show file tree
Hide file tree
Showing 6 changed files with 93 additions and 65 deletions.
5 changes: 5 additions & 0 deletions .changeset/eleven-snakes-look.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@urql/exchange-graphcache': patch
---

Allow `offlineExchange` to once again issue all request policies, instead of mapping them to `cache-first`. When replaying operations after rehydrating it will now prioritise network policies, and before rehydrating receiving a network result will prevent a network request from being issued again.
6 changes: 6 additions & 0 deletions .changeset/two-ants-relate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@urql/exchange-graphcache': patch
'@urql/core': patch
---

Add `OperationContext.optimistic` flag as an internal indication on whether a mutation triggered an optimistic update in `@urql/exchange-graphcache`'s `cacheExchange`.
8 changes: 7 additions & 1 deletion exchanges/graphcache/src/cacheExchange.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
makeOperation,
Operation,
OperationResult,
OperationContext,
RequestPolicy,
CacheOutcome,
} from '@urql/core';
Expand Down Expand Up @@ -144,6 +145,7 @@ export const cacheExchange =

// This registers queries with the data layer to ensure commutativity
const prepareForwardedOperation = (operation: Operation) => {
let context: Partial<OperationContext> | undefined;
if (operation.kind === 'query') {
// Pre-reserve the position of the result layer
reserveLayer(store.data, operation.key);
Expand All @@ -155,6 +157,7 @@ export const cacheExchange =
reexecutingOperations.delete(operation.key);
// Mark operation layer as done
noopDataState(store.data, operation.key);
return operation;
} else if (
operation.kind === 'mutation' &&
operation.context.requestPolicy !== 'network-only'
Expand All @@ -175,6 +178,9 @@ export const cacheExchange =
const pendingOperations: Operations = new Set();
collectPendingOperations(pendingOperations, dependencies);
executePendingOperations(operation, pendingOperations, true);

// Mark operation as optimistic
context = { optimistic: true };
}
}

Expand All @@ -190,7 +196,7 @@ export const cacheExchange =
)
: operation.variables,
},
operation.context
{ ...operation.context, ...context }
);
};

Expand Down
101 changes: 38 additions & 63 deletions exchanges/graphcache/src/offlineExchange.ts
Original file line number Diff line number Diff line change
@@ -1,63 +1,28 @@
import { pipe, share, merge, makeSubject, filter, onPush } from 'wonka';
import { SelectionNode } from '@0no-co/graphql.web';

import {
Operation,
OperationResult,
Exchange,
ExchangeIO,
CombinedError,
RequestPolicy,
stringifyDocument,
createRequest,
makeOperation,
} from '@urql/core';

import {
getMainOperation,
getFragments,
isInlineFragment,
isFieldNode,
shouldInclude,
getSelectionSet,
getName,
} from './ast';

import {
SerializedRequest,
OptimisticMutationConfig,
Variables,
CacheExchangeOpts,
StorageAdapter,
} from './types';
import { SerializedRequest, CacheExchangeOpts, StorageAdapter } from './types';

import { cacheExchange } from './cacheExchange';
import { toRequestPolicy } from './helpers/operation';

/** Determines whether a given query contains an optimistic mutation field */
const isOptimisticMutation = <T extends OptimisticMutationConfig>(
config: T,
operation: Operation
) => {
const vars: Variables = operation.variables || {};
const fragments = getFragments(operation.query);
const selections = [...getSelectionSet(getMainOperation(operation.query))];

let field: void | SelectionNode;
while ((field = selections.pop())) {
if (!shouldInclude(field, vars)) {
continue;
} else if (!isFieldNode(field)) {
const fragmentNode = !isInlineFragment(field)
? fragments[getName(field)]
: field;
if (fragmentNode) selections.push(...getSelectionSet(fragmentNode));
} else if (config[getName(field)]) {
return true;
}
}

return false;
};
const policyLevel = {
'cache-only': 0,
'cache-first': 1,
'network-only': 2,
'cache-and-network': 3,
} as const;

/** Input parameters for the {@link offlineExchange}.
* @remarks
Expand Down Expand Up @@ -126,7 +91,6 @@ export const offlineExchange =
) {
const { forward: outerForward, client, dispatchDebug } = input;
const { source: reboundOps$, next } = makeSubject<Operation>();
const optimisticMutations = opts.optimistic || {};
const failedQueue: Operation[] = [];
let hasRehydrated = false;
let isFlushingQueue = false;
Expand All @@ -148,23 +112,35 @@ export const offlineExchange =
}
};

const filterQueue = (key: number) => {
for (let i = failedQueue.length - 1; i >= 0; i--)
if (failedQueue[i].key === key) failedQueue.splice(i, 1);
};

const flushQueue = () => {
if (!isFlushingQueue) {
isFlushingQueue = true;

const sent = new Set<number>();
isFlushingQueue = true;
for (let i = 0; i < failedQueue.length; i++) {
const operation = failedQueue[i];
if (operation.kind === 'mutation' || !sent.has(operation.key)) {
if (operation.kind !== 'subscription')
next(makeOperation('teardown', operation));
sent.add(operation.key);
next(toRequestPolicy(operation, 'cache-first'));
if (operation.kind !== 'subscription') {
next(makeOperation('teardown', operation));
let overridePolicy: RequestPolicy = 'cache-first';
for (let i = 0; i < failedQueue.length; i++) {
const { requestPolicy } = failedQueue[i].context;
if (policyLevel[requestPolicy] > policyLevel[overridePolicy])
overridePolicy = requestPolicy;
}
next(toRequestPolicy(operation, overridePolicy));
} else {
next(toRequestPolicy(operation, 'cache-first'));
}
}
}

failedQueue.length = 0;
isFlushingQueue = false;
failedQueue.length = 0;
updateMetadata();
}
};
Expand All @@ -176,8 +152,8 @@ export const offlineExchange =
if (
hasRehydrated &&
res.operation.kind === 'mutation' &&
isOfflineError(res.error, res) &&
isOptimisticMutation(optimisticMutations, res.operation)
res.operation.context.optimistic &&
isOfflineError(res.error, res)
) {
failedQueue.push(res.operation);
updateMetadata();
Expand Down Expand Up @@ -231,9 +207,7 @@ export const offlineExchange =
if (operation.kind === 'query' && !hasRehydrated) {
failedQueue.push(operation);
} else if (operation.kind === 'teardown') {
for (let i = failedQueue.length - 1; i >= 0; i--)
if (failedQueue[i].key === operation.key)
failedQueue.splice(i, 1);
filterQueue(operation.key);
}
})
),
Expand All @@ -242,13 +216,14 @@ export const offlineExchange =
return pipe(
cacheResults$(opsAndRebound$),
filter(res => {
if (
res.operation.kind === 'query' &&
isOfflineError(res.error, res)
) {
next(toRequestPolicy(res.operation, 'cache-only'));
failedQueue.push(res.operation);
return false;
if (res.operation.kind === 'query') {
if (isOfflineError(res.error, res)) {
next(toRequestPolicy(res.operation, 'cache-only'));
failedQueue.push(res.operation);
return false;
} else if (!hasRehydrated) {
filterQueue(res.operation.key);
}
}
return true;
})
Expand Down
31 changes: 30 additions & 1 deletion exchanges/graphcache/src/test-utils/examples-1.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -534,14 +534,43 @@ it('correctly resolves optimistic updates on Relay schemas', () => {
`;

write(store, { query: getRoot }, queryData);
writeOptimistic(store, { query: updateItem, variables: { id: '2' } }, 1);
const { dependencies } = writeOptimistic(
store,
{ query: updateItem, variables: { id: '2' } },
1
);
expect(dependencies.size).not.toBe(0);
InMemoryData.noopDataState(store.data, 1);
const queryRes = query(store, { query: getRoot });

expect(queryRes.partial).toBe(false);
expect(queryRes.data).not.toBe(null);
});

it('skips non-optimistic mutation fields on writes', () => {
const store = new Store();

const updateItem = gql`
mutation UpdateItem($id: ID!) {
updateItem(id: $id) {
__typename
item {
__typename
id
name
}
}
}
`;

const { dependencies } = writeOptimistic(
store,
{ query: updateItem, variables: { id: '2' } },
1
);
expect(dependencies.size).toBe(0);
});

it('allows cumulative optimistic updates', () => {
let counter = 1;

Expand Down
7 changes: 7 additions & 0 deletions packages/core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -528,6 +528,13 @@ export interface OperationContext {
* @see {@link https://beta.reactjs.org/blog/2022/03/29/react-v18#new-suspense-features} for more information on React Suspense.
*/
suspense?: boolean;
/** A metdata flag indicating whether this operation triggered optimistic updates.
*
* @remarks
* This configuration flag is reserved for `@urql/exchange-graphcache` and is flipped
* when an operation triggerd optimistic updates.
*/
optimistic?: boolean;
[key: string]: any;
}

Expand Down

0 comments on commit 7ddccc1

Please sign in to comment.