@sitecore-jss/sitecore-jss
Version:
This module is provided as a part of Sitecore JavaScript Rendering SDK. It contains the core JSS APIs (layout service) and utilities.
145 lines (144 loc) • 7.62 kB
JavaScript
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
import { GraphQLClient as Client } from 'graphql-request';
import parse from 'url-parse';
import debuggers from './debug';
import TimeoutPromise from './utils/timeout-promise';
/**
* Represents a default retry strategy for handling retry attempts in case of specific HTTP status codes.
* This class implements the RetryStrategy interface and provides methods to determine whether a request
* should be retried and calculates the delay before the next retry attempt.
*/
export class DefaultRetryStrategy {
/**
* @param {object} options Configurable options for retry mechanism.
* @param {number[]} [options.statusCodes] HTTP status codes to trigger retries on. Default is [429].
* @param {string[]} [options.errorCodes] Node error codes to trigger retries. Default is ['ECONNRESET', 'ETIMEDOUT', 'EPROTO'].
* @param {number} [options.factor] Factor by which the delay increases with each retry attempt. Default is 2.
*/
constructor(options = {}) {
this.statusCodes = options.statusCodes || [429];
this.errorCodes = options.errorCodes || ['ECONNRESET', 'ETIMEDOUT', 'EPROTO'];
this.factor = options.factor || 2;
}
shouldRetry(error, attempt, retries) {
var _a;
const isStatusCodeError = ((_a = error.response) === null || _a === void 0 ? void 0 : _a.status) !== undefined && this.statusCodes.includes(error.response.status);
const isNodeErrorCode = error.code !== undefined && this.errorCodes.includes(error.code);
return retries > 0 && attempt <= retries && (isStatusCodeError || isNodeErrorCode);
}
getDelay(error, attempt) {
var _a;
const rawHeaders = (_a = error.response) === null || _a === void 0 ? void 0 : _a.headers;
const retryAfterHeader = rawHeaders === null || rawHeaders === void 0 ? void 0 : rawHeaders.get('Retry-After');
if (retryAfterHeader !== null &&
retryAfterHeader !== undefined &&
retryAfterHeader.trim() !== '') {
const delaySeconds = Number.parseFloat(retryAfterHeader);
return delaySeconds * 1000;
}
else {
return Math.pow(this.factor, attempt - 1) * 1000;
}
}
}
/**
* A GraphQL client for Sitecore APIs that uses the 'graphql-request' library.
* https://github.com/prisma-labs/graphql-request
*/
export class GraphQLRequestClient {
/**
* Provides ability to execute graphql query using given `endpoint`
* @param {string} endpoint The Graphql endpoint
* @param {GraphQLRequestClientConfig} [clientConfig] GraphQL request client configuration.
*/
constructor(endpoint, clientConfig = {}) {
var _a;
this.endpoint = endpoint;
this.headers = {};
if (clientConfig.apiKey) {
this.headers.sc_apikey = clientConfig.apiKey;
}
if (clientConfig.headers) {
this.headers = Object.assign(Object.assign({}, this.headers), clientConfig.headers);
}
if (!endpoint || !parse(endpoint).hostname) {
throw new Error(`Invalid GraphQL endpoint '${endpoint}'. Verify that 'layoutServiceHost' property in 'scjssconfig.json' file or appropriate environment variable is set`);
}
this.timeout = clientConfig.timeout;
this.retries = (_a = clientConfig.retries) !== null && _a !== void 0 ? _a : 3;
this.retryStrategy =
clientConfig.retryStrategy ||
new DefaultRetryStrategy({ statusCodes: [429, 502, 503, 504, 520, 521, 522, 523, 524] });
this.client = new Client(endpoint, {
headers: this.headers,
fetch: clientConfig.fetch,
});
this.debug = clientConfig.debugger || debuggers.http;
}
/**
* Factory method for creating a GraphQLRequestClientFactory.
* @param {object} config - client configuration options.
* @param {string} config.endpoint - endpoint
* @param {string} [config.apiKey] - apikey
*/
static createClientFactory({ endpoint, apiKey, }) {
return (config = {}) => new GraphQLRequestClient(endpoint, Object.assign(Object.assign({}, config), { apiKey }));
}
/**
* Execute graphql request
* @param {string | DocumentNode} query graphql query
* @param {object} [variables] graphql variables
* @param {RequestOptions} [options] Options for configuring a GraphQL request.
*/
request(query, variables, options) {
return __awaiter(this, void 0, void 0, function* () {
let attempt = 1;
const retryer = () => __awaiter(this, void 0, void 0, function* () {
// Note we don't have access to raw request/response with graphql-request
// but we should log whatever we have.
this.debug('request: %o', {
url: this.endpoint,
headers: Object.assign(Object.assign({}, this.headers), options === null || options === void 0 ? void 0 : options.headers),
query,
variables,
});
const startTimestamp = Date.now();
const fetchWithOptionalTimeout = [this.client.request(query, variables, options === null || options === void 0 ? void 0 : options.headers)];
if (this.timeout) {
this.abortTimeout = new TimeoutPromise(this.timeout);
fetchWithOptionalTimeout.push(this.abortTimeout.start);
}
return Promise.race(fetchWithOptionalTimeout).then((data) => {
var _a;
(_a = this.abortTimeout) === null || _a === void 0 ? void 0 : _a.clear();
this.debug('response in %dms: %o', Date.now() - startTimestamp, data);
return Promise.resolve(data);
}, (error) => __awaiter(this, void 0, void 0, function* () {
var _a, _b;
(_a = this.abortTimeout) === null || _a === void 0 ? void 0 : _a.clear();
this.debug('response error: %o', error.response || error.message || error);
const status = ((_b = error.response) === null || _b === void 0 ? void 0 : _b.status) || error.code;
const shouldRetry = this.retryStrategy.shouldRetry(error, attempt, this.retries);
if (shouldRetry) {
const delayMs = this.retryStrategy.getDelay(error, attempt);
this.debug('Error: %s. Retrying in %dms (attempt %d).', status, delayMs, attempt);
attempt++;
return new Promise((resolve) => setTimeout(resolve, delayMs)).then(retryer);
}
else {
return Promise.reject(error);
}
}));
});
return retryer();
});
}
}