4.1 KB138 lines
Blame
1/**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 */
7
8import {atom, useAtomValue} from 'jotai';
9import {loadable} from 'jotai/utils';
10import {randomId} from 'shared/utils';
11import serverAPI from './ClientToServerAPI';
12import {Internal} from './Internal';
13import {atomFamilyWeak, readAtom} from './jotaiUtils';
14
15const bulkFetchedFlagsAtom = atom<Promise<Record<string, boolean>>>(() => {
16 if (Internal.featureFlags == null) {
17 return Promise.resolve({});
18 }
19 const knownFlags: Array<string> = Object.values(Internal.featureFlags ?? {});
20 return bulkFetchFeatureFlags(knownFlags);
21});
22
23/**
24 * Boolean values to enable features via remote config.
25 * TODO: we could cache values in localstorage to avoid async lookup time if you've previously fetched it
26 */
27export const featureFlagAsync = atomFamilyWeak((name?: string) => {
28 if (name == null) {
29 // OSS doesn't have access to feature flags, so they are always "false" by setting the name to null
30 return atom(Promise.resolve(false));
31 }
32
33 const knownFlags: Array<string> = Object.values(Internal.featureFlags ?? {});
34 if (knownFlags.includes(name)) {
35 return atom(get => get(bulkFetchedFlagsAtom).then(flags => flags[name]));
36 }
37
38 return atom(fetchFeatureFlag(name));
39});
40
41export const qeFlagAsync = atomFamilyWeak((name?: string) => {
42 if (name == null) {
43 // OSS doesn't have access to feature flags, so they are always "false" by setting the name to null
44 return atom(Promise.resolve(false));
45 }
46 return atom(fetchQeFlag(name));
47});
48
49export const featureFlagLoadable = atomFamilyWeak((name?: string) => {
50 return loadable(featureFlagAsync(name));
51});
52
53/** Access featureFlag state without suspending or throwing */
54export function useFeatureFlagSync(name: string | undefined) {
55 const flag = useAtomValue(featureFlagLoadable(name));
56 return flag.state === 'hasData' ? flag.data : false;
57}
58
59/** Access featureFlag, suspending if not yet loaded */
60export function useFeatureFlagAsync(name: string | undefined) {
61 const flag = useAtomValue(featureFlagAsync(name));
62 return flag;
63}
64
65export function getFeatureFlag(name: string | undefined, default_?: boolean): Promise<boolean> {
66 if (name == null) {
67 return Promise.resolve(default_ ?? false);
68 }
69 return readAtom(featureFlagAsync(name));
70}
71
72export function getQeFlag(name: string | undefined, default_?: boolean): Promise<boolean> {
73 if (name == null) {
74 return Promise.resolve(default_ ?? false);
75 }
76 return readAtom(qeFlagAsync(name));
77}
78
79async function fetchFeatureFlag(name: string | undefined, default_?: boolean): Promise<boolean> {
80 if (name == null) {
81 return default_ ?? false;
82 }
83 serverAPI.postMessage({
84 type: 'fetchFeatureFlag',
85 name,
86 });
87 const response = await serverAPI.nextMessageMatching(
88 'fetchedFeatureFlag',
89 message => message.name === name,
90 );
91 return response.passes;
92}
93
94async function fetchQeFlag(name: string | undefined, default_?: boolean): Promise<boolean> {
95 if (name == null) {
96 return default_ ?? false;
97 }
98 serverAPI.postMessage({
99 type: 'fetchQeFlag',
100 name,
101 });
102 const response = await serverAPI.nextMessageMatching(
103 'fetchedQeFlag',
104 message => message.name === name,
105 );
106 return response.passes;
107}
108
109let featureFlagOverrides: Record<string, boolean> | undefined = undefined;
110export const __TEST__ = {
111 overrideFeatureFlag: (name: string, value: boolean) => {
112 featureFlagOverrides ??= {};
113 featureFlagOverrides[name] = value;
114 },
115 clearFeatureFlagOverrides: () => {
116 featureFlagOverrides = undefined;
117 },
118};
119
120export async function bulkFetchFeatureFlags(
121 names: Array<string>,
122): Promise<Record<string, boolean>> {
123 if (featureFlagOverrides) {
124 return Object.fromEntries(names.map(name => [name, featureFlagOverrides?.[name] ?? false]));
125 }
126 const id = randomId();
127 serverAPI.postMessage({
128 type: 'bulkFetchFeatureFlags',
129 names,
130 id,
131 });
132 const response = await serverAPI.nextMessageMatching(
133 'bulkFetchedFeatureFlags',
134 message => message.id === id,
135 );
136 return response.result;
137}
138