lightswitch-js-sdk
Version:
light switch javascript sdk
277 lines (276 loc) • 12.1 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());
});
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const types_1 = require("./types");
const LSLogger_1 = require("./LSLogger");
const utils_1 = require("./utils");
const reconnecting_eventsource_1 = __importDefault(require("reconnecting-eventsource"));
const error_1 = require("./error");
const logger = (0, LSLogger_1.LSLogger)(types_1.LogLevel.DEBUG);
const FLAG_NOT_FOUND = 1000;
const VARIATION_NOT_FOUND = 1001;
const MEMBER_NOT_FOUND = 2000;
const SDK_KEY_ALREADY_EXISTS = 3000;
const SDK_KEY_NOT_FOUND = 3001;
class LightSwitch {
constructor() {
this.sdkKey = '';
this.logLevel = types_1.LogLevel.DEBUG; // 로그 레벨
// private lastUpdated = ''; // feature flag 데이터 갱신 날짜
this.flags = new Map();
this.onError = null;
this.eventSource = null;
this.onFlagChanged = null;
this.userKey = '';
this.reconnectTime = 3000;
this.SERVER_URL = 'http://localhost:8000';
this.INIT_REQUEST_PATH = this.SERVER_URL + '/api/v1/sdk/init';
this.SSE_CONNECT_PATH = this.SERVER_URL + '/api/v1/sse/subscribe';
}
static getInstance() {
if (!LightSwitch.instance) {
LightSwitch.instance = new LightSwitch();
}
return LightSwitch.instance;
}
init(config) {
return __awaiter(this, void 0, void 0, function* () {
if (LightSwitch.instance != null && LightSwitch.isInitialized) {
logger.info('lightswitch is already initialized, skip init process');
return;
}
const { sdkKey, onError, onFlagChanged, reconnectTime, endpoint } = config;
this.sdkKey = sdkKey;
if (reconnectTime) {
this.reconnectTime = reconnectTime;
}
if (onError) {
this.onError = onError;
}
if (endpoint) {
this.SERVER_URL = endpoint;
this.INIT_REQUEST_PATH = this.SERVER_URL + '/api/v1/sdk/init';
this.SSE_CONNECT_PATH = this.SERVER_URL + '/api/v1/sse/subscribe';
}
this.onFlagChanged = onFlagChanged;
yield this.getInitData();
logger.debug('success to getInitData');
yield this.getUserKey();
logger.debug('success to getUserKey, start connecting SSE');
this.eventSource = this.getEventSource(this.userKey, this.reconnectTime);
this.addSseListener();
LightSwitch.isInitialized = true;
// logger.info('success to initialize client sdk');
});
}
getVariationValue(flag, LSUser) {
let percentage = (0, utils_1.getHashedPercentageForObjectIds)([LSUser.getUserId(), flag.title], 1);
logger.info(percentage);
if (flag.variations) {
for (let i = 0; i < flag.variations.length; i++) {
const variation = flag.variations[i];
percentage -= variation.portion;
logger.info(`percentage : ${percentage}, portion : ${variation.portion}`);
if (percentage < 0) {
// logger.info(`value : ${variation.value}`);
return variation.value;
}
}
}
return flag.defaultValue;
}
getTypedValue(flag, value) {
if (flag.type == 'STRING') {
return value;
}
else if (flag.type == 'BOOLEAN') {
if (value == 'TRUE')
return true;
else
return false;
}
else if (flag.type == 'INTEGER') {
return parseInt(value);
}
}
getFlag(name, LSUser, defaultVal) {
var _a;
const flag = this.flags.get(name);
if (!flag) {
logger.warning(`${name} 플래그가 존재하지 않습니다. 기본값을 이용합니다.`);
return defaultVal;
}
if (!flag.active) {
logger.info(`id : ${flag === null || flag === void 0 ? void 0 : flag.flagId} title : ${flag === null || flag === void 0 ? void 0 : flag.title} 플래그가 활성화 되어있지 않습니다. 기본값을 이용합니다.`);
return defaultVal;
// return this.getTypedValue(flag, flag.defaultValue) as T;
}
else {
if (flag.keywords.length > 0 && ((_a = LSUser.properties) === null || _a === void 0 ? void 0 : _a.size) > 0) {
logger.info(`id : ${flag === null || flag === void 0 ? void 0 : flag.flagId} title : ${flag.title} 플래그가 활성화 되어 있습니다. 키워드를 확인합니다.`);
for (let i = 0; i < flag.keywords.length; i++) {
const keyword = flag.keywords[i];
const isEqual = (0, utils_1.compareObjectsAndMaps)(keyword.properties, LSUser.properties);
if (isEqual) {
return this.getTypedValue(flag, keyword.value);
}
else {
logger.info(`키워드 대상이 아닙니다. portion을 계산합니다.`);
}
}
}
logger.info(`id : ${flag === null || flag === void 0 ? void 0 : flag.flagId} title : ${flag.title} 플래그가 활성화 되어 있습니다. portion을 계산합니다.`);
const evaluatedValue = this.getVariationValue(flag, LSUser);
return this.getTypedValue(flag, evaluatedValue);
}
}
handleError(error) {
if (this.onError) {
this.onError(error);
}
else {
throw error;
}
}
getBooleanFlag(name, LSUser, defaultVal) {
const booleanFlag = this.getFlag(name, LSUser, defaultVal);
if (typeof booleanFlag != 'boolean') {
this.handleError(new error_1.LSTypeCastError(`${name} 플래그를 BOOLEAN 타입으로 캐스팅하는데 실패했습니다.`));
}
return booleanFlag;
}
getIntegerFlag(name, LSUser, defaultVal) {
const integerFlag = this.getFlag(name, LSUser, defaultVal);
if (typeof integerFlag != 'number') {
this.handleError(new error_1.LSTypeCastError(`${name} 플래그를 INTEGER 타입으로 캐스팅하는데 실패했습니다.`));
}
return integerFlag;
}
getStringFlag(name, LSUser, defaultVal) {
const stringFlag = this.getFlag(name, LSUser, defaultVal);
if (typeof stringFlag != 'string') {
this.handleError(new error_1.LSTypeCastError(`${name} 플래그를 STRING 타입으로 캐스팅하는데 실패했습니다.`));
}
return stringFlag;
}
getEventSource(userKey, reconnectTime) {
return new reconnecting_eventsource_1.default(this.SSE_CONNECT_PATH + '/' + userKey, {
// indicating if CORS should be set to include credentials, default `false`
withCredentials: true,
// the maximum time to wait before attempting to reconnect in ms, default `3000`
// note: wait time is randomised to prevent all clients from attempting to reconnect simultaneously
max_retry_time: reconnectTime,
// underlying EventSource class, default `EventSource`
eventSourceClass: EventSource,
});
}
getInitData() {
var _a;
return __awaiter(this, void 0, void 0, function* () {
const response = yield (0, utils_1.postRequest)(this.INIT_REQUEST_PATH, {
sdkKey: this.sdkKey,
});
if ((response === null || response === void 0 ? void 0 : response.code) == SDK_KEY_NOT_FOUND) {
this.handleError(new error_1.LSServerError(response.message));
}
const newFlags = response === null || response === void 0 ? void 0 : response.data;
newFlags.forEach((flag) => {
this.flags.set(flag.title, flag);
});
(_a = this.onFlagChanged) === null || _a === void 0 ? void 0 : _a.call(this);
});
}
getUserKey() {
return __awaiter(this, void 0, void 0, function* () {
try {
logger.info(this.sdkKey);
const response = yield (0, utils_1.postRequest)(`${this.SSE_CONNECT_PATH}`, {
sdkKey: this.sdkKey,
});
this.userKey = response.data.userKey;
// logger.info(`receive userKey data : ${JSON.stringify(response)}`);
}
catch (error) {
if (error instanceof Error) {
this.handleError(new error_1.LSServerError(error.message));
}
}
});
}
getAllFlags() {
return this.flags;
}
addSseListener() {
var _a;
(_a = this.eventSource) === null || _a === void 0 ? void 0 : _a.addEventListener('sse', (event) => {
var _a;
if (event.data === 'SSE connected') {
return;
}
const data = JSON.parse(event.data);
logger.info(data.type);
switch (data.type) {
case 'CREATE':
const createData = data.data;
this.addFlag(createData);
break;
case 'UPDATE':
const updateData = data.data;
this.updateFlag(updateData);
break;
case 'DELETE':
const deleteData = data.data;
this.deleteFlag(deleteData);
break;
case 'SWITCH': // not used
const switchData = data.data;
this.switchFlag(switchData);
break;
}
(_a = this.onFlagChanged) === null || _a === void 0 ? void 0 : _a.call(this);
});
}
addFlag(flag) {
logger.info('addFlag call');
this.flags.set(flag.title, flag);
}
updateFlag(newFlag) {
logger.info('updateFlag call');
this.flags.delete(newFlag.title);
this.flags.set(newFlag.title, newFlag);
}
deleteFlag(title) {
logger.info('deleteFlag call');
this.flags.delete(title.title);
}
switchFlag(sw) {
logger.info('switchFlag call');
const flag = this.flags.get(sw.title);
const newFlag = JSON.parse(JSON.stringify(flag));
newFlag.active = sw.active;
this.flags.set(sw.title, newFlag);
}
destroy() {
var _a;
if (!LightSwitch.isInitialized) {
throw new Error('LightSwitch is not initialized.');
}
(_a = this.eventSource) === null || _a === void 0 ? void 0 : _a.close();
LightSwitch.isInitialized = false;
logger.debug('call destroy');
}
}
LightSwitch.instance = null;
LightSwitch.isInitialized = false;
exports.default = LightSwitch;