diff --git a/CHANGELOG.md b/CHANGELOG.md index 009b8c57dd2..34563322234 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,9 @@ [@PowerKiKi](https://github.com/PowerKiKi) in [#3692](https://github.com/apollographql/apollo-client/pull/3692) - Corrected `ApolloClient.queryManager` typing as it may be `undefined`. <br/> [@danilobuerger](https://github.com/danilobuerger) in [#3661](https://github.com/apollographql/apollo-client/pull/3661) +- Make sure using a `no-cache` fetch policy with subscriptions prevents data + from being cached. <br/> + [@hwillson](https://github.com/hwillson) in [#3773](https://github.com/apollographql/apollo-client/pull/3773) - Documentation updates. <br/> [@hwillson](https://github.com/hwillson) in [#3750](https://github.com/apollographql/apollo-client/pull/3750) <br/> [@hwillson](https://github.com/hwillson) in [#3754](https://github.com/apollographql/apollo-client/pull/3754) <br/> diff --git a/packages/apollo-cache-inmemory/src/mapCache.ts b/packages/apollo-cache-inmemory/src/mapCache.ts index f8ac715e88b..35c00823d10 100644 --- a/packages/apollo-cache-inmemory/src/mapCache.ts +++ b/packages/apollo-cache-inmemory/src/mapCache.ts @@ -5,22 +5,28 @@ import { NormalizedCache, NormalizedCacheObject, StoreObject } from './types'; * Note that you need a polyfill for Object.entries for this to work. */ export class MapCache implements NormalizedCache { - cache: Map<string, StoreObject>; + private cache: Map<string, StoreObject>; + constructor(data: NormalizedCacheObject = {}) { this.cache = new Map(Object.entries(data)); } - get(dataId: string): StoreObject { + + public get(dataId: string): StoreObject { return this.cache.get(`${dataId}`); } - set(dataId: string, value: StoreObject): void { + + public set(dataId: string, value: StoreObject): void { this.cache.set(`${dataId}`, value); } - delete(dataId: string): void { + + public delete(dataId: string): void { this.cache.delete(`${dataId}`); } - clear(): void { + + public clear(): void { return this.cache.clear(); } + public toObject(): NormalizedCacheObject { const obj: NormalizedCacheObject = {}; this.cache.forEach((dataId, key) => { @@ -28,6 +34,7 @@ export class MapCache implements NormalizedCache { }); return obj; } + public replace(newData: NormalizedCacheObject): void { this.cache.clear(); Object.entries(newData).forEach(([dataId, value]) => diff --git a/packages/apollo-cache-inmemory/src/recordingCache.ts b/packages/apollo-cache-inmemory/src/recordingCache.ts index 518fbae6ea3..a71249ef8cb 100644 --- a/packages/apollo-cache-inmemory/src/recordingCache.ts +++ b/packages/apollo-cache-inmemory/src/recordingCache.ts @@ -1,10 +1,10 @@ import { NormalizedCache, NormalizedCacheObject, StoreObject } from './types'; export class RecordingCache implements NormalizedCache { - constructor(private readonly data: NormalizedCacheObject = {}) {} - private recordedData: NormalizedCacheObject = {}; + constructor(private readonly data: NormalizedCacheObject = {}) {} + public record( transaction: (recordingCache: RecordingCache) => void, ): NormalizedCacheObject { diff --git a/packages/apollo-client/src/__tests__/graphqlSubscriptions.ts b/packages/apollo-client/src/__tests__/graphqlSubscriptions.ts index c5bc1cbefc9..89a70da8aff 100644 --- a/packages/apollo-client/src/__tests__/graphqlSubscriptions.ts +++ b/packages/apollo-client/src/__tests__/graphqlSubscriptions.ts @@ -1,190 +1,212 @@ -import gql from 'graphql-tag'; -import { InMemoryCache } from 'apollo-cache-inmemory'; - -import { mockObservableLink, MockedSubscription } from '../__mocks__/mockLinks'; - -import ApolloClient from '../'; - -import { QueryManager } from '../core/QueryManager'; -import { DataStore } from '../data/store'; - -describe('GraphQL Subscriptions', () => { - const results = [ - 'Dahivat Pandya', - 'Vyacheslav Kim', - 'Changping Chen', - 'Amanda Liu', - ].map(name => ({ result: { data: { user: { name } } }, delay: 10 })); - - let sub1: MockedSubscription; - let options: any; - let defaultOptions: any; - let defaultSub1: MockedSubscription; - beforeEach(() => { - sub1 = { - request: { - query: gql` - subscription UserInfo($name: String) { - user(name: $name) { - name - } - } - `, - variables: { - name: 'Changping Chen', - }, - }, - }; - - options = { - query: gql` - subscription UserInfo($name: String) { - user(name: $name) { - name - } - } - `, - variables: { - name: 'Changping Chen', - }, - }; - - defaultSub1 = { - request: { - query: gql` - subscription UserInfo($name: String = "Changping Chen") { - user(name: $name) { - name - } - } - `, - variables: { - name: 'Changping Chen', - }, - }, - }; - - defaultOptions = { - query: gql` - subscription UserInfo($name: String = "Changping Chen") { - user(name: $name) { - name - } - } - `, - }; - }); - - it('should start a subscription on network interface and unsubscribe', done => { - const link = mockObservableLink(defaultSub1); - // This test calls directly through Apollo Client - const client = new ApolloClient({ - link, - cache: new InMemoryCache({ addTypename: false }), - }); - - let count = 0; - const sub = client.subscribe(defaultOptions).subscribe({ - next(result) { - count++; - expect(result).toEqual(results[0].result); - - // Test unsubscribing - if (count > 1) { - throw new Error('next fired after unsubscribing'); - } - sub.unsubscribe(); - done(); - }, - }); - - link.simulateResult(results[0]); - }); - - it('should subscribe with default values', done => { - const link = mockObservableLink(sub1); - // This test calls directly through Apollo Client - const client = new ApolloClient({ - link, - cache: new InMemoryCache({ addTypename: false }), - }); - - let count = 0; - const sub = client.subscribe(options).subscribe({ - next(result) { - expect(result).toEqual(results[0].result); - - // Test unsubscribing - if (count > 1) { - throw new Error('next fired after unsubscribing'); - } - sub.unsubscribe(); - - done(); - }, - }); - - link.simulateResult(results[0]); - }); - - it('should multiplex subscriptions', done => { - const link = mockObservableLink(sub1); - const queryManager = new QueryManager({ - link, - store: new DataStore(new InMemoryCache({ addTypename: false })), - }); - - const obs = queryManager.startGraphQLSubscription(options); - - let counter = 0; - - // tslint:disable-next-line - obs.subscribe({ - next(result) { - expect(result).toEqual(results[0].result); - counter++; - if (counter === 2) { - done(); - } - }, - }) as any; - - // Subscribe again. Should also receive the same result. - // tslint:disable-next-line - obs.subscribe({ - next(result) { - expect(result).toEqual(results[0].result); - counter++; - if (counter === 2) { - done(); - } - }, - }) as any; - - link.simulateResult(results[0]); - }); - - it('should receive multiple results for a subscription', done => { - const link = mockObservableLink(sub1); - let numResults = 0; - const queryManager = new QueryManager({ - link, - store: new DataStore(new InMemoryCache({ addTypename: false })), - }); - - // tslint:disable-next-line - queryManager.startGraphQLSubscription(options).subscribe({ - next(result) { - expect(result).toEqual(results[numResults].result); - numResults++; - if (numResults === 4) { - done(); - } - }, - }) as any; - - for (let i = 0; i < 4; i++) { - link.simulateResult(results[i]); - } - }); -}); +import gql from 'graphql-tag'; +import { InMemoryCache } from 'apollo-cache-inmemory'; + +import { mockObservableLink, MockedSubscription } from '../__mocks__/mockLinks'; + +import ApolloClient from '../'; + +import { QueryManager } from '../core/QueryManager'; +import { DataStore } from '../data/store'; + +describe('GraphQL Subscriptions', () => { + const results = [ + 'Dahivat Pandya', + 'Vyacheslav Kim', + 'Changping Chen', + 'Amanda Liu', + ].map(name => ({ result: { data: { user: { name } } }, delay: 10 })); + + let sub1: MockedSubscription; + let options: any; + let defaultOptions: any; + let defaultSub1: MockedSubscription; + beforeEach(() => { + sub1 = { + request: { + query: gql` + subscription UserInfo($name: String) { + user(name: $name) { + name + } + } + `, + variables: { + name: 'Changping Chen', + }, + }, + }; + + options = { + query: gql` + subscription UserInfo($name: String) { + user(name: $name) { + name + } + } + `, + variables: { + name: 'Changping Chen', + }, + }; + + defaultSub1 = { + request: { + query: gql` + subscription UserInfo($name: String = "Changping Chen") { + user(name: $name) { + name + } + } + `, + variables: { + name: 'Changping Chen', + }, + }, + }; + + defaultOptions = { + query: gql` + subscription UserInfo($name: String = "Changping Chen") { + user(name: $name) { + name + } + } + `, + }; + }); + + it('should start a subscription on network interface and unsubscribe', done => { + const link = mockObservableLink(defaultSub1); + // This test calls directly through Apollo Client + const client = new ApolloClient({ + link, + cache: new InMemoryCache({ addTypename: false }), + }); + + let count = 0; + const sub = client.subscribe(defaultOptions).subscribe({ + next(result) { + count++; + expect(result).toEqual(results[0].result); + + // Test unsubscribing + if (count > 1) { + throw new Error('next fired after unsubscribing'); + } + sub.unsubscribe(); + done(); + }, + }); + + link.simulateResult(results[0]); + }); + + it('should subscribe with default values', done => { + const link = mockObservableLink(sub1); + // This test calls directly through Apollo Client + const client = new ApolloClient({ + link, + cache: new InMemoryCache({ addTypename: false }), + }); + + let count = 0; + const sub = client.subscribe(options).subscribe({ + next(result) { + expect(result).toEqual(results[0].result); + + // Test unsubscribing + if (count > 1) { + throw new Error('next fired after unsubscribing'); + } + sub.unsubscribe(); + + done(); + }, + }); + + link.simulateResult(results[0]); + }); + + it('should multiplex subscriptions', done => { + const link = mockObservableLink(sub1); + const queryManager = new QueryManager({ + link, + store: new DataStore(new InMemoryCache({ addTypename: false })), + }); + + const obs = queryManager.startGraphQLSubscription(options); + + let counter = 0; + + // tslint:disable-next-line + obs.subscribe({ + next(result) { + expect(result).toEqual(results[0].result); + counter++; + if (counter === 2) { + done(); + } + }, + }) as any; + + // Subscribe again. Should also receive the same result. + // tslint:disable-next-line + obs.subscribe({ + next(result) { + expect(result).toEqual(results[0].result); + counter++; + if (counter === 2) { + done(); + } + }, + }) as any; + + link.simulateResult(results[0]); + }); + + it('should receive multiple results for a subscription', done => { + const link = mockObservableLink(sub1); + let numResults = 0; + const queryManager = new QueryManager({ + link, + store: new DataStore(new InMemoryCache({ addTypename: false })), + }); + + // tslint:disable-next-line + queryManager.startGraphQLSubscription(options).subscribe({ + next(result) { + expect(result).toEqual(results[numResults].result); + numResults++; + if (numResults === 4) { + done(); + } + }, + }) as any; + + for (let i = 0; i < 4; i++) { + link.simulateResult(results[i]); + } + }); + + it('should not cache subscription data if a `no-cache` fetch policy is used', done => { + const link = mockObservableLink(sub1); + const cache = new InMemoryCache({ addTypename: false }); + const client = new ApolloClient({ + link, + cache, + }); + + expect(cache.extract()).toEqual({}); + + options.fetchPolicy = 'no-cache'; + const sub = client.subscribe(options).subscribe({ + next() { + expect(cache.extract()).toEqual({}); + sub.unsubscribe(); + done(); + }, + }); + + link.simulateResult(results[0]); + }); +}); diff --git a/packages/apollo-client/src/core/QueryManager.ts b/packages/apollo-client/src/core/QueryManager.ts index ff0fe886912..df885e0c195 100644 --- a/packages/apollo-client/src/core/QueryManager.ts +++ b/packages/apollo-client/src/core/QueryManager.ts @@ -859,29 +859,6 @@ export class QueryManager<TStore> { }); } - private getObservableQueryPromises( - includeStandby?: boolean, - ): Promise<ApolloQueryResult<any>>[] { - const observableQueryPromises: Promise<ApolloQueryResult<any>>[] = []; - this.queries.forEach(({ observableQuery }, queryId) => { - if (!observableQuery) return; - const fetchPolicy = observableQuery.options.fetchPolicy; - - observableQuery.resetLastResults(); - if ( - fetchPolicy !== 'cache-only' && - (includeStandby || fetchPolicy !== 'standby') - ) { - observableQueryPromises.push(observableQuery.refetch()); - } - - this.setQuery(queryId, () => ({ newData: null })); - this.invalidate(true, queryId); - }); - - return observableQueryPromises; - } - public reFetchObservableQueries( includeStandby?: boolean, ): Promise<ApolloQueryResult<any>[]> { @@ -913,6 +890,9 @@ export class QueryManager<TStore> { options: SubscriptionOptions, ): Observable<any> { const { query } = options; + const isCacheEnabled = !( + options.fetchPolicy && options.fetchPolicy === 'no-cache' + ); const cache = this.dataStore.getCache(); let transformedDoc = cache.transformDocument(query); @@ -928,20 +908,25 @@ export class QueryManager<TStore> { return new Observable(observer => { observers.push(observer); - // If this is the first observer, actually initiate the network subscription + // If this is the first observer, actually initiate the network + // subscription. if (observers.length === 1) { const handler = { next: (result: FetchResult) => { - this.dataStore.markSubscriptionResult( - result, - transformedDoc, - variables, - ); - this.broadcastQueries(); + if (isCacheEnabled) { + this.dataStore.markSubscriptionResult( + result, + transformedDoc, + variables, + ); + this.broadcastQueries(); + } - // It's slightly awkward that the data for subscriptions doesn't come from the store. + // It's slightly awkward that the data for subscriptions doesn't + // come from the store. observers.forEach(obs => { - // XXX I'd prefer a different way to handle errors for subscriptions + // XXX I'd prefer a different way to handle errors for + // subscriptions. if (obs.next) obs.next(result); }); }, @@ -1055,6 +1040,29 @@ export class QueryManager<TStore> { }); } + private getObservableQueryPromises( + includeStandby?: boolean, + ): Promise<ApolloQueryResult<any>>[] { + const observableQueryPromises: Promise<ApolloQueryResult<any>>[] = []; + this.queries.forEach(({ observableQuery }, queryId) => { + if (!observableQuery) return; + const fetchPolicy = observableQuery.options.fetchPolicy; + + observableQuery.resetLastResults(); + if ( + fetchPolicy !== 'cache-only' && + (includeStandby || fetchPolicy !== 'standby') + ) { + observableQueryPromises.push(observableQuery.refetch()); + } + + this.setQuery(queryId, () => ({ newData: null })); + this.invalidate(true, queryId); + }); + + return observableQueryPromises; + } + // Takes a request id, query id, a query document and information associated with the query // and send it to the network interface. Returns // a promise for the result associated with that request. diff --git a/packages/apollo-client/src/core/watchQueryOptions.ts b/packages/apollo-client/src/core/watchQueryOptions.ts index addf271d707..441d00d0e99 100644 --- a/packages/apollo-client/src/core/watchQueryOptions.ts +++ b/packages/apollo-client/src/core/watchQueryOptions.ts @@ -143,6 +143,11 @@ export interface SubscriptionOptions<TVariables = OperationVariables> { * GraphQL document to that variable's value. */ variables?: TVariables; + + /** + * Specifies the {@link FetchPolicy} to be used for this subscription. + */ + fetchPolicy?: FetchPolicy; } export type RefetchQueryDescription = Array<string | PureQueryOptions>; diff --git a/packages/apollo-client/src/util/subscribeAndCount.ts b/packages/apollo-client/src/util/subscribeAndCount.ts index 23685547eef..f38872b0a3c 100644 --- a/packages/apollo-client/src/util/subscribeAndCount.ts +++ b/packages/apollo-client/src/util/subscribeAndCount.ts @@ -3,7 +3,7 @@ import { ApolloQueryResult } from '../../src/core/types'; import { Subscription } from '../../src/util/Observable'; export default function subscribeAndCount( - done: (...args) => void, + done: jest.DoneCallback, observable: ObservableQuery<any>, cb: (handleCount: number, result: ApolloQueryResult<any>) => any, ): Subscription { diff --git a/packages/apollo-client/src/util/wrap.ts b/packages/apollo-client/src/util/wrap.ts index bfe752308d7..a50387071c9 100644 --- a/packages/apollo-client/src/util/wrap.ts +++ b/packages/apollo-client/src/util/wrap.ts @@ -1,6 +1,6 @@ // I'm not sure why mocha doesn't provide something like this, you can't // always use promises -export default (done: (...args) => void, cb: (...args: any[]) => any) => ( +export default (done: jest.DoneCallback, cb: (...args: any[]) => any) => ( ...args: any[] ) => { try { diff --git a/packages/apollo-client/tsconfig.json b/packages/apollo-client/tsconfig.json index 1b00f4438f4..42d347c8f0a 100644 --- a/packages/apollo-client/tsconfig.json +++ b/packages/apollo-client/tsconfig.json @@ -10,9 +10,13 @@ "strictNullChecks": true, "noUnusedParameters": true }, + "include": ["src/**/*.ts"], + "exclude": [ + "**/__tests__/**/*", + "**/__mocks__/**/*" + ], "files": [ "node_modules/typescript/lib/lib.es2015.d.ts", - "node_modules/typescript/lib/lib.dom.d.ts", - "src/index.ts" + "node_modules/typescript/lib/lib.dom.d.ts" ] } diff --git a/packages/apollo-utilities/src/getFromAST.ts b/packages/apollo-utilities/src/getFromAST.ts index 1db77ac1fbf..3db665b514e 100644 --- a/packages/apollo-utilities/src/getFromAST.ts +++ b/packages/apollo-utilities/src/getFromAST.ts @@ -192,16 +192,18 @@ export function getDefaultValues( ) { const defaultValues = definition.variableDefinitions .filter(({ defaultValue }) => defaultValue) - .map(({ variable, defaultValue }): { [key: string]: JsonValue } => { - const defaultValueObj: { [key: string]: JsonValue } = {}; - valueToObjectRepresentation( - defaultValueObj, - variable.name, - defaultValue as ValueNode, - ); - - return defaultValueObj; - }); + .map( + ({ variable, defaultValue }): { [key: string]: JsonValue } => { + const defaultValueObj: { [key: string]: JsonValue } = {}; + valueToObjectRepresentation( + defaultValueObj, + variable.name, + defaultValue as ValueNode, + ); + + return defaultValueObj; + }, + ); return assign({}, ...defaultValues); } diff --git a/packages/graphql-anywhere/src/graphql-async.ts b/packages/graphql-anywhere/src/graphql-async.ts index f20b494ef48..f9051b33e4b 100644 --- a/packages/graphql-anywhere/src/graphql-async.ts +++ b/packages/graphql-anywhere/src/graphql-async.ts @@ -42,7 +42,6 @@ import { * but below is an exported alternative that is async. * In the 5.0 version, this will be the only export again * and it will be async - * */ export function graphql( resolver: Resolver, diff --git a/packages/graphql-anywhere/src/graphql.ts b/packages/graphql-anywhere/src/graphql.ts index 352b31b3614..da535d5ea72 100644 --- a/packages/graphql-anywhere/src/graphql.ts +++ b/packages/graphql-anywhere/src/graphql.ts @@ -75,7 +75,6 @@ export type ExecOptions = { * but below is an exported alternative that is async. * In the 5.0 version, this will be the only export again * and it will be async - * */ export function graphql( resolver: Resolver,