@microsoft/omnichannel-chat-sdk
Version:
Microsoft Omnichannel Chat SDK
349 lines (348 loc) • 16.8 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
var Enums_1 = require("../common/Enums");
var AWTSerializer_1 = require("../common/AWTSerializer");
var AWTRetryPolicy_1 = require("../common/AWTRetryPolicy");
var AWTKillSwitch_1 = require("../common/AWTKillSwitch");
var AWTClockSkewManager_1 = require("../common/AWTClockSkewManager");
var Version = require("./Version");
var Utils = require("../common/Utils");
var AWTNotificationManager_1 = require("../common/AWTNotificationManager");
var AWTTransmissionManagerCore_1 = require("./AWTTransmissionManagerCore");
var MaxConnections = 2;
var MaxRetries = 1;
var Method = 'POST';
/**
* Class managing the sending of requests.
*/
var AWTHttpManager = /** @class */ (function () {
/**
* @constructor
* @param {object} requestQueue - The queue that contains the requests to be sent.
* @param {string} collectorUrl - The collector url to which the requests must be sent.
* @param {object} _queueManager - The queue manager that we should add requests back to if needed.
* @param {object} _httpInterface - The http interface that should be used to send HTTP requests.
*/
function AWTHttpManager(_requestQueue, collectorUrl, _queueManager, _httpInterface, clockSkewRefreshDurationInMins) {
var _this = this;
this._requestQueue = _requestQueue;
this._queueManager = _queueManager;
this._httpInterface = _httpInterface;
this._urlString = '?qsp=true&content-type=application%2Fbond-compact-binary&client-id=NO_AUTH&sdk-version='
+ Version.FullVersionString;
this._killSwitch = new AWTKillSwitch_1.default();
this._paused = false;
this._useBeacons = false;
this._activeConnections = 0;
this._clockSkewManager = new AWTClockSkewManager_1.default(clockSkewRefreshDurationInMins);
if (!Utils.isUint8ArrayAvailable()) {
this._urlString += '&content-encoding=base64';
}
this._urlString = collectorUrl + this._urlString;
if (!this._httpInterface) {
this._useBeacons = !Utils.isReactNative(); //Only use beacons if not running in React Native
this._httpInterface = {
sendPOST: function (urlString, data, ontimeout, onerror, onload, sync) {
try {
if (Utils.useFetchRequest()) {
//Use the fetch API to send events in React Native and ServiceWorkerGlobal Scope
fetch(urlString, {
body: data,
method: Method
}).then(function (response) {
var headerMap = {};
if (response.headers) {
response.headers.forEach(function (value, name) {
headerMap[name] = value;
});
}
onload(response.status, headerMap);
}).catch(function (error) {
//In case there is an error in the request. Set the status to 0
//so that the events can be retried later.
onerror(0, {});
});
}
else if (Utils.useXDomainRequest()) {
var xdr = new XDomainRequest();
xdr.open(Method, urlString);
//can't get the status code in xdr.
xdr.onload = function () {
// we will assume onload means the request succeeded.
onload(200, null);
};
xdr.onerror = function () {
// we will assume onerror means we need to drop the events.
onerror(400, null);
};
xdr.ontimeout = function () {
// we will assume ontimeout means we need to retry the events.
ontimeout(500, null);
};
xdr.send(data);
}
else {
var xhr_1 = new XMLHttpRequest();
xhr_1.open(Method, urlString, !sync);
xhr_1.onload = function () {
onload(xhr_1.status, _this._convertAllHeadersToMap(xhr_1.getAllResponseHeaders()));
};
xhr_1.onerror = function () {
onerror(xhr_1.status, _this._convertAllHeadersToMap(xhr_1.getAllResponseHeaders()));
};
xhr_1.ontimeout = function () {
ontimeout(xhr_1.status, _this._convertAllHeadersToMap(xhr_1.getAllResponseHeaders()));
};
xhr_1.send(data);
}
}
catch (e) {
// we will assume exception means we need to drop the events.
onerror(400, null);
}
}
};
}
}
/**
* Check if there is an idle connection overwhich we can send a request.
* @return {boolean} True if there is an idle connection, false otherwise.
*/
AWTHttpManager.prototype.hasIdleConnection = function () {
return this._activeConnections < MaxConnections;
};
/**
* Send requests in the request queue up if there is an idle connection, sending is
* not pause and clock skew manager allows sending request.
*/
AWTHttpManager.prototype.sendQueuedRequests = function () {
while (this.hasIdleConnection() && !this._paused && this._requestQueue.length > 0
&& this._clockSkewManager.allowRequestSending()) {
this._activeConnections++;
this._sendRequest(this._requestQueue.shift(), 0, false);
}
//No more requests to send, tell TPM to try to schedule timer
//in case it was waiting for idle connections
if (this.hasIdleConnection()) {
AWTTransmissionManagerCore_1.default.scheduleTimer();
}
};
/**
* Check if there are no active requests being sent.
* @return {boolean} True if idle, false otherwise.
*/
AWTHttpManager.prototype.isCompletelyIdle = function () {
return this._activeConnections === 0;
};
/**
* Queue all the remaning requests to be sent. The requests will be
* sent using HTML5 Beacons if they are available.
*/
AWTHttpManager.prototype.teardown = function () {
while (this._requestQueue.length > 0) {
this._sendRequest(this._requestQueue.shift(), 0, true);
}
};
/**
* Pause the sending of requests. No new requests will be sent.
*/
AWTHttpManager.prototype.pause = function () {
this._paused = true;
};
/**
* Resume the sending of requests.
*/
AWTHttpManager.prototype.resume = function () {
this._paused = false;
this.sendQueuedRequests();
};
/**
* Removes any pending requests to be sent.
*/
AWTHttpManager.prototype.removeQueuedRequests = function () {
this._requestQueue.length = 0;
};
/**
* Sends a request synchronously to the Aria collector. This api is used to send
* a request containing a single immediate event.
*
* @param request - The request to be sent.
* @param token - The token used to send the request.
*/
AWTHttpManager.prototype.sendSynchronousRequest = function (request, token) {
//This will not take into account the max connections restriction. Since this is sync, we can
//only send one of this request at a time and thus should not worry about multiple connections
//being used to send synchronoush events.
if (this._paused) {
//If paused then convert to High priority. It will be added back to queue in _sendRequest
request[token][0].priority = Enums_1.AWTEventPriority.High;
}
//Increment active connection since we are still going to use a connection to send the request.
this._activeConnections++;
//For sync requests we will not wait for the clock skew.
this._sendRequest(request, 0, false, true);
};
AWTHttpManager.prototype._sendRequest = function (request, retryCount, isTeardown, isSynchronous) {
var _this = this;
if (isSynchronous === void 0) { isSynchronous = false; }
try {
if (this._paused) {
this._activeConnections--;
this._queueManager.addBackRequest(request);
return;
}
var tokenCount_1 = 0;
var apikey_1 = '';
for (var token in request) {
if (request.hasOwnProperty(token)) {
if (!this._killSwitch.isTenantKilled(token)) {
if (apikey_1.length > 0) {
apikey_1 += ',';
}
apikey_1 += token;
tokenCount_1++;
}
else {
AWTNotificationManager_1.default.eventsRejected(request[token], Enums_1.AWTEventsRejectedReason.KillSwitch);
delete request[token];
}
}
}
if (tokenCount_1 > 0) {
var payloadResult = AWTSerializer_1.default.getPayloadBlob(request, tokenCount_1);
if (payloadResult.remainingRequest) {
this._requestQueue.push(payloadResult.remainingRequest);
}
var urlString = this._urlString + '&x-apikey=' + apikey_1 + '&client-time-epoch-millis='
+ Date.now().toString();
if (this._clockSkewManager.shouldAddClockSkewHeaders()) {
urlString = urlString + '&time-delta-to-apply-millis=' + this._clockSkewManager.getClockSkewHeaderValue();
}
var data = void 0;
if (!Utils.isUint8ArrayAvailable()) {
data = AWTSerializer_1.default.base64Encode(payloadResult.payloadBlob);
}
else {
data = new Uint8Array(payloadResult.payloadBlob);
}
for (var token in request) {
if (request.hasOwnProperty(token)) {
//Increment the send attempt count
for (var i = 0; i < request[token].length; ++i) {
request[token][i].sendAttempt > 0 ? request[token][i].sendAttempt++ : request[token][i].sendAttempt = 1;
}
}
}
//beacons will not be used if an http interface was passed by the customer
if (this._useBeacons && isTeardown && Utils.isBeaconsSupported()) {
if (navigator.sendBeacon(urlString, data)) {
//Request sent via beacon.
return;
}
}
//Send sync requests if the request is immediate or we are tearing down telemetry.
this._httpInterface.sendPOST(urlString, data, function (status, headers) {
_this._retryRequestIfNeeded(status, headers, request, tokenCount_1, apikey_1, retryCount, isTeardown, isSynchronous);
}, function (status, headers) {
_this._retryRequestIfNeeded(status, headers, request, tokenCount_1, apikey_1, retryCount, isTeardown, isSynchronous);
}, function (status, headers) {
_this._retryRequestIfNeeded(status, headers, request, tokenCount_1, apikey_1, retryCount, isTeardown, isSynchronous);
}, isTeardown || isSynchronous);
}
else if (!isTeardown) {
this._handleRequestFinished(false, {}, isTeardown, isSynchronous);
}
}
catch (e) {
//If we catch any error while sending the request, drop the request.
this._handleRequestFinished(false, {}, isTeardown, isSynchronous);
}
};
AWTHttpManager.prototype._retryRequestIfNeeded = function (status, headers, request, tokenCount, apikey, retryCount, isTeardown, isSynchronous) {
var _this = this;
var shouldRetry = true;
if (typeof status !== 'undefined') {
if (headers) {
var killedTokens = this._killSwitch.setKillSwitchTenants(headers['kill-tokens'], headers['kill-duration-seconds']);
this._clockSkewManager.setClockSkew(headers['time-delta-millis']);
for (var i = 0; i < killedTokens.length; ++i) {
AWTNotificationManager_1.default.eventsRejected(request[killedTokens[i]], Enums_1.AWTEventsRejectedReason.KillSwitch);
delete request[killedTokens[i]];
tokenCount--;
}
}
else {
this._clockSkewManager.setClockSkew(null);
}
if (status === 200) {
this._handleRequestFinished(true, request, isTeardown, isSynchronous);
return;
}
if (!AWTRetryPolicy_1.default.shouldRetryForStatus(status) || tokenCount <= 0) {
shouldRetry = false;
}
}
if (shouldRetry) {
if (isSynchronous) {
//Synchronous events only contain a single event so the apiKey is equal to the token for that event.
//Convert the event to High priority and add back to queue to be sent as High event.
this._activeConnections--;
request[apikey][0].priority = Enums_1.AWTEventPriority.High;
this._queueManager.addBackRequest(request);
}
else if (retryCount < MaxRetries) {
for (var token in request) {
if (request.hasOwnProperty(token)) {
AWTNotificationManager_1.default.eventsRetrying(request[token]);
}
}
setTimeout(function () { return _this._sendRequest(request, retryCount + 1, false); }, AWTRetryPolicy_1.default.getMillisToBackoffForRetry(retryCount));
}
else {
this._activeConnections--;
AWTTransmissionManagerCore_1.default.backOffTransmission();
this._queueManager.addBackRequest(request);
}
}
else {
this._handleRequestFinished(false, request, isTeardown, isSynchronous);
}
};
AWTHttpManager.prototype._handleRequestFinished = function (success, request, isTeardown, isSynchronous) {
if (success) {
AWTTransmissionManagerCore_1.default.clearBackOff();
}
for (var token in request) {
if (request.hasOwnProperty(token)) {
if (success) {
AWTNotificationManager_1.default.eventsSent(request[token]);
}
else {
AWTNotificationManager_1.default.eventsDropped(request[token], Enums_1.AWTEventsDroppedReason.NonRetryableStatus);
}
}
}
this._activeConnections--;
if (!isSynchronous && !isTeardown) {
//Only continue sending more requests as long as the current request was not an synchronous request or sent
//during teardown. We want to return after just sending this one sync request.
this.sendQueuedRequests();
}
};
/**
* Converts the XHR getAllResponseHeaders to a map containing the header key and value.
*/
AWTHttpManager.prototype._convertAllHeadersToMap = function (headersString) {
var headers = {};
if (headersString) {
var headersArray = headersString.split('\n');
for (var i = 0; i < headersArray.length; ++i) {
var header = headersArray[i].split(': ');
headers[header[0]] = header[1];
}
}
return headers;
};
return AWTHttpManager;
}());
exports.default = AWTHttpManager;