UNPKG

expo-edge-speech

Version:

Text-to-speech library for Expo using Microsoft Edge TTS service

333 lines (332 loc) 12.5 kB
"use strict"; /** * Provides connection-scoped memory management for real-time audio streaming. * Handles automatic cleanup, memory tracking, and audio data coordination. */ Object.defineProperty(exports, "__esModule", { value: true }); exports.getStorageService = exports.StorageService = void 0; const constants_1 = require("../constants"); /** * Connection-scoped memory management service * Manages audio buffering for real-time streaming with automatic cleanup */ class StorageService { static instance = null; /** Connection buffers mapped by connection ID */ connectionBuffers = new Map(); /** Cleanup interval timer */ cleanupTimer = null; /** Service configuration */ config; constructor(config) { this.config = { maxBufferSize: 16 * 1024 * 1024, // 16MB max buffer size cleanupInterval: constants_1.CONNECTION_LIFECYCLE.POOL_MANAGEMENT.CLEANUP_INTERVAL, warningThreshold: 0.8, // 80% of memory limit ...config, }; this.startCleanupInterval(); } /** * Get singleton instance of storage service */ static getInstance(config) { if (!StorageService.instance) { StorageService.instance = new StorageService(config); } return StorageService.instance; } /** * Create buffer for new connection * @param connectionId - The connection ID to create buffer for * @param allowExisting - If true, gracefully handle existing buffers instead of throwing error */ createConnectionBuffer(connectionId, allowExisting = false) { console.log(`[StorageService] Creating buffer for connectionId: ${connectionId}, allowExisting: ${allowExisting}`); if (this.connectionBuffers.has(connectionId)) { if (allowExisting) { console.log("[StorageService] Buffer already exists for connection, but allowExisting=true:", connectionId); return; // Gracefully handle existing buffer } else { console.log("[StorageService] Buffer already exists for connection:", connectionId); throw new Error(`Buffer already exists for connection: ${connectionId}`); } } const buffer = { connectionId, audioChunks: [], totalSize: 0, createdAt: new Date(), lastActivity: new Date(), state: "active", }; this.connectionBuffers.set(connectionId, buffer); console.log(`[StorageService] Buffer created successfully for connectionId: ${connectionId}`); } /** * Add audio chunk to connection buffer */ addAudioChunk(connectionId, audioData) { console.log(`[StorageService] addAudioChunk called with connectionId: ${connectionId}`); const buffer = this.connectionBuffers.get(connectionId); if (!buffer) { console.error(`[StorageService] No buffer found for connection: ${connectionId}`); console.log(`[StorageService] Available buffers:`, Array.from(this.connectionBuffers.keys())); throw new Error(`No buffer found for connection: ${connectionId}`); } if (buffer.state !== "active") { return false; // Buffer is being cleaned up or completed } // Check memory limits before adding const newSize = buffer.totalSize + audioData.length; if (newSize > this.config.maxBufferSize) { throw new Error(`Buffer size limit exceeded for connection ${connectionId}: ${newSize} > ${this.config.maxBufferSize}`); } // Add chunk to buffer buffer.audioChunks.push(new Uint8Array(audioData)); buffer.totalSize = newSize; buffer.lastActivity = new Date(); // Check warning threshold if (newSize > this.config.maxBufferSize * this.config.warningThreshold) { console.warn(`Memory usage warning for connection ${connectionId}: ${newSize} bytes (${Math.round((newSize / this.config.maxBufferSize) * 100)}%)`); } return true; } /** * Get merged audio data for connection */ getMergedAudioData(connectionId) { console.log(`[StorageService] getMergedAudioData called with connectionId: ${connectionId}`); console.log(`[StorageService] Available buffers:`, Array.from(this.connectionBuffers.keys())); const buffer = this.connectionBuffers.get(connectionId); if (!buffer) { console.error(`[StorageService] No buffer found for connection: ${connectionId}`); throw new Error(`No buffer found for connection: ${connectionId}`); } console.log(`[StorageService] Found buffer for connectionId: ${connectionId}, chunks: ${buffer.audioChunks.length}`); return this.mergeAudioChunks(buffer.audioChunks); } /** * Mark connection buffer as completed (no more data expected) */ markConnectionCompleted(connectionId) { const buffer = this.connectionBuffers.get(connectionId); if (buffer) { buffer.state = "completed"; buffer.lastActivity = new Date(); } } /** * Clean up connection buffer and free memory */ cleanupConnection(connectionId) { const buffer = this.connectionBuffers.get(connectionId); if (!buffer) { return false; } // Mark as cleaning to prevent new additions buffer.state = "cleaning"; // Clear audio chunks to free memory buffer.audioChunks.length = 0; buffer.totalSize = 0; // Remove from map this.connectionBuffers.delete(connectionId); return true; } /** * Get buffer info for connection */ getConnectionBufferInfo(connectionId) { const buffer = this.connectionBuffers.get(connectionId); if (!buffer) { return { exists: false, size: 0, chunkCount: 0, state: "none", lastActivity: null, }; } return { exists: true, size: buffer.totalSize, chunkCount: buffer.audioChunks.length, state: buffer.state, lastActivity: buffer.lastActivity, }; } /** * Get memory usage statistics */ getMemoryStats() { let totalMemoryUsed = 0; let largestBuffer = 0; let activeConnections = 0; for (const buffer of this.connectionBuffers.values()) { totalMemoryUsed += buffer.totalSize; largestBuffer = Math.max(largestBuffer, buffer.totalSize); if (buffer.state === "active") { activeConnections++; } } return { totalMemoryUsed, activeConnections, largestBuffer, memoryLimitPerConnection: this.config.maxBufferSize, globalMemoryLimit: this.config.maxBufferSize * 10, // Support up to 10 concurrent connections }; } /** * Merge multiple audio chunks into single Uint8Array */ mergeAudioChunks(chunks) { if (chunks.length === 0) { return new Uint8Array(0); } if (chunks.length === 1) { return new Uint8Array(chunks[0]); } // Calculate total size const totalSize = chunks.reduce((sum, chunk) => sum + chunk.length, 0); // Create merged array const merged = new Uint8Array(totalSize); let offset = 0; for (const chunk of chunks) { merged.set(chunk, offset); offset += chunk.length; } return merged; } /** * Start automatic cleanup interval */ startCleanupInterval() { if (this.cleanupTimer) { clearInterval(this.cleanupTimer); } this.cleanupTimer = setInterval(() => { this.performAutomaticCleanup(); }, this.config.cleanupInterval); } /** * Perform automatic cleanup of stale connections */ performAutomaticCleanup() { const now = new Date(); const staleConnections = []; for (const [connectionId, buffer] of this.connectionBuffers) { const timeSinceActivity = now.getTime() - buffer.lastActivity.getTime(); const isStale = timeSinceActivity > constants_1.CONNECTION_LIFECYCLE.TIMEOUTS.GRACEFUL_CLOSE * 2; if ((buffer.state === "completed" || isStale) && buffer.state !== "cleaning") { staleConnections.push(connectionId); } } // Clean up stale connections for (const connectionId of staleConnections) { this.cleanupConnection(connectionId); } if (staleConnections.length > 0) { console.debug(`Cleaned up ${staleConnections.length} stale connection buffers`); } } /** * Validate audio data format and size */ validateAudioData(audioData) { // Basic validation if (!audioData || audioData.length === 0) { return false; } // Check minimum chunk size if (audioData.length < constants_1.AUDIO_STREAMING.CHUNK_PROCESSING.MIN_CHUNK_SIZE) { return false; } // Check maximum chunk size if (audioData.length > constants_1.AUDIO_STREAMING.CHUNK_PROCESSING.MAX_CHUNK_SIZE) { return false; } return true; } /** * Estimate buffer size for streaming coordination */ estimateBufferSize(connectionId, additionalBytes = 0) { const buffer = this.connectionBuffers.get(connectionId); const currentSize = buffer ? buffer.totalSize : 0; const estimatedSize = currentSize + additionalBytes; const maxBufferSize = this.config.maxBufferSize; const remainingCapacity = maxBufferSize - estimatedSize; const utilizationPercent = (estimatedSize / maxBufferSize) * 100; return { currentSize, estimatedSize, remainingCapacity, utilizationPercent, }; } /** * Check if connection can accept more data */ canAcceptMoreData(connectionId, additionalBytes = 0) { const buffer = this.connectionBuffers.get(connectionId); if (!buffer || buffer.state !== "active") { return false; } const estimatedSize = buffer.totalSize + additionalBytes; const maxBufferSize = this.config.maxBufferSize; return estimatedSize <= maxBufferSize; } /** * Cleanup service and free all resources */ destroy() { // Stop cleanup timer if (this.cleanupTimer) { clearInterval(this.cleanupTimer); this.cleanupTimer = null; } // Clean up all connection buffers for (const connectionId of this.connectionBuffers.keys()) { this.cleanupConnection(connectionId); } // Clear singleton instance StorageService.instance = null; } // ============================================================================= // StateManager Integration Methods // ============================================================================= /** * Initialize storage service (required by StateManager) */ async initialize() { // Storage service is ready to use after construction // Cleanup timer is already started in constructor } /** * Cleanup storage service and free resources (required by StateManager) */ async cleanup() { this.destroy(); } /** * Register callback for connection state changes (required by StateManager) */ onConnectionStateChange(callback) { // Storage service doesn't manage connection states directly, // but this method is required for StateManager compatibility // Connection state changes are handled by NetworkService } /** * Get active connection count (for StateManager) */ getActiveConnectionCount() { return this.connectionBuffers.size; } } exports.StorageService = StorageService; // Export singleton instance getter for convenience const getStorageService = (config) => StorageService.getInstance(config); exports.getStorageService = getStorageService;