@contentstack/management
Version:
The Content Management API is used to manage the content of your Contentstack account
596 lines (574 loc) • 25.1 kB
JavaScript
import _defineProperty from "@babel/runtime/helpers/defineProperty";
import _asyncToGenerator from "@babel/runtime/helpers/asyncToGenerator";
function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
import _regeneratorRuntime from "@babel/runtime/regenerator";
import Axios from 'axios';
import OAuthHandler from './oauthHandler';
import { validateAndSanitizeConfig } from './Util';
var defaultConfig = {
maxRequests: 5,
retryLimit: 5,
retryDelay: 300,
// Enhanced retry configuration for transient network failures
retryOnError: true,
retryOnNetworkFailure: true,
retryOnDnsFailure: true,
retryOnSocketFailure: true,
retryOnHttpServerError: true,
maxNetworkRetries: 3,
networkRetryDelay: 100,
// Base delay for network retries (ms)
networkBackoffStrategy: 'exponential',
// 'exponential' or 'fixed'
delayMs: null // Delay in milliseconds before making each request
};
/**
* Creates a concurrency queue manager for Axios requests with retry logic and rate limiting.
* @param {Object} options - Configuration options.
* @param {Object} options.axios - Axios instance to manage.
* @param {Object=} options.config - Queue configuration options.
* @param {number=} options.config.maxRequests - Maximum concurrent requests, defaults to 5.
* @param {number=} options.config.retryLimit - Maximum retry attempts for errors, defaults to 5.
* @param {number=} options.config.retryDelay - Delay between retries in milliseconds, defaults to 300.
* @param {boolean=} options.config.retryOnError - Enable retry on error, defaults to true.
* @param {boolean=} options.config.retryOnNetworkFailure - Enable retry on network failures, defaults to true.
* @param {boolean=} options.config.retryOnDnsFailure - Enable retry on DNS failures, defaults to true.
* @param {boolean=} options.config.retryOnSocketFailure - Enable retry on socket failures, defaults to true.
* @param {boolean=} options.config.retryOnHttpServerError - Enable retry on HTTP 5xx errors, defaults to true.
* @param {number=} options.config.maxNetworkRetries - Maximum network retry attempts, defaults to 3.
* @param {number=} options.config.networkRetryDelay - Base delay for network retries in milliseconds, defaults to 100.
* @param {string=} options.config.networkBackoffStrategy - Backoff strategy ('exponential' or 'fixed'), defaults to 'exponential'.
* @param {number=} options.config.delayMs - Delay before each request in milliseconds.
* @param {Function=} options.config.retryCondition - Custom function to determine if error can be retried.
* @param {Function=} options.config.logHandler - Log handler function.
* @param {Function=} options.config.refreshToken - Token refresh function.
* @param {string=} options.config.authtoken - Auth token.
* @param {string=} options.config.authorization - Authorization token.
* @returns {Object} ConcurrencyQueue instance with request/response interceptors attached to Axios.
* @throws {Error} If axios instance is not provided or configuration is invalid.
*/
export function ConcurrencyQueue(_ref) {
var _this = this;
var axios = _ref.axios,
config = _ref.config;
if (!axios) {
throw Error('Axios instance is not present');
}
if (config) {
if (config.maxRequests && config.maxRequests <= 0) {
throw Error('Concurrency Manager Error: minimum concurrent requests is 1');
} else if (config.retryLimit && config.retryLimit <= 0) {
throw Error('Retry Policy Error: minimum retry limit is 1');
} else if (config.retryDelay && config.retryDelay < 300) {
throw Error('Retry Policy Error: minimum retry delay for requests is 300');
}
// Validate network retry configuration
if (config.maxNetworkRetries && config.maxNetworkRetries < 0) {
throw Error('Network Retry Policy Error: maxNetworkRetries cannot be negative');
}
if (config.networkRetryDelay && config.networkRetryDelay < 50) {
throw Error('Network Retry Policy Error: minimum network retry delay is 50ms');
}
}
this.config = Object.assign({}, defaultConfig, config);
this.queue = [];
this.running = [];
this.paused = false;
// Helper function to determine if an error is a transient network failure
var isTransientNetworkError = function isTransientNetworkError(error) {
// DNS resolution failures
if (_this.config.retryOnDnsFailure && error.code === 'EAI_AGAIN') {
return {
type: 'DNS_RESOLUTION',
reason: 'DNS resolution failure (EAI_AGAIN)'
};
}
// Socket and connection errors
if (_this.config.retryOnSocketFailure) {
var socketErrorCodes = ['ECONNRESET', 'ETIMEDOUT', 'ECONNREFUSED', 'ENOTFOUND', 'EHOSTUNREACH'];
if (socketErrorCodes.includes(error.code)) {
return {
type: 'SOCKET_ERROR',
reason: "Socket error: ".concat(error.code)
};
}
}
// Connection timeouts
if (_this.config.retryOnNetworkFailure && error.code === 'ECONNABORTED') {
return {
type: 'TIMEOUT',
reason: 'Connection timeout'
};
}
// HTTP 5xx server errors
if (_this.config.retryOnHttpServerError && error.response && error.response.status >= 500 && error.response.status <= 599) {
return {
type: 'HTTP_SERVER_ERROR',
reason: "HTTP ".concat(error.response.status, " server error")
};
}
return null;
};
// Calculate retry delay with jitter and backoff strategy
var calculateNetworkRetryDelay = function calculateNetworkRetryDelay(attempt) {
var baseDelay = _this.config.networkRetryDelay;
var delay;
if (_this.config.networkBackoffStrategy === 'exponential') {
delay = baseDelay * Math.pow(2, attempt - 1);
} else {
delay = baseDelay; // Fixed delay
}
var jitter = Math.random() * 100;
return delay + jitter;
};
// Log retry attempts
var logRetryAttempt = function logRetryAttempt(errorInfo, attempt, delay) {
var message = "Transient ".concat(errorInfo.type, " detected: ").concat(errorInfo.reason, ". Retry attempt ").concat(attempt, "/").concat(_this.config.maxNetworkRetries, " in ").concat(delay, "ms");
if (_this.config.logHandler) {
_this.config.logHandler('warning', message);
} else {
console.warn("[Contentstack SDK] ".concat(message));
}
};
// Log final failure
var logFinalFailure = function logFinalFailure(errorInfo, maxRetries) {
var message = "Final retry failed for ".concat(errorInfo.type, ": ").concat(errorInfo.reason, ". Exceeded max retries (").concat(maxRetries, ").");
if (_this.config.logHandler) {
_this.config.logHandler('error', message);
} else {
console.error("[Contentstack SDK] ".concat(message));
}
};
// Enhanced retry function for network errors
var _retryNetworkError = function retryNetworkError(error, errorInfo, attempt) {
if (attempt > _this.config.maxNetworkRetries) {
logFinalFailure(errorInfo, _this.config.maxNetworkRetries);
// Final error message
var finalError = new Error("Network request failed after ".concat(_this.config.maxNetworkRetries, " retries: ").concat(errorInfo.reason));
finalError.code = error.code;
finalError.originalError = error;
finalError.retryAttempts = attempt - 1;
return Promise.reject(finalError);
}
var delay = calculateNetworkRetryDelay(attempt);
logRetryAttempt(errorInfo, attempt, delay);
// Initialize retry count if not present
if (!error.config.networkRetryCount) {
error.config.networkRetryCount = 0;
}
error.config.networkRetryCount = attempt;
return new Promise(function (resolve, reject) {
setTimeout(function () {
// Keep the request in running queue to maintain maxRequests constraint
// Set retry flags to ensure proper queue handling
var sanitizedConfig = validateAndSanitizeConfig(updateRequestConfig(error, "Network retry ".concat(attempt), delay));
sanitizedConfig.retryCount = sanitizedConfig.retryCount || 0;
// Use axios directly but ensure the running queue is properly managed
// The request interceptor will handle this retry appropriately
axios(sanitizedConfig).then(function (response) {
// On successful retry, call the original onComplete to properly clean up
if (error.config.onComplete) {
error.config.onComplete();
}
shift(); // Process next queued request
resolve(response);
})["catch"](function (retryError) {
// Check if this is still a transient error and we can retry again
var retryErrorInfo = isTransientNetworkError(retryError);
if (retryErrorInfo) {
_retryNetworkError(retryError, retryErrorInfo, attempt + 1).then(resolve)["catch"](function (finalError) {
// On final failure, clean up the running queue
if (error.config.onComplete) {
error.config.onComplete();
}
shift(); // Process next queued request
reject(finalError);
});
} else {
// On non-retryable error, clean up the running queue
if (error.config.onComplete) {
error.config.onComplete();
}
shift(); // Process next queued request
reject(retryError);
}
});
}, delay);
});
};
// Initial shift will check running request,
// and adds request to running queue if max requests are not running
this.initialShift = /*#__PURE__*/_asyncToGenerator(/*#__PURE__*/_regeneratorRuntime.mark(function _callee() {
return _regeneratorRuntime.wrap(function (_context) {
while (1) switch (_context.prev = _context.next) {
case 0:
if (!(_this.running.length < _this.config.maxRequests && !_this.paused)) {
_context.next = 1;
break;
}
_context.next = 1;
return shift();
case 1:
case "end":
return _context.stop();
}
}, _callee);
}));
// INTERNAL: Shift the queued item to running queue
var shift = /*#__PURE__*/function () {
var _ref3 = _asyncToGenerator(/*#__PURE__*/_regeneratorRuntime.mark(function _callee2() {
var queueItem;
return _regeneratorRuntime.wrap(function (_context2) {
while (1) switch (_context2.prev = _context2.next) {
case 0:
if (!(_this.queue.length && !_this.paused)) {
_context2.next = 2;
break;
}
queueItem = _this.queue.shift(); // Add configurable delay before making the request if specified
if (!(_this.config.delayMs && _this.config.delayMs > 0)) {
_context2.next = 1;
break;
}
_context2.next = 1;
return new Promise(function (resolve) {
return setTimeout(resolve, _this.config.delayMs);
});
case 1:
queueItem.resolve(queueItem.request);
_this.running.push(queueItem);
case 2:
case "end":
return _context2.stop();
}
}, _callee2);
}));
return function shift() {
return _ref3.apply(this, arguments);
};
}();
// Append the request at start of queue
this.unshift = function (requestPromise) {
_this.queue.unshift(requestPromise);
};
this.push = function (requestPromise) {
_this.queue.push(requestPromise);
_this.initialShift()["catch"](console.error);
};
this.clear = function () {
var requests = _this.queue.splice(0, _this.queue.length);
requests.forEach(function (element) {
element.request.source.cancel();
});
};
// Detach the interceptors
this.detach = function () {
axios.interceptors.request.eject(_this.interceptors.request);
axios.interceptors.response.eject(_this.interceptors.response);
_this.interceptors = {
request: null,
response: null
};
};
// Request interceptor to queue the request
var requestHandler = function requestHandler(request) {
var _axios$oauth;
if (typeof request.data === 'function') {
request.formdata = request.data;
request.data = transformFormData(request);
}
if (axios !== null && axios !== void 0 && (_axios$oauth = axios.oauth) !== null && _axios$oauth !== void 0 && _axios$oauth.accessToken) {
var isTokenExpired = axios.oauth.tokenExpiryTime && Date.now() > axios.oauth.tokenExpiryTime;
if (isTokenExpired) {
return refreshAccessToken()["catch"](function (error) {
throw new Error('Failed to refresh access token: ' + error.message);
});
}
}
request.retryCount = (request === null || request === void 0 ? void 0 : request.retryCount) || 0;
setAuthorizationHeaders(request);
if (request.cancelToken === undefined) {
var source = Axios.CancelToken.source();
request.cancelToken = source.token;
request.source = source;
}
if (_this.paused && request.retryCount > 0) {
return new Promise(function (resolve, reject) {
_this.unshift({
request: request,
resolve: resolve,
reject: reject
});
});
} else if (request.retryCount > 0) {
return request;
}
return new Promise(function (resolve, reject) {
request.onComplete = function () {
_this.running.pop({
request: request,
resolve: resolve,
reject: reject
});
};
_this.push({
request: request,
resolve: resolve,
reject: reject
});
});
};
var setAuthorizationHeaders = function setAuthorizationHeaders(request) {
var _axios$oauth2;
if (request.headers.authorization && request.headers.authorization !== undefined) {
if (_this.config.authorization && _this.config.authorization !== undefined) {
request.headers.authorization = _this.config.authorization;
request.authorization = _this.config.authorization;
}
delete request.headers.authtoken;
} else if (request.headers.authtoken && request.headers.authtoken !== undefined && _this.config.authtoken && _this.config.authtoken !== undefined) {
request.headers.authtoken = _this.config.authtoken;
request.authtoken = _this.config.authtoken;
} else if (axios !== null && axios !== void 0 && (_axios$oauth2 = axios.oauth) !== null && _axios$oauth2 !== void 0 && _axios$oauth2.accessToken) {
// If OAuth access token is available in axios instance
request.headers.authorization = "Bearer ".concat(axios.oauth.accessToken);
request.authorization = "Bearer ".concat(axios.oauth.accessToken);
delete request.headers.authtoken;
}
};
// Refresh Access Token
var refreshAccessToken = /*#__PURE__*/function () {
var _ref4 = _asyncToGenerator(/*#__PURE__*/_regeneratorRuntime.mark(function _callee3() {
var _t;
return _regeneratorRuntime.wrap(function (_context3) {
while (1) switch (_context3.prev = _context3.next) {
case 0:
_context3.prev = 0;
_context3.next = 1;
return new OAuthHandler(axios).refreshAccessToken();
case 1:
_this.paused = false; // Resume the request queue once the token is refreshed
// Retry the requests that were pending due to token expiration
_this.running.forEach(function (_ref5) {
var request = _ref5.request,
resolve = _ref5.resolve,
reject = _ref5.reject;
// Retry the request with sanitized configuration to prevent SSRF
var sanitizedConfig = validateAndSanitizeConfig(request);
axios(sanitizedConfig).then(resolve)["catch"](reject);
});
_this.running = []; // Clear the running queue after retrying requests
_context3.next = 3;
break;
case 2:
_context3.prev = 2;
_t = _context3["catch"](0);
_this.paused = false; // stop queueing requests on failure
_this.running.forEach(function (_ref6) {
var reject = _ref6.reject;
return reject(_t);
}); // Reject all queued requests
_this.running = []; // Clear the running queue
case 3:
case "end":
return _context3.stop();
}
}, _callee3, null, [[0, 2]]);
}));
return function refreshAccessToken() {
return _ref4.apply(this, arguments);
};
}();
var _delay = function delay(time) {
var isRefreshToken = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
if (!_this.paused) {
_this.paused = true;
// Check for current running request.
// Wait for running queue to complete.
// Wait and prosed the Queued request.
if (_this.running.length > 0) {
setTimeout(function () {
_delay(time, isRefreshToken);
}, time);
}
return new Promise(function (resolve) {
return setTimeout(function () {
_this.paused = false;
if (isRefreshToken) {
return refreshToken();
} else {
for (var i = 0; i < _this.config.maxRequests; i++) {
_this.initialShift()["catch"](console.error);
}
}
}, time);
});
}
};
var refreshToken = function refreshToken() {
return config.refreshToken().then(function (token) {
if (token.authorization) {
axios.defaults.headers.authorization = token.authorization;
axios.defaults.authorization = token.authorization;
axios.httpClientParams.authorization = token.authorization;
axios.httpClientParams.headers.authorization = token.authorization;
_this.config.authorization = token.authorization;
} else if (token.authtoken) {
axios.defaults.headers.authtoken = token.authtoken;
axios.defaults.authtoken = token.authtoken;
axios.httpClientParams.authtoken = token.authtoken;
axios.httpClientParams.headers.authtoken = token.authtoken;
_this.config.authtoken = token.authtoken;
}
})["catch"](function (error) {
_this.queue.forEach(function (queueItem) {
queueItem.reject({
errorCode: '401',
errorMessage: error instanceof Error ? error.message : error,
code: 'Unauthorized',
message: 'Unable to refresh token',
name: 'Token Error',
config: queueItem.request,
stack: error instanceof Error ? error.stack : null
});
});
_this.queue = [];
_this.running = [];
})["finally"](function () {
_this.queue.forEach(function (queueItem) {
if (_this.config.authorization) {
queueItem.request.headers.authorization = _this.config.authorization;
queueItem.request.authorization = _this.config.authorization;
}
if (_this.config.authtoken) {
queueItem.request.headers.authtoken = _this.config.authtoken;
queueItem.request.authtoken = _this.config.authtoken;
}
});
for (var i = 0; i < _this.config.maxRequests; i++) {
_this.initialShift()["catch"](console.error);
}
});
};
// Response interceptor used for
var responseHandler = function responseHandler(response) {
response.config.onComplete();
shift();
return response;
};
var responseErrorHandler = function responseErrorHandler(error) {
var networkError = error.config.retryCount;
var retryErrorType = null;
// First, check for transient network errors
var networkErrorInfo = isTransientNetworkError(error);
if (networkErrorInfo && _this.config.retryOnNetworkFailure) {
var networkRetryCount = error.config.networkRetryCount || 0;
return _retryNetworkError(error, networkErrorInfo, networkRetryCount + 1);
}
// Original retry logic for non-network errors
if (!_this.config.retryOnError || networkError > _this.config.retryLimit) {
return Promise.reject(responseHandler(error));
}
// Check rate limit remaining header before retrying
var wait = _this.config.retryDelay;
var response = error.response;
if (!response) {
if (error.code === 'ECONNABORTED') {
var timeoutMs = error.config.timeout || _this.config.timeout || 'unknown';
error.response = _objectSpread(_objectSpread({}, error.response), {}, {
status: 408,
statusText: "timeout of ".concat(timeoutMs, "ms exceeded")
});
response = error.response;
} else {
return Promise.reject(responseHandler(error));
}
} else if (response.status === 401 && _this.config.refreshToken) {
retryErrorType = "Error with status: ".concat(response.status);
networkError++;
if (networkError > _this.config.retryLimit) {
return Promise.reject(responseHandler(error));
}
_this.running.shift();
// Cool down the running requests
_delay(wait, response.status === 401);
error.config.retryCount = networkError;
// SSRF Prevention: Validate URL before making request
var sanitizedConfig = validateAndSanitizeConfig(updateRequestConfig(error, retryErrorType, wait));
return axios(sanitizedConfig);
}
if (_this.config.retryCondition && _this.config.retryCondition(error)) {
retryErrorType = error.response ? "Error with status: ".concat(response.status) : "Error Code:".concat(error.code);
networkError++;
return _this.retry(error, retryErrorType, networkError, wait);
}
return Promise.reject(responseHandler(error));
};
this.retry = function (error, retryErrorType, retryCount, waittime) {
var delaytime = waittime;
if (retryCount > _this.config.retryLimit) {
return Promise.reject(responseHandler(error));
}
if (_this.config.retryDelayOptions) {
if (_this.config.retryDelayOptions.customBackoff) {
delaytime = _this.config.retryDelayOptions.customBackoff(retryCount, error);
if (delaytime && delaytime <= 0) {
return Promise.reject(responseHandler(error));
}
} else if (_this.config.retryDelayOptions.base) {
delaytime = _this.config.retryDelayOptions.base * retryCount;
}
} else {
delaytime = _this.config.retryDelay;
}
error.config.retryCount = retryCount;
return new Promise(function (resolve) {
return setTimeout(function () {
// SSRF Prevention: Validate URL before making request
var sanitizedConfig = validateAndSanitizeConfig(updateRequestConfig(error, retryErrorType, delaytime));
return resolve(axios(sanitizedConfig));
}, delaytime);
});
};
this.interceptors = {
request: null,
response: null
};
var updateRequestConfig = function updateRequestConfig(error, retryErrorType, wait) {
var requestConfig = error.config;
var message = "".concat(retryErrorType, " error occurred. Waiting for ").concat(wait, " ms before retrying...");
if (_this.config.logHandler) {
_this.config.logHandler('warning', message);
} else {
console.warn("[Contentstack SDK] ".concat(message));
}
if (axios !== undefined && axios.defaults !== undefined) {
if (axios.defaults.agent === requestConfig.agent) {
delete requestConfig.agent;
}
if (axios.defaults.httpAgent === requestConfig.httpAgent) {
delete requestConfig.httpAgent;
}
if (axios.defaults.httpsAgent === requestConfig.httpsAgent) {
delete requestConfig.httpsAgent;
}
}
requestConfig.data = transformFormData(requestConfig);
requestConfig.transformRequest = [function (data) {
return data;
}];
return requestConfig;
};
var transformFormData = function transformFormData(request) {
if (request.formdata) {
var formdata = request.formdata();
request.headers = _objectSpread(_objectSpread({}, request.headers), formdata.getHeaders());
return formdata;
}
return request.data;
};
// Adds interseptors in axios to queue request
this.interceptors.request = axios.interceptors.request.use(requestHandler);
this.interceptors.response = axios.interceptors.response.use(responseHandler, responseErrorHandler);
}