-
Notifications
You must be signed in to change notification settings - Fork 47.2k
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
Add fetch Instrumentation to Dedupe Fetches #25516
Conversation
let entry: AbortSignal | void = (currentCache.get(createSignal): any); | ||
if (entry === undefined) { | ||
entry = createSignal(); | ||
// $FlowFixMe[incompatible-use] found when upgrading Flow |
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.
Nit: I don't get why a FlowFixMe is necessary here. Is it because the createSignal
call resets the type refinement? Would a const binding above fix it?
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 like it was added during a codemod to getCacheForType
below, and then you copy pasted. Guessing it's easy to fix.
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 is just temporary until I add the lifetime to Flight next, and getCacheForType will be replaced by cache().
// Notably we currently don't consider non-standard (or future) options. | ||
// This might not be safe. TODO: warn for non-standard extensions differing. | ||
// IF YOU CHANGE THIS UPDATE THE simpleCacheKey ABOVE. | ||
return JSON.stringify([ |
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.
Since this isn't actually used as a key in a map we could just make it an explicit comparison of two Request objects.
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.
Intuitively that seems like it would be faster to me. But if you're not confident either way, seems fine to leave as is and benchmark later.
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.
OTOH this is less code
let url: string; | ||
let cacheKey: string; | ||
if (typeof resource === 'string' && !options) { | ||
// Fast path. |
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.
Clever
const request = new Request(resource, options); | ||
if ( | ||
(request.method !== 'GET' && request.method !== 'HEAD') || | ||
// $FlowFixMe: keepalive is real |
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.
😆
export const DefaultCacheDispatcher: CacheDispatcher = { | ||
getCacheSignal(): AbortSignal { | ||
throw new Error('Not implemented.'); | ||
if (!currentCache) { |
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.
Why unroll this? Does this really help?
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.
Just that I don't want to call the Dispatcher and create a dependency on the object wrapper. But this is going away soon anyway as we need a proper lifetime for it.
return originalFetch(resource, options); | ||
} | ||
// Normalize the Request | ||
let url: string; |
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.
Do you need to normalize url if it's a relative path string? In case history.push gets called while the cache is alive.
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.
Only for the fast path. Good catch. Originally I didn't have the fast path.
const value = cacheEntries[i + 1]; | ||
if (key === cacheKey) { | ||
match = value; | ||
// I would've preferred a labelled break but lint says no. |
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.
change the lint config? labeled break is fine IMO
match = originalFetch(resource, options); | ||
cache.set(url, [cacheKey, match]); | ||
} else { | ||
// We use an array as the inner data structure since it's lighter and |
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.
lighter than both {} and Map? I know those use arrays internally for small sizes right?
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.
Didn't benchmark yet but maybe yea.
// eslint-disable-next-line react-internal/no-production-logging | ||
console.warn( | ||
'React was unable to patch the fetch() function in this environment. ' + | ||
'Suspensey APIs might not work correctly as a result.', |
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.
Is this the first time we've used "Suspensey" in public facing messages? I think I dig it
Btw, I did not enable this in www so the tests don't run in |
1c532d5
to
c6f4e52
Compare
…gnore In the final passes, we ignore the newly generated Promises and use the previous ones. This ensures that if those generate errors, that we intentionally ignore those.
c6f4e52
to
3ea86d0
Compare
Summary: This sync includes the following changes: - **[ab075a232](facebook/react@ab075a232 )**: Do not unmount layout effects on initial Offscreen mount ([#25592](facebook/react#25592)) //<Samuel Susla>// - **[765805bf8](facebook/react@765805bf8 )**: Fix type check for null ([#25595](facebook/react#25595)) //<Sebastian Markbåge>// - **[2ac77aab9](facebook/react@2ac77aab9 )**: Clean up vestige of useOpaqueIdentifier ([#25587](facebook/react#25587)) //<Andrew Clark>// - **[bdd3d0807](facebook/react@bdd3d0807 )**: Extract logic for detecting bad fallback to helper //<Andrew Clark>// - **[952dfff3f](facebook/react@952dfff3f )**: Split suspended work loop logic into separate functions //<Andrew Clark>// - **[d2c0ab10d](facebook/react@d2c0ab10d )**: In work loop, add enum of reasons for suspending //<Andrew Clark>// - **[5450dd409](facebook/react@5450dd409 )**: Strict Mode: Reuse memoized result from first pass ([#25583](facebook/react#25583)) //<Andrew Clark>// - **[d2a0176a1](facebook/react@d2a0176a1 )**: Detect and warn if use(promise) is wrapped with try/catch block ([#25543](facebook/react#25543)) //<Andrew Clark>// - **[cf3932be5](facebook/react@cf3932be5 )**: Remove old react-fetch, react-fs and react-pg libraries ([#25577](facebook/react#25577)) //<Sebastian Markbåge>// - **[28a574ea8](facebook/react@28a574ea8 )**: Try assigning fetch to globalThis if global assignment fails ([#25571](facebook/react#25571)) //<Sebastian Markbåge>// - **[09def5990](facebook/react@09def5990 )**: [Float] handle noscript context for Resources ([#25559](facebook/react#25559)) //<Josh Story>// - **[17204056d](facebook/react@17204056d )**: [Float] fix coordination of resource identity and hydration ([#25569](facebook/react#25569)) //<Josh Story>// - **[d925a8d0b](facebook/react@d925a8d0b )**: Flight client error stack ([#25560](facebook/react#25560)) //<Josh Story>// - **[996b00b78](facebook/react@996b00b78 )**: [Tiny] Fixed incorrect import in `react-server-dom-webpack` ([#25554](facebook/react#25554)) //<Leo Lamprecht>// - **[e7c5af45c](facebook/react@e7c5af45c )**: Update cache() and use() to the canary aka next channel ([#25502](facebook/react#25502)) //<Sebastian Markbåge>// - **[fa77f52e7](facebook/react@fa77f52e7 )**: Unify promise switch statements //<Andrew Clark>// - **[7572e4931](facebook/react@7572e4931 )**: Track thenable state in work loop //<Andrew Clark>// - **[7fc3eefd8](facebook/react@7fc3eefd8 )**: Revert yieldy behavior for non-use Suspense (in Flight, too) //<Andrew Clark>// - **[61f9b5e97](facebook/react@61f9b5e97 )**: [Float] support <base> as Resource ([#25546](facebook/react#25546)) //<Josh Story>// - **[1d3fc9c9c](facebook/react@1d3fc9c9c )**: Bug fix when resolving cache ([#25545](facebook/react#25545)) //<Sebastian Markbåge>// - **[cce18e350](facebook/react@cce18e350 )**: [Flight] Use AsyncLocalStorage to extend the scope of the cache to micro tasks ([#25542](facebook/react#25542)) //<Sebastian Markbåge>// - **[caa84c8da](facebook/react@caa84c8da )**: Revert fetch instrumentation to only RSC ([#25540](facebook/react#25540)) //<Sebastian Markbåge>// - **[0c11baa6a](facebook/react@0c11baa6a )**: add warnings for non-resources rendered outside body or head ([#25532](facebook/react#25532)) //<Josh Story>// - **[9236abdb5](facebook/react@9236abdb5 )**: when float is enabled only push title and script as a single unit ([#25536](facebook/react#25536)) //<Josh Story>// - **[dd5c20825](facebook/react@dd5c20825 )**: Revert yieldy behavior for non-use Suspense ([#25537](facebook/react#25537)) //<Andrew Clark>// - **[934177598](facebook/react@934177598 )**: fix transposed escape functions ([#25534](facebook/react#25534)) //<Josh Story>// - **[d1ced9fd5](facebook/react@d1ced9fd5 )**: [Float] support all links as Resources ([#25515](facebook/react#25515)) //<Josh Story>// - **[973b90bdf](facebook/react@973b90bdf )**: [Float] support meta tags as Resources ([#25514](facebook/react#25514)) //<Josh Story>// - **[79c582981](facebook/react@79c582981 )**: Let ReactDOM initialize in RSC ([#25503](facebook/react#25503)) //<Sebastian Markbåge>// - **[1f7a2f577](facebook/react@1f7a2f577 )**: [Float] support title tags as Resources ([#25508](facebook/react#25508)) //<Josh Story>// - **[c63580787](facebook/react@c63580787 )**: Support `use` in `act` testing API ([#25523](facebook/react#25523)) //<Andrew Clark>// - **[65e32e58b](facebook/react@65e32e58b )**: Add fetch Instrumentation to Dedupe Fetches ([#25516](facebook/react#25516)) //<Sebastian Markbåge>// - **[9336e29d9](facebook/react@9336e29d9 )**: [useEvent] Lint for presence of useEvent functions in dependency lists ([#25512](facebook/react#25512)) //<lauren>// - **[3cc792bfb](facebook/react@3cc792bfb )**: [useEvent] Non-stable function identity ([#25473](facebook/react#25473)) //<lauren>// - **[987292815](facebook/react@987292815 )**: Remove feature flag enableStrictEffects ([#25387](facebook/react#25387)) //<Samuel Susla>// - **[8e2bde6f2](facebook/react@8e2bde6f2 )**: Add cache() API ([#25506](facebook/react#25506)) //<Sebastian Markbåge>// - **[9cdf8a99e](facebook/react@9cdf8a99e )**: [Codemod] Update copyright header to Meta ([#25315](facebook/react#25315)) //<Andrew Clark>// - **[e54015e26](facebook/react@e54015e26 )**: Refactor: fill in the flow missing type ([#25496](facebook/react#25496)) //<c0dedance>// - **[3b1fd5767](facebook/react@3b1fd5767 )**: refactor: Flow: typing of Scheduler ([#25485](facebook/react#25485)) //<bubucuo>// - **[14072ce64](facebook/react@14072ce64 )**: Add detach to Offscreen component ([#25265](facebook/react#25265)) //<Samuel Susla>// - **[3bb71dfd4](facebook/react@3bb71dfd4 )**: Rename react-server-dom-webpack entry points to /client and /server ([#25504](facebook/react#25504)) //<Sebastian Markbåge>// - **[71f2c8cf1](facebook/react@71f2c8cf1 )**: move resource acquisition to mutation phase ([#25500](facebook/react#25500)) //<Josh Story>// - **[500bea532](facebook/react@500bea532 )**: Add option to load Fizz runtime from external file ([#25499](facebook/react#25499)) //<Andrew Clark>// - **[4494f2a86](facebook/react@4494f2a86 )**: [Float] add support for scripts and other enhancements ([#25480](facebook/react#25480)) //<Josh Story>// - **[9ecf84ed7](facebook/react@9ecf84ed7 )**: Bugfix: Suspending in shell during discrete update ([#25495](facebook/react#25495)) //<Andrew Clark>// Changelog: [General][Changed] - React Native sync for revisions 54f297a...ab075a2 jest_e2e[run_all_tests] Reviewed By: kassens Differential Revision: D40897093 fbshipit-source-id: 6a040315834dea5c0ab994ea94d91f5605b9d6b0
* Add fetch instrumentation in cached contexts * Avoid unhandled rejection errors for Promises that we intentionally ignore In the final passes, we ignore the newly generated Promises and use the previous ones. This ensures that if those generate errors, that we intentionally ignore those. * Add extra fetch properties if there were any
Have you considered finding another way but money patching (yeah, you called it instrumentation, but let's be frank — it's monkey patching)? I assume everyone there knows its pitfalls, but let me remind — https://en.wikipedia.org/wiki/Monkey_patch#Pitfalls. |
Summary: This sync includes the following changes: - **[ab075a232](facebook/react@ab075a232 )**: Do not unmount layout effects on initial Offscreen mount ([facebook#25592](facebook/react#25592)) //<Samuel Susla>// - **[765805bf8](facebook/react@765805bf8 )**: Fix type check for null ([facebook#25595](facebook/react#25595)) //<Sebastian Markbåge>// - **[2ac77aab9](facebook/react@2ac77aab9 )**: Clean up vestige of useOpaqueIdentifier ([facebook#25587](facebook/react#25587)) //<Andrew Clark>// - **[bdd3d0807](facebook/react@bdd3d0807 )**: Extract logic for detecting bad fallback to helper //<Andrew Clark>// - **[952dfff3f](facebook/react@952dfff3f )**: Split suspended work loop logic into separate functions //<Andrew Clark>// - **[d2c0ab10d](facebook/react@d2c0ab10d )**: In work loop, add enum of reasons for suspending //<Andrew Clark>// - **[5450dd409](facebook/react@5450dd409 )**: Strict Mode: Reuse memoized result from first pass ([facebook#25583](facebook/react#25583)) //<Andrew Clark>// - **[d2a0176a1](facebook/react@d2a0176a1 )**: Detect and warn if use(promise) is wrapped with try/catch block ([facebook#25543](facebook/react#25543)) //<Andrew Clark>// - **[cf3932be5](facebook/react@cf3932be5 )**: Remove old react-fetch, react-fs and react-pg libraries ([facebook#25577](facebook/react#25577)) //<Sebastian Markbåge>// - **[28a574ea8](facebook/react@28a574ea8 )**: Try assigning fetch to globalThis if global assignment fails ([facebook#25571](facebook/react#25571)) //<Sebastian Markbåge>// - **[09def5990](facebook/react@09def5990 )**: [Float] handle noscript context for Resources ([facebook#25559](facebook/react#25559)) //<Josh Story>// - **[17204056d](facebook/react@17204056d )**: [Float] fix coordination of resource identity and hydration ([facebook#25569](facebook/react#25569)) //<Josh Story>// - **[d925a8d0b](facebook/react@d925a8d0b )**: Flight client error stack ([facebook#25560](facebook/react#25560)) //<Josh Story>// - **[996b00b78](facebook/react@996b00b78 )**: [Tiny] Fixed incorrect import in `react-server-dom-webpack` ([facebook#25554](facebook/react#25554)) //<Leo Lamprecht>// - **[e7c5af45c](facebook/react@e7c5af45c )**: Update cache() and use() to the canary aka next channel ([facebook#25502](facebook/react#25502)) //<Sebastian Markbåge>// - **[fa77f52e7](facebook/react@fa77f52e7 )**: Unify promise switch statements //<Andrew Clark>// - **[7572e4931](facebook/react@7572e4931 )**: Track thenable state in work loop //<Andrew Clark>// - **[7fc3eefd8](facebook/react@7fc3eefd8 )**: Revert yieldy behavior for non-use Suspense (in Flight, too) //<Andrew Clark>// - **[61f9b5e97](facebook/react@61f9b5e97 )**: [Float] support <base> as Resource ([facebook#25546](facebook/react#25546)) //<Josh Story>// - **[1d3fc9c9c](facebook/react@1d3fc9c9c )**: Bug fix when resolving cache ([facebook#25545](facebook/react#25545)) //<Sebastian Markbåge>// - **[cce18e350](facebook/react@cce18e350 )**: [Flight] Use AsyncLocalStorage to extend the scope of the cache to micro tasks ([facebook#25542](facebook/react#25542)) //<Sebastian Markbåge>// - **[caa84c8da](facebook/react@caa84c8da )**: Revert fetch instrumentation to only RSC ([facebook#25540](facebook/react#25540)) //<Sebastian Markbåge>// - **[0c11baa6a](facebook/react@0c11baa6a )**: add warnings for non-resources rendered outside body or head ([facebook#25532](facebook/react#25532)) //<Josh Story>// - **[9236abdb5](facebook/react@9236abdb5 )**: when float is enabled only push title and script as a single unit ([facebook#25536](facebook/react#25536)) //<Josh Story>// - **[dd5c20825](facebook/react@dd5c20825 )**: Revert yieldy behavior for non-use Suspense ([facebook#25537](facebook/react#25537)) //<Andrew Clark>// - **[934177598](facebook/react@934177598 )**: fix transposed escape functions ([facebook#25534](facebook/react#25534)) //<Josh Story>// - **[d1ced9fd5](facebook/react@d1ced9fd5 )**: [Float] support all links as Resources ([facebook#25515](facebook/react#25515)) //<Josh Story>// - **[973b90bdf](facebook/react@973b90bdf )**: [Float] support meta tags as Resources ([facebook#25514](facebook/react#25514)) //<Josh Story>// - **[79c582981](facebook/react@79c582981 )**: Let ReactDOM initialize in RSC ([facebook#25503](facebook/react#25503)) //<Sebastian Markbåge>// - **[1f7a2f577](facebook/react@1f7a2f577 )**: [Float] support title tags as Resources ([facebook#25508](facebook/react#25508)) //<Josh Story>// - **[c63580787](facebook/react@c63580787 )**: Support `use` in `act` testing API ([facebook#25523](facebook/react#25523)) //<Andrew Clark>// - **[65e32e58b](facebook/react@65e32e58b )**: Add fetch Instrumentation to Dedupe Fetches ([facebook#25516](facebook/react#25516)) //<Sebastian Markbåge>// - **[9336e29d9](facebook/react@9336e29d9 )**: [useEvent] Lint for presence of useEvent functions in dependency lists ([facebook#25512](facebook/react#25512)) //<lauren>// - **[3cc792bfb](facebook/react@3cc792bfb )**: [useEvent] Non-stable function identity ([facebook#25473](facebook/react#25473)) //<lauren>// - **[987292815](facebook/react@987292815 )**: Remove feature flag enableStrictEffects ([facebook#25387](facebook/react#25387)) //<Samuel Susla>// - **[8e2bde6f2](facebook/react@8e2bde6f2 )**: Add cache() API ([facebook#25506](facebook/react#25506)) //<Sebastian Markbåge>// - **[9cdf8a99e](facebook/react@9cdf8a99e )**: [Codemod] Update copyright header to Meta ([facebook#25315](facebook/react#25315)) //<Andrew Clark>// - **[e54015e26](facebook/react@e54015e26 )**: Refactor: fill in the flow missing type ([facebook#25496](facebook/react#25496)) //<c0dedance>// - **[3b1fd5767](facebook/react@3b1fd5767 )**: refactor: Flow: typing of Scheduler ([facebook#25485](facebook/react#25485)) //<bubucuo>// - **[14072ce64](facebook/react@14072ce64 )**: Add detach to Offscreen component ([facebook#25265](facebook/react#25265)) //<Samuel Susla>// - **[3bb71dfd4](facebook/react@3bb71dfd4 )**: Rename react-server-dom-webpack entry points to /client and /server ([facebook#25504](facebook/react#25504)) //<Sebastian Markbåge>// - **[71f2c8cf1](facebook/react@71f2c8cf1 )**: move resource acquisition to mutation phase ([facebook#25500](facebook/react#25500)) //<Josh Story>// - **[500bea532](facebook/react@500bea532 )**: Add option to load Fizz runtime from external file ([facebook#25499](facebook/react#25499)) //<Andrew Clark>// - **[4494f2a86](facebook/react@4494f2a86 )**: [Float] add support for scripts and other enhancements ([facebook#25480](facebook/react#25480)) //<Josh Story>// - **[9ecf84ed7](facebook/react@9ecf84ed7 )**: Bugfix: Suspending in shell during discrete update ([facebook#25495](facebook/react#25495)) //<Andrew Clark>// Changelog: [General][Changed] - React Native sync for revisions 54f297a...ab075a2 jest_e2e[run_all_tests] Reviewed By: kassens Differential Revision: D40897093 fbshipit-source-id: 6a040315834dea5c0ab994ea94d91f5605b9d6b0
Dedupe GET and HEAD fetches while a React Cache is active.
Note that this doesn't say when this should happen nor the lifetime of these cache entries. Currently it's just during render and lives for the lifetime of the current Cache, but that will be expanded, in a limited form, in tasks outside of render. This PR isn't concerned about that yet. It's just saying which things will be deduped and that it's React doing it - when requiring
"react"
.Passing an explicit AbortSignal can be used to opt out of this behavior.
This doesn't instrument the Promises for sync resolution. It also yields new Responses every time and reading the body is still a macrotask operation so it's still better to have an outer
cache()
but this gives deduping at the network layer to avoid costly mistakes and to make the simple case simple.Browsers do this kind of deduping anyway. It's really mostly for the
cache: 'no-store'
option and cache-control that we diverge slightly. So it's not a huge divergence in behavior.Notably
POST
requests are not deduped so they have to be wrapped incache()
explicitly. This is unfortunate since those are often used for reads as well. E.g. GraphQL. We might want to expand the implicit caching of some of those as well by assuming thatPOST
in render are idempotent since anything in render needs to be idempotent anyway. We'd have to be more careful about this when we expand to cache in unknown contexts though. Maybe a compromise is to only dedupe these on the server.