UNPKG

channelape-sdk

Version:
295 lines 13.7 kB
"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