Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Chores/loading status #169

Merged
merged 4 commits into from
Jun 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
96 changes: 69 additions & 27 deletions flagsmith-core.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,26 @@
import {
FlagSource,
GetValueOptions,
IDatadogRum,
IFlags,
IFlagsmith,
GetValueOptions,
IFlagsmithResponse,
IFlagsmithTrait,
IInitConfig,
IState,
ITraits,
IFlagsmithTrait,
LoadingState,
} from './types';
// @ts-ignore
import deepEqual from 'fast-deep-equal';

enum FlagSource {
"NONE" = "NONE",
"DEFAULT_FLAGS" = "DEFAULT_FLAGS",
"CACHE" = "CACHE",
"SERVER" = "SERVER",
}

export type LikeFetch = (input: Partial<RequestInfo>, init?: Partial<RequestInit>) => Promise<Partial<Response>>
let _fetch: LikeFetch;
type RequestOptions = {
Expand All @@ -30,8 +42,6 @@ let AsyncStorage: AsyncStorageType = null;
const FLAGSMITH_KEY = "BULLET_TRAIN_DB";
const FLAGSMITH_EVENT = "BULLET_TRAIN_EVENT";
const defaultAPI = 'https://edge.api.flagsmith.com/api/v1/';
// @ts-ignore
import deepEqual from 'fast-deep-equal';
let eventSource:typeof EventSource;
const initError = function (caller:string) {
return "Attempted to " + caller + " a user before calling flagsmith.init. Call flagsmith.init first, if you wish to prevent it sending a request for flags, call init with preventFetch:true."
Expand All @@ -44,6 +54,8 @@ const FLAGSMITH_FLAG_ANALYTICS_KEY = "flagsmith_enabled_";
const FLAGSMITH_TRAIT_ANALYTICS_KEY = "flagsmith_trait_";

const Flagsmith = class {
_trigger?:(()=>void)|null= null
_triggerLoadingState?:(()=>void)|null= null
timestamp: number|null = null
isLoading = false
eventSource:EventSource|null = null
Expand Down Expand Up @@ -119,6 +131,13 @@ const Flagsmith = class {
let resolved = false;
this.log("Get Flags")
this.isLoading = true;

if (!this.loadingState.isFetching) {
this.setLoadingState({
...this.loadingState,
isFetching: true
})
}
const handleResponse = ({ flags: features, traits }:IFlagsmithResponse) => {
this.isLoading = false;
if (identity) {
Expand Down Expand Up @@ -201,7 +220,7 @@ const Flagsmith = class {
isFromServer: true,
flagsChanged: !flagsEqual,
traitsChanged: !traitsEqual
});
}, this._loadedState(FlagSource.SERVER));
}
};

Expand Down Expand Up @@ -282,6 +301,7 @@ const Flagsmith = class {
};

datadogRum: IDatadogRum | null = null;
loadingState: LoadingState = {isLoading: true, isFetching: true, error: null, source: FlagSource.NONE}
canUseStorage = false
analyticsInterval: NodeJS.Timer | null= null
api: string|null= null
Expand All @@ -298,7 +318,6 @@ const Flagsmith = class {
oldFlags:IFlags|null= null
onChange:IInitConfig['onChange']|null= null
onError:IInitConfig['onError']|null = null
trigger?:(()=>void)|null= null
identity?: string|null= null
ticks: number|null= null
timer: number|null= null
Expand All @@ -325,11 +344,12 @@ const Flagsmith = class {
AsyncStorage: _AsyncStorage,
identity,
traits,
_trigger,
state,
cacheOptions,
angularHttpClient,
}: IInitConfig) {
_trigger,
_triggerLoadingStateChange,
}: IInitConfig) {

return new Promise((resolve, reject) => {
this.environmentID = environmentID;
Expand All @@ -338,17 +358,18 @@ const Flagsmith = class {
this.getFlagInterval = null;
this.analyticsInterval = null;

this.onChange = (previousFlags, params) => {
this.onChange = (previousFlags, params, loadingState) => {
this.setLoadingState(loadingState)
if(onChange) {
onChange(previousFlags, params)
onChange(previousFlags, params, this.loadingState)
}
if(this.trigger) {
if(this._trigger) {
this.log("trigger called")
this.trigger()
this._trigger()
}
}

this.trigger = _trigger || this.trigger;
this._trigger = _trigger || this._trigger;
this.onError = onError? (message:any)=> {
if (message instanceof Error) {
onError(message)
Expand All @@ -370,6 +391,14 @@ const Flagsmith = class {
this.flags = Object.assign({}, defaultFlags) || {};
this.initialised = true;
this.ticks = 10000;
if(Object.keys(this.flags).length){
//Flags have been passed as part of SSR / default flags, update state silently for initial render
this.loadingState = {
...this.loadingState,
isLoading: false,
source: FlagSource.DEFAULT_FLAGS
}
}
if (realtime && typeof window !== 'undefined') {
const connectionUrl = eventSourceUrl + "sse/environments/" + environmentID + "/stream";
if(!eventSource) {
Expand Down Expand Up @@ -559,16 +588,17 @@ const Flagsmith = class {
}

if (this.flags) { // retrieved flags from local storage
if (this.onChange) {
this.log("onChange called")
this.onChange(null, { isFromServer: false, flagsChanged: true, traitsChanged: !!this.traits });
}
const shouldFetchFlags = !preventFetch && (!this.cacheOptions.skipAPI||!cachePopulated)
this.onChange?.(null,
{ isFromServer: false, flagsChanged: true, traitsChanged: !!this.traits },
this._loadedState(FlagSource.CACHE, shouldFetchFlags)
);
this.oldFlags = this.flags;
resolve(true);
if (this.cacheOptions.skipAPI && cachePopulated) {
this.log("Skipping API, using cache")
}
if (!preventFetch && (!this.cacheOptions.skipAPI||!cachePopulated)) {
if (shouldFetchFlags) {
this.getFlags();
}
} else {
Expand All @@ -586,10 +616,10 @@ const Flagsmith = class {
this.getFlags(resolve, reject)
} else {
if (defaultFlags) {
if (this.onChange) {
this.log("onChange called")
this.onChange(null, { isFromServer: false, flagsChanged: true, traitsChanged: !!this.traits });
}
this.onChange?.(null,
{ isFromServer: false, flagsChanged: true, traitsChanged: !!this.traits },
this._loadedState(FlagSource.DEFAULT_FLAGS)
);
}
resolve(true);
}
Expand All @@ -601,10 +631,7 @@ const Flagsmith = class {
this.getFlags(resolve, reject);
} else {
if (defaultFlags) {
if (this.onChange) {
this.log("onChange called")
this.onChange(null, { isFromServer: false, flagsChanged: true, traitsChanged:!!this.traits });
}
this.onChange?.(null, { isFromServer: false, flagsChanged: true, traitsChanged:!!this.traits },this._loadedState(FlagSource.CACHE));
}
resolve(true);
}
Expand All @@ -615,6 +642,15 @@ const Flagsmith = class {
});
}

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

getAllFlags() {
return this.flags;
}
Expand Down Expand Up @@ -682,7 +718,13 @@ const Flagsmith = class {
AsyncStorage!.setItem(FLAGSMITH_EVENT, events);
}
}

setLoadingState(loadingState:LoadingState) {
if(!deepEqual(loadingState,this.loadingState)) {
this.loadingState = { ...loadingState };
this.log("Loading state changed", loadingState)
this._triggerLoadingState?.()
}
}
logout() {
this.identity = null;
this.traits = null;
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": "3.18.4",
"version": "3.19.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
1 change: 1 addition & 0 deletions lib/flagsmith-es/src/readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
This folder contains auto-generated sourcemaps.
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": "3.18.4",
"version": "3.19.0",
"description": "Feature flagging to support continuous development",
"main": "./index.js",
"repository": {
Expand Down
1 change: 1 addition & 0 deletions lib/flagsmith/src/readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
This folder contains auto-generated sourcemaps.
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": "3.18.4",
"version": "3.19.0",
"description": "Feature flagging to support continuous development",
"main": "./index.js",
"repository": {
Expand Down
1 change: 1 addition & 0 deletions lib/react-native-flagsmith/src/readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
This folder contains auto-generated sourcemaps.
29 changes: 27 additions & 2 deletions react.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,19 @@ export const FlagsmithProvider: FC<FlagsmithContextType> = ({
flagsmith, options, serverState, children,
}) => {
const firstRenderRef = useRef(true)
if (flagsmith && !flagsmith?.trigger) {
flagsmith.trigger = ()=>{
if (flagsmith && !flagsmith?._trigger) {
flagsmith._trigger = ()=>{
flagsmith.log("React - trigger event received")
events.emit('event');
}
}

if (flagsmith && !flagsmith?._triggerLoadingState) {
flagsmith._triggerLoadingState = ()=>{
events.emit('loading_event');
}
}

if (serverState && !flagsmith.initialised) {
flagsmith.setState(serverState)
}
Expand Down Expand Up @@ -89,6 +95,25 @@ const getRenderKey = (flagsmith: IFlagsmith, flags: string[], traits: string[] =
.join(',')
}

export function useFlagsmithLoading() {
const flagsmith = useContext(FlagsmithContext)
const [loadingState, setLoadingState] = useState(flagsmith?.loadingState);
const eventListener = useCallback(()=>{
setLoadingState(flagsmith?.loadingState)
},[flagsmith])
const eventRef = useRef(false);
if(!eventRef.current) {
eventRef.current = true;
events.on('loading_event', eventListener)
}
useEffect(()=>{
return () => {
events.off('loading_event', eventListener)
}
}, [])
return loadingState
}

export function useFlags<F extends string=string, T extends string=string>(_flags: readonly F[], _traits: readonly T[] = []): {
[K in F]: IFlagsmithFeature
} & {
Expand Down
28 changes: 26 additions & 2 deletions types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,19 @@ export declare type IDatadogRum = {
}
}

export declare enum FlagSource {
"NONE" = "NONE",
"DEFAULT_FLAGS" = "DEFAULT_FLAGS",
"CACHE" = "CACHE",
"SERVER" = "SERVER",
}

export declare type LoadingState = {
error: Error | null, // Current error, resets on next attempt to fetch flags
isFetching: bool, // Whether there is a current request to fetch server flags
isLoading: bool, // Whether any flag data exists
source: FlagSource //Indicates freshness of flags
}
export interface IInitConfig<F extends string = string, T extends string = string> {
AsyncStorage?: any;
api?: string;
Expand All @@ -67,11 +80,12 @@ export interface IInitConfig<F extends string = string, T extends string = strin
headers?: object;
identity?: string;
traits?: ITraits<T>;
onChange?: (previousFlags: IFlags<F> | null, params: IRetrieveInfo) => void;
onChange?: (previousFlags: IFlags<F> | null, params: IRetrieveInfo, loadingState:LoadingState) => void;
onError?: (err: Error) => void;
preventFetch?: boolean;
state?: IState;
_trigger?: () => void;
_triggerLoadingStateChange?: () => void;
}

export interface IFlagsmithResponse {
Expand Down Expand Up @@ -160,10 +174,20 @@ export interface IFlagsmith<F extends string = string, T extends string = string
* Whether the flagsmith SDK is initialised
*/
initialised?: boolean;

/**
* Returns ths current loading state
*/
loadingState?: LoadingState;

/**
* Used internally, this function will callback separately to onChange whenever flags are updated
*/
trigger?: () => void;
_trigger?: () => void;
/**
* Used internally, this function will trigger the useFlagsmithLoading hook when loading state changes
*/
_triggerLoadingState?: () => void;
/**
* Used internally, this function will console log if enableLogs is being set within flagsmith.init
*/
Expand Down