addons/isl/src/featureFlags.tsblame
View source
b69ab311/**
b69ab312 * Copyright (c) Meta Platforms, Inc. and affiliates.
b69ab313 *
b69ab314 * This source code is licensed under the MIT license found in the
b69ab315 * LICENSE file in the root directory of this source tree.
b69ab316 */
b69ab317
b69ab318import {atom, useAtomValue} from 'jotai';
b69ab319import {loadable} from 'jotai/utils';
b69ab3110import {randomId} from 'shared/utils';
b69ab3111import serverAPI from './ClientToServerAPI';
b69ab3112import {Internal} from './Internal';
b69ab3113import {atomFamilyWeak, readAtom} from './jotaiUtils';
b69ab3114
b69ab3115const bulkFetchedFlagsAtom = atom<Promise<Record<string, boolean>>>(() => {
b69ab3116 if (Internal.featureFlags == null) {
b69ab3117 return Promise.resolve({});
b69ab3118 }
b69ab3119 const knownFlags: Array<string> = Object.values(Internal.featureFlags ?? {});
b69ab3120 return bulkFetchFeatureFlags(knownFlags);
b69ab3121});
b69ab3122
b69ab3123/**
b69ab3124 * Boolean values to enable features via remote config.
b69ab3125 * TODO: we could cache values in localstorage to avoid async lookup time if you've previously fetched it
b69ab3126 */
b69ab3127export const featureFlagAsync = atomFamilyWeak((name?: string) => {
b69ab3128 if (name == null) {
b69ab3129 // OSS doesn't have access to feature flags, so they are always "false" by setting the name to null
b69ab3130 return atom(Promise.resolve(false));
b69ab3131 }
b69ab3132
b69ab3133 const knownFlags: Array<string> = Object.values(Internal.featureFlags ?? {});
b69ab3134 if (knownFlags.includes(name)) {
b69ab3135 return atom(get => get(bulkFetchedFlagsAtom).then(flags => flags[name]));
b69ab3136 }
b69ab3137
b69ab3138 return atom(fetchFeatureFlag(name));
b69ab3139});
b69ab3140
b69ab3141export const qeFlagAsync = atomFamilyWeak((name?: string) => {
b69ab3142 if (name == null) {
b69ab3143 // OSS doesn't have access to feature flags, so they are always "false" by setting the name to null
b69ab3144 return atom(Promise.resolve(false));
b69ab3145 }
b69ab3146 return atom(fetchQeFlag(name));
b69ab3147});
b69ab3148
b69ab3149export const featureFlagLoadable = atomFamilyWeak((name?: string) => {
b69ab3150 return loadable(featureFlagAsync(name));
b69ab3151});
b69ab3152
b69ab3153/** Access featureFlag state without suspending or throwing */
b69ab3154export function useFeatureFlagSync(name: string | undefined) {
b69ab3155 const flag = useAtomValue(featureFlagLoadable(name));
b69ab3156 return flag.state === 'hasData' ? flag.data : false;
b69ab3157}
b69ab3158
b69ab3159/** Access featureFlag, suspending if not yet loaded */
b69ab3160export function useFeatureFlagAsync(name: string | undefined) {
b69ab3161 const flag = useAtomValue(featureFlagAsync(name));
b69ab3162 return flag;
b69ab3163}
b69ab3164
b69ab3165export function getFeatureFlag(name: string | undefined, default_?: boolean): Promise<boolean> {
b69ab3166 if (name == null) {
b69ab3167 return Promise.resolve(default_ ?? false);
b69ab3168 }
b69ab3169 return readAtom(featureFlagAsync(name));
b69ab3170}
b69ab3171
b69ab3172export function getQeFlag(name: string | undefined, default_?: boolean): Promise<boolean> {
b69ab3173 if (name == null) {
b69ab3174 return Promise.resolve(default_ ?? false);
b69ab3175 }
b69ab3176 return readAtom(qeFlagAsync(name));
b69ab3177}
b69ab3178
b69ab3179async function fetchFeatureFlag(name: string | undefined, default_?: boolean): Promise<boolean> {
b69ab3180 if (name == null) {
b69ab3181 return default_ ?? false;
b69ab3182 }
b69ab3183 serverAPI.postMessage({
b69ab3184 type: 'fetchFeatureFlag',
b69ab3185 name,
b69ab3186 });
b69ab3187 const response = await serverAPI.nextMessageMatching(
b69ab3188 'fetchedFeatureFlag',
b69ab3189 message => message.name === name,
b69ab3190 );
b69ab3191 return response.passes;
b69ab3192}
b69ab3193
b69ab3194async function fetchQeFlag(name: string | undefined, default_?: boolean): Promise<boolean> {
b69ab3195 if (name == null) {
b69ab3196 return default_ ?? false;
b69ab3197 }
b69ab3198 serverAPI.postMessage({
b69ab3199 type: 'fetchQeFlag',
b69ab31100 name,
b69ab31101 });
b69ab31102 const response = await serverAPI.nextMessageMatching(
b69ab31103 'fetchedQeFlag',
b69ab31104 message => message.name === name,
b69ab31105 );
b69ab31106 return response.passes;
b69ab31107}
b69ab31108
b69ab31109let featureFlagOverrides: Record<string, boolean> | undefined = undefined;
b69ab31110export const __TEST__ = {
b69ab31111 overrideFeatureFlag: (name: string, value: boolean) => {
b69ab31112 featureFlagOverrides ??= {};
b69ab31113 featureFlagOverrides[name] = value;
b69ab31114 },
b69ab31115 clearFeatureFlagOverrides: () => {
b69ab31116 featureFlagOverrides = undefined;
b69ab31117 },
b69ab31118};
b69ab31119
b69ab31120export async function bulkFetchFeatureFlags(
b69ab31121 names: Array<string>,
b69ab31122): Promise<Record<string, boolean>> {
b69ab31123 if (featureFlagOverrides) {
b69ab31124 return Object.fromEntries(names.map(name => [name, featureFlagOverrides?.[name] ?? false]));
b69ab31125 }
b69ab31126 const id = randomId();
b69ab31127 serverAPI.postMessage({
b69ab31128 type: 'bulkFetchFeatureFlags',
b69ab31129 names,
b69ab31130 id,
b69ab31131 });
b69ab31132 const response = await serverAPI.nextMessageMatching(
b69ab31133 'bulkFetchedFeatureFlags',
b69ab31134 message => message.id === id,
b69ab31135 );
b69ab31136 return response.result;
b69ab31137}