Fix TTS module initialization and dependency issues. Update module IDs for consistency, improve circular dependency detection, and fix UI Controller event handling.

This commit is contained in:
2025-04-04 19:15:28 +00:00
parent 02c7b9ef28
commit 49a5af252c
33 changed files with 7227 additions and 4060 deletions
+456 -13
View File
@@ -9,6 +9,24 @@ export class BaseModule {
this.state = 'PENDING';
this.progress = 0;
this.progressCallback = null;
// Add standard event target for custom events
this.eventTarget = document.createElement('div');
// Add standard configuration object
this.config = {};
// Track event listeners for cleanup
this._eventListeners = [];
// Resource loading tracking
this._loadingResources = new Map();
this._totalResources = 0;
this._loadedResources = 0;
// Dependencies
this.dependencies = [];
this._loadedDependencies = new Map();
}
/**
@@ -23,15 +41,10 @@ export class BaseModule {
this.changeState('LOADING');
this.reportProgress(10, "Starting initialization");
// Load dependencies
const depsLoaded = await this.loadDependencies();
if (!depsLoaded) {
this.changeState('ERROR');
this.reportProgress(100, "Failed to load dependencies");
return false;
}
// Skip loadDependencies() call - now handled automatically
const depStatus = await this.waitForDependencies();
// Wait for dependencies
const depStatus = await this._waitForModuleDependencies();
if (!depStatus) {
// If dependencies aren't available, report waiting
this.changeState('WAITING');
@@ -59,24 +72,100 @@ export class BaseModule {
}
/**
* Load module dependencies - Override this in child classes
* @returns {Promise} - Resolves when dependencies are loaded
* Wait for module dependencies
* @returns {Promise<boolean>} - Resolves when dependencies are ready
*/
async _waitForModuleDependencies() {
if (!this.dependencies || this.dependencies.length === 0) {
return true;
}
try {
this.reportProgress(15, "Waiting for dependencies");
// Get moduleRegistry - first try import then fallback to window
const registry = window.moduleRegistry;
if (!registry) {
console.error(`${this.id}: Module registry not found, will retry`);
// Retry after a short delay to allow registry to be initialized
await new Promise(resolve => setTimeout(resolve, 100));
// Try again
const retryRegistry = window.moduleRegistry;
if (!retryRegistry) {
console.error(`${this.id}: Module registry still not found after retry`);
return false;
}
console.log(`${this.id}: Found module registry after retry`);
return this._continueWaitForDependencies(retryRegistry);
}
return this._continueWaitForDependencies(registry);
} catch (error) {
console.error(`${this.id}: Error waiting for dependencies:`, error);
return false;
}
}
/**
* Continue waiting for dependencies using the provided registry
* @param {ModuleRegistry} registry - The module registry
* @returns {Promise<boolean>} - Resolves when dependencies are ready
* @private
*/
async _continueWaitForDependencies(registry) {
try {
// Wait for all dependencies to be ready
const results = await registry.waitForModules(this.dependencies);
// Store references to dependencies
for (let i = 0; i < this.dependencies.length; i++) {
const depId = this.dependencies[i];
const depModule = registry.getModule(depId);
if (depModule) {
this._loadedDependencies.set(depId, depModule);
}
}
const allDepsReady = results.every(ready => ready === true);
if (allDepsReady) {
this.reportProgress(20, "Dependencies ready");
return true;
} else {
this.reportProgress(15, "Some dependencies not ready");
return false;
}
} catch (error) {
console.error(`${this.id}: Error in _continueWaitForDependencies:`, error);
return false;
}
}
/**
* Legacy method for backwards compatibility
* @deprecated Use dependencies array property instead
* @returns {Promise<boolean>} - Resolves when dependencies are loaded
*/
async loadDependencies() {
// This is now handled by _waitForModuleDependencies
return Promise.resolve(true);
}
/**
* Wait for dependencies to be ready - Override this in child classes
* @returns {Promise} - Resolves when dependencies are ready
* Legacy method for backwards compatibility
* @deprecated No longer needed as waitForDependencies is handled automatically
* @returns {Promise<boolean>} - Resolves when dependencies are ready
*/
async waitForDependencies() {
// This is now handled by _waitForModuleDependencies
return Promise.resolve(true);
}
/**
* Initialize the module - Override this in child classes
* @returns {Promise} - Resolves when initialization is complete
* @returns {Promise<boolean>} - Resolves when initialization is complete
*/
async initialize() {
return Promise.resolve(true);
@@ -116,6 +205,360 @@ export class BaseModule {
getState() {
return this.state;
}
/**
* Dispatch a module event
* @param {string} name - Event name
* @param {Object} detail - Event details
*/
dispatchEvent(name, detail = {}) {
const event = new CustomEvent(name, {
detail: { moduleId: this.id, ...detail },
bubbles: true
});
document.dispatchEvent(event);
return event;
}
/**
* Add an event listener with automatic tracking for cleanup
* @param {EventTarget} target - Event target (document, window, etc)
* @param {string} type - Event type
* @param {Function} listener - Event listener
* @param {Object} options - Event listener options
*/
addEventListener(target, type, listener, options = {}) {
target.addEventListener(type, listener, options);
this._eventListeners.push({ target, type, listener, options });
}
/**
* Remove a specific event listener
* @param {EventTarget} target - Event target
* @param {string} type - Event type
* @param {Function} listener - Event listener
* @param {Object} options - Event listener options
*/
removeEventListener(target, type, listener, options = {}) {
target.removeEventListener(type, listener, options);
this._eventListeners = this._eventListeners.filter(
item => !(item.target === target &&
item.type === type &&
item.listener === listener)
);
}
/**
* Remove all event listeners registered through addEventListener
*/
removeAllEventListeners() {
this._eventListeners.forEach(({ target, type, listener, options }) => {
target.removeEventListener(type, listener, options);
});
this._eventListeners = [];
}
/**
* Get a reference to another module
* @param {string} moduleId - ID of the module to get
* @returns {BaseModule|null} - The module or null if not found
*/
getModule(moduleId) {
// First check our dependency cache
if (this._loadedDependencies.has(moduleId)) {
return this._loadedDependencies.get(moduleId);
}
// Then check in the registry
return window.moduleRegistry ?
window.moduleRegistry.getModule(moduleId) : null;
}
/**
* Auto-bind methods to preserve 'this' context
* @param {Array<string>} methodNames - Array of method names to bind
*/
bindMethods(methodNames) {
methodNames.forEach(methodName => {
if (typeof this[methodName] === 'function') {
this[methodName] = this[methodName].bind(this);
} else {
console.warn(`Method ${methodName} not found on ${this.id} module`);
}
});
}
/**
* Update configuration
* @param {Object} newConfig - New configuration to merge
*/
updateConfig(newConfig = {}) {
this.config = { ...this.config, ...newConfig };
}
/**
* Get current configuration
* @returns {Object} - Current configuration
*/
getConfig() {
return { ...this.config };
}
/**
* Load a JavaScript file
* @param {string} url - URL of the script to load
* @param {boolean} [isModule=false] - Whether to load as a module
* @returns {Promise<HTMLScriptElement>} - Promise resolving to the loaded script element
*/
loadScript(url, isModule = false) {
return new Promise((resolve, reject) => {
// Track this resource
this._trackResource(url);
const script = document.createElement('script');
script.src = url;
if (isModule) {
script.type = 'module';
}
script.onload = () => {
this._resourceLoaded(url);
resolve(script);
};
script.onerror = (error) => {
this._resourceFailed(url, error);
reject(new Error(`Failed to load script: ${url}`));
};
document.head.appendChild(script);
});
}
/**
* Load a CSS stylesheet
* @param {string} url - URL of the stylesheet to load
* @returns {Promise<HTMLLinkElement>} - Promise resolving to the loaded link element
*/
loadCSS(url) {
return new Promise((resolve, reject) => {
// Track this resource
this._trackResource(url);
const link = document.createElement('link');
link.href = url;
link.rel = 'stylesheet';
link.onload = () => {
this._resourceLoaded(url);
resolve(link);
};
link.onerror = (error) => {
this._resourceFailed(url, error);
reject(new Error(`Failed to load stylesheet: ${url}`));
};
document.head.appendChild(link);
});
}
/**
* Preload an image
* @param {string} url - URL of the image to load
* @returns {Promise<HTMLImageElement>} - Promise resolving to the loaded image element
*/
loadImage(url) {
return new Promise((resolve, reject) => {
// Track this resource
this._trackResource(url);
const img = new Image();
img.onload = () => {
this._resourceLoaded(url);
resolve(img);
};
img.onerror = (error) => {
this._resourceFailed(url, error);
reject(new Error(`Failed to load image: ${url}`));
};
img.src = url;
});
}
/**
* Load JSON data
* @param {string} url - URL of the JSON file to load
* @returns {Promise<Object>} - Promise resolving to the parsed JSON data
*/
loadJSON(url) {
return new Promise((resolve, reject) => {
// Track this resource
this._trackResource(url);
fetch(url)
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error ${response.status}`);
}
return response.json();
})
.then(data => {
this._resourceLoaded(url);
resolve(data);
})
.catch(error => {
this._resourceFailed(url, error);
reject(new Error(`Failed to load JSON: ${url} - ${error.message}`));
});
});
}
/**
* Load a generic resource with fetch
* @param {string} url - URL of the resource to load
* @param {string} [responseType='text'] - Response type ('text', 'blob', 'arrayBuffer', etc.)
* @returns {Promise<any>} - Promise resolving to the loaded resource
*/
loadResource(url, responseType = 'text') {
return new Promise((resolve, reject) => {
// Track this resource
this._trackResource(url);
fetch(url)
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error ${response.status}`);
}
switch(responseType) {
case 'json': return response.json();
case 'blob': return response.blob();
case 'arrayBuffer': return response.arrayBuffer();
case 'formData': return response.formData();
case 'text':
default: return response.text();
}
})
.then(data => {
this._resourceLoaded(url);
resolve(data);
})
.catch(error => {
this._resourceFailed(url, error);
reject(new Error(`Failed to load resource: ${url} - ${error.message}`));
});
});
}
/**
* Load multiple resources at once
* @param {Array<Object>} resources - Array of resource descriptors
* @returns {Promise<Array>} - Promise resolving to an array of loaded resources
* @example
* loadResources([
* { type: 'script', url: '/js/lib.js' },
* { type: 'css', url: '/css/style.css' },
* { type: 'image', url: '/img/logo.png' },
* { type: 'json', url: '/data/config.json' }
* ])
*/
loadResources(resources) {
const promises = resources.map(resource => {
switch(resource.type) {
case 'script':
return this.loadScript(resource.url, resource.isModule);
case 'css':
return this.loadCSS(resource.url);
case 'image':
return this.loadImage(resource.url);
case 'json':
return this.loadJSON(resource.url);
default:
return this.loadResource(resource.url, resource.responseType);
}
});
return Promise.all(promises);
}
/**
* Track a resource being loaded
* @param {string} url - URL of the resource
* @private
*/
_trackResource(url) {
this._loadingResources.set(url, {
started: Date.now(),
completed: false,
failed: false
});
this._totalResources++;
// Report progress
this._updateResourceProgress();
}
/**
* Mark a resource as successfully loaded
* @param {string} url - URL of the resource
* @private
*/
_resourceLoaded(url) {
if (this._loadingResources.has(url)) {
const resource = this._loadingResources.get(url);
resource.completed = true;
resource.completedAt = Date.now();
this._loadedResources++;
// Report progress
this._updateResourceProgress();
}
}
/**
* Mark a resource as failed to load
* @param {string} url - URL of the resource
* @param {Error} error - Error that occurred
* @private
*/
_resourceFailed(url, error) {
if (this._loadingResources.has(url)) {
const resource = this._loadingResources.get(url);
resource.failed = true;
resource.error = error;
resource.completedAt = Date.now();
this._loadedResources++;
// Log the error
console.error(`${this.id}: Failed to load resource:`, url, error);
// Report progress
this._updateResourceProgress();
}
}
/**
* Update loading progress based on resources
* @private
*/
_updateResourceProgress() {
if (this._totalResources === 0) return;
const percent = Math.round((this._loadedResources / this._totalResources) * 100);
this.reportProgress(percent, `Loading resources: ${this._loadedResources}/${this._totalResources}`);
}
/**
* Dispose resources when module is destroyed
* Override in child classes to add custom cleanup
*/
dispose() {
this.removeAllEventListeners();
}
}
/**