expo-edge-speech
Version:
Text-to-speech library for Expo using Microsoft Edge TTS service
444 lines (443 loc) • 16.7 kB
JavaScript
"use strict";
/**
* Provides centralized state management using all services and complete type definitions.
* Coordinates state between Network, Storage, Voice, and Audio services while maintaining
* expo-speech API compatibility and implementing thread-safe state updates.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.StateManager = exports.ApplicationState = void 0;
const types_1 = require("../types");
const constants_1 = require("../constants"); // Added import
const audioService_1 = require("../services/audioService");
const commonUtils_1 = require("../utils/commonUtils");
// =============================================================================
// State Management Types and Interfaces
// =============================================================================
/**
* Application state enumeration
*/
var ApplicationState;
(function (ApplicationState) {
ApplicationState["Idle"] = "idle";
ApplicationState["Initializing"] = "initializing";
ApplicationState["Ready"] = "ready";
ApplicationState["Synthesizing"] = "synthesizing";
ApplicationState["Playing"] = "playing";
ApplicationState["Paused"] = "paused";
ApplicationState["Error"] = "error";
ApplicationState["Cleaning"] = "cleaning";
})(ApplicationState || (exports.ApplicationState = ApplicationState = {}));
// =============================================================================
// State Management Service
// =============================================================================
/**
* Centralized state management service for expo-edge-speech
* Coordinates state between all services and provides thread-safe state updates
*/
class StateManager {
// Service instances
storageService;
networkService;
voiceService;
audioService;
// State tracking
applicationState = ApplicationState.Idle;
configuration;
activeSessions = new Map();
stateChangeListeners = new Set();
// Thread safety
stateUpdateQueue = [];
isProcessingStateUpdates = false;
constructor(storageService, networkService, voiceService, audioService, initialConfiguration) {
this.storageService = storageService;
this.networkService = networkService;
this.voiceService = voiceService;
this.audioService = audioService;
// Initialize default configuration
this.configuration = this.createDefaultConfiguration(initialConfiguration);
// Set up service integration
this.initializeServiceIntegration();
}
// =============================================================================
// Configuration Management
// =============================================================================
/**
* Create default configuration using complete constants
*/
createDefaultConfiguration(overrides) {
const defaultConfig = {
audioFormat: {
format: "audio-24khz-48kbitrate-mono-mp3",
sampleRate: 24000,
bitRate: 48000,
channels: 1,
},
connectionPooling: {
maxConnections: constants_1.EDGE_TTS_CONFIG.connectionPoolSize,
connectionTimeout: constants_1.EDGE_TTS_CONFIG.connectionTimeout,
reuseConnections: false, // Assuming 'false' is the desired default, EDGE_TTS_CONFIG doesn't specify this
},
wordBoundary: {
enabled: true,
offsetCompensation: 8750000, // 8,750,000 ticks padding
},
};
return { ...defaultConfig, ...overrides };
}
/**
* Get current configuration
*/
getConfiguration() {
return { ...this.configuration };
}
/**
* Update configuration with state change notification
*/
updateConfiguration(updates) {
return this.enqueueStateUpdate(() => {
const previousConfig = { ...this.configuration };
this.configuration = { ...this.configuration, ...updates };
this.notifyStateChange({
type: "configuration",
previousState: previousConfig,
newState: this.configuration,
timestamp: new Date(),
context: { updates },
});
});
}
// =============================================================================
// Application State Management
// =============================================================================
/**
* Get current application state
*/
getApplicationState() {
return this.applicationState;
}
/**
* Update application state with notifications
*/
updateApplicationState(newState) {
return this.enqueueStateUpdate(() => {
if (this.applicationState !== newState) {
const previousState = this.applicationState;
this.applicationState = newState;
this.notifyStateChange({
type: "application",
previousState,
newState,
timestamp: new Date(),
});
}
});
}
// =============================================================================
// Speech Synthesis Session Management
// =============================================================================
/**
* Create new synthesis session using service integration
*/
async createSynthesisSession(text, options) {
const sessionId = (0, commonUtils_1.generateSessionId)();
const connectionId = (0, commonUtils_1.generateConnectionId)();
const session = {
id: sessionId,
connectionId,
text,
options,
state: ApplicationState.Initializing,
createdAt: new Date(),
lastActivity: new Date(),
};
// Note: Connection buffer creation is handled by ConnectionManager
// to avoid duplicate buffer creation issues
await this.enqueueStateUpdate(() => {
this.activeSessions.set(sessionId, session);
this.notifyStateChange({
type: "synthesis",
previousState: null,
newState: session,
timestamp: new Date(),
context: { action: "created" },
});
});
return session;
}
/**
* Update synthesis session state
*/
async updateSynthesisSession(sessionId, updates) {
await this.enqueueStateUpdate(() => {
const session = this.activeSessions.get(sessionId);
if (session) {
const previousSession = { ...session };
const updatedSession = {
...session,
...updates,
lastActivity: new Date(),
};
this.activeSessions.set(sessionId, updatedSession);
this.notifyStateChange({
type: "synthesis",
previousState: previousSession,
newState: updatedSession,
timestamp: new Date(),
context: { sessionId, updates },
});
}
});
}
/**
* Remove synthesis session and cleanup
*/
async removeSynthesisSession(sessionId) {
await this.enqueueStateUpdate(() => {
const session = this.activeSessions.get(sessionId);
if (session) {
// Cleanup connection via storage service
this.storageService.cleanupConnection(session.connectionId);
this.activeSessions.delete(sessionId);
this.notifyStateChange({
type: "synthesis",
previousState: session,
newState: null,
timestamp: new Date(),
context: { action: "removed", sessionId },
});
}
});
}
/**
* Get active synthesis sessions
*/
getActiveSessions() {
return Array.from(this.activeSessions.values());
}
/**
* Get specific synthesis session
*/
getSynthesisSession(sessionId) {
return this.activeSessions.get(sessionId);
}
// =============================================================================
// Service Coordination
// =============================================================================
/**
* Initialize service integration with state tracking
*/
initializeServiceIntegration() {
// Storage service connection state tracking (if available)
if (this.storageService.onConnectionStateChange) {
this.storageService.onConnectionStateChange((connectionId, state) => {
this.handleConnectionStateChange(connectionId, state);
});
}
// Network service connection tracking (if available)
if (this.networkService.onConnectionStateChange) {
this.networkService.onConnectionStateChange((connectionId, state) => {
this.handleConnectionStateChange(connectionId, state);
});
}
// Audio service playback state tracking (if available)
if (this.audioService.onPlaybackStateChange) {
this.audioService.onPlaybackStateChange((state, context) => {
this.handleAudioStateChange(state, context);
});
}
}
/**
* Handle connection state changes from services
*/
handleConnectionStateChange(connectionId, state) {
this.enqueueStateUpdate(() => {
// Find sessions using this connection
const affectedSessions = Array.from(this.activeSessions.values()).filter((session) => session.connectionId === connectionId);
affectedSessions.forEach((session) => {
let newSessionState;
switch (state) {
case types_1.ConnectionState.Connecting:
newSessionState = ApplicationState.Initializing;
break;
case types_1.ConnectionState.Connected:
newSessionState = ApplicationState.Ready;
break;
case types_1.ConnectionState.Synthesizing:
newSessionState = ApplicationState.Synthesizing;
break;
case types_1.ConnectionState.Error:
newSessionState = ApplicationState.Error;
break;
default:
newSessionState = ApplicationState.Idle;
}
this.updateSynthesisSession(session.id, { state: newSessionState });
});
this.notifyStateChange({
type: "connection",
previousState: null,
newState: state,
timestamp: new Date(),
context: { connectionId, affectedSessions: affectedSessions.length },
});
});
}
/**
* Handle audio state changes from audio service
*/
handleAudioStateChange(state, context) {
this.enqueueStateUpdate(() => {
let newAppState;
switch (state) {
case audioService_1.AudioPlaybackState.Playing:
newAppState = ApplicationState.Playing;
break;
case audioService_1.AudioPlaybackState.Paused:
newAppState = ApplicationState.Paused;
break;
case audioService_1.AudioPlaybackState.Error:
newAppState = ApplicationState.Error;
break;
case audioService_1.AudioPlaybackState.Completed:
case audioService_1.AudioPlaybackState.Stopped:
newAppState = ApplicationState.Idle;
break;
default:
newAppState = this.applicationState;
}
if (newAppState !== this.applicationState) {
this.updateApplicationState(newAppState);
}
this.notifyStateChange({
type: "audio",
previousState: null,
newState: state,
timestamp: new Date(),
context,
});
});
}
// =============================================================================
// Thread-Safe State Updates
// =============================================================================
/**
* Enqueue state update for thread-safe processing
*/
async enqueueStateUpdate(updateFn) {
return new Promise((resolve, reject) => {
this.stateUpdateQueue.push(() => {
try {
updateFn();
resolve();
}
catch (error) {
reject(error);
}
});
this.processStateUpdateQueue();
});
}
/**
* Process queued state updates
*/
async processStateUpdateQueue() {
if (this.isProcessingStateUpdates || this.stateUpdateQueue.length === 0) {
return;
}
this.isProcessingStateUpdates = true;
try {
while (this.stateUpdateQueue.length > 0) {
const updateFn = this.stateUpdateQueue.shift();
if (updateFn) {
updateFn();
}
}
}
finally {
this.isProcessingStateUpdates = false;
}
}
// =============================================================================
// Event Listeners and Notifications
// =============================================================================
/**
* Add state change listener
*/
addStateChangeListener(listener) {
this.stateChangeListeners.add(listener);
}
/**
* Remove state change listener
*/
removeStateChangeListener(listener) {
this.stateChangeListeners.delete(listener);
}
/**
* Notify all listeners of state change
*/
notifyStateChange(event) {
this.stateChangeListeners.forEach((listener) => {
try {
listener(event);
}
catch (error) {
console.error("Error in state change listener:", error);
}
});
}
// =============================================================================
// Utility Methods
// =============================================================================
/**
* Get comprehensive state summary
*/
getStateSummary() {
return {
application: this.applicationState,
configuration: this.getConfiguration(),
activeSessions: this.activeSessions.size,
connections: this.storageService.getActiveConnectionCount(),
memoryUsage: this.storageService.getMemoryStats?.() || {},
};
}
/**
* Initialize state manager
*/
async initialize() {
await this.updateApplicationState(ApplicationState.Initializing);
try {
// Initialize all services
await this.storageService.initialize?.();
await this.voiceService.initialize?.();
await this.audioService.initialize?.();
await this.updateApplicationState(ApplicationState.Ready);
}
catch (error) {
await this.updateApplicationState(ApplicationState.Error);
throw error;
}
}
/**
* Cleanup state manager and all services
*/
async cleanup() {
await this.updateApplicationState(ApplicationState.Cleaning);
try {
// Cleanup all active sessions
const sessionIds = Array.from(this.activeSessions.keys());
await Promise.all(sessionIds.map((sessionId) => this.removeSynthesisSession(sessionId)));
// Cleanup services
await this.storageService.cleanup?.();
await this.networkService.cleanup?.();
await this.audioService.cleanup?.();
// Clear listeners
this.stateChangeListeners.clear();
await this.updateApplicationState(ApplicationState.Idle);
}
catch (error) {
await this.updateApplicationState(ApplicationState.Error);
throw error;
}
}
}
exports.StateManager = StateManager;