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