import type { Decision, EvalContext, Provider } from "./types";
/**
* FeatureFlagService is the framework-level Toggle Router. UI code asks the
* Service for decisions; the Service consults its configured {@link Provider}.
*
* The class is intentionally side-effect free. Mounting it inside a React
* tree is handled by `./context.tsx`; the Service itself works outside of
* React (unit tests, web workers, Node CLI tools, ...).
*
* Always-on safety: every public entry point returns the caller's default
* when no provider matches. Business code never has to guard against a
* missing flag.
*/
export class FeatureFlagService {
private provider: Provider | null;
constructor(provider: Provider | null = null) {
this.provider = provider;
}
/**
* Swap the underlying provider at runtime. Useful when fresh config
* arrives from the backend; the React provider tree re-renders
* automatically because the consumer hooks subscribe to the wrapper.
*/
setProvider(provider: Provider | null): void {
this.provider = provider;
}
/**
* Returns true when the named flag evaluates to an "on" state. When the
* flag is unknown the caller's default is returned.
*
* @example
* if (flags.isEnabled("billing_new_invoice_email", { userId }, false)) {
* return ;
* }
* return ;
*/
isEnabled(key: string, ctx: EvalContext, defaultValue: boolean): boolean {
return this.decision(key, ctx, defaultValue).enabled;
}
/**
* Returns the raw variant for a multi-arm flag, falling back to
* `defaultValue` when nothing matches.
*/
variant(key: string, ctx: EvalContext, defaultValue: string): string {
if (!this.provider) {
return defaultValue;
}
const d = this.provider.lookup(key, ctx);
if (!d) return defaultValue;
return d.variant;
}
/**
* Full structured decision. Used by diagnostic overlays and tests.
*/
decision(key: string, ctx: EvalContext, defaultValue: boolean): Decision {
if (!this.provider) {
return defaultDecision(key, defaultValue);
}
const d = this.provider.lookup(key, ctx);
if (!d) return defaultDecision(key, defaultValue);
return { ...d, key };
}
/** Returns the wrapped provider (read-only) for diagnostics. */
getProvider(): Provider | null {
return this.provider;
}
}
function defaultDecision(key: string, value: boolean): Decision {
return {
key,
enabled: value,
variant: value ? "on" : "off",
reason: "default",
source: "default",
};
}