Skip to content

Commit

Permalink
Merge pull request #252 from Flagsmith/feat/support-multiple-environm…
Browse files Browse the repository at this point in the history
…ent-caching

feat: support multiple environment caching
  • Loading branch information
kyle-ssg authored Oct 9, 2024
2 parents 3d0e3b2 + f4eaca3 commit 8951a32
Show file tree
Hide file tree
Showing 9 changed files with 104 additions and 68 deletions.
56 changes: 32 additions & 24 deletions flagsmith-core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,9 @@ type RequestOptions = {
}

let AsyncStorage: AsyncStorageType = null;
const FLAGSMITH_KEY = "BULLET_TRAIN_DB";
const FLAGSMITH_EVENT = "BULLET_TRAIN_EVENT";
const DEFAULT_FLAGSMITH_KEY = "FLAGSMITH_DB";
const DEFAULT_FLAGSMITH_EVENT = "FLAGSMITH_EVENT";
let FlagsmithEvent = DEFAULT_FLAGSMITH_EVENT;
const defaultAPI = 'https://edge.api.flagsmith.com/api/v1/';
let eventSource: typeof EventSource;
const initError = function(caller: string) {
Expand Down Expand Up @@ -80,7 +81,7 @@ const Flagsmith = class {
}

getFlags = () => {
let { api, evaluationContext } = this;
const { api, evaluationContext } = this;
this.log("Get Flags")
this.isLoading = true;

Expand Down Expand Up @@ -271,7 +272,7 @@ const Flagsmith = class {
timer: number|null= null
dtrum= null
withTraits?: ITraits|null= null
cacheOptions = {ttl:0, skipAPI: false, loadStale: false}
cacheOptions = {ttl:0, skipAPI: false, loadStale: false, storageKey: undefined as string|undefined}
async init(config: IInitConfig) {
const evaluationContext = toEvaluationContext(config.evaluationContext || {});
try {
Expand All @@ -290,7 +291,7 @@ const Flagsmith = class {
enableDynatrace,
enableAnalytics,
realtime,
eventSourceUrl= "https://realtime.flagsmith.com/",
eventSourceUrl= "https://realtime.flagsmith.com/",
AsyncStorage: _AsyncStorage,
identity,
traits,
Expand Down Expand Up @@ -331,7 +332,7 @@ const Flagsmith = class {
onError?.(message);
};
this.enableLogs = enableLogs || false;
this.cacheOptions = cacheOptions ? { skipAPI: !!cacheOptions.skipAPI, ttl: cacheOptions.ttl || 0, loadStale: !!cacheOptions.loadStale } : this.cacheOptions;
this.cacheOptions = cacheOptions ? { skipAPI: !!cacheOptions.skipAPI, ttl: cacheOptions.ttl || 0, storageKey:cacheOptions.storageKey, loadStale: !!cacheOptions.loadStale } : this.cacheOptions;
if (!this.cacheOptions.ttl && this.cacheOptions.skipAPI) {
console.warn("Flagsmith: you have set a cache ttl of 0 and are skipping API calls, this means the API will not be hit unless you clear local storage.")
}
Expand All @@ -345,6 +346,9 @@ const Flagsmith = class {
this.ticks = 10000;
this.timer = this.enableLogs ? new Date().valueOf() : null;
this.cacheFlags = typeof AsyncStorage !== 'undefined' && !!cacheFlags;

FlagsmithEvent = DEFAULT_FLAGSMITH_EVENT + "_" + evaluationContext.environment.apiKey;

if (_AsyncStorage) {
AsyncStorage = _AsyncStorage;
}
Expand Down Expand Up @@ -381,7 +385,7 @@ const Flagsmith = class {
}

if (AsyncStorage && this.canUseStorage) {
AsyncStorage.getItem(FLAGSMITH_EVENT)
AsyncStorage.getItem(FlagsmithEvent)
.then((res)=>{
try {
this.evaluationEvent = JSON.parse(res!) || {}
Expand All @@ -398,12 +402,12 @@ const Flagsmith = class {
}

if (AsyncStorage && this.canUseStorage) {
AsyncStorage.getItem(FLAGSMITH_EVENT, (err, res) => {
AsyncStorage.getItem(FlagsmithEvent, (err, res) => {
if (res && this.evaluationContext.environment) {
const json = JSON.parse(res);
if (json[this.evaluationContext.environment.apiKey]) {
const state = this.getState();
this.log("Retrieved events from cache", res);
const state = this.getState();
this.log("Retrieved events from cache", res);
this.setState({
...state,
evaluationEvent: json[this.evaluationContext.environment.apiKey],
Expand Down Expand Up @@ -453,7 +457,7 @@ const Flagsmith = class {
...json,
evaluationContext: toEvaluationContext({
...json.evaluationContext,
identity: !!json.evaluationContext?.identity ? {
identity: json.evaluationContext?.identity ? {
...json.evaluationContext?.identity,
traits: {
...json.evaluationContext?.identity?.traits || {},
Expand Down Expand Up @@ -510,7 +514,7 @@ const Flagsmith = class {
}
};
try {
const res = AsyncStorage.getItemSync? AsyncStorage.getItemSync(FLAGSMITH_KEY) : await AsyncStorage.getItem(FLAGSMITH_KEY);
const res = AsyncStorage.getItemSync? AsyncStorage.getItemSync(this.getStorageKey()) : await AsyncStorage.getItem(this.getStorageKey());
await onRetrievedStorage(null, res)
} catch (e) {}
}
Expand Down Expand Up @@ -538,15 +542,6 @@ const Flagsmith = class {
}
}

private _loadedState(error: any = null, source: FlagSource, isFetching = false) {
return {
error,
isFetching,
isLoading: false,
source
}
}

getAllFlags() {
return this.flags;
}
Expand Down Expand Up @@ -660,7 +655,7 @@ const Flagsmith = class {
}

setContext = (clientEvaluationContext: ClientEvaluationContext) => {
let evaluationContext = toEvaluationContext(clientEvaluationContext);
const evaluationContext = toEvaluationContext(clientEvaluationContext);
this.evaluationContext = {
...evaluationContext,
environment: evaluationContext.environment || this.evaluationContext.environment,
Expand Down Expand Up @@ -745,6 +740,19 @@ const Flagsmith = class {
return res;
};

private _loadedState(error: any = null, source: FlagSource, isFetching = false) {
return {
error,
isFetching,
isLoading: false,
source
}
}

private getStorageKey = ()=> {
return this.cacheOptions?.storageKey || DEFAULT_FLAGSMITH_KEY + "_" + this.evaluationContext.environment?.apiKey
}

private log(...args: (unknown)[]) {
if (this.enableLogs) {
console.log.apply(this, ['FLAGSMITH:', new Date().valueOf() - (this.timer || 0), 'ms', ...args]);
Expand All @@ -756,7 +764,7 @@ const Flagsmith = class {
this.ts = new Date().valueOf();
const state = JSON.stringify(this.getState());
this.log('Setting storage', state);
AsyncStorage!.setItem(FLAGSMITH_KEY, state);
AsyncStorage!.setItem(this.getStorageKey(), state);
}
}

Expand Down Expand Up @@ -820,7 +828,7 @@ const Flagsmith = class {
private updateEventStorage() {
if (this.enableAnalytics) {
const events = JSON.stringify(this.getState().evaluationEvent);
AsyncStorage!.setItem(FLAGSMITH_EVENT, events);
AsyncStorage!.setItem(FlagsmithEvent, events);
}
}

Expand Down
2 changes: 1 addition & 1 deletion lib/flagsmith-es/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "flagsmith-es",
"version": "5.0.0",
"version": "6.0.0",
"description": "Feature flagging to support continuous development. This is an esm equivalent of the standard flagsmith npm module.",
"main": "./index.js",
"type": "module",
Expand Down
2 changes: 1 addition & 1 deletion lib/flagsmith/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "flagsmith",
"version": "5.0.0",
"version": "6.0.0",
"description": "Feature flagging to support continuous development",
"main": "./index.js",
"repository": {
Expand Down
2 changes: 1 addition & 1 deletion lib/react-native-flagsmith/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "react-native-flagsmith",
"version": "5.0.0",
"version": "6.0.0",
"description": "Feature flagging to support continuous development",
"main": "./index.js",
"repository": {
Expand Down
67 changes: 32 additions & 35 deletions test/cache.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Sample test
import {
defaultState,
defaultStateAlt,
FLAGSMITH_KEY,
getFlagsmith,
getStateToCheck,
identityState,
Expand Down Expand Up @@ -33,7 +33,21 @@ describe('Cache', () => {
onChange,
});
await flagsmith.init(initConfig);
const cache = await AsyncStorage.getItem('BULLET_TRAIN_DB');
const cache = await AsyncStorage.getItem(FLAGSMITH_KEY);
expect(getStateToCheck(JSON.parse(`${cache}`))).toEqual(defaultState);
});
test('should set cache after init with custom key', async () => {
const onChange = jest.fn();
const customKey = 'custom_key';
const { flagsmith, initConfig, AsyncStorage, mockFetch } = getFlagsmith({
cacheFlags: true,
cacheOptions: {
storageKey: customKey,
},
onChange,
});
await flagsmith.init(initConfig);
const cache = await AsyncStorage.getItem(customKey);
expect(getStateToCheck(JSON.parse(`${cache}`))).toEqual(defaultState);
});
test('should call onChange with cache then eventually with an API response', async () => {
Expand All @@ -53,7 +67,7 @@ describe('Cache', () => {
cacheFlags: true,
onChange,
});
await AsyncStorage.setItem('BULLET_TRAIN_DB', JSON.stringify(defaultStateAlt));
await AsyncStorage.setItem(FLAGSMITH_KEY, JSON.stringify(defaultStateAlt));
await flagsmith.init(initConfig);

// Flags retrieved from cache
Expand Down Expand Up @@ -86,7 +100,7 @@ describe('Cache', () => {
identity: testIdentity,
onChange,
});
await AsyncStorage.setItem('BULLET_TRAIN_DB', JSON.stringify({
await AsyncStorage.setItem(FLAGSMITH_KEY, JSON.stringify({
...defaultStateAlt,
identity: 'bad_identity',
}));
Expand All @@ -102,7 +116,7 @@ describe('Cache', () => {
onChange,
cacheOptions: { ttl: 1 },
});
await AsyncStorage.setItem('BULLET_TRAIN_DB', JSON.stringify({
await AsyncStorage.setItem(FLAGSMITH_KEY, JSON.stringify({
...defaultStateAlt,
ts: new Date().valueOf() - 100,
}));
Expand All @@ -120,7 +134,7 @@ describe('Cache', () => {
onChange,
cacheOptions: { ttl: 1, loadStale: true },
});
await AsyncStorage.setItem('BULLET_TRAIN_DB', JSON.stringify({
await AsyncStorage.setItem(FLAGSMITH_KEY, JSON.stringify({
...defaultStateAlt,
ts: new Date().valueOf() - 100,
}));
Expand All @@ -138,7 +152,7 @@ describe('Cache', () => {
onChange,
cacheOptions: { ttl: 1000 },
});
await AsyncStorage.setItem('BULLET_TRAIN_DB', JSON.stringify({
await AsyncStorage.setItem(FLAGSMITH_KEY, JSON.stringify({
...defaultStateAlt,
ts: new Date().valueOf(),
}));
Expand All @@ -155,7 +169,7 @@ describe('Cache', () => {
cacheFlags: false,
onChange,
});
await AsyncStorage.setItem('BULLET_TRAIN_DB', JSON.stringify({
await AsyncStorage.setItem(FLAGSMITH_KEY, JSON.stringify({
...defaultStateAlt,
ts: new Date().valueOf(),
}));
Expand All @@ -173,25 +187,7 @@ describe('Cache', () => {
onChange,
cacheOptions: { ttl: 1000, skipAPI: true },
});
await AsyncStorage.setItem('BULLET_TRAIN_DB', JSON.stringify({
...defaultStateAlt,
ts: new Date().valueOf(),
}));
await flagsmith.init(initConfig);
expect(onChange).toHaveBeenCalledTimes(1);
expect(mockFetch).toHaveBeenCalledTimes(0);
expect(getStateToCheck(flagsmith.getState())).toEqual({
...defaultStateAlt,
});
});
test('should not get flags from API when skipAPI is set', async () => {
const onChange = jest.fn();
const { flagsmith, initConfig, AsyncStorage, mockFetch } = getFlagsmith({
cacheFlags: true,
onChange,
cacheOptions: { ttl: 1000, skipAPI: true },
});
await AsyncStorage.setItem('BULLET_TRAIN_DB', JSON.stringify({
await AsyncStorage.setItem(FLAGSMITH_KEY, JSON.stringify({
...defaultStateAlt,
ts: new Date().valueOf(),
}));
Expand All @@ -209,7 +205,7 @@ describe('Cache', () => {
onChange,
cacheOptions: { ttl: 1, skipAPI: true, loadStale: true },
});
await AsyncStorage.setItem('BULLET_TRAIN_DB', JSON.stringify({
await AsyncStorage.setItem(FLAGSMITH_KEY, JSON.stringify({
...defaultStateAlt,
ts: new Date().valueOf() - 100,
}));
Expand All @@ -220,14 +216,15 @@ describe('Cache', () => {
...defaultStateAlt,
});
});

test('should validate flags are unchanged when fetched', async () => {
const onChange = jest.fn();
const { flagsmith, initConfig, AsyncStorage, mockFetch } = getFlagsmith({
onChange,
cacheFlags: true,
preventFetch: true,
});
await AsyncStorage.setItem('BULLET_TRAIN_DB', JSON.stringify({
await AsyncStorage.setItem(FLAGSMITH_KEY, JSON.stringify({
...defaultState,
}));
await flagsmith.init(initConfig);
Expand Down Expand Up @@ -273,7 +270,7 @@ describe('Cache', () => {
preventFetch: true,
defaultFlags: defaultState.flags,
});
await AsyncStorage.setItem('BULLET_TRAIN_DB', JSON.stringify({
await AsyncStorage.setItem(FLAGSMITH_KEY, JSON.stringify({
...defaultState,
}));
await flagsmith.init(initConfig);
Expand Down Expand Up @@ -319,7 +316,7 @@ describe('Cache', () => {
preventFetch: true,
});
const storage = new SyncStorageMock();
await storage.setItem('BULLET_TRAIN_DB', JSON.stringify({
await storage.setItem(FLAGSMITH_KEY, JSON.stringify({
...defaultState,
}));
flagsmith.init({
Expand All @@ -345,7 +342,7 @@ describe('Cache', () => {
preventFetch: true,
});
const storage = new SyncStorageMock();
await storage.setItem('BULLET_TRAIN_DB', JSON.stringify({
await storage.setItem(FLAGSMITH_KEY, JSON.stringify({
...identityState,
}));
const ts = Date.now();
Expand All @@ -356,8 +353,8 @@ describe('Cache', () => {
});
expect(flagsmith.getAllTraits()).toEqual({
...identityState.traits,
ts
})
ts,
});
});
test('should cache transient traits correctly', async () => {
const onChange = jest.fn();
Expand Down Expand Up @@ -395,4 +392,4 @@ describe('Cache', () => {
},
})
});
});
});
4 changes: 2 additions & 2 deletions test/default-flags.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// Sample test
import { defaultState, defaultStateAlt, getFlagsmith, getStateToCheck } from './test-constants';
import { defaultState, defaultStateAlt, FLAGSMITH_KEY, getFlagsmith, getStateToCheck } from './test-constants';
import { IFlags } from '../types';

describe('Default Flags', () => {
Expand Down Expand Up @@ -51,7 +51,7 @@ describe('Default Flags', () => {
cacheFlags: true,
defaultFlags: {...defaultFlags, ...itemsToRemove},
});
await AsyncStorage.setItem('BULLET_TRAIN_DB', JSON.stringify({
await AsyncStorage.setItem(FLAGSMITH_KEY, JSON.stringify({
...defaultState,
flags: {
...defaultFlags,
Expand Down
Loading

0 comments on commit 8951a32

Please sign in to comment.