UNPKG

@varia-bly/variably-sdk

Version:

Official JavaScript/TypeScript SDK for Variably feature flags, experimentation, and real-time dynamic configurations

614 lines (474 loc) โ€ข 16.1 kB
# Variably SDK - Dynamic Configuration Client Real-time dynamic configuration management with intelligent fallback and comprehensive caching. ## ๐Ÿš€ Features - **Real-time Updates**: WebSocket-based instant configuration propagation (<5 seconds) - **Intelligent Fallback**: Automatic fallback to polling when WebSocket is unavailable - **ETag Support**: HTTP 304 optimization reduces bandwidth by 85% - **Smart Caching**: Multi-level caching with pattern-based invalidation - **Type Safety**: Full TypeScript support with generic configuration types - **Connection Management**: Automatic reconnection with exponential backoff - **Event-Driven**: Subscribe to configuration changes with callback functions ## ๐Ÿ“ฆ Installation ```bash npm install @variably/sdk # or yarn add @variably/sdk ``` ## ๐Ÿ Quick Start ### Basic Real-time Configuration ```typescript import { DynamicConfigClient } from '@variably/sdk'; // Initialize client const configClient = new DynamicConfigClient({ apiKey: 'your-api-key', jwtToken: 'your-jwt-token', // Required for WebSocket auth projectId: 'your-project-id', baseUrl: 'https://api.variably.com', enableRealtime: true }); // Define user context const userContext = { userId: 'user-123', email: 'user@example.com', attributes: { plan: 'premium', beta_user: true } }; // Get configuration values const featureEnabled = await configClient.getConfigBool( 'new-feature-flag', false, // default value userContext ); const themeConfig = await configClient.getConfigJSON( 'ui-theme', { theme: 'light', primaryColor: '#007bff' }, userContext ); console.log('Feature enabled:', featureEnabled); console.log('Theme config:', themeConfig); ``` ### Real-time Configuration Changes ```typescript // Subscribe to specific configuration changes const unsubscribe = configClient.onConfigChange('new-feature-flag', (result) => { console.log('Configuration updated!', { key: result.key, newValue: result.value, version: result.version, realTime: result.realTimeUpdate, reason: result.reason }); // Update your application state updateFeatureFlag(result.value); }); // Subscribe to all configuration changes const unsubscribeAll = configClient.onAnyConfigChange((result) => { console.log(`Config "${result.key}" changed to:`, result.value); // Handle different configurations switch (result.key) { case 'ui-theme': applyTheme(result.value); break; case 'feature-limits': updateLimits(result.value); break; } }); // Clean up subscriptions unsubscribe(); unsubscribeAll(); ``` ## ๐Ÿ”ง Configuration Options ```typescript interface DynamicConfigClientConfig { /** API key for authentication */ apiKey: string; /** JWT token for WebSocket authentication */ jwtToken?: string; /** Base URL for the API */ baseUrl?: string; /** Project ID for configuration subscriptions */ projectId: string; /** Enable real-time updates via WebSocket (default: true) */ enableRealtime?: boolean; /** Polling interval for fallback mode in ms (default: 30000) */ pollingInterval?: number; /** Cache configuration */ cache?: { ttl?: number; // Cache TTL in ms (default: 300000 = 5min) maxSize?: number; // Max cache entries (default: 1000) enabled?: boolean; // Enable caching (default: true) }; /** WebSocket configuration */ websocket?: { reconnectInterval?: number; // Reconnect interval (default: 5000) maxReconnectAttempts?: number; // Max reconnect attempts (default: 10) connectionTimeout?: number; // Connection timeout (default: 10000) autoReconnect?: boolean; // Auto reconnect (default: true) }; /** Enable debug logging (default: false) */ debug?: boolean; } ``` ## ๐Ÿ“ API Reference ### Core Methods #### `getConfig<T>(configKey: string, defaultValue: T, userContext: UserContext): Promise<T>` Get a configuration value with automatic type inference. ```typescript // Boolean configuration const enabled = await configClient.getConfigBool('feature-flag', false, userContext); // String configuration const endpoint = await configClient.getConfigString('api-endpoint', 'https://api.example.com', userContext); // Number configuration const maxItems = await configClient.getConfigNumber('max-items', 100, userContext); // JSON configuration const config = await configClient.getConfigJSON('complex-config', { timeout: 5000 }, userContext); ``` #### `evaluateConfig<T>(configKey: string, defaultValue: T, userContext: UserContext): Promise<DynamicConfigResult<T>>` Get detailed configuration evaluation results. ```typescript const result = await configClient.evaluateConfig('feature-flag', false, userContext); console.log({ key: result.key, value: result.value, reason: result.reason, // 'api_evaluation', 'rule_match', 'default', etc. ruleId: result.ruleId, // Rule that matched (if any) version: result.version, // Configuration version etag: result.etag, // ETag for caching cacheHit: result.cacheHit, // Whether result came from cache realTimeUpdate: result.realTimeUpdate, // Whether from real-time update updatedAt: result.updatedAt, // When config was last updated error: result.error // Error if evaluation failed }); ``` ### Event Subscriptions #### `onConfigChange<T>(configKey: string, callback: ConfigChangeCallback<T>): () => void` Subscribe to changes for a specific configuration. ```typescript const unsubscribe = configClient.onConfigChange('my-config', (result) => { if (result.realTimeUpdate) { console.log('Real-time update received!'); } else { console.log('Polling detected change'); } // Update your application updateMyFeature(result.value); }); // Unsubscribe when no longer needed unsubscribe(); ``` #### `onAnyConfigChange(callback: ConfigChangeCallback): () => void` Subscribe to changes for all configurations in the project. ```typescript const unsubscribe = configClient.onAnyConfigChange((result) => { console.log(`Configuration "${result.key}" changed to:`, result.value); // Centralized configuration change handling handleConfigChange(result.key, result.value); }); ``` ### Connection Management #### `getConnectionStatus(): ConnectionStatus` Get current connection information. ```typescript const status = configClient.getConnectionStatus(); console.log({ mode: status.mode, // 'realtime' | 'polling' | 'offline' connected: status.connected, // WebSocket connection status fallbackActive: status.fallbackActive // Whether polling fallback is active }); ``` #### `refreshConfigs(userContext: UserContext): Promise<void>` Manually refresh all cached configurations. ```typescript await configClient.refreshConfigs(userContext); console.log('All configurations refreshed'); ``` #### `disconnect(): void` Disconnect and clean up resources. ```typescript configClient.disconnect(); ``` ## ๐ŸŒ Usage Patterns ### React Integration ```typescript import React, { useState, useEffect } from 'react'; import { DynamicConfigClient } from '@variably/sdk'; const useConfig = <T>(configKey: string, defaultValue: T, userContext: any) => { const [value, setValue] = useState<T>(defaultValue); const [loading, setLoading] = useState(true); useEffect(() => { const configClient = new DynamicConfigClient({ apiKey: process.env.REACT_APP_VARIABLY_API_KEY!, jwtToken: process.env.REACT_APP_VARIABLY_JWT_TOKEN!, projectId: process.env.REACT_APP_VARIABLY_PROJECT_ID!, enableRealtime: true }); // Load initial value configClient.getConfig(configKey, defaultValue, userContext) .then(setValue) .finally(() => setLoading(false)); // Subscribe to changes const unsubscribe = configClient.onConfigChange(configKey, (result) => { setValue(result.value); }); return () => { unsubscribe(); configClient.disconnect(); }; }, [configKey, defaultValue, userContext]); return { value, loading }; }; // Usage in component const MyComponent = () => { const { value: featureEnabled, loading } = useConfig( 'new-feature', false, { userId: 'user-123' } ); if (loading) return <div>Loading...</div>; return ( <div> {featureEnabled ? ( <NewFeature /> ) : ( <OldFeature /> )} </div> ); }; ``` ### Node.js Service Integration ```typescript import { DynamicConfigClient } from '@variably/sdk'; class ConfigurationService { private configClient: DynamicConfigClient; private configs = new Map<string, any>(); constructor() { this.configClient = new DynamicConfigClient({ apiKey: process.env.VARIABLY_API_KEY!, jwtToken: process.env.VARIABLY_JWT_TOKEN!, projectId: process.env.VARIABLY_PROJECT_ID!, enableRealtime: true, debug: process.env.NODE_ENV === 'development' }); // Subscribe to all configuration changes this.configClient.onAnyConfigChange((result) => { this.configs.set(result.key, result.value); this.handleConfigChange(result.key, result.value); }); } async initialize() { // Load initial configurations const configKeys = ['rate-limits', 'feature-flags', 'api-endpoints']; const userContext = { userId: 'service-user' }; for (const key of configKeys) { const value = await this.configClient.getConfig(key, {}, userContext); this.configs.set(key, value); } console.log('Configuration service initialized'); } getConfig(key: string, defaultValue: any = null) { return this.configs.get(key) ?? defaultValue; } private handleConfigChange(key: string, value: any) { console.log(`Configuration "${key}" updated:`, value); // Handle specific configuration changes switch (key) { case 'rate-limits': this.updateRateLimits(value); break; case 'api-endpoints': this.updateApiEndpoints(value); break; } } private updateRateLimits(limits: any) { // Update application rate limits } private updateApiEndpoints(endpoints: any) { // Update service endpoints } async shutdown() { this.configClient.disconnect(); } } // Usage const configService = new ConfigurationService(); await configService.initialize(); // Use throughout your application const rateLimits = configService.getConfig('rate-limits', { default: 1000 }); ``` ## ๐Ÿ”„ Connection Modes ### Real-time Mode (Default) Uses WebSocket for instant updates: ```typescript const configClient = new DynamicConfigClient({ apiKey: 'your-api-key', jwtToken: 'your-jwt-token', projectId: 'your-project-id', enableRealtime: true, // WebSocket enabled pollingInterval: 60000 // Fallback polling interval }); ``` **Benefits:** - Sub-5 second update propagation - Efficient bandwidth usage - Real-time user experience ### Polling Mode Uses HTTP polling for updates: ```typescript const configClient = new DynamicConfigClient({ apiKey: 'your-api-key', projectId: 'your-project-id', enableRealtime: false, // WebSocket disabled pollingInterval: 30000 // 30 second polling }); ``` **Benefits:** - Works in restricted network environments - No WebSocket dependencies - Simpler firewall configuration ### Hybrid Mode (Recommended) Combines both approaches with intelligent fallback: ```typescript const configClient = new DynamicConfigClient({ apiKey: 'your-api-key', jwtToken: 'your-jwt-token', projectId: 'your-project-id', enableRealtime: true, // Try WebSocket first pollingInterval: 60000, // Fallback to polling if WebSocket fails websocket: { maxReconnectAttempts: 5, reconnectInterval: 5000, autoReconnect: true } }); ``` **Benefits:** - Best of both worlds - Automatic fallback for reliability - Optimal performance and compatibility ## ๐Ÿงช Testing ### Mock Client for Testing ```typescript import { DynamicConfigClient } from '@variably/sdk'; // Create a mock client for testing class MockDynamicConfigClient { private configs = new Map<string, any>(); private callbacks = new Map<string, Function[]>(); async getConfig<T>(key: string, defaultValue: T): Promise<T> { return this.configs.get(key) ?? defaultValue; } setMockConfig(key: string, value: any) { this.configs.set(key, value); // Trigger callbacks const callbacks = this.callbacks.get(key) || []; callbacks.forEach(callback => callback({ key, value, realTimeUpdate: false })); } onConfigChange(key: string, callback: Function) { if (!this.callbacks.has(key)) { this.callbacks.set(key, []); } this.callbacks.get(key)!.push(callback); return () => { const callbacks = this.callbacks.get(key) || []; const index = callbacks.indexOf(callback); if (index > -1) { callbacks.splice(index, 1); } }; } disconnect() { // Mock cleanup } } // Use in tests describe('Feature with Dynamic Config', () => { let mockClient: MockDynamicConfigClient; beforeEach(() => { mockClient = new MockDynamicConfigClient(); }); it('should enable feature when config is true', async () => { mockClient.setMockConfig('my-feature', true); const enabled = await mockClient.getConfig('my-feature', false); expect(enabled).toBe(true); }); it('should react to config changes', (done) => { mockClient.onConfigChange('my-feature', (result) => { expect(result.value).toBe(true); done(); }); mockClient.setMockConfig('my-feature', true); }); }); ``` ## ๐Ÿšจ Error Handling The SDK handles errors gracefully: ```typescript try { const result = await configClient.evaluateConfig('my-config', 'default', userContext); if (result.error) { console.warn('Config evaluation failed:', result.error.message); console.log('Using default value:', result.value); } else { console.log('Config loaded successfully:', result.value); } } catch (error) { // This should rarely happen as errors are handled internally console.error('Unexpected error:', error); } ``` ### Common Error Scenarios - **Invalid API Key**: Returns default values with error details - **Network Issues**: Automatic retry with exponential backoff - **WebSocket Failures**: Automatic fallback to polling mode - **Invalid JWT Token**: Falls back to polling mode only - **Configuration Not Found**: Returns default value with 'not_found' reason ## ๐Ÿ“Š Performance Optimization ### Caching Strategy ```typescript const configClient = new DynamicConfigClient({ apiKey: 'your-api-key', projectId: 'your-project-id', cache: { ttl: 300000, // 5 minute cache TTL maxSize: 1000, // Max 1000 cached entries enabled: true // Enable caching } }); ``` ### Batch Loading ```typescript // Load multiple configurations efficiently const configs = await Promise.all([ configClient.getConfig('config1', 'default1', userContext), configClient.getConfig('config2', 'default2', userContext), configClient.getConfig('config3', 'default3', userContext) ]); ``` ### Connection Management ```typescript // Monitor connection status configClient.onConnectionState((state) => { console.log('Connection state:', state); if (state === 'connected') { console.log('Real-time updates available'); } else if (state === 'disconnected') { console.log('Falling back to polling'); } }); ``` ## ๐Ÿ”— Related Documentation - [Main SDK Documentation](./README.md) - [Feature Flags Client](./docs/feature-flags.md) - [Analytics Integration](./docs/analytics.md) - [API Reference](./docs/api-reference.md) ## ๐Ÿค Support For questions and support: - Documentation: [https://docs.variably.com](https://docs.variably.com) - GitHub Issues: [https://github.com/variably/sdk-js/issues](https://github.com/variably/sdk-js/issues) - Discord: [https://discord.gg/variably](https://discord.gg/variably) ## ๐Ÿ“„ License MIT License - see [LICENSE](./LICENSE) file for details.