@sailboat-computer/data-storage
Version:
Shared data storage library for sailboat computer v3
610 lines (537 loc) • 17.4 kB
text/typescript
/**
* Storage Configuration Manager
*
* This implementation uses the @sailboat-computer/config-client package to interact
* with the Configuration Service, with fallback to local files when the service is unavailable.
*/
import { createConfigClient, ConfigClient, ConfigChangeEvent } from '@sailboat-computer/config-client';
import { EventBus, EventCategory } from '../event-bus';
import { createLogger } from '../utils/logger';
import { StorageConfig, UnifiedStorageManagerOptions } from '../types';
import { IStorageConfigManager } from './IStorageConfigManager';
import path from 'path';
import fs from 'fs/promises';
import { loadConfig, saveConfig } from '@sailboat-computer/config-loader';
// Create logger
const logger = createLogger('storage-config-manager');
/**
* Default storage configuration
*/
export const DEFAULT_STORAGE_CONFIG: StorageConfig = {
providers: {
hot: {
type: 'redis',
config: {
host: 'localhost',
port: 6379,
password: '',
db: 0,
tls: false
}
},
batching: {
timeBased: {
enabled: true,
intervalMs: 5000
},
sizeBased: {
enabled: true,
maxBatchSize: 100
},
priorityOverride: true
}
}
};
/**
* Default unified storage manager options
*/
export const DEFAULT_UNIFIED_STORAGE_MANAGER_OPTIONS: UnifiedStorageManagerOptions = {
localStorage: {
directory: './data',
maxSizeBytes: 104857600,
encrypt: false,
compressionLevel: 6
},
sync: {
enabled: true,
intervalMs: 60000,
batchSize: 100
},
resilience: {
circuitBreaker: {
failureThreshold: 3,
resetTimeoutMs: 30000
},
retry: {
maxRetries: 3,
baseDelayMs: 1000
},
timeout: {
defaultTimeoutMs: 5000,
operationTimeouts: {
store: 10000,
retrieve: 15000,
update: 10000,
delete: 5000,
storeBatch: 30000,
retrieveBatch: 30000,
cleanup: 60000,
migrate: 60000
}
},
bulkhead: {
maxConcurrentOperations: 10,
maxQueueSize: 100
},
rateLimit: {
defaultTokensPerSecond: 50,
defaultBucketSize: 100,
tokensPerSecond: 50,
bucketSize: 100,
waitWhenLimited: true,
maxWaitTimeMs: 5000,
bypassOperations: ['retrieve']
}
},
logger: {
level: 'info',
console: true
}
};
/**
* Storage configuration manager options
*/
export interface StorageConfigManagerOptions {
/**
* Configuration service URL
* If not provided, will use CONFIG_SERVICE_URL environment variable or default to http://localhost:3000
*/
serviceUrl?: string;
/**
* Local configuration directory
* If not provided, will use './config'
*/
localDir?: string;
/**
* Environment to use
* If not provided, will use 'production'
*/
environment?: string;
/**
* Service name
* If not provided, will use 'data-manager'
*/
serviceName?: string;
/**
* Configuration ID
* If not provided, will use 'config'
*/
configId?: string;
/**
* Custom logger
*/
logger?: {
info: (message: string, meta?: any) => void;
warn: (message: string, meta?: any) => void;
error: (message: string, meta?: any) => void;
debug: (message: string, meta?: any) => void;
};
}
/**
* Storage configuration manager
*/
export class StorageConfigManager implements IStorageConfigManager {
private storageConfig: StorageConfig;
private unifiedStorageManagerOptions: UnifiedStorageManagerOptions;
private configClient: ConfigClient;
private eventBus?: EventBus;
private stopWatching?: () => void;
private localDir: string;
private serviceName: string;
private configId: string;
private environment: string;
/**
* Create a new storage configuration manager
* @param options Configuration options
*/
constructor(options: StorageConfigManagerOptions = {}) {
this.storageConfig = DEFAULT_STORAGE_CONFIG;
this.unifiedStorageManagerOptions = DEFAULT_UNIFIED_STORAGE_MANAGER_OPTIONS;
this.localDir = options.localDir || './config';
this.serviceName = options.serviceName || 'data-manager';
this.configId = options.configId || 'config';
this.environment = options.environment || 'production';
const serviceUrl = options.serviceUrl ||
process.env.CONFIG_SERVICE_URL ||
'http://localhost:3000';
// Create config client
this.configClient = createConfigClient({
serviceUrl,
serviceName: this.serviceName,
fallbackDir: this.localDir,
environment: this.environment,
resilience: {
timeoutMs: 5000,
circuitBreaker: {
failureThreshold: 3,
resetTimeoutMs: 30000,
},
retry: {
maxRetries: 3,
baseDelayMs: 1000,
maxDelayMs: 10000,
},
},
logger: options.logger || logger,
});
logger.info('Storage configuration manager initialized', {
serviceUrl,
localDir: this.localDir,
environment: this.environment,
serviceName: this.serviceName,
configId: this.configId
});
}
/**
* Initialize the configuration manager
*/
async initialize(): Promise<void> {
try {
// Try to load configuration from service first
try {
const config = await this.configClient.getConfig<any>(this.configId);
// Extract storage config and unified storage manager options
this.extractConfig(config);
logger.info('Configuration loaded from service');
// Cache the configuration locally
await this.cacheConfigLocally(config);
} catch (serviceError) {
logger.warn('Failed to load configuration from service, falling back to local file', {
error: (serviceError as Error).message
});
// Fall back to local file
try {
const config = await loadConfig<any>(
`${this.serviceName}/${this.configId}.json`,
this.createFullConfig(),
{ baseDir: this.localDir }
);
// Extract storage config and unified storage manager options
this.extractConfig(config);
logger.info('Configuration loaded from local file');
} catch (localError) {
logger.warn('Failed to load configuration from local file, using default', {
error: (localError as Error).message
});
// Use default configuration
this.storageConfig = DEFAULT_STORAGE_CONFIG;
this.unifiedStorageManagerOptions = DEFAULT_UNIFIED_STORAGE_MANAGER_OPTIONS;
// Cache the default configuration
await this.cacheConfigLocally(this.createFullConfig());
}
}
// Start watching for configuration changes
this.startWatching();
logger.info('Configuration manager initialized successfully');
} catch (error) {
logger.error('Failed to initialize configuration manager', { error: (error as Error).message });
throw error;
}
}
/**
* Extract storage config and unified storage manager options from full config
* @param config Full configuration
*/
private extractConfig(config: any): void {
// Extract storage config
this.storageConfig = {
providers: config.providers || DEFAULT_STORAGE_CONFIG.providers
};
// Extract unified storage manager options
this.unifiedStorageManagerOptions = {
localStorage: config.localStorage || DEFAULT_UNIFIED_STORAGE_MANAGER_OPTIONS.localStorage,
sync: config.sync || DEFAULT_UNIFIED_STORAGE_MANAGER_OPTIONS.sync,
resilience: config.resilience || DEFAULT_UNIFIED_STORAGE_MANAGER_OPTIONS.resilience,
logger: config.logger || DEFAULT_UNIFIED_STORAGE_MANAGER_OPTIONS.logger
};
}
/**
* Create full configuration from storage config and unified storage manager options
* @returns Full configuration
*/
private createFullConfig(): any {
return {
providers: this.storageConfig.providers,
localStorage: this.unifiedStorageManagerOptions.localStorage,
sync: this.unifiedStorageManagerOptions.sync,
resilience: this.unifiedStorageManagerOptions.resilience,
logger: this.unifiedStorageManagerOptions.logger,
features: {
useConfigService: true,
useLocalFallback: true,
enableVersioning: true,
enableSchemaValidation: true
}
};
}
/**
* Cache configuration locally
* @param config Configuration to cache
*/
private async cacheConfigLocally(config: any): Promise<void> {
try {
// Ensure directory exists
const configDir = path.join(this.localDir, this.serviceName);
await fs.mkdir(configDir, { recursive: true });
// Save configuration to file
await saveConfig(
`${this.serviceName}/${this.configId}.json`,
config,
'Cached from configuration service',
undefined,
{ baseDir: this.localDir }
);
logger.debug('Configuration cached locally');
} catch (error) {
logger.error('Failed to cache configuration locally', { error: (error as Error).message });
// Don't throw error, as this is a non-critical operation
}
}
/**
* Start watching for configuration changes
*/
private startWatching(): void {
// Stop any existing watcher
if (this.stopWatching) {
this.stopWatching();
}
// Start watching for configuration changes
this.stopWatching = this.configClient.watchConfig<any>(
this.configId,
this.handleConfigChange.bind(this)
);
logger.info('Started watching for configuration changes');
}
/**
* Handle configuration change event
* @param event Configuration change event
*/
private async handleConfigChange(event: ConfigChangeEvent<any>): Promise<void> {
try {
logger.info('Configuration changed', {
version: event.version,
timestamp: event.timestamp,
});
// Extract storage config and unified storage manager options
this.extractConfig(event.data);
// Cache the updated configuration locally
await this.cacheConfigLocally(event.data);
// Apply changes
await this.applyChanges();
} catch (error) {
logger.error('Failed to handle configuration change', { error: (error as Error).message });
}
}
/**
* Set the event bus
* @param eventBus Event bus
*/
setEventBus(eventBus: EventBus): void {
this.eventBus = eventBus;
// Subscribe to configuration change events
this.eventBus.subscribe('system.config_changed.v1', async (event: any) => {
if (event.data && event.data.service === this.serviceName) {
await this.reload();
}
});
}
/**
* Reload configuration
*/
async reload(): Promise<void> {
try {
// Try to load configuration from service first
try {
const config = await this.configClient.getConfig<any>(this.configId);
// Extract storage config and unified storage manager options
this.extractConfig(config);
logger.info('Configuration reloaded from service');
// Cache the configuration locally
await this.cacheConfigLocally(config);
} catch (serviceError) {
logger.warn('Failed to reload configuration from service, falling back to local file', {
error: (serviceError as Error).message
});
// Fall back to local file
try {
const config = await loadConfig<any>(
`${this.serviceName}/${this.configId}.json`,
this.createFullConfig(),
{ baseDir: this.localDir }
);
// Extract storage config and unified storage manager options
this.extractConfig(config);
logger.info('Configuration reloaded from local file');
} catch (localError) {
logger.error('Failed to reload configuration from local file', {
error: (localError as Error).message
});
throw localError;
}
}
logger.info('Configuration reloaded');
} catch (error) {
logger.error('Failed to reload configuration', { error: (error as Error).message });
throw error;
}
}
/**
* Reset configuration to default
*/
async resetToDefault(): Promise<void> {
try {
const defaultConfig = this.createFullConfig();
// Try to save default configuration to service
try {
await this.configClient.saveConfig(
this.configId,
defaultConfig,
'Reset to default configuration'
);
logger.info('Default configuration saved to service');
} catch (serviceError) {
logger.warn('Failed to save default configuration to service, falling back to local file', {
error: (serviceError as Error).message
});
}
// Always save to local file as well
await saveConfig(
`${this.serviceName}/${this.configId}.json`,
defaultConfig,
'Reset to default configuration',
undefined,
{ baseDir: this.localDir }
);
// Reset in-memory configuration
this.storageConfig = DEFAULT_STORAGE_CONFIG;
this.unifiedStorageManagerOptions = DEFAULT_UNIFIED_STORAGE_MANAGER_OPTIONS;
// Publish configuration change event
await this.publishConfigChanged();
logger.info('Configuration reset to default');
} catch (error) {
logger.error('Failed to reset configuration to default', { error: (error as Error).message });
throw error;
}
}
/**
* Apply configuration changes
*/
async applyChanges(): Promise<void> {
// Publish configuration change event
await this.publishConfigChanged();
logger.info('Configuration changes applied');
}
/**
* Publish configuration change event
*/
async publishConfigChanged(): Promise<void> {
if (!this.eventBus) {
logger.warn('Event bus not available, cannot publish configuration change event');
return;
}
try {
// Create configuration change event
const configChangedEvent = {
service: this.serviceName,
timestamp: new Date(),
config: this.createFullConfig()
};
// Publish event
await this.eventBus.publish(
'system.config_changed.v1',
configChangedEvent,
{
category: EventCategory.CONFIGURATION,
priority: 'high'
}
);
logger.info('Published configuration change event');
} catch (error) {
logger.error('Failed to publish configuration change event', {
error: (error as Error).message
});
// Continue operation even if publishing the event fails
}
}
/**
* Get the storage configuration
*/
getStorageConfig(): StorageConfig {
return this.storageConfig;
}
/**
* Get the unified storage manager options
*/
getUnifiedStorageManagerOptions(): UnifiedStorageManagerOptions {
return this.unifiedStorageManagerOptions;
}
/**
* Save configuration
* @param config Configuration to save
* @param description Optional description
*/
async saveConfig(config: StorageConfig, description?: string): Promise<void> {
try {
// Update storage config
this.storageConfig = config;
// Create full configuration
const fullConfig = this.createFullConfig();
// Try to save configuration to service
try {
await this.configClient.saveConfig(
this.configId,
fullConfig,
description || 'Updated configuration'
);
logger.info('Configuration saved to service');
} catch (serviceError) {
logger.warn('Failed to save configuration to service, falling back to local file', {
error: (serviceError as Error).message
});
}
// Always save to local file as well
await saveConfig(
`${this.serviceName}/${this.configId}.json`,
fullConfig,
description || 'Updated configuration',
undefined,
{ baseDir: this.localDir }
);
// Publish configuration change event
await this.publishConfigChanged();
logger.info('Configuration saved');
} catch (error) {
logger.error('Failed to save configuration', { error: (error as Error).message });
throw error;
}
}
/**
* Shutdown the configuration manager
*/
shutdown(): void {
// Stop watching for configuration changes
if (this.stopWatching) {
this.stopWatching();
this.stopWatching = undefined;
}
logger.info('Configuration manager shut down');
}
}
/**
* Create a new storage configuration manager
* @param options Configuration options
* @returns Storage configuration manager
*/
export function createStorageConfigManager(options: StorageConfigManagerOptions = {}): StorageConfigManager {
return new StorageConfigManager(options);
}