From 20c88ebacaeab704b546df79f275206a47039e52 Mon Sep 17 00:00:00 2001 From: Dan Dascalescu Date: Tue, 17 Jul 2018 22:24:52 -0700 Subject: [PATCH 01/47] Pagination: fix typo --- docs/source/features/pagination.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/features/pagination.md b/docs/source/features/pagination.md index 299e35b2fce..f2943d77b72 100644 --- a/docs/source/features/pagination.md +++ b/docs/source/features/pagination.md @@ -208,4 +208,4 @@ const FEED_QUERY = gql` `; ``` -This would result in the accumulated feed in every query or `fetchMore` being placed in the store under the `feed` key, which we could later use of imperative store updates. In this example, we also use the `@connection` directive's optional `filter` argument, which allows us to include some arguments of the query in the store key. In this case, we want to include the `type` query argument in the store key, which results in multiple store values that accumulate pages from each type of feed. +This would result in the accumulated feed in every query or `fetchMore` being placed in the store under the `feed` key, which we could later use for imperative store updates. In this example, we also use the `@connection` directive's optional `filter` argument, which allows us to include some arguments of the query in the store key. In this case, we want to include the `type` query argument in the store key, which results in multiple store values that accumulate pages from each type of feed. From 71e5774c1544f2240eb1c9eb1f3e39e9bc433c0d Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Wed, 18 Jul 2018 15:33:25 -0700 Subject: [PATCH 02/47] docs: Update link to `create-react-app` instructions. The existing page for "Add React to a New App" (previously; now broken: https://reactjs.org/docs/add-react-to-a-new-app.html) has been moved to https://reactjs.org/docs/create-a-new-react-app.html. This updates the documentation to accommodate that change! --- docs/source/essentials/get-started.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/essentials/get-started.md b/docs/source/essentials/get-started.md index 07e612b812c..1b467193a1e 100644 --- a/docs/source/essentials/get-started.md +++ b/docs/source/essentials/get-started.md @@ -19,7 +19,7 @@ npm install apollo-boost react-apollo graphql --save - `react-apollo`: View layer integration for React - `graphql`: Also parses your GraphQL queries -> If you'd like to walk through this tutorial yourself, we recommend either running a new React project locally with [`create-react-app`](https://reactjs.org/docs/add-react-to-a-new-app.html) or creating a new React sandbox on [CodeSandbox](https://codesandbox.io/). For reference, we will be using [this Launchpad](https://launchpad.graphql.com/w5xlvm3vzz) as our GraphQL server for our sample app, which pulls exchange rate data from the Coinbase API. If you'd like to skip ahead and see the app we're about to build, you can view it on [CodeSandbox](https://codesandbox.io/s/nn9y2wzyw4). +> If you'd like to walk through this tutorial yourself, we recommend either running a new React project locally with [`create-react-app`](https://reactjs.org/docs/create-a-new-react-app.html) or creating a new React sandbox on [CodeSandbox](https://codesandbox.io/). For reference, we will be using [this Launchpad](https://launchpad.graphql.com/w5xlvm3vzz) as our GraphQL server for our sample app, which pulls exchange rate data from the Coinbase API. If you'd like to skip ahead and see the app we're about to build, you can view it on [CodeSandbox](https://codesandbox.io/s/nn9y2wzyw4).

Create a client

From 61bd8a608a169c7d3a82e741ee92e975e0055372 Mon Sep 17 00:00:00 2001 From: clarencenpy Date: Fri, 13 Jul 2018 15:21:05 -0700 Subject: [PATCH 03/47] Merge deferred patches with the initial ExecutionResult --- .../apollo-client/src/core/QueryManager.ts | 9 ++- packages/apollo-client/src/core/types.ts | 20 +++++- packages/apollo-client/src/data/queries.ts | 18 ++++- packages/apollo-client/src/data/store.ts | 68 ++++++++++++++++++- 4 files changed, 109 insertions(+), 6 deletions(-) diff --git a/packages/apollo-client/src/core/QueryManager.ts b/packages/apollo-client/src/core/QueryManager.ts index f543bf79d69..9f8182042e2 100644 --- a/packages/apollo-client/src/core/QueryManager.ts +++ b/packages/apollo-client/src/core/QueryManager.ts @@ -33,7 +33,12 @@ import { } from './watchQueryOptions'; import { ObservableQuery } from './ObservableQuery'; import { NetworkStatus, isNetworkRequestInFlight } from './networkStatus'; -import { QueryListener, ApolloQueryResult, FetchType } from './types'; +import { + QueryListener, + ApolloQueryResult, + FetchType, + ExecutionPatchResult, +} from './types'; import { graphQLResultHasError } from 'apollo-utilities'; export interface QueryInfo { @@ -1059,7 +1064,7 @@ export class QueryManager { return new Promise>((resolve, reject) => { this.addFetchQueryPromise(requestId, resolve, reject); const subscription = execute(this.deduplicator, operation).subscribe({ - next: (result: ExecutionResult) => { + next: (result: ExecutionResult | ExecutionPatchResult) => { // default the lastRequestId to 1 const { lastRequestId } = this.getQuery(queryId); if (requestId >= (lastRequestId || 1)) { diff --git a/packages/apollo-client/src/core/types.ts b/packages/apollo-client/src/core/types.ts index 2d44d8b5a5f..59f1f3ff14d 100644 --- a/packages/apollo-client/src/core/types.ts +++ b/packages/apollo-client/src/core/types.ts @@ -1,4 +1,4 @@ -import { DocumentNode, GraphQLError } from 'graphql'; +import { DocumentNode, ExecutionResult, GraphQLError } from 'graphql'; import { QueryStoreValue } from '../data/queries'; import { NetworkStatus } from './networkStatus'; import { FetchResult } from 'apollo-link'; @@ -42,3 +42,21 @@ export type MutationQueryReducer = ( export type MutationQueryReducersMap = { [queryName: string]: MutationQueryReducer; }; + +/** + * Define a new type for patches that are sent as a result of using defer. + * Its is basically the same as ExecutionResult, except that it has a "path" + * field that keeps track of the where the patch is to be merged with the + * original result. + */ +export interface ExecutionPatchResult { + data?: { [key: string]: any }; + errors?: GraphQLError[]; + path: (string | number)[]; +} + +export function isPatch( + data: ExecutionResult | ExecutionPatchResult, +): data is ExecutionPatchResult { + return (data as ExecutionPatchResult).path !== undefined; +} diff --git a/packages/apollo-client/src/data/queries.ts b/packages/apollo-client/src/data/queries.ts index 35264114f8d..e7b31a2bb03 100644 --- a/packages/apollo-client/src/data/queries.ts +++ b/packages/apollo-client/src/data/queries.ts @@ -3,6 +3,7 @@ import { print } from 'graphql/language/printer'; import { isEqual } from 'apollo-utilities'; import { NetworkStatus } from '../core/networkStatus'; +import { ExecutionPatchResult, isPatch } from '../core/types'; export type QueryStoreValue = { document: DocumentNode; @@ -114,11 +115,26 @@ export class QueryStore { public markQueryResult( queryId: string, - result: ExecutionResult, + result: ExecutionResult | ExecutionPatchResult, fetchMoreForQueryId: string | undefined, ) { if (!this.store[queryId]) return; + // Merge graphqlErrors from patch, if any + if (isPatch(result)) { + if (result.errors) { + const errors: GraphQLError[] = []; + this.store[queryId].graphQLErrors!.forEach(error => { + errors.push(error); + }); + result.errors.forEach(error => { + errors.push(error); + }); + this.store[queryId].graphQLErrors = errors; + } + return; + } + this.store[queryId].networkError = null; this.store[queryId].graphQLErrors = result.errors && result.errors.length ? result.errors : []; diff --git a/packages/apollo-client/src/data/store.ts b/packages/apollo-client/src/data/store.ts index 05ad5ef9bec..f0a0ca7d706 100644 --- a/packages/apollo-client/src/data/store.ts +++ b/packages/apollo-client/src/data/store.ts @@ -7,7 +7,11 @@ import { tryFunctionOrLogError, graphQLResultHasError, } from 'apollo-utilities'; -import { MutationQueryReducer } from '../core/types'; +import { + ExecutionPatchResult, + isPatch, + MutationQueryReducer, +} from '../core/types'; export type QueryWithUpdater = { updater: MutationQueryReducer; @@ -33,13 +37,73 @@ export class DataStore { return this.cache; } - public markQueryResult( + private mergePatch( result: ExecutionResult, + patch: ExecutionPatchResult, + ): void { + if (patch.errors) { + } + + if (result) { + let curKeyIndex = 0; + let curKey: string | number; + let curPointer: Record = result as Record; + while (curKeyIndex !== patch.path.length) { + curKey = patch.path[curKeyIndex++]; + const isLeaf = curKeyIndex === patch.path.length; + if (isLeaf) { + if (patch.data) { + // Data may not exist if there is an error in the patch + curPointer[curKey] = patch.data; + } + } else { + if (curPointer[curKey] === undefined) { + // This is indicative of a patch that is not ready to be merged, which + // can happen if patches for inner objects arrive before its parent. + // The graphql execution phase must make sure that this does not + // happen. + throw new Error( + `Failed to merge patch with path '[${patch.path}]'`, + ); + } + if (curPointer[curKey] === null) { + // Check whether it should be an array or an object by looking at the + // next key, then create the object if it is not present. + if (typeof patch.path[curKeyIndex] === 'string') { + curPointer[curKey] = {}; + } else if (typeof patch.path[curKeyIndex] === 'number') { + curPointer[curKey] = []; + } + } + curPointer = curPointer[curKey]; + } + } + } + } + + public markQueryResult( + result: ExecutionResult | ExecutionPatchResult, document: DocumentNode, variables: any, fetchMoreForQueryId: string | undefined, ignoreErrors: boolean = false, ) { + if (isPatch(result)) { + const originalResult: ExecutionResult | null = this.cache.read({ + query: document, + variables: variables, + rootId: 'ROOT_QUERY', + optimistic: false, + }); + if (originalResult) { + this.mergePatch(originalResult, result); + result = { data: originalResult }; + } else { + // Nothing may be written to cache if the first response had an error + return; + } + } + let writeWithErrors = !graphQLResultHasError(result); if (ignoreErrors && graphQLResultHasError(result) && result.data) { writeWithErrors = true; From 806dec634e1c8503ff7dc7287a37cf3824da950f Mon Sep 17 00:00:00 2001 From: clarencenpy Date: Mon, 16 Jul 2018 20:24:37 -0700 Subject: [PATCH 04/47] Expose a loadingState tree for deferred queries The motivation behind this is the need to distinguish between "pending" or "null" states for deferred fields. --- .../apollo-client/src/core/ObservableQuery.ts | 5 +++ .../apollo-client/src/core/QueryManager.ts | 12 +++++ .../apollo-client/src/core/networkStatus.ts | 8 +++- packages/apollo-client/src/core/types.ts | 1 + packages/apollo-client/src/data/queries.ts | 45 ++++++++++++++++++- packages/apollo-client/src/index.ts | 7 ++- packages/apollo-utilities/src/directives.ts | 40 +++++++++++++++++ packages/apollo-utilities/src/getFromAST.ts | 22 ++++----- 8 files changed, 126 insertions(+), 14 deletions(-) diff --git a/packages/apollo-client/src/core/ObservableQuery.ts b/packages/apollo-client/src/core/ObservableQuery.ts index 3df67f34a35..05219bb963c 100644 --- a/packages/apollo-client/src/core/ObservableQuery.ts +++ b/packages/apollo-client/src/core/ObservableQuery.ts @@ -31,6 +31,7 @@ export type ApolloCurrentResult = { networkStatus: NetworkStatus; error?: ApolloError; partial?: boolean; + loadingState?: Record; }; export interface FetchMoreOptions< @@ -218,6 +219,10 @@ export class ObservableQuery< result.errors = queryStoreValue.graphQLErrors; } + if (queryStoreValue) { + result.loadingState = queryStoreValue.loadingState; + } + if (!partial) { const stale = false; this.lastResult = { ...result, stale }; diff --git a/packages/apollo-client/src/core/QueryManager.ts b/packages/apollo-client/src/core/QueryManager.ts index 9f8182042e2..c578d6606bd 100644 --- a/packages/apollo-client/src/core/QueryManager.ts +++ b/packages/apollo-client/src/core/QueryManager.ts @@ -13,6 +13,7 @@ import { isProduction, maybeDeepFreeze, hasDirectives, + initDeferredFieldLoadingStates, } from 'apollo-utilities'; import { QueryScheduler } from '../scheduler/scheduler'; @@ -38,6 +39,7 @@ import { ApolloQueryResult, FetchType, ExecutionPatchResult, + isPatch, } from './types'; import { graphQLResultHasError } from 'apollo-utilities'; @@ -567,6 +569,7 @@ export class QueryManager { loading: isNetworkRequestInFlight(queryStoreValue.networkStatus), networkStatus: queryStoreValue.networkStatus, stale: true, + loadingState: queryStoreValue.loadingState, }; } else { resultFromStore = { @@ -574,6 +577,7 @@ export class QueryManager { loading: isNetworkRequestInFlight(queryStoreValue.networkStatus), networkStatus: queryStoreValue.networkStatus, stale: false, + loadingState: queryStoreValue.loadingState, }; } @@ -1065,6 +1069,12 @@ export class QueryManager { this.addFetchQueryPromise(requestId, resolve, reject); const subscription = execute(this.deduplicator, operation).subscribe({ next: (result: ExecutionResult | ExecutionPatchResult) => { + // Keep track of the individual loading states of each deferred field + let loadingState; + if (!isPatch(result) && hasDirectives(['defer'], document)) { + loadingState = initDeferredFieldLoadingStates(document); + } + // default the lastRequestId to 1 const { lastRequestId } = this.getQuery(queryId); if (requestId >= (lastRequestId || 1)) { @@ -1091,6 +1101,7 @@ export class QueryManager { queryId, result, fetchMoreForQueryId, + loadingState, ); this.invalidate(true, queryId, fetchMoreForQueryId); @@ -1147,6 +1158,7 @@ export class QueryManager { loading: false, networkStatus: NetworkStatus.ready, stale: false, + loadingState: {}, }); }, }); diff --git a/packages/apollo-client/src/core/networkStatus.ts b/packages/apollo-client/src/core/networkStatus.ts index c49fab9882c..b2be87c9013 100644 --- a/packages/apollo-client/src/core/networkStatus.ts +++ b/packages/apollo-client/src/core/networkStatus.ts @@ -43,6 +43,12 @@ export enum NetworkStatus { * No request is in flight for this query, but one or more errors were detected. */ error = 8, + + /** + * Only a partial response has been received, this could come from the usage + * of @defer/live/stream directives. + */ + partial = 9, } /** @@ -52,5 +58,5 @@ export enum NetworkStatus { export function isNetworkRequestInFlight( networkStatus: NetworkStatus, ): boolean { - return networkStatus < 7; + return networkStatus < 7 || networkStatus === 9; } diff --git a/packages/apollo-client/src/core/types.ts b/packages/apollo-client/src/core/types.ts index 59f1f3ff14d..bd3ece1b48f 100644 --- a/packages/apollo-client/src/core/types.ts +++ b/packages/apollo-client/src/core/types.ts @@ -21,6 +21,7 @@ export type ApolloQueryResult = { loading: boolean; networkStatus: NetworkStatus; stale: boolean; + loadingState?: Record; }; export enum FetchType { diff --git a/packages/apollo-client/src/data/queries.ts b/packages/apollo-client/src/data/queries.ts index e7b31a2bb03..076948bc6c1 100644 --- a/packages/apollo-client/src/data/queries.ts +++ b/packages/apollo-client/src/data/queries.ts @@ -4,6 +4,7 @@ import { isEqual } from 'apollo-utilities'; import { NetworkStatus } from '../core/networkStatus'; import { ExecutionPatchResult, isPatch } from '../core/types'; +import { cloneDeep } from 'apollo-utilities'; export type QueryStoreValue = { document: DocumentNode; @@ -12,6 +13,7 @@ export type QueryStoreValue = { networkStatus: NetworkStatus; networkError?: Error | null; graphQLErrors?: GraphQLError[]; + loadingState?: Record; metadata: any; }; @@ -117,11 +119,52 @@ export class QueryStore { queryId: string, result: ExecutionResult | ExecutionPatchResult, fetchMoreForQueryId: string | undefined, + loadingState?: Record, ) { if (!this.store[queryId]) return; - // Merge graphqlErrors from patch, if any + // Set up loadingState if it is passed in by QueryManager + if (loadingState) { + this.store[queryId].loadingState = loadingState; + } + if (isPatch(result)) { + // Update loadingState for every patch received, by traversing its path + const path = (result as ExecutionPatchResult).path; + console.log(`path: ${JSON.stringify(path, null, 2)}`); + let index = 0; + const copy = cloneDeep(this.store[queryId].loadingState); + let curPointer = copy; + while (index < path.length) { + const key = path[index++]; + if (curPointer) { + curPointer = curPointer[key]; + if (index === path.length) { + // Reached the leaf node + if (Array.isArray(result.data)) { + // At the time of instantiating the loadingState from the query AST, + // we have no way of telling if a field is an array type. Therefore, + // once we receive a patch that has array data, we need to update the + // loadingState with an array with the appropriate number of elements. + + const children = cloneDeep(curPointer!.children); + const childrenArray = []; + for (let i = 0; i < result.data.length; i++) { + childrenArray.push(children); + } + curPointer!.children = childrenArray; + } + curPointer!.loading = false; + break; + } + if (curPointer!.children) { + curPointer = curPointer!.children; + } + } + } + this.store[queryId].loadingState = copy; + + // Merge graphqlErrors from patch, if any if (result.errors) { const errors: GraphQLError[] = []; this.store[queryId].graphQLErrors!.forEach(error => { diff --git a/packages/apollo-client/src/index.ts b/packages/apollo-client/src/index.ts index db5a1e5f25c..4af9ecdc3d2 100644 --- a/packages/apollo-client/src/index.ts +++ b/packages/apollo-client/src/index.ts @@ -23,9 +23,12 @@ export * from './core/types'; export { ApolloError } from './errors/ApolloError'; -import ApolloClient, { ApolloClientOptions } from './ApolloClient'; +import ApolloClient, { + ApolloClientOptions, + DefaultOptions, +} from './ApolloClient'; -export { ApolloClientOptions }; +export { ApolloClientOptions, DefaultOptions }; // export the client as both default and named export { ApolloClient }; diff --git a/packages/apollo-utilities/src/directives.ts b/packages/apollo-utilities/src/directives.ts index 1a884497dcf..fcc232e485a 100644 --- a/packages/apollo-utilities/src/directives.ts +++ b/packages/apollo-utilities/src/directives.ts @@ -136,6 +136,46 @@ export function getDirectiveNames(doc: DocumentNode) { return directiveNames; } +function extractDeferredFieldsToTree( + selection: SelectionNode, +): Record { + const hasDeferDirective: boolean = + selection.directives && + selection.directives.length > 0 && + selection.directives.findIndex(directive => { + return directive.name.value === 'defer'; + }) !== -1; + const isLeaf: boolean = + !(selection as FieldNode).selectionSet || + (selection as FieldNode).selectionSet.selections.length === 0; + + if (isLeaf) { + return hasDeferDirective ? { loading: true } : undefined; + } + + const map: { loading: boolean; children: Record } = { + loading: hasDeferDirective ? true : undefined, + children: undefined, + }; + let hasDeferredChild = false; + + for (const childSelection of (selection as FieldNode).selectionSet + .selections) { + const name = (childSelection as FieldNode).name.value; + const subtree = extractDeferredFieldsToTree(childSelection); + if (subtree !== undefined) { + if (!hasDeferredChild) hasDeferredChild = true; + if (!map.children) map.children = {}; + map.children[name] = subtree; + } + } + return map; +} + +export function initDeferredFieldLoadingStates(doc: DocumentNode) { + return extractDeferredFieldsToTree(doc.definitions[0] as any).children; +} + export function hasDirectives(names: string[], doc: DocumentNode) { return getDirectiveNames(doc).some( (name: string) => names.indexOf(name) > -1, 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); } From df030b8c6fe09ae7e06cebc674f3f21087448bd7 Mon Sep 17 00:00:00 2001 From: clarencenpy Date: Tue, 17 Jul 2018 01:58:11 -0700 Subject: [PATCH 05/47] Improved DX by exposing a proxied version of the loadingState tree This allows us to omit `children` from the access path. --- .../apollo-client/src/core/QueryManager.ts | 52 ++++++++++- packages/apollo-client/src/data/queries.ts | 89 +++++++++++++++++-- packages/apollo-utilities/src/directives.ts | 40 --------- 3 files changed, 129 insertions(+), 52 deletions(-) diff --git a/packages/apollo-client/src/core/QueryManager.ts b/packages/apollo-client/src/core/QueryManager.ts index c578d6606bd..5cc514c730b 100644 --- a/packages/apollo-client/src/core/QueryManager.ts +++ b/packages/apollo-client/src/core/QueryManager.ts @@ -1,5 +1,10 @@ import { execute, ApolloLink, FetchResult } from 'apollo-link'; -import { ExecutionResult, DocumentNode } from 'graphql'; +import { + ExecutionResult, + DocumentNode, + SelectionNode, + FieldNode, +} from 'graphql'; import { print } from 'graphql/language/printer'; import { DedupLink as Deduplicator } from 'apollo-link-dedup'; import { Cache } from 'apollo-cache'; @@ -13,7 +18,6 @@ import { isProduction, maybeDeepFreeze, hasDirectives, - initDeferredFieldLoadingStates, } from 'apollo-utilities'; import { QueryScheduler } from '../scheduler/scheduler'; @@ -604,7 +608,7 @@ export class QueryManager { if (isDifferentResult || previouslyHadError) { try { - observer.next(maybeDeepFreeze(resultFromStore)); + observer.next(resultFromStore); } catch (e) { // Throw error outside this control flow to avoid breaking Apollo's state setTimeout(() => { @@ -1244,3 +1248,45 @@ export class QueryManager { }; } } + +function extractDeferredFieldsToTree( + selection: SelectionNode, +): Record | boolean { + const hasDeferDirective: boolean = (selection.directives && + selection.directives.length > 0 && + selection.directives.findIndex(directive => { + return directive.name.value === 'defer'; + }) !== -1) as boolean; + const isLeaf: boolean = + !(selection as FieldNode).selectionSet || + (selection as FieldNode).selectionSet!.selections.length === 0; + + if (isLeaf) { + return hasDeferDirective ? { _loading: true } : true; + } + + const map: { _loading?: boolean; _children?: Record } = { + _loading: hasDeferDirective ? true : undefined, + _children: undefined, + }; + let hasDeferredChild = false; + + for (const childSelection of (selection as FieldNode).selectionSet! + .selections) { + const name = (childSelection as FieldNode).name.value; + const subtree = extractDeferredFieldsToTree(childSelection); + if (subtree !== undefined) { + if (!hasDeferredChild) hasDeferredChild = true; + if (!map._children) map._children = {}; + map._children[name] = subtree; + } + } + return map; +} + +function initDeferredFieldLoadingStates(doc: DocumentNode) { + return (extractDeferredFieldsToTree(doc.definitions[0] as any) as Record< + string, + any + >)._children; +} diff --git a/packages/apollo-client/src/data/queries.ts b/packages/apollo-client/src/data/queries.ts index 076948bc6c1..14715810c63 100644 --- a/packages/apollo-client/src/data/queries.ts +++ b/packages/apollo-client/src/data/queries.ts @@ -13,6 +13,7 @@ export type QueryStoreValue = { networkStatus: NetworkStatus; networkError?: Error | null; graphQLErrors?: GraphQLError[]; + _loadingState?: Record; loadingState?: Record; metadata: any; }; @@ -125,15 +126,15 @@ export class QueryStore { // Set up loadingState if it is passed in by QueryManager if (loadingState) { - this.store[queryId].loadingState = loadingState; + this.store[queryId]._loadingState = loadingState; + this.store[queryId].loadingState = proxify(loadingState); } if (isPatch(result)) { // Update loadingState for every patch received, by traversing its path const path = (result as ExecutionPatchResult).path; - console.log(`path: ${JSON.stringify(path, null, 2)}`); let index = 0; - const copy = cloneDeep(this.store[queryId].loadingState); + const copy = cloneDeep(this.store[queryId]._loadingState); let curPointer = copy; while (index < path.length) { const key = path[index++]; @@ -147,22 +148,24 @@ export class QueryStore { // once we receive a patch that has array data, we need to update the // loadingState with an array with the appropriate number of elements. - const children = cloneDeep(curPointer!.children); + const children = cloneDeep(curPointer!._children); const childrenArray = []; for (let i = 0; i < result.data.length; i++) { childrenArray.push(children); } - curPointer!.children = childrenArray; + curPointer!._children = childrenArray; } - curPointer!.loading = false; + curPointer!._loading = false; break; } - if (curPointer!.children) { - curPointer = curPointer!.children; + if (curPointer!._children) { + curPointer = curPointer!._children; } } } - this.store[queryId].loadingState = copy; + + this.store[queryId]._loadingState = copy; + this.store[queryId].loadingState = proxify(copy); // Merge graphqlErrors from patch, if any if (result.errors) { @@ -247,3 +250,71 @@ export class QueryStore { ); } } + +/** + * Given a loadingState tree, it returns a proxified version of it that + * reduces the amount of boilerplate code required to access nested fields. + * Also defaults _isLoaded on any field that is not deferred to true. + */ +function proxify( + loadingState?: Record, +): Record | undefined { + if (!loadingState) return loadingState; + // if (1 === 1) return loadingState; + + return new Proxy(loadingState, { + get(target, prop) { + if (target && target[prop as string | number]) { + const o = target[prop as string | number]; + if (typeof o !== 'object') + return new Proxy( + {}, + { + get(_, prop) { + return prop === '_isLoaded' ? true : undefined; + }, + }, + ); + if (o._loading) { + // Object is still loading + return new Proxy(o, { + get(_, prop) { + // Its children are still loading, so any other property access + // should return undefined. Forces user to check for existence + // of parent first. + return prop === '_isLoaded' ? false : undefined; + }, + }); + } + if (o._children) { + if (Array.isArray(o._children)) { + const proxiedChildren = o._children.map((c: any) => proxify(c)); + return new Proxy(o._children, { + get(_, prop) { + return prop === '_isLoaded' ? true : proxiedChildren[prop]; + }, + }); + } else { + const proxiedChildren = proxify(o._children) || {}; + return new Proxy(o._children, { + get(_, prop) { + return prop === '_isLoaded' + ? true + : proxiedChildren[prop as string | number]; + }, + }); + } + } else { + // This is a deferred leaf node and loading is complete + return new Proxy(o, { + get(_, prop) { + return prop === '_isLoaded'; + }, + }); + } + } + // Otherwise, _isLoaded is the only valid property that can be accessed + return prop === '_isLoaded' ? true : undefined; + }, + }); +} diff --git a/packages/apollo-utilities/src/directives.ts b/packages/apollo-utilities/src/directives.ts index fcc232e485a..1a884497dcf 100644 --- a/packages/apollo-utilities/src/directives.ts +++ b/packages/apollo-utilities/src/directives.ts @@ -136,46 +136,6 @@ export function getDirectiveNames(doc: DocumentNode) { return directiveNames; } -function extractDeferredFieldsToTree( - selection: SelectionNode, -): Record { - const hasDeferDirective: boolean = - selection.directives && - selection.directives.length > 0 && - selection.directives.findIndex(directive => { - return directive.name.value === 'defer'; - }) !== -1; - const isLeaf: boolean = - !(selection as FieldNode).selectionSet || - (selection as FieldNode).selectionSet.selections.length === 0; - - if (isLeaf) { - return hasDeferDirective ? { loading: true } : undefined; - } - - const map: { loading: boolean; children: Record } = { - loading: hasDeferDirective ? true : undefined, - children: undefined, - }; - let hasDeferredChild = false; - - for (const childSelection of (selection as FieldNode).selectionSet - .selections) { - const name = (childSelection as FieldNode).name.value; - const subtree = extractDeferredFieldsToTree(childSelection); - if (subtree !== undefined) { - if (!hasDeferredChild) hasDeferredChild = true; - if (!map.children) map.children = {}; - map.children[name] = subtree; - } - } - return map; -} - -export function initDeferredFieldLoadingStates(doc: DocumentNode) { - return extractDeferredFieldsToTree(doc.definitions[0] as any).children; -} - export function hasDirectives(names: string[], doc: DocumentNode) { return getDirectiveNames(doc).some( (name: string) => names.indexOf(name) > -1, From fbf74f7132ae1195ab9f7e6cb5947371a09f9a36 Mon Sep 17 00:00:00 2001 From: clarencenpy Date: Tue, 17 Jul 2018 09:01:46 -0700 Subject: [PATCH 06/47] Deep freeze all fields on resultFromStore except loadingState --- packages/apollo-client/src/core/QueryManager.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/apollo-client/src/core/QueryManager.ts b/packages/apollo-client/src/core/QueryManager.ts index 5cc514c730b..6253f35d181 100644 --- a/packages/apollo-client/src/core/QueryManager.ts +++ b/packages/apollo-client/src/core/QueryManager.ts @@ -608,7 +608,14 @@ export class QueryManager { if (isDifferentResult || previouslyHadError) { try { - observer.next(resultFromStore); + observer.next({ + data: maybeDeepFreeze(resultFromStore.data), + loading: maybeDeepFreeze(resultFromStore.loading), + networkStatus: maybeDeepFreeze(resultFromStore.networkStatus), + stale: maybeDeepFreeze(resultFromStore.stale), + // Freeze everything except this since we are proxifying it + loadingState: resultFromStore.loadingState, + }); } catch (e) { // Throw error outside this control flow to avoid breaking Apollo's state setTimeout(() => { From 195e77994c5be5709562be6c0f6d5a9b9ce0c35e Mon Sep 17 00:00:00 2001 From: clarencenpy Date: Tue, 17 Jul 2018 09:02:27 -0700 Subject: [PATCH 07/47] Improved DX, omit `_isLoaded` from the access path --- packages/apollo-client/src/data/queries.ts | 51 ++++------------------ 1 file changed, 9 insertions(+), 42 deletions(-) diff --git a/packages/apollo-client/src/data/queries.ts b/packages/apollo-client/src/data/queries.ts index 14715810c63..e1fd1f12f68 100644 --- a/packages/apollo-client/src/data/queries.ts +++ b/packages/apollo-client/src/data/queries.ts @@ -254,67 +254,34 @@ export class QueryStore { /** * Given a loadingState tree, it returns a proxified version of it that * reduces the amount of boilerplate code required to access nested fields. - * Also defaults _isLoaded on any field that is not deferred to true. + * The intercepted getter will return either true (is loaded) or undefined. */ function proxify( loadingState?: Record, ): Record | undefined { if (!loadingState) return loadingState; - // if (1 === 1) return loadingState; return new Proxy(loadingState, { get(target, prop) { if (target && target[prop as string | number]) { const o = target[prop as string | number]; - if (typeof o !== 'object') - return new Proxy( - {}, - { - get(_, prop) { - return prop === '_isLoaded' ? true : undefined; - }, - }, - ); if (o._loading) { - // Object is still loading - return new Proxy(o, { - get(_, prop) { - // Its children are still loading, so any other property access - // should return undefined. Forces user to check for existence - // of parent first. - return prop === '_isLoaded' ? false : undefined; - }, - }); + // Object is still loading, so we return undefined to prevent + // other property access on it. Therefore, we force the user to + // do checks on deferred fields. + return undefined; } if (o._children) { if (Array.isArray(o._children)) { - const proxiedChildren = o._children.map((c: any) => proxify(c)); - return new Proxy(o._children, { - get(_, prop) { - return prop === '_isLoaded' ? true : proxiedChildren[prop]; - }, - }); + return o._children.map((c: any) => proxify(c)); } else { - const proxiedChildren = proxify(o._children) || {}; - return new Proxy(o._children, { - get(_, prop) { - return prop === '_isLoaded' - ? true - : proxiedChildren[prop as string | number]; - }, - }); + return proxify(o._children) || {}; } } else { - // This is a deferred leaf node and loading is complete - return new Proxy(o, { - get(_, prop) { - return prop === '_isLoaded'; - }, - }); + // This is a leaf node and loading is complete + return true; } } - // Otherwise, _isLoaded is the only valid property that can be accessed - return prop === '_isLoaded' ? true : undefined; }, }); } From b578bf6a71c6935f8f1d1911a3ed0c73db7692c6 Mon Sep 17 00:00:00 2001 From: clarencenpy Date: Tue, 17 Jul 2018 09:47:36 -0700 Subject: [PATCH 08/47] Handle inline fragments in query --- .../apollo-client/src/core/QueryManager.ts | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/packages/apollo-client/src/core/QueryManager.ts b/packages/apollo-client/src/core/QueryManager.ts index 6253f35d181..4deb5add8f7 100644 --- a/packages/apollo-client/src/core/QueryManager.ts +++ b/packages/apollo-client/src/core/QueryManager.ts @@ -4,6 +4,7 @@ import { DocumentNode, SelectionNode, FieldNode, + Kind, } from 'graphql'; import { print } from 'graphql/language/printer'; import { DedupLink as Deduplicator } from 'apollo-link-dedup'; @@ -1256,6 +1257,10 @@ export class QueryManager { } } +/** + * Recursive function that extracts a tree that mirrors the shape of the query, + * adding _loading property to fields which are deferred. + */ function extractDeferredFieldsToTree( selection: SelectionNode, ): Record | boolean { @@ -1265,8 +1270,9 @@ function extractDeferredFieldsToTree( return directive.name.value === 'defer'; }) !== -1) as boolean; const isLeaf: boolean = - !(selection as FieldNode).selectionSet || - (selection as FieldNode).selectionSet!.selections.length === 0; + selection.kind !== Kind.INLINE_FRAGMENT && + (!(selection as FieldNode).selectionSet || + (selection as FieldNode).selectionSet!.selections.length === 0); if (isLeaf) { return hasDeferDirective ? { _loading: true } : true; @@ -1280,12 +1286,18 @@ function extractDeferredFieldsToTree( for (const childSelection of (selection as FieldNode).selectionSet! .selections) { - const name = (childSelection as FieldNode).name.value; const subtree = extractDeferredFieldsToTree(childSelection); if (subtree !== undefined) { if (!hasDeferredChild) hasDeferredChild = true; - if (!map._children) map._children = {}; - map._children[name] = subtree; + if (childSelection.kind === Kind.INLINE_FRAGMENT) { + if (typeof subtree !== 'boolean') { + map._children = Object.assign(map._children || {}, subtree._children); + } + } else { + if (!map._children) map._children = {}; + const name = (childSelection as FieldNode).name.value; + map._children[name] = subtree; + } } } return map; From 7238f05868cfc7bf6a6d165e0cddf870a54b4165 Mon Sep 17 00:00:00 2001 From: clarencenpy Date: Tue, 17 Jul 2018 12:24:16 -0700 Subject: [PATCH 09/47] Remove usage of Proxy and just return a compacted loadingState tree --- packages/apollo-client/src/data/queries.ts | 50 ++++++++++------------ 1 file changed, 23 insertions(+), 27 deletions(-) diff --git a/packages/apollo-client/src/data/queries.ts b/packages/apollo-client/src/data/queries.ts index e1fd1f12f68..bf4a439f9ca 100644 --- a/packages/apollo-client/src/data/queries.ts +++ b/packages/apollo-client/src/data/queries.ts @@ -127,7 +127,7 @@ export class QueryStore { // Set up loadingState if it is passed in by QueryManager if (loadingState) { this.store[queryId]._loadingState = loadingState; - this.store[queryId].loadingState = proxify(loadingState); + this.store[queryId].loadingState = compactLoadingStateTree(loadingState); } if (isPatch(result)) { @@ -165,7 +165,7 @@ export class QueryStore { } this.store[queryId]._loadingState = copy; - this.store[queryId].loadingState = proxify(copy); + this.store[queryId].loadingState = compactLoadingStateTree(copy); // Merge graphqlErrors from patch, if any if (result.errors) { @@ -252,36 +252,32 @@ export class QueryStore { } /** - * Given a loadingState tree, it returns a proxified version of it that + * Given a loadingState tree, it returns a compacted version of it that * reduces the amount of boilerplate code required to access nested fields. - * The intercepted getter will return either true (is loaded) or undefined. + * The structure of this will mirror the response data, with deferred fields + * set to undefined until its patch is received. */ -function proxify( +function compactLoadingStateTree( loadingState?: Record, ): Record | undefined { if (!loadingState) return loadingState; + const state: Record = {}; - return new Proxy(loadingState, { - get(target, prop) { - if (target && target[prop as string | number]) { - const o = target[prop as string | number]; - if (o._loading) { - // Object is still loading, so we return undefined to prevent - // other property access on it. Therefore, we force the user to - // do checks on deferred fields. - return undefined; - } - if (o._children) { - if (Array.isArray(o._children)) { - return o._children.map((c: any) => proxify(c)); - } else { - return proxify(o._children) || {}; - } - } else { - // This is a leaf node and loading is complete - return true; - } + for (let key in loadingState) { + const o = loadingState[key]; + if (o._loading) { + continue; + } + if (o._children) { + if (Array.isArray(o._children)) { + state[key] = o._children.map((c: any) => compactLoadingStateTree(c)); + } else { + state[key] = compactLoadingStateTree(o._children); } - }, - }); + continue; + } + state[key] = true; + } + + return state; } From ab3a3bd3611047a5c2c179ef273ddeba5f93716c Mon Sep 17 00:00:00 2001 From: clarencenpy Date: Tue, 17 Jul 2018 12:39:25 -0700 Subject: [PATCH 10/47] Use experimental release of `apollo-link-dedup` --- packages/apollo-client/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/apollo-client/package.json b/packages/apollo-client/package.json index 73bd30efbec..db1ea919cfd 100644 --- a/packages/apollo-client/package.json +++ b/packages/apollo-client/package.json @@ -49,7 +49,7 @@ "@types/zen-observable": "^0.8.0", "apollo-cache": "^1.1.12", "apollo-link": "^1.0.0", - "apollo-link-dedup": "^1.0.0", + "apollo-link-dedup": "0.0.0-alpha.0", "apollo-utilities": "^1.0.16", "symbol-observable": "^1.0.2", "zen-observable": "^0.8.0" From b6f32dd2d2492d87b3eb45a827086fa2a93a0af2 Mon Sep 17 00:00:00 2001 From: clarencenpy Date: Tue, 17 Jul 2018 17:28:19 -0700 Subject: [PATCH 11/47] Handle FragmentSpreads in the query --- .../apollo-client/src/core/QueryManager.ts | 72 +++++++++++++------ 1 file changed, 51 insertions(+), 21 deletions(-) diff --git a/packages/apollo-client/src/core/QueryManager.ts b/packages/apollo-client/src/core/QueryManager.ts index 4deb5add8f7..2d3f68dcb08 100644 --- a/packages/apollo-client/src/core/QueryManager.ts +++ b/packages/apollo-client/src/core/QueryManager.ts @@ -5,6 +5,7 @@ import { SelectionNode, FieldNode, Kind, + FragmentDefinitionNode, } from 'graphql'; import { print } from 'graphql/language/printer'; import { DedupLink as Deduplicator } from 'apollo-link-dedup'; @@ -609,14 +610,7 @@ export class QueryManager { if (isDifferentResult || previouslyHadError) { try { - observer.next({ - data: maybeDeepFreeze(resultFromStore.data), - loading: maybeDeepFreeze(resultFromStore.loading), - networkStatus: maybeDeepFreeze(resultFromStore.networkStatus), - stale: maybeDeepFreeze(resultFromStore.stale), - // Freeze everything except this since we are proxifying it - loadingState: resultFromStore.loadingState, - }); + observer.next(maybeDeepFreeze(resultFromStore)); } catch (e) { // Throw error outside this control flow to avoid breaking Apollo's state setTimeout(() => { @@ -1081,12 +1075,6 @@ export class QueryManager { this.addFetchQueryPromise(requestId, resolve, reject); const subscription = execute(this.deduplicator, operation).subscribe({ next: (result: ExecutionResult | ExecutionPatchResult) => { - // Keep track of the individual loading states of each deferred field - let loadingState; - if (!isPatch(result) && hasDirectives(['defer'], document)) { - loadingState = initDeferredFieldLoadingStates(document); - } - // default the lastRequestId to 1 const { lastRequestId } = this.getQuery(queryId); if (requestId >= (lastRequestId || 1)) { @@ -1109,6 +1097,13 @@ export class QueryManager { })); } + // Initialize a tree of individual loading states for each deferred + // field, when the initial response arrives. + let loadingState; + if (!isPatch(result) && hasDirectives(['defer'], document)) { + loadingState = initDeferredFieldLoadingStates(document); + } + this.queryStore.markQueryResult( queryId, result, @@ -1263,6 +1258,7 @@ export class QueryManager { */ function extractDeferredFieldsToTree( selection: SelectionNode, + fragmentMap: Record, ): Record | boolean { const hasDeferDirective: boolean = (selection.directives && selection.directives.length > 0 && @@ -1282,13 +1278,32 @@ function extractDeferredFieldsToTree( _loading: hasDeferDirective ? true : undefined, _children: undefined, }; - let hasDeferredChild = false; + + // Replace FragmentSpreads with its actual selectionSet + const expandedFragments: SelectionNode[] = []; + (selection as FieldNode).selectionSet!.selections.forEach(childSelection => { + if (childSelection.kind === Kind.FRAGMENT_SPREAD) { + const fragmentName = childSelection.name.value; + fragmentMap[fragmentName].forEach((selection: SelectionNode) => { + expandedFragments.push(selection); + }); + } + }); + + // Remove FragmentSpreads + (selection as FieldNode).selectionSet!.selections = (selection as FieldNode).selectionSet!.selections.filter( + selection => selection.kind !== Kind.FRAGMENT_SPREAD, + ); + + // Add expanded FragmentSpreads to the current selection set + (selection as FieldNode).selectionSet!.selections = (selection as FieldNode).selectionSet!.selections.concat( + expandedFragments, + ); for (const childSelection of (selection as FieldNode).selectionSet! .selections) { - const subtree = extractDeferredFieldsToTree(childSelection); + const subtree = extractDeferredFieldsToTree(childSelection, fragmentMap); if (subtree !== undefined) { - if (!hasDeferredChild) hasDeferredChild = true; if (childSelection.kind === Kind.INLINE_FRAGMENT) { if (typeof subtree !== 'boolean') { map._children = Object.assign(map._children || {}, subtree._children); @@ -1304,8 +1319,23 @@ function extractDeferredFieldsToTree( } function initDeferredFieldLoadingStates(doc: DocumentNode) { - return (extractDeferredFieldsToTree(doc.definitions[0] as any) as Record< - string, - any - >)._children; + // Collect all the fragment definitions + const fragmentMap: Record = {}; + doc.definitions + .filter(definition => definition.kind === Kind.FRAGMENT_DEFINITION) + .forEach(definition => { + const fragmentName = (definition as FragmentDefinitionNode).name.value; + fragmentMap[ + fragmentName + ] = (definition as FragmentDefinitionNode).selectionSet.selections; + }); + + const operationDefinition = doc.definitions.filter( + definition => definition.kind === Kind.OPERATION_DEFINITION, + )[0]; // Take the first element since we do not support multiple operations + + return (extractDeferredFieldsToTree( + operationDefinition as any, + fragmentMap, + ) as Record)._children; } From aafe10b124210c35ba12ec64e71db584acd72699 Mon Sep 17 00:00:00 2001 From: clarencenpy Date: Tue, 17 Jul 2018 18:04:18 -0700 Subject: [PATCH 12/47] Always treat patches returning a different result Even though the actual data might look the same (i.e. the patched data is null) --- packages/apollo-client/src/core/QueryManager.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/apollo-client/src/core/QueryManager.ts b/packages/apollo-client/src/core/QueryManager.ts index 2d3f68dcb08..aea42468bb5 100644 --- a/packages/apollo-client/src/core/QueryManager.ts +++ b/packages/apollo-client/src/core/QueryManager.ts @@ -608,7 +608,15 @@ export class QueryManager { lastResult.data === resultFromStore.data ); - if (isDifferentResult || previouslyHadError) { + if ( + isDifferentResult || + previouslyHadError || + resultFromStore.loadingState + ) { + // If loadingState is present, this is a patch from a deferred + // query, and we should always treat it as a different result + // even though the actual data might be the same (i.e. the patch's + // data could be null. try { observer.next(maybeDeepFreeze(resultFromStore)); } catch (e) { From b4069cb6cf5deaace4c705f0bdf0f26a8d2c3a31 Mon Sep 17 00:00:00 2001 From: clarencenpy Date: Wed, 18 Jul 2018 23:33:41 -0700 Subject: [PATCH 13/47] Handle fields on fragment spreads that already exist in selectionSet @defer must be specified for all of the selections, matching up with the behavior of Apollo Server. --- .../apollo-client/src/core/QueryManager.ts | 31 +++++++++++++++++-- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/packages/apollo-client/src/core/QueryManager.ts b/packages/apollo-client/src/core/QueryManager.ts index aea42468bb5..750c0a7a93d 100644 --- a/packages/apollo-client/src/core/QueryManager.ts +++ b/packages/apollo-client/src/core/QueryManager.ts @@ -1304,9 +1304,34 @@ function extractDeferredFieldsToTree( ); // Add expanded FragmentSpreads to the current selection set - (selection as FieldNode).selectionSet!.selections = (selection as FieldNode).selectionSet!.selections.concat( - expandedFragments, - ); + expandedFragments.forEach(fragSelection => { + const fragFieldName = (fragSelection as FieldNode).name.value; + const existingSelection = (selection as FieldNode).selectionSet!.selections.find( + selection => + selection.kind !== Kind.INLINE_FRAGMENT && + (selection as FieldNode).name.value === fragFieldName, + ); + if (existingSelection) { + const fragSelectionHasDefer = + fragSelection.directives && + fragSelection.directives.findIndex( + directive => directive.name.value === 'defer', + ) >= 0; + if (!fragSelectionHasDefer) { + // Make sure that the existingSelection is not deferred, since all + // selections of the field must specify defer in order for the field + // to be deferred. This should match the behavior on apollo-server. + if (existingSelection.directives) { + existingSelection.directives = existingSelection.directives.filter( + directive => directive.name.value !== 'defer', + ); + } + } + } else { + // Add it to the selectionSet + (selection as FieldNode).selectionSet!.selections.push(fragSelection); + } + }); for (const childSelection of (selection as FieldNode).selectionSet! .selections) { From dc56ccb0a1f92fd4294f1278ce19167a40c26687 Mon Sep 17 00:00:00 2001 From: clarencenpy Date: Wed, 18 Jul 2018 23:42:36 -0700 Subject: [PATCH 14/47] Pass a flag to indicated deferred queries to the link stack --- packages/apollo-client/src/core/QueryManager.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/apollo-client/src/core/QueryManager.ts b/packages/apollo-client/src/core/QueryManager.ts index 750c0a7a93d..4358d5a5a78 100644 --- a/packages/apollo-client/src/core/QueryManager.ts +++ b/packages/apollo-client/src/core/QueryManager.ts @@ -1074,6 +1074,7 @@ export class QueryManager { // TODO: Should this be included for all entry points via // buildOperationForLink? forceFetch: !this.queryDeduplication, + isDeferred: hasDirectives(['defer'], document), }); let resultFromStore: any; From 0dbdb34040d4063e8d0ca4f343bab156db98ef61 Mon Sep 17 00:00:00 2001 From: clarencenpy Date: Thu, 19 Jul 2018 00:17:30 -0700 Subject: [PATCH 15/47] Pin package dependencies to the alpha versions with defer support --- packages/apollo-boost/package.json | 2 +- packages/apollo-client/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/apollo-boost/package.json b/packages/apollo-boost/package.json index 338c349b4b0..1e59c85f6c5 100644 --- a/packages/apollo-boost/package.json +++ b/packages/apollo-boost/package.json @@ -39,7 +39,7 @@ "apollo-client": "^2.3.5", "apollo-link": "^1.0.6", "apollo-link-error": "^1.0.3", - "apollo-link-http": "^1.3.1", + "apollo-link-http": "1.6.0-alpha.0", "apollo-link-state": "^0.4.0", "graphql-tag": "^2.4.2" }, diff --git a/packages/apollo-client/package.json b/packages/apollo-client/package.json index db1ea919cfd..c7a62f256be 100644 --- a/packages/apollo-client/package.json +++ b/packages/apollo-client/package.json @@ -49,7 +49,7 @@ "@types/zen-observable": "^0.8.0", "apollo-cache": "^1.1.12", "apollo-link": "^1.0.0", - "apollo-link-dedup": "0.0.0-alpha.0", + "apollo-link-dedup": "1.1.0-alpha.0", "apollo-utilities": "^1.0.16", "symbol-observable": "^1.0.2", "zen-observable": "^0.8.0" From 48115e3e70a1d32a1105d8ef5d6f5d9ec4ad6374 Mon Sep 17 00:00:00 2001 From: Hugh Willson Date: Thu, 19 Jul 2018 10:10:55 -0400 Subject: [PATCH 16/47] Changelog updates --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 318e45712aa..7e81893395f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ [@MikaelCarpenter](https://github.com/MikaelCarpenter) in [#3609](https://github.com/apollographql/apollo-client/pull/3609) [@Gamezpedia](https://github.com/Gamezpedia) in [#3612](https://github.com/apollographql/apollo-client/pull/3612) [@jinxac](https://github.com/jinxac) in [#3647](https://github.com/apollographql/apollo-client/pull/3647) + [@abernix](https://github.com/abernix) in [#3705](https://github.com/apollographql/apollo-client/pull/3705) - Updated `graphql` `peerDependencies` to handle 14.x versions. [@ivank](https://github.com/ivank) in [#3598](https://github.com/apollographql/apollo-client/pull/3598) - Add optional generic type params for variables on low level methods. From a0c57f6745eeced758bedaf5fa38f45c29dd97ba Mon Sep 17 00:00:00 2001 From: Hugh Willson Date: Thu, 19 Jul 2018 10:13:15 -0400 Subject: [PATCH 17/47] Changelog updates --- CHANGELOG.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e81893395f..785d3085941 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,8 +13,9 @@ [@chentsulin](https://github.com/chentsulin) in [#3608](https://github.com/apollographql/apollo-client/pull/3608) [@MikaelCarpenter](https://github.com/MikaelCarpenter) in [#3609](https://github.com/apollographql/apollo-client/pull/3609) [@Gamezpedia](https://github.com/Gamezpedia) in [#3612](https://github.com/apollographql/apollo-client/pull/3612) - [@jinxac](https://github.com/jinxac) in [#3647](https://github.com/apollographql/apollo-client/pull/3647) - [@abernix](https://github.com/abernix) in [#3705](https://github.com/apollographql/apollo-client/pull/3705) + [@jinxac](https://github.com/jinxac) in [#3647](https://github.com/apollographql/apollo-client/pull/3647) + [@abernix](https://github.com/abernix) in [#3705](https://github.com/apollographql/apollo-client/pull/3705) + [@dandv](https://github.com/dandv) in [#3703](https://github.com/apollographql/apollo-client/pull/3703) - Updated `graphql` `peerDependencies` to handle 14.x versions. [@ivank](https://github.com/ivank) in [#3598](https://github.com/apollographql/apollo-client/pull/3598) - Add optional generic type params for variables on low level methods. From b4b205d6e4b875c3eaaaff76a649a7854b171f3f Mon Sep 17 00:00:00 2001 From: Hugh Willson Date: Thu, 19 Jul 2018 13:20:58 -0400 Subject: [PATCH 18/47] chore: Publish - apollo-boost@0.2.0-alpha.0 - apollo-cache-inmemory@1.2.6-alpha.0 - apollo-cache@1.1.13-alpha.0 - apollo-client@2.4.0-alpha.0 - apollo-utilities@1.0.17-alpha.0 - graphql-anywhere@4.1.15-alpha.0 --- packages/apollo-boost/package.json | 10 +++++----- packages/apollo-cache-inmemory/package.json | 8 ++++---- packages/apollo-cache/package.json | 4 ++-- packages/apollo-client/package.json | 8 ++++---- packages/apollo-utilities/package.json | 2 +- packages/graphql-anywhere/package.json | 4 ++-- 6 files changed, 18 insertions(+), 18 deletions(-) diff --git a/packages/apollo-boost/package.json b/packages/apollo-boost/package.json index 1e59c85f6c5..19a335a7802 100644 --- a/packages/apollo-boost/package.json +++ b/packages/apollo-boost/package.json @@ -1,6 +1,6 @@ { "name": "apollo-boost", - "version": "0.1.10", + "version": "0.2.0-alpha.0", "description": "The easiest way to get started with Apollo Client", "author": "Peggy Rayzis ", "contributors": [ @@ -34,9 +34,9 @@ "filesize": "npm run build && npm run build:browser" }, "dependencies": { - "apollo-cache": "^1.1.12", - "apollo-cache-inmemory": "^1.2.5", - "apollo-client": "^2.3.5", + "apollo-cache": "^1.1.13-alpha.0", + "apollo-cache-inmemory": "^1.2.6-alpha.0", + "apollo-client": "^2.4.0-alpha.0", "apollo-link": "^1.0.6", "apollo-link-error": "^1.0.3", "apollo-link-http": "1.6.0-alpha.0", @@ -46,7 +46,7 @@ "devDependencies": { "@types/graphql": "0.12.7", "@types/jest": "22.2.3", - "apollo-utilities": "^1.0.16", + "apollo-utilities": "^1.0.17-alpha.0", "browserify": "15.2.0", "fetch-mock": "6.5.0", "graphql": "0.13.2", diff --git a/packages/apollo-cache-inmemory/package.json b/packages/apollo-cache-inmemory/package.json index 3407c5c3180..cb8a59a793a 100644 --- a/packages/apollo-cache-inmemory/package.json +++ b/packages/apollo-cache-inmemory/package.json @@ -1,6 +1,6 @@ { "name": "apollo-cache-inmemory", - "version": "1.2.5", + "version": "1.2.6-alpha.0", "description": "Core abstract of Caching layer for Apollo Client", "author": "James Baxley ", "contributors": [ @@ -40,9 +40,9 @@ "filesize": "npm run build:browser" }, "dependencies": { - "apollo-cache": "^1.1.12", - "apollo-utilities": "^1.0.16", - "graphql-anywhere": "^4.1.14" + "apollo-cache": "^1.1.13-alpha.0", + "apollo-utilities": "^1.0.17-alpha.0", + "graphql-anywhere": "^4.1.15-alpha.0" }, "peerDependencies": { "graphql": "0.11.7 || ^0.12.0 || ^0.13.0" diff --git a/packages/apollo-cache/package.json b/packages/apollo-cache/package.json index 52f12acaee1..7896d9d07fa 100644 --- a/packages/apollo-cache/package.json +++ b/packages/apollo-cache/package.json @@ -1,6 +1,6 @@ { "name": "apollo-cache", - "version": "1.1.12", + "version": "1.1.13-alpha.0", "description": "Core abstract of Caching layer for Apollo Client", "author": "James Baxley ", "contributors": [ @@ -39,7 +39,7 @@ "filesize": "npm run build && npm run build:browser" }, "dependencies": { - "apollo-utilities": "^1.0.16" + "apollo-utilities": "^1.0.17-alpha.0" }, "devDependencies": { "@types/graphql": "0.12.7", diff --git a/packages/apollo-client/package.json b/packages/apollo-client/package.json index c7a62f256be..6d1aa333d84 100644 --- a/packages/apollo-client/package.json +++ b/packages/apollo-client/package.json @@ -1,7 +1,7 @@ { "name": "apollo-client", "private": true, - "version": "2.3.5", + "version": "2.4.0-alpha.0", "description": "A simple yet functional GraphQL client.", "main": "./lib/bundle.umd.js", "module": "./lib/index.js", @@ -47,10 +47,10 @@ "license": "MIT", "dependencies": { "@types/zen-observable": "^0.8.0", - "apollo-cache": "^1.1.12", + "apollo-cache": "^1.1.13-alpha.0", "apollo-link": "^1.0.0", "apollo-link-dedup": "1.1.0-alpha.0", - "apollo-utilities": "^1.0.16", + "apollo-utilities": "^1.0.17-alpha.0", "symbol-observable": "^1.0.2", "zen-observable": "^0.8.0" }, @@ -64,7 +64,7 @@ "@types/jest": "22.2.3", "@types/lodash": "4.14.112", "@types/node": "10.5.2", - "apollo-cache-inmemory": "^1.2.5", + "apollo-cache-inmemory": "^1.2.6-alpha.0", "benchmark": "2.1.4", "browserify": "15.2.0", "bundlesize": "0.17.0", diff --git a/packages/apollo-utilities/package.json b/packages/apollo-utilities/package.json index d678739c0ba..091dfc8a964 100644 --- a/packages/apollo-utilities/package.json +++ b/packages/apollo-utilities/package.json @@ -1,6 +1,6 @@ { "name": "apollo-utilities", - "version": "1.0.16", + "version": "1.0.17-alpha.0", "description": "Utilities for working with GraphQL ASTs", "author": "James Baxley ", "contributors": [ diff --git a/packages/graphql-anywhere/package.json b/packages/graphql-anywhere/package.json index c584ade2fcd..83750a52ffa 100644 --- a/packages/graphql-anywhere/package.json +++ b/packages/graphql-anywhere/package.json @@ -1,6 +1,6 @@ { "name": "graphql-anywhere", - "version": "4.1.14", + "version": "4.1.15-alpha.0", "description": "Run GraphQL queries with no schema and just one resolver", "main": "./lib/bundle.umd.js", "module": "./lib/index.js", @@ -39,7 +39,7 @@ ], "license": "MIT", "dependencies": { - "apollo-utilities": "^1.0.16" + "apollo-utilities": "^1.0.17-alpha.0" }, "devDependencies": { "@types/graphql": "0.12.7", From 4f62157ca107f6a44509ff74dfa15c68e4c28a51 Mon Sep 17 00:00:00 2001 From: Hugh Willson Date: Thu, 19 Jul 2018 13:24:19 -0400 Subject: [PATCH 19/47] Slight changes to make alpha publishing easier --- package.json | 3 ++- packages/apollo-client/scripts/deploy.sh | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index dde6d4d11dd..ec30cc803bc 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,8 @@ "coverage": "lerna run -- coverage", "coverage:upload": "codecov", "danger": "danger run --verbose", - "deploy": "lerna publish -m \"chore: Publish\" --independent && cd packages/apollo-client && npm run deploy" + "deploy": "lerna publish -m \"chore: Publish\" --independent && cd packages/apollo-client && npm run deploy", + "deploy-alpha": "lerna publish --npm-tag alpha -m \"chore: Publish\" --independent && cd packages/apollo-client && npm run deploy" }, "bundlesize": [ { diff --git a/packages/apollo-client/scripts/deploy.sh b/packages/apollo-client/scripts/deploy.sh index 55775e84250..1bac7cc57d4 100755 --- a/packages/apollo-client/scripts/deploy.sh +++ b/packages/apollo-client/scripts/deploy.sh @@ -59,4 +59,4 @@ cp ../../LICENSE npm/ # flow typings # cp -R flow-typed npm/ -cd npm && npm publish +cd npm && npm publish --tag alpha From 00dccf3791bd7fb7d8b42d3f8e7deb68f158f896 Mon Sep 17 00:00:00 2001 From: Hugh Willson Date: Thu, 19 Jul 2018 13:29:24 -0400 Subject: [PATCH 20/47] chore: Publish - apollo-boost@0.2.0-alpha.1 - apollo-cache-inmemory@1.2.6-alpha.1 - apollo-cache@1.1.13-alpha.1 - apollo-client@2.4.0-alpha.1 - apollo-utilities@1.0.17-alpha.1 - graphql-anywhere@4.1.15-alpha.1 --- packages/apollo-boost/package.json | 10 +++++----- packages/apollo-cache-inmemory/package.json | 8 ++++---- packages/apollo-cache/package.json | 4 ++-- packages/apollo-client/package.json | 8 ++++---- packages/apollo-utilities/package.json | 2 +- packages/graphql-anywhere/package.json | 4 ++-- 6 files changed, 18 insertions(+), 18 deletions(-) diff --git a/packages/apollo-boost/package.json b/packages/apollo-boost/package.json index 19a335a7802..f0ad960a364 100644 --- a/packages/apollo-boost/package.json +++ b/packages/apollo-boost/package.json @@ -1,6 +1,6 @@ { "name": "apollo-boost", - "version": "0.2.0-alpha.0", + "version": "0.2.0-alpha.1", "description": "The easiest way to get started with Apollo Client", "author": "Peggy Rayzis ", "contributors": [ @@ -34,9 +34,9 @@ "filesize": "npm run build && npm run build:browser" }, "dependencies": { - "apollo-cache": "^1.1.13-alpha.0", - "apollo-cache-inmemory": "^1.2.6-alpha.0", - "apollo-client": "^2.4.0-alpha.0", + "apollo-cache": "^1.1.13-alpha.1", + "apollo-cache-inmemory": "^1.2.6-alpha.1", + "apollo-client": "^2.4.0-alpha.1", "apollo-link": "^1.0.6", "apollo-link-error": "^1.0.3", "apollo-link-http": "1.6.0-alpha.0", @@ -46,7 +46,7 @@ "devDependencies": { "@types/graphql": "0.12.7", "@types/jest": "22.2.3", - "apollo-utilities": "^1.0.17-alpha.0", + "apollo-utilities": "^1.0.17-alpha.1", "browserify": "15.2.0", "fetch-mock": "6.5.0", "graphql": "0.13.2", diff --git a/packages/apollo-cache-inmemory/package.json b/packages/apollo-cache-inmemory/package.json index cb8a59a793a..44b7fad84df 100644 --- a/packages/apollo-cache-inmemory/package.json +++ b/packages/apollo-cache-inmemory/package.json @@ -1,6 +1,6 @@ { "name": "apollo-cache-inmemory", - "version": "1.2.6-alpha.0", + "version": "1.2.6-alpha.1", "description": "Core abstract of Caching layer for Apollo Client", "author": "James Baxley ", "contributors": [ @@ -40,9 +40,9 @@ "filesize": "npm run build:browser" }, "dependencies": { - "apollo-cache": "^1.1.13-alpha.0", - "apollo-utilities": "^1.0.17-alpha.0", - "graphql-anywhere": "^4.1.15-alpha.0" + "apollo-cache": "^1.1.13-alpha.1", + "apollo-utilities": "^1.0.17-alpha.1", + "graphql-anywhere": "^4.1.15-alpha.1" }, "peerDependencies": { "graphql": "0.11.7 || ^0.12.0 || ^0.13.0" diff --git a/packages/apollo-cache/package.json b/packages/apollo-cache/package.json index 7896d9d07fa..5f2ac7a84e6 100644 --- a/packages/apollo-cache/package.json +++ b/packages/apollo-cache/package.json @@ -1,6 +1,6 @@ { "name": "apollo-cache", - "version": "1.1.13-alpha.0", + "version": "1.1.13-alpha.1", "description": "Core abstract of Caching layer for Apollo Client", "author": "James Baxley ", "contributors": [ @@ -39,7 +39,7 @@ "filesize": "npm run build && npm run build:browser" }, "dependencies": { - "apollo-utilities": "^1.0.17-alpha.0" + "apollo-utilities": "^1.0.17-alpha.1" }, "devDependencies": { "@types/graphql": "0.12.7", diff --git a/packages/apollo-client/package.json b/packages/apollo-client/package.json index 6d1aa333d84..b434d2f1e91 100644 --- a/packages/apollo-client/package.json +++ b/packages/apollo-client/package.json @@ -1,7 +1,7 @@ { "name": "apollo-client", "private": true, - "version": "2.4.0-alpha.0", + "version": "2.4.0-alpha.1", "description": "A simple yet functional GraphQL client.", "main": "./lib/bundle.umd.js", "module": "./lib/index.js", @@ -47,10 +47,10 @@ "license": "MIT", "dependencies": { "@types/zen-observable": "^0.8.0", - "apollo-cache": "^1.1.13-alpha.0", + "apollo-cache": "^1.1.13-alpha.1", "apollo-link": "^1.0.0", "apollo-link-dedup": "1.1.0-alpha.0", - "apollo-utilities": "^1.0.17-alpha.0", + "apollo-utilities": "^1.0.17-alpha.1", "symbol-observable": "^1.0.2", "zen-observable": "^0.8.0" }, @@ -64,7 +64,7 @@ "@types/jest": "22.2.3", "@types/lodash": "4.14.112", "@types/node": "10.5.2", - "apollo-cache-inmemory": "^1.2.6-alpha.0", + "apollo-cache-inmemory": "^1.2.6-alpha.1", "benchmark": "2.1.4", "browserify": "15.2.0", "bundlesize": "0.17.0", diff --git a/packages/apollo-utilities/package.json b/packages/apollo-utilities/package.json index 091dfc8a964..50bae97360b 100644 --- a/packages/apollo-utilities/package.json +++ b/packages/apollo-utilities/package.json @@ -1,6 +1,6 @@ { "name": "apollo-utilities", - "version": "1.0.17-alpha.0", + "version": "1.0.17-alpha.1", "description": "Utilities for working with GraphQL ASTs", "author": "James Baxley ", "contributors": [ diff --git a/packages/graphql-anywhere/package.json b/packages/graphql-anywhere/package.json index 83750a52ffa..c18956e48c5 100644 --- a/packages/graphql-anywhere/package.json +++ b/packages/graphql-anywhere/package.json @@ -1,6 +1,6 @@ { "name": "graphql-anywhere", - "version": "4.1.15-alpha.0", + "version": "4.1.15-alpha.1", "description": "Run GraphQL queries with no schema and just one resolver", "main": "./lib/bundle.umd.js", "module": "./lib/index.js", @@ -39,7 +39,7 @@ ], "license": "MIT", "dependencies": { - "apollo-utilities": "^1.0.17-alpha.0" + "apollo-utilities": "^1.0.17-alpha.1" }, "devDependencies": { "@types/graphql": "0.12.7", From a9483b117ff5c47a3a0aa1a9b3c9dc1e2b5528a4 Mon Sep 17 00:00:00 2001 From: clarencenpy Date: Thu, 19 Jul 2018 18:02:03 -0700 Subject: [PATCH 21/47] Made changes based on comments received --- .../apollo-client/src/core/ObservableQuery.ts | 2 +- .../apollo-client/src/core/QueryManager.ts | 220 +++++++++--------- packages/apollo-client/src/core/types.ts | 6 +- packages/apollo-client/src/data/queries.ts | 72 +++--- packages/apollo-client/src/data/store.ts | 3 - 5 files changed, 158 insertions(+), 145 deletions(-) diff --git a/packages/apollo-client/src/core/ObservableQuery.ts b/packages/apollo-client/src/core/ObservableQuery.ts index 05219bb963c..16021dc5655 100644 --- a/packages/apollo-client/src/core/ObservableQuery.ts +++ b/packages/apollo-client/src/core/ObservableQuery.ts @@ -220,7 +220,7 @@ export class ObservableQuery< } if (queryStoreValue) { - result.loadingState = queryStoreValue.loadingState; + result.loadingState = queryStoreValue.compactedLoadingState; } if (!partial) { diff --git a/packages/apollo-client/src/core/QueryManager.ts b/packages/apollo-client/src/core/QueryManager.ts index 4358d5a5a78..205a801641c 100644 --- a/packages/apollo-client/src/core/QueryManager.ts +++ b/packages/apollo-client/src/core/QueryManager.ts @@ -616,7 +616,7 @@ export class QueryManager { // If loadingState is present, this is a patch from a deferred // query, and we should always treat it as a different result // even though the actual data might be the same (i.e. the patch's - // data could be null. + // data could be null). try { observer.next(maybeDeepFreeze(resultFromStore)); } catch (e) { @@ -1110,7 +1110,7 @@ export class QueryManager { // field, when the initial response arrives. let loadingState; if (!isPatch(result) && hasDirectives(['defer'], document)) { - loadingState = initDeferredFieldLoadingStates(document); + loadingState = this.initDeferredFieldLoadingStates(document); } this.queryStore.markQueryResult( @@ -1259,117 +1259,129 @@ export class QueryManager { }, }; } -} -/** - * Recursive function that extracts a tree that mirrors the shape of the query, - * adding _loading property to fields which are deferred. - */ -function extractDeferredFieldsToTree( - selection: SelectionNode, - fragmentMap: Record, -): Record | boolean { - const hasDeferDirective: boolean = (selection.directives && - selection.directives.length > 0 && - selection.directives.findIndex(directive => { - return directive.name.value === 'defer'; - }) !== -1) as boolean; - const isLeaf: boolean = - selection.kind !== Kind.INLINE_FRAGMENT && - (!(selection as FieldNode).selectionSet || - (selection as FieldNode).selectionSet!.selections.length === 0); - - if (isLeaf) { - return hasDeferDirective ? { _loading: true } : true; + /** + * Given a DocumentNode, traverse the tree and initialize loading states for + * all deferred fields. + */ + private initDeferredFieldLoadingStates(doc: DocumentNode) { + // Collect all the fragment definitions + const fragmentMap: Record = {}; + doc.definitions + .filter(definition => definition.kind === Kind.FRAGMENT_DEFINITION) + .forEach(definition => { + const fragmentName = (definition as FragmentDefinitionNode).name.value; + fragmentMap[ + fragmentName + ] = (definition as FragmentDefinitionNode).selectionSet.selections; + }); + + const operationDefinition = doc.definitions.filter( + definition => definition.kind === Kind.OPERATION_DEFINITION, + )[0]; // Take the first element since we do not support multiple operations + + return (this.extractDeferredFieldsToTree( + operationDefinition as any, + fragmentMap, + ) as Record)._children; } - const map: { _loading?: boolean; _children?: Record } = { - _loading: hasDeferDirective ? true : undefined, - _children: undefined, - }; - - // Replace FragmentSpreads with its actual selectionSet - const expandedFragments: SelectionNode[] = []; - (selection as FieldNode).selectionSet!.selections.forEach(childSelection => { - if (childSelection.kind === Kind.FRAGMENT_SPREAD) { - const fragmentName = childSelection.name.value; - fragmentMap[fragmentName].forEach((selection: SelectionNode) => { - expandedFragments.push(selection); - }); + /** + * Recursive function that extracts a tree that mirrors the shape of the query, + * adding _loading property to fields which are deferred. + */ + private extractDeferredFieldsToTree( + selection: SelectionNode, + fragmentMap: Record, + ): Record | boolean { + const hasDeferDirective: boolean = (selection.directives && + selection.directives.length > 0 && + selection.directives.findIndex(directive => { + return directive.name.value === 'defer'; + }) !== -1) as boolean; + const isLeaf: boolean = + selection.kind !== Kind.INLINE_FRAGMENT && + (!(selection as FieldNode).selectionSet || + (selection as FieldNode).selectionSet!.selections.length === 0); + + if (isLeaf) { + return hasDeferDirective ? { _loading: true } : true; } - }); - - // Remove FragmentSpreads - (selection as FieldNode).selectionSet!.selections = (selection as FieldNode).selectionSet!.selections.filter( - selection => selection.kind !== Kind.FRAGMENT_SPREAD, - ); - - // Add expanded FragmentSpreads to the current selection set - expandedFragments.forEach(fragSelection => { - const fragFieldName = (fragSelection as FieldNode).name.value; - const existingSelection = (selection as FieldNode).selectionSet!.selections.find( - selection => - selection.kind !== Kind.INLINE_FRAGMENT && - (selection as FieldNode).name.value === fragFieldName, + + const map: { _loading?: boolean; _children?: Record } = { + _loading: hasDeferDirective ? true : undefined, + _children: undefined, + }; + + // Replace FragmentSpreads with its actual selectionSet + const expandedFragments: SelectionNode[] = []; + (selection as FieldNode).selectionSet!.selections.forEach( + childSelection => { + if (childSelection.kind === Kind.FRAGMENT_SPREAD) { + const fragmentName = childSelection.name.value; + fragmentMap[fragmentName].forEach((selection: SelectionNode) => { + expandedFragments.push(selection); + }); + } + }, ); - if (existingSelection) { - const fragSelectionHasDefer = - fragSelection.directives && - fragSelection.directives.findIndex( - directive => directive.name.value === 'defer', - ) >= 0; - if (!fragSelectionHasDefer) { - // Make sure that the existingSelection is not deferred, since all - // selections of the field must specify defer in order for the field - // to be deferred. This should match the behavior on apollo-server. - if (existingSelection.directives) { - existingSelection.directives = existingSelection.directives.filter( - directive => directive.name.value !== 'defer', - ); + + // Remove FragmentSpreads + (selection as FieldNode).selectionSet!.selections = (selection as FieldNode).selectionSet!.selections.filter( + selection => selection.kind !== Kind.FRAGMENT_SPREAD, + ); + + // Add expanded FragmentSpreads to the current selection set + expandedFragments.forEach(fragSelection => { + const fragFieldName = (fragSelection as FieldNode).name.value; + const existingSelection = (selection as FieldNode).selectionSet!.selections.find( + selection => + selection.kind !== Kind.INLINE_FRAGMENT && + (selection as FieldNode).name.value === fragFieldName, + ); + if (existingSelection) { + const fragSelectionHasDefer = + fragSelection.directives && + fragSelection.directives.findIndex( + directive => directive.name.value === 'defer', + ) >= 0; + if (!fragSelectionHasDefer) { + // Make sure that the existingSelection is not deferred, since all + // selections of the field must specify defer in order for the field + // to be deferred. This should match the behavior on apollo-server. + if (existingSelection.directives) { + existingSelection.directives = existingSelection.directives.filter( + directive => directive.name.value !== 'defer', + ); + } } + } else { + // Add it to the selectionSet + (selection as FieldNode).selectionSet!.selections.push(fragSelection); } - } else { - // Add it to the selectionSet - (selection as FieldNode).selectionSet!.selections.push(fragSelection); - } - }); - - for (const childSelection of (selection as FieldNode).selectionSet! - .selections) { - const subtree = extractDeferredFieldsToTree(childSelection, fragmentMap); - if (subtree !== undefined) { - if (childSelection.kind === Kind.INLINE_FRAGMENT) { - if (typeof subtree !== 'boolean') { - map._children = Object.assign(map._children || {}, subtree._children); + }); + + for (const childSelection of (selection as FieldNode).selectionSet! + .selections) { + const subtree = this.extractDeferredFieldsToTree( + childSelection, + fragmentMap, + ); + if (subtree !== undefined) { + if (childSelection.kind === Kind.INLINE_FRAGMENT) { + if (typeof subtree !== 'boolean') { + map._children = Object.assign( + map._children || {}, + subtree._children, + ); + } + } else { + if (!map._children) map._children = {}; + const name = (childSelection as FieldNode).name.value; + map._children[name] = subtree; } - } else { - if (!map._children) map._children = {}; - const name = (childSelection as FieldNode).name.value; - map._children[name] = subtree; } } + return map; } - return map; -} - -function initDeferredFieldLoadingStates(doc: DocumentNode) { - // Collect all the fragment definitions - const fragmentMap: Record = {}; - doc.definitions - .filter(definition => definition.kind === Kind.FRAGMENT_DEFINITION) - .forEach(definition => { - const fragmentName = (definition as FragmentDefinitionNode).name.value; - fragmentMap[ - fragmentName - ] = (definition as FragmentDefinitionNode).selectionSet.selections; - }); - - const operationDefinition = doc.definitions.filter( - definition => definition.kind === Kind.OPERATION_DEFINITION, - )[0]; // Take the first element since we do not support multiple operations - - return (extractDeferredFieldsToTree( - operationDefinition as any, - fragmentMap, - ) as Record)._children; } diff --git a/packages/apollo-client/src/core/types.ts b/packages/apollo-client/src/core/types.ts index bd3ece1b48f..eb366d4f840 100644 --- a/packages/apollo-client/src/core/types.ts +++ b/packages/apollo-client/src/core/types.ts @@ -46,13 +46,11 @@ export type MutationQueryReducersMap = { /** * Define a new type for patches that are sent as a result of using defer. - * Its is basically the same as ExecutionResult, except that it has a "path" + * It is basically the same as ExecutionResult, except that it has a "path" * field that keeps track of the where the patch is to be merged with the * original result. */ -export interface ExecutionPatchResult { - data?: { [key: string]: any }; - errors?: GraphQLError[]; +export interface ExecutionPatchResult extends ExecutionResult { path: (string | number)[]; } diff --git a/packages/apollo-client/src/data/queries.ts b/packages/apollo-client/src/data/queries.ts index bf4a439f9ca..299caba66f2 100644 --- a/packages/apollo-client/src/data/queries.ts +++ b/packages/apollo-client/src/data/queries.ts @@ -13,8 +13,8 @@ export type QueryStoreValue = { networkStatus: NetworkStatus; networkError?: Error | null; graphQLErrors?: GraphQLError[]; - _loadingState?: Record; loadingState?: Record; + compactedLoadingState?: Record; metadata: any; }; @@ -126,15 +126,17 @@ export class QueryStore { // Set up loadingState if it is passed in by QueryManager if (loadingState) { - this.store[queryId]._loadingState = loadingState; - this.store[queryId].loadingState = compactLoadingStateTree(loadingState); + this.store[queryId].loadingState = loadingState; + this.store[queryId].compactedLoadingState = this.compactLoadingStateTree( + loadingState, + ); } if (isPatch(result)) { // Update loadingState for every patch received, by traversing its path const path = (result as ExecutionPatchResult).path; let index = 0; - const copy = cloneDeep(this.store[queryId]._loadingState); + const copy = cloneDeep(this.store[queryId].loadingState); let curPointer = copy; while (index < path.length) { const key = path[index++]; @@ -164,8 +166,10 @@ export class QueryStore { } } - this.store[queryId]._loadingState = copy; - this.store[queryId].loadingState = compactLoadingStateTree(copy); + this.store[queryId].loadingState = copy; + this.store[queryId].compactedLoadingState = this.compactLoadingStateTree( + copy, + ); // Merge graphqlErrors from patch, if any if (result.errors) { @@ -249,35 +253,37 @@ export class QueryStore { {} as { [queryId: string]: QueryStoreValue }, ); } -} -/** - * Given a loadingState tree, it returns a compacted version of it that - * reduces the amount of boilerplate code required to access nested fields. - * The structure of this will mirror the response data, with deferred fields - * set to undefined until its patch is received. - */ -function compactLoadingStateTree( - loadingState?: Record, -): Record | undefined { - if (!loadingState) return loadingState; - const state: Record = {}; - - for (let key in loadingState) { - const o = loadingState[key]; - if (o._loading) { - continue; - } - if (o._children) { - if (Array.isArray(o._children)) { - state[key] = o._children.map((c: any) => compactLoadingStateTree(c)); - } else { - state[key] = compactLoadingStateTree(o._children); + /** + * Given a loadingState tree, it returns a compacted version of it that + * reduces the amount of boilerplate code required to access nested fields. + * The structure of this will mirror the response data, with deferred fields + * set to undefined until its patch is received. + */ + private compactLoadingStateTree( + loadingState?: Record, + ): Record | undefined { + if (!loadingState) return loadingState; + const state: Record = {}; + + for (let key in loadingState) { + const o = loadingState[key]; + if (o._loading) { + continue; } - continue; + if (o._children) { + if (Array.isArray(o._children)) { + state[key] = o._children.map((c: any) => + this.compactLoadingStateTree(c), + ); + } else { + state[key] = this.compactLoadingStateTree(o._children); + } + continue; + } + state[key] = true; } - state[key] = true; - } - return state; + return state; + } } diff --git a/packages/apollo-client/src/data/store.ts b/packages/apollo-client/src/data/store.ts index f0a0ca7d706..86c7d74eef9 100644 --- a/packages/apollo-client/src/data/store.ts +++ b/packages/apollo-client/src/data/store.ts @@ -41,9 +41,6 @@ export class DataStore { result: ExecutionResult, patch: ExecutionPatchResult, ): void { - if (patch.errors) { - } - if (result) { let curKeyIndex = 0; let curKey: string | number; From b72cf3e08d6c233308b4c2ee442ad86b2bb7277f Mon Sep 17 00:00:00 2001 From: clarencenpy Date: Fri, 20 Jul 2018 17:11:21 -0700 Subject: [PATCH 22/47] Initialize loadingState correctly when an array is returned in the initial response --- .../apollo-client/src/core/QueryManager.ts | 69 ++++++++++++++----- packages/apollo-client/src/data/queries.ts | 2 +- 2 files changed, 53 insertions(+), 18 deletions(-) diff --git a/packages/apollo-client/src/core/QueryManager.ts b/packages/apollo-client/src/core/QueryManager.ts index 205a801641c..7d6a5dbc29d 100644 --- a/packages/apollo-client/src/core/QueryManager.ts +++ b/packages/apollo-client/src/core/QueryManager.ts @@ -1110,7 +1110,10 @@ export class QueryManager { // field, when the initial response arrives. let loadingState; if (!isPatch(result) && hasDirectives(['defer'], document)) { - loadingState = this.initDeferredFieldLoadingStates(document); + loadingState = this.initDeferredFieldLoadingStates( + document, + result, + ); } this.queryStore.markQueryResult( @@ -1264,7 +1267,10 @@ export class QueryManager { * Given a DocumentNode, traverse the tree and initialize loading states for * all deferred fields. */ - private initDeferredFieldLoadingStates(doc: DocumentNode) { + private initDeferredFieldLoadingStates( + doc: DocumentNode, + result: ExecutionResult, + ) { // Collect all the fragment definitions const fragmentMap: Record = {}; doc.definitions @@ -1283,16 +1289,21 @@ export class QueryManager { return (this.extractDeferredFieldsToTree( operationDefinition as any, fragmentMap, + result.data, ) as Record)._children; } /** * Recursive function that extracts a tree that mirrors the shape of the query, - * adding _loading property to fields which are deferred. + * adding _loading property to fields which are deferred. Expands + * FragmentSpread according to the fragment map that is passed in. + * The actual data from the initial response is passed in so that we can + * reference the query schema against the data, and handle arrays that we find. */ private extractDeferredFieldsToTree( selection: SelectionNode, fragmentMap: Record, + data: Record | undefined, ): Record | boolean { const hasDeferDirective: boolean = (selection.directives && selection.directives.length > 0 && @@ -1363,22 +1374,46 @@ export class QueryManager { for (const childSelection of (selection as FieldNode).selectionSet! .selections) { - const subtree = this.extractDeferredFieldsToTree( - childSelection, - fragmentMap, - ); - if (subtree !== undefined) { - if (childSelection.kind === Kind.INLINE_FRAGMENT) { - if (typeof subtree !== 'boolean') { - map._children = Object.assign( - map._children || {}, - subtree._children, - ); + if (childSelection.kind === Kind.INLINE_FRAGMENT) { + const subtree = this.extractDeferredFieldsToTree( + childSelection, + fragmentMap, + data, + ); + if (typeof subtree !== 'boolean') { + // Not a leaf node + map._children = Object.assign(map._children || {}, subtree._children); + } + } else { + const childName = (childSelection as FieldNode).name.value; + let childData; + let isArray = false; + if (data) { + childData = data[childName]; + isArray = Array.isArray(childData); + } + const subtree = this.extractDeferredFieldsToTree( + childSelection, + fragmentMap, + // Just pass in the first elem of array, all of them will have + // the same fields + isArray && childData.length !== 0 ? childData[0] : childData, + ); + + if (!map._children) map._children = {}; + if (isArray) { + // Make sure that the shape of loadingState matches the shape of the + // data. If an array is returned for a field, the loadingState should + // be initialized with the correct number of elements. + const subtreeArr = []; + for (let i = 0; i < childData.length; i++) { + if (typeof subtree !== 'boolean') { + subtreeArr.push(subtree._children); + } } + map._children[childName] = { _children: subtreeArr }; } else { - if (!map._children) map._children = {}; - const name = (childSelection as FieldNode).name.value; - map._children[name] = subtree; + map._children[childName] = subtree; } } } diff --git a/packages/apollo-client/src/data/queries.ts b/packages/apollo-client/src/data/queries.ts index 299caba66f2..3c71c48404a 100644 --- a/packages/apollo-client/src/data/queries.ts +++ b/packages/apollo-client/src/data/queries.ts @@ -160,7 +160,7 @@ export class QueryStore { curPointer!._loading = false; break; } - if (curPointer!._children) { + if (curPointer && curPointer!._children) { curPointer = curPointer!._children; } } From 01b8e0dffb338d049db7b5f230f0622baf02815c Mon Sep 17 00:00:00 2001 From: clarencenpy Date: Mon, 23 Jul 2018 06:56:33 -0700 Subject: [PATCH 23/47] chore: Publish - apollo-boost@0.2.0-alpha.2 - apollo-cache-inmemory@1.2.6-alpha.2 - apollo-cache@1.1.13-alpha.2 - apollo-client@2.4.0-alpha.2 - apollo-utilities@1.0.17-alpha.2 - graphql-anywhere@4.1.15-alpha.2 --- packages/apollo-boost/package.json | 10 +++++----- packages/apollo-cache-inmemory/package.json | 8 ++++---- packages/apollo-cache/package.json | 4 ++-- packages/apollo-client/package.json | 8 ++++---- packages/apollo-utilities/package.json | 2 +- packages/graphql-anywhere/package.json | 4 ++-- 6 files changed, 18 insertions(+), 18 deletions(-) diff --git a/packages/apollo-boost/package.json b/packages/apollo-boost/package.json index f0ad960a364..ad948e1005e 100644 --- a/packages/apollo-boost/package.json +++ b/packages/apollo-boost/package.json @@ -1,6 +1,6 @@ { "name": "apollo-boost", - "version": "0.2.0-alpha.1", + "version": "0.2.0-alpha.2", "description": "The easiest way to get started with Apollo Client", "author": "Peggy Rayzis ", "contributors": [ @@ -34,9 +34,9 @@ "filesize": "npm run build && npm run build:browser" }, "dependencies": { - "apollo-cache": "^1.1.13-alpha.1", - "apollo-cache-inmemory": "^1.2.6-alpha.1", - "apollo-client": "^2.4.0-alpha.1", + "apollo-cache": "^1.1.13-alpha.2", + "apollo-cache-inmemory": "^1.2.6-alpha.2", + "apollo-client": "^2.4.0-alpha.2", "apollo-link": "^1.0.6", "apollo-link-error": "^1.0.3", "apollo-link-http": "1.6.0-alpha.0", @@ -46,7 +46,7 @@ "devDependencies": { "@types/graphql": "0.12.7", "@types/jest": "22.2.3", - "apollo-utilities": "^1.0.17-alpha.1", + "apollo-utilities": "^1.0.17-alpha.2", "browserify": "15.2.0", "fetch-mock": "6.5.0", "graphql": "0.13.2", diff --git a/packages/apollo-cache-inmemory/package.json b/packages/apollo-cache-inmemory/package.json index 44b7fad84df..fb9a351221a 100644 --- a/packages/apollo-cache-inmemory/package.json +++ b/packages/apollo-cache-inmemory/package.json @@ -1,6 +1,6 @@ { "name": "apollo-cache-inmemory", - "version": "1.2.6-alpha.1", + "version": "1.2.6-alpha.2", "description": "Core abstract of Caching layer for Apollo Client", "author": "James Baxley ", "contributors": [ @@ -40,9 +40,9 @@ "filesize": "npm run build:browser" }, "dependencies": { - "apollo-cache": "^1.1.13-alpha.1", - "apollo-utilities": "^1.0.17-alpha.1", - "graphql-anywhere": "^4.1.15-alpha.1" + "apollo-cache": "^1.1.13-alpha.2", + "apollo-utilities": "^1.0.17-alpha.2", + "graphql-anywhere": "^4.1.15-alpha.2" }, "peerDependencies": { "graphql": "0.11.7 || ^0.12.0 || ^0.13.0" diff --git a/packages/apollo-cache/package.json b/packages/apollo-cache/package.json index 5f2ac7a84e6..0358a50262c 100644 --- a/packages/apollo-cache/package.json +++ b/packages/apollo-cache/package.json @@ -1,6 +1,6 @@ { "name": "apollo-cache", - "version": "1.1.13-alpha.1", + "version": "1.1.13-alpha.2", "description": "Core abstract of Caching layer for Apollo Client", "author": "James Baxley ", "contributors": [ @@ -39,7 +39,7 @@ "filesize": "npm run build && npm run build:browser" }, "dependencies": { - "apollo-utilities": "^1.0.17-alpha.1" + "apollo-utilities": "^1.0.17-alpha.2" }, "devDependencies": { "@types/graphql": "0.12.7", diff --git a/packages/apollo-client/package.json b/packages/apollo-client/package.json index b434d2f1e91..107d7742857 100644 --- a/packages/apollo-client/package.json +++ b/packages/apollo-client/package.json @@ -1,7 +1,7 @@ { "name": "apollo-client", "private": true, - "version": "2.4.0-alpha.1", + "version": "2.4.0-alpha.2", "description": "A simple yet functional GraphQL client.", "main": "./lib/bundle.umd.js", "module": "./lib/index.js", @@ -47,10 +47,10 @@ "license": "MIT", "dependencies": { "@types/zen-observable": "^0.8.0", - "apollo-cache": "^1.1.13-alpha.1", + "apollo-cache": "^1.1.13-alpha.2", "apollo-link": "^1.0.0", "apollo-link-dedup": "1.1.0-alpha.0", - "apollo-utilities": "^1.0.17-alpha.1", + "apollo-utilities": "^1.0.17-alpha.2", "symbol-observable": "^1.0.2", "zen-observable": "^0.8.0" }, @@ -64,7 +64,7 @@ "@types/jest": "22.2.3", "@types/lodash": "4.14.112", "@types/node": "10.5.2", - "apollo-cache-inmemory": "^1.2.6-alpha.1", + "apollo-cache-inmemory": "^1.2.6-alpha.2", "benchmark": "2.1.4", "browserify": "15.2.0", "bundlesize": "0.17.0", diff --git a/packages/apollo-utilities/package.json b/packages/apollo-utilities/package.json index 50bae97360b..0101ca22ea0 100644 --- a/packages/apollo-utilities/package.json +++ b/packages/apollo-utilities/package.json @@ -1,6 +1,6 @@ { "name": "apollo-utilities", - "version": "1.0.17-alpha.1", + "version": "1.0.17-alpha.2", "description": "Utilities for working with GraphQL ASTs", "author": "James Baxley ", "contributors": [ diff --git a/packages/graphql-anywhere/package.json b/packages/graphql-anywhere/package.json index c18956e48c5..2c451d987a6 100644 --- a/packages/graphql-anywhere/package.json +++ b/packages/graphql-anywhere/package.json @@ -1,6 +1,6 @@ { "name": "graphql-anywhere", - "version": "4.1.15-alpha.1", + "version": "4.1.15-alpha.2", "description": "Run GraphQL queries with no schema and just one resolver", "main": "./lib/bundle.umd.js", "module": "./lib/index.js", @@ -39,7 +39,7 @@ ], "license": "MIT", "dependencies": { - "apollo-utilities": "^1.0.17-alpha.1" + "apollo-utilities": "^1.0.17-alpha.2" }, "devDependencies": { "@types/graphql": "0.12.7", From 7be7bc4fb96173d56192274354df079c448033ac Mon Sep 17 00:00:00 2001 From: clarencenpy Date: Mon, 23 Jul 2018 22:37:03 -0700 Subject: [PATCH 24/47] Add client docs for @defer --- docs/_config.yml | 1 + docs/source/features/defer-support.md | 166 ++++++++++++++++++++++++++ 2 files changed, 167 insertions(+) create mode 100644 docs/source/features/defer-support.md diff --git a/docs/_config.yml b/docs/_config.yml index b30976afb39..3a79adb059d 100644 --- a/docs/_config.yml +++ b/docs/_config.yml @@ -37,6 +37,7 @@ sidebar_categories: - features/server-side-rendering - features/performance - features/developer-tooling + - features/defer-support Advanced: - advanced/boost-migration - advanced/subscriptions diff --git a/docs/source/features/defer-support.md b/docs/source/features/defer-support.md new file mode 100644 index 00000000000..3d5c323c498 --- /dev/null +++ b/docs/source/features/defer-support.md @@ -0,0 +1,166 @@ +--- +title: Defer Support +description: Optimize data loading with the @defer directive +--- + +

The @defer Directive

+ +Many applications that use Apollo fetch data from a variety of microservices, which may all have varying latencies and cache characteristics. Apollo comes with a built-in directive for deferring parts of your GraphQL query in a declarative way, so that fields that take a long time to resolve do not need to slow down your entire query. + +There are 3 main reasons why you may want to defer a field: + +1. **Field is expensive to load.** This includes private data that is not cached (like user progress), or information that requires more computation on the backend (like calculating price quotes on Airbnb). +2. **Field is not on the critical path for interactivity.** This includes the comments section of a story, or the number of claps received. +3. **Field is expensive to send.** Even if the field may resolve quickly (ready to send back), users might still choose to defer it if the cost of transport is too expensive. + +

Motivating Example

+```graphql +query NewsFeed { + newsFeed { + stories { + text + comments { + text + } + } + recommendedForYou { + story { + text + comments { + text + } + } + matchScore + } + } +} +``` +Given the above query that populates a NewsFeed page, observe that the time needed for different fields to resolve may be significantly different. `stories` is highly public data that we can cache in CDNs (fast), while `recommendedForYou` is personalized and may need to be computed for every user (slooow). Also, we might not need `comments` to be displayed immediately, so slowing down our query to wait for them to be fetched is not the best idea. + +We can rewrite the above query with `@defer`: + +```graphql +query NewsFeed { + newsFeed { + stories { + text + comments @defer { + text + } + } + recommendedForYou @defer { + story { + text + comments @defer { + text + } + } + matchScore + } + } +} +``` + +Under the hood, Apollo Server will return an initial response without waiting for deferred fields to resolve, before streaming patches for each deferred field asynchronously as they complete. + +```json +// Initial response +{ + "data": { + "newsFeed": { + "stories": [{ "text": "...", "comments": null }], + "recommendedForYou": null + } + } +} +``` + +```json +// Patch for "recommendedForYou" +{ + "path": ["newsFeed", "recommendedForYou"], + "data": [ + { + "story": { + "text": "..." + }, + "matchScore": 99 + } + ] +} +``` + +```json +// Patch for "comments", sent for each story +{ + "path": ["newsFeed", "stories", 1, "comments"], + "data": [ + { + "text": "..." + } + ] +} +``` + +If an error is thrown within a resolver, they get sent along with its closest deferred parent, and get merged with the `graphQLErrors` array. + +

Distinguishing between "pending" and "null"

+ +You may have noticed that deferred fields get returned as null in the initial response. So how can we know which fields are pending so that we can show some loading indicator? To deal with that, Apollo Client now exposes field-level loading information in a new property called loadingState that you can check for in your UI components. The shape of loadingState mirrors that of your data: + +```jsx harmony + + {({ loading, error, data, loadingState }) => { + if (loading) return 'loading...'; + return loadingState.newsFeed.recommendedForYou + ? data.newsFeed.recommendedForYou + ? data /* render component here */ + : 'No recommended content' + : 'Loading recommended content'; + }} + +``` + +

Transport

+ +There is no additional set up required to use `@defer`. By default, deferred responses are transmitted using [Multipart HTTP](https://www.w3.org/Protocols/rfc1341/7_2_Multipart.html), which is supported by `apollo-link-http`. + +

Where can I use @defer?

+ +- `@defer` can be applied on any `FIELD` of a `Query` operation. It is illegal to use `@defer` on an `INLINE_FRAGMENT` or `FRAGMENT_SPREAD`. + +- Mutations: Not supported. + +- Non-Nullable Types: Not allowed and will throw a validation error. This is because deferred fields are returned as `null` in the initial response. Deferring non-nullable types may also lead to unexpected behavior when errors occur, since errors will propagate up to the nearest nullable parent as per the GraphQL spec. We want to avoid letting errors on deferred fields clobber the initial data that was loaded already. + +- Nesting: `@defer` can be nested arbitrarily. For example, we can defer a list type, and defer a field on an object in the list. During execution, we ensure that the patch for a parent field will be sent before its children, even if the child object resolves first. This will simplify the logic for merging patches. + +- Use in GraphQL fragments: Supported. If there are multiple declarations of a field within the query, **all** of them have to contain `@defer` for the field to be deferred. This could happen if we have use a fragment like this: + + ```graphql + fragment StoryDetail on Story { + id + text + } + query { + newsFeed { + stories { + text @defer + ...StoryDetail + } + } + } + ``` + In this case, `text` will not be deferred since `@defer` was not applied in the fragment definition. + + A common pattern around fragments is to bind it to a component and reuse them across different parts of your UI. This is why it would be ideal to make sure that the `@defer` behavior of fields in a fragment is not overridden. + +

Performance Considerations

+ +`@defer` is one of those features that work best if used in moderation. If it is used too granularly (on many nested fields), the overhead of performing patching and re-rendering could be worse than just waiting for the full query to resolve. Try to limit `@defer` to fields that take a significantly longer time to load. This is super easy to figure out if you have Apollo Engine set up! + +

Use with other GraphQL servers

+ +If you are sending queries to a GraphQL server that does not support `@defer`, it is likely that the `@defer` directive is simply ignored, or a GraphQL validation error is thrown. + +If you would like to implement a GraphQL server that is able to interoperate with Apollo Client, please look at the documentation [here](https://github.com/apollographql/apollo-server/blob/defer-support/docs/source/defer-support.md). From 5f1c6e09e7ab7892f849b56783b58d6a2bdce2a3 Mon Sep 17 00:00:00 2001 From: clarencenpy Date: Tue, 24 Jul 2018 14:17:32 -0700 Subject: [PATCH 25/47] Set `returnPartialData` to be true for deferred queries This is so that we suppress errors on missing fields during a cache read, potentially causing us to overwrite data that we already received. --- .../apollo-cache-inmemory/src/inMemoryCache.ts | 3 ++- .../apollo-cache-inmemory/src/readFromStore.ts | 9 ++++----- packages/apollo-cache/src/cache.ts | 4 +--- packages/apollo-client/src/core/QueryManager.ts | 15 ++++++++++++--- packages/apollo-client/src/data/store.ts | 1 + 5 files changed, 20 insertions(+), 12 deletions(-) diff --git a/packages/apollo-cache-inmemory/src/inMemoryCache.ts b/packages/apollo-cache-inmemory/src/inMemoryCache.ts index bb05861af0c..b833cbbf3b4 100644 --- a/packages/apollo-cache-inmemory/src/inMemoryCache.ts +++ b/packages/apollo-cache-inmemory/src/inMemoryCache.ts @@ -86,7 +86,7 @@ export class InMemoryCache extends ApolloCache { return this.data.toObject(); } - public read(query: Cache.ReadOptions): T | null { + public read(query: Cache.DiffOptions): T | null { if (query.rootId && this.data.get(query.rootId) === undefined) { return null; } @@ -99,6 +99,7 @@ export class InMemoryCache extends ApolloCache { fragmentMatcherFunction: this.config.fragmentMatcher.match, previousResult: query.previousResult, config: this.config, + returnPartialData: query.returnPartialData, }); } diff --git a/packages/apollo-cache-inmemory/src/readFromStore.ts b/packages/apollo-cache-inmemory/src/readFromStore.ts index 5e3607ed5cf..85a1f03821e 100644 --- a/packages/apollo-cache-inmemory/src/readFromStore.ts +++ b/packages/apollo-cache-inmemory/src/readFromStore.ts @@ -16,7 +16,6 @@ import { import { Cache } from 'apollo-cache'; import { - ReadQueryOptions, IdValueWithPreviousResult, ReadStoreContext, DiffQueryAgainstStoreOptions, @@ -49,13 +48,13 @@ export const ID_KEY = typeof Symbol !== 'undefined' ? Symbol('id') : '@@id'; * will be returned to preserve referential equality. */ export function readQueryFromStore( - options: ReadQueryOptions, + options: DiffQueryAgainstStoreOptions, ): QueryType { - const optsPatch = { returnPartialData: false }; - + // Defaults returnPartialData to false unless true is passed in + // for a deferred query return diffQueryAgainstStore({ ...options, - ...optsPatch, + returnPartialData: options.returnPartialData || false, }).result; } diff --git a/packages/apollo-cache/src/cache.ts b/packages/apollo-cache/src/cache.ts index af71d5df3bc..4650f4ce0bf 100644 --- a/packages/apollo-cache/src/cache.ts +++ b/packages/apollo-cache/src/cache.ts @@ -9,9 +9,7 @@ export type Transaction = (c: ApolloCache) => void; export abstract class ApolloCache implements DataProxy { // required to implement // core API - public abstract read( - query: Cache.ReadOptions, - ): T | null; + public abstract read(query: Cache.DiffOptions): T | null; public abstract write( write: Cache.WriteOptions, ): void; diff --git a/packages/apollo-client/src/core/QueryManager.ts b/packages/apollo-client/src/core/QueryManager.ts index 7d6a5dbc29d..2b6b34b4712 100644 --- a/packages/apollo-client/src/core/QueryManager.ts +++ b/packages/apollo-client/src/core/QueryManager.ts @@ -988,7 +988,9 @@ export class QueryManager { ) { const { variables, query } = observableQuery.options; const lastResult = observableQuery.getLastResult(); - const { newData } = this.getQuery(observableQuery.queryId); + const { newData, document } = this.getQuery(observableQuery.queryId); + const isDeferred = + document !== null ? hasDirectives(['defer'], document) : false; // XXX test this if (newData) { return maybeDeepFreeze({ data: newData.result, partial: false }); @@ -1000,6 +1002,11 @@ export class QueryManager { variables, previousResult: lastResult ? lastResult.data : undefined, optimistic, + // Setting returnPartialData to true for deferred queries, so that + // an error does not get thrown if fields are missing. + // Returning {data: {}} will give us problems as it clobbers the + // data that we have already received. + returnPartialData: isDeferred, }); return maybeDeepFreeze({ data, partial: false }); @@ -1069,12 +1076,13 @@ export class QueryManager { fetchMoreForQueryId?: string; }): Promise { const { variables, context, errorPolicy = 'none', fetchPolicy } = options; + const isDeferred = hasDirectives(['defer'], document); const operation = this.buildOperationForLink(document, variables, { ...context, // TODO: Should this be included for all entry points via // buildOperationForLink? forceFetch: !this.queryDeduplication, - isDeferred: hasDirectives(['defer'], document), + isDeferred, }); let resultFromStore: any; @@ -1109,7 +1117,7 @@ export class QueryManager { // Initialize a tree of individual loading states for each deferred // field, when the initial response arrives. let loadingState; - if (!isPatch(result) && hasDirectives(['defer'], document)) { + if (!isPatch(result) && isDeferred) { loadingState = this.initDeferredFieldLoadingStates( document, result, @@ -1150,6 +1158,7 @@ export class QueryManager { variables, query: document, optimistic: false, + returnPartialData: isDeferred, }); // this will throw an error if there are missing fields in // the results which can happen with errors from the server. diff --git a/packages/apollo-client/src/data/store.ts b/packages/apollo-client/src/data/store.ts index 86c7d74eef9..243c9d3f4f7 100644 --- a/packages/apollo-client/src/data/store.ts +++ b/packages/apollo-client/src/data/store.ts @@ -91,6 +91,7 @@ export class DataStore { variables: variables, rootId: 'ROOT_QUERY', optimistic: false, + returnPartialData: true, }); if (originalResult) { this.mergePatch(originalResult, result); From 4f5fc8553a8299284063fe379ef7a0e6fa46f83e Mon Sep 17 00:00:00 2001 From: clarencenpy Date: Tue, 24 Jul 2018 17:31:54 -0700 Subject: [PATCH 26/47] Null check --- packages/apollo-client/src/data/queries.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/apollo-client/src/data/queries.ts b/packages/apollo-client/src/data/queries.ts index 3c71c48404a..e2c5662a11a 100644 --- a/packages/apollo-client/src/data/queries.ts +++ b/packages/apollo-client/src/data/queries.ts @@ -140,7 +140,7 @@ export class QueryStore { let curPointer = copy; while (index < path.length) { const key = path[index++]; - if (curPointer) { + if (curPointer && curPointer[key]) { curPointer = curPointer[key]; if (index === path.length) { // Reached the leaf node From 891c21b76879ed8874726181068e50faf2aed540 Mon Sep 17 00:00:00 2001 From: clarencenpy Date: Tue, 24 Jul 2018 17:36:33 -0700 Subject: [PATCH 27/47] chore: Publish - apollo-boost@0.2.0-alpha.4 - apollo-cache-inmemory@1.2.6-alpha.4 - apollo-cache@1.1.13-alpha.4 - apollo-client@2.4.0-alpha.4 - apollo-utilities@1.0.17-alpha.4 - graphql-anywhere@4.1.15-alpha.4 --- packages/apollo-boost/package.json | 10 +++++----- packages/apollo-cache-inmemory/package.json | 8 ++++---- packages/apollo-cache/package.json | 4 ++-- packages/apollo-client/package.json | 8 ++++---- packages/apollo-utilities/package.json | 2 +- packages/graphql-anywhere/package.json | 4 ++-- 6 files changed, 18 insertions(+), 18 deletions(-) diff --git a/packages/apollo-boost/package.json b/packages/apollo-boost/package.json index ad948e1005e..da8db9c5af0 100644 --- a/packages/apollo-boost/package.json +++ b/packages/apollo-boost/package.json @@ -1,6 +1,6 @@ { "name": "apollo-boost", - "version": "0.2.0-alpha.2", + "version": "0.2.0-alpha.4", "description": "The easiest way to get started with Apollo Client", "author": "Peggy Rayzis ", "contributors": [ @@ -34,9 +34,9 @@ "filesize": "npm run build && npm run build:browser" }, "dependencies": { - "apollo-cache": "^1.1.13-alpha.2", - "apollo-cache-inmemory": "^1.2.6-alpha.2", - "apollo-client": "^2.4.0-alpha.2", + "apollo-cache": "^1.1.13-alpha.4", + "apollo-cache-inmemory": "^1.2.6-alpha.4", + "apollo-client": "^2.4.0-alpha.4", "apollo-link": "^1.0.6", "apollo-link-error": "^1.0.3", "apollo-link-http": "1.6.0-alpha.0", @@ -46,7 +46,7 @@ "devDependencies": { "@types/graphql": "0.12.7", "@types/jest": "22.2.3", - "apollo-utilities": "^1.0.17-alpha.2", + "apollo-utilities": "^1.0.17-alpha.4", "browserify": "15.2.0", "fetch-mock": "6.5.0", "graphql": "0.13.2", diff --git a/packages/apollo-cache-inmemory/package.json b/packages/apollo-cache-inmemory/package.json index fb9a351221a..da964c01d94 100644 --- a/packages/apollo-cache-inmemory/package.json +++ b/packages/apollo-cache-inmemory/package.json @@ -1,6 +1,6 @@ { "name": "apollo-cache-inmemory", - "version": "1.2.6-alpha.2", + "version": "1.2.6-alpha.4", "description": "Core abstract of Caching layer for Apollo Client", "author": "James Baxley ", "contributors": [ @@ -40,9 +40,9 @@ "filesize": "npm run build:browser" }, "dependencies": { - "apollo-cache": "^1.1.13-alpha.2", - "apollo-utilities": "^1.0.17-alpha.2", - "graphql-anywhere": "^4.1.15-alpha.2" + "apollo-cache": "^1.1.13-alpha.4", + "apollo-utilities": "^1.0.17-alpha.4", + "graphql-anywhere": "^4.1.15-alpha.4" }, "peerDependencies": { "graphql": "0.11.7 || ^0.12.0 || ^0.13.0" diff --git a/packages/apollo-cache/package.json b/packages/apollo-cache/package.json index 0358a50262c..08b22f65376 100644 --- a/packages/apollo-cache/package.json +++ b/packages/apollo-cache/package.json @@ -1,6 +1,6 @@ { "name": "apollo-cache", - "version": "1.1.13-alpha.2", + "version": "1.1.13-alpha.4", "description": "Core abstract of Caching layer for Apollo Client", "author": "James Baxley ", "contributors": [ @@ -39,7 +39,7 @@ "filesize": "npm run build && npm run build:browser" }, "dependencies": { - "apollo-utilities": "^1.0.17-alpha.2" + "apollo-utilities": "^1.0.17-alpha.4" }, "devDependencies": { "@types/graphql": "0.12.7", diff --git a/packages/apollo-client/package.json b/packages/apollo-client/package.json index 107d7742857..69087b38437 100644 --- a/packages/apollo-client/package.json +++ b/packages/apollo-client/package.json @@ -1,7 +1,7 @@ { "name": "apollo-client", "private": true, - "version": "2.4.0-alpha.2", + "version": "2.4.0-alpha.4", "description": "A simple yet functional GraphQL client.", "main": "./lib/bundle.umd.js", "module": "./lib/index.js", @@ -47,10 +47,10 @@ "license": "MIT", "dependencies": { "@types/zen-observable": "^0.8.0", - "apollo-cache": "^1.1.13-alpha.2", + "apollo-cache": "^1.1.13-alpha.4", "apollo-link": "^1.0.0", "apollo-link-dedup": "1.1.0-alpha.0", - "apollo-utilities": "^1.0.17-alpha.2", + "apollo-utilities": "^1.0.17-alpha.4", "symbol-observable": "^1.0.2", "zen-observable": "^0.8.0" }, @@ -64,7 +64,7 @@ "@types/jest": "22.2.3", "@types/lodash": "4.14.112", "@types/node": "10.5.2", - "apollo-cache-inmemory": "^1.2.6-alpha.2", + "apollo-cache-inmemory": "^1.2.6-alpha.4", "benchmark": "2.1.4", "browserify": "15.2.0", "bundlesize": "0.17.0", diff --git a/packages/apollo-utilities/package.json b/packages/apollo-utilities/package.json index 0101ca22ea0..2d00c09d724 100644 --- a/packages/apollo-utilities/package.json +++ b/packages/apollo-utilities/package.json @@ -1,6 +1,6 @@ { "name": "apollo-utilities", - "version": "1.0.17-alpha.2", + "version": "1.0.17-alpha.4", "description": "Utilities for working with GraphQL ASTs", "author": "James Baxley ", "contributors": [ diff --git a/packages/graphql-anywhere/package.json b/packages/graphql-anywhere/package.json index 2c451d987a6..10dd0e439ed 100644 --- a/packages/graphql-anywhere/package.json +++ b/packages/graphql-anywhere/package.json @@ -1,6 +1,6 @@ { "name": "graphql-anywhere", - "version": "4.1.15-alpha.2", + "version": "4.1.15-alpha.4", "description": "Run GraphQL queries with no schema and just one resolver", "main": "./lib/bundle.umd.js", "module": "./lib/index.js", @@ -39,7 +39,7 @@ ], "license": "MIT", "dependencies": { - "apollo-utilities": "^1.0.17-alpha.2" + "apollo-utilities": "^1.0.17-alpha.4" }, "devDependencies": { "@types/graphql": "0.12.7", From 0b31c04021d6a3c4b790c5f555fe14b53b29f511 Mon Sep 17 00:00:00 2001 From: clarencenpy Date: Tue, 24 Jul 2018 18:03:15 -0700 Subject: [PATCH 28/47] Update dependencies --- packages/apollo-boost/package.json | 2 +- packages/apollo-client/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/apollo-boost/package.json b/packages/apollo-boost/package.json index da8db9c5af0..f6800b0bc21 100644 --- a/packages/apollo-boost/package.json +++ b/packages/apollo-boost/package.json @@ -39,7 +39,7 @@ "apollo-client": "^2.4.0-alpha.4", "apollo-link": "^1.0.6", "apollo-link-error": "^1.0.3", - "apollo-link-http": "1.6.0-alpha.0", + "apollo-link-http": "1.6.0-alpha.2", "apollo-link-state": "^0.4.0", "graphql-tag": "^2.4.2" }, diff --git a/packages/apollo-client/package.json b/packages/apollo-client/package.json index 69087b38437..babb1fac64f 100644 --- a/packages/apollo-client/package.json +++ b/packages/apollo-client/package.json @@ -49,7 +49,7 @@ "@types/zen-observable": "^0.8.0", "apollo-cache": "^1.1.13-alpha.4", "apollo-link": "^1.0.0", - "apollo-link-dedup": "1.1.0-alpha.0", + "apollo-link-dedup": "1.1.0-alpha.2", "apollo-utilities": "^1.0.17-alpha.4", "symbol-observable": "^1.0.2", "zen-observable": "^0.8.0" From bdb248f78247d7d76f1a50afd07d996ce5d07377 Mon Sep 17 00:00:00 2001 From: clarencenpy Date: Tue, 24 Jul 2018 21:38:13 -0700 Subject: [PATCH 29/47] Add some info about patches that get streamed in behind the scenes To be removed when defer is out of alpha. Right now, it is helpful for users to see the effect of adding @defer. --- packages/apollo-client/src/core/QueryManager.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/apollo-client/src/core/QueryManager.ts b/packages/apollo-client/src/core/QueryManager.ts index 2b6b34b4712..2a1e7ab343a 100644 --- a/packages/apollo-client/src/core/QueryManager.ts +++ b/packages/apollo-client/src/core/QueryManager.ts @@ -1117,13 +1117,22 @@ export class QueryManager { // Initialize a tree of individual loading states for each deferred // field, when the initial response arrives. let loadingState; - if (!isPatch(result) && isDeferred) { + if (isDeferred && !isPatch(result)) { loadingState = this.initDeferredFieldLoadingStates( document, result, ); } + // Provide some info about patches that get streamed in behind + // the scenes + // TODO: Remove this when out of alpha preview + if (isDeferred && isPatch(result)) { + console.info( + `Patch received: ${JSON.stringify(result, null, 2)}`, + ); + } + this.queryStore.markQueryResult( queryId, result, From 94608168c783da6583bd990c6408b9a39492d016 Mon Sep 17 00:00:00 2001 From: clarencenpy Date: Tue, 24 Jul 2018 21:50:09 -0700 Subject: [PATCH 30/47] chore: Publish - apollo-boost@0.2.0-alpha.10 - apollo-cache-inmemory@1.2.6-alpha.10 - apollo-cache@1.1.13-alpha.10 - apollo-client@2.4.0-alpha.10 - apollo-utilities@1.0.17-alpha.10 - graphql-anywhere@4.1.15-alpha.10 --- packages/apollo-boost/package.json | 10 +++++----- packages/apollo-cache-inmemory/package.json | 8 ++++---- packages/apollo-cache/package.json | 4 ++-- packages/apollo-client/package.json | 8 ++++---- packages/apollo-utilities/package.json | 2 +- packages/graphql-anywhere/package.json | 4 ++-- 6 files changed, 18 insertions(+), 18 deletions(-) diff --git a/packages/apollo-boost/package.json b/packages/apollo-boost/package.json index f6800b0bc21..5dfb57e866c 100644 --- a/packages/apollo-boost/package.json +++ b/packages/apollo-boost/package.json @@ -1,6 +1,6 @@ { "name": "apollo-boost", - "version": "0.2.0-alpha.4", + "version": "0.2.0-alpha.10", "description": "The easiest way to get started with Apollo Client", "author": "Peggy Rayzis ", "contributors": [ @@ -34,9 +34,9 @@ "filesize": "npm run build && npm run build:browser" }, "dependencies": { - "apollo-cache": "^1.1.13-alpha.4", - "apollo-cache-inmemory": "^1.2.6-alpha.4", - "apollo-client": "^2.4.0-alpha.4", + "apollo-cache": "1.1.13-alpha.10", + "apollo-cache-inmemory": "1.2.6-alpha.10", + "apollo-client": "2.4.0-alpha.10", "apollo-link": "^1.0.6", "apollo-link-error": "^1.0.3", "apollo-link-http": "1.6.0-alpha.2", @@ -46,7 +46,7 @@ "devDependencies": { "@types/graphql": "0.12.7", "@types/jest": "22.2.3", - "apollo-utilities": "^1.0.17-alpha.4", + "apollo-utilities": "1.0.17-alpha.10", "browserify": "15.2.0", "fetch-mock": "6.5.0", "graphql": "0.13.2", diff --git a/packages/apollo-cache-inmemory/package.json b/packages/apollo-cache-inmemory/package.json index da964c01d94..43bea0af5e8 100644 --- a/packages/apollo-cache-inmemory/package.json +++ b/packages/apollo-cache-inmemory/package.json @@ -1,6 +1,6 @@ { "name": "apollo-cache-inmemory", - "version": "1.2.6-alpha.4", + "version": "1.2.6-alpha.10", "description": "Core abstract of Caching layer for Apollo Client", "author": "James Baxley ", "contributors": [ @@ -40,9 +40,9 @@ "filesize": "npm run build:browser" }, "dependencies": { - "apollo-cache": "^1.1.13-alpha.4", - "apollo-utilities": "^1.0.17-alpha.4", - "graphql-anywhere": "^4.1.15-alpha.4" + "apollo-cache": "1.1.13-alpha.10", + "apollo-utilities": "1.0.17-alpha.10", + "graphql-anywhere": "4.1.15-alpha.10" }, "peerDependencies": { "graphql": "0.11.7 || ^0.12.0 || ^0.13.0" diff --git a/packages/apollo-cache/package.json b/packages/apollo-cache/package.json index 08b22f65376..d3faa7c8a9f 100644 --- a/packages/apollo-cache/package.json +++ b/packages/apollo-cache/package.json @@ -1,6 +1,6 @@ { "name": "apollo-cache", - "version": "1.1.13-alpha.4", + "version": "1.1.13-alpha.10", "description": "Core abstract of Caching layer for Apollo Client", "author": "James Baxley ", "contributors": [ @@ -39,7 +39,7 @@ "filesize": "npm run build && npm run build:browser" }, "dependencies": { - "apollo-utilities": "^1.0.17-alpha.4" + "apollo-utilities": "1.0.17-alpha.10" }, "devDependencies": { "@types/graphql": "0.12.7", diff --git a/packages/apollo-client/package.json b/packages/apollo-client/package.json index babb1fac64f..477c105a37a 100644 --- a/packages/apollo-client/package.json +++ b/packages/apollo-client/package.json @@ -1,7 +1,7 @@ { "name": "apollo-client", "private": true, - "version": "2.4.0-alpha.4", + "version": "2.4.0-alpha.10", "description": "A simple yet functional GraphQL client.", "main": "./lib/bundle.umd.js", "module": "./lib/index.js", @@ -47,10 +47,10 @@ "license": "MIT", "dependencies": { "@types/zen-observable": "^0.8.0", - "apollo-cache": "^1.1.13-alpha.4", + "apollo-cache": "1.1.13-alpha.10", "apollo-link": "^1.0.0", "apollo-link-dedup": "1.1.0-alpha.2", - "apollo-utilities": "^1.0.17-alpha.4", + "apollo-utilities": "1.0.17-alpha.10", "symbol-observable": "^1.0.2", "zen-observable": "^0.8.0" }, @@ -64,7 +64,7 @@ "@types/jest": "22.2.3", "@types/lodash": "4.14.112", "@types/node": "10.5.2", - "apollo-cache-inmemory": "^1.2.6-alpha.4", + "apollo-cache-inmemory": "1.2.6-alpha.10", "benchmark": "2.1.4", "browserify": "15.2.0", "bundlesize": "0.17.0", diff --git a/packages/apollo-utilities/package.json b/packages/apollo-utilities/package.json index 2d00c09d724..2ca71b49f15 100644 --- a/packages/apollo-utilities/package.json +++ b/packages/apollo-utilities/package.json @@ -1,6 +1,6 @@ { "name": "apollo-utilities", - "version": "1.0.17-alpha.4", + "version": "1.0.17-alpha.10", "description": "Utilities for working with GraphQL ASTs", "author": "James Baxley ", "contributors": [ diff --git a/packages/graphql-anywhere/package.json b/packages/graphql-anywhere/package.json index 10dd0e439ed..e9a3f0bf069 100644 --- a/packages/graphql-anywhere/package.json +++ b/packages/graphql-anywhere/package.json @@ -1,6 +1,6 @@ { "name": "graphql-anywhere", - "version": "4.1.15-alpha.4", + "version": "4.1.15-alpha.10", "description": "Run GraphQL queries with no schema and just one resolver", "main": "./lib/bundle.umd.js", "module": "./lib/index.js", @@ -39,7 +39,7 @@ ], "license": "MIT", "dependencies": { - "apollo-utilities": "^1.0.17-alpha.4" + "apollo-utilities": "1.0.17-alpha.10" }, "devDependencies": { "@types/graphql": "0.12.7", From 39cf4fb4a4a6d2c2279dde02e85df43c49be11f7 Mon Sep 17 00:00:00 2001 From: clarencenpy Date: Wed, 25 Jul 2018 12:15:21 -0700 Subject: [PATCH 31/47] Publish with exact versioning --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ec30cc803bc..dfb293d049d 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "coverage:upload": "codecov", "danger": "danger run --verbose", "deploy": "lerna publish -m \"chore: Publish\" --independent && cd packages/apollo-client && npm run deploy", - "deploy-alpha": "lerna publish --npm-tag alpha -m \"chore: Publish\" --independent && cd packages/apollo-client && npm run deploy" + "deploy-alpha": "lerna publish --npm-tag alpha -m \"chore: Publish\" --independent --exact && cd packages/apollo-client && npm run deploy" }, "bundlesize": [ { From dcd0a37d138f8f24b67c088104cb75497f2874c6 Mon Sep 17 00:00:00 2001 From: clarencenpy Date: Wed, 25 Jul 2018 21:40:44 -0700 Subject: [PATCH 32/47] Update client docs --- docs/source/features/defer-support.md | 78 ++++++++++++++++++++------- 1 file changed, 59 insertions(+), 19 deletions(-) diff --git a/docs/source/features/defer-support.md b/docs/source/features/defer-support.md index 3d5c323c498..56d087c29da 100644 --- a/docs/source/features/defer-support.md +++ b/docs/source/features/defer-support.md @@ -1,11 +1,30 @@ --- -title: Defer Support +title: Deferred Queries description: Optimize data loading with the @defer directive --- -

The @defer Directive

+

Setting up

-Many applications that use Apollo fetch data from a variety of microservices, which may all have varying latencies and cache characteristics. Apollo comes with a built-in directive for deferring parts of your GraphQL query in a declarative way, so that fields that take a long time to resolve do not need to slow down your entire query. +Note: `@defer` support is an experimental feature that is only available in the alpha preview of Apollo Server and Apollo Client. + +- On the server: + + ``` + npm install apollo-server@alpha + ``` + +- On the client, if you are using Apollo Boost: + ``` + npm install apollo-boost@alpha react-apollo@alpha + ``` + Or if you are using Apollo Client: + ``` + npm install apollo-client@alpha apollo-cache-inmemory@alpha apollo-link-http@alpha apollo-link-error apollo-link + ``` + +

The `@defer` Directive

+ +Many applications that use Apollo fetch data from a variety of microservices, which may each have varying latencies and cache characteristics. Apollo comes with a built-in directive for deferring parts of your GraphQL query in a declarative way, so that fields that take a long time to resolve do not need to slow down your entire query. There are 3 main reasons why you may want to defer a field: @@ -13,7 +32,8 @@ There are 3 main reasons why you may want to defer a field: 2. **Field is not on the critical path for interactivity.** This includes the comments section of a story, or the number of claps received. 3. **Field is expensive to send.** Even if the field may resolve quickly (ready to send back), users might still choose to defer it if the cost of transport is too expensive. -

Motivating Example

+As an example, take a look at the following query that populates a NewsFeed page: + ```graphql query NewsFeed { newsFeed { @@ -30,14 +50,17 @@ query NewsFeed { text } } - matchScore + matchScore } } } ``` -Given the above query that populates a NewsFeed page, observe that the time needed for different fields to resolve may be significantly different. `stories` is highly public data that we can cache in CDNs (fast), while `recommendedForYou` is personalized and may need to be computed for every user (slooow). Also, we might not need `comments` to be displayed immediately, so slowing down our query to wait for them to be fetched is not the best idea. -We can rewrite the above query with `@defer`: +It is likely that the time needed for different fields in a query to resolve are significantly different. `stories` is highly public data that we can cache in CDNs (fast), while `recommendedForYou` is personalized and may need to be computed for every user (slooow). Also, we might not need `comments` to be displayed immediately, so slowing down our query to wait for them to be fetched is not the best idea. + +

How to use `@defer`

+ +We can optimize the above query with `@defer`: ```graphql query NewsFeed { @@ -61,7 +84,7 @@ query NewsFeed { } ``` -Under the hood, Apollo Server will return an initial response without waiting for deferred fields to resolve, before streaming patches for each deferred field asynchronously as they complete. +Once you have added `@defer`, Apollo Server will return an initial response without waiting for deferred fields to resolve, using `null` as placeholders for them. Then, it streams patches for each deferred field asynchronously as they resolve. ```json // Initial response @@ -102,11 +125,26 @@ Under the hood, Apollo Server will return an initial response without waiting fo } ``` -If an error is thrown within a resolver, they get sent along with its closest deferred parent, and get merged with the `graphQLErrors` array. +If an error is thrown within a resolver, the error gets sent along with its closest deferred parent, and is merged with the `graphQLErrors` array on the client. + +```json +// Patch for "comments" if there is an error +{ + "path": ["newsFeed", "stories", 1, "comments"], + "data": null, + "errors": [ + { + "message": "Failed to fetch comments" + } + ] +} +```

Distinguishing between "pending" and "null"

-You may have noticed that deferred fields get returned as null in the initial response. So how can we know which fields are pending so that we can show some loading indicator? To deal with that, Apollo Client now exposes field-level loading information in a new property called loadingState that you can check for in your UI components. The shape of loadingState mirrors that of your data: +You may have noticed that deferred fields are returned as `null` in the initial response. So how can we know which fields are pending so that we can show some loading indicator? To deal with that, Apollo Client now exposes field-level loading information in a new property called `loadingState` that you can check for in your UI components. The shape of `loadingState` mirrors that of your data. For example, if `data.newsFeed.stories` is ready, `loadingState.newsFeed.stories` will be `true`. + +You can use it in a React component like this: ```jsx harmony @@ -121,13 +159,11 @@ You may have noticed that deferred fields get returned as null in the initial re ``` -

Transport

+

Where is `@defer` allowed?

-There is no additional set up required to use `@defer`. By default, deferred responses are transmitted using [Multipart HTTP](https://www.w3.org/Protocols/rfc1341/7_2_Multipart.html), which is supported by `apollo-link-http`. +- `@defer` can be applied on any `FIELD` of a `Query` operation. It also takes an optional argument `if`, that is a `boolean` controlling whether it is active, similar to `@include`. -

Where can I use @defer?

- -- `@defer` can be applied on any `FIELD` of a `Query` operation. It is illegal to use `@defer` on an `INLINE_FRAGMENT` or `FRAGMENT_SPREAD`. +- `@include` and `@skip` take precedence over `@defer`. - Mutations: Not supported. @@ -135,7 +171,7 @@ There is no additional set up required to use `@defer`. By default, deferred res - Nesting: `@defer` can be nested arbitrarily. For example, we can defer a list type, and defer a field on an object in the list. During execution, we ensure that the patch for a parent field will be sent before its children, even if the child object resolves first. This will simplify the logic for merging patches. -- Use in GraphQL fragments: Supported. If there are multiple declarations of a field within the query, **all** of them have to contain `@defer` for the field to be deferred. This could happen if we have use a fragment like this: +- GraphQL fragments: Supported. If there are multiple declarations of a field within the query, **all** of them have to contain `@defer` for the field to be deferred. This could happen if we have use a fragment like this: ```graphql fragment StoryDetail on Story { @@ -155,12 +191,16 @@ There is no additional set up required to use `@defer`. By default, deferred res A common pattern around fragments is to bind it to a component and reuse them across different parts of your UI. This is why it would be ideal to make sure that the `@defer` behavior of fields in a fragment is not overridden. -

Performance Considerations

+

Transport

+ +There is no additional setup for the transport required to use `@defer`. By default, deferred responses are transmitted using [Multipart HTTP](https://www.w3.org/Protocols/rfc1341/7_2_Multipart.html). For browsers that do not support the [ReadableStream](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream) API used to read streaming responses, we will just fallback to normal query execution ignoring `@defer`. + +

Performance Considerations

`@defer` is one of those features that work best if used in moderation. If it is used too granularly (on many nested fields), the overhead of performing patching and re-rendering could be worse than just waiting for the full query to resolve. Try to limit `@defer` to fields that take a significantly longer time to load. This is super easy to figure out if you have Apollo Engine set up! -

Use with other GraphQL servers

+

Use with other GraphQL servers

If you are sending queries to a GraphQL server that does not support `@defer`, it is likely that the `@defer` directive is simply ignored, or a GraphQL validation error is thrown. -If you would like to implement a GraphQL server that is able to interoperate with Apollo Client, please look at the documentation [here](https://github.com/apollographql/apollo-server/blob/defer-support/docs/source/defer-support.md). +If you would want to implement a GraphQL server that is able to interoperate with Apollo Client, please look at the documentation [here](https://github.com/apollographql/apollo-server/blob/defer-support/docs/source/defer-support.md). From d6b50db2088f59244302cffbad909430c8c12f8e Mon Sep 17 00:00:00 2001 From: clarencenpy Date: Thu, 26 Jul 2018 16:07:55 -0700 Subject: [PATCH 33/47] chore: Publish - apollo-boost@0.2.0-alpha.11 --- packages/apollo-boost/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/apollo-boost/package.json b/packages/apollo-boost/package.json index 5dfb57e866c..33e7f544885 100644 --- a/packages/apollo-boost/package.json +++ b/packages/apollo-boost/package.json @@ -1,6 +1,6 @@ { "name": "apollo-boost", - "version": "0.2.0-alpha.10", + "version": "0.2.0-alpha.11", "description": "The easiest way to get started with Apollo Client", "author": "Peggy Rayzis ", "contributors": [ @@ -39,7 +39,7 @@ "apollo-client": "2.4.0-alpha.10", "apollo-link": "^1.0.6", "apollo-link-error": "^1.0.3", - "apollo-link-http": "1.6.0-alpha.2", + "apollo-link-http": "1.6.0-alpha.3", "apollo-link-state": "^0.4.0", "graphql-tag": "^2.4.2" }, From fa240ec4045a2b291c34db207c3fe6d081252791 Mon Sep 17 00:00:00 2001 From: clarencenpy Date: Thu, 26 Jul 2018 17:11:47 -0700 Subject: [PATCH 34/47] Update dependencies --- packages/apollo-boost/package.json | 2 +- packages/apollo-client/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/apollo-boost/package.json b/packages/apollo-boost/package.json index 33e7f544885..b77338a682d 100644 --- a/packages/apollo-boost/package.json +++ b/packages/apollo-boost/package.json @@ -39,7 +39,7 @@ "apollo-client": "2.4.0-alpha.10", "apollo-link": "^1.0.6", "apollo-link-error": "^1.0.3", - "apollo-link-http": "1.6.0-alpha.3", + "apollo-link-http": "alpha", "apollo-link-state": "^0.4.0", "graphql-tag": "^2.4.2" }, diff --git a/packages/apollo-client/package.json b/packages/apollo-client/package.json index 477c105a37a..a39c75ce3cf 100644 --- a/packages/apollo-client/package.json +++ b/packages/apollo-client/package.json @@ -49,7 +49,7 @@ "@types/zen-observable": "^0.8.0", "apollo-cache": "1.1.13-alpha.10", "apollo-link": "^1.0.0", - "apollo-link-dedup": "1.1.0-alpha.2", + "apollo-link-dedup": "alpha", "apollo-utilities": "1.0.17-alpha.10", "symbol-observable": "^1.0.2", "zen-observable": "^0.8.0" From 502b2c40ba633568f01da767bbd4d87898a63889 Mon Sep 17 00:00:00 2001 From: clarencenpy Date: Thu, 26 Jul 2018 18:23:17 -0700 Subject: [PATCH 35/47] chore: Publish - apollo-boost@0.2.0-alpha.12 - apollo-cache-inmemory@1.2.6-alpha.12 - apollo-cache@1.1.13-alpha.12 - apollo-client@2.4.0-alpha.12 - apollo-utilities@1.0.17-alpha.12 - graphql-anywhere@4.1.15-alpha.12 --- packages/apollo-boost/package.json | 10 +++++----- packages/apollo-cache-inmemory/package.json | 8 ++++---- packages/apollo-cache/package.json | 4 ++-- packages/apollo-client/package.json | 8 ++++---- packages/apollo-utilities/package.json | 2 +- packages/graphql-anywhere/package.json | 4 ++-- 6 files changed, 18 insertions(+), 18 deletions(-) diff --git a/packages/apollo-boost/package.json b/packages/apollo-boost/package.json index b77338a682d..328c216920b 100644 --- a/packages/apollo-boost/package.json +++ b/packages/apollo-boost/package.json @@ -1,6 +1,6 @@ { "name": "apollo-boost", - "version": "0.2.0-alpha.11", + "version": "0.2.0-alpha.12", "description": "The easiest way to get started with Apollo Client", "author": "Peggy Rayzis ", "contributors": [ @@ -34,9 +34,9 @@ "filesize": "npm run build && npm run build:browser" }, "dependencies": { - "apollo-cache": "1.1.13-alpha.10", - "apollo-cache-inmemory": "1.2.6-alpha.10", - "apollo-client": "2.4.0-alpha.10", + "apollo-cache": "1.1.13-alpha.12", + "apollo-cache-inmemory": "1.2.6-alpha.12", + "apollo-client": "2.4.0-alpha.12", "apollo-link": "^1.0.6", "apollo-link-error": "^1.0.3", "apollo-link-http": "alpha", @@ -46,7 +46,7 @@ "devDependencies": { "@types/graphql": "0.12.7", "@types/jest": "22.2.3", - "apollo-utilities": "1.0.17-alpha.10", + "apollo-utilities": "1.0.17-alpha.12", "browserify": "15.2.0", "fetch-mock": "6.5.0", "graphql": "0.13.2", diff --git a/packages/apollo-cache-inmemory/package.json b/packages/apollo-cache-inmemory/package.json index 43bea0af5e8..c43bc7662ff 100644 --- a/packages/apollo-cache-inmemory/package.json +++ b/packages/apollo-cache-inmemory/package.json @@ -1,6 +1,6 @@ { "name": "apollo-cache-inmemory", - "version": "1.2.6-alpha.10", + "version": "1.2.6-alpha.12", "description": "Core abstract of Caching layer for Apollo Client", "author": "James Baxley ", "contributors": [ @@ -40,9 +40,9 @@ "filesize": "npm run build:browser" }, "dependencies": { - "apollo-cache": "1.1.13-alpha.10", - "apollo-utilities": "1.0.17-alpha.10", - "graphql-anywhere": "4.1.15-alpha.10" + "apollo-cache": "1.1.13-alpha.12", + "apollo-utilities": "1.0.17-alpha.12", + "graphql-anywhere": "4.1.15-alpha.12" }, "peerDependencies": { "graphql": "0.11.7 || ^0.12.0 || ^0.13.0" diff --git a/packages/apollo-cache/package.json b/packages/apollo-cache/package.json index d3faa7c8a9f..1c774bcee91 100644 --- a/packages/apollo-cache/package.json +++ b/packages/apollo-cache/package.json @@ -1,6 +1,6 @@ { "name": "apollo-cache", - "version": "1.1.13-alpha.10", + "version": "1.1.13-alpha.12", "description": "Core abstract of Caching layer for Apollo Client", "author": "James Baxley ", "contributors": [ @@ -39,7 +39,7 @@ "filesize": "npm run build && npm run build:browser" }, "dependencies": { - "apollo-utilities": "1.0.17-alpha.10" + "apollo-utilities": "1.0.17-alpha.12" }, "devDependencies": { "@types/graphql": "0.12.7", diff --git a/packages/apollo-client/package.json b/packages/apollo-client/package.json index a39c75ce3cf..1cf1aeadf2a 100644 --- a/packages/apollo-client/package.json +++ b/packages/apollo-client/package.json @@ -1,7 +1,7 @@ { "name": "apollo-client", "private": true, - "version": "2.4.0-alpha.10", + "version": "2.4.0-alpha.12", "description": "A simple yet functional GraphQL client.", "main": "./lib/bundle.umd.js", "module": "./lib/index.js", @@ -47,10 +47,10 @@ "license": "MIT", "dependencies": { "@types/zen-observable": "^0.8.0", - "apollo-cache": "1.1.13-alpha.10", + "apollo-cache": "1.1.13-alpha.12", "apollo-link": "^1.0.0", "apollo-link-dedup": "alpha", - "apollo-utilities": "1.0.17-alpha.10", + "apollo-utilities": "1.0.17-alpha.12", "symbol-observable": "^1.0.2", "zen-observable": "^0.8.0" }, @@ -64,7 +64,7 @@ "@types/jest": "22.2.3", "@types/lodash": "4.14.112", "@types/node": "10.5.2", - "apollo-cache-inmemory": "1.2.6-alpha.10", + "apollo-cache-inmemory": "1.2.6-alpha.12", "benchmark": "2.1.4", "browserify": "15.2.0", "bundlesize": "0.17.0", diff --git a/packages/apollo-utilities/package.json b/packages/apollo-utilities/package.json index 2ca71b49f15..5cf999faa01 100644 --- a/packages/apollo-utilities/package.json +++ b/packages/apollo-utilities/package.json @@ -1,6 +1,6 @@ { "name": "apollo-utilities", - "version": "1.0.17-alpha.10", + "version": "1.0.17-alpha.12", "description": "Utilities for working with GraphQL ASTs", "author": "James Baxley ", "contributors": [ diff --git a/packages/graphql-anywhere/package.json b/packages/graphql-anywhere/package.json index e9a3f0bf069..54319fd161d 100644 --- a/packages/graphql-anywhere/package.json +++ b/packages/graphql-anywhere/package.json @@ -1,6 +1,6 @@ { "name": "graphql-anywhere", - "version": "4.1.15-alpha.10", + "version": "4.1.15-alpha.12", "description": "Run GraphQL queries with no schema and just one resolver", "main": "./lib/bundle.umd.js", "module": "./lib/index.js", @@ -39,7 +39,7 @@ ], "license": "MIT", "dependencies": { - "apollo-utilities": "1.0.17-alpha.10" + "apollo-utilities": "1.0.17-alpha.12" }, "devDependencies": { "@types/graphql": "0.12.7", From 33ad6c1b41307bc8d5e96107f2db60e8fe68fec8 Mon Sep 17 00:00:00 2001 From: clarencenpy Date: Mon, 30 Jul 2018 18:55:33 -0700 Subject: [PATCH 36/47] Update NetworkStatus to reflect partial state --- packages/apollo-client/src/core/QueryManager.ts | 4 +++- packages/apollo-client/src/core/networkStatus.ts | 2 +- packages/apollo-client/src/data/queries.ts | 14 ++++++++------ 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/packages/apollo-client/src/core/QueryManager.ts b/packages/apollo-client/src/core/QueryManager.ts index 2a1e7ab343a..c9f03c4207c 100644 --- a/packages/apollo-client/src/core/QueryManager.ts +++ b/packages/apollo-client/src/core/QueryManager.ts @@ -1129,7 +1129,7 @@ export class QueryManager { // TODO: Remove this when out of alpha preview if (isDeferred && isPatch(result)) { console.info( - `Patch received: ${JSON.stringify(result, null, 2)}`, + `Patch received for path ${JSON.stringify(result.path)}`, ); } @@ -1137,6 +1137,7 @@ export class QueryManager { queryId, result, fetchMoreForQueryId, + isDeferred, loadingState, ); @@ -1184,6 +1185,7 @@ export class QueryManager { reject(error); }, complete: () => { + console.log('apollo client complete!'); this.removeFetchQueryPromise(requestId); this.setQuery(queryId, ({ subscriptions }) => ({ subscriptions: subscriptions.filter(x => x !== subscription), diff --git a/packages/apollo-client/src/core/networkStatus.ts b/packages/apollo-client/src/core/networkStatus.ts index b2be87c9013..83a9cec0ae3 100644 --- a/packages/apollo-client/src/core/networkStatus.ts +++ b/packages/apollo-client/src/core/networkStatus.ts @@ -58,5 +58,5 @@ export enum NetworkStatus { export function isNetworkRequestInFlight( networkStatus: NetworkStatus, ): boolean { - return networkStatus < 7 || networkStatus === 9; + return networkStatus < 7; } diff --git a/packages/apollo-client/src/data/queries.ts b/packages/apollo-client/src/data/queries.ts index e2c5662a11a..1e47275443c 100644 --- a/packages/apollo-client/src/data/queries.ts +++ b/packages/apollo-client/src/data/queries.ts @@ -1,10 +1,9 @@ -import { DocumentNode, GraphQLError, ExecutionResult } from 'graphql'; +import { DocumentNode, ExecutionResult, GraphQLError } from 'graphql'; import { print } from 'graphql/language/printer'; -import { isEqual } from 'apollo-utilities'; +import { cloneDeep, isEqual } from 'apollo-utilities'; import { NetworkStatus } from '../core/networkStatus'; import { ExecutionPatchResult, isPatch } from '../core/types'; -import { cloneDeep } from 'apollo-utilities'; export type QueryStoreValue = { document: DocumentNode; @@ -120,12 +119,13 @@ export class QueryStore { queryId: string, result: ExecutionResult | ExecutionPatchResult, fetchMoreForQueryId: string | undefined, + isDeferred: boolean, loadingState?: Record, ) { if (!this.store[queryId]) return; - // Set up loadingState if it is passed in by QueryManager - if (loadingState) { + // Set up loadingState if it is passed in by QueryManager for deferred queries + if (isDeferred && loadingState) { this.store[queryId].loadingState = loadingState; this.store[queryId].compactedLoadingState = this.compactLoadingStateTree( loadingState, @@ -189,7 +189,9 @@ export class QueryStore { this.store[queryId].graphQLErrors = result.errors && result.errors.length ? result.errors : []; this.store[queryId].previousVariables = null; - this.store[queryId].networkStatus = NetworkStatus.ready; + this.store[queryId].networkStatus = isDeferred + ? NetworkStatus.partial + : NetworkStatus.ready; // If we have a `fetchMoreForQueryId` then we need to update the network // status for that query. See the branch for query initialization for more From 148b7ecb4af9bfa3e27dc94fdbd1ed8df17cf088 Mon Sep 17 00:00:00 2001 From: clarencenpy Date: Mon, 30 Jul 2018 22:05:57 -0700 Subject: [PATCH 37/47] Refactored to return correct loadingState on complete --- .../apollo-client/src/core/QueryManager.ts | 72 +++++++++++++++---- packages/apollo-client/src/data/queries.ts | 45 +----------- 2 files changed, 62 insertions(+), 55 deletions(-) diff --git a/packages/apollo-client/src/core/QueryManager.ts b/packages/apollo-client/src/core/QueryManager.ts index a0083cbf60b..d582a7543f9 100644 --- a/packages/apollo-client/src/core/QueryManager.ts +++ b/packages/apollo-client/src/core/QueryManager.ts @@ -20,6 +20,7 @@ import { isProduction, maybeDeepFreeze, hasDirectives, + cloneDeep, } from 'apollo-utilities'; import { QueryScheduler } from '../scheduler/scheduler'; @@ -1081,6 +1082,47 @@ export class QueryManager { }); } + /** + * Given a loadingState tree, update it with the patch by traversing its path + */ + private updateLoadingState( + curLoadingState: Record, + result: ExecutionPatchResult, + ) { + const path = result.path; + let index = 0; + const copy = cloneDeep(curLoadingState); + let curPointer = copy; + while (index < path.length) { + const key = path[index++]; + if (curPointer && curPointer[key]) { + curPointer = curPointer[key]; + if (index === path.length) { + // Reached the leaf node + if (Array.isArray(result.data)) { + // At the time of instantiating the loadingState from the query AST, + // we have no way of telling if a field is an array type. Therefore, + // once we receive a patch that has array data, we need to update the + // loadingState with an array with the appropriate number of elements. + + const children = cloneDeep(curPointer!._children); + const childrenArray = []; + for (let i = 0; i < result.data.length; i++) { + childrenArray.push(children); + } + curPointer!._children = childrenArray; + } + curPointer!._loading = false; + break; + } + if (curPointer && curPointer!._children) { + curPointer = curPointer!._children; + } + } + } + return copy; + } + // 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. @@ -1109,6 +1151,7 @@ export class QueryManager { let resultFromStore: any; let errorsFromStore: any; + let curLoadingState: Record; return new Promise>((resolve, reject) => { this.addFetchQueryPromise(requestId, resolve, reject); @@ -1138,21 +1181,24 @@ export class QueryManager { // Initialize a tree of individual loading states for each deferred // field, when the initial response arrives. - let loadingState; if (isDeferred && !isPatch(result)) { - loadingState = this.initDeferredFieldLoadingStates( + curLoadingState = this.initFieldLevelLoadingStates( document, result, ); } - // Provide some info about patches that get streamed in behind - // the scenes - // TODO: Remove this when out of alpha preview if (isDeferred && isPatch(result)) { + // TODO: Remove console.info when out of alpha console.info( `Patch received for path ${JSON.stringify(result.path)}`, ); + + // Update loadingState for every patch received, by traversing its path + curLoadingState = this.updateLoadingState( + curLoadingState, + result, + ); } this.queryStore.markQueryResult( @@ -1160,7 +1206,7 @@ export class QueryManager { result, fetchMoreForQueryId, isDeferred, - loadingState, + curLoadingState, ); this.invalidate(true, queryId, fetchMoreForQueryId); @@ -1218,7 +1264,7 @@ export class QueryManager { loading: false, networkStatus: NetworkStatus.ready, stale: false, - loadingState: {}, + loadingState: curLoadingState, }); }, }); @@ -1306,9 +1352,9 @@ export class QueryManager { /** * Given a DocumentNode, traverse the tree and initialize loading states for - * all deferred fields. + * all fields. Deferred fields are initialized with `_loading` set to true. */ - private initDeferredFieldLoadingStates( + private initFieldLevelLoadingStates( doc: DocumentNode, result: ExecutionResult, ) { @@ -1327,7 +1373,7 @@ export class QueryManager { definition => definition.kind === Kind.OPERATION_DEFINITION, )[0]; // Take the first element since we do not support multiple operations - return (this.extractDeferredFieldsToTree( + return (this.createLoadingStateTree( operationDefinition as any, fragmentMap, result.data, @@ -1341,7 +1387,7 @@ export class QueryManager { * The actual data from the initial response is passed in so that we can * reference the query schema against the data, and handle arrays that we find. */ - private extractDeferredFieldsToTree( + private createLoadingStateTree( selection: SelectionNode, fragmentMap: Record, data: Record | undefined, @@ -1416,7 +1462,7 @@ export class QueryManager { for (const childSelection of (selection as FieldNode).selectionSet! .selections) { if (childSelection.kind === Kind.INLINE_FRAGMENT) { - const subtree = this.extractDeferredFieldsToTree( + const subtree = this.createLoadingStateTree( childSelection, fragmentMap, data, @@ -1433,7 +1479,7 @@ export class QueryManager { childData = data[childName]; isArray = Array.isArray(childData); } - const subtree = this.extractDeferredFieldsToTree( + const subtree = this.createLoadingStateTree( childSelection, fragmentMap, // Just pass in the first elem of array, all of them will have diff --git a/packages/apollo-client/src/data/queries.ts b/packages/apollo-client/src/data/queries.ts index 1e47275443c..cd1065fd063 100644 --- a/packages/apollo-client/src/data/queries.ts +++ b/packages/apollo-client/src/data/queries.ts @@ -1,6 +1,6 @@ import { DocumentNode, ExecutionResult, GraphQLError } from 'graphql'; import { print } from 'graphql/language/printer'; -import { cloneDeep, isEqual } from 'apollo-utilities'; +import { isEqual } from 'apollo-utilities'; import { NetworkStatus } from '../core/networkStatus'; import { ExecutionPatchResult, isPatch } from '../core/types'; @@ -124,7 +124,7 @@ export class QueryStore { ) { if (!this.store[queryId]) return; - // Set up loadingState if it is passed in by QueryManager for deferred queries + // Store loadingState along with a compacted version of it if (isDeferred && loadingState) { this.store[queryId].loadingState = loadingState; this.store[queryId].compactedLoadingState = this.compactLoadingStateTree( @@ -132,45 +132,7 @@ export class QueryStore { ); } - if (isPatch(result)) { - // Update loadingState for every patch received, by traversing its path - const path = (result as ExecutionPatchResult).path; - let index = 0; - const copy = cloneDeep(this.store[queryId].loadingState); - let curPointer = copy; - while (index < path.length) { - const key = path[index++]; - if (curPointer && curPointer[key]) { - curPointer = curPointer[key]; - if (index === path.length) { - // Reached the leaf node - if (Array.isArray(result.data)) { - // At the time of instantiating the loadingState from the query AST, - // we have no way of telling if a field is an array type. Therefore, - // once we receive a patch that has array data, we need to update the - // loadingState with an array with the appropriate number of elements. - - const children = cloneDeep(curPointer!._children); - const childrenArray = []; - for (let i = 0; i < result.data.length; i++) { - childrenArray.push(children); - } - curPointer!._children = childrenArray; - } - curPointer!._loading = false; - break; - } - if (curPointer && curPointer!._children) { - curPointer = curPointer!._children; - } - } - } - - this.store[queryId].loadingState = copy; - this.store[queryId].compactedLoadingState = this.compactLoadingStateTree( - copy, - ); - + if (isDeferred && isPatch(result)) { // Merge graphqlErrors from patch, if any if (result.errors) { const errors: GraphQLError[] = []; @@ -182,7 +144,6 @@ export class QueryStore { }); this.store[queryId].graphQLErrors = errors; } - return; } this.store[queryId].networkError = null; From de2cdfba1d6d83201a24c2554c000377bba55131 Mon Sep 17 00:00:00 2001 From: clarencenpy Date: Tue, 31 Jul 2018 00:39:02 -0700 Subject: [PATCH 38/47] Initialize loadingState when reading deferred query from cache --- .../apollo-client/src/core/QueryManager.ts | 78 +++++++++++-------- packages/apollo-client/src/data/queries.ts | 14 +++- 2 files changed, 60 insertions(+), 32 deletions(-) diff --git a/packages/apollo-client/src/core/QueryManager.ts b/packages/apollo-client/src/core/QueryManager.ts index d582a7543f9..9f803e563b4 100644 --- a/packages/apollo-client/src/core/QueryManager.ts +++ b/packages/apollo-client/src/core/QueryManager.ts @@ -396,7 +396,21 @@ export class QueryManager { !shouldFetch || fetchPolicy === 'cache-and-network'; if (shouldDispatchClientResult) { - this.queryStore.markQueryResultClient(queryId, !shouldFetch); + const query = this.queryStore.get(queryId); + + // Initialize loadingState with the cached results if it is a deferred query + let loadingState; + if (hasDirectives(['defer'], query.document)) { + loadingState = this.initFieldLevelLoadingStates(query.document, { + data: storeResult, + }); + } + + this.queryStore.markQueryResultClient( + queryId, + !shouldFetch, + loadingState, + ); this.invalidate(true, queryId, fetchMoreForQueryId); @@ -1386,6 +1400,9 @@ export class QueryManager { * FragmentSpread according to the fragment map that is passed in. * The actual data from the initial response is passed in so that we can * reference the query schema against the data, and handle arrays that we find. + * In the case where a partial result is passed in (might be retrieved from + * cache), it would set the `_loading` states depending on which fields are + * available. */ private createLoadingStateTree( selection: SelectionNode, @@ -1402,16 +1419,18 @@ export class QueryManager { (!(selection as FieldNode).selectionSet || (selection as FieldNode).selectionSet!.selections.length === 0); + const isLoaded = data !== undefined && data !== null; if (isLeaf) { - return hasDeferDirective ? { _loading: true } : true; + return hasDeferDirective ? { _loading: !isLoaded } : false; } const map: { _loading?: boolean; _children?: Record } = { - _loading: hasDeferDirective ? true : undefined, + _loading: hasDeferDirective ? !isLoaded : false, _children: undefined, }; - // Replace FragmentSpreads with its actual selectionSet + // Extract child selections, replacing FragmentSpreads with its actual selectionSet + const selections: SelectionNode[] = []; const expandedFragments: SelectionNode[] = []; (selection as FieldNode).selectionSet!.selections.forEach( childSelection => { @@ -1420,19 +1439,16 @@ export class QueryManager { fragmentMap[fragmentName].forEach((selection: SelectionNode) => { expandedFragments.push(selection); }); + } else { + selections.push(childSelection); } }, ); - // Remove FragmentSpreads - (selection as FieldNode).selectionSet!.selections = (selection as FieldNode).selectionSet!.selections.filter( - selection => selection.kind !== Kind.FRAGMENT_SPREAD, - ); - // Add expanded FragmentSpreads to the current selection set expandedFragments.forEach(fragSelection => { const fragFieldName = (fragSelection as FieldNode).name.value; - const existingSelection = (selection as FieldNode).selectionSet!.selections.find( + const existingSelection = selections.find( selection => selection.kind !== Kind.INLINE_FRAGMENT && (selection as FieldNode).name.value === fragFieldName, @@ -1455,22 +1471,23 @@ export class QueryManager { } } else { // Add it to the selectionSet - (selection as FieldNode).selectionSet!.selections.push(fragSelection); + selections.push(fragSelection); } }); - for (const childSelection of (selection as FieldNode).selectionSet! - .selections) { + // Initialize loadingState recursively for childSelections + for (const childSelection of selections) { if (childSelection.kind === Kind.INLINE_FRAGMENT) { const subtree = this.createLoadingStateTree( childSelection, fragmentMap, data, ); - if (typeof subtree !== 'boolean') { - // Not a leaf node - map._children = Object.assign(map._children || {}, subtree._children); - } + // Inline fragment node cannot be a leaf node, must have children + map._children = Object.assign( + map._children || {}, + (subtree as Record)._children, + ); } else { const childName = (childSelection as FieldNode).name.value; let childData; @@ -1479,28 +1496,27 @@ export class QueryManager { childData = data[childName]; isArray = Array.isArray(childData); } - const subtree = this.createLoadingStateTree( - childSelection, - fragmentMap, - // Just pass in the first elem of array, all of them will have - // the same fields - isArray && childData.length !== 0 ? childData[0] : childData, - ); if (!map._children) map._children = {}; if (isArray) { // Make sure that the shape of loadingState matches the shape of the // data. If an array is returned for a field, the loadingState should // be initialized with the correct number of elements. - const subtreeArr = []; - for (let i = 0; i < childData.length; i++) { - if (typeof subtree !== 'boolean') { - subtreeArr.push(subtree._children); - } - } + const subtreeArr = childData.map((d: any) => { + const subtree = this.createLoadingStateTree( + childSelection, + fragmentMap, + d, + ); + return typeof subtree === 'boolean' ? subtree : subtree._children; + }); map._children[childName] = { _children: subtreeArr }; } else { - map._children[childName] = subtree; + map._children[childName] = this.createLoadingStateTree( + childSelection, + fragmentMap, + childData, + ); } } } diff --git a/packages/apollo-client/src/data/queries.ts b/packages/apollo-client/src/data/queries.ts index cd1065fd063..e07c6df8d49 100644 --- a/packages/apollo-client/src/data/queries.ts +++ b/packages/apollo-client/src/data/queries.ts @@ -183,9 +183,21 @@ export class QueryStore { } } - public markQueryResultClient(queryId: string, complete: boolean) { + public markQueryResultClient( + queryId: string, + complete: boolean, + loadingState?: Record, + ) { if (!this.store[queryId]) return; + // Store loadingState if it is passed in + if (loadingState) { + this.store[queryId].loadingState = loadingState; + this.store[queryId].compactedLoadingState = this.compactLoadingStateTree( + loadingState, + ); + } + this.store[queryId].networkError = null; this.store[queryId].previousVariables = null; this.store[queryId].networkStatus = complete From 893ead88eaf300adf6a13ec56a150ddd9a48f920 Mon Sep 17 00:00:00 2001 From: clarencenpy Date: Tue, 31 Jul 2018 01:05:34 -0700 Subject: [PATCH 39/47] chore: Publish - apollo-boost@0.2.0-alpha.13 - apollo-cache-inmemory@1.2.6-alpha.13 - apollo-cache@1.1.13-alpha.13 - apollo-client@2.4.0-alpha.13 - apollo-utilities@1.0.17-alpha.13 - graphql-anywhere@4.1.15-alpha.13 --- packages/apollo-boost/package.json | 10 +++++----- packages/apollo-cache-inmemory/package.json | 8 ++++---- packages/apollo-cache/package.json | 4 ++-- packages/apollo-client/package.json | 8 ++++---- packages/apollo-utilities/package.json | 2 +- packages/graphql-anywhere/package.json | 4 ++-- 6 files changed, 18 insertions(+), 18 deletions(-) diff --git a/packages/apollo-boost/package.json b/packages/apollo-boost/package.json index cfb095d45e3..314b1d30261 100644 --- a/packages/apollo-boost/package.json +++ b/packages/apollo-boost/package.json @@ -1,6 +1,6 @@ { "name": "apollo-boost", - "version": "0.2.0-alpha.12", + "version": "0.2.0-alpha.13", "description": "The easiest way to get started with Apollo Client", "author": "Peggy Rayzis ", "contributors": [ @@ -34,9 +34,9 @@ "filesize": "npm run build && npm run build:browser" }, "dependencies": { - "apollo-cache": "1.1.13-alpha.12", - "apollo-cache-inmemory": "1.2.6-alpha.12", - "apollo-client": "2.4.0-alpha.12", + "apollo-cache": "1.1.13-alpha.13", + "apollo-cache-inmemory": "1.2.6-alpha.13", + "apollo-client": "2.4.0-alpha.13", "apollo-link": "^1.0.6", "apollo-link-error": "^1.0.3", "apollo-link-http": "alpha", @@ -46,7 +46,7 @@ "devDependencies": { "@types/graphql": "0.12.7", "@types/jest": "22.2.3", - "apollo-utilities": "1.0.17-alpha.12", + "apollo-utilities": "1.0.17-alpha.13", "browserify": "15.2.0", "fetch-mock": "6.5.1", "graphql": "0.13.2", diff --git a/packages/apollo-cache-inmemory/package.json b/packages/apollo-cache-inmemory/package.json index 394b7e9eef1..14e32058cd3 100644 --- a/packages/apollo-cache-inmemory/package.json +++ b/packages/apollo-cache-inmemory/package.json @@ -1,6 +1,6 @@ { "name": "apollo-cache-inmemory", - "version": "1.2.6-alpha.12", + "version": "1.2.6-alpha.13", "description": "Core abstract of Caching layer for Apollo Client", "author": "James Baxley ", "contributors": [ @@ -40,9 +40,9 @@ "filesize": "npm run build:browser" }, "dependencies": { - "apollo-cache": "1.1.13-alpha.12", - "apollo-utilities": "1.0.17-alpha.12", - "graphql-anywhere": "4.1.15-alpha.12" + "apollo-cache": "1.1.13-alpha.13", + "apollo-utilities": "1.0.17-alpha.13", + "graphql-anywhere": "4.1.15-alpha.13" }, "peerDependencies": { "graphql": "0.11.7 || ^0.12.0 || ^0.13.0" diff --git a/packages/apollo-cache/package.json b/packages/apollo-cache/package.json index be0910b71b3..be51bb09db6 100644 --- a/packages/apollo-cache/package.json +++ b/packages/apollo-cache/package.json @@ -1,6 +1,6 @@ { "name": "apollo-cache", - "version": "1.1.13-alpha.12", + "version": "1.1.13-alpha.13", "description": "Core abstract of Caching layer for Apollo Client", "author": "James Baxley ", "contributors": [ @@ -39,7 +39,7 @@ "filesize": "npm run build && npm run build:browser" }, "dependencies": { - "apollo-utilities": "1.0.17-alpha.12" + "apollo-utilities": "1.0.17-alpha.13" }, "devDependencies": { "@types/graphql": "0.12.7", diff --git a/packages/apollo-client/package.json b/packages/apollo-client/package.json index 7e41d28a5b5..ef287e16eb0 100644 --- a/packages/apollo-client/package.json +++ b/packages/apollo-client/package.json @@ -1,7 +1,7 @@ { "name": "apollo-client", "private": true, - "version": "2.4.0-alpha.12", + "version": "2.4.0-alpha.13", "description": "A simple yet functional GraphQL client.", "main": "./lib/bundle.umd.js", "module": "./lib/index.js", @@ -47,10 +47,10 @@ "license": "MIT", "dependencies": { "@types/zen-observable": "^0.8.0", - "apollo-cache": "1.1.13-alpha.12", + "apollo-cache": "1.1.13-alpha.13", "apollo-link": "^1.0.0", "apollo-link-dedup": "alpha", - "apollo-utilities": "1.0.17-alpha.12", + "apollo-utilities": "1.0.17-alpha.13", "symbol-observable": "^1.0.2", "zen-observable": "^0.8.0" }, @@ -64,7 +64,7 @@ "@types/jest": "22.2.3", "@types/lodash": "4.14.112", "@types/node": "10.5.2", - "apollo-cache-inmemory": "1.2.6-alpha.12", + "apollo-cache-inmemory": "1.2.6-alpha.13", "benchmark": "2.1.4", "browserify": "15.2.0", "bundlesize": "0.17.0", diff --git a/packages/apollo-utilities/package.json b/packages/apollo-utilities/package.json index 02da281aea0..aee1bd62777 100644 --- a/packages/apollo-utilities/package.json +++ b/packages/apollo-utilities/package.json @@ -1,6 +1,6 @@ { "name": "apollo-utilities", - "version": "1.0.17-alpha.12", + "version": "1.0.17-alpha.13", "description": "Utilities for working with GraphQL ASTs", "author": "James Baxley ", "contributors": [ diff --git a/packages/graphql-anywhere/package.json b/packages/graphql-anywhere/package.json index a383b7453ab..00b8c68984b 100644 --- a/packages/graphql-anywhere/package.json +++ b/packages/graphql-anywhere/package.json @@ -1,6 +1,6 @@ { "name": "graphql-anywhere", - "version": "4.1.15-alpha.12", + "version": "4.1.15-alpha.13", "description": "Run GraphQL queries with no schema and just one resolver", "main": "./lib/bundle.umd.js", "module": "./lib/index.js", @@ -39,7 +39,7 @@ ], "license": "MIT", "dependencies": { - "apollo-utilities": "1.0.17-alpha.12" + "apollo-utilities": "1.0.17-alpha.13" }, "devDependencies": { "@types/graphql": "0.12.7", From e995d19e578a2f6e82276b908247753625deb8ad Mon Sep 17 00:00:00 2001 From: clarencenpy Date: Thu, 2 Aug 2018 12:31:18 -0700 Subject: [PATCH 40/47] Make sure that loadingState is in sync with data read from cache --- .../src/inMemoryCache.ts | 3 +- .../src/readFromStore.ts | 9 ++-- .../apollo-client/src/core/ObservableQuery.ts | 12 +++--- .../apollo-client/src/core/QueryManager.ts | 41 ++++++++++++++----- packages/apollo-client/src/data/queries.ts | 16 ++++++++ 5 files changed, 59 insertions(+), 22 deletions(-) diff --git a/packages/apollo-cache-inmemory/src/inMemoryCache.ts b/packages/apollo-cache-inmemory/src/inMemoryCache.ts index b833cbbf3b4..bb05861af0c 100644 --- a/packages/apollo-cache-inmemory/src/inMemoryCache.ts +++ b/packages/apollo-cache-inmemory/src/inMemoryCache.ts @@ -86,7 +86,7 @@ export class InMemoryCache extends ApolloCache { return this.data.toObject(); } - public read(query: Cache.DiffOptions): T | null { + public read(query: Cache.ReadOptions): T | null { if (query.rootId && this.data.get(query.rootId) === undefined) { return null; } @@ -99,7 +99,6 @@ export class InMemoryCache extends ApolloCache { fragmentMatcherFunction: this.config.fragmentMatcher.match, previousResult: query.previousResult, config: this.config, - returnPartialData: query.returnPartialData, }); } diff --git a/packages/apollo-cache-inmemory/src/readFromStore.ts b/packages/apollo-cache-inmemory/src/readFromStore.ts index 85a1f03821e..40fa6a07a22 100644 --- a/packages/apollo-cache-inmemory/src/readFromStore.ts +++ b/packages/apollo-cache-inmemory/src/readFromStore.ts @@ -20,6 +20,7 @@ import { ReadStoreContext, DiffQueryAgainstStoreOptions, StoreObject, + ReadQueryOptions, } from './types'; /** @@ -48,13 +49,13 @@ export const ID_KEY = typeof Symbol !== 'undefined' ? Symbol('id') : '@@id'; * will be returned to preserve referential equality. */ export function readQueryFromStore( - options: DiffQueryAgainstStoreOptions, + options: ReadQueryOptions, ): QueryType { - // Defaults returnPartialData to false unless true is passed in - // for a deferred query + const optsPatch = { returnPartialData: false }; + return diffQueryAgainstStore({ ...options, - returnPartialData: options.returnPartialData || false, + ...optsPatch, }).result; } diff --git a/packages/apollo-client/src/core/ObservableQuery.ts b/packages/apollo-client/src/core/ObservableQuery.ts index 16021dc5655..9d04f890ef1 100644 --- a/packages/apollo-client/src/core/ObservableQuery.ts +++ b/packages/apollo-client/src/core/ObservableQuery.ts @@ -1,10 +1,10 @@ import { isEqual, - tryFunctionOrLogError, maybeDeepFreeze, + tryFunctionOrLogError, } from 'apollo-utilities'; import { GraphQLError } from 'graphql'; -import { NetworkStatus, isNetworkRequestInFlight } from './networkStatus'; +import { isNetworkRequestInFlight, NetworkStatus } from './networkStatus'; import { Observable, Observer, Subscription } from '../util/Observable'; import { QueryScheduler } from '../scheduler/scheduler'; @@ -14,12 +14,12 @@ import { ApolloError } from '../errors/ApolloError'; import { QueryManager } from './QueryManager'; import { ApolloQueryResult, FetchType, OperationVariables } from './types'; import { - ModifiableWatchQueryOptions, - WatchQueryOptions, + ErrorPolicy, FetchMoreQueryOptions, + ModifiableWatchQueryOptions, SubscribeToMoreOptions, - ErrorPolicy, UpdateQueryFn, + WatchQueryOptions, } from './watchQueryOptions'; import { QueryStoreValue } from '../data/queries'; @@ -221,6 +221,8 @@ export class ObservableQuery< if (queryStoreValue) { result.loadingState = queryStoreValue.compactedLoadingState; + } else { + result.loading = true; } if (!partial) { diff --git a/packages/apollo-client/src/core/QueryManager.ts b/packages/apollo-client/src/core/QueryManager.ts index 9f803e563b4..34df39cb0e5 100644 --- a/packages/apollo-client/src/core/QueryManager.ts +++ b/packages/apollo-client/src/core/QueryManager.ts @@ -1028,23 +1028,36 @@ export class QueryManager { if (newData) { return maybeDeepFreeze({ data: newData.result, partial: false }); } else { - try { - // the query is brand new, so we read from the store to see if anything is there - const data = this.dataStore.getCache().read({ + if (isDeferred) { + // For deferred queries, we actually want to use partial data + // since certain fields might still be streaming in. + // Setting returnPartialData to true so that + // an error does not get thrown if fields are missing. + const diffResult = this.dataStore.getCache().diff({ query, variables, previousResult: lastResult ? lastResult.data : undefined, optimistic, - // Setting returnPartialData to true for deferred queries, so that - // an error does not get thrown if fields are missing. - // Returning {data: {}} will give us problems as it clobbers the - // data that we have already received. - returnPartialData: isDeferred, + returnPartialData: true, }); - return maybeDeepFreeze({ data, partial: false }); - } catch (e) { - return maybeDeepFreeze({ data: {}, partial: true }); + return maybeDeepFreeze({ + data: diffResult.result, + partial: !diffResult.complete, + }); + } else { + try { + // the query is brand new, so we read from the store to see if anything is there + const data = this.dataStore.getCache().read({ + query, + variables, + previousResult: lastResult ? lastResult.data : undefined, + optimistic, + }); + return maybeDeepFreeze({ data, partial: false }); + } catch (e) { + return maybeDeepFreeze({ data: {}, partial: true }); + } } } } @@ -1267,6 +1280,12 @@ export class QueryManager { reject(error); }, complete: () => { + if (isDeferred) { + this.queryStore.markQueryComplete(queryId); + this.invalidate(true, queryId, fetchMoreForQueryId); + this.broadcastQueries(); + } + this.removeFetchQueryPromise(requestId); this.setQuery(queryId, ({ subscriptions }) => ({ subscriptions: subscriptions.filter(x => x !== subscription), diff --git a/packages/apollo-client/src/data/queries.ts b/packages/apollo-client/src/data/queries.ts index e07c6df8d49..202d8e87255 100644 --- a/packages/apollo-client/src/data/queries.ts +++ b/packages/apollo-client/src/data/queries.ts @@ -115,6 +115,22 @@ export class QueryStore { } } + public markQueryComplete(queryId: string) { + if (!this.store[queryId]) return; + this.store[queryId].networkStatus = NetworkStatus.ready; + } + + public updateLoadingState( + queryId: string, + loadingState: Record, + ) { + if (!this.store[queryId]) return; + this.store[queryId].loadingState = loadingState; + this.store[queryId].compactedLoadingState = this.compactLoadingStateTree( + loadingState, + ); + } + public markQueryResult( queryId: string, result: ExecutionResult | ExecutionPatchResult, From 701e20c1fbbd0122bd0c183a793d27be44c19172 Mon Sep 17 00:00:00 2001 From: clarencenpy Date: Fri, 3 Aug 2018 14:39:44 -0700 Subject: [PATCH 41/47] Set test environment to 'node' --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index e6e10b0b3eb..d5c3dee29f4 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ } ], "jest": { + "testEnvironment": "node", "transform": { ".(ts|tsx)": "/node_modules/ts-jest/preprocessor.js" } From 6f633daa5ce74a85b97ba26db341db7fecc1ef02 Mon Sep 17 00:00:00 2001 From: clarencenpy Date: Tue, 7 Aug 2018 19:02:11 -0700 Subject: [PATCH 42/47] Broadcasting changes from cache should update loadingState --- packages/apollo-client/src/core/ObservableQuery.ts | 8 +++++++- packages/apollo-client/src/core/QueryManager.ts | 14 +++++++++++++- packages/apollo-client/src/data/queries.ts | 5 +++++ 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/packages/apollo-client/src/core/ObservableQuery.ts b/packages/apollo-client/src/core/ObservableQuery.ts index 9d04f890ef1..a5d4cdcdc91 100644 --- a/packages/apollo-client/src/core/ObservableQuery.ts +++ b/packages/apollo-client/src/core/ObservableQuery.ts @@ -23,6 +23,7 @@ import { } from './watchQueryOptions'; import { QueryStoreValue } from '../data/queries'; +import { hasDirectives } from 'apollo-utilities'; export type ApolloCurrentResult = { data: T | {}; @@ -222,7 +223,12 @@ export class ObservableQuery< if (queryStoreValue) { result.loadingState = queryStoreValue.compactedLoadingState; } else { - result.loading = true; + if (hasDirectives(['defer'], this.options.query)) { + // Make sure that we have loadingState for deferred queries + // If the queryStore has not been initialized, set loading to true and + // wait for the next update. + result.loading = true; + } } if (!partial) { diff --git a/packages/apollo-client/src/core/QueryManager.ts b/packages/apollo-client/src/core/QueryManager.ts index 34df39cb0e5..c287856204f 100644 --- a/packages/apollo-client/src/core/QueryManager.ts +++ b/packages/apollo-client/src/core/QueryManager.ts @@ -400,7 +400,7 @@ export class QueryManager { // Initialize loadingState with the cached results if it is a deferred query let loadingState; - if (hasDirectives(['defer'], query.document)) { + if (query.isDeferred) { loadingState = this.initFieldLevelLoadingStates(query.document, { data: storeResult, }); @@ -1104,6 +1104,18 @@ export class QueryManager { // See here for more detail: https://github.com/apollostack/apollo-client/issues/231 .filter((x: QueryListener) => !!x) .forEach((listener: QueryListener) => { + if (info.newData) { + // Make sure that loadingState is updated for deferred queries + const queryStoreValue = this.queryStore.get(id); + if (queryStoreValue && queryStoreValue.isDeferred) { + this.queryStore.updateLoadingState( + id, + this.initFieldLevelLoadingStates(queryStoreValue.document, { + data: info.newData.result, + }), + ); + } + } listener(this.queryStore.get(id), info.newData); }); }); diff --git a/packages/apollo-client/src/data/queries.ts b/packages/apollo-client/src/data/queries.ts index 202d8e87255..a52ee2e3af9 100644 --- a/packages/apollo-client/src/data/queries.ts +++ b/packages/apollo-client/src/data/queries.ts @@ -4,9 +4,11 @@ import { isEqual } from 'apollo-utilities'; import { NetworkStatus } from '../core/networkStatus'; import { ExecutionPatchResult, isPatch } from '../core/types'; +import { hasDirectives } from 'apollo-utilities'; export type QueryStoreValue = { document: DocumentNode; + isDeferred: boolean; variables: Object; previousVariables?: Object | null; networkStatus: NetworkStatus; @@ -91,6 +93,9 @@ export class QueryStore { // before the initial fetch is done, you'll get an error. this.store[query.queryId] = { document: query.document, + isDeferred: query.document.definitions + ? hasDirectives(['defer'], query.document) + : false, variables: query.variables, previousVariables, networkError: null, From 3cbfc09b9d9d2ef0c71d11bd2308fcdc9da3473d Mon Sep 17 00:00:00 2001 From: clarencenpy Date: Tue, 7 Aug 2018 19:12:37 -0700 Subject: [PATCH 43/47] Remove `console.info` for each patch --- packages/apollo-client/src/core/QueryManager.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/apollo-client/src/core/QueryManager.ts b/packages/apollo-client/src/core/QueryManager.ts index b172cbe97ff..9e4041045c1 100644 --- a/packages/apollo-client/src/core/QueryManager.ts +++ b/packages/apollo-client/src/core/QueryManager.ts @@ -1243,11 +1243,6 @@ export class QueryManager { } if (isDeferred && isPatch(result)) { - // TODO: Remove console.info when out of alpha - console.info( - `Patch received for path ${JSON.stringify(result.path)}`, - ); - // Update loadingState for every patch received, by traversing its path curLoadingState = this.updateLoadingState( curLoadingState, From 41b4b8c694955c7aaf7bb2bc8e6d14935b415239 Mon Sep 17 00:00:00 2001 From: clarencenpy Date: Tue, 7 Aug 2018 19:17:29 -0700 Subject: [PATCH 44/47] Update CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e18e7555ed8..ce747fbfc07 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Apollo Client (vNext) +- Added `@defer` support [#3686](https://github.com/apollographql/apollo-client/pull/3686) - Adjusted the `graphql` peer dependency to cover explicit minor ranges. Since the ^ operator only covers any minor version if the major version is not 0 (since a major version of 0 is technically considered development by From dc53866ec2e072db1c80df51111c43525b9754f7 Mon Sep 17 00:00:00 2001 From: clarencenpy Date: Tue, 7 Aug 2018 19:18:35 -0700 Subject: [PATCH 45/47] chore: Publish - apollo-boost@0.2.0-alpha.14 - apollo-cache-inmemory@1.2.6-alpha.14 - apollo-cache@1.1.13-alpha.14 - apollo-client@2.4.0-alpha.14 - apollo-utilities@1.0.17-alpha.14 - graphql-anywhere@4.1.15-alpha.14 --- packages/apollo-boost/package.json | 10 +++++----- packages/apollo-cache-inmemory/package.json | 8 ++++---- packages/apollo-cache/package.json | 4 ++-- packages/apollo-client/package.json | 8 ++++---- packages/apollo-utilities/package.json | 2 +- packages/graphql-anywhere/package.json | 4 ++-- 6 files changed, 18 insertions(+), 18 deletions(-) diff --git a/packages/apollo-boost/package.json b/packages/apollo-boost/package.json index 7075b61f9f9..cc679b4ff3f 100644 --- a/packages/apollo-boost/package.json +++ b/packages/apollo-boost/package.json @@ -1,6 +1,6 @@ { "name": "apollo-boost", - "version": "0.2.0-alpha.13", + "version": "0.2.0-alpha.14", "description": "The easiest way to get started with Apollo Client", "author": "Peggy Rayzis ", "contributors": [ @@ -34,9 +34,9 @@ "filesize": "npm run build && npm run build:browser" }, "dependencies": { - "apollo-cache": "1.1.13-alpha.13", - "apollo-cache-inmemory": "1.2.6-alpha.13", - "apollo-client": "2.4.0-alpha.13", + "apollo-cache": "1.1.13-alpha.14", + "apollo-cache-inmemory": "1.2.6-alpha.14", + "apollo-client": "2.4.0-alpha.14", "apollo-link": "^1.0.6", "apollo-link-error": "^1.0.3", "apollo-link-http": "alpha", @@ -46,7 +46,7 @@ "devDependencies": { "@types/graphql": "0.12.7", "@types/jest": "22.2.3", - "apollo-utilities": "1.0.17-alpha.13", + "apollo-utilities": "1.0.17-alpha.14", "browserify": "15.2.0", "fetch-mock": "6.5.2", "graphql": "0.13.2", diff --git a/packages/apollo-cache-inmemory/package.json b/packages/apollo-cache-inmemory/package.json index be92ac7f00d..1931e50ca37 100644 --- a/packages/apollo-cache-inmemory/package.json +++ b/packages/apollo-cache-inmemory/package.json @@ -1,6 +1,6 @@ { "name": "apollo-cache-inmemory", - "version": "1.2.6-alpha.13", + "version": "1.2.6-alpha.14", "description": "Core abstract of Caching layer for Apollo Client", "author": "James Baxley ", "contributors": [ @@ -40,9 +40,9 @@ "filesize": "npm run build:browser" }, "dependencies": { - "apollo-cache": "1.1.13-alpha.13", - "apollo-utilities": "1.0.17-alpha.13", - "graphql-anywhere": "4.1.15-alpha.13" + "apollo-cache": "1.1.13-alpha.14", + "apollo-utilities": "1.0.17-alpha.14", + "graphql-anywhere": "4.1.15-alpha.14" }, "peerDependencies": { "graphql": "0.11.7 || ^0.12.0 || ^0.13.0" diff --git a/packages/apollo-cache/package.json b/packages/apollo-cache/package.json index 2e8dc2e47fe..1edcd0c1590 100644 --- a/packages/apollo-cache/package.json +++ b/packages/apollo-cache/package.json @@ -1,6 +1,6 @@ { "name": "apollo-cache", - "version": "1.1.13-alpha.13", + "version": "1.1.13-alpha.14", "description": "Core abstract of Caching layer for Apollo Client", "author": "James Baxley ", "contributors": [ @@ -39,7 +39,7 @@ "filesize": "npm run build && npm run build:browser" }, "dependencies": { - "apollo-utilities": "1.0.17-alpha.13" + "apollo-utilities": "1.0.17-alpha.14" }, "devDependencies": { "@types/graphql": "0.12.7", diff --git a/packages/apollo-client/package.json b/packages/apollo-client/package.json index 764914b7fca..a7aa597adb3 100644 --- a/packages/apollo-client/package.json +++ b/packages/apollo-client/package.json @@ -1,7 +1,7 @@ { "name": "apollo-client", "private": true, - "version": "2.4.0-alpha.13", + "version": "2.4.0-alpha.14", "description": "A simple yet functional GraphQL client.", "main": "./lib/bundle.umd.js", "module": "./lib/index.js", @@ -47,10 +47,10 @@ "license": "MIT", "dependencies": { "@types/zen-observable": "^0.8.0", - "apollo-cache": "1.1.13-alpha.13", + "apollo-cache": "1.1.13-alpha.14", "apollo-link": "^1.0.0", "apollo-link-dedup": "alpha", - "apollo-utilities": "1.0.17-alpha.13", + "apollo-utilities": "1.0.17-alpha.14", "symbol-observable": "^1.0.2", "zen-observable": "^0.8.0" }, @@ -65,7 +65,7 @@ "@types/jest": "22.2.3", "@types/lodash": "4.14.116", "@types/node": "10.5.6", - "apollo-cache-inmemory": "1.2.6-alpha.13", + "apollo-cache-inmemory": "1.2.6-alpha.14", "benchmark": "2.1.4", "browserify": "15.2.0", "bundlesize": "0.17.0", diff --git a/packages/apollo-utilities/package.json b/packages/apollo-utilities/package.json index 4b9502fa68d..a27b26668b6 100644 --- a/packages/apollo-utilities/package.json +++ b/packages/apollo-utilities/package.json @@ -1,6 +1,6 @@ { "name": "apollo-utilities", - "version": "1.0.17-alpha.13", + "version": "1.0.17-alpha.14", "description": "Utilities for working with GraphQL ASTs", "author": "James Baxley ", "contributors": [ diff --git a/packages/graphql-anywhere/package.json b/packages/graphql-anywhere/package.json index 6df5d2ecc2a..1a30c89c06f 100644 --- a/packages/graphql-anywhere/package.json +++ b/packages/graphql-anywhere/package.json @@ -1,6 +1,6 @@ { "name": "graphql-anywhere", - "version": "4.1.15-alpha.13", + "version": "4.1.15-alpha.14", "description": "Run GraphQL queries with no schema and just one resolver", "main": "./lib/bundle.umd.js", "module": "./lib/index.js", @@ -39,7 +39,7 @@ ], "license": "MIT", "dependencies": { - "apollo-utilities": "1.0.17-alpha.13" + "apollo-utilities": "1.0.17-alpha.14" }, "devDependencies": { "@types/graphql": "0.12.7", From 77e0c0c8ad8d60aa6731457bee7ed70bd98efc3f Mon Sep 17 00:00:00 2001 From: clarencenpy Date: Tue, 7 Aug 2018 20:41:58 -0700 Subject: [PATCH 46/47] Update defer docs with how to do preloading --- docs/source/features/defer-support.md | 87 +++++++++++++++++++++++---- 1 file changed, 75 insertions(+), 12 deletions(-) diff --git a/docs/source/features/defer-support.md b/docs/source/features/defer-support.md index 56d087c29da..db844d56a00 100644 --- a/docs/source/features/defer-support.md +++ b/docs/source/features/defer-support.md @@ -5,7 +5,7 @@ description: Optimize data loading with the @defer directive

Setting up

-Note: `@defer` support is an experimental feature that is only available in the alpha preview of Apollo Server and Apollo Client. +> Note: `@defer` support is an **experimental feature** that is only available in alpha versions of Apollo Server and Apollo Client. - On the server: @@ -19,7 +19,7 @@ Note: `@defer` support is an experimental feature that is only available in the ``` Or if you are using Apollo Client: ``` - npm install apollo-client@alpha apollo-cache-inmemory@alpha apollo-link-http@alpha apollo-link-error apollo-link + npm install apollo-client@alpha apollo-cache-inmemory@alpha apollo-link-http@alpha apollo-link-error apollo-link react-apollo@alpha ```

The `@defer` Directive

@@ -38,15 +38,19 @@ As an example, take a look at the following query that populates a NewsFeed page query NewsFeed { newsFeed { stories { - text + id + title comments { + id text } } recommendedForYou { story { - text + id + title comments { + id text } } @@ -66,15 +70,19 @@ We can optimize the above query with `@defer`: query NewsFeed { newsFeed { stories { - text + id + title comments @defer { + id text } } recommendedForYou @defer { story { - text + id + title comments @defer { + id text } } @@ -91,7 +99,7 @@ Once you have added `@defer`, Apollo Server will return an initial response with { "data": { "newsFeed": { - "stories": [{ "text": "...", "comments": null }], + "stories": [{ "id": "...", "title": "...", "comments": null }], "recommendedForYou": null } } @@ -105,7 +113,9 @@ Once you have added `@defer`, Apollo Server will return an initial response with "data": [ { "story": { - "text": "..." + "id": "...", + "title": "...", + "comments": null }, "matchScore": 99 } @@ -176,12 +186,12 @@ You can use it in a React component like this: ```graphql fragment StoryDetail on Story { id - text + title } query { newsFeed { stories { - text @defer + title @defer ...StoryDetail } } @@ -199,8 +209,61 @@ There is no additional setup for the transport required to use `@defer`. By defa `@defer` is one of those features that work best if used in moderation. If it is used too granularly (on many nested fields), the overhead of performing patching and re-rendering could be worse than just waiting for the full query to resolve. Try to limit `@defer` to fields that take a significantly longer time to load. This is super easy to figure out if you have Apollo Engine set up! +

Preloading Data with `@defer`

+ +Another super useful pattern for using `@defer` is preloading data that will be required in subsequent views. For illustration, imagine that each story has a `text` field that takes a long time to load. `text` is not required when we load the newsfeed view - we only need it to show the story detail view, which makes a query like this: + +```graphql +query StoryDetail($id: ID!) { + story(id: $id) { + id + title + text @defer + comments @defer { + id + text + } + } +} +``` + +However, instead for waiting for the user to navigate to the story detail view before firing that query, we could add `text` as a deferred field when we first load the newsfeed. This will allow `text` to preload in the background for all the stories. + +```graphql +query NewsFeed { + newsFeed { + stories { + id + title + text @defer # Not needed now, but preload it first + comments @defer { + id + text + } + } + } +} +``` + +Then, we will need to set up a [cache redirect](https://www.apollographql.com/docs/react/advanced/caching.html#cacheRedirect) to tell Apollo Client where to look for cached data for the `StoryDetail` query. + +```javascript +const client = new ApolloClient({ + uri: 'http://localhost:4000/graphql', + cacheRedirects: { + Query: { + story: (_, { id }, { getCacheKey }) => + getCacheKey({ __typename: 'Story', id }), + }, + }, +}); +``` + +Now, when the user navigates to each story detail view, it will load instantly as the data required is already fetched and stored in the cache. + +

Use with other GraphQL servers

-If you are sending queries to a GraphQL server that does not support `@defer`, it is likely that the `@defer` directive is simply ignored, or a GraphQL validation error is thrown. +If you are sending queries to a GraphQL server that does not support `@defer`, it is likely that the `@defer` directive is simply ignored, or a GraphQL validation error is thrown: `Unknown directive "defer"` -If you would want to implement a GraphQL server that is able to interoperate with Apollo Client, please look at the documentation [here](https://github.com/apollographql/apollo-server/blob/defer-support/docs/source/defer-support.md). +To implement a GraphQL server that will interoperate with Apollo Client for `@defer` support, please look at the [specification here](https://github.com/apollographql/apollo-server/blob/defer-support/docs/source/defer-support.md). From d904386ec2f4d3a34899563862426e6278bdc22b Mon Sep 17 00:00:00 2001 From: clarencenpy Date: Wed, 8 Aug 2018 17:40:54 -0700 Subject: [PATCH 47/47] Add type for `loadingState` --- packages/apollo-client/src/core/ObservableQuery.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/apollo-client/src/core/ObservableQuery.ts b/packages/apollo-client/src/core/ObservableQuery.ts index 7455895a958..49b25e90165 100644 --- a/packages/apollo-client/src/core/ObservableQuery.ts +++ b/packages/apollo-client/src/core/ObservableQuery.ts @@ -32,7 +32,17 @@ export type ApolloCurrentResult = { networkStatus: NetworkStatus; error?: ApolloError; partial?: boolean; - loadingState?: Record; + loadingState?: T | {}; + // loadingState is exposed to the client for deferred queries, with a shape + // that mirrors that of the data, but instead of the leaf nodes being + // GraphQLOutputType, it is (undefined | boolean). + // Right now, we have not accounted for this difference, but I think it is + // still usable in the context of checking for the presence of a field. + // + // TODO: Additional work needs to be done in `apollo-codegen-core` to generate + // a separate type for the loadingState, which will then be passed in as + // follows - ApolloCurrentResult + // Open issue here: https://github.com/apollographql/apollo-cli/issues/539 }; export interface FetchMoreOptions<