-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
8 changed files
with
337 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
import { useEffect, useState } from 'react'; | ||
import { useService } from './use-service'; | ||
import RemoteConfigService, { | ||
RemoteConfig, | ||
RemoteConfigKey, | ||
} from '~/services/remote-config'; | ||
|
||
export function useRemoteConfig<K extends RemoteConfigKey>( | ||
key: K | ||
): RemoteConfig[K] { | ||
const remoteConfigService = useService(RemoteConfigService); | ||
const [value, setValue] = useState(() => | ||
remoteConfigService.getConfigValue(key) | ||
); | ||
|
||
useEffect(() => { | ||
setValue(remoteConfigService.getConfigValue(key)); | ||
|
||
const unsubscribe = remoteConfigService.subscribe(key, (newValue) => | ||
setValue(newValue) | ||
); | ||
|
||
return () => { | ||
unsubscribe(); | ||
}; | ||
}, [key, remoteConfigService]); | ||
|
||
return value; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,137 @@ | ||
import remoteConfig from '@react-native-firebase/remote-config'; | ||
import { BehaviorSubject } from 'rxjs'; | ||
import { singleton } from 'tsyringe'; | ||
import { Duration } from '~/utils/Duration'; | ||
|
||
export type RemoteConfig = { | ||
some_feature_flag: boolean; | ||
}; | ||
|
||
export type RemoteConfigValue = boolean | number | string; | ||
|
||
export type RemoteConfigKey = keyof RemoteConfig; | ||
|
||
export type RemoteConfigSubjects = { | ||
[K in RemoteConfigKey]: BehaviorSubject<RemoteConfig[K]>; | ||
}; | ||
|
||
export type RemoteConfigSubscribeCallback<K extends RemoteConfigKey> = ( | ||
value: RemoteConfig[K] | ||
) => void; | ||
|
||
export type RemoteConfigUnsubscribe = () => void; | ||
|
||
export type RemoteConfigSettings = { | ||
timeout: Duration; | ||
cache: Duration; | ||
}; | ||
|
||
@singleton() | ||
export default class RemoteConfigService { | ||
private static readonly DEFAULTS: RemoteConfig = { | ||
some_feature_flag: true, | ||
}; | ||
|
||
private static readonly SETTINGS: RemoteConfigSettings = { | ||
timeout: Duration.from('seconds', 30), | ||
cache: Duration.from('minutes', 10), | ||
}; | ||
|
||
private readonly configs: RemoteConfigSubjects = { | ||
some_feature_flag: new BehaviorSubject( | ||
RemoteConfigService.DEFAULTS.some_feature_flag | ||
), | ||
}; | ||
|
||
constructor() { | ||
this.init(); | ||
} | ||
|
||
private removeOnConfigUpdated = remoteConfig().onConfigUpdated(() => { | ||
this.updateConfigs(); | ||
}); | ||
|
||
private async init() { | ||
await this.setConfigSettings(); | ||
await this.setDefaults(); | ||
await this.fetchAndActivate(); | ||
this.updateConfigs(); | ||
} | ||
|
||
private fetchAndActivate() { | ||
return remoteConfig().fetchAndActivate(); | ||
} | ||
|
||
private setConfigSettings() { | ||
return remoteConfig().setConfigSettings({ | ||
minimumFetchIntervalMillis: | ||
RemoteConfigService.SETTINGS.cache.to('milliseconds'), | ||
fetchTimeMillis: RemoteConfigService.SETTINGS.timeout.to('milliseconds'), | ||
}); | ||
} | ||
|
||
private setDefaults() { | ||
return remoteConfig().setDefaults(RemoteConfigService.DEFAULTS); | ||
} | ||
|
||
private async updateConfigs() { | ||
Object.keys(RemoteConfigService.DEFAULTS).forEach((key) => { | ||
this.updateConfig(key as RemoteConfigKey); | ||
}); | ||
} | ||
|
||
private updateConfig(key: RemoteConfigKey) { | ||
const $config: BehaviorSubject<any> = this.configs[key]; | ||
const value = this.getRemoteValue(key); | ||
|
||
if ($config.getValue() !== value) { | ||
$config.next(value); | ||
} | ||
} | ||
|
||
private getRemoteValue<K extends RemoteConfigKey>(key: K): RemoteConfig[K] { | ||
switch (typeof RemoteConfigService.DEFAULTS[key]) { | ||
case 'boolean': | ||
return this.getRemoteBoolean(key) as any; | ||
|
||
case 'number': | ||
return this.getRemoteNumber(key) as any; | ||
|
||
case 'string': | ||
return this.getRemoteString(key) as any; | ||
} | ||
} | ||
|
||
private getRemoteBoolean(key: RemoteConfigKey) { | ||
return remoteConfig().getValue(key).asBoolean(); | ||
} | ||
|
||
private getRemoteNumber(key: RemoteConfigKey) { | ||
return remoteConfig().getValue(key).asNumber(); | ||
} | ||
|
||
private getRemoteString(key: RemoteConfigKey) { | ||
return remoteConfig().getValue(key).asString(); | ||
} | ||
|
||
getConfigValue<K extends RemoteConfigKey>(key: K): RemoteConfig[K] { | ||
return this.configs[key].getValue(); | ||
} | ||
|
||
subscribe<K extends RemoteConfigKey>( | ||
key: K, | ||
callback: RemoteConfigSubscribeCallback<K> | ||
): RemoteConfigUnsubscribe { | ||
const subscription = this.configs[key].subscribe((value) => { | ||
callback(value); | ||
}); | ||
|
||
return () => { | ||
subscription.unsubscribe(); | ||
}; | ||
} | ||
|
||
destroy() { | ||
this.removeOnConfigUpdated(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,135 @@ | ||
export type DurationUnit = | ||
| 'nanoseconds' | ||
| 'microseconds' | ||
| 'milliseconds' | ||
| 'seconds' | ||
| 'minutes' | ||
| 'hours' | ||
| 'days'; | ||
|
||
export type DurationComponents = Partial<Record<DurationUnit, number>>; | ||
|
||
const NANOSECONDS = 1; | ||
const MICROSECONDS = NANOSECONDS * 1000; | ||
const MILLISECONDS = MICROSECONDS * 1000; | ||
const SECONDS = MILLISECONDS * 1000; | ||
const MINUTES = SECONDS * 60; | ||
const HOURS = MINUTES * 60; | ||
const DAYS = HOURS * 24; | ||
|
||
const MULTIPLIERS: Record<DurationUnit, number> = { | ||
nanoseconds: NANOSECONDS, | ||
microseconds: MICROSECONDS, | ||
milliseconds: MILLISECONDS, | ||
seconds: SECONDS, | ||
minutes: MINUTES, | ||
hours: HOURS, | ||
days: DAYS, | ||
}; | ||
|
||
const UNITS: DurationUnit[] = [ | ||
'nanoseconds', | ||
'microseconds', | ||
'milliseconds', | ||
'seconds', | ||
'minutes', | ||
'hours', | ||
'days', | ||
]; | ||
|
||
export class Duration { | ||
private constructor(private readonly _nanoseconds: number) {} | ||
|
||
static from(components: DurationComponents): Duration; | ||
static from(unit: DurationUnit, value: number): Duration; | ||
static from( | ||
componentsOrUnit: DurationComponents | DurationUnit, | ||
value?: number | ||
): Duration { | ||
if (typeof componentsOrUnit === 'object') { | ||
return Duration.fromComponents(componentsOrUnit); | ||
} | ||
|
||
return Duration.fromUnit(componentsOrUnit, value!); | ||
} | ||
|
||
private static fromComponents(components: DurationComponents) { | ||
let nanoseconds = 0; | ||
|
||
for (const unit of UNITS) { | ||
const multiplier = MULTIPLIERS[unit]; | ||
const value = components[unit] ?? 0; | ||
nanoseconds += value * multiplier; | ||
} | ||
|
||
return new Duration(nanoseconds); | ||
} | ||
|
||
private static fromUnit(unit: DurationUnit, value: number) { | ||
return new Duration(value * MULTIPLIERS[unit]); | ||
} | ||
|
||
to(unit: DurationUnit) { | ||
return this._nanoseconds / MULTIPLIERS[unit]; | ||
} | ||
|
||
add(duration: Duration): Duration; | ||
add(components: DurationComponents): Duration; | ||
add(unit: DurationUnit, value: number): Duration; | ||
add( | ||
durationOrcomponentsOrUnit: Duration | DurationComponents | DurationUnit, | ||
value?: number | ||
): Duration { | ||
if (durationOrcomponentsOrUnit instanceof Duration) { | ||
return this.addDuration(durationOrcomponentsOrUnit); | ||
} | ||
|
||
if (typeof durationOrcomponentsOrUnit === 'object') { | ||
return this.addComponents(durationOrcomponentsOrUnit); | ||
} | ||
|
||
return this.addUnit(durationOrcomponentsOrUnit, value!); | ||
} | ||
|
||
private addDuration(duration: Duration) { | ||
return new Duration(this._nanoseconds + duration._nanoseconds); | ||
} | ||
|
||
private addComponents(components: DurationComponents) { | ||
return this.addDuration(Duration.fromComponents(components)); | ||
} | ||
|
||
private addUnit(unit: DurationUnit, value: number) { | ||
return new Duration(this._nanoseconds + value * MULTIPLIERS[unit]); | ||
} | ||
|
||
sub(duration: Duration): Duration; | ||
sub(components: DurationComponents): Duration; | ||
sub(unit: DurationUnit, value: number): Duration; | ||
sub( | ||
durationOrComponentsOrUnit: Duration | DurationComponents | DurationUnit, | ||
value?: number | ||
): Duration { | ||
if (durationOrComponentsOrUnit instanceof Duration) { | ||
return this.subDuration(durationOrComponentsOrUnit); | ||
} | ||
|
||
if (typeof durationOrComponentsOrUnit === 'object') { | ||
return this.subComponents(durationOrComponentsOrUnit); | ||
} | ||
|
||
return this.subUnit(durationOrComponentsOrUnit, value!); | ||
} | ||
|
||
private subDuration(duration: Duration) { | ||
return new Duration(this._nanoseconds - duration._nanoseconds); | ||
} | ||
|
||
private subComponents(components: DurationComponents) { | ||
return this.subDuration(Duration.fromComponents(components)); | ||
} | ||
|
||
private subUnit(unit: DurationUnit, value: number) { | ||
return new Duration(this._nanoseconds - value * MULTIPLIERS[unit]); | ||
} | ||
} |
Oops, something went wrong.