Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 20s
Build & Deploy / 🧪 QA (push) Failing after 34s
Build & Deploy / 🏗️ Build (push) Has started running
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Smoke Test (push) Has been cancelled
Build & Deploy / ⚡ Lighthouse (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
171 lines
6.6 KiB
Plaintext
171 lines
6.6 KiB
Plaintext
/**
|
|
*
|
|
* client
|
|
*
|
|
*/
|
|
import { isObject } from './utils.mjs';
|
|
/** This file is the entry point for browsers, re-export common elements. */
|
|
export * from './common.mjs';
|
|
/**
|
|
* Creates a disposable GraphQL over HTTP client to transmit
|
|
* GraphQL operation results.
|
|
*
|
|
* @category Client
|
|
*/
|
|
export function createClient(options) {
|
|
const { credentials = 'same-origin', referrer, referrerPolicy, shouldRetry = () => false, } = options;
|
|
const fetchFn = (options.fetchFn || fetch);
|
|
const AbortControllerImpl = (options.abortControllerImpl ||
|
|
AbortController);
|
|
// we dont use yet another AbortController here because of
|
|
// node's max EventEmitters listeners being only 10
|
|
const client = (() => {
|
|
let disposed = false;
|
|
const listeners = [];
|
|
return {
|
|
get disposed() {
|
|
return disposed;
|
|
},
|
|
onDispose(cb) {
|
|
if (disposed) {
|
|
// empty the call stack and then call the cb
|
|
setTimeout(() => cb(), 0);
|
|
return () => {
|
|
// noop
|
|
};
|
|
}
|
|
listeners.push(cb);
|
|
return () => {
|
|
listeners.splice(listeners.indexOf(cb), 1);
|
|
};
|
|
},
|
|
dispose() {
|
|
if (disposed)
|
|
return;
|
|
disposed = true;
|
|
// we copy the listeners so that onDispose unlistens dont "pull the rug under our feet"
|
|
for (const listener of [...listeners]) {
|
|
listener();
|
|
}
|
|
},
|
|
};
|
|
})();
|
|
return {
|
|
subscribe(request, sink) {
|
|
if (client.disposed)
|
|
throw new Error('Client has been disposed');
|
|
const control = new AbortControllerImpl();
|
|
const unlisten = client.onDispose(() => {
|
|
unlisten();
|
|
control.abort();
|
|
});
|
|
(async () => {
|
|
var _a;
|
|
let retryingErr = null, retries = 0;
|
|
for (;;) {
|
|
if (retryingErr) {
|
|
const should = await shouldRetry(retryingErr, retries);
|
|
// requst might've been canceled while waiting for retry
|
|
if (control.signal.aborted)
|
|
return;
|
|
if (!should)
|
|
throw retryingErr;
|
|
retries++;
|
|
}
|
|
try {
|
|
const url = typeof options.url === 'function'
|
|
? await options.url(request)
|
|
: options.url;
|
|
if (control.signal.aborted)
|
|
return;
|
|
const headers = typeof options.headers === 'function'
|
|
? await options.headers()
|
|
: (_a = options.headers) !== null && _a !== void 0 ? _a : {};
|
|
if (control.signal.aborted)
|
|
return;
|
|
let res;
|
|
try {
|
|
res = await fetchFn(url, {
|
|
signal: control.signal,
|
|
method: 'POST',
|
|
headers: Object.assign(Object.assign({}, headers), { 'content-type': 'application/json; charset=utf-8', accept: 'application/graphql-response+json, application/json' }),
|
|
credentials,
|
|
referrer,
|
|
referrerPolicy,
|
|
body: JSON.stringify(request),
|
|
});
|
|
}
|
|
catch (err) {
|
|
throw new NetworkError(err);
|
|
}
|
|
if (!res.ok)
|
|
throw new NetworkError(res);
|
|
if (!res.body)
|
|
throw new Error('Missing response body');
|
|
const contentType = res.headers.get('content-type');
|
|
if (!contentType)
|
|
throw new Error('Missing response content-type');
|
|
if (!contentType.includes('application/graphql-response+json') &&
|
|
!contentType.includes('application/json')) {
|
|
throw new Error(`Unsupported response content-type ${contentType}`);
|
|
}
|
|
const result = await res.json();
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
sink.next(result);
|
|
return control.abort();
|
|
}
|
|
catch (err) {
|
|
if (control.signal.aborted)
|
|
return;
|
|
// all non-network errors are worth reporting immediately
|
|
if (!(err instanceof NetworkError))
|
|
throw err;
|
|
// try again
|
|
retryingErr = err;
|
|
}
|
|
}
|
|
})()
|
|
.then(() => sink.complete())
|
|
.catch((err) => sink.error(err));
|
|
return () => control.abort();
|
|
},
|
|
dispose() {
|
|
client.dispose();
|
|
},
|
|
};
|
|
}
|
|
/**
|
|
* A network error caused by the client or an unexpected response from the server.
|
|
*
|
|
* To avoid bundling DOM typings (because the client can run in Node env too),
|
|
* you should supply the `Response` generic depending on your Fetch implementation.
|
|
*
|
|
* @category Client
|
|
*/
|
|
export class NetworkError extends Error {
|
|
constructor(msgOrErrOrResponse) {
|
|
let message, response;
|
|
if (isResponseLike(msgOrErrOrResponse)) {
|
|
response = msgOrErrOrResponse;
|
|
message =
|
|
'Server responded with ' +
|
|
msgOrErrOrResponse.status +
|
|
': ' +
|
|
msgOrErrOrResponse.statusText;
|
|
}
|
|
else if (msgOrErrOrResponse instanceof Error)
|
|
message = msgOrErrOrResponse.message;
|
|
else
|
|
message = String(msgOrErrOrResponse);
|
|
super(message);
|
|
this.name = this.constructor.name;
|
|
this.response = response;
|
|
}
|
|
}
|
|
function isResponseLike(val) {
|
|
return (isObject(val) &&
|
|
typeof val['ok'] === 'boolean' &&
|
|
typeof val['status'] === 'number' &&
|
|
typeof val['statusText'] === 'string');
|
|
}
|