Browse Source
* added feature flag system * Update packages/web/src/core/services/featureFlags.ts Co-authored-by: Copilot <[email protected]> * remove process.env --------- Co-authored-by: Copilot <[email protected]>pull/804/head
committed by
GitHub
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); |
||||
Loading…
Reference in new issue