featurehub-javascript-client-sdk
Version:
FeatureHub client/browser SDK
358 lines • 14 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
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 });
exports.FeatureHubPollingClient = exports.BrowserPollingService = exports.PollingBase = void 0;
const models_1 = require("./models");
const feature_hub_config_1 = require("./feature_hub_config");
const cross_sha256_1 = require("cross-sha256");
const base64 = __importStar(require("@juanelas/base64"));
class PollingBase {
constructor(url, frequency, callback) {
this._stopped = false;
this._busy = false;
this._outstandingPromises = [];
this.url = url;
this._frequency = frequency;
this._shaHeader = '0';
this._callback = callback;
this._busy = false;
}
attributeHeader(header) {
this._header = header;
this._shaHeader = (header === undefined || header.length === 0) ? '0' :
base64.encode(new cross_sha256_1.sha256().update(header).digest(), true, false);
return new Promise((resolve) => {
resolve();
});
}
stop() {
this._stopped = true;
}
get frequency() {
return this._frequency;
}
parseCacheControl(cacheHeader) {
const maxAge = cacheHeader === null || cacheHeader === void 0 ? void 0 : cacheHeader.match(/max-age=(\d+)/);
if (maxAge) {
const newFreq = parseInt(maxAge[1], 10);
if (newFreq > 0) {
this._frequency = newFreq * 1000;
}
}
}
delayTimer() {
return __awaiter(this, void 0, void 0, function* () {
return new Promise(((resolve) => {
resolve();
}));
});
}
get busy() {
return this._busy;
}
resolveOutstanding() {
const outstanding = [...this._outstandingPromises];
this._outstandingPromises = [];
outstanding.forEach(e => e.resolve());
}
rejectOutstanding(result) {
const outstanding = [...this._outstandingPromises];
this._outstandingPromises = [];
outstanding.forEach(e => e.reject(result));
}
}
exports.PollingBase = PollingBase;
class BrowserPollingService extends PollingBase {
constructor(options, url, frequency, callback) {
super(url, frequency, callback);
this._options = options;
}
loadLocalState(url) {
if (url !== this.localStorageLastUrl) {
this.localStorageLastUrl = url;
const storedData = BrowserPollingService.localStorageRequestor().getItem(url);
if (storedData) {
try {
const data = JSON.parse(storedData);
if (data.e) {
this._callback(data.e);
}
}
catch (_) {
}
}
}
}
poll() {
if (this._busy) {
return new Promise((resolve, reject) => {
this._outstandingPromises.push({ resolve: resolve, reject: reject });
});
}
if (this._stopped) {
return new Promise((resolve) => {
resolve();
});
}
return new Promise((resolve, reject) => {
const calculatedUrl = `${this.url}&contextSha=${this._shaHeader}`;
this.loadLocalState(this.url);
const req = BrowserPollingService.httpRequestor();
req.open('GET', calculatedUrl);
req.setRequestHeader('Content-type', 'application/json');
if (this._etag) {
req.setRequestHeader('if-none-match', this._etag);
}
if (this._header) {
req.setRequestHeader('x-featurehub', this._header);
}
req.send();
req.onreadystatechange = () => {
if (req.readyState === 4) {
if (req.status === 200 || req.status == 236) {
this._etag = req.getResponseHeader('etag');
this.parseCacheControl(req.getResponseHeader('cache-control'));
const environments = JSON.parse(req.responseText);
try {
BrowserPollingService.localStorageRequestor().setItem(this.url, JSON.stringify({ e: environments }));
}
catch (_) {
feature_hub_config_1.fhLog.error('featurehub: unable to cache features');
}
this._callback(environments);
this._stopped = (req.status === 236);
this._busy = false;
this.resolveOutstanding();
resolve();
}
else if (req.status == 304) {
this._busy = false;
this.resolveOutstanding();
resolve();
}
else {
this._busy = false;
this.rejectOutstanding(req.status);
reject(req.status);
}
}
};
});
}
}
exports.BrowserPollingService = BrowserPollingService;
BrowserPollingService.httpRequestor = () => {
return new XMLHttpRequest();
};
BrowserPollingService.localStorageRequestor = () => {
if (window.localStorage) {
return localStorage;
}
return {
getItem: () => null,
setItem: () => { }
};
};
class FeatureHubPollingClient {
constructor(repository, config, frequency, options = {}) {
this._startable = true;
this._frequency = frequency;
this._repository = repository;
this._options = options;
this._config = config;
this._url = config.getHost() + 'features?' + config.getApiKeys().map(e => 'apiKey=' + encodeURIComponent(e)).join('&');
}
_initService() {
if (this._pollingService === undefined && this._startable) {
this._pollingService =
FeatureHubPollingClient.pollingClientProvider(this._options, this._url, this._frequency, (e) => this.response(e));
feature_hub_config_1.fhLog.trace(`featurehub: initialized polling client to ${this._url}`);
}
}
contextChange(header) {
return __awaiter(this, void 0, void 0, function* () {
if (!this._config.clientEvaluated()) {
if (this._xHeader !== header) {
this._xHeader = header;
this._initService();
if (this._pollingService) {
yield this._pollingService.attributeHeader(header);
}
this._restartTimer();
}
}
return new Promise((resolve) => resolve());
});
}
clientEvaluated() {
return this._config.clientEvaluated();
}
requiresReplacementOnHeaderChange() {
return false;
}
close() {
this.stop();
}
stop() {
var _a;
feature_hub_config_1.fhLog.trace('polling stopping');
if (this._currentTimer) {
clearTimeout(this._currentTimer);
this._currentTimer = undefined;
}
if (this._pollPromiseReject !== undefined) {
this._pollPromiseReject('Never came live');
}
(_a = this._pollingService) === null || _a === void 0 ? void 0 : _a.stop();
this._pollingService = undefined;
}
poll() {
var _a;
if (this._pollPromiseResolve !== undefined || ((_a = this._pollingService) === null || _a === void 0 ? void 0 : _a.busy)) {
return new Promise((resolve) => resolve());
}
if (!this._startable) {
return new Promise((_, reject) => reject());
}
this._initService();
return new Promise((resolve, reject) => {
this._pollPromiseReject = reject;
this._pollPromiseResolve = resolve;
this._restartTimer();
});
}
get canStart() {
return this._startable;
}
get pollingFrequency() {
var _a;
return (_a = this._pollingService) === null || _a === void 0 ? void 0 : _a.frequency;
}
get active() {
var _a;
return ((_a = this._pollingService) === null || _a === void 0 ? void 0 : _a.busy) || this._currentTimer !== undefined;
}
get awaitingFirstSuccess() {
return this._pollPromiseReject !== undefined;
}
_restartTimer() {
var _a;
if (this._pollingService === undefined || ((_a = this._pollingService) === null || _a === void 0 ? void 0 : _a.busy) || !this._startable) {
return;
}
feature_hub_config_1.fhLog.trace('polling restarting');
if (this._currentTimer) {
clearTimeout(this._currentTimer);
this._currentTimer = undefined;
}
this._pollFunc();
}
_pollFunc() {
this._pollingService.poll()
.then(() => {
feature_hub_config_1.fhLog.trace('poll successful');
this._readyNextPoll();
if (this._pollPromiseResolve !== undefined) {
try {
this._pollPromiseResolve();
}
catch (e) {
feature_hub_config_1.fhLog.error('Failed to process resolve', e);
}
}
this._pollPromiseReject = undefined;
this._pollPromiseResolve = undefined;
})
.catch((status) => {
feature_hub_config_1.fhLog.trace('poll failed', status);
if (status === 404 || status == 400) {
if (status == 404) {
feature_hub_config_1.fhLog.error('The API Key provided does not exist, stopping polling.');
}
this._repository.notify(models_1.SSEResultState.Failure, null);
this._startable = false;
this.stop();
if (this._pollPromiseReject) {
try {
this._pollPromiseReject(status);
}
catch (e) {
feature_hub_config_1.fhLog.error('Failed to process reject', e);
}
}
this._pollPromiseReject = undefined;
this._pollPromiseResolve = undefined;
}
else {
this._readyNextPoll();
if (status == 503) {
feature_hub_config_1.fhLog.log('The backend is not ready, waiting for the next poll.');
}
}
}).finally(() => {
});
}
_readyNextPoll() {
var _a;
if (this._pollingService && this._pollingService.frequency > 0) {
feature_hub_config_1.fhLog.trace('starting timer for poll', this._pollingService.frequency);
this._currentTimer = setTimeout(() => this._restartTimer(), this._pollingService.frequency);
}
else {
feature_hub_config_1.fhLog.trace('no polling service or 0 frequency, stopping polling.', this._pollingService === undefined, (_a = this._pollingService) === null || _a === void 0 ? void 0 : _a.frequency);
}
}
response(environments) {
if (environments.length === 0) {
this._startable = false;
this.stop();
this._repository.notify(models_1.SSEResultState.Failure, null);
}
else {
const features = new Array();
environments.forEach(e => {
if (e.features.length > 0) {
e.features.forEach(f => {
f.environmentId = e.id;
});
features.push(...e.features);
}
});
this._repository.notify(models_1.SSEResultState.Features, features);
}
}
}
exports.FeatureHubPollingClient = FeatureHubPollingClient;
FeatureHubPollingClient.pollingClientProvider = (opt, url, freq, callback) => new BrowserPollingService(opt, url, freq, callback);
//# sourceMappingURL=polling_sdk.js.map