# 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.