/** * Browser/React Native-safe feature flag client for platform-service. * * Polls GET /api/flags/poll on a configurable interval and caches results. * No Node.js dependencies — uses globalThis.fetch. * * @example * ```ts * import { createFeatureFlagClient } from '@bytelyst/feature-flag-client'; * * const flags = createFeatureFlagClient({ * baseUrl: 'http://localhost:4003/api', * productId: 'nomgap', * platform: 'mobile', * }); * * await flags.init({ userId: 'user-123' }); * if (flags.isEnabled('premium_body_viz')) { ... } * ``` */ import type { FeatureFlagClient, FeatureFlagClientConfig } from './types.js'; export function createFeatureFlagClient(config: FeatureFlagClientConfig): FeatureFlagClient { const { baseUrl, productId, platform, pollIntervalMs = 5 * 60 * 1000, storage, storagePrefix, } = config; const prefix = storagePrefix ?? productId; const STORAGE_KEY = `${prefix}-feature-flags`; let flags: Record = {}; let initialized = false; let intervalId: ReturnType | null = null; let userId: string | undefined; // Restore from storage on creation if (storage) { try { const cached = storage.getItem(STORAGE_KEY); if (cached) flags = JSON.parse(cached); } catch { // Ignore parse errors } } async function fetchFlags(): Promise { try { const parts = [`platform=${encodeURIComponent(platform)}`]; if (userId) parts.push(`userId=${encodeURIComponent(userId)}`); const requestId = typeof globalThis.crypto?.randomUUID === 'function' ? globalThis.crypto.randomUUID() : `${Date.now()}-${Math.random().toString(36).slice(2, 10)}`; const res = await globalThis.fetch(`${baseUrl}/flags/poll?${parts.join('&')}`, { headers: { 'x-product-id': productId, 'x-request-id': requestId }, }); if (!res.ok) return; const data = (await res.json()) as { flags?: Record }; flags = data.flags ?? {}; // Persist to storage if (storage) { try { storage.setItem(STORAGE_KEY, JSON.stringify(flags)); } catch { // Storage write failure — non-fatal } } } catch { // Keep existing flags on network error } } async function init(params?: { userId?: string }): Promise { if (initialized) return; initialized = true; userId = params?.userId; await fetchFlags(); intervalId = setInterval(() => { void fetchFlags(); }, pollIntervalMs); } function isEnabled(key: string): boolean { return flags[key] === true; } function getAllFlags(): Readonly> { return flags; } async function refresh(): Promise { await fetchFlags(); } function stop(): void { if (intervalId) clearInterval(intervalId); intervalId = null; flags = {}; initialized = false; userId = undefined; } return { init, isEnabled, getAllFlags, refresh, stop }; }