-
Notifications
You must be signed in to change notification settings - Fork 19
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
Feat(core): add feature flag caching #21
base: main
Are you sure you want to change the base?
Changes from all commits
8df2e44
d73a0ad
2f7f0e3
55627e6
443cc08
c3e075b
1e0d542
0033e0d
5de80ab
008d37d
2ef57e4
7d72fe5
013f01b
e086c29
6f354e6
c37e887
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -20,7 +20,7 @@ export type ABConfig<T extends string = string> = { | |||||
|
||||||
type Settings< | ||||||
FlagName extends string, | ||||||
Flags extends Record<FlagName, FlagValueString> = Record<FlagName, FlagValueString> | ||||||
Flags extends Record<FlagName, FlagValueString> = Record<FlagName, FlagValueString>, | ||||||
> = { | ||||||
flags?: { | ||||||
defaultValues?: { | ||||||
|
@@ -48,10 +48,15 @@ interface PersistentStorage { | |||||
set: (key: string, value: string) => void; | ||||||
} | ||||||
|
||||||
type FlagCacheConfig = { | ||||||
refetchFlags: boolean; | ||||||
timeToLiveInMinutes: number; | ||||||
}; | ||||||
|
||||||
export type AbbyConfig< | ||||||
FlagName extends string = string, | ||||||
Tests extends Record<string, ABConfig> = Record<string, ABConfig>, | ||||||
Flags extends Record<FlagName, FlagValueString> = Record<FlagName, FlagValueString> | ||||||
Flags extends Record<FlagName, FlagValueString> = Record<FlagName, FlagValueString>, | ||||||
> = { | ||||||
projectId: string; | ||||||
apiUrl?: string; | ||||||
|
@@ -60,17 +65,25 @@ export type AbbyConfig< | |||||
flags?: Flags; | ||||||
settings?: Settings<F.NoInfer<FlagName>, Flags>; | ||||||
debug?: boolean; | ||||||
flagCacheConfig?: FlagCacheConfig; | ||||||
}; | ||||||
|
||||||
export class Abby< | ||||||
FlagName extends string, | ||||||
TestName extends string, | ||||||
Tests extends Record<string, ABConfig>, | ||||||
Flags extends Record<FlagName, FlagValueString> | ||||||
Flags extends Record<FlagName, FlagValueString>, | ||||||
> { | ||||||
private log = (...args: any[]) => | ||||||
this.config.debug ? console.log(`core.Abby`, ...args) : () => {}; | ||||||
|
||||||
private testDevtoolOverrides: Map<keyof Tests, Tests[keyof Tests]["variants"][number]> = | ||||||
new Map(); | ||||||
|
||||||
private flagDevtoolOverrides: Map<FlagName, boolean> = new Map(); | ||||||
|
||||||
#flagTimeoutMap: Map<string, Date> = new Map(); | ||||||
|
||||||
#data: LocalData<FlagName, TestName> = { | ||||||
tests: {} as any, | ||||||
flags: {} as any, | ||||||
|
@@ -92,13 +105,20 @@ export class Abby< | |||||
private persistantFlagStorage?: PersistentStorage | ||||||
) { | ||||||
this._cfg = config as AbbyConfig<FlagName, Tests, Flags>; | ||||||
this.#data.flags = Object.keys(config.flags ?? {}).reduce((acc, flagName) => { | ||||||
acc[flagName as FlagName] = this.getDefaultFlagValue( | ||||||
flagName as FlagName, | ||||||
config.flags as any | ||||||
); | ||||||
return acc; | ||||||
}, {} as Record<FlagName, FlagValue>); | ||||||
this.#data.flags = Object.keys(config.flags ?? {}).reduce( | ||||||
(acc, flagName) => { | ||||||
acc[flagName as FlagName] = this.getDefaultFlagValue( | ||||||
flagName as FlagName, | ||||||
config.flags as any | ||||||
); | ||||||
const validUntil = new Date( | ||||||
new Date().getTime() + 1000 * 60 * (this.config.flagCacheConfig?.timeToLiveInMinutes ?? 1) | ||||||
); // flagdefault timeout is 1 minute | ||||||
this.#flagTimeoutMap.set(flagName, validUntil); | ||||||
return acc; | ||||||
}, | ||||||
{} as Record<FlagName, FlagValue> | ||||||
); | ||||||
this.#data.tests = config.tests ?? ({} as any); | ||||||
} | ||||||
|
||||||
|
@@ -139,22 +159,28 @@ export class Abby< | |||||
data: AbbyDataResponse | ||||||
): LocalData<FlagName, TestName> { | ||||||
return { | ||||||
tests: data.tests.reduce((acc, { name, weights }) => { | ||||||
if (!acc[name as keyof Tests]) { | ||||||
tests: data.tests.reduce( | ||||||
(acc, { name, weights }) => { | ||||||
if (!acc[name as keyof Tests]) { | ||||||
return acc; | ||||||
} | ||||||
|
||||||
// assigned the fetched weights to the initial config | ||||||
acc[name as keyof Tests] = { | ||||||
...acc[name as keyof Tests], | ||||||
weights, | ||||||
}; | ||||||
return acc; | ||||||
} | ||||||
|
||||||
// assigned the fetched weights to the initial config | ||||||
acc[name as keyof Tests] = { | ||||||
...acc[name as keyof Tests], | ||||||
weights, | ||||||
}; | ||||||
return acc; | ||||||
}, (this.config.tests ?? {}) as any), | ||||||
flags: data.flags.reduce((acc, { name, value }) => { | ||||||
acc[name] = value; | ||||||
return acc; | ||||||
}, {} as Record<string, FlagValue>), | ||||||
}, | ||||||
(this.config.tests ?? {}) as any | ||||||
), | ||||||
flags: data.flags.reduce( | ||||||
(acc, { name, value }) => { | ||||||
acc[name] = value; | ||||||
return acc; | ||||||
}, | ||||||
{} as Record<string, FlagValue> | ||||||
), | ||||||
}; | ||||||
} | ||||||
|
||||||
|
@@ -201,6 +227,39 @@ export class Abby< | |||||
return this.getProjectData(); | ||||||
} | ||||||
|
||||||
/** | ||||||
* Helper function to retrieve the time a flag is valid | ||||||
* @param key | ||||||
* @returns | ||||||
*/ | ||||||
getFeatureFlagTimeout<F extends FlagName>(key: F) { | ||||||
return this.#flagTimeoutMap.get(key); | ||||||
} | ||||||
|
||||||
/** | ||||||
* Helper function to check if a featureflag should be refetched | ||||||
* @param key name of the featureflag | ||||||
* @returns value of flag | ||||||
*/ | ||||||
getValidFlag<F extends FlagName>(key: F, refetch: boolean | undefined) { | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this may break every call in every lib. if this is not intended, maybe do |
||||||
if (!refetch) return this.#data.flags[key]; | ||||||
const flagTime = this.#flagTimeoutMap.get(key); | ||||||
if (!flagTime) return this.#data.flags[key]; | ||||||
|
||||||
const now = new Date(); | ||||||
if (flagTime.getTime() <= now.getTime()) { | ||||||
this.refetchFlags(); | ||||||
} | ||||||
return this.#data.flags[key]; | ||||||
} | ||||||
|
||||||
/** | ||||||
* helper function to make testing easier | ||||||
*/ | ||||||
refetchFlags() { | ||||||
this.loadProjectData(); | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. loadProjectData() has return type void. |
||||||
} | ||||||
|
||||||
/** | ||||||
* Function to get the value of a feature flag. This includes | ||||||
* the overrides from the dev tools and the local overrides if in development mode | ||||||
|
@@ -213,8 +272,6 @@ export class Abby< | |||||
): FlagValueStringToType<CurrentFlag> { | ||||||
this.log(`getFeatureFlag()`, key); | ||||||
|
||||||
const storedValue = this.#data.flags[key as unknown as FlagName]; | ||||||
|
||||||
const localOverride = this.flagOverrides?.get(key as unknown as FlagName); | ||||||
|
||||||
if (localOverride != null) { | ||||||
|
@@ -234,6 +291,10 @@ export class Abby< | |||||
return devOverride; | ||||||
} | ||||||
} | ||||||
const storedValue = this.getValidFlag( | ||||||
key as unknown as FlagName, | ||||||
this.config.flagCacheConfig?.refetchFlags | ||||||
); | ||||||
|
||||||
const flagType = this._cfg.flags?.[key]; | ||||||
const defaultValue = this._cfg.settings?.flags?.defaultValues?.[flagType!]; | ||||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
guess this shouldn't be "just booleans" anymore, right?
the fix should be another PR :)