apollo-link-timeout
Version:
Abort requests that take longer than a specified timeout period
95 lines • 4.83 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
// note, this import is modified when building for ESM via `script/fix_apollo_import.mjs`
const core_1 = require("@apollo/client/core");
const TimeoutError_js_1 = require("./TimeoutError.js");
const DEFAULT_TIMEOUT = 15000;
/**
* Aborts the request if the timeout expires before the response is received.
*/
class TimeoutLink extends core_1.ApolloLink {
/**
* Creates a new TimeoutLink instance.
* Aborts the request if the timeout expires before the response is received.
*
* @param timeout - The timeout in milliseconds for the request. Default is 15000ms (15 seconds).
* @param statusCode - The HTTP status code to return when a timeout occurs. Default is 408 (Request Timeout).
*/
constructor(timeout, statusCode) {
super();
this.timeout = timeout || DEFAULT_TIMEOUT;
this.statusCode = statusCode;
}
request(operation, forward) {
let controller;
let ourController;
// override timeout from query context
const requestTimeout = operation.getContext().timeout || this.timeout;
const operationType = operation.query.definitions.find((def) => def.kind === 'OperationDefinition').operation;
if (requestTimeout <= 0 || operationType === 'subscription') {
return forward(operation); // skip this link if timeout is zero or it's a subscription request
}
// add abort controller and signal object to fetchOptions if they don't already exist
if (typeof AbortController !== 'undefined') {
const context = operation.getContext();
let fetchOptions = context.fetchOptions || {};
ourController = new AbortController();
controller = fetchOptions.controller || ourController;
fetchOptions = Object.assign(Object.assign({}, fetchOptions), { controller, signal: controller.signal });
operation.setContext({ fetchOptions });
}
const chainObservable = forward(operation); // observable for remaining link chain
// create local observable with timeout functionality (unsubscibe from chain observable and
// return an error if the timeout expires before chain observable resolves)
const localObservable = new core_1.Observable((observer) => {
let timer;
// listen to chainObservable for result and pass to localObservable if received before timeout
const subscription = chainObservable.subscribe((result) => {
clearTimeout(timer);
observer.next(result);
observer.complete();
}, (error) => {
clearTimeout(timer);
observer.error(error);
observer.complete();
});
// if timeout expires before observable completes, abort call, unsubscribe, and return error
timer = setTimeout(() => {
if (controller) {
if (controller.signal.aborted) {
// already aborted from somewhere else
return;
}
controller.abort(); // abort fetch operation
// if the AbortController in the operation context is one we created,
// it's now "used up", so we need to remove it to avoid blocking any
// future retry of the operation.
const context = operation.getContext();
const fetchOptions = context.fetchOptions || {};
if (fetchOptions.controller === ourController && fetchOptions.signal === ourController.signal) {
operation.setContext(Object.assign(Object.assign({}, fetchOptions), { controller: undefined, signal: undefined }));
}
}
observer.error(new TimeoutError_js_1.default('Timeout exceeded', requestTimeout, this.statusCode));
subscription.unsubscribe();
}, requestTimeout);
const cancelTimeout = () => {
clearTimeout(timer);
subscription.unsubscribe();
};
const ctxRef = operation.getContext().timeoutRef;
if (ctxRef) {
ctxRef({ unsubscribe: cancelTimeout });
}
// cancel timeout if aborted from somewhere else
controller.signal.addEventListener("abort", () => {
cancelTimeout();
}, { once: true });
// this function is called when a client unsubscribes from localObservable
return cancelTimeout;
});
return localObservable;
}
}
exports.default = TimeoutLink;
//# sourceMappingURL=timeoutLink.js.map