From cae3a5924b068802709d01116a1fd1eccc19355a Mon Sep 17 00:00:00 2001 From: Ben Newman Date: Fri, 18 Jan 2019 18:09:42 -0500 Subject: [PATCH 1/5] Support ApolloClient#stop for safe client disposal. Inspired by https://github.com/apollographql/react-apollo/issues/2738, it should be possible to shut down an ApolloClient instance without making assumptions about its implementation details. This client.stop() method should also be useful for server-side rendering, where you're supposed to create (and throw away) a new ApolloClient instance for each request. I'm not entirely sure this implementation cleans up everything that might need to be cleaned up, but I am sure it's better than nothing. --- packages/apollo-client/src/ApolloClient.ts | 11 +++++++++++ packages/apollo-client/src/core/QueryManager.ts | 11 +++++++++++ packages/apollo-client/src/scheduler/scheduler.ts | 15 +++++++++++++++ 3 files changed, 37 insertions(+) diff --git a/packages/apollo-client/src/ApolloClient.ts b/packages/apollo-client/src/ApolloClient.ts index 3a270aab636..30e37563923 100644 --- a/packages/apollo-client/src/ApolloClient.ts +++ b/packages/apollo-client/src/ApolloClient.ts @@ -219,6 +219,17 @@ export default class ApolloClient implements DataProxy { this.clientAwareness.version = clientAwarenessVersion; } } + + /** + * Call this method to terminate any active client processes, making it safe + * to dispose of this ApolloClient instance. + */ + public stop() { + if (this.queryManager) { + this.queryManager.stop(); + } + } + /** * This watches the cache store of the query according to the options specified and * returns an {@link ObservableQuery}. We can subscribe to this {@link ObservableQuery} and diff --git a/packages/apollo-client/src/core/QueryManager.ts b/packages/apollo-client/src/core/QueryManager.ts index f8b6ddf11c1..e45e93ec382 100644 --- a/packages/apollo-client/src/core/QueryManager.ts +++ b/packages/apollo-client/src/core/QueryManager.ts @@ -107,6 +107,17 @@ export class QueryManager { this.scheduler = new QueryScheduler({ queryManager: this, ssrMode }); } + /** + * Call this method to terminate any active query processes, making it safe + * to dispose of this QueryManager instance. + */ + public stop() { + this.scheduler.stop(); + this.fetchQueryRejectFns.forEach(reject => { + reject(new Error('QueryManager stopped while query was in flight')); + }); + } + public mutate({ mutation, variables, diff --git a/packages/apollo-client/src/scheduler/scheduler.ts b/packages/apollo-client/src/scheduler/scheduler.ts index 9a320b1dd02..2c19f5eb7db 100644 --- a/packages/apollo-client/src/scheduler/scheduler.ts +++ b/packages/apollo-client/src/scheduler/scheduler.ts @@ -50,6 +50,21 @@ export class QueryScheduler { this.ssrMode = ssrMode || false; } + /** + * Call this method to terminate any active scheduler timers, making it safe + * to dispose of this QueryScheduler instance. + */ + public stop() { + Object.keys(this.registeredQueries).forEach(queryId => { + this.stopPollingQuery(queryId); + }); + // After calling this.stopPollingQuery for all registered queries, calling + // fetchQueriesOnInterval will remove the corresponding intervals. + Object.keys(this.intervalQueries).forEach(interval => { + this.fetchQueriesOnInterval(+interval); + }); + } + public checkInFlight(queryId: string) { const query = this.queryManager.queryStore.get(queryId); From a064589431ce5177e287dcc65c3457692b723f0c Mon Sep 17 00:00:00 2001 From: Ben Newman Date: Fri, 18 Jan 2019 18:17:34 -0500 Subject: [PATCH 2/5] Increase apollo-client bundle size limit after adding stop methods. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c2e8026fb76..5b1ee679846 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ { "name": "apollo-client", "path": "./packages/apollo-client/lib/bundle.min.js", - "maxSize": "9.3 kB" + "maxSize": "9.4 kB" }, { "name": "apollo-utilities", From c5006bdf183ea7b6d25de26b1f345fd37468bccb Mon Sep 17 00:00:00 2001 From: Ben Newman Date: Fri, 18 Jan 2019 18:40:06 -0500 Subject: [PATCH 3/5] Exercise ApolloClient#stop and QueryManager#stop in tests. --- .../src/__tests__/ApolloClient.ts | 2 ++ .../src/scheduler/__tests__/scheduler.ts | 18 ++++++++++++++---- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/packages/apollo-client/src/__tests__/ApolloClient.ts b/packages/apollo-client/src/__tests__/ApolloClient.ts index 64bcaee9dcd..167a30b9dd6 100644 --- a/packages/apollo-client/src/__tests__/ApolloClient.ts +++ b/packages/apollo-client/src/__tests__/ApolloClient.ts @@ -2190,6 +2190,8 @@ describe('ApolloClient', () => { expect(queryOptions.fetchPolicy).toEqual( defaultOptions.query!.fetchPolicy, ); + + client.stop(); }); }); diff --git a/packages/apollo-client/src/scheduler/__tests__/scheduler.ts b/packages/apollo-client/src/scheduler/__tests__/scheduler.ts index 03f5b6de19b..c009d8d9ef3 100644 --- a/packages/apollo-client/src/scheduler/__tests__/scheduler.ts +++ b/packages/apollo-client/src/scheduler/__tests__/scheduler.ts @@ -76,7 +76,7 @@ describe('QueryScheduler', () => { }); setTimeout(() => { expect(timesFired).toBeGreaterThanOrEqual(0); - scheduler.stopPollingQuery(queryId); + queryManager.stop(); done(); }, 120); }); @@ -114,7 +114,7 @@ describe('QueryScheduler', () => { queryManager, }); let timesFired = 0; - let queryId = scheduler.startPollingQuery( + const queryId = scheduler.startPollingQuery( queryOptions, 'fake-id', queryStoreValue => { @@ -127,6 +127,7 @@ describe('QueryScheduler', () => { setTimeout(() => { expect(timesFired).toEqual(1); + queryManager.stop(); done(); }, 170); }); @@ -174,6 +175,7 @@ describe('QueryScheduler', () => { setTimeout(() => { expect(timesFired).toEqual(1); + queryManager.stop(); done(); }, 100); }); @@ -229,6 +231,7 @@ describe('QueryScheduler', () => { // timesFired end up greater than 2. expect(timesFired).toEqual(2); subscription.unsubscribe(); + queryManager.stop(); done(); }, 100); }); @@ -261,6 +264,7 @@ describe('QueryScheduler', () => { let observableQuery = scheduler.registerPollingQuery(queryOptions); const subscription = observableQuery.subscribe({ next() { + queryManager.stop(); done.fail( new Error('Observer provided a result despite a network error.'), ); @@ -271,6 +275,7 @@ describe('QueryScheduler', () => { const queryId = scheduler.intervalQueries[queryOptions.pollInterval][0]; expect(scheduler.checkInFlight(queryId)).toBe(false); subscription.unsubscribe(); + queryManager.stop(); done(); }, }); @@ -305,6 +310,7 @@ describe('QueryScheduler', () => { const subscription = observer.subscribe({}); setTimeout(() => { subscription.unsubscribe(); + queryManager.stop(); done(); }, 100); }); @@ -344,6 +350,7 @@ describe('QueryScheduler', () => { ]; expect(queries.length).toEqual(1); expect(queries[0]).toEqual(queryId); + queryManager.stop(); }); it('should add multiple queries to an interval correctly', () => { @@ -416,6 +423,8 @@ describe('QueryScheduler', () => { expect(queryIds.length).toEqual(2); expect(scheduler.registeredQueries[queryIds[0]]).toEqual(queryOptions1); expect(scheduler.registeredQueries[queryIds[1]]).toEqual(queryOptions2); + + queryManager.stop(); }); it('should remove queries from the interval list correctly', done => { @@ -459,6 +468,7 @@ describe('QueryScheduler', () => { setTimeout(() => { expect(timesFired).toEqual(1); + queryManager.stop(); done(); }, 100); }); @@ -504,7 +514,7 @@ describe('QueryScheduler', () => { scheduler.stopPollingQuery(queryId); }); setTimeout(() => { - let queryId2 = scheduler.startPollingQuery( + scheduler.startPollingQuery( queryOptions, 'fake-id2', () => { @@ -514,7 +524,7 @@ describe('QueryScheduler', () => { expect(scheduler.intervalQueries[20].length).toEqual(1); setTimeout(() => { expect(timesFired).toBeGreaterThanOrEqual(1); - scheduler.stopPollingQuery(queryId2); + queryManager.stop(); done(); }, 80); }, 200); From 2396d362cfbfd8e6f114c6e659323d9b7a31a02c Mon Sep 17 00:00:00 2001 From: Ben Newman Date: Fri, 18 Jan 2019 18:42:00 -0500 Subject: [PATCH 4/5] Add ApolloClient#stop to the API docs. --- docs/source/api/apollo-client.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/source/api/apollo-client.md b/docs/source/api/apollo-client.md index caa380c7228..dd93be381ac 100644 --- a/docs/source/api/apollo-client.md +++ b/docs/source/api/apollo-client.md @@ -38,9 +38,10 @@ These options will be merged with options supplied with each request. > **Note:** The React Apollo `` component uses Apollo Client's `watchQuery` functionality, so if you would like to set `defaultOptions` when using ``, be sure to set them under the `defaultOptions.watchQuery` property. -The `ApolloClient` class is the core API for Apollo, and the one you'll need to use no matter which integration you are using: +The `ApolloClient` class is the core API for Apollo, and the one you'll need to use no matter which integration you are using: {% tsapibox ApolloClient.constructor %} +{% tsapibox ApolloClient.stop %} {% tsapibox ApolloClient.watchQuery %} {% tsapibox ApolloClient.query %} {% tsapibox ApolloClient.mutate %} From edc2828497d1b639ce2e489d795d42b51f1a3e69 Mon Sep 17 00:00:00 2001 From: Ben Newman Date: Fri, 18 Jan 2019 18:52:20 -0500 Subject: [PATCH 5/5] Small tweaks to ApolloClient#stop documentation. --- docs/source/api/apollo-client.md | 2 +- packages/apollo-client/src/ApolloClient.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/api/apollo-client.md b/docs/source/api/apollo-client.md index dd93be381ac..c52e1e2265e 100644 --- a/docs/source/api/apollo-client.md +++ b/docs/source/api/apollo-client.md @@ -41,7 +41,6 @@ These options will be merged with options supplied with each request. The `ApolloClient` class is the core API for Apollo, and the one you'll need to use no matter which integration you are using: {% tsapibox ApolloClient.constructor %} -{% tsapibox ApolloClient.stop %} {% tsapibox ApolloClient.watchQuery %} {% tsapibox ApolloClient.query %} {% tsapibox ApolloClient.mutate %} @@ -54,6 +53,7 @@ The `ApolloClient` class is the core API for Apollo, and the one you'll need to {% tsapibox ApolloClient.onResetStore %} {% tsapibox ApolloClient.clearStore %} {% tsapibox ApolloClient.onClearStore %} +{% tsapibox ApolloClient.stop %}

ObservableQuery

diff --git a/packages/apollo-client/src/ApolloClient.ts b/packages/apollo-client/src/ApolloClient.ts index 30e37563923..7021b4c5b70 100644 --- a/packages/apollo-client/src/ApolloClient.ts +++ b/packages/apollo-client/src/ApolloClient.ts @@ -222,7 +222,7 @@ export default class ApolloClient implements DataProxy { /** * Call this method to terminate any active client processes, making it safe - * to dispose of this ApolloClient instance. + * to dispose of this `ApolloClient` instance. */ public stop() { if (this.queryManager) {