Skip to content

Commit

Permalink
Merge branch 'breaking_changes_update_supported_runtimes' into breaki…
Browse files Browse the repository at this point in the history
…ng_changes_ready_method
  • Loading branch information
EmilianoSanchez committed Oct 8, 2024
2 parents 65d5dc7 + e1a49f5 commit ddfae4a
Show file tree
Hide file tree
Showing 51 changed files with 301 additions and 654 deletions.
2 changes: 1 addition & 1 deletion .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@
"message": "Don't declare const enum, because it is not supported by Babel used for building RN SDK"
}
],
"compat/compat": ["error", "defaults, ie 10, node 6"],
"compat/compat": ["error", "defaults, node >=14"],
"no-throw-literal": "error",
"import/no-default-export": "error",
"import/no-self-import": "error"
Expand Down
3 changes: 2 additions & 1 deletion CHANGES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
- Removed `/mySegments` endpoint from SplitAPI module, as it is replaced by `/memberships` endpoint.
- Removed support for MY_SEGMENTS_UPDATE and MY_SEGMENTS_UPDATE_V2 notification types, as they are replaced by MEMBERSHIPS_MS_UPDATE and MEMBERSHIPS_LS_UPDATE notification types.
- Removed the deprecated `GOOGLE_ANALYTICS_TO_SPLIT` and `SPLIT_TO_GOOGLE_ANALYTICS` integrations.
- Bugfixing - Fixed an issue with the client `ready` method that was causing the returned promise to hang on async/await syntax if the promise was rejected. The fix implies a breaking change, since the promise rejection must be handled by the user.
- Removed internal ponyfills for `Map`, `Set` and `Array.from` global objects, dropping support for IE and other outdated browsers. The SDK now requires the runtime environment to support these features natively or to provide a polyfill.
- Bugfixing - Fixed an issue with the client `ready` method that was causing the returned promise to hang on async/await syntax if the promise was rejected. The fix implies a breaking change, since now the user must handle the promise rejection explicitly.

1.17.0 (September 6, 2024)
- Added `sync.requestOptions.getHeaderOverrides` configuration option to enhance SDK HTTP request Headers for Authorization Frameworks.
Expand Down
194 changes: 104 additions & 90 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@
"@typescript-eslint/parser": "^6.6.0",
"cross-env": "^7.0.2",
"eslint": "^8.48.0",
"eslint-plugin-compat": "^4.2.0",
"eslint-plugin-compat": "^6.0.1",
"eslint-plugin-import": "^2.25.3",
"fetch-mock": "^9.11.0",
"ioredis": "^4.28.0",
Expand Down
9 changes: 4 additions & 5 deletions src/evaluator/__tests__/evaluate-features.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
import { evaluateFeatures, evaluateFeaturesByFlagSets } from '../index';
import { EXCEPTION, NOT_IN_SPLIT, SPLIT_ARCHIVED, SPLIT_KILLED, SPLIT_NOT_FOUND } from '../../utils/labels';
import { loggerMock } from '../../logger/__tests__/sdkLogger.mock';
import { _Set } from '../../utils/lang/sets';
import { WARN_FLAGSET_WITHOUT_FLAGS } from '../../logger/constants';

const splitsMock = {
Expand All @@ -17,8 +16,8 @@ const splitsMock = {
};

const flagSetsMock = {
reg_and_config: new _Set(['regular', 'config']),
arch_and_killed: new _Set(['killed', 'archived']),
reg_and_config: new Set(['regular', 'config']),
arch_and_killed: new Set(['killed', 'archived']),
};

const mockStorage = {
Expand All @@ -38,7 +37,7 @@ const mockStorage = {
return splits;
},
getNamesByFlagSets(flagSets) {
return flagSets.map(flagset => flagSetsMock[flagset] || new _Set());
return flagSets.map(flagset => flagSetsMock[flagset] || new Set());
}
}
};
Expand Down Expand Up @@ -192,7 +191,7 @@ describe('EVALUATOR - Multiple evaluations at once by flag sets', () => {
// Should support async storage too
expect(await getResultsByFlagsets(['inexistent_set1', 'inexistent_set2'], {
splits: {
getNamesByFlagSets(flagSets) { return Promise.resolve(flagSets.map(flagset => flagSetsMock[flagset] || new _Set())); }
getNamesByFlagSets(flagSets) { return Promise.resolve(flagSets.map(flagset => flagSetsMock[flagset] || new Set())); }
}
})).toEqual({});
expect(loggerMock.warn.mock.calls).toEqual([
Expand Down
10 changes: 5 additions & 5 deletions src/evaluator/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { IStorageAsync, IStorageSync } from '../storages/types';
import { IEvaluationResult } from './types';
import { SplitIO } from '../types';
import { ILogger } from '../logger/types';
import { ISet, setToArray, returnSetsUnion, _Set } from '../utils/lang/sets';
import { returnSetsUnion } from '../utils/lang/sets';
import { WARN_FLAGSET_WITHOUT_FLAGS } from '../logger/constants';

const treatmentException = {
Expand Down Expand Up @@ -97,12 +97,12 @@ export function evaluateFeaturesByFlagSets(
storage: IStorageSync | IStorageAsync,
method: string,
): MaybeThenable<Record<string, IEvaluationResult>> {
let storedFlagNames: MaybeThenable<ISet<string>[]>;
let storedFlagNames: MaybeThenable<Set<string>[]>;

function evaluate(
featureFlagsByFlagSets: ISet<string>[],
featureFlagsByFlagSets: Set<string>[],
) {
let featureFlags = new _Set();
let featureFlags = new Set<string>();
for (let i = 0; i < flagSets.length; i++) {
const featureFlagByFlagSet = featureFlagsByFlagSets[i];
if (featureFlagByFlagSet.size) {
Expand All @@ -113,7 +113,7 @@ export function evaluateFeaturesByFlagSets(
}

return featureFlags.size ?
evaluateFeatures(log, key, setToArray(featureFlags), attributes, storage) :
evaluateFeatures(log, key, Array.from(featureFlags), attributes, storage) :
{};
}

Expand Down
3 changes: 1 addition & 2 deletions src/evaluator/matchers/semver_inlist.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import { _Set } from '../../utils/lang/sets';
import { Semver } from '../../utils/Semver';

export function inListSemverMatcherContext(ruleAttr: string[]) {
// @TODO ruleAttr validation should be done at the `parser` or `matchersTransform` level to reuse for all matchers
if (!ruleAttr || ruleAttr.length === 0) throw new Error('whitelistMatcherData is required for IN_LIST_SEMVER matcher type');

const listOfSemvers = new _Set(ruleAttr.map((version) => new Semver(version).version));
const listOfSemvers = new Set(ruleAttr.map((version) => new Semver(version).version));

return function inListSemverMatcher(runtimeAttr: string): boolean {
const runtimeSemver = new Semver(runtimeAttr).version;
Expand Down
4 changes: 1 addition & 3 deletions src/evaluator/matchers/whitelist.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import { _Set } from '../../utils/lang/sets';

export function whitelistMatcherContext(ruleAttr: string[]) {
const whitelistSet = new _Set(ruleAttr);
const whitelistSet = new Set(ruleAttr);

return function whitelistMatcher(runtimeAttr: string): boolean {
const isInWhitelist = whitelistSet.has(runtimeAttr);
Expand Down
3 changes: 1 addition & 2 deletions src/listeners/browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,6 @@ export class BrowserSignalListener implements ISignalListener {
* Returns true if beacon API was used successfully, false otherwise.
*/
private _sendBeacon(url: string, data: any, extraMetadata?: {}) {
// eslint-disable-next-line compat/compat
if (typeof navigator !== 'undefined' && navigator.sendBeacon) {
const json = {
entries: data,
Expand All @@ -130,7 +129,7 @@ export class BrowserSignalListener implements ISignalListener {
const payload = JSON.stringify(json);

// https://xgwang.me/posts/you-may-not-know-beacon/#it-may-throw-error%2C-be-sure-to-catch
try { // eslint-disable-next-line compat/compat
try {
return navigator.sendBeacon(url, payload);
} catch (e) {
return false;
Expand Down
3 changes: 1 addition & 2 deletions src/logger/__tests__/index.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { LogLevel } from '../../types';
import { _Map } from '../../utils/lang/maps';
import { Logger, LogLevels, isLogLevelString, _sprintf } from '../index';

// We'll set this only once. These are the constants we will use for
Expand Down Expand Up @@ -59,7 +58,7 @@ function testLogLevels(levelToTest: LogLevel) {
const logMethod = levelToTest.toLowerCase();
const logCategory = `test-category-${logMethod}`;
const instance = new Logger({ prefix: logCategory, showLevel },
useCodes ? new _Map([[1, 'Test log for level %s with showLevel: %s %s']]) : undefined);
useCodes ? new Map([[1, 'Test log for level %s with showLevel: %s %s']]) : undefined);

LOG_LEVELS_IN_ORDER.forEach((logLevel, i) => {
const logMsg = `Test log for level ${levelToTest} with showLevel: ${showLevel} ${logLevelLogsCounter}`;
Expand Down
3 changes: 1 addition & 2 deletions src/logger/browser/DebugLogger.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { Logger } from '../index';
import { codesDebug } from '../messages/debug';
import { _Map } from '../../utils/lang/maps';

export function DebugLogger() {
return new Logger({ logLevel: 'DEBUG' }, new _Map(codesDebug));
return new Logger({ logLevel: 'DEBUG' }, new Map(codesDebug));
}
3 changes: 1 addition & 2 deletions src/logger/browser/ErrorLogger.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { Logger } from '../index';
import { codesError } from '../messages/error';
import { _Map } from '../../utils/lang/maps';

export function ErrorLogger() {
return new Logger({ logLevel: 'ERROR' }, new _Map(codesError));
return new Logger({ logLevel: 'ERROR' }, new Map(codesError));
}
3 changes: 1 addition & 2 deletions src/logger/browser/InfoLogger.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { Logger } from '../index';
import { codesInfo } from '../messages/info';
import { _Map } from '../../utils/lang/maps';

export function InfoLogger() {
return new Logger({ logLevel: 'INFO' }, new _Map(codesInfo));
return new Logger({ logLevel: 'INFO' }, new Map(codesInfo));
}
3 changes: 1 addition & 2 deletions src/logger/browser/WarnLogger.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { Logger } from '../index';
import { codesWarn } from '../messages/warn';
import { _Map } from '../../utils/lang/maps';

export function WarnLogger() {
return new Logger({ logLevel: 'WARN' }, new _Map(codesWarn));
return new Logger({ logLevel: 'WARN' }, new Map(codesWarn));
}
7 changes: 3 additions & 4 deletions src/logger/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { objectAssign } from '../utils/lang/objectAssign';
import { ILoggerOptions, ILogger } from './types';
import { find, isObject } from '../utils/lang';
import { LogLevel } from '../types';
import { IMap, _Map } from '../utils/lang/maps';

export const LogLevels: { [level: string]: LogLevel } = {
DEBUG: 'DEBUG',
Expand Down Expand Up @@ -47,12 +46,12 @@ const defaultOptions = {
export class Logger implements ILogger {

private options: Required<ILoggerOptions>;
private codes: IMap<number, string>;
private codes: Map<number, string>;
private logLevel: number;

constructor(options?: ILoggerOptions, codes?: IMap<number, string>) {
constructor(options?: ILoggerOptions, codes?: Map<number, string>) {
this.options = objectAssign({}, defaultOptions, options);
this.codes = codes || new _Map();
this.codes = codes || new Map();
this.logLevel = LogLevelIndexes[this.options.logLevel];
}

Expand Down
3 changes: 1 addition & 2 deletions src/services/decorateHeaders.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { objectAssign } from '../utils/lang/objectAssign';
import { _Set } from '../utils/lang/sets';
import { ISettings } from '../types';

const FORBIDDEN_HEADERS = new _Set([
const FORBIDDEN_HEADERS = new Set([
'splitsdkclientkey',
'splitsdkversion',
'splitsdkmachineip',
Expand Down
3 changes: 1 addition & 2 deletions src/storages/AbstractSplitsCacheAsync.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { ISplitsCacheAsync } from './types';
import { ISplit } from '../dtos/types';
import { objectAssign } from '../utils/lang/objectAssign';
import { ISet } from '../utils/lang/sets';

/**
* This class provides a skeletal implementation of the ISplitsCacheAsync interface
Expand All @@ -18,7 +17,7 @@ export abstract class AbstractSplitsCacheAsync implements ISplitsCacheAsync {
abstract getChangeNumber(): Promise<number>
abstract getAll(): Promise<ISplit[]>
abstract getSplitNames(): Promise<string[]>
abstract getNamesByFlagSets(flagSets: string[]): Promise<ISet<string>[]>
abstract getNamesByFlagSets(flagSets: string[]): Promise<Set<string>[]>
abstract trafficTypeExists(trafficType: string): Promise<boolean>
abstract clear(): Promise<boolean | void>

Expand Down
3 changes: 1 addition & 2 deletions src/storages/AbstractSplitsCacheSync.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { ISplitsCacheSync } from './types';
import { ISplit } from '../dtos/types';
import { objectAssign } from '../utils/lang/objectAssign';
import { ISet } from '../utils/lang/sets';
import { IN_SEGMENT, IN_LARGE_SEGMENT } from '../utils/constants';

/**
Expand Down Expand Up @@ -80,7 +79,7 @@ export abstract class AbstractSplitsCacheSync implements ISplitsCacheSync {
return false;
}

abstract getNamesByFlagSets(flagSets: string[]): ISet<string>[]
abstract getNamesByFlagSets(flagSets: string[]): Set<string>[]

}

Expand Down
13 changes: 6 additions & 7 deletions src/storages/inLocalStorage/SplitsCacheInLocal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { isFiniteNumber, toNumber, isNaNNumber } from '../../utils/lang';
import { KeyBuilderCS } from '../KeyBuilderCS';
import { ILogger } from '../../logger/types';
import { LOG_PREFIX } from './constants';
import { ISet, _Set, setToArray } from '../../utils/lang/sets';
import { ISettings } from '../../types';
import { getStorageHash } from '../KeyBuilder';

Expand Down Expand Up @@ -259,12 +258,12 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync {
// if the filter didn't change, nothing is done
}

getNamesByFlagSets(flagSets: string[]): ISet<string>[] {
getNamesByFlagSets(flagSets: string[]): Set<string>[] {
return flagSets.map(flagSet => {
const flagSetKey = this.keys.buildFlagSetKey(flagSet);
const flagSetFromLocalStorage = localStorage.getItem(flagSetKey);

return new _Set(flagSetFromLocalStorage ? JSON.parse(flagSetFromLocalStorage) : []);
return new Set(flagSetFromLocalStorage ? JSON.parse(flagSetFromLocalStorage) : []);
});
}

Expand All @@ -279,10 +278,10 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync {

const flagSetFromLocalStorage = localStorage.getItem(flagSetKey);

const flagSetCache = new _Set(flagSetFromLocalStorage ? JSON.parse(flagSetFromLocalStorage) : []);
const flagSetCache = new Set(flagSetFromLocalStorage ? JSON.parse(flagSetFromLocalStorage) : []);
flagSetCache.add(featureFlag.name);

localStorage.setItem(flagSetKey, JSON.stringify(setToArray(flagSetCache)));
localStorage.setItem(flagSetKey, JSON.stringify(Array.from(flagSetCache)));
});
}

Expand All @@ -301,15 +300,15 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync {

if (!flagSetFromLocalStorage) return;

const flagSetCache = new _Set(JSON.parse(flagSetFromLocalStorage));
const flagSetCache = new Set(JSON.parse(flagSetFromLocalStorage));
flagSetCache.delete(featureFlagName);

if (flagSetCache.size === 0) {
localStorage.removeItem(flagSetKey);
return;
}

localStorage.setItem(flagSetKey, JSON.stringify(setToArray(flagSetCache)));
localStorage.setItem(flagSetKey, JSON.stringify(Array.from(flagSetCache)));
}

}
29 changes: 14 additions & 15 deletions src/storages/inLocalStorage/__tests__/SplitsCacheInLocal.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { SplitsCacheInLocal } from '../SplitsCacheInLocal';
import { KeyBuilderCS } from '../../KeyBuilderCS';
import { splitWithUserTT, splitWithAccountTT, splitWithAccountTTAndUsesSegments, something, somethingElse, featureFlagOne, featureFlagTwo, featureFlagThree, featureFlagWithEmptyFS, featureFlagWithoutFS } from '../../__tests__/testUtils';
import { ISplit } from '../../../dtos/types';
import { _Set } from '../../../utils/lang/sets';
import { fullSettings } from '../../../utils/settingsValidation/__tests__/settings.mocks';


Expand Down Expand Up @@ -174,7 +173,7 @@ test('SPLIT CACHE / LocalStorage / flag set cache tests', () => {
}
}
}, new KeyBuilderCS('SPLITIO', 'user'));
const emptySet = new _Set([]);
const emptySet = new Set([]);

cache.addSplits([
[featureFlagOne.name, featureFlagOne],
Expand All @@ -183,21 +182,21 @@ test('SPLIT CACHE / LocalStorage / flag set cache tests', () => {
]);
cache.addSplit(featureFlagWithEmptyFS.name, featureFlagWithEmptyFS);

expect(cache.getNamesByFlagSets(['o'])).toEqual([new _Set(['ff_one', 'ff_two'])]);
expect(cache.getNamesByFlagSets(['n'])).toEqual([new _Set(['ff_one'])]);
expect(cache.getNamesByFlagSets(['e'])).toEqual([new _Set(['ff_one', 'ff_three'])]);
expect(cache.getNamesByFlagSets(['o'])).toEqual([new Set(['ff_one', 'ff_two'])]);
expect(cache.getNamesByFlagSets(['n'])).toEqual([new Set(['ff_one'])]);
expect(cache.getNamesByFlagSets(['e'])).toEqual([new Set(['ff_one', 'ff_three'])]);
expect(cache.getNamesByFlagSets(['t'])).toEqual([emptySet]); // 't' not in filter
expect(cache.getNamesByFlagSets(['o', 'n', 'e'])).toEqual([new _Set(['ff_one', 'ff_two']), new _Set(['ff_one']), new _Set(['ff_one', 'ff_three'])]);
expect(cache.getNamesByFlagSets(['o', 'n', 'e'])).toEqual([new Set(['ff_one', 'ff_two']), new Set(['ff_one']), new Set(['ff_one', 'ff_three'])]);

cache.addSplit(featureFlagOne.name, { ...featureFlagOne, sets: ['1'] });

expect(cache.getNamesByFlagSets(['1'])).toEqual([emptySet]); // '1' not in filter
expect(cache.getNamesByFlagSets(['o'])).toEqual([new _Set(['ff_two'])]);
expect(cache.getNamesByFlagSets(['o'])).toEqual([new Set(['ff_two'])]);
expect(cache.getNamesByFlagSets(['n'])).toEqual([emptySet]);

cache.addSplit(featureFlagOne.name, { ...featureFlagOne, sets: ['x'] });
expect(cache.getNamesByFlagSets(['x'])).toEqual([new _Set(['ff_one'])]);
expect(cache.getNamesByFlagSets(['o', 'e', 'x'])).toEqual([new _Set(['ff_two']), new _Set(['ff_three']), new _Set(['ff_one'])]);
expect(cache.getNamesByFlagSets(['x'])).toEqual([new Set(['ff_one'])]);
expect(cache.getNamesByFlagSets(['o', 'e', 'x'])).toEqual([new Set(['ff_two']), new Set(['ff_three']), new Set(['ff_one'])]);


cache.removeSplit(featureFlagOne.name);
Expand All @@ -214,7 +213,7 @@ test('SPLIT CACHE / LocalStorage / flag set cache tests', () => {
// if FlagSets are not defined, it should store all FlagSets in memory.
test('SPLIT CACHE / LocalStorage / flag set cache tests without filters', () => {
const cacheWithoutFilters = new SplitsCacheInLocal(fullSettings, new KeyBuilderCS('SPLITIO', 'user'));
const emptySet = new _Set([]);
const emptySet = new Set([]);

cacheWithoutFilters.addSplits([
[featureFlagOne.name, featureFlagOne],
Expand All @@ -223,12 +222,12 @@ test('SPLIT CACHE / LocalStorage / flag set cache tests without filters', () =>
]);
cacheWithoutFilters.addSplit(featureFlagWithEmptyFS.name, featureFlagWithEmptyFS);

expect(cacheWithoutFilters.getNamesByFlagSets(['o'])).toEqual([new _Set(['ff_one', 'ff_two'])]);
expect(cacheWithoutFilters.getNamesByFlagSets(['n'])).toEqual([new _Set(['ff_one'])]);
expect(cacheWithoutFilters.getNamesByFlagSets(['e'])).toEqual([new _Set(['ff_one', 'ff_three'])]);
expect(cacheWithoutFilters.getNamesByFlagSets(['t'])).toEqual([new _Set(['ff_two', 'ff_three'])]);
expect(cacheWithoutFilters.getNamesByFlagSets(['o'])).toEqual([new Set(['ff_one', 'ff_two'])]);
expect(cacheWithoutFilters.getNamesByFlagSets(['n'])).toEqual([new Set(['ff_one'])]);
expect(cacheWithoutFilters.getNamesByFlagSets(['e'])).toEqual([new Set(['ff_one', 'ff_three'])]);
expect(cacheWithoutFilters.getNamesByFlagSets(['t'])).toEqual([new Set(['ff_two', 'ff_three'])]);
expect(cacheWithoutFilters.getNamesByFlagSets(['y'])).toEqual([emptySet]);
expect(cacheWithoutFilters.getNamesByFlagSets(['o', 'n', 'e'])).toEqual([new _Set(['ff_one', 'ff_two']), new _Set(['ff_one']), new _Set(['ff_one', 'ff_three'])]);
expect(cacheWithoutFilters.getNamesByFlagSets(['o', 'n', 'e'])).toEqual([new Set(['ff_one', 'ff_two']), new Set(['ff_one']), new Set(['ff_one', 'ff_three'])]);

// Validate that the feature flag cache is cleared when calling `clear` method
cacheWithoutFilters.clear();
Expand Down
Loading

0 comments on commit ddfae4a

Please sign in to comment.