Skip to content

Instantly share code, notes, and snippets.

@richtera
Created June 30, 2019 14:04
Show Gist options
  • Save richtera/ec539df29d129136f38f4365ae8a6895 to your computer and use it in GitHub Desktop.
Save richtera/ec539df29d129136f38f4365ae8a6895 to your computer and use it in GitHub Desktop.
/* eslint-disable @typescript-eslint/no-use-before-define */
import { print } from 'graphql';
import { filter, make, merge, mergeMap, pipe, share, takeUntil } from 'wonka';
import { CombinedError } from 'urql';
import 'isomorphic-fetch';
/** A default exchange for fetching GraphQL requests. */
export const fetchExchange = ({ forward }) => {
const isOperationFetchable = (operation) => {
const { operationName } = operation;
return operationName === 'query' || operationName === 'mutation';
};
return ops$ => {
const sharedOps$ = share(ops$);
const fetchResults$ = pipe(
sharedOps$,
filter(isOperationFetchable),
mergeMap(operation => {
const { key } = operation;
const teardown$ = pipe(
sharedOps$,
filter(op => op.operationName === 'teardown' && op.key === key)
);
return pipe(
createFetchSource(operation),
takeUntil(teardown$)
);
})
);
const forward$ = pipe(
sharedOps$,
filter(op => !isOperationFetchable(op)),
forward
);
return merge([fetchResults$, forward$]);
};
};
const createFetchSource = (operation) => {
if (operation.operationName === 'subscription') {
throw new Error(
'Received a subscription operation in the httpExchange. You are probably trying to create a subscription. Have you added a subscriptionExchange?'
);
}
return make(([next, complete]) => {
const abortController =
typeof AbortController !== 'undefined'
? new AbortController()
: undefined;
const { context } = operation;
const extraOptions =
typeof context.fetchOptions === 'function'
? context.fetchOptions()
: context.fetchOptions || {};
const extraPromise = typeof extraOptions.then === 'function' ? extraOptions : null;
const fetchOptions = {
body: JSON.stringify({
query: print(operation.query),
variables: operation.variables,
}),
...extraOptions,
method: 'POST',
headers: {
'content-type': 'application/json',
...extraOptions.headers,
},
signal:
abortController !== undefined ? abortController.signal : undefined,
extraPromise,
};
executeFetch(operation, fetchOptions).then(result => {
if (result !== undefined) {
if (result !== undefined) {
next(result);
}
}
complete();
});
return () => {
if (abortController !== undefined) {
abortController.abort();
}
};
});
};
const executeFetch = (operation, opts) => {
let response;
const { url } = operation.context;
return (opts.extraPromise || Promise.resolve(null))
.then((extra) => {
let { extraPromise, ...myOpts } = opts;
if (extra) {
const { headers, ...rest } = extra;
const { headers: optsHeaders, extraPromise, ...optsRest } = opts;
myOpts = {
...optsRest,
...rest,
headers: {
...optsHeaders,
...headers,
},
};
}
return fetch(url, myOpts);
})
.then(res => {
response = res;
checkStatus(opts.redirect, response);
return response.json();
})
.then(result => ({
operation,
data: result.data,
error: Array.isArray(result.errors)
? new CombinedError({
graphQLErrors: result.errors,
response,
})
: undefined,
}))
.catch(err => {
if (err.name === 'AbortError') {
return undefined;
}
return {
operation,
data: undefined,
error: new CombinedError({
networkError: err,
response,
}),
};
});
};
const checkStatus = (redirectMode = 'follow', response) => {
const statusRangeEnd = redirectMode === 'manual' ? 400 : 300;
if (response.status < 200 || response.status > statusRangeEnd) {
throw new Error(response.statusText);
}
};
export default fetchExchange;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment