UNPKG

apollo-link-timeout

Version:

Abort requests that take longer than a specified timeout period

95 lines 4.83 kB
"use strict"; 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