///
import React from "react";
import { Flags, FlagScalar, GetValueFromKeyPath, GetValueFromKeyPathString, KeyPath, KeyPathString } from "./types";
import { Backend } from "./backends";
const MISSING_CONTEXT = Symbol();
const NOOP = () => null;
const isFlagScalar = (value: any): value is FlagScalar => {
return typeof value === "string" || typeof value === "number" || typeof value === "boolean";
};
export const createFlags = () => {
type B = Backend;
type ProviderProps = React.PropsWithChildren<{
backend: B;
}>;
type KeyPathStringFlagProps> = {
keyPath: K;
defaultValue: GetValueFromKeyPathString;
render(value: GetValueFromKeyPathString): React.ReactNode;
fallback?(): React.ReactNode;
};
type KeyPathFlagProps> = {
keyPath: KP;
defaultValue: GetValueFromKeyPath;
render(value: GetValueFromKeyPath): React.ReactNode;
fallback?(): React.ReactNode;
};
const calleeStr = (keyPath: string[], defaultValue: any, format: "hook" | "component") => () => {
const keyPathStr = JSON.stringify(keyPath);
const defaultValueStr = JSON.stringify(defaultValue);
return format == "hook"
? `useFlag(${keyPathStr}, ${defaultValueStr})`
: ``;
};
const Context = React.createContext(MISSING_CONTEXT);
Context.displayName = "Flag";
const FlagBackendProvider: React.FC = ({ backend, children }) => {
return {children};
};
FlagBackendProvider.displayName = "FlagBackendProvider";
const internalUseFlag = (keyPath: string | string[], defaultValue: any, displayCallee: () => string) => {
const keyPath_ = (Array.isArray(keyPath) ? keyPath : keyPath.split(".")) as KeyPath;
if (defaultValue === undefined) {
throw new Error(`Calling \`${displayCallee()}\` requires that you provide a default value that matches the type of the flag.`);
}
const expectedType = typeof defaultValue;
const backend = React.useContext(Context);
if (backend === MISSING_CONTEXT) {
if (process.env.NODE_ENV !== "development") {
return defaultValue;
}
throw new Error(`Calling \`${displayCallee()}\` requires that the application is wrapped in a \`\``);
}
const ext = backend.toExternalStore(keyPath_, defaultValue);
let result = React.useSyncExternalStore(ext.subscribe, ext.getSnapshot, ext.getServerSnapshot);
if ((result === undefined || result === null) && process.env.NODE_ENV === "development") {
console.warn(`\`${displayCallee()}\` does not return anything from backend "${backend.name}".`);
}
result ??= defaultValue;
if (!isFlagScalar(result)) {
throw new Error(
`Calling \`${displayCallee()}\` requires that the result is a boolean, number or string. Instead returned ${JSON.stringify(
result
)}.`
);
}
if (typeof result !== expectedType) {
if (process.env.NODE_ENV === "development") {
console.warn(
`Expected result of \`${displayCallee()}\` to be a ${expectedType} (based on the default value of ${JSON.stringify(
defaultValue
)}). Instead returned ${JSON.stringify(result)}. Falling back to default value.`
);
}
return defaultValue;
}
return result;
};
function Flag>(props: KeyPathStringFlagProps): JSX.Element;
function Flag>(props: KeyPathFlagProps): JSX.Element;
function Flag({ keyPath, defaultValue, render, fallback }: any): JSX.Element {
fallback ??= NOOP;
const flag = internalUseFlag(keyPath, defaultValue, calleeStr(keyPath, defaultValue, "component"));
return flag === false ? fallback() : render(flag);
}
Flag.displayName = "Flag";
function useFlag>(keyPath: K, defaultValue: GetValueFromKeyPathString): GetValueFromKeyPathString;
function useFlag>(keyPath: KP, defaultValue: GetValueFromKeyPath): GetValueFromKeyPath;
function useFlag(keyPath: any, defaultValue: any) {
React.useDebugValue(keyPath);
return internalUseFlag(keyPath, defaultValue, calleeStr(keyPath, defaultValue, "hook"));
}
return {
FlagBackendProvider,
Flag,
useFlag,
};
};