From dac4694e5571fb5ff1fd83af6c5bef045522b08b Mon Sep 17 00:00:00 2001 From: Ben Newman Date: Thu, 13 Dec 2018 09:31:22 -0500 Subject: [PATCH 01/11] Avoid async/await to reduce bundle size. Part of #4224. This change reduces the minified+gzip'd bundle size of apollo-client from 10385 bytes to 9771 bytes, a ~6% decrease, according to this command: npx terser -m -c < lib/bundle.umd.js | gzip | wc -c As long as we're only building one bundle, and it has to work in the 15% minority of browsers that do not natively support async/await, we should avoid wasting precious bundle size on generator runtime code, especially since we had only one await expression in the entire package (so the runtime code was nowhere close to paying for itself). --- .../apollo-client/src/core/QueryManager.ts | 29 ++++++++++--------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/packages/apollo-client/src/core/QueryManager.ts b/packages/apollo-client/src/core/QueryManager.ts index 19ca3778339..7abc28f4b48 100644 --- a/packages/apollo-client/src/core/QueryManager.ts +++ b/packages/apollo-client/src/core/QueryManager.ts @@ -191,7 +191,7 @@ export class QueryManager { optimisticResponse, }); - const completeMutation = async () => { + const completeMutation = () => { if (error) { this.mutationStore.markMutationError(mutationId, error); } @@ -204,7 +204,7 @@ export class QueryManager { this.broadcastQueries(); if (error) { - throw error; + return Promise.reject(error); } // allow for conditional refetches @@ -239,20 +239,21 @@ export class QueryManager { refetchQueryPromises.push(this.query(queryOptions)); } - if (awaitRefetchQueries) { - await Promise.all(refetchQueryPromises); - } + return Promise.all( + awaitRefetchQueries ? refetchQueryPromises : [], + ).then(() => { + this.setQuery(mutationId, () => ({ document: undefined })); - this.setQuery(mutationId, () => ({ document: undefined })); - if ( - errorPolicy === 'ignore' && - storeResult && - graphQLResultHasError(storeResult) - ) { - delete storeResult.errors; - } + if ( + errorPolicy === 'ignore' && + storeResult && + graphQLResultHasError(storeResult) + ) { + delete storeResult.errors; + } - return storeResult as FetchResult; + return storeResult as FetchResult; + }); }; execute(this.link, operation).subscribe({ From 948706fd1f62ac652c2670f7a8dfcbf339e73d70 Mon Sep 17 00:00:00 2001 From: Ben Newman Date: Thu, 13 Dec 2018 18:16:14 -0500 Subject: [PATCH 02/11] Inline tslib helpers for all packages. If you look at the lib/bundle.umd.js files for the apollo-client packages, you'll see multiple (re)declarations of TypeScript helpers like __extends, __assign, and __rest. This happens because TypeScript injects those helper function declarations into each module that uses them, so the declarations appear multiple times when those compiled modules are concatenated. Because the declarations share some identical code, gzip compression is able to eliminate some of the repetition, but it would be better if each helper appeared exactly once in each bundle.umd.js file. One solution is to rely on a shared runtime library, which means putting "importHelpers":true in your tsconfig.json file, and `npm install`ing the tslib package, from which the helpers can be imported. However, the tslib package contains all the helpers anyone could ever need, which is far more than we actually need (about 2KB minified with gzip, which is a lot). For this reason, we definitely do not want our bundles to end up calling require("tslib") at any point. Rollup to the rescue! By using rollup-plugin-node-resolve with { module: true, only: ['tslib'] }, we can inline exactly the tslib exports that we need, once, at the top of each bundle.umd.js file. There will still be some overlap between the helpers declared by different apollo-client packages, but at least now they will be exactly the same, so gzip can work its magic with maximal efficiency. --- config/rollup.config.js | 12 +++++++++++- config/tsconfig.base.json | 1 + package.json | 1 + 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/config/rollup.config.js b/config/rollup.config.js index dfda8a1193c..b09c4e914e6 100644 --- a/config/rollup.config.js +++ b/config/rollup.config.js @@ -1,3 +1,4 @@ +import node from 'rollup-plugin-node-resolve'; import sourcemaps from 'rollup-plugin-sourcemaps'; export const globals = { @@ -36,7 +37,16 @@ export default (name, override = {}) => { ); config.plugins = config.plugins || []; - config.plugins.push(sourcemaps()); + config.plugins.push( + sourcemaps(), + node({ + // Inline anything imported from the tslib package, e.g. __extends + // and __assign. This depends on the "importHelpers":true option in + // tsconfig.base.json. + module: true, + only: ['tslib'], + }), + ); return config; }; diff --git a/config/tsconfig.base.json b/config/tsconfig.base.json index 20e8a489eff..7662c0f0c7b 100644 --- a/config/tsconfig.base.json +++ b/config/tsconfig.base.json @@ -5,6 +5,7 @@ "noUnusedLocals": true, "skipLibCheck": true, "moduleResolution": "node", + "importHelpers": true, "removeComments": true, "sourceMap": true, "declaration": true, diff --git a/package.json b/package.json index f536312f227..cb6f9f3d04b 100644 --- a/package.json +++ b/package.json @@ -111,6 +111,7 @@ "rollup-plugin-sourcemaps": "0.4.2", "rxjs": "6.3.3", "ts-jest": "23.1.4", + "tslib": "^1.9.3", "tslint": "5.12.1", "typescript": "3.2.2", "uglify-js": "3.4.9", From b0a5b8e6023d1be7a0d63cbb840a02a66043e7b4 Mon Sep 17 00:00:00 2001 From: Ben Newman Date: Fri, 14 Dec 2018 14:01:25 -0500 Subject: [PATCH 03/11] Avoid importing graphql/language/printer in apollo-cache-inmemory. Importing the whole GraphQL printer just for nicer errors isn't worth the extra bundle size. JSON.stringify isn't as pretty, but provides all the necessary information. --- .../__tests__/__snapshots__/mapCache.ts.snap | 11 ++--------- .../__tests__/__snapshots__/roundtrip.ts.snap | 11 ++--------- .../__snapshots__/writeToStore.ts.snap | 11 ++--------- .../apollo-cache-inmemory/src/writeToStore.ts | 19 ++++++++++--------- 4 files changed, 16 insertions(+), 36 deletions(-) diff --git a/packages/apollo-cache-inmemory/src/__tests__/__snapshots__/mapCache.ts.snap b/packages/apollo-cache-inmemory/src/__tests__/__snapshots__/mapCache.ts.snap index 0ff0bd5d836..c3711026b09 100644 --- a/packages/apollo-cache-inmemory/src/__tests__/__snapshots__/mapCache.ts.snap +++ b/packages/apollo-cache-inmemory/src/__tests__/__snapshots__/mapCache.ts.snap @@ -246,14 +246,7 @@ Object { exports[`MapCache writing to the store throws when trying to write an object without id that was previously queried with id 1`] = ` "Error writing result to store for query: - query Failure { - item { - stringField - } -} - + {\\"kind\\":\\"Document\\",\\"definitions\\":[{\\"kind\\":\\"OperationDefinition\\",\\"operation\\":\\"query\\",\\"name\\":{\\"kind\\":\\"Name\\",\\"value\\":\\"Failure\\"},\\"variableDefinitions\\":[],\\"directives\\":[],\\"selectionSet\\":{\\"kind\\":\\"SelectionSet\\",\\"selections\\":[{\\"kind\\":\\"Field\\",\\"name\\":{\\"kind\\":\\"Name\\",\\"value\\":\\"item\\"},\\"arguments\\":[],\\"directives\\":[],\\"selectionSet\\":{\\"kind\\":\\"SelectionSet\\",\\"selections\\":[{\\"kind\\":\\"Field\\",\\"name\\":{\\"kind\\":\\"Name\\",\\"value\\":\\"stringField\\"},\\"arguments\\":[],\\"directives\\":[]}]}}]}}],\\"loc\\":{\\"start\\":0,\\"end\\":106}} Store error: the application attempted to write an object with no provided id but the store already contains an id of abcd for this object. The selectionSet that was trying to be written is: -item { - stringField -}" +{\\"kind\\":\\"Field\\",\\"name\\":{\\"kind\\":\\"Name\\",\\"value\\":\\"item\\"},\\"arguments\\":[],\\"directives\\":[],\\"selectionSet\\":{\\"kind\\":\\"SelectionSet\\",\\"selections\\":[{\\"kind\\":\\"Field\\",\\"name\\":{\\"kind\\":\\"Name\\",\\"value\\":\\"stringField\\"},\\"arguments\\":[],\\"directives\\":[]}]}}" `; diff --git a/packages/apollo-cache-inmemory/src/__tests__/__snapshots__/roundtrip.ts.snap b/packages/apollo-cache-inmemory/src/__tests__/__snapshots__/roundtrip.ts.snap index 42032481637..b42d4237c6d 100644 --- a/packages/apollo-cache-inmemory/src/__tests__/__snapshots__/roundtrip.ts.snap +++ b/packages/apollo-cache-inmemory/src/__tests__/__snapshots__/roundtrip.ts.snap @@ -4,14 +4,7 @@ exports[ `writing to the store throws when trying to write an object without id that was previously queried with id 1` ] = ` "Error writing result to store for query: - query Failure { - item { - stringField - } -} - + {\\"kind\\":\\"Document\\",\\"definitions\\":[{\\"kind\\":\\"OperationDefinition\\",\\"operation\\":\\"query\\",\\"name\\":{\\"kind\\":\\"Name\\",\\"value\\":\\"Failure\\"},\\"variableDefinitions\\":[],\\"directives\\":[],\\"selectionSet\\":{\\"kind\\":\\"SelectionSet\\",\\"selections\\":[{\\"kind\\":\\"Field\\",\\"name\\":{\\"kind\\":\\"Name\\",\\"value\\":\\"item\\"},\\"arguments\\":[],\\"directives\\":[],\\"selectionSet\\":{\\"kind\\":\\"SelectionSet\\",\\"selections\\":[{\\"kind\\":\\"Field\\",\\"name\\":{\\"kind\\":\\"Name\\",\\"value\\":\\"stringField\\"},\\"arguments\\":[],\\"directives\\":[]}]}}]}}],\\"loc\\":{\\"start\\":0,\\"end\\":106}} Store error: the application attempted to write an object with no provided id but the store already contains an id of abcd for this object. The selectionSet that was trying to be written is: -item { - stringField -}" +{\\"kind\\":\\"Field\\",\\"name\\":{\\"kind\\":\\"Name\\",\\"value\\":\\"item\\"},\\"arguments\\":[],\\"directives\\":[],\\"selectionSet\\":{\\"kind\\":\\"SelectionSet\\",\\"selections\\":[{\\"kind\\":\\"Field\\",\\"name\\":{\\"kind\\":\\"Name\\",\\"value\\":\\"stringField\\"},\\"arguments\\":[],\\"directives\\":[]}]}}" `; diff --git a/packages/apollo-cache-inmemory/src/__tests__/__snapshots__/writeToStore.ts.snap b/packages/apollo-cache-inmemory/src/__tests__/__snapshots__/writeToStore.ts.snap index 42032481637..b42d4237c6d 100644 --- a/packages/apollo-cache-inmemory/src/__tests__/__snapshots__/writeToStore.ts.snap +++ b/packages/apollo-cache-inmemory/src/__tests__/__snapshots__/writeToStore.ts.snap @@ -4,14 +4,7 @@ exports[ `writing to the store throws when trying to write an object without id that was previously queried with id 1` ] = ` "Error writing result to store for query: - query Failure { - item { - stringField - } -} - + {\\"kind\\":\\"Document\\",\\"definitions\\":[{\\"kind\\":\\"OperationDefinition\\",\\"operation\\":\\"query\\",\\"name\\":{\\"kind\\":\\"Name\\",\\"value\\":\\"Failure\\"},\\"variableDefinitions\\":[],\\"directives\\":[],\\"selectionSet\\":{\\"kind\\":\\"SelectionSet\\",\\"selections\\":[{\\"kind\\":\\"Field\\",\\"name\\":{\\"kind\\":\\"Name\\",\\"value\\":\\"item\\"},\\"arguments\\":[],\\"directives\\":[],\\"selectionSet\\":{\\"kind\\":\\"SelectionSet\\",\\"selections\\":[{\\"kind\\":\\"Field\\",\\"name\\":{\\"kind\\":\\"Name\\",\\"value\\":\\"stringField\\"},\\"arguments\\":[],\\"directives\\":[]}]}}]}}],\\"loc\\":{\\"start\\":0,\\"end\\":106}} Store error: the application attempted to write an object with no provided id but the store already contains an id of abcd for this object. The selectionSet that was trying to be written is: -item { - stringField -}" +{\\"kind\\":\\"Field\\",\\"name\\":{\\"kind\\":\\"Name\\",\\"value\\":\\"item\\"},\\"arguments\\":[],\\"directives\\":[],\\"selectionSet\\":{\\"kind\\":\\"SelectionSet\\",\\"selections\\":[{\\"kind\\":\\"Field\\",\\"name\\":{\\"kind\\":\\"Name\\",\\"value\\":\\"stringField\\"},\\"arguments\\":[],\\"directives\\":[]}]}}" `; diff --git a/packages/apollo-cache-inmemory/src/writeToStore.ts b/packages/apollo-cache-inmemory/src/writeToStore.ts index ee12dc91019..3abdd511720 100644 --- a/packages/apollo-cache-inmemory/src/writeToStore.ts +++ b/packages/apollo-cache-inmemory/src/writeToStore.ts @@ -5,7 +5,6 @@ import { InlineFragmentNode, FragmentDefinitionNode, } from 'graphql'; -import { print } from 'graphql/language/printer'; import { FragmentMatcher } from './readFromStore'; import { @@ -45,7 +44,7 @@ export class WriteError extends Error { export function enhanceErrorWithDocument(error: Error, document: DocumentNode) { // XXX A bit hacky maybe ... const enhancedError = new WriteError( - `Error writing result to store for query:\n ${print(document)}`, + `Error writing result to store for query:\n ${JSON.stringify(document)}`, ); enhancedError.message += '\n' + error.message; enhancedError.stack = error.stack; @@ -360,7 +359,7 @@ export class StoreWriter { escapedId.id } for this object. The selectionSet` + ` that was trying to be written is:\n` + - print(field), + JSON.stringify(field), ); } // checks if we "lost" the typename @@ -371,7 +370,7 @@ export class StoreWriter { escapedId.typename } for the object of id ${escapedId.id}. The selectionSet` + ` that was trying to be written is:\n` + - print(field), + JSON.stringify(field), ); } @@ -469,11 +468,13 @@ function mergeWithGenerated( const value = generated[key]; const realValue = real[key]; - if (isIdValue(value) && - isGeneratedId(value.id) && - isIdValue(realValue) && - ! isEqual(value, realValue) && - mergeWithGenerated(value.id, realValue.id, cache)) { + if ( + isIdValue(value) && + isGeneratedId(value.id) && + isIdValue(realValue) && + !isEqual(value, realValue) && + mergeWithGenerated(value.id, realValue.id, cache) + ) { madeChanges = true; } }); From b4cfd4d4e694c22fa734607ee305268111b51240 Mon Sep 17 00:00:00 2001 From: Ben Newman Date: Fri, 14 Dec 2018 14:07:24 -0500 Subject: [PATCH 04/11] Avoid importing graphql/language/visitor in apollo-cache-inmemory. While knowing QueryDocumentKeys was helpful in theory, in practice we were importing the entire graphql/language/visitor module unnecessarily, and then had to tiptoe around the strange absence of primitive-valued keys (for example, QueryDocumentKeys.StringValue is an empty array, when it arguably should be ["value"]). --- .../src/queryKeyMaker.ts | 48 +++++-------------- 1 file changed, 12 insertions(+), 36 deletions(-) diff --git a/packages/apollo-cache-inmemory/src/queryKeyMaker.ts b/packages/apollo-cache-inmemory/src/queryKeyMaker.ts index cf3ed80f898..18c7b59c131 100644 --- a/packages/apollo-cache-inmemory/src/queryKeyMaker.ts +++ b/packages/apollo-cache-inmemory/src/queryKeyMaker.ts @@ -1,6 +1,10 @@ -import { CacheKeyNode } from "./optimism"; -import { DocumentNode, SelectionSetNode, FragmentSpreadNode, FragmentDefinitionNode } from "graphql"; -import { QueryDocumentKeys } from "graphql/language/visitor"; +import { CacheKeyNode } from './optimism'; +import { + DocumentNode, + SelectionSetNode, + FragmentSpreadNode, + FragmentDefinitionNode, +} from 'graphql'; const CIRCULAR = Object.create(null); const objToStr = Object.prototype.toString; @@ -117,42 +121,14 @@ class PerQueryKeyMaker { } } -const queryKeyMap: { - [key: string]: { [key: string]: boolean } -} = Object.create(null); - -Object.keys(QueryDocumentKeys).forEach(parentKind => { - const childKeys = queryKeyMap[parentKind] = Object.create(null); - - (QueryDocumentKeys as { - [key: string]: any[] - })[parentKind].forEach(childKey => { - childKeys[childKey] = true; - }); - - if (parentKind === "FragmentSpread") { - // A custom key that we include when looking up FragmentSpread nodes. - childKeys["fragment"] = true; - } -}); - function safeSortedKeys(object: { [key: string]: any }): string[] { const keys = Object.keys(object); - const keyCount = keys.length; - const knownKeys = typeof object.kind === "string" && queryKeyMap[object.kind]; - - // Remove unknown object-valued keys from the array, but leave keys with - // non-object values untouched. - let target = 0; - for (let source = target; source < keyCount; ++source) { - const key = keys[source]; - const value = object[key]; - const isObjectOrArray = value !== null && typeof value === "object"; - if (! isObjectOrArray || ! knownKeys || knownKeys[key] === true) { - keys[target++] = key; - } + + // Exclude any .loc properties. + const locIndex = keys.indexOf('loc'); + if (locIndex >= 0) { + keys.splice(locIndex, 1); } - keys.length = target; return keys.sort(); } From 70c4cf3fdbad6e994df2c2cae5c0f3684bd5fe18 Mon Sep 17 00:00:00 2001 From: Ben Newman Date: Fri, 14 Dec 2018 14:37:05 -0500 Subject: [PATCH 05/11] Avoid importing graphql/language/printer in apollo-client. --- CHANGELOG.md | 8 ++++++-- packages/apollo-client/rollup.config.js | 1 - packages/apollo-client/src/__tests__/client.ts | 11 ----------- packages/apollo-client/src/core/QueryManager.ts | 6 ++---- packages/apollo-client/src/data/mutations.ts | 8 +++++--- packages/apollo-client/src/data/queries.ts | 3 +-- packages/apollo-client/src/index.ts | 2 -- 7 files changed, 14 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ac927e49817..b0d80e42549 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ ## Apollo Client (vNext) +### Apollo Client (vNext) + +- The `apollo-client` package no longer exports a `printAST` function from + `graphql/language/printer`. If you need this functionality, import it + directly: `import { print } from "graphql/language/printer"` + ## Apollo Client (2.4.9) ### Apollo Client (2.4.9) @@ -71,8 +77,6 @@ [@PowerKiKi](https://github.com/PowerKiKi) in [#3693](https://github.com/apollographql/apollo-client/pull/3693)
[@nandito](https://github.com/nandito) in [#3865](https://github.com/apollographql/apollo-client/pull/3865) -### Apollo Utilities (1.0.27) - - Schema/AST tranformation utilities have been updated to work properly with `@client` directives.
[@justinmakaila](https://github.com/justinmakaila) in [#3482](https://github.com/apollographql/apollo-client/pull/3482) diff --git a/packages/apollo-client/rollup.config.js b/packages/apollo-client/rollup.config.js index 8bf170e2b42..adaf06f3a37 100644 --- a/packages/apollo-client/rollup.config.js +++ b/packages/apollo-client/rollup.config.js @@ -3,7 +3,6 @@ import build, { globals } from '../../config/rollup.config'; const globalsOverride = { ...globals, 'symbol-observable': '$$observable', - 'graphql/language/printer': 'print', }; export default build('apollo.core', { diff --git a/packages/apollo-client/src/__tests__/client.ts b/packages/apollo-client/src/__tests__/client.ts index 56935619577..27aed28de30 100644 --- a/packages/apollo-client/src/__tests__/client.ts +++ b/packages/apollo-client/src/__tests__/client.ts @@ -1,7 +1,6 @@ import { cloneDeep, assign } from 'lodash'; import { GraphQLError, ExecutionResult, DocumentNode } from 'graphql'; import gql from 'graphql-tag'; -import { print } from 'graphql/language/printer'; import { ApolloLink, Observable } from 'apollo-link'; import { InMemoryCache, @@ -1992,16 +1991,6 @@ describe('client', () => { }); }); - it('should expose a method called printAST that is prints graphql queries', () => { - const query = gql` - query { - fortuneCookie - } - `; - - expect(printAST(query)).toBe(print(query)); - }); - it('should pass a network error correctly on a mutation', done => { const mutation = gql` mutation { diff --git a/packages/apollo-client/src/core/QueryManager.ts b/packages/apollo-client/src/core/QueryManager.ts index 7abc28f4b48..4160d5bc4fa 100644 --- a/packages/apollo-client/src/core/QueryManager.ts +++ b/packages/apollo-client/src/core/QueryManager.ts @@ -1,6 +1,5 @@ import { execute, ApolloLink, FetchResult } from 'apollo-link'; import { ExecutionResult, DocumentNode } from 'graphql'; -import { print } from 'graphql/language/printer'; import { DedupLink as Deduplicator } from 'apollo-link-dedup'; import { Cache } from 'apollo-cache'; import { @@ -145,7 +144,6 @@ export class QueryManager { getDefaultValues(getMutationDefinition(mutation)), variables, )); - const mutationString = print(mutation); this.setQuery(mutationId, () => ({ document: mutation })); @@ -169,7 +167,7 @@ export class QueryManager { return ret; }; - this.mutationStore.initMutation(mutationId, mutationString, variables); + this.mutationStore.initMutation(mutationId, mutation, variables); this.dataStore.markMutationInit({ mutationId, @@ -534,7 +532,7 @@ export class QueryManager { console.info( 'An unhandled error was thrown because no error handler is registered ' + 'for the query ' + - print(queryStoreValue.document), + JSON.stringify(queryStoreValue.document), ); } } diff --git a/packages/apollo-client/src/data/mutations.ts b/packages/apollo-client/src/data/mutations.ts index 6303ab65395..645d30c4ffc 100644 --- a/packages/apollo-client/src/data/mutations.ts +++ b/packages/apollo-client/src/data/mutations.ts @@ -1,3 +1,5 @@ +import { DocumentNode } from 'graphql'; + export class MutationStore { private store: { [mutationId: string]: MutationStoreValue } = {}; @@ -11,11 +13,11 @@ export class MutationStore { public initMutation( mutationId: string, - mutationString: string, + mutation: DocumentNode, variables: Object | undefined, ) { this.store[mutationId] = { - mutationString: mutationString, + mutation, variables: variables || {}, loading: true, error: null, @@ -50,7 +52,7 @@ export class MutationStore { } export interface MutationStoreValue { - mutationString: string; + mutation: DocumentNode; variables: Object; loading: boolean; error: Error | null; diff --git a/packages/apollo-client/src/data/queries.ts b/packages/apollo-client/src/data/queries.ts index 9e120f4c18c..1735082ddfd 100644 --- a/packages/apollo-client/src/data/queries.ts +++ b/packages/apollo-client/src/data/queries.ts @@ -1,5 +1,4 @@ import { DocumentNode, GraphQLError, ExecutionResult } from 'graphql'; -import { print } from 'graphql/language/printer'; import { isEqual } from 'apollo-utilities'; import { NetworkStatus } from '../core/networkStatus'; @@ -40,7 +39,7 @@ export class QueryStore { if ( previousQuery && previousQuery.document !== query.document && - print(previousQuery.document) !== print(query.document) + !isEqual(previousQuery.document, query.document) ) { // XXX we're throwing an error here to catch bugs where a query gets overwritten by a new one. // we should implement a separate action for refetching so that QUERY_INIT may never overwrite diff --git a/packages/apollo-client/src/index.ts b/packages/apollo-client/src/index.ts index 359210cc94e..fcb7db015e5 100644 --- a/packages/apollo-client/src/index.ts +++ b/packages/apollo-client/src/index.ts @@ -1,5 +1,3 @@ -export { print as printAST } from 'graphql/language/printer'; - export { ObservableQuery, FetchMoreOptions, From d4f3346d50d89d7d276c8ec5215617cf7c1120f2 Mon Sep 17 00:00:00 2001 From: Ben Newman Date: Sat, 15 Dec 2018 20:15:03 -0500 Subject: [PATCH 06/11] Remove QueryManager from public ApolloClient API. By privatizing the QueryManager, we give ourselves room to change its API in drastic ways without affecting the ApolloClient API. --- packages/apollo-client/src/ApolloClient.ts | 66 +++++++++---------- .../apollo-client/src/__tests__/client.ts | 6 +- 2 files changed, 36 insertions(+), 36 deletions(-) diff --git a/packages/apollo-client/src/ApolloClient.ts b/packages/apollo-client/src/ApolloClient.ts index 3a270aab636..4f67b238e25 100644 --- a/packages/apollo-client/src/ApolloClient.ts +++ b/packages/apollo-client/src/ApolloClient.ts @@ -63,7 +63,7 @@ export default class ApolloClient implements DataProxy { public link: ApolloLink; public store: DataStore; public cache: ApolloCache; - public queryManager: QueryManager | undefined; + private queryManager: QueryManager | undefined; public disableNetworkFetches: boolean; public version: string; public queryDeduplication: boolean; @@ -418,38 +418,6 @@ export default class ApolloClient implements DataProxy { return execute(this.link, payload); } - /** - * This initializes the query manager that tracks queries and the cache - */ - public initQueryManager(): QueryManager { - if (!this.queryManager) { - this.queryManager = new QueryManager({ - link: this.link, - store: this.store, - queryDeduplication: this.queryDeduplication, - ssrMode: this.ssrMode, - clientAwareness: this.clientAwareness, - onBroadcast: () => { - if (this.devToolsHookCb) { - this.devToolsHookCb({ - action: {}, - state: { - queries: this.queryManager - ? this.queryManager.queryStore.getStore() - : {}, - mutations: this.queryManager - ? this.queryManager.mutationStore.getStore() - : {}, - }, - dataWithOptimisticResults: this.cache.extract(true), - }); - } - }, - }); - } - return this.queryManager; - } - /** * Resets your entire store by clearing out your cache and then re-executing * all of your active queries. This makes it so that you may guarantee that @@ -557,6 +525,38 @@ export default class ApolloClient implements DataProxy { return this.initProxy().restore(serializedState); } + /** + * This initializes the query manager that tracks queries and the cache + */ + private initQueryManager(): QueryManager { + if (!this.queryManager) { + this.queryManager = new QueryManager({ + link: this.link, + store: this.store, + queryDeduplication: this.queryDeduplication, + ssrMode: this.ssrMode, + clientAwareness: this.clientAwareness, + onBroadcast: () => { + if (this.devToolsHookCb) { + this.devToolsHookCb({ + action: {}, + state: { + queries: this.queryManager + ? this.queryManager.queryStore.getStore() + : {}, + mutations: this.queryManager + ? this.queryManager.mutationStore.getStore() + : {}, + }, + dataWithOptimisticResults: this.cache.extract(true), + }); + } + }, + }); + } + return this.queryManager; + } + /** * Initializes a data proxy for this client instance if one does not already * exist and returns either a previously initialized proxy instance or the diff --git a/packages/apollo-client/src/__tests__/client.ts b/packages/apollo-client/src/__tests__/client.ts index 27aed28de30..5ffd282a666 100644 --- a/packages/apollo-client/src/__tests__/client.ts +++ b/packages/apollo-client/src/__tests__/client.ts @@ -28,11 +28,11 @@ describe('client', () => { cache: new InMemoryCache(), }); - expect(client.queryManager).toBeUndefined(); + expect((client as any).queryManager).toBeUndefined(); // We only create the query manager on the first query - client.initQueryManager(); - expect(client.queryManager).toBeDefined(); + (client as any).initQueryManager(); + expect((client as any).queryManager).toBeDefined(); expect(client.cache).toBeDefined(); }); From f2bcf70d7277cd5ef4eada3fe9112d617a68b702 Mon Sep 17 00:00:00 2001 From: Ben Newman Date: Sun, 16 Dec 2018 13:40:50 -0500 Subject: [PATCH 07/11] Simplify fetchQuery promise tracking in QueryManager. We're only tracking fetchQuery promises in order to cancel the in-flight ones when clearStore is called, so we can get away with just storing the reject functions, thereby saving bundle size. --- .../apollo-client/src/core/QueryManager.ts | 74 ++++++++----------- 1 file changed, 29 insertions(+), 45 deletions(-) diff --git a/packages/apollo-client/src/core/QueryManager.ts b/packages/apollo-client/src/core/QueryManager.ts index 4160d5bc4fa..35d3f3141c1 100644 --- a/packages/apollo-client/src/core/QueryManager.ts +++ b/packages/apollo-client/src/core/QueryManager.ts @@ -53,11 +53,6 @@ export interface QueryInfo { cancel?: (() => void); } -export interface QueryPromise { - resolve: (result: ApolloQueryResult) => void; - reject: (error: Error) => void; -} - export class QueryManager { public scheduler: QueryScheduler; public link: ApolloLink; @@ -78,10 +73,10 @@ export class QueryManager { // subscriptions as well private queries: Map = new Map(); - // A map going from a requestId to a promise that has not yet been resolved. We use this to keep - // track of queries that are inflight and reject them in case some - // destabalizing action occurs (e.g. reset of the Apollo store). - private fetchQueryPromises: Map = new Map(); + // A set of Promise reject functions for fetchQuery promises that have not + // yet been resolved, used to keep track of in-flight queries so that we can + // reject them in case a destabilizing event occurs (e.g. Apollo store reset). + private fetchQueryRejectFns = new Set(); // A map going from the name of a query to an observer issued for it by watchQuery. This is // generally used to refetches for refetchQueries and to update mutation results through @@ -410,8 +405,6 @@ export class QueryManager { this.broadcastQueries(); } - this.removeFetchQueryPromise(requestId); - throw new ApolloError({ networkError: error }); } }); @@ -695,21 +688,18 @@ export class QueryManager { throw new Error('pollInterval option only supported on watchQuery.'); } - const requestId = this.idCounter; - return new Promise>((resolve, reject) => { - this.addFetchQueryPromise(requestId, resolve, reject); - - return this.watchQuery(options, false) + this.fetchQueryRejectFns.add(reject); + this.watchQuery(options, false) .result() - .then(result => { - this.removeFetchQueryPromise(requestId); - resolve(result); - }) - .catch(error => { - this.removeFetchQueryPromise(requestId); - reject(error); - }); + .then(resolve, reject) + // Since neither resolve nor reject throw or return a value, this .then + // handler is guaranteed to execute. Note that it doesn't really matter + // when we remove the reject function from this.fetchQueryRejectFns, + // since resolve and reject are mutually idempotent. In fact, it would + // not be incorrect to let reject functions accumulate over time; it's + // just a waste of memory. + .then(() => this.fetchQueryRejectFns.delete(reject)); }); } @@ -762,23 +752,6 @@ export class QueryManager { }); } - // Adds a promise to this.fetchQueryPromises for a given request ID. - public addFetchQueryPromise( - requestId: number, - resolve: (result: ApolloQueryResult) => void, - reject: (error: Error) => void, - ) { - this.fetchQueryPromises.set(requestId.toString(), { - resolve, - reject, - }); - } - - // Removes the promise in this.fetchQueryPromises for a particular request ID. - public removeFetchQueryPromise(requestId: number) { - this.fetchQueryPromises.delete(requestId.toString()); - } - // Adds an ObservableQuery to this.observableQueries and to this.observableQueriesByName. public addObservableQuery( queryId: string, @@ -821,7 +794,7 @@ export class QueryManager { // in the data portion of the store. So, we cancel the promises and observers // that we have issued so far and not yet resolved (in the case of // queries). - this.fetchQueryPromises.forEach(({ reject }) => { + this.fetchQueryRejectFns.forEach(reject => { reject( new Error( 'Store reset while query was in flight(not completed in link chain)', @@ -1104,8 +1077,13 @@ export class QueryManager { let resultFromStore: any; let errorsFromStore: any; + let rejectFetchPromise: (reason?: any) => void; + return new Promise>((resolve, reject) => { - this.addFetchQueryPromise(requestId, resolve, reject); + // Need to assign the reject function to the rejectFetchPromise variable + // in the outer scope so that we can refer to it in the .catch handler. + this.fetchQueryRejectFns.add(rejectFetchPromise = reject); + const subscription = execute(this.deduplicator, operation).subscribe({ next: (result: ExecutionResult) => { // default the lastRequestId to 1 @@ -1171,7 +1149,8 @@ export class QueryManager { } }, error: (error: ApolloError) => { - this.removeFetchQueryPromise(requestId); + this.fetchQueryRejectFns.delete(reject); + this.setQuery(queryId, ({ subscriptions }) => ({ subscriptions: subscriptions.filter(x => x !== subscription), })); @@ -1179,7 +1158,8 @@ export class QueryManager { reject(error); }, complete: () => { - this.removeFetchQueryPromise(requestId); + this.fetchQueryRejectFns.delete(reject); + this.setQuery(queryId, ({ subscriptions }) => ({ subscriptions: subscriptions.filter(x => x !== subscription), })); @@ -1197,6 +1177,10 @@ export class QueryManager { this.setQuery(queryId, ({ subscriptions }) => ({ subscriptions: subscriptions.concat([subscription]), })); + + }).catch(error => { + this.fetchQueryRejectFns.delete(rejectFetchPromise); + throw error; }); } From 42902f12452cf17dfa821a9d04a7def3d15c4185 Mon Sep 17 00:00:00 2001 From: Ben Newman Date: Tue, 18 Dec 2018 08:34:41 -0500 Subject: [PATCH 08/11] Remove QueryKeyMaker abstraction. (#4245) It's nice to be able to assume that (sub)queries with equivalent structure share the same object identities, but the cache will work without that assumption, and enforcing that discipline is not free, both in terms of runtime cost and in terms of bundle size. In the future, the `graphql-tag` tag package should provide a version of the `gql` template tag function that returns immutable structures that share object identity where possible. This commit alone saves 450ish bytes after minification and gzip! --- .../src/__tests__/queryKeyMaker.ts | 51 ------- .../src/queryKeyMaker.ts | 134 ------------------ .../src/readFromStore.ts | 9 +- 3 files changed, 2 insertions(+), 192 deletions(-) delete mode 100644 packages/apollo-cache-inmemory/src/__tests__/queryKeyMaker.ts delete mode 100644 packages/apollo-cache-inmemory/src/queryKeyMaker.ts diff --git a/packages/apollo-cache-inmemory/src/__tests__/queryKeyMaker.ts b/packages/apollo-cache-inmemory/src/__tests__/queryKeyMaker.ts deleted file mode 100644 index d1e252747cc..00000000000 --- a/packages/apollo-cache-inmemory/src/__tests__/queryKeyMaker.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { QueryKeyMaker } from '../queryKeyMaker'; -import { CacheKeyNode } from '../optimism'; -import gql from 'graphql-tag'; -import { DocumentNode } from 'graphql'; - -describe('QueryKeyMaker', () => { - const cacheKeyRoot = new CacheKeyNode(); - const queryKeyMaker = new QueryKeyMaker(cacheKeyRoot); - - it('should work', () => { - const query1: DocumentNode = gql` - query { - foo - bar - } - `; - - const query2: DocumentNode = gql` - query { - # comment - foo - bar - } - `; - - const keyMaker1 = queryKeyMaker.forQuery(query1); - const keyMaker2 = queryKeyMaker.forQuery(query2); - - expect(keyMaker1.lookupQuery(query2)).toBe(keyMaker2.lookupQuery(query1)); - - expect(keyMaker1.lookupQuery(query1)).toBe(keyMaker2.lookupQuery(query2)); - - let checkCount = 0; - query1.definitions.forEach((def1, i) => { - const def2 = query2.definitions[i]; - expect(def1).not.toBe(def2); - if ( - def1.kind === 'OperationDefinition' && - def2.kind === 'OperationDefinition' - ) { - expect(def1.selectionSet).not.toBe(def2.selectionSet); - expect(keyMaker1.lookupSelectionSet(def1.selectionSet)).toBe( - keyMaker2.lookupSelectionSet(def2.selectionSet), - ); - ++checkCount; - } - }); - - expect(checkCount).toBe(1); - }); -}); diff --git a/packages/apollo-cache-inmemory/src/queryKeyMaker.ts b/packages/apollo-cache-inmemory/src/queryKeyMaker.ts deleted file mode 100644 index 18c7b59c131..00000000000 --- a/packages/apollo-cache-inmemory/src/queryKeyMaker.ts +++ /dev/null @@ -1,134 +0,0 @@ -import { CacheKeyNode } from './optimism'; -import { - DocumentNode, - SelectionSetNode, - FragmentSpreadNode, - FragmentDefinitionNode, -} from 'graphql'; - -const CIRCULAR = Object.create(null); -const objToStr = Object.prototype.toString; - -export class QueryKeyMaker { - private perQueryKeyMakers = new Map(); - - constructor(private cacheKeyRoot: CacheKeyNode) {} - - public forQuery(document: DocumentNode) { - if (! this.perQueryKeyMakers.has(document)) { - this.perQueryKeyMakers.set( - document, - new PerQueryKeyMaker(this.cacheKeyRoot, document), - ); - } - return this.perQueryKeyMakers.get(document); - } -} - -class PerQueryKeyMaker { - private cache = new Map; - - constructor( - private cacheKeyRoot: CacheKeyNode, - private query: DocumentNode, - ) { - this.lookupArray = this.cacheMethod(this.lookupArray); - this.lookupObject = this.cacheMethod(this.lookupObject); - this.lookupFragmentSpread = this.cacheMethod(this.lookupFragmentSpread); - } - - private cacheMethod(method: (value: V) => R): typeof method { - return (value: V) => { - if (this.cache.has(value)) { - const cached = this.cache.get(value); - if (cached === CIRCULAR) { - throw new Error("QueryKeyMaker cannot handle circular query structures"); - } - return cached; - } - this.cache.set(value, CIRCULAR); - try { - const result = method.call(this, value); - this.cache.set(value, result); - return result; - } catch (e) { - this.cache.delete(value); - throw e; - } - }; - } - - public lookupQuery(document: DocumentNode): object { - return this.lookupObject(document); - } - - public lookupSelectionSet(selectionSet: SelectionSetNode) { - return this.lookupObject(selectionSet); - } - - private lookupFragmentSpread(fragmentSpread: FragmentSpreadNode): object { - const name = fragmentSpread.name.value; - let fragment: FragmentDefinitionNode = null; - - this.query.definitions.some(definition => { - if (definition.kind === "FragmentDefinition" && - definition.name.value === name) { - fragment = definition; - return true; - } - return false; - }); - - // Include the key object computed from the FragmentDefinition named by - // this FragmentSpreadNode. - return this.lookupObject({ - ...fragmentSpread, - fragment, - }); - } - - private lookupAny(value: any): object { - if (Array.isArray(value)) { - return this.lookupArray(value); - } - - if (typeof value === "object" && value !== null) { - if (value.kind === "FragmentSpread") { - return this.lookupFragmentSpread(value); - } - return this.lookupObject(value); - } - - return value; - } - - private lookupArray(array: any[]): object { - const elements = array.map(this.lookupAny, this); - return this.cacheKeyRoot.lookup( - objToStr.call(array), - this.cacheKeyRoot.lookupArray(elements), - ); - } - - private lookupObject(object: { [key: string]: any }): object { - const keys = safeSortedKeys(object); - const values = keys.map(key => this.lookupAny(object[key])); - return this.cacheKeyRoot.lookup( - objToStr.call(object), - this.cacheKeyRoot.lookupArray(keys), - this.cacheKeyRoot.lookupArray(values), - ); - } -} - -function safeSortedKeys(object: { [key: string]: any }): string[] { - const keys = Object.keys(object); - - // Exclude any .loc properties. - const locIndex = keys.indexOf('loc'); - if (locIndex >= 0) { - keys.splice(locIndex, 1); - } - - return keys.sort(); -} diff --git a/packages/apollo-cache-inmemory/src/readFromStore.ts b/packages/apollo-cache-inmemory/src/readFromStore.ts index bdfef3c86fe..e8fa033baca 100644 --- a/packages/apollo-cache-inmemory/src/readFromStore.ts +++ b/packages/apollo-cache-inmemory/src/readFromStore.ts @@ -43,7 +43,6 @@ import { wrap, CacheKeyNode } from './optimism'; export { OptimisticWrapperFunction } from './optimism'; import { DepTrackingCache } from './depTrackingCache'; -import { QueryKeyMaker } from './queryKeyMaker'; export type VariableMap = { [name: string]: any }; @@ -94,8 +93,6 @@ type ExecSelectionSetOptions = { }; export class StoreReader { - private keyMaker: QueryKeyMaker; - constructor( private cacheKeyRoot = new CacheKeyNode, ) { @@ -105,8 +102,6 @@ export class StoreReader { executeSelectionSet, } = reader; - reader.keyMaker = new QueryKeyMaker(cacheKeyRoot); - this.executeStoreQuery = wrap((options: ExecStoreQueryOptions) => { return executeStoreQuery.call(this, options); }, { @@ -122,7 +117,7 @@ export class StoreReader { // the cache when relevant data have changed. if (contextValue.store instanceof DepTrackingCache) { return reader.cacheKeyRoot.lookup( - reader.keyMaker.forQuery(query).lookupQuery(query), + query, contextValue.store, fragmentMatcher, JSON.stringify(variableValues), @@ -143,7 +138,7 @@ export class StoreReader { }: ExecSelectionSetOptions) { if (execContext.contextValue.store instanceof DepTrackingCache) { return reader.cacheKeyRoot.lookup( - reader.keyMaker.forQuery(execContext.query).lookupSelectionSet(selectionSet), + selectionSet, execContext.contextValue.store, execContext.fragmentMatcher, JSON.stringify(execContext.variableValues), From b4f0c8ea6c7564d8762b663903847919a6d37105 Mon Sep 17 00:00:00 2001 From: Ben Newman Date: Tue, 18 Dec 2018 08:48:04 -0500 Subject: [PATCH 09/11] Improve (and shorten) query polling implementation. (#4243) This implementation has the following benefits: - It collapses the QueryScheduler abstraction into the QueryManager (which was always ultimately responsible for managing the lifetime of polling timers), thus simplifying the relationship between the QueryManager and its ObservableQuery objects. - It's about 100 bytes smaller than the previous implementation, after minification and gzip. - It uses setTimeout rather than setInterval, so event loop starvation never leads to a rapid succession of setInterval catch-up calls. - It guarantees at most one timeout will be pending for an arbitrary number of polling queries, rather than a separate timer for every distinct polling interval. - Fewer independent timers means better batching behavior, usually. - Though there may be a delay between the desired polling time for a given query and the actual polling time, the delay is never greater than the minimum polling interval across all queries, which changes dynamically as polling queries are started and stopped. --- .../apollo-client/src/__mocks__/mockLinks.ts | 5 +- .../apollo-client/src/core/ObservableQuery.ts | 39 +--- .../apollo-client/src/core/QueryManager.ts | 138 +++++++++++- .../__tests__/scheduler.ts | 129 +++++------ .../apollo-client/src/scheduler/scheduler.ts | 203 ------------------ packages/apollo-client/tsconfig.test.json | 1 - 6 files changed, 199 insertions(+), 316 deletions(-) rename packages/apollo-client/src/{scheduler => core}/__tests__/scheduler.ts (77%) delete mode 100644 packages/apollo-client/src/scheduler/scheduler.ts diff --git a/packages/apollo-client/src/__mocks__/mockLinks.ts b/packages/apollo-client/src/__mocks__/mockLinks.ts index ac6e917d19a..3dd4dbd49ac 100644 --- a/packages/apollo-client/src/__mocks__/mockLinks.ts +++ b/packages/apollo-client/src/__mocks__/mockLinks.ts @@ -3,6 +3,7 @@ import { ApolloLink, FetchResult, Observable, + GraphQLRequest, // Observer, } from 'apollo-link'; @@ -25,7 +26,7 @@ export function mockObservableLink(): MockSubscriptionLink { } export interface MockedResponse { - request: Operation; + request: GraphQLRequest; result?: FetchResult; error?: Error; delay?: number; @@ -145,7 +146,7 @@ export class MockSubscriptionLink extends ApolloLink { } } -function requestToKey(request: Operation): string { +function requestToKey(request: GraphQLRequest): string { const queryString = request.query && print(request.query); return JSON.stringify({ diff --git a/packages/apollo-client/src/core/ObservableQuery.ts b/packages/apollo-client/src/core/ObservableQuery.ts index ad43ceee3c4..26782b2cbdf 100644 --- a/packages/apollo-client/src/core/ObservableQuery.ts +++ b/packages/apollo-client/src/core/ObservableQuery.ts @@ -2,11 +2,7 @@ import { isEqual, tryFunctionOrLogError, cloneDeep } from 'apollo-utilities'; import { GraphQLError } from 'graphql'; import { NetworkStatus, isNetworkRequestInFlight } from './networkStatus'; import { Observable, Observer, Subscription } from '../util/Observable'; - -import { QueryScheduler } from '../scheduler/scheduler'; - import { ApolloError } from '../errors/ApolloError'; - import { QueryManager } from './QueryManager'; import { ApolloQueryResult, FetchType, OperationVariables } from './types'; import { @@ -68,10 +64,8 @@ export class ObservableQuery< */ public variables: TVariables; - private isCurrentlyPolling: boolean; private shouldSubscribe: boolean; private isTornDown: boolean; - private scheduler: QueryScheduler; private queryManager: QueryManager; private observers: Observer>[]; private subscriptionHandles: Subscription[]; @@ -81,11 +75,11 @@ export class ObservableQuery< private lastError: ApolloError; constructor({ - scheduler, + queryManager, options, shouldSubscribe = true, }: { - scheduler: QueryScheduler; + queryManager: QueryManager; options: WatchQueryOptions; shouldSubscribe?: boolean; }) { @@ -94,18 +88,16 @@ export class ObservableQuery< ); // active state - this.isCurrentlyPolling = false; this.isTornDown = false; // query information this.options = options; this.variables = options.variables || ({} as TVariables); - this.queryId = scheduler.queryManager.generateQueryId(); + this.queryId = queryManager.generateQueryId(); this.shouldSubscribe = shouldSubscribe; // related classes - this.scheduler = scheduler; - this.queryManager = scheduler.queryManager; + this.queryManager = queryManager; // interal data stores this.observers = []; @@ -524,11 +516,8 @@ export class ObservableQuery< } public stopPolling() { - if (this.isCurrentlyPolling) { - this.scheduler.stopPollingQuery(this.queryId); - this.options.pollInterval = undefined; - this.isCurrentlyPolling = false; - } + this.queryManager.stopPollingQuery(this.queryId); + this.options.pollInterval = undefined; } public startPolling(pollInterval: number) { @@ -541,13 +530,8 @@ export class ObservableQuery< ); } - if (this.isCurrentlyPolling) { - this.scheduler.stopPollingQuery(this.queryId); - this.isCurrentlyPolling = false; - } this.options.pollInterval = pollInterval; - this.isCurrentlyPolling = true; - this.scheduler.startPollingQuery(this.options, this.queryId); + this.queryManager.startPollingQuery(this.options, this.queryId); } private onSubscribe(observer: Observer>) { @@ -598,8 +582,7 @@ export class ObservableQuery< ); } - this.isCurrentlyPolling = true; - this.scheduler.startPollingQuery(this.options, this.queryId); + this.queryManager.startPollingQuery(this.options, this.queryId); } const observer: Observer> = { @@ -627,11 +610,7 @@ export class ObservableQuery< private tearDownQuery() { this.isTornDown = true; - - if (this.isCurrentlyPolling) { - this.scheduler.stopPollingQuery(this.queryId); - this.isCurrentlyPolling = false; - } + this.queryManager.stopPollingQuery(this.queryId); // stop all active GraphQL subscriptions this.subscriptionHandles.forEach(sub => sub.unsubscribe()); diff --git a/packages/apollo-client/src/core/QueryManager.ts b/packages/apollo-client/src/core/QueryManager.ts index 35d3f3141c1..18243d05a0a 100644 --- a/packages/apollo-client/src/core/QueryManager.ts +++ b/packages/apollo-client/src/core/QueryManager.ts @@ -13,8 +13,6 @@ import { hasDirectives, } from 'apollo-utilities'; -import { QueryScheduler } from '../scheduler/scheduler'; - import { isApolloError, ApolloError } from '../errors/ApolloError'; import { Observer, Subscription, Observable } from '../util/Observable'; @@ -54,7 +52,6 @@ export interface QueryInfo { } export class QueryManager { - public scheduler: QueryScheduler; public link: ApolloLink; public mutationStore: MutationStore = new MutationStore(); public queryStore: QueryStore = new QueryStore(); @@ -66,6 +63,8 @@ export class QueryManager { private onBroadcast: () => void; + private ssrMode: boolean; + // let's not start at zero to avoid pain with bad checks private idCounter = 1; @@ -104,7 +103,7 @@ export class QueryManager { this.dataStore = store; this.onBroadcast = onBroadcast; this.clientAwareness = clientAwareness; - this.scheduler = new QueryScheduler({ queryManager: this, ssrMode }); + this.ssrMode = ssrMode; } public mutate({ @@ -662,7 +661,7 @@ export class QueryManager { let transformedOptions = { ...options } as WatchQueryOptions; return new ObservableQuery({ - scheduler: this.scheduler, + queryManager: this, options: transformedOptions, shouldSubscribe: shouldSubscribe, }); @@ -1269,4 +1268,133 @@ export class QueryManager { }, }; } + + public checkInFlight(queryId: string) { + const query = this.queryStore.get(queryId); + + return ( + query && + query.networkStatus !== NetworkStatus.ready && + query.networkStatus !== NetworkStatus.error + ); + } + + // Map from client ID to { interval, options }. + public pollingInfoByQueryId = new Map(); + + private nextPoll: { + time: number; + timeout: NodeJS.Timeout; + } | null = null; + + public startPollingQuery( + options: WatchQueryOptions, + queryId: string, + listener?: QueryListener, + ): string { + const { pollInterval } = options; + + if (!pollInterval) { + throw new Error( + 'Attempted to start a polling query without a polling interval.', + ); + } + + // Do not poll in SSR mode + if (!this.ssrMode) { + this.pollingInfoByQueryId.set(queryId, { + interval: pollInterval, + // Avoid polling until at least pollInterval milliseconds from now. + // The -10 is a fudge factor to help with tests that rely on simulated + // timeouts via jest.runTimersToTime. + lastPollTimeMs: Date.now() - 10, + options: { + ...options, + fetchPolicy: 'network-only', + }, + }); + + if (listener) { + this.addQueryListener(queryId, listener); + } + + this.schedulePoll(pollInterval); + } + + return queryId; + } + + public stopPollingQuery(queryId: string) { + // Since the master polling interval dynamically adjusts to the contents of + // this.pollingInfoByQueryId, stopping a query from polling is as easy as + // removing it from the map. + this.pollingInfoByQueryId.delete(queryId); + } + + // Calling this method ensures a poll will happen within the specified time + // limit, canceling any pending polls that would not happen in time. + private schedulePoll(timeLimitMs: number) { + const now = Date.now(); + + if (this.nextPoll) { + if (timeLimitMs < this.nextPoll.time - now) { + // The next poll will happen too far in the future, so cancel it, and + // fall through to scheduling a new timeout. + clearTimeout(this.nextPoll.timeout); + } else { + // The next poll will happen within timeLimitMs, so all is well. + return; + } + } + + this.nextPoll = { + // Estimated time when the timeout will fire. + time: now + timeLimitMs, + + timeout: setTimeout(() => { + this.nextPoll = null; + let nextTimeLimitMs = Infinity; + + this.pollingInfoByQueryId.forEach((info, queryId) => { + // Pick next timeout according to current minimum interval. + if (info.interval < nextTimeLimitMs) { + nextTimeLimitMs = info.interval; + } + + if (!this.checkInFlight(queryId)) { + // If this query was last polled more than interval milliseconds + // ago, poll it now. Note that there may be a small delay between + // the desired polling time and the actual polling time (equal to + // at most the minimum polling interval across all queries), but + // that's the tradeoff to batching polling intervals. + if (Date.now() - info.lastPollTimeMs >= info.interval) { + const updateLastPollTime = () => { + info.lastPollTimeMs = Date.now(); + }; + this.fetchQuery(queryId, info.options, FetchType.poll).then( + // Set info.lastPollTimeMs after the fetch completes, whether + // or not it succeeded. Promise.prototype.finally would be nice + // here, but we don't have a polyfill for that at the moment, + // and this code has historically silenced errors, which is not + // the behavior of .finally(updateLastPollTime). + updateLastPollTime, + updateLastPollTime + ); + } + } + }); + + // If there were no entries in this.pollingInfoByQueryId, then + // nextTimeLimitMs will still be Infinity, so this.schedulePoll will + // not be called, thus ending the master polling interval. + if (isFinite(nextTimeLimitMs)) { + this.schedulePoll(nextTimeLimitMs); + } + }, timeLimitMs), + }; + } } diff --git a/packages/apollo-client/src/scheduler/__tests__/scheduler.ts b/packages/apollo-client/src/core/__tests__/scheduler.ts similarity index 77% rename from packages/apollo-client/src/scheduler/__tests__/scheduler.ts rename to packages/apollo-client/src/core/__tests__/scheduler.ts index 03f5b6de19b..edf7511ef94 100644 --- a/packages/apollo-client/src/scheduler/__tests__/scheduler.ts +++ b/packages/apollo-client/src/core/__tests__/scheduler.ts @@ -2,13 +2,29 @@ import { InMemoryCache } from 'apollo-cache-inmemory'; import gql from 'graphql-tag'; import { stripSymbols } from 'apollo-utilities'; -import { QueryScheduler } from '../scheduler'; -import { QueryManager } from '../../core/QueryManager'; +import { QueryManager } from '../QueryManager'; import { WatchQueryOptions } from '../../core/watchQueryOptions'; import { mockSingleLink } from '../../__mocks__/mockLinks'; import { NetworkStatus } from '../../core/networkStatus'; import { DataStore } from '../../data/store'; +import { ObservableQuery } from '../../core/ObservableQuery'; + +// Used only for unit testing. +function registerPollingQuery( + queryManager: QueryManager, + queryOptions: WatchQueryOptions, +): ObservableQuery { + if (!queryOptions.pollInterval) { + throw new Error( + 'Attempted to register a non-polling query with the scheduler.', + ); + } + return new ObservableQuery({ + queryManager, + options: queryOptions, + }); +} describe('QueryScheduler', () => { it('should throw an error if we try to start polling a non-polling query', () => { @@ -17,10 +33,6 @@ describe('QueryScheduler', () => { store: new DataStore(new InMemoryCache({ addTypename: false })), }); - const scheduler = new QueryScheduler({ - queryManager, - }); - const query = gql` query { author { @@ -33,7 +45,7 @@ describe('QueryScheduler', () => { query, }; expect(() => { - scheduler.startPollingQuery(queryOptions, null as never); + queryManager.startPollingQuery(queryOptions, null as never); }).toThrow(); }); @@ -67,16 +79,13 @@ describe('QueryScheduler', () => { link: link, }); - const scheduler = new QueryScheduler({ - queryManager, - }); let timesFired = 0; - const queryId = scheduler.startPollingQuery(queryOptions, 'fake-id', () => { + const queryId = queryManager.startPollingQuery(queryOptions, 'fake-id', () => { timesFired += 1; }); setTimeout(() => { expect(timesFired).toBeGreaterThanOrEqual(0); - scheduler.stopPollingQuery(queryId); + queryManager.stopPollingQuery(queryId); done(); }, 120); }); @@ -110,17 +119,14 @@ describe('QueryScheduler', () => { store: new DataStore(new InMemoryCache({ addTypename: false })), link: link, }); - const scheduler = new QueryScheduler({ - queryManager, - }); let timesFired = 0; - let queryId = scheduler.startPollingQuery( + let queryId = queryManager.startPollingQuery( queryOptions, 'fake-id', queryStoreValue => { if (queryStoreValue.networkStatus !== NetworkStatus.poll) { timesFired += 1; - scheduler.stopPollingQuery(queryId); + queryManager.stopPollingQuery(queryId); } }, ); @@ -159,11 +165,8 @@ describe('QueryScheduler', () => { link, }); - const scheduler = new QueryScheduler({ - queryManager, - }); let timesFired = 0; - let observableQuery = scheduler.registerPollingQuery(queryOptions); + let observableQuery = registerPollingQuery(queryManager, queryOptions); let subscription = observableQuery.subscribe({ next(result) { timesFired += 1; @@ -206,11 +209,8 @@ describe('QueryScheduler', () => { store: new DataStore(new InMemoryCache({ addTypename: false })), link, }); - const scheduler = new QueryScheduler({ - queryManager, - }); let timesFired = 0; - let observableQuery = scheduler.registerPollingQuery(queryOptions); + let observableQuery = registerPollingQuery(queryManager, queryOptions); let subscription = observableQuery.subscribe({ next(result) { expect(stripSymbols(result.data)).toEqual(data[timesFired]); @@ -255,10 +255,7 @@ describe('QueryScheduler', () => { store: new DataStore(new InMemoryCache({ addTypename: false })), link, }); - const scheduler = new QueryScheduler({ - queryManager, - }); - let observableQuery = scheduler.registerPollingQuery(queryOptions); + let observableQuery = registerPollingQuery(queryManager, queryOptions); const subscription = observableQuery.subscribe({ next() { done.fail( @@ -268,8 +265,9 @@ describe('QueryScheduler', () => { error(errorVal) { expect(errorVal).toBeDefined(); - const queryId = scheduler.intervalQueries[queryOptions.pollInterval][0]; - expect(scheduler.checkInFlight(queryId)).toBe(false); + queryManager.pollingInfoByQueryId.forEach((_: any, queryId: string) => { + expect(queryManager.checkInFlight(queryId)).toBe(false); + }); subscription.unsubscribe(); done(); }, @@ -298,10 +296,7 @@ describe('QueryScheduler', () => { store: new DataStore(new InMemoryCache()), link, }); - const scheduler = new QueryScheduler({ - queryManager, - }); - const observer = scheduler.registerPollingQuery(queryOptions); + const observer = registerPollingQuery(queryManager, queryOptions); const subscription = observer.subscribe({}); setTimeout(() => { subscription.unsubscribe(); @@ -326,24 +321,20 @@ describe('QueryScheduler', () => { request: queryOptions, result: { data }, }); - const queryManager = new QueryManager({ + const queryManager = new QueryManager({ store: new DataStore(new InMemoryCache()), link, }); - const scheduler = new QueryScheduler({ - queryManager, - }); const queryId = 'fake-id'; - scheduler.addQueryOnInterval(queryId, queryOptions); - expect(Object.keys(scheduler.intervalQueries).length).toEqual(1); - expect(Object.keys(scheduler.intervalQueries)[0]).toEqual( - queryOptions.pollInterval.toString(), - ); - const queries = (scheduler.intervalQueries)[ - queryOptions.pollInterval.toString() - ]; - expect(queries.length).toEqual(1); - expect(queries[0]).toEqual(queryId); + queryManager.startPollingQuery(queryOptions, queryId); + + let count = 0; + queryManager.pollingInfoByQueryId.forEach((info: { interval: number }, qid: string) => { + ++count; + expect(info.interval).toEqual(queryOptions.pollInterval); + expect(qid).toEqual(queryId); + }); + expect(count).toEqual(1); }); it('should add multiple queries to an interval correctly', () => { @@ -391,31 +382,26 @@ describe('QueryScheduler', () => { }, ), }); - const scheduler = new QueryScheduler({ - queryManager, - }); - const observable1 = scheduler.registerPollingQuery(queryOptions1); + const observable1 = registerPollingQuery(queryManager, queryOptions1); observable1.subscribe({ next() { //do nothing }, }); - const observable2 = scheduler.registerPollingQuery(queryOptions2); + const observable2 = registerPollingQuery(queryManager, queryOptions2); observable2.subscribe({ next() { //do nothing }, }); - const keys = Object.keys(scheduler.intervalQueries); - expect(keys.length).toEqual(1); - expect(keys[0]).toEqual(String(interval)); - - const queryIds = (scheduler.intervalQueries)[keys[0]]; - expect(queryIds.length).toEqual(2); - expect(scheduler.registeredQueries[queryIds[0]]).toEqual(queryOptions1); - expect(scheduler.registeredQueries[queryIds[1]]).toEqual(queryOptions2); + let count = 0; + queryManager.pollingInfoByQueryId.forEach((info: { interval: number }) => { + expect(info.interval).toEqual(interval); + ++count; + }); + expect(count).toEqual(2); }); it('should remove queries from the interval list correctly', done => { @@ -440,11 +426,8 @@ describe('QueryScheduler', () => { result: { data }, }), }); - const scheduler = new QueryScheduler({ - queryManager, - }); let timesFired = 0; - const observable = scheduler.registerPollingQuery({ + const observable = registerPollingQuery(queryManager, { query, pollInterval: 10, }); @@ -453,7 +436,7 @@ describe('QueryScheduler', () => { timesFired += 1; expect(stripSymbols(result.data)).toEqual(data); subscription.unsubscribe(); - expect(Object.keys(scheduler.registeredQueries).length).toEqual(0); + expect(queryManager.pollingInfoByQueryId.size).toEqual(0); }, }); @@ -496,25 +479,21 @@ describe('QueryScheduler', () => { store: new DataStore(new InMemoryCache({ addTypename: false })), link: link, }); - const scheduler = new QueryScheduler({ - queryManager, - }); let timesFired = 0; - let queryId = scheduler.startPollingQuery(queryOptions, 'fake-id', () => { - scheduler.stopPollingQuery(queryId); + let queryId = queryManager.startPollingQuery(queryOptions, 'fake-id', () => { + queryManager.stopPollingQuery(queryId); }); setTimeout(() => { - let queryId2 = scheduler.startPollingQuery( + let queryId2 = queryManager.startPollingQuery( queryOptions, 'fake-id2', () => { timesFired += 1; }, ); - expect(scheduler.intervalQueries[20].length).toEqual(1); setTimeout(() => { expect(timesFired).toBeGreaterThanOrEqual(1); - scheduler.stopPollingQuery(queryId2); + queryManager.stopPollingQuery(queryId2); done(); }, 80); }, 200); diff --git a/packages/apollo-client/src/scheduler/scheduler.ts b/packages/apollo-client/src/scheduler/scheduler.ts deleted file mode 100644 index 9a320b1dd02..00000000000 --- a/packages/apollo-client/src/scheduler/scheduler.ts +++ /dev/null @@ -1,203 +0,0 @@ -// The QueryScheduler is supposed to be a mechanism that schedules polling queries such that -// they are clustered into the time slots of the QueryBatcher and are batched together. It -// also makes sure that for a given polling query, if one instance of the query is inflight, -// another instance will not be fired until the query returns or times out. We do this because -// another query fires while one is already in flight, the data will stay in the "loading" state -// even after the first query has returned. - -// At the moment, the QueryScheduler implements the one-polling-instance-at-a-time logic and -// adds queries to the QueryBatcher queue. - -import { QueryManager } from '../core/QueryManager'; - -import { FetchType, QueryListener } from '../core/types'; - -import { ObservableQuery } from '../core/ObservableQuery'; - -import { WatchQueryOptions } from '../core/watchQueryOptions'; - -import { NetworkStatus } from '../core/networkStatus'; - -export class QueryScheduler { - // Map going from queryIds to query options that are in flight. - public inFlightQueries: { [queryId: string]: WatchQueryOptions } = {}; - - // Map going from query ids to the query options associated with those queries. Contains all of - // the queries, both in flight and not in flight. - public registeredQueries: { [queryId: string]: WatchQueryOptions } = {}; - - // Map going from polling interval with to the query ids that fire on that interval. - // These query ids are associated with a set of options in the this.registeredQueries. - public intervalQueries: { [interval: number]: string[] } = {}; - - // We use this instance to actually fire queries (i.e. send them to the batching - // mechanism). - public queryManager: QueryManager; - - // Map going from polling interval widths to polling timers. - private pollingTimers: { [interval: number]: any } = {}; - - private ssrMode: boolean = false; - - constructor({ - queryManager, - ssrMode, - }: { - queryManager: QueryManager; - ssrMode?: boolean; - }) { - this.queryManager = queryManager; - this.ssrMode = ssrMode || false; - } - - public checkInFlight(queryId: string) { - const query = this.queryManager.queryStore.get(queryId); - - return ( - query && - query.networkStatus !== NetworkStatus.ready && - query.networkStatus !== NetworkStatus.error - ); - } - - public fetchQuery( - queryId: string, - options: WatchQueryOptions, - fetchType: FetchType, - ) { - return new Promise((resolve, reject) => { - this.queryManager - .fetchQuery(queryId, options, fetchType) - .then(result => { - resolve(result); - }) - .catch(error => { - reject(error); - }); - }); - } - - public startPollingQuery( - options: WatchQueryOptions, - queryId: string, - listener?: QueryListener, - ): string { - if (!options.pollInterval) { - throw new Error( - 'Attempted to start a polling query without a polling interval.', - ); - } - - // Do not poll in SSR mode - if (this.ssrMode) return queryId; - - this.registeredQueries[queryId] = options; - - if (listener) { - this.queryManager.addQueryListener(queryId, listener); - } - this.addQueryOnInterval(queryId, options); - - return queryId; - } - - public stopPollingQuery(queryId: string) { - // Remove the query options from one of the registered queries. - // The polling function will then take care of not firing it anymore. - delete this.registeredQueries[queryId]; - } - - // Fires the all of the queries on a particular interval. Called on a setInterval. - public fetchQueriesOnInterval(interval: number) { - // XXX this "filter" here is nasty, because it does two things at the same time. - // 1. remove queries that have stopped polling - // 2. call fetchQueries for queries that are polling and not in flight. - // TODO: refactor this to make it cleaner - this.intervalQueries[interval] = this.intervalQueries[interval].filter( - queryId => { - // If queryOptions can't be found from registeredQueries or if it has a - // different interval, it means that this queryId is no longer registered - // and should be removed from the list of queries firing on this interval. - // - // We don't remove queries from intervalQueries immediately in - // stopPollingQuery so that we can keep the timer consistent when queries - // are removed and replaced, and to avoid quadratic behavior when stopping - // many queries. - if ( - !( - this.registeredQueries.hasOwnProperty(queryId) && - this.registeredQueries[queryId].pollInterval === interval - ) - ) { - return false; - } - - // Don't fire this instance of the polling query is one of the instances is already in - // flight. - if (this.checkInFlight(queryId)) { - return true; - } - - const queryOptions = this.registeredQueries[queryId]; - const pollingOptions = { ...queryOptions } as WatchQueryOptions; - pollingOptions.fetchPolicy = 'network-only'; - // don't let unhandled rejections happen - this.fetchQuery(queryId, pollingOptions, FetchType.poll).catch( - () => {}, - ); - return true; - }, - ); - - if (this.intervalQueries[interval].length === 0) { - clearInterval(this.pollingTimers[interval]); - delete this.intervalQueries[interval]; - } - } - - // Adds a query on a particular interval to this.intervalQueries and then fires - // that query with all the other queries executing on that interval. Note that the query id - // and query options must have been added to this.registeredQueries before this function is called. - public addQueryOnInterval( - queryId: string, - queryOptions: WatchQueryOptions, - ) { - const interval = queryOptions.pollInterval; - - if (!interval) { - throw new Error( - `A poll interval is required to start polling query with id '${queryId}'.`, - ); - } - - // If there are other queries on this interval, this query will just fire with those - // and we don't need to create a new timer. - if ( - this.intervalQueries.hasOwnProperty(interval.toString()) && - this.intervalQueries[interval].length > 0 - ) { - this.intervalQueries[interval].push(queryId); - } else { - this.intervalQueries[interval] = [queryId]; - // set up the timer for the function that will handle this interval - this.pollingTimers[interval] = setInterval(() => { - this.fetchQueriesOnInterval(interval); - }, interval); - } - } - - // Used only for unit testing. - public registerPollingQuery( - queryOptions: WatchQueryOptions, - ): ObservableQuery { - if (!queryOptions.pollInterval) { - throw new Error( - 'Attempted to register a non-polling query with the scheduler.', - ); - } - return new ObservableQuery({ - scheduler: this, - options: queryOptions, - }); - } -} diff --git a/packages/apollo-client/tsconfig.test.json b/packages/apollo-client/tsconfig.test.json index ebc64752590..1c4af829781 100644 --- a/packages/apollo-client/tsconfig.test.json +++ b/packages/apollo-client/tsconfig.test.json @@ -19,6 +19,5 @@ "src/core/__tests__/fetchPolicies.ts", "src/data/__tests__/queries.ts", "src/errors/__tests__/ApolloError.ts", - "src/scheduler/__tests__/scheduler.ts" ] } From 8c85dc923265d2490f76afb6a24016e3acd8fe90 Mon Sep 17 00:00:00 2001 From: Ben Newman Date: Wed, 2 Jan 2019 14:04:57 -0500 Subject: [PATCH 10/11] Simplify getDirectiveNames and remove flattenSelections helper. Though it's possible that someone might have been using flattenSelections directly, the function signature seems awfully specific to the needs of the former implementation of getDirectiveNames. --- CHANGELOG.md | 20 +++++++++ packages/apollo-utilities/src/directives.ts | 50 +++++---------------- 2 files changed, 31 insertions(+), 39 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b0d80e42549..0a707b6ab47 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,26 @@ `graphql/language/printer`. If you need this functionality, import it directly: `import { print } from "graphql/language/printer"` +### Apollo Cache In-Memory (vNext) + +- The `flattenSelections` helper function is no longer exported from + `apollo-utilities`, since `getDirectiveNames` has been reimplemented + without using `flattenSelections`, and `flattenSelections` has no clear + purpose now. If you need the old functionality, use a visitor: + ```ts + import { visit } from "graphql/language/visitor"; + + function flattenSelections(selection: SelectionNode) { + const selections: SelectionNode[] = []; + visit(selection, { + SelectionSet(ss) { + selections.push(...ss.selections); + } + }); + return selections; + } + ``` + ## Apollo Client (2.4.9) ### Apollo Client (2.4.9) diff --git a/packages/apollo-utilities/src/directives.ts b/packages/apollo-utilities/src/directives.ts index 2e218878413..60235fcdcd3 100644 --- a/packages/apollo-utilities/src/directives.ts +++ b/packages/apollo-utilities/src/directives.ts @@ -2,7 +2,6 @@ // the `skip` and `include` directives within GraphQL. import { FieldNode, - OperationDefinitionNode, SelectionNode, VariableNode, BooleanValueNode, @@ -10,6 +9,8 @@ import { DocumentNode, } from 'graphql'; +import { visit } from 'graphql/language/visitor'; + import { argumentsObjectFromField } from './storeUtils'; export type DirectiveInfo = { @@ -95,45 +96,16 @@ export function shouldInclude( return res; } -export function flattenSelections(selection: SelectionNode): SelectionNode[] { - if ( - !(selection as FieldNode).selectionSet || - !((selection as FieldNode).selectionSet.selections.length > 0) - ) - return [selection]; - - return [selection].concat( - (selection as FieldNode).selectionSet.selections - .map(selectionNode => - [selectionNode].concat(flattenSelections(selectionNode)), - ) - .reduce((selections, selected) => selections.concat(selected), []), - ); -} - export function getDirectiveNames(doc: DocumentNode) { - // operation => [names of directives]; - const directiveNames = doc.definitions - .filter( - (definition: OperationDefinitionNode) => - definition.selectionSet && definition.selectionSet.selections, - ) - // operation => [[Selection]] - .map(x => flattenSelections(x as any)) - // [[Selection]] => [Selection] - .reduce((selections, selected) => selections.concat(selected), []) - // [Selection] => [Selection with Directives] - .filter( - (selection: SelectionNode) => - selection.directives && selection.directives.length > 0, - ) - // [Selection with Directives] => [[Directives]] - .map((selection: SelectionNode) => selection.directives) - // [[Directives]] => [Directives] - .reduce((directives, directive) => directives.concat(directive), []) - // [Directives] => [Name] - .map((directive: DirectiveNode) => directive.name.value); - return directiveNames; + const names: string[] = []; + + visit(doc, { + Directive(node) { + names.push(node.name.value); + }, + }); + + return names; } export function hasDirectives(names: string[], doc: DocumentNode) { From 2815cac54b601f25830c568511340b205ead4f95 Mon Sep 17 00:00:00 2001 From: Ben Newman Date: Thu, 3 Jan 2019 16:52:20 -0500 Subject: [PATCH 11/11] Lower bundle size limits to reflect recent improvements. --- package.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index cb6f9f3d04b..ffdd34a168e 100644 --- a/package.json +++ b/package.json @@ -24,22 +24,22 @@ { "name": "apollo-cache", "path": "./packages/apollo-cache/lib/bundle.min.js", - "maxSize": "1 kB" + "maxSize": "900 B" }, { "name": "apollo-cache-inmemory", "path": "./packages/apollo-cache-inmemory/lib/bundle.min.js", - "maxSize": "7 kB" + "maxSize": "6.2 kB" }, { "name": "apollo-client", "path": "./packages/apollo-client/lib/bundle.min.js", - "maxSize": "10.25 kB" + "maxSize": "9.15 kB" }, { "name": "apollo-utilities", "path": "./packages/apollo-utilities/lib/bundle.min.js", - "maxSize": "5 kB" + "maxSize": "4.3 kB" } ], "jest": {