RFC: setContextExchange
Summary
A simple exchange to set the operation context, should accept a function, or async function, which returns the new context. Coming from apollo where this was built-in and really handy, I miss it.
I originally wanted this for use with third party auth libraries (auth0, in my case), but I think there's a ton of general flexibility with it as well.
The authExchange API is really great and well-designed, however, a lot of third party auth services already take care of all of that, and just give you a token which you need to set in the headers. Urql makes it pretty difficult to set an async fetch option (as discussed in plenty of other issues).
Proposed Solution
import { Operation, Exchange } from 'urql';
import { fromPromise, map, mergeMap, pipe } from 'wonka';
type Context = Operation['context'];
type ContextSetter = (operation: Operation) => Context | Promise<Context>;
export const setContextExchange =
(contextSetter: ContextSetter): Exchange =>
({ forward }) =>
(ops$) =>
pipe(
ops$,
mergeMap((operation) =>
pipe(
fromPromise(Promise.resolve(contextSetter(operation))),
map((newContext) => ({ ...operation, context: newContext }))
)
),
forward
);
Pretty straightforward, but unless you're familiar with both wonka and how exchanges work, it is very confusing. Would be nicer to just import it from urql instead :)
We probably don't want to always accept promises and also allow for synchronous calls so this can be used in front of a cache exchange as well 🤔 Other than that, fully agree that we can make this happen 👍
This doesn't do a map of incoming operations but does show you what I mean in terms of supporting promises and non-promise maps ❤️ https://github.com/FormidableLabs/urql/blob/dbcc738279806e29bb712297932b4cfcbcb95c46/packages/storybook-addon/src/exchange.ts#L10
Other than that, I'm happy to accept a mapOperation exchange PR to @urql/core ✌️
@kitten this does already work with promises and non-promises (notice the signature type ContextSetter = (operation: Operation) => Context | Promise<Context>;)
Instead of checking 'then' in result and conditionally using fromPromise, which is potentially (theoretically) dangerous, it uses Promise.resolve to always convert the value to a Promise first.
Is there a downside to just always using fromPromise?
Yea, if you always use fromPromise then it's very likely that you'll at least introduce a micro-tick delay when calling Promise.resolve which means that the result won't ever be available synchronously.
The check 'then' in x is indeed a little tricky, however, thanks to the types we know and can assume that there'll only be two different return types, which imho makes it pretty safe ✌️
Interesting point, hadn't thought of that. Out of curiosity, what sort of side effects could be seen by adding an async function (which introduces a micro-tick delay on every operation) in the exchange pipeline?
For reference, and any future readers, here's an example of how we're using it:
export const UrqlProvider: React.FC = (props) => {
const { getAccessTokenSilently } = useAuth0();
const client = createClient({
url: 'http://localhost:3000/graphql',
exchanges: [
dedupExchange,
cacheExchange,
setContextExchange(async (operation) => {
const origFetchOptions =
typeof operation.context.fetchOptions === 'function'
? operation.context.fetchOptions()
: operation.context.fetchOptions ?? {};
const authToken = await getAccessTokenSilently();
const fetchOptions = {
...origFetchOptions,
headers: { ...origFetchOptions.headers, authorization: authToken },
};
return { ...operation.context, fetchOptions };
}),
fetchExchange,
],
});
return <Provider value={client}>{props.children}</Provider>;
};
I'll work on getting a PR up sometime! Does the naming mapOperation imply you'd like it to be more generic and return the entire operation (rather than just the operation.context object)?
There'll be no side effects of it's right in front of the fetchExchange admittedly, but I'm more thinking of people who'd like to use it in front of the cacheExchange. We currently impose and assume that all cached operations resolve synchronously and a delay would break that assumption