Expose cacheSignal() alongside cache() (#33557)
This was really meant to be there from the beginning. A `cache()`:ed entry has a life time. On the server this ends when the render finishes. On the client this ends when the cache of that scope gets refreshed. When a cache is no longer needed, it should be possible to abort any outstanding network requests or other resources. That's what `cacheSignal()` gives you. It returns an `AbortSignal` which aborts when the cache lifetime is done based on the same execution scope as a `cache()`ed function - i.e. `AsyncLocalStorage` on the server or the render scope on the client. ```js import {cacheSignal} from 'react'; async function Component() { await fetch(url, { signal: cacheSignal() }); } ``` For `fetch` in particular, a patch should really just do this automatically for you. But it's useful for other resources like database connections. Another reason it's useful to have a `cacheSignal()` is to ignore any errors that might have triggered from the act of being aborted. This is just a general useful JavaScript pattern if you have access to a signal: ```js async function getData(id, signal) { try { await queryDatabase(id, { signal }); } catch (x) { if (!signal.aborted) { logError(x); // only log if it's a real error and not due to cancellation } return null; } } ``` This just gets you a convenient way to get to it without drilling through so a more idiomatic code in React might look something like. ```js import {cacheSignal} from "react"; async function getData(id) { try { await queryDatabase(id); } catch (x) { if (!cacheSignal()?.aborted) { logError(x); } return null; } } ``` If it's called outside of a React render, we normally treat any cached functions as uncached. They're not an error call. They can still load data. It's just not cached. This is not like an aborted signal because then you couldn't issue any requests. It's also not like an infinite abort signal because it's not actually cached forever. Therefore, `cacheSignal()` returns `null` when called outside of a React render scope. Notably the `signal` option passed to `renderToReadableStream` in both SSR (Fizz) and RSC (Flight Server) is not the same instance that comes out of `cacheSignal()`. If you abort the `signal` passed in, then the `cacheSignal()` is also aborted with the same reason. However, the `cacheSignal()` can also get aborted if the render completes successfully or fatally errors during render - allowing any outstanding work that wasn't used to clean up. In the future we might also expand on this to give different [`TaskSignal`](https://developer.mozilla.org/en-US/docs/Web/API/TaskSignal) to different scopes to pass different render or network priorities. On the client version of `"react"` this exposes a noop (both for Fiber/Fizz) due to `disableClientCache` flag but it's exposed so that you can write shared code.