channelape-sdk
Version:
A client for interacting with ChannelApe's API
295 lines • 13.7 kB
JavaScript
"use strict";
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());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
const axios_1 = require("axios");
const channelape_logger_1 = require("channelape-logger");
const ChannelApeError_1 = require("./model/ChannelApeError");
const HttpRequestMethod_1 = require("./model/HttpRequestMethod");
const RequestLogger_1 = require("./utils/RequestLogger");
const GENERIC_ERROR_CODE = -1;
class RequestClientWrapper {
constructor(requestClientWrapperConfiguration) {
this.requestClientWrapperConfiguration = requestClientWrapperConfiguration;
this.logger = new channelape_logger_1.Logger('RequestClientWrapper', channelape_logger_1.LogLevel.VERBOSE);
this.requestQueue = [];
this.pendingRequests = 0;
this.requestLogger = new RequestLogger_1.default(this.requestClientWrapperConfiguration.logLevel, this.requestClientWrapperConfiguration.endpoint);
this.maximumConcurrentConnections = requestClientWrapperConfiguration.maximumConcurrentConnections;
this.isJwtToken = this.isItJwtToken(this.requestClientWrapperConfiguration.session);
}
get(url, params, callback) {
this.handleRequest({
url,
params,
callback,
method: HttpRequestMethod_1.default.GET
});
}
put(url, params, callback) {
this.handleRequest({
url,
params,
callback,
method: HttpRequestMethod_1.default.PUT
});
}
patch(url, params, callback) {
this.handleRequest({
url,
params,
callback,
method: HttpRequestMethod_1.default.PATCH
});
}
post(url, params, callback) {
this.handleRequest({
url,
params,
callback,
method: HttpRequestMethod_1.default.POST
});
}
handleRequest(requestConfig) {
if (this.pendingRequests < this.maximumConcurrentConnections) {
this.prepareRequest(requestConfig);
}
else {
this.requestQueue.unshift(requestConfig);
}
}
delete(url, params, callback) {
this.handleRequest({
url,
params,
callback,
method: HttpRequestMethod_1.default.DELETE
});
}
prepareRequest(requestConfig) {
return __awaiter(this, void 0, void 0, function* () {
this.pendingRequests = this.pendingRequests + 1;
yield this.makeRequest(requestConfig.method, new Date(), 0, requestConfig.url, requestConfig.params, requestConfig.callback);
});
}
makeRequest(method, callStart, numberOfCalls, url, options, callback) {
return __awaiter(this, void 0, void 0, function* () {
const callDetails = { options, callStart, callCountForThisRequest: numberOfCalls };
options.baseURL = this.requestClientWrapperConfiguration.endpoint;
if (options.headers === undefined) {
options.headers = {};
}
if (this.isJwtToken) {
const accessToken = this.requestClientWrapperConfiguration.session;
options.headers.Authorization = `Bearer ${accessToken}`;
}
else {
this.logger.warn('Using deprecated method of authentication. Please update to use JWT.');
options.headers['X-Channel-Ape-Authorization-Token'] = this.requestClientWrapperConfiguration.session;
}
options.headers['Content-Type'] = 'application/json';
options.timeout = this.requestClientWrapperConfiguration.timeout;
options.method = method;
try {
this.requestLogger.logCall(method, url, options);
let requestPromise;
switch (method) {
case HttpRequestMethod_1.default.GET:
requestPromise = axios_1.default.get(url, options);
break;
case HttpRequestMethod_1.default.PUT:
const putData = options.data === undefined ? '' : options.data;
requestPromise = axios_1.default.put(url, putData, options);
break;
case HttpRequestMethod_1.default.POST:
const postData = options.data === undefined ? '' : options.data;
requestPromise = axios_1.default.post(url, postData, options);
break;
case HttpRequestMethod_1.default.PATCH:
const patchData = options.data === undefined ? '' : options.data;
requestPromise = axios_1.default.patch(url, patchData, options);
break;
case HttpRequestMethod_1.default.DELETE:
requestPromise = axios_1.default.delete(url, options);
break;
default:
throw new ChannelApeError_1.default('HTTP Request Method could not be determined', {}, '', []);
}
requestPromise
.then((response) => {
const requestResponse = {
response,
error: undefined,
body: response == null ? {} : response.data
};
this.handleResponse(requestResponse, callback, url, callDetails, method);
})
.catch((e) => {
if (e.code || e.message === 'Network Error') {
e.code = e.code ? e.code : e.message;
this.handleResponse({ body: {}, error: undefined, response: { config: e.config }, code: e.code }, callback, url, callDetails, method);
return;
}
if (this.shouldRequestBeRetried(e.error, e.response) && e.response) {
this.handleResponse(e, callback, url, callDetails, method);
}
else {
try {
const apiErrors = e.response == null || e.response.data == null || e.response.data.errors == null
? [] : e.response.data.errors;
const finalError = new ChannelApeError_1.default(e.message, e.response, url, apiErrors);
this.requestCompleted();
callback(finalError, e, e.body);
}
catch (e) {
callback(new Error('API Error'), e, {});
this.requestLogger.logCallbackError(e);
}
}
});
}
catch (e) {
const requestResponse = {
error: e,
body: {},
response: undefined
};
this.handleResponse(requestResponse, callback, '', callDetails, method);
return;
}
});
}
handleResponse(requestResponse, callback, uri, callDetails, method) {
this.requestLogger.logResponse(requestResponse.error, requestResponse.response, requestResponse.body, requestResponse.code);
let finalError = null;
if (this.didRequestFail(requestResponse)) {
if (this.didRequestTimeout(callDetails.callStart)) {
const maximumRetryLimitExceededMessage = this.getMaximumRetryLimitExceededMessage(callDetails.callStart, callDetails.callCountForThisRequest);
finalError = new ChannelApeError_1.default(maximumRetryLimitExceededMessage, requestResponse.response, uri, []);
}
else if (this.shouldRequestBeRetried(requestResponse.error, requestResponse.response, requestResponse.code)) {
this.retryRequest(method, uri, callback, callDetails);
return;
}
if (requestResponse.error) {
finalError = new ChannelApeError_1.default(requestResponse.error.message, requestResponse.response, uri, [{
code: GENERIC_ERROR_CODE,
message: requestResponse.error.message
}]);
}
else if (this.isApiError(requestResponse.body) && requestResponse.response && requestResponse.body) {
finalError = new ChannelApeError_1.default(`${requestResponse.response.status} ${requestResponse.response.statusText}`, requestResponse.response, uri, requestResponse.body.errors);
}
}
this.requestCompleted();
try {
callback(finalError, requestResponse.response, requestResponse.body);
}
catch (e) {
callback(new Error('API Error'), e, {});
this.requestLogger.logCallbackError(e);
}
}
getMaximumRetryLimitExceededMessage(callStart, numberOfCalls) {
const elapsedTimeMs = new Date().getTime() - callStart.getTime();
return `A problem with the ChannelApe API has been encountered.
Your request was tried a total of ${numberOfCalls} times over the course of ${elapsedTimeMs} milliseconds`;
}
didRequestTimeout(callStart) {
const now = new Date();
const totalElapsedTime = now.getTime() - callStart.getTime();
return totalElapsedTime > this.requestClientWrapperConfiguration.maximumRequestRetryTimeout;
}
requestCompleted() {
this.pendingRequests = this.pendingRequests - 1;
if (this.requestQueue.length > 0) {
const nextRequestConfig = this.requestQueue.pop();
if (nextRequestConfig) {
this.prepareRequest(nextRequestConfig);
}
}
}
shouldRequestBeRetried(error, response, code) {
if (code) {
return true;
}
if (response == null) {
return false;
}
return (!error && ((response.status >= 500 && response.status <= 599) || response.status === 429));
}
isApiError(body) {
if (typeof body === 'undefined' || typeof body.errors === 'undefined') {
return false;
}
return body.errors.length > 0;
}
retryRequest(method, url, callback, callDetails) {
return __awaiter(this, void 0, void 0, function* () {
const jitterDelayAmountMs = this.getJitterDelayMs();
setTimeout(() => __awaiter(this, void 0, void 0, function* () {
callDetails.callCountForThisRequest += 1;
this.requestLogger.logDelay(callDetails.callCountForThisRequest, jitterDelayAmountMs, {
method,
endpoint: url
});
yield this.makeRequest(method, callDetails.callStart, callDetails.callCountForThisRequest, url, callDetails.options, callback);
}), jitterDelayAmountMs);
});
}
getJitterDelayMs() {
const range = (this.requestClientWrapperConfiguration.maximumRequestRetryRandomDelay -
this.requestClientWrapperConfiguration.minimumRequestRetryRandomDelay) + 1;
return (Math.floor(Math.random() * (range))) +
this.requestClientWrapperConfiguration.minimumRequestRetryRandomDelay;
}
didRequestFail(requestResponse) {
if ((requestResponse.error || requestResponse instanceof Error) ||
(requestResponse.code && !doesValueStartWithNumber(requestResponse.code, 2)) ||
(requestResponse.response && !doesValueStartWithNumber(requestResponse.response.status, 2))) {
return true;
}
return false;
function doesValueStartWithNumber(value, number) {
if (value === undefined || value === null || value === '') {
return false;
}
let stringValue = value;
if (Number.isInteger(value)) {
stringValue = value.toString();
}
return stringValue.startsWith(number.toString());
}
}
parseJwt(token) {
let output;
try {
const base64Url = token.split('.')[1];
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
const jsonPayload = decodeURIComponent(atob(base64).split('').map((c) => {
// tslint:disable-next-line: prefer-template
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
}).join(''));
return JSON.parse(jsonPayload);
}
catch (e) {
output = '';
}
return output;
}
isItJwtToken(token) {
if (this.parseJwt(token)) {
return true;
}
return false;
}
}
exports.default = RequestClientWrapper;
//# sourceMappingURL=RequestClientWrapper.js.map