Refactored modules and updated loader.

This commit is contained in:
2025-04-06 18:35:04 +00:00
parent fc693ae695
commit 0ab639fd25
37 changed files with 3530 additions and 5989 deletions
+503 -88
View File
@@ -17,6 +17,7 @@ console.log('Module registry initialized and assigned to window.moduleRegistry')
const ModuleState = {
PENDING: 'PENDING',
LOADING: 'LOADING',
FETCHING: 'FETCHING', // Added new state for fetching resources
WAITING: 'WAITING',
INITIALIZING: 'INITIALIZING',
FINISHED: 'FINISHED',
@@ -37,6 +38,7 @@ const ModuleLoader = (function() {
let moduleWeights = {};
let createdModules = new Set(); // Track which modules we've created UI elements for
let gameLoopModule = null; // Add variable to hold game loop instance
let moduleTimings = {}; // Track timing data for modules
/**
* Initialize the loader
@@ -80,14 +82,15 @@ const ModuleLoader = (function() {
* Setup event listeners for module communication
*/
function setupEventListeners() {
// Listen for module progress events
document.addEventListener('module:progress', handleModuleProgress);
// Listen for module state change events
document.addEventListener('module:stateChange', handleModuleStateChange);
// Listen for module status message events
document.addEventListener('module:message', handleModuleMessage);
// Listen for module progress events
document.addEventListener('module:progress', handleModuleProgress);
}
/**
@@ -103,35 +106,35 @@ const ModuleLoader = (function() {
];
// Define modules with their weights
const modulesToLoad = [
let modulesToLoad = [
// Core functionality modules
{ id: 'persistence-manager', script: '/js/persistence-manager.js', weight: 40 },
{ id: 'localization', script: '/js/localization.js', weight: 40 },
{ id: 'text-processor', script: '/js/text-processor.js', weight: 40 },
{ id: 'paragraph-layout', script: '/js/paragraph-layout.js', weight: 40 },
{ id: 'layout-renderer', script: '/js/layout-renderer.js', weight: 45 }, // Add Layout Renderer module
{ id: 'animation-queue', script: '/js/animation-queue.js', weight: 50 },
{ id: 'persistence-manager', script: '/js/persistence-manager-module.js', weight: 12 },
{ id: 'localization', script: '/js/localization-module.js', weight: 12 },
{ id: 'text-processor', script: '/js/text-processor-module.js', weight: 15 },
{ id: 'paragraph-layout', script: '/js/paragraph-layout-module.js', weight: 17 },
{ id: 'layout-renderer', script: '/js/layout-renderer-module.js', weight: 13 }, // Add Layout Renderer module
{ id: 'animation-queue', script: '/js/animation-queue-module.js', weight: 12 },
// Audio and TTS modules
{ id: 'audio-manager', script: '/js/audio-manager.js', weight: 60 },
{ id: 'kokoro', script: '/js/kokoro-tts-module.js', weight: 65 },
{ id: 'browser', script: '/js/browser-tts-module.js', weight: 65 },
{ id: 'elevenlabs', script: '/js/elevenlabs-tts-module.js', weight: 65 },
{ id: 'openai', script: '/js/openai-tts-module.js', weight: 65 },
{ id: 'tts-factory', script: '/js/tts-factory.js', weight: 70 }, // TTSFactory must be loaded before TTSPlayer
{ id: 'tts', script: '/js/tts-player.js', weight: 75 },
{ id: 'audio-manager', script: '/js/audio-manager-module.js', weight: 12 },
{ id: 'kokoro', script: '/js/kokoro-tts-module.js', weight: 50 },
{ id: 'browser', script: '/js/browser-tts-module.js', weight: 12 },
{ id: 'elevenlabs', script: '/js/elevenlabs-tts-module.js', weight: 12 },
{ id: 'openai', script: '/js/openai-tts-module.js', weight: 12 },
{ id: 'tts-factory', script: '/js/tts-factory-module.js', weight: 13 }, // TTSFactory must be loaded before TTSPlayer
{ id: 'tts-player', script: '/js/tts-player-module.js', weight: 13 },
// UI and interaction modules
{ id: 'text-buffer', script: '/js/text-buffer.js', weight: 50 },
{ id: 'ui-effects', script: '/js/ui-effects.js', weight: 50 }, // Add UI Effects module
{ id: 'ui-input-handler', script: '/js/ui-input-handler.js', weight: 50 }, // Add UI Input Handler module
{ id: 'ui-display-handler', script: '/js/ui-display-handler.js', weight: 60 }, // Add UI Display Handler module
{ id: 'ui-controller', script: '/js/ui-controller.js', weight: 100 },
{ id: 'options-ui', script: '/js/options-ui.js', weight: 40 },
{ id: 'socket-client', script: '/js/socket-client.js', weight: 60 },
{ id: 'text-buffer', script: '/js/text-buffer-module.js', weight: 12 },
{ id: 'ui-effects', script: '/js/ui-effects-module.js', weight: 12 }, // Add UI Effects module
{ id: 'ui-input-handler', script: '/js/ui-input-handler-module.js', weight: 27 }, // Add UI Input Handler module
{ id: 'ui-display-handler', script: '/js/ui-display-handler-module.js', weight: 27 }, // Add UI Display Handler module
{ id: 'ui-controller', script: '/js/ui-controller-module.js', weight: 27 },
{ id: 'options-ui', script: '/js/options-ui-module.js', weight: 13 },
{ id: 'socket-client', script: '/js/socket-client-module.js', weight: 17 },
// Main game module - should be last to load
{ id: 'game-loop', script: '/js/game-loop.js', weight: 25 }
{ id: 'game-loop', script: '/js/game-loop-module.js', weight: 27 }
];
// Store module weights for progress calculation
@@ -141,7 +144,7 @@ const ModuleLoader = (function() {
// Create a module list entry for each module
modulesToLoad.forEach(module => {
createModuleListItem(module.id, getModuleNameFromId(module.id));
createModuleItem(module.id, getModuleNameFromId(module.id));
});
// Load dependencies first
@@ -150,7 +153,271 @@ const ModuleLoader = (function() {
// Load each module script
const loadPromises = modulesToLoad.map(module => loadScript(module.script));
return Promise.all(loadPromises);
const loadResult = await Promise.all(loadPromises);
// Wait briefly for modules to register
await new Promise(resolve => setTimeout(resolve, 100));
// Analyze dependencies and detect circular references
analyzeModuleDependencies();
return loadResult;
}
/**
* Analyze module dependencies to detect circular references and print detailed diagnostics
*/
function analyzeModuleDependencies() {
const registry = window.moduleRegistry;
if (!registry || !registry.modules) {
console.error("Module Registry not available for dependency analysis");
return;
}
// Build dependency graph
const graph = {};
// Initialize the graph with all modules
Object.keys(registry.modules).forEach(moduleId => {
graph[moduleId] = [];
});
// Add dependencies to graph
Object.entries(registry.modules).forEach(([moduleId, module]) => {
if (module.dependencies && Array.isArray(module.dependencies)) {
module.dependencies.forEach(depId => {
// Check if dependency exists
if (!registry.modules[depId]) {
console.warn(`Module ${moduleId} depends on missing module ${depId}`);
} else {
graph[moduleId].push(depId);
}
});
}
});
// Detect circular dependencies using DFS
const detectCycles = () => {
const visited = {};
const recStack = {};
const cycles = [];
const pathStack = [];
const dfs = (node, path = []) => {
// Node is already in recursion stack - we found a cycle
if (recStack[node]) {
const cycleStart = path.indexOf(node);
if (cycleStart !== -1) {
const cycle = path.slice(cycleStart).concat(node);
cycles.push(cycle);
return true;
}
}
// If already visited and not in recursion, no cycle through this node
if (visited[node]) {
return false;
}
// Mark node as visited and add to recursion stack
visited[node] = true;
recStack[node] = true;
pathStack.push(node);
// Visit all neighbors
const hasCycle = graph[node].some(neighbor => {
return dfs(neighbor, [...pathStack]);
});
// Remove from recursion stack and path
recStack[node] = false;
pathStack.pop();
return hasCycle;
};
// Start DFS from each node
Object.keys(graph).forEach(node => {
if (!visited[node]) {
dfs(node);
}
});
return cycles;
};
// Find all circular dependencies
const cycles = detectCycles();
// Display detailed information about circular dependencies
if (cycles.length > 0) {
console.group("%cCircular Dependencies Detected", "color: red; font-weight: bold");
cycles.forEach((cycle, index) => {
console.log(`%cCircular Dependency Chain ${index + 1}:`, "font-weight: bold");
// Print the cycle with dependency details
cycle.forEach((moduleId, i) => {
const module = registry.modules[moduleId] || { name: 'Unknown' };
const nextModuleId = cycle[(i + 1) % cycle.length];
const nextModule = registry.modules[nextModuleId] || { name: 'Unknown' };
console.log(
`%c${moduleId}%c (${module.name}) depends on %c${nextModuleId}%c (${nextModule.name})`,
"color: blue; font-weight: bold",
"color: black",
"color: blue; font-weight: bold",
"color: black"
);
// Print the actual dependencies declared by this module
if (module.dependencies && Array.isArray(module.dependencies)) {
console.log(` Dependencies declared by ${moduleId}:`, module.dependencies);
}
});
// Suggest potential solutions
console.log('%cPossible solutions:', 'font-weight: bold');
console.log('1. Remove one of the dependencies from the chain');
console.log('2. Use dynamic dependency resolution instead of static dependencies');
console.log('3. Create an interface module that both modules depend on');
console.log('4. Refactor module responsibilities to eliminate circular needs');
});
console.groupEnd();
} else {
console.log("%cNo circular dependencies detected", "color: green; font-weight: bold");
}
// Calculate an optimized loading order using topological sort
const calculateOptimalLoadOrder = () => {
const result = [];
const visited = {};
const temp = {}; // Temporary marks for detecting cycles
const visit = (node) => {
// Node is already in result, skip
if (visited[node]) return;
// If temp is true, we have a cycle
if (temp[node]) {
console.warn(`Skipping cycle involving ${node} during topological sort`);
return;
}
// Mark node as being processed
temp[node] = true;
// Process all dependencies first
if (graph[node]) {
graph[node].forEach(dep => visit(dep));
}
// Mark as visited and add to result
temp[node] = false;
visited[node] = true;
result.push(node);
};
// Visit all nodes
Object.keys(graph).forEach(node => {
if (!visited[node]) {
visit(node);
}
});
return result.reverse(); // Reverse to get correct order
};
const optimalOrder = calculateOptimalLoadOrder();
// Print the optimal loading order
console.group("%cOptimal Module Loading Order", "color: green; font-weight: bold");
optimalOrder.forEach((moduleId, index) => {
const module = registry.modules[moduleId] || { name: 'Unknown' };
console.log(`${index + 1}. %c${moduleId}%c (${module.name})`,
"font-weight: bold", "font-weight: normal");
});
console.groupEnd();
// Compare with actual loading order and suggest improvements
console.log("%cRecommended changes to module loading order in loader.js:", "font-weight: bold");
if (optimalOrder.length > 0) {
console.log("const modulesToLoad = [");
optimalOrder.forEach((moduleId, index) => {
// Generate a reasonable path based on the moduleId
const scriptPath = `/js/${moduleId}.js`;
console.log(` { id: '${moduleId}', script: '${scriptPath}', weight: ${index * 5 + 5} },`);
});
console.log("];");
}
}
/**
* Sort modules by their dependencies to create an optimal loading order
* This function can be used before initialization to ensure modules are loaded in the correct order
* @param {Array} modules - Array of module objects with id and dependencies
* @returns {Array} - Sorted array of modules
*/
function sortModulesByDependencies(modules) {
// Build a dependency graph
const graph = {};
// Initialize the graph with all modules
modules.forEach(module => {
graph[module.id] = { module, dependencies: [] };
});
// Add dependencies to the graph
// We need to do this in a second pass because some modules might reference others that come later in the array
modules.forEach(module => {
if (module.dependencies && Array.isArray(module.dependencies)) {
module.dependencies.forEach(depId => {
if (graph[depId]) {
graph[module.id].dependencies.push(depId);
} else {
console.warn(`Module ${module.id} depends on unknown module ${depId}`);
}
});
}
});
// Perform a topological sort
const result = [];
const visited = {};
const temp = {}; // For cycle detection
function visit(nodeId) {
// Node is already in result, skip
if (visited[nodeId]) return;
// If temp is true, we have a cycle
if (temp[nodeId]) {
console.warn(`Circular dependency detected involving ${nodeId}. Skipping.`);
return;
}
// Mark node as being processed
temp[nodeId] = true;
// Process all dependencies first
if (graph[nodeId] && graph[nodeId].dependencies) {
graph[nodeId].dependencies.forEach(depId => {
visit(depId);
});
}
// Mark as visited and add to result
temp[nodeId] = false;
visited[nodeId] = true;
result.push(graph[nodeId].module);
}
// Visit all nodes
Object.keys(graph).forEach(nodeId => {
if (!visited[nodeId]) {
visit(nodeId);
}
});
return result.reverse(); // Reverse for correct dependency order
}
/**
@@ -159,12 +426,78 @@ const ModuleLoader = (function() {
* @returns {Promise} - Resolves when script is loaded
*/
function loadScript(src) {
// Extract module ID from src path
const moduleId = src.split('/').pop().replace('.js', '');
// Update state to LOADING if this is a module
if (moduleId && moduleWeights[moduleId]) {
// Ensure module item exists in UI
const moduleItem = document.getElementById(`module-${moduleId}`);
if (!moduleItem) {
createModuleItem(moduleId, getModuleNameFromId(moduleId));
}
// Set initial progress to 0%
handleModuleProgress({
detail: { moduleId, progress: 0 }
});
// Set state to loading
updateModuleState(moduleId, ModuleState.LOADING);
// Record start time for this module (for timing data)
if (!moduleTimings[moduleId]) {
moduleTimings[moduleId] = {};
}
moduleTimings[moduleId].startTime = performance.now();
}
return new Promise((resolve, reject) => {
const script = document.createElement('script');
script.type = 'module';
script.src = src;
script.onload = () => resolve();
script.onerror = () => reject(new Error(`Failed to load script: ${src}`));
// Monitor loading progress using a fake progress indicator (0-10%)
if (moduleId && moduleWeights[moduleId]) {
let loadProgress = 0;
const progressInterval = setInterval(() => {
loadProgress = Math.min(loadProgress + 1, 9); // Max 9% until actual load completes
handleModuleProgress({
detail: { moduleId, progress: loadProgress }
});
}, 100);
script.onload = () => {
clearInterval(progressInterval);
// Final progress at 10% when script is loaded
handleModuleProgress({
detail: { moduleId, progress: 10 }
});
// Record script load complete time
if (moduleTimings[moduleId]) {
moduleTimings[moduleId].scriptLoadTime = performance.now();
}
resolve();
};
script.onerror = (error) => {
clearInterval(progressInterval);
updateModuleState(moduleId, ModuleState.ERROR);
// Record error time
if (moduleTimings[moduleId]) {
moduleTimings[moduleId].errorTime = performance.now();
}
reject(new Error(`Failed to load script: ${src}`));
};
} else {
script.onload = () => resolve();
script.onerror = () => reject(new Error(`Failed to load script: ${src}`));
}
document.head.appendChild(script);
});
}
@@ -292,27 +625,50 @@ const ModuleLoader = (function() {
}
/**
* Create a module list item in the UI
* Create a module list item
* @param {string} id - Module ID
* @param {string} name - Module display name
* @param {string} name - Module name
* @returns {HTMLLIElement} List item element
*/
function createModuleListItem(id, name) {
if (!modulesList) return;
// Check if we've already created this module item
if (createdModules.has(id)) return;
// Mark this module as created
function createModuleItem(id, name) {
if (!modulesList || createdModules.has(id)) return null;
createdModules.add(id);
const moduleItem = document.createElement('li');
moduleItem.className = 'module-item';
moduleItem.id = `module-${id}`;
moduleItem.innerHTML = `
<span class="module-name">${name}</span>
<span class="module-status status-pending">Pending</span>
`;
modulesList.appendChild(moduleItem);
// Create elements dynamically
const li = document.createElement('li');
li.id = `module-${id}`;
li.className = 'module-item';
// Set initial progress to 0 using CSS variable
li.style.setProperty('--progress-width', '0%');
// Create module name element
const moduleName = document.createElement('div');
moduleName.className = 'module-name';
moduleName.textContent = name;
// Create module status element
const moduleStatus = document.createElement('div');
moduleStatus.className = 'module-status status-pending';
moduleStatus.textContent = 'Pending';
// Create module status details element
const moduleDetails = document.createElement('div');
moduleDetails.className = 'module-status-detail';
moduleDetails.textContent = '';
// Append all elements to the list item
li.appendChild(moduleName);
li.appendChild(moduleStatus);
li.appendChild(moduleDetails);
// Force a reflow to ensure animation works
void li.offsetWidth;
// Add to modules list
modulesList.appendChild(li);
return li;
}
/**
@@ -320,7 +676,17 @@ const ModuleLoader = (function() {
*/
function handleModuleProgress(event) {
const { moduleId, progress } = event.detail;
updateModuleProgress(moduleId, progress);
// Get the module element
const moduleItem = document.querySelector(`#module-${moduleId}`);
if (moduleItem) {
// Update module item's before pseudo-element width using CSS variable
moduleItem.style.setProperty('--progress-width', `${progress}%`);
// Also set a data attribute for browsers that don't support CSS variables
moduleItem.setAttribute('data-progress', progress);
}
updateOverallProgress();
}
@@ -329,9 +695,37 @@ const ModuleLoader = (function() {
*/
function handleModuleStateChange(event) {
const { moduleId, state } = event.detail;
// Update UI with the new state
updateModuleState(moduleId, state);
// If module is finished, update overall completion
if (state === ModuleState.FINISHED) {
// This triggers only when ALL modules are complete, so modules would be removed too quickly
// if (areAllModulesComplete()) {
// hideLoadingOverlay();
// }
const moduleItem = document.getElementById(`module-${moduleId}`);
if (moduleItem) {
// Ensure module-finished class is added with a small delay to avoid race conditions
setTimeout(() => {
moduleItem.classList.add('module-finished');
}, 100);
}
}
updateOverallProgress();
// Record timing data
if (moduleTimings[moduleId]) {
moduleTimings[moduleId][state] = performance.now();
// If the module is finished or has an error, calculate total time
if (state === ModuleState.FINISHED || state === ModuleState.ERROR) {
const startTime = moduleTimings[moduleId].startTime || 0;
moduleTimings[moduleId].totalTime = performance.now() - startTime;
}
}
// Check if all modules are finished after each state change
checkAllFinished();
}
@@ -349,33 +743,45 @@ const ModuleLoader = (function() {
*/
function checkAllFinished() {
const modules = moduleRegistry.getAllModules();
const allFinished = Object.values(modules).every(module => {
// Add detailed logging of all module states
console.log('Module states:', Object.entries(modules).map(([id, module]) => {
return `${id}: ${module.getState()}`;
}));
// First determine which modules are pending
const pendingModules = Object.values(modules).filter(module => {
const state = module.getState();
return state === ModuleState.FINISHED || state === ModuleState.ERROR;
return state !== ModuleState.FINISHED && state !== ModuleState.ERROR;
});
// Log pending modules (if any)
if (pendingModules.length > 0) {
console.log('Modules still pending:', pendingModules.map(m => `${m.id} (${m.getState()})`));
} else {
console.log('No modules pending - all modules are in FINISHED or ERROR state');
}
// Determine if all modules are finished based on pendingModules
const allFinished = pendingModules.length === 0;
if (allFinished && !isLoadingComplete) {
console.log('All modules finished loading. Proceeding to finalization...');
finalizeLoading();
} else if (!allFinished) {
// Log which modules are not finished yet
const pendingModules = Object.values(modules).filter(module => {
const state = module.getState();
return state !== ModuleState.FINISHED && state !== ModuleState.ERROR;
});
if (pendingModules.length > 0) {
console.log('Modules still pending:', pendingModules.map(m => `${m.id} (${m.getState()})`))
}
} else if (allFinished && isLoadingComplete) {
console.log('All modules are finished but isLoadingComplete is already true');
}
}
/**
* Finalize the loading process
*/
function finalizeLoading() {
console.log('Loading completed. Finalizing...');
try {
// Display timing data
displayModuleTimings();
completeFinalization();
} catch (error) {
console.error('Error during finalization:', error);
@@ -409,6 +815,36 @@ const ModuleLoader = (function() {
}
}
/**
* Display module timing data to help with weight optimization
*/
function displayModuleTimings() {
console.group('Module Loading Performance Data');
console.log('This data can be used to optimize module weights:');
// Format timing data as tuples [moduleId, totalTime, weight]
const timingData = Object.entries(moduleTimings)
.filter(([moduleId, timing]) => timing.totalTime !== undefined)
.map(([moduleId, timing]) => {
return [moduleId, Math.round(timing.totalTime), moduleWeights[moduleId] || 1];
})
.sort((a, b) => b[1] - a[1]); // Sort by total time (descending)
// Create a table for easy reading
console.table(timingData.map(([moduleId, time, weight]) => {
return {
moduleId,
'totalTime (ms)': time,
'current weight': weight,
'suggested weight': Math.max(1, Math.round(time / 50)) // Simple heuristic based on time
};
}));
console.log('Raw timing data:');
console.table(moduleTimings);
console.groupEnd();
}
/**
* Hide the loading overlay with a fade out animation
* Then completely remove it from the DOM
@@ -445,16 +881,6 @@ const ModuleLoader = (function() {
}
});
// Fallback in case the transition event doesn't fire
setTimeout(() => {
if (loadingOverlay && loadingOverlay.parentNode) {
console.log('Module Loader: Removing overlay from DOM (fallback)');
loadingOverlay.parentNode.removeChild(loadingOverlay);
loadingOverlay = null;
}
// Execute callback in fallback as well
if (callback) callback();
}, 1000); // Wait longer than the transition duration
}
/**
@@ -477,11 +903,12 @@ const ModuleLoader = (function() {
'status-waiting',
'status-initializing',
'status-finished',
'status-error'
'status-error',
'status-fetching'
);
// Add appropriate class and text
let statusText = '';
// Set the new status
let statusText = 'Unknown';
switch (state) {
case ModuleState.PENDING:
statusElement.classList.add('status-pending');
@@ -491,6 +918,10 @@ const ModuleLoader = (function() {
statusElement.classList.add('status-loading');
statusText = 'Loading';
break;
case ModuleState.FETCHING:
statusElement.classList.add('status-fetching');
statusText = 'Fetching';
break;
case ModuleState.WAITING:
statusElement.classList.add('status-waiting');
statusText = 'Waiting';
@@ -508,25 +939,9 @@ const ModuleLoader = (function() {
statusText = 'Error';
break;
}
statusElement.textContent = statusText;
}
/**
* Update the progress of a module
* @param {string} id - Module ID
* @param {number} progress - Progress percentage (0-100)
*/
function updateModuleProgress(id, progress) {
// Module states are now managed by the module itself
// Update any additional UI elements for module progress if needed
const moduleItem = document.getElementById(`module-${id}`);
if (moduleItem) {
// Update progress display if needed
}
}
/**
* Update the status text of a module in the UI
* @param {string} id - Module ID