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:
@@ -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);
|
||||
Reference in New Issue
Block a user