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:
+456
-13
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user