@launchdarkly/react-native-client-sdk
Version:
React Native LaunchDarkly SDK
288 lines • 11.5 kB
JavaScript
const XMLReadyStateMap = ['UNSENT', 'OPENED', 'HEADERS_RECEIVED', 'LOADING', 'DONE'];
const defaultOptions = {
body: undefined,
headers: {},
method: 'GET',
timeout: 0,
withCredentials: false,
retryAndHandleError: undefined,
initialRetryDelayMillis: 1000,
logger: undefined,
};
const maxRetryDelay = 30 * 1000; // Maximum retry delay 30 seconds.
const jitterRatio = 0.5; // Delay should be 50%-100% of calculated time.
export function backoff(base, retryCount) {
const delay = base * Math.pow(2, retryCount);
return Math.min(delay, maxRetryDelay);
}
export function jitter(computedDelayMillis) {
return computedDelayMillis - Math.trunc(Math.random() * jitterRatio * computedDelayMillis);
}
export default class EventSource {
constructor(url, options) {
this.ERROR = -1;
this.CONNECTING = 0;
this.OPEN = 1;
this.CLOSED = 2;
this._lastIndexProcessed = 0;
this._status = this.CONNECTING;
this._eventHandlers = {
open: [],
message: [],
error: [],
close: [],
};
this._xhr = new XMLHttpRequest();
this._initialRetryDelayMillis = 1000;
this._retryCount = 0;
const opts = Object.assign(Object.assign({}, defaultOptions), options);
this._url = url;
this._method = opts.method;
this._timeout = opts.timeout;
this._withCredentials = opts.withCredentials;
this._headers = opts.headers;
this._body = opts.body;
this._retryAndHandleError = opts.retryAndHandleError;
this._initialRetryDelayMillis = opts.initialRetryDelayMillis;
this._logger = opts.logger;
this._tryConnect(true);
}
_getNextRetryDelay() {
const delay = jitter(backoff(this._initialRetryDelayMillis, this._retryCount));
this._retryCount += 1;
return delay;
}
_tryConnect(initialConnection = false) {
var _a, _b;
let delay = initialConnection ? 0 : this._getNextRetryDelay();
if (initialConnection) {
(_a = this._logger) === null || _a === void 0 ? void 0 : _a.debug(`[EventSource] opening new connection.`);
}
else {
(_b = this._logger) === null || _b === void 0 ? void 0 : _b.debug(`[EventSource] Will open new connection in ${delay} ms.`);
this.dispatch('retry', { type: 'retry', delayMillis: delay });
}
this._connectTimer = setTimeout(() => {
if (!initialConnection) {
this.close();
}
this._open();
}, delay);
}
_open() {
try {
this._lastIndexProcessed = 0;
this._status = this.CONNECTING;
this._xhr.open(this._method, this._url, true);
if (this._withCredentials) {
this._xhr.withCredentials = true;
}
this._xhr.setRequestHeader('Accept', 'text/event-stream');
this._xhr.setRequestHeader('Cache-Control', 'no-cache');
this._xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
if (this._headers) {
Object.entries(this._headers).forEach(([key, value]) => {
this._xhr.setRequestHeader(key, value);
});
}
if (typeof this._lastEventId !== 'undefined') {
this._xhr.setRequestHeader('Last-Event-ID', this._lastEventId);
}
this._xhr.timeout = this._timeout;
this._xhr.onreadystatechange = () => {
var _a, _b, _c, _d;
if (this._status === this.CLOSED) {
return;
}
(_a = this._logger) === null || _a === void 0 ? void 0 : _a.debug(`[EventSource][onreadystatechange] ReadyState: ${XMLReadyStateMap[this._xhr.readyState] || 'Unknown'}(${this._xhr.readyState}), status: ${this._xhr.status}`);
if (this._xhr.readyState !== XMLHttpRequest.DONE &&
this._xhr.readyState !== XMLHttpRequest.LOADING) {
return;
}
if (this._xhr.status >= 200 && this._xhr.status < 400) {
if (this._status === this.CONNECTING) {
this._retryCount = 0;
this._status = this.OPEN;
this.dispatch('open', { type: 'open' });
(_b = this._logger) === null || _b === void 0 ? void 0 : _b.debug('[EventSource][onreadystatechange][OPEN] Connection opened.');
}
// retry from server gets set here
this._handleEvent(this._xhr.responseText || '');
if (this._xhr.readyState === XMLHttpRequest.DONE) {
(_c = this._logger) === null || _c === void 0 ? void 0 : _c.debug('[EventSource][onreadystatechange][DONE] Operation done.');
this._tryConnect();
}
}
else {
this._status = this.ERROR;
this.dispatch('error', {
type: 'error',
message: this._xhr.responseText,
xhrStatus: this._xhr.status,
xhrState: this._xhr.readyState,
});
if (this._xhr.readyState === XMLHttpRequest.DONE) {
(_d = this._logger) === null || _d === void 0 ? void 0 : _d.debug('[EventSource][onreadystatechange][ERROR] Response status error.');
if (!this._retryAndHandleError) {
// by default just try and reconnect if there's an error.
this._tryConnect();
}
else {
// custom retry logic taking into account status codes.
const shouldRetry = this._retryAndHandleError({
status: this._xhr.status,
message: this._xhr.responseText,
});
if (shouldRetry) {
this._tryConnect();
}
}
}
}
};
this._xhr.onerror = () => {
if (this._status === this.CLOSED) {
return;
}
this._status = this.ERROR;
this.dispatch('error', {
type: 'error',
message: this._xhr.responseText,
xhrStatus: this._xhr.status,
xhrState: this._xhr.readyState,
});
};
if (this._body) {
this._xhr.send(this._body);
}
else {
this._xhr.send();
}
if (this._timeout > 0) {
setTimeout(() => {
if (this._xhr.readyState === XMLHttpRequest.LOADING) {
this.dispatch('error', { type: 'timeout' });
this.close();
}
}, this._timeout);
}
}
catch (e) {
this._status = this.ERROR;
this.dispatch('error', {
type: 'exception',
message: e.message,
error: e,
});
}
}
_handleEvent(response) {
const parts = response.slice(this._lastIndexProcessed).split('\n');
const indexOfDoubleNewline = response.lastIndexOf('\n\n');
if (indexOfDoubleNewline !== -1) {
this._lastIndexProcessed = indexOfDoubleNewline + 2;
}
let data = [];
let retry = 0;
let line = '';
// eslint-disable-next-line no-plusplus
for (let i = 0; i < parts.length; i++) {
line = parts[i].replace(/^(\s|\u00A0)+|(\s|\u00A0)+$/g, '');
if (line.indexOf('event') === 0) {
this._eventType = line.replace(/event:?\s*/, '');
}
else if (line.indexOf('retry') === 0) {
retry = parseInt(line.replace(/retry:?\s*/, ''), 10);
if (!Number.isNaN(retry)) {
// GOTCHA: Ignore the server retry recommendation. Use our own custom getNextRetryDelay logic.
// this.pollingInterval = retry;
}
}
else if (line.indexOf('data') === 0) {
data.push(line.replace(/data:?\s*/, ''));
}
else if (line.indexOf('id:') === 0) {
this._lastEventId = line.replace(/id:?\s*/, '');
}
else if (line.indexOf('id') === 0) {
this._lastEventId = undefined;
}
else if (line === '') {
if (data.length > 0) {
const eventType = this._eventType || 'message';
const event = {
type: eventType,
data: data.join('\n'),
url: this._url,
lastEventId: this._lastEventId,
};
this.dispatch(eventType, event);
data = [];
this._eventType = undefined;
}
}
}
}
addEventListener(type, listener) {
if (this._eventHandlers[type] === undefined) {
this._eventHandlers[type] = [];
}
this._eventHandlers[type].push(listener);
}
removeEventListener(type, listener) {
if (this._eventHandlers[type] !== undefined) {
this._eventHandlers[type] = this._eventHandlers[type].filter((handler) => handler !== listener);
}
}
removeAllEventListeners(type) {
const availableTypes = Object.keys(this._eventHandlers);
if (type === undefined) {
availableTypes.forEach((eventType) => {
this._eventHandlers[eventType] = [];
});
}
else {
if (!availableTypes.includes(type)) {
throw Error(`[EventSource] '${type}' type is not supported event type.`);
}
this._eventHandlers[type] = [];
}
}
dispatch(type, data) {
var _a, _b;
(_a = this._eventHandlers[type]) === null || _a === void 0 ? void 0 : _a.forEach((handler) => handler(data));
switch (type) {
case 'open':
this.onopen();
break;
case 'close':
this.onclose();
break;
case 'error':
(_b = this._logger) === null || _b === void 0 ? void 0 : _b.debug(`[EventSource][dispatch][ERROR]: ${JSON.stringify(data)}`);
this.onerror(data);
break;
case 'retry':
this.onretrying(data);
break;
default:
break;
}
}
close() {
this._status = this.CLOSED;
clearTimeout(this._connectTimer);
if (this._xhr) {
this._xhr.abort();
}
this.dispatch('close', { type: 'close' });
}
getStatus() {
return this._status;
}
onopen() { }
onclose() { }
onerror(_err) { }
onretrying(_e) { }
}
//# sourceMappingURL=EventSource.js.map