Added feature flags system (#803)

* added feature flag system

* Update packages/web/src/core/services/featureFlags.ts

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* remove process.env

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Dan Ditomaso
2025-08-22 13:47:24 -04:00
committed by GitHub
co-authored by Copilot
parent 1de92cd2e9
commit 449fb3ac36
5 changed files with 138 additions and 3 deletions
@@ -0,0 +1,22 @@
import {
type FlagKey,
type Flags,
featureFlags,
} from "@core/services/featureFlags.ts";
import * as React from "react";
export function useFeatureFlags(): Flags {
return React.useSyncExternalStore(
(cb) => featureFlags.subscribe(cb),
() => featureFlags.all(),
() => featureFlags.all(),
);
}
export function useFeatureFlag(key: FlagKey): boolean {
return React.useSyncExternalStore(
(cb) => featureFlags.subscribe(cb),
() => featureFlags.get(key),
() => featureFlags.get(key),
);
}
@@ -0,0 +1,10 @@
import { featureFlags } from "@core/services/featureFlags";
const isDev = typeof import.meta !== "undefined" && import.meta.env?.DEV;
console.log(`Dev mode: ${isDev}`);
if (isDev) {
featureFlags.setOverrides({
persistNodeDB: true,
});
}
@@ -0,0 +1,89 @@
import { z } from "zod";
/** Map feature keys -> env var names (Vite exposes only VITE_*). */
export const FLAG_ENV = {
persistNodeDB: "VITE_PERSIST_NODE_DB",
persistMessages: "VITE_PERSIST_MESSAGES",
} as const;
export type FlagKey = keyof typeof FLAG_ENV;
export type Flags = Record<FlagKey, boolean>;
type Listener = () => void;
const present = z
.string()
.optional()
.transform((v) => v !== undefined);
const mutableShape: Record<string, z.ZodTypeAny> = {};
for (const envName of Object.values(FLAG_ENV)) {
mutableShape[envName] = present;
}
const EnvSchema = z.object(mutableShape);
class FeatureFlags {
private base: Flags;
private overrides: Partial<Flags> = {};
private listeners = new Set<Listener>();
constructor(base: Flags) {
this.base = base;
}
get(key: FlagKey): boolean {
return this.overrides[key] ?? this.base[key];
}
/** Get all flags */
all(): Flags {
return { ...this.base, ...this.overrides };
}
/** Optional dev/test override. Pass null to clear. */
setOverride(key: FlagKey, val: boolean | null) {
if (val === null) {
delete this.overrides[key];
} else {
this.overrides[key] = val;
}
this.emit();
}
/** Set many at once */
setOverrides(partial: Partial<Flags>) {
for (const [k, v] of Object.entries(partial)) {
this.setOverride(k as FlagKey, v as boolean);
if (v === null) {
delete this.overrides[k as FlagKey];
} else {
this.overrides[k as FlagKey] = v as boolean;
}
}
this.emit();
}
subscribe(fn: Listener) {
this.listeners.add(fn);
return () => this.listeners.delete(fn);
}
private emit() {
for (const listener of this.listeners) {
listener();
}
}
}
export function createFeatureFlags(env: Record<string, unknown>): FeatureFlags {
const parsed = EnvSchema.parse(env);
const base = Object.fromEntries(
(Object.keys(FLAG_ENV) as FlagKey[]).map((k) => [
k,
parsed[FLAG_ENV[k]] as boolean,
]),
) as Flags;
return new FeatureFlags(base);
}
export const featureFlags = createFeatureFlags(import.meta.env);