From ce7cbaf33e30871d248b914742e3011e805fcf0e Mon Sep 17 00:00:00 2001 From: Phil Pluckthun Date: Fri, 11 Oct 2019 17:17:50 +0100 Subject: [PATCH 1/3] Add stale flag to OperationResults --- src/hooks/useMutation.ts | 7 +++++-- src/hooks/useQuery.ts | 10 ++++++++-- src/hooks/useSubscription.ts | 21 +++++++++++---------- src/types.ts | 2 ++ 4 files changed, 26 insertions(+), 14 deletions(-) diff --git a/src/hooks/useMutation.ts b/src/hooks/useMutation.ts index 52f2077c49..ebbfb439d2 100644 --- a/src/hooks/useMutation.ts +++ b/src/hooks/useMutation.ts @@ -7,6 +7,7 @@ import { CombinedError, createRequest } from '../utils'; export interface UseMutationState { fetching: boolean; + stale: boolean; data?: T; error?: CombinedError; extensions?: Record; @@ -27,6 +28,7 @@ export const useMutation = ( const [state, setState] = useState>({ fetching: false, + stale: false, error: undefined, data: undefined, extensions: undefined, @@ -36,6 +38,7 @@ export const useMutation = ( (variables?: V, context?: Partial) => { setState({ fetching: true, + stale: false, error: undefined, data: undefined, extensions: undefined, @@ -47,8 +50,8 @@ export const useMutation = ( client.executeMutation(request, context || {}), toPromise ).then(result => { - const { data, error, extensions } = result; - setState({ fetching: false, data, error, extensions }); + const { stale, data, error, extensions } = result; + setState({ fetching: false, stale: !!stale, data, error, extensions }); return result; }); }, diff --git a/src/hooks/useQuery.ts b/src/hooks/useQuery.ts index f6858edea8..cfb541ccc4 100644 --- a/src/hooks/useQuery.ts +++ b/src/hooks/useQuery.ts @@ -10,6 +10,7 @@ import { useRequest } from './useRequest'; const initialState: UseQueryState = { fetching: false, + stale: false, data: undefined, error: undefined, extensions: undefined, @@ -26,6 +27,7 @@ export interface UseQueryArgs { export interface UseQueryState { fetching: boolean; + stale: boolean; data?: T; error?: CombinedError; extensions?: Record; @@ -70,8 +72,9 @@ export const useQuery = ( fromValue({ fetching: true }), pipe( query$, - map(({ data, error, extensions }) => ({ + map(({ stale, data, error, extensions }) => ({ fetching: false, + stale: !!stale, data, error, extensions, @@ -82,7 +85,10 @@ export const useQuery = ( ]); }), // The individual partial results are merged into each previous result - scan((result, partial) => ({ ...result, ...partial }), initialState) + scan( + (result, partial) => ({ ...result, stale: false, ...partial }), + initialState + ) ), useMemo(() => (args.pause ? null : makeQuery$()), [args.pause, makeQuery$]), initialState diff --git a/src/hooks/useSubscription.ts b/src/hooks/useSubscription.ts index f37b2e5287..7918fd2a37 100644 --- a/src/hooks/useSubscription.ts +++ b/src/hooks/useSubscription.ts @@ -10,6 +10,7 @@ import { OperationContext } from '../types'; const initialState: UseSubscriptionState = { fetching: false, + stale: false, data: undefined, error: undefined, extensions: undefined, @@ -26,6 +27,7 @@ export type SubscriptionHandler = (prev: R | undefined, data: T) => R; export interface UseSubscriptionState { fetching: boolean; + stale: boolean; data?: T; error?: CombinedError; extensions?: Record; @@ -71,8 +73,9 @@ export const useSubscription = ( fromValue({ fetching: true }), pipe( subscription$, - map(({ data, error, extensions }) => ({ + map(({ stale, data, error, extensions }) => ({ fetching: true, + stale: !!stale, data, error, extensions, @@ -86,15 +89,13 @@ export const useSubscription = ( scan((result, partial: any) => { const { current: handler } = handlerRef; // If a handler has been passed, it's used to merge new data in - if (partial.data !== undefined && typeof handler === 'function') { - return { - ...result, - ...partial, - data: handler(result.data, partial.data), - }; - } else { - return { ...result, ...partial }; - } + const data = + partial.data !== undefined + ? typeof handler === 'function' + ? handler(result.data, partial.data) + : partial.data + : result.data; + return { ...result, stale: false, ...partial, data }; }, initialState) ), useMemo(() => (args.pause ? null : makeSubscription$()), [ diff --git a/src/types.ts b/src/types.ts index 49b88b18bc..9c046cea11 100644 --- a/src/types.ts +++ b/src/types.ts @@ -68,6 +68,8 @@ export interface OperationResult { error?: CombinedError; /** Optional extensions return by the Graphql server. */ extensions?: Record; + /** Optional stale flag added by exchanges that return stale results. */ + stale?: boolean; } /** Input parameters for to an Exchange factory function. */ From 7d82e7ff439c41df628fd698f1583ad4dc26e19b Mon Sep 17 00:00:00 2001 From: Phil Pluckthun Date: Fri, 11 Oct 2019 17:18:03 +0100 Subject: [PATCH 2/3] Update tests to include stale flag --- src/hooks/__snapshots__/useMutation.test.tsx.snap | 1 + src/hooks/__snapshots__/useQuery.test.tsx.snap | 1 + src/hooks/__snapshots__/useSubscription.test.tsx.snap | 1 + src/hooks/useQuery.spec.ts | 4 ++++ src/hooks/useSubscription.test.tsx | 7 ++++++- 5 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/hooks/__snapshots__/useMutation.test.tsx.snap b/src/hooks/__snapshots__/useMutation.test.tsx.snap index bc1b8d00fa..e51218fe32 100644 --- a/src/hooks/__snapshots__/useMutation.test.tsx.snap +++ b/src/hooks/__snapshots__/useMutation.test.tsx.snap @@ -6,5 +6,6 @@ Object { "error": undefined, "extensions": undefined, "fetching": false, + "stale": false, } `; diff --git a/src/hooks/__snapshots__/useQuery.test.tsx.snap b/src/hooks/__snapshots__/useQuery.test.tsx.snap index b4db087677..8e42fcf5c3 100644 --- a/src/hooks/__snapshots__/useQuery.test.tsx.snap +++ b/src/hooks/__snapshots__/useQuery.test.tsx.snap @@ -6,5 +6,6 @@ Object { "error": undefined, "extensions": undefined, "fetching": true, + "stale": false, } `; diff --git a/src/hooks/__snapshots__/useSubscription.test.tsx.snap b/src/hooks/__snapshots__/useSubscription.test.tsx.snap index 3202ca5456..bfca60e161 100644 --- a/src/hooks/__snapshots__/useSubscription.test.tsx.snap +++ b/src/hooks/__snapshots__/useSubscription.test.tsx.snap @@ -6,5 +6,6 @@ Object { "error": 5678, "extensions": undefined, "fetching": true, + "stale": false, } `; diff --git a/src/hooks/useQuery.spec.ts b/src/hooks/useQuery.spec.ts index 00f3442efd..f7d3b9d5a9 100644 --- a/src/hooks/useQuery.spec.ts +++ b/src/hooks/useQuery.spec.ts @@ -61,6 +61,8 @@ describe('useQuery', () => { const [state] = result.current; expect(state).toEqual({ fetching: true, + stale: false, + extensions: undefined, error: undefined, data: undefined, }); @@ -126,6 +128,8 @@ describe('useQuery', () => { const [state] = result.current; expect(state).toEqual({ fetching: false, + stale: false, + extensions: undefined, error: 1, data: 0, }); diff --git a/src/hooks/useSubscription.test.tsx b/src/hooks/useSubscription.test.tsx index c3ca90775e..80da947a9a 100644 --- a/src/hooks/useSubscription.test.tsx +++ b/src/hooks/useSubscription.test.tsx @@ -80,7 +80,12 @@ describe('on subscription', () => { * result of the state change. */ wrapper.update(); - expect(state).toEqual({ ...data, fetching: true }); + expect(state).toEqual({ + ...data, + extensions: undefined, + fetching: true, + stale: false, + }); }); }); From 6e087b8fb14f8cd075b7d95f39e4818edb6bbe7c Mon Sep 17 00:00:00 2001 From: Phil Pluckthun Date: Fri, 11 Oct 2019 17:24:03 +0100 Subject: [PATCH 3/3] Add stale:true to cache-and-network results --- src/exchanges/cache.test.ts | 9 ++++++++- src/exchanges/cache.ts | 13 ++++++++----- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/src/exchanges/cache.test.ts b/src/exchanges/cache.test.ts index de0c0b25e4..6b202592c7 100755 --- a/src/exchanges/cache.test.ts +++ b/src/exchanges/cache.test.ts @@ -5,6 +5,7 @@ import { publish, Source, Subject, + forEach, scan, toPromise, } from 'wonka'; @@ -77,9 +78,13 @@ describe('on query', () => { it('respects cache-and-network', () => { const [ops$, next, complete] = input; + const result = jest.fn(); const exchange = cacheExchange(exchangeArgs)(ops$); - publish(exchange); + pipe( + exchange, + forEach(result) + ); next(queryOperation); next({ @@ -93,6 +98,8 @@ describe('on query', () => { complete(); expect(forwardedOperations.length).toBe(1); expect(reexecuteOperation).toHaveBeenCalledTimes(1); + expect(result).toHaveBeenCalledTimes(2); + expect(result.mock.calls[1][0].stale).toBe(true); expect(reexecuteOperation.mock.calls[0][0]).toEqual({ ...queryOperation, diff --git a/src/exchanges/cache.ts b/src/exchanges/cache.ts index 407bfd525a..24babe979b 100755 --- a/src/exchanges/cache.ts +++ b/src/exchanges/cache.ts @@ -57,16 +57,19 @@ export const cacheExchange: Exchange = ({ forward, client }) => { filter(op => !shouldSkip(op) && isOperationCached(op)), map(operation => { const cachedResult = resultCache.get(operation.key); - if (operation.context.requestPolicy === 'cache-and-network') { - reexecuteOperation(client, operation); - } - - return { + const result: OperationResult = { ...cachedResult, operation: addMetadata(operation, { cacheOutcome: cachedResult ? 'hit' : 'miss', }), }; + + if (operation.context.requestPolicy === 'cache-and-network') { + result.stale = true; + reexecuteOperation(client, operation); + } + + return result; }) );