Files
gridpilot.gg/apps/website/lib/feature/FeatureFlagService.ts
2026-01-07 22:05:53 +01:00

116 lines
3.2 KiB
TypeScript

/**
* FeatureFlagService - Manages feature flags for both server and client
*
* API-Driven Integration:
* Fetches feature flags from the API endpoint GET /features
* Returns empty flags on error (secure by default)
*
* Server: Fetches from API at ${NEXT_PUBLIC_API_BASE_URL || 'http://localhost:3001'}/features
* Client: Reads from session context or provides mock implementation
*/
// Server-side implementation
export class FeatureFlagService {
private flags: Set<string>;
constructor(flags?: string[]) {
if (flags) {
this.flags = new Set(flags);
} else {
// Parse from environment variable (fallback for backward compatibility)
const flagsEnv = process.env.FEATURE_FLAGS;
this.flags = flagsEnv
? new Set(flagsEnv.split(',').map(f => f.trim()))
: new Set();
}
}
/**
* Check if a feature flag is enabled
*/
isEnabled(flag: string): boolean {
return this.flags.has(flag);
}
/**
* Get all enabled flags
*/
getEnabledFlags(): string[] {
return Array.from(this.flags);
}
/**
* Factory method to create service by fetching from API
* Fetches from ${NEXT_PUBLIC_API_BASE_URL || 'http://localhost:3001'}/features
* On error, returns empty flags (secure by default)
*/
static async fromAPI(): Promise<FeatureFlagService> {
const baseUrl = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:3001';
const url = `${baseUrl}/features`;
try {
// Use next: { revalidate: 0 } for Next.js server runtime
// This is equivalent to cache: 'no-store' but is the preferred Next.js convention
const response = await fetch(url, {
next: { revalidate: 0 },
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
// Parse JSON { features: Record<string, string> }
// Enable flags whose value is 'enabled'
const enabledFlags: string[] = [];
if (data.features && typeof data.features === 'object') {
Object.entries(data.features).forEach(([flag, value]) => {
if (value === 'enabled') {
enabledFlags.push(flag);
}
});
}
return new FeatureFlagService(enabledFlags);
} catch (error) {
// Log error but return empty flags (secure by default)
console.error('Failed to fetch feature flags from API:', error);
return new FeatureFlagService([]);
}
}
}
// Client-side context interface
export interface FeatureFlagContextType {
isEnabled: (flag: string) => boolean;
getEnabledFlags: () => string[];
}
// Mock implementation for client-side when no context is available
export class MockFeatureFlagService implements FeatureFlagContextType {
private flags: Set<string>;
constructor(flags: string[] = []) {
this.flags = new Set(flags);
}
isEnabled(flag: string): boolean {
return this.flags.has(flag);
}
getEnabledFlags(): string[] {
return Array.from(this.flags);
}
}
// Default mock instance for client-side usage
// Enables all features for development/demo mode
export const mockFeatureFlags = new MockFeatureFlagService([
'driver_profiles',
'team_profiles',
'wallets',
'sponsors',
'team_feature',
'alpha_features'
]);