-
Notifications
You must be signed in to change notification settings - Fork 2.7k
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
HttpLink
/BatchHttpLink
: Abort AbortController signal more granularly.
#11040
HttpLink
/BatchHttpLink
: Abort AbortController signal more granularly.
#11040
Conversation
🦋 Changeset detectedLatest commit: fcf54d6 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
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 |
size-limit report 📦
|
const { controller: _controller, signal } = createSignalIfSupported(); | ||
controller = _controller; | ||
if (controller) (options as any).signal = signal; | ||
let controller: AbortController | undefined; |
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.
🎉 Love this change. Love seeing any
start to disappear!
src/link/http/__tests__/HttpLink.ts
Outdated
expect(created).toBe(true); | ||
expect(aborted).toBe(true); |
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.
Another reason is that I prefer to inline the setup is that it isolates test state. Because we are asserting on test state that can be written by any test in this describe
block, it is impossible to run these tests in parallel because any given test could update this state and affect another. If we can move this state to be local to the test, we don't have to worry about race conditions or test state that affects each other. These types of test failures are SUPER difficult to debug when they happen.
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.
Jest will never run multiple tests in the same file at the same time (unless you actively specify that they should, using test.concurrent
) - test parallelism in jest is always about running multiple different files in different node
instances at the same time. A file itself will always run top-to-bottom.
There can never be any collision between tests - or otherwise, these tests writing on globalThis
, or any kind of mock, would be incredibly dangerous from the start :)
The only way parallelism affects test flimsyness is CPU usage: if the CPU is maxed out, tests just run slower.
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.
Thats fair. I forget jest parallelism out-of-the-box is per-file, not per test.
My point still remains though that I tend to prefer avoiding shared state between multiple tests because they become impossible to debug if the tests need to run in a certain order to pass.
I like the refactor you did though extracting to a function. You're still able to reduce the boilerplate, but my concern about shared state is gone. Thanks for that!
src/link/http/__tests__/HttpLink.ts
Outdated
sub.unsubscribe(); | ||
|
||
expect(created).toBe(true); | ||
expect(aborted).toBe(true); |
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 test is interesting to me. Based on the implementation and the description in the changeset, I would have expected aborted
here to be false
since "the AbortController
will only be .abort()
ed by outside events" (according to the changeset), yet I see no "outside events" in this test.
Looking through some of the other tests, perhaps a tweak to the name of the test would help? The fact that the request hasn't completed is relevant here.
My suggestion 👇
it("unsubscribing cancels internally created AbortController when request hasn't completed"
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.
sub.unsubscribe();
- tearing down the running link subscription because the result it isn't needed anymore would be that outside event.
But yeah, I can tweak the wording a bit :)
src/link/http/__tests__/HttpLink.ts
Outdated
150, | ||
); | ||
it('passing a signal in means no AbortController is created internally', () => { | ||
const link = createHttpLink({ uri: 'data', fetch: fetch as any, fetchOptions: { signal: {} } }); |
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.
const link = createHttpLink({ uri: 'data', fetch: fetch as any, fetchOptions: { signal: {} } }); | |
const controller = new AbortController(); | |
const link = createHttpLink({ uri: 'data', fetch: fetch as any, fetchOptions: { signal: controller.signal } }); |
I'd prefer if we could test this like the real world to ensure it works as expected.
Yet another reason why I prefer inlining the setup since you'd benefit from not mocking out AbortController
here 🙂
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 problem with using the unmocked AbortController
would be that we cannot test if there is ever a new AbortController()
call inside the link - I think mocking of globals is mostly impossible.
I'll see what I can do here.
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 actually wonder how valuable this test is since its encroaching on the "testing internal implementation details" territory. We should only need to care what the observable end-user effects are, which in this case is that we don't want to abort requests after they've completed. Whether or not we create an abort controller internally or not technically shouldn't matter.
This would solve the "can't mock global" problem since you wouldn't need to check whether something was created internal to the link or not.
Thoughts?
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'll noodle on that for a bit. I feel like it's important to test that the external AbortSignal is not overwritten, so maybe I can adjust the test accordingly.
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.
Any chance you could add some of these tests for batch http link? Since the batch link doesn't use HttpLink
under the hood, this should give us confidence that implementation works as expected (plus it provides documentation that the batch link also works this way)
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.
Sure thing.
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.
Once you're happy with how the tests are written for the HttpLink, I'll recreate them over there :)
remove optional chaining
Okay, that should be it - requesting a re-review :) |
/release:pr |
A new release has been made for this PR. You can install it with |
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'm almost completely satisfied with the tests. See my latest comments. Appreciate the refactor you did with the test setup!
src/link/http/__tests__/HttpLink.ts
Outdated
const mockStatus = { | ||
aborted: false, | ||
created: false | ||
} |
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.
const mockStatus = { | |
aborted: false, | |
created: false | |
} | |
const mockStatus: 'unused' | 'created' | 'aborted' = 'unused'; |
Would something like this be easier to work with than 2 booleans? Its impossible to abort an AbortController
that wasn't created, so this should encapsulate the behavior nicely. Feel free to use undefined
in place of unused
here if you prefer that to mean "never instantiated".
Its just a test though, so I'm not hung up on this, just figured the single value might be a bit nicer to work with than multiple flags.
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've given that another refactor - this is now all gone and we're working with a wrapped version of the original AbortController
.
src/link/http/__tests__/HttpLink.ts
Outdated
}), | ||
150, | ||
); | ||
it('passing a signal in means no AbortController is created internally', () => { |
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 more I think about this, the more I think this test isn't needed. It's testing an implementation detail rather than externally observable behavior. I feel like your next 2 tests test the observable behavior you're trying to fix in this PR and is plenty sufficient.
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.
It's pretty much the "this PR didn't remove intended existing behaviour" test - but I've changed it a bit, so now it should work without touching any implementation details.
Co-authored-by: Jerel Miller <[email protected]>
…ortcontroller-ganularity
Okay, at this point I'm so convinced that you will be okay with the tests that I already copied them over ;) |
function mockFetch() { | ||
const text = jest.fn(async () => '{}'); | ||
const fetch = jest.fn(async (uri, options) => ({ text })); | ||
return { text, fetch } | ||
} |
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 global text
and fetch
mocks in this file are... weird. They do not properly reset in-between tests, and I'm too afraid to touch that, so here's this new helper for these new tests instead, which creates non-global helpers and doesn't influence other types.
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.
💯 🔥 Thanks so much for updating those tests! Love the changes you made there. 🚢 it
Before this change, when
HttpLink
/BatchHttpLink
created anAbortController
internally, that would always be
.abort
ed after the request was finished in anyway.
This could cause issues with Sentry Session Replay and Next.js App Router Cache
invalidations, which just replayed the fetch with the same options - including the
cancelled
AbortSignal
.With this change, the
AbortController
will only be.abort()
ed by outside events,not as a consequence of the request finishing or erroring.
Compare: getsentry/sentry-javascript#8345 and apollographql/apollo-client-nextjs#24
Checklist: