-
Notifications
You must be signed in to change notification settings - Fork 2.7k
Cancel running ObservableQuery
link on unsubscribe
#12633
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Cancel running ObservableQuery
link on unsubscribe
#12633
Conversation
🦋 Changeset detectedLatest commit: 5b828d8 The changes in this PR will be included in the next version bump. Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
commit: |
size-limit report 📦
|
988eb04
to
7fe6af1
Compare
@@ -489,7 +490,8 @@ describe("ApolloClient", () => { | |||
expect(onRequestUnsubscribe).toHaveBeenCalledTimes(1); | |||
}); | |||
|
|||
it("causes link unsubscription after multiple requests finish", async () => { | |||
// restoring original test from https://github.com/apollographql/apollo-client/blob/326cbe8260b1cc04b339ed2b968e23c08a392cc4/src/core/__tests__/ApolloClient/general.test.ts#L366-L461 | |||
it("causes link unsubscription after reobserve", async () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
restoring original test from
apollo-client/src/core/__tests__/ApolloClient/general.test.ts
Lines 366 to 461 in 326cbe8
it("causes link unsubscription after reobserve", async () => { | |
const expResult = { | |
data: { | |
allPeople: { | |
people: [ | |
{ | |
name: "Luke Skywalker", | |
}, | |
], | |
}, | |
}, | |
}; | |
const request = { | |
query: gql` | |
query people($offset: Int) { | |
allPeople(first: $offset) { | |
people { | |
name | |
} | |
} | |
} | |
`, | |
variables: undefined, | |
}; | |
const mockedResponse = { | |
request, | |
result: expResult, | |
}; | |
const onRequestSubscribe = jest.fn(); | |
const onRequestUnsubscribe = jest.fn(); | |
const mockedSingleLink = new ApolloLink(() => { | |
return new Observable((observer) => { | |
onRequestSubscribe(); | |
// Delay (100ms) must be bigger than sum of reobserve and unsubscribe awaits (5ms each) | |
// to show clearly that the connection was aborted before completing | |
const timer = setTimeout(() => { | |
observer.next(mockedResponse.result); | |
observer.complete(); | |
}, 100); | |
return () => { | |
onRequestUnsubscribe(); | |
clearTimeout(timer); | |
}; | |
}); | |
}); | |
const client = new ApolloClient({ | |
link: mockedSingleLink, | |
cache: new InMemoryCache(), | |
defaultOptions: { | |
watchQuery: { | |
fetchPolicy: "cache-and-network", | |
returnPartialData: false, | |
notifyOnNetworkStatusChange: true, | |
}, | |
query: { | |
fetchPolicy: "network-only", | |
}, | |
}, | |
queryDeduplication: false, | |
}); | |
const observableQuery = client.watchQuery< | |
(typeof expResult)["data"], | |
{ offset?: number | undefined } | |
>({ | |
query: request.query, | |
variables: request.variables, | |
}); | |
const stream = new ObservableStream(observableQuery); | |
expect(onRequestSubscribe).toHaveBeenCalledTimes(1); | |
// This is the most important part of this test | |
// Check that reobserve cancels the previous connection while watchQuery remains active | |
void observableQuery.reobserve({ variables: { offset: 20 } }); | |
await waitFor(() => { | |
// Verify that previous connection was aborted by reobserve | |
expect(onRequestUnsubscribe).toHaveBeenCalledTimes(1); | |
}); | |
stream.unsubscribe(); | |
await wait(10); | |
expect(onRequestSubscribe).toHaveBeenCalledTimes(2); | |
expect(onRequestUnsubscribe).toHaveBeenCalledTimes(2); | |
}); |
5dcdb67
to
bda126f
Compare
@@ -413,7 +414,7 @@ describe("ApolloClient", () => { | |||
expect(stream.unsubscribe).not.toThrow(); | |||
}); | |||
|
|||
it("unsubscribes from link after initial reobserve when unsubscribing immediately", async () => { | |||
it("causes link unsubscription if unsubscribed", async () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
16b2610
to
44b8378
Compare
@@ -80,7 +81,6 @@ interface TrackedOperation { | |||
variables: OperationVariables; | |||
} | |||
|
|||
const newNetworkStatusSymbol: any = Symbol(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The changes around newNetworkStatusSymbol
actually wouldn't be necessary after all, since I didn't have to add a second internal option.
On the other hand, I believe it's an improvement to type safety and readability, so I'd suggest to keep it in anyways.
@@ -1708,11 +1743,11 @@ Did you mean to call refetch(variables) instead of refetch({ variables })?`, | |||
// this.options.fetchPolicy is "cache-and-network" or "network-only". When | |||
// this.options.fetchPolicy is any other policy ("cache-first", "cache-only", | |||
// "standby", or "no-cache"), we call this.reobserve() as usual. | |||
private reobserveCacheFirst() { | |||
private reobserveCacheFirst(): void { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The return value was never used, so I changed this to void
ad068f6
to
47d6e84
Compare
ObservableQuery
link on unsubscribeObservableQuery
link on unsubscribe
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks really good! If you're down with changing useLazyQuery
to default to the non-retain
version, I'll approve once those changes are made.
src/react/hooks/useLazyQuery.ts
Outdated
// ensuring the key exists in options | ||
variables: executeOptions?.variables, | ||
}) | ||
.retain(/* create a persistent subscription on the query */); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I still think it makes sense to remove this by default and give control over to the user. We didn't have this type of control/mechanism before and even using AbortController
in 3.x was problematic since changes to context
would mean calling reobserve
more often than it should have (i.e. #11835).
Now that we have a really elegant solution here, I think having consistency throughout the client is useful and its a single line of code to restore this behavior in app code. Doing so also means someone won't have to resort to AbortController
to achieve this behavior which is going to be a lot more code.
One more pro is that retain
makes it more obvious that the promise will be kept around rather than being buried as an implementation detail.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Co-authored-by: Jerel Miller <[email protected]>
Co-authored-by: Jerel Miller <[email protected]>
…xpose it instead
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The code generally looks good but I think we'll need to dig a bit deeper to change the value resolves/rejected from the promise when the in-flight query is aborted, otherwise it just resolves with data: undefined
and that might be surprising.
The useLazyQuery
tests fail for this reason so we'll need to revisit those. I can confirm though calling retain
in those tests does work as expected so thats great news!
…not retained and not required anymore
@jerelmiller what do you think about |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks great! Thank you!
Fixes #12554