/** * Ritz Runtime - WebAssembly DOM Bridge * * Provides the runtime environment for Ritz components compiled to WASM. */ class RitzRuntime { constructor(rootElement) { this.rootElement = rootElement || document.getElementById('app'); this.nodes = new Map(); this.nextNodeId = 1; this.log = []; this.wasmInstance = null; this.debugEnabled = true; } // ===== DOM Manipulation Bridge ===== js_create_element(tagPtr) { const tag = this.readString(tagPtr); const element = document.createElement(tag); const id = this.nextNodeId++; this.nodes.set(id, element); this._log(`createElement("${tag}") = ${id}`); return id; } js_create_text(textPtr) { const text = this.readString(textPtr); const textNode = document.createTextNode(text); const id = this.nextNodeId++; this.nodes.set(id, textNode); this._log(`createTextNode("${text}") = ${id}`); return id; } js_append_child(parentId, childId) { const parent = this.nodes.get(parentId); const child = this.nodes.get(childId); if (parent && child) { parent.appendChild(child); this._log(`appendChild(${parentId}, ${childId})`); } } js_set_attribute(elemId, namePtr, valuePtr) { const element = this.nodes.get(elemId); const name = this.readString(namePtr); const value = this.readString(valuePtr); if (element) { if (name === 'class') { element.className = value; } else { element.setAttribute(name, value); } this._log(`setAttribute(${elemId}, "${name}", "${value}")`); } } js_bind_event(elemId, eventNamePtr, handlerNamePtr) { const element = this.nodes.get(elemId); const eventName = this.readString(eventNamePtr); const handlerName = this.readString(handlerNamePtr); if (element && this.wasmInstance) { const handler = this.wasmInstance.exports[handlerName]; if (handler) { element.addEventListener(eventName, () => { this._log(`[Event] ${eventName} → ${handlerName}()`); handler(); this.updateSignals(); }); this._log(`bindEvent(${elemId}, "${eventName}", "${handlerName}")`); } else { console.error(`Handler ${handlerName} not found in WASM exports`); } } } js_toggle_debug_console() { // Bridge function to toggle debug console from WASM if (window.ritzLoader) { window.ritzLoader.toggleDebugConsole(); } this._log('[Debug] Toggle debug console from WASM'); } js_set_root(elemId) { const element = this.nodes.get(elemId); if (element) { this.rootElement.innerHTML = ''; this.rootElement.appendChild(element); this._log(`setRoot(${elemId})`); } } js_update_text_content(nodeId, textPtr) { const node = this.nodes.get(nodeId); const text = this.readString(textPtr); if (node) { node.textContent = text; this._log(`updateTextContent(${nodeId}, "${text}")`); } } js_create_text_with_int(prefixPtr, intValue) { const prefix = this.readString(prefixPtr); const text = prefix + intValue.toString(); const textNode = document.createTextNode(text); const id = this.nextNodeId++; this.nodes.set(id, textNode); this._log(`createTextWithInt("${prefix}", ${intValue}) = "${text}" (id=${id})`); return id; } js_update_text_with_int(nodeId, prefixPtr, intValue) { const node = this.nodes.get(nodeId); const prefix = this.readString(prefixPtr); const text = prefix + intValue.toString(); if (node) { node.textContent = text; this._log(`updateTextWithInt(${nodeId}, "${prefix}", ${intValue}) = "${text}"`); } } // ===== Signal Management ===== updateSignals() { // Discover and update all signals if (!this.wasmInstance) return; const exports = this.wasmInstance.exports; // Find all get_* functions (signal getters) for (const [name, fn] of Object.entries(exports)) { if (name.startsWith('get_') && typeof fn === 'function') { const signalName = name.substring(4); // Remove 'get_' prefix const value = fn(); this._log(`[Signal] ${signalName} = ${value}`); // Update DOM elements bound to this signal // TODO: Track signal-to-DOM bindings during render for efficiency this.updateSignalDisplay(signalName, value); } } // Re-evaluate all reactive conditionals for (const [name, fn] of Object.entries(exports)) { if (name.startsWith('reevaluate_conditional_') && typeof fn === 'function') { fn(); this._log(`[Reactive] ${name}()`); } } // Re-evaluate all reactive match statements for (const [name, fn] of Object.entries(exports)) { if (name.startsWith('reevaluate_match_') && typeof fn === 'function') { fn(); this._log(`[Reactive] ${name}()`); } } // Update debug console stats if debug mode is enabled if (window.ritzLoader && this.debugEnabled) { window.ritzLoader.updateDebugStats(); } } updateSignalDisplay(signalName, value) { // Find DOM elements that should display this signal // IMPORTANT: Only search within this runtime's root element to avoid cross-contamination const selectors = [ `[data-signal="${signalName}"]`, `.${signalName}`, '.count', // Hardcoded for demo - TODO: track bindings properly ]; selectors.forEach(selector => { // Only query within this runtime's root element, not the entire document const elements = this.rootElement.querySelectorAll(selector); elements.forEach(el => { // Check if text contains the signal name or looks like a counter const text = el.textContent; if (text.includes(signalName) || text.match(/\d+/)) { // Replace the number in the text el.textContent = text.replace(/\d+/, value); } }); }); } // ===== Reactive Conditional Rendering ===== js_create_marker() { // Create a comment node as a marker for conditional insertion const marker = document.createComment('conditional-marker'); const id = this.nextNodeId++; this.nodes.set(id, marker); this._log(`Created marker node ${id}`); return id; } js_replace_at_marker(markerId, oldNodeId, newNodeId) { // Replace DOM node at marker position const marker = this.nodes.get(markerId); if (!marker) { console.error(`[replace_at_marker] Marker ${markerId} not found`); return; } const parent = marker.parentNode; if (!parent) { console.error(`[replace_at_marker] Marker ${markerId} has no parent`); return; } this._log(`[replace_at_marker] marker=${markerId}, old=${oldNodeId}, new=${newNodeId}`); // Remove old node if it exists (0 means no old node to remove) if (oldNodeId !== 0) { const oldNode = this.nodes.get(oldNodeId); if (!oldNode) { console.warn(`[replace_at_marker] Old node ${oldNodeId} not found in registry`); } else if (!oldNode.parentNode) { console.warn(`[replace_at_marker] Old node ${oldNodeId} not in DOM`); } else if (oldNode.parentNode !== parent) { console.warn(`[replace_at_marker] Old node ${oldNodeId} in wrong parent`); oldNode.parentNode.removeChild(oldNode); this._log(`[replace_at_marker] Removed ${oldNodeId} from wrong parent`); } else { // Remove from correct parent parent.removeChild(oldNode); this._log(`[replace_at_marker] Removed ${oldNodeId} from correct parent`); } } // Insert new node after marker (only if newNodeId is not 0) if (newNodeId !== 0) { const newNode = this.nodes.get(newNodeId); if (!newNode) { console.error(`[replace_at_marker] New node ${newNodeId} not found`); return; } // Insert right after the marker // If newNode is already in DOM elsewhere, this will move it const nextSibling = marker.nextSibling; if (nextSibling) { parent.insertBefore(newNode, nextSibling); this._log(`[replace_at_marker] Inserted ${newNodeId} before sibling`); } else { parent.appendChild(newNode); this._log(`[replace_at_marker] Appended ${newNodeId} (no sibling)`); } } } // ===== Memory & String Utilities ===== readString(ptr) { if (!this.memory) return ''; const bytes = new Uint8Array(this.memory.buffer, ptr); let length = 0; while (bytes[length] !== 0) length++; return new TextDecoder().decode(bytes.slice(0, length)); } setMemory(memory) { this.memory = memory; } setWasmInstance(instance) { this.wasmInstance = instance; } // ===== Logging & Debug ===== _log(message) { if (this.debugEnabled) { this.log.push(message); } } getLog() { return this.log; } clearLog() { this.log = []; } } /** * RitzLoader - Simplified WASM loading */ class RitzLoader { constructor(options = {}) { this.rootElement = options.rootElement || document.getElementById('app'); this.statusElement = options.statusElement || document.getElementById('status'); this.debug = options.debug !== undefined ? options.debug : false; this.onLoad = options.onLoad || (() => {}); this.onError = options.onError || ((err) => console.error(err)); this.debugConsoleRuntime = null; } async load(wasmPath, componentName = null, props = {}) { try { // Auto-detect component name from wasm filename if not provided if (!componentName) { // Extract from path: "Main.wasm" → "Main", "path/to/Main.wasm" → "Main" componentName = wasmPath.split('/').pop().replace('.wasm', ''); } this.setStatus('⏳ Creating runtime...'); const runtime = new RitzRuntime(this.rootElement); this.setStatus(`⏳ Loading ${wasmPath}...`); const response = await fetch(wasmPath); if (!response.ok) { throw new Error(`Failed to load ${wasmPath}: ${response.status}`); } const wasmBytes = await response.arrayBuffer(); this.setStatus('⏳ Instantiating WASM...'); // Create imports for WASM const imports = { env: { js_create_element: (ptr) => runtime.js_create_element(ptr), js_create_text: (ptr) => runtime.js_create_text(ptr), js_create_text_with_int: (p, i) => runtime.js_create_text_with_int(p, i), js_append_child: (p, c) => runtime.js_append_child(p, c), js_set_attribute: (e, n, v) => runtime.js_set_attribute(e, n, v), js_set_root: (e) => runtime.js_set_root(e), js_bind_event: (e, n, h) => runtime.js_bind_event(e, n, h), js_update_text_content: (n, t) => runtime.js_update_text_content(n, t), js_update_text_with_int: (n, p, i) => runtime.js_update_text_with_int(n, p, i), js_create_marker: () => runtime.js_create_marker(), js_replace_at_marker: (m, o, n) => runtime.js_replace_at_marker(m, o, n), } }; const { instance } = await WebAssembly.instantiate(wasmBytes, imports); // Setup runtime runtime.setMemory(instance.exports.memory); runtime.setWasmInstance(instance); this.setStatus(`⏳ Rendering ${componentName}...`); // Call the main entry point (which renders top-level component and sets root) const mainFn = instance.exports.main; if (!mainFn) { throw new Error(`main function not found in WASM exports`); } mainFn(); // Update signals runtime.updateSignals(); this.setStatus(`✅ ${componentName} loaded!`, 'success'); // Make runtime globally available for debugging window.ritzRuntime = runtime; window.ritzInstance = instance; this.onLoad(runtime, instance); // Load debug console if debug mode is enabled if (this.debug) { await this.loadDebugConsole(componentName); } return { runtime, instance }; } catch (error) { this.setStatus(`❌ Error: ${error.message}`, 'error'); this.onError(error); throw error; } } async loadDebugConsole(appComponentName) { try { console.log('[Debug] Loading debug console...'); // Create a container for the debug console const debugContainer = document.createElement('div'); debugContainer.id = 'debug-console-root'; document.body.appendChild(debugContainer); console.log('[Debug] Created container:', debugContainer); // Create debug console runtime const debugRuntime = new RitzRuntime(debugContainer); debugRuntime.debugEnabled = false; // Don't log debug console's own operations // Load DebugConsole.wasm from runtime directory const debugWasmPath = '../runtime/DebugConsole.wasm'; console.log('[Debug] Fetching:', debugWasmPath); const response = await fetch(debugWasmPath); if (!response.ok) { console.warn('Could not load debug console:', response.status); return; } console.log('[Debug] DebugConsole.wasm loaded successfully'); const wasmBytes = await response.arrayBuffer(); // Create imports for debug console WASM const imports = { env: { js_create_element: (ptr) => debugRuntime.js_create_element(ptr), js_create_text: (ptr) => debugRuntime.js_create_text(ptr), js_create_text_with_int: (p, i) => debugRuntime.js_create_text_with_int(p, i), js_append_child: (p, c) => debugRuntime.js_append_child(p, c), js_set_attribute: (e, n, v) => debugRuntime.js_set_attribute(e, n, v), js_set_root: (e) => debugRuntime.js_set_root(e), js_bind_event: (e, n, h) => debugRuntime.js_bind_event(e, n, h), js_update_text_content: (n, t) => debugRuntime.js_update_text_content(n, t), js_update_text_with_int: (n, p, i) => debugRuntime.js_update_text_with_int(n, p, i), js_create_marker: () => debugRuntime.js_create_marker(), js_replace_at_marker: (m, o, n) => debugRuntime.js_replace_at_marker(m, o, n), js_toggle_debug_console: () => debugRuntime.js_toggle_debug_console(), } }; const { instance } = await WebAssembly.instantiate(wasmBytes, imports); // Setup debug runtime debugRuntime.setMemory(instance.exports.memory); debugRuntime.setWasmInstance(instance); // Render debug console console.log('[Debug] Rendering debug console...'); instance.exports.main(); debugRuntime.updateSignals(); this.debugConsoleRuntime = debugRuntime; console.log('[Debug] Debug console rendered. Checking DOM...'); const debugElement = document.querySelector('.ritz-debug'); console.log('[Debug] Debug element:', debugElement); console.log('[Debug] Debug element classes:', debugElement?.className); // Hide the loader status element (we have our own in debug console) if (this.statusElement) { this.statusElement.style.display = 'none'; } // Populate runtime stats this.updateDebugStats(appComponentName); // Setup keyboard shortcut (Ctrl+`) document.addEventListener('keydown', (e) => { if (e.ctrlKey && e.key === '`') { e.preventDefault(); console.log('[Debug] Toggle debug console'); this.toggleDebugConsole(); } }); // Make debug console and loader globally available window.ritzDebugConsole = debugRuntime; window.ritzLoader = this; console.log('🎭 Ritz Debug Console loaded! Press Ctrl+` to toggle.'); } catch (error) { console.warn('Failed to load debug console:', error); } } toggleDebugConsole() { // Toggle the debug console by toggling body class // The Ritz debug console component manages its own visibility via signal // But we need to trigger the body class for layout document.body.classList.toggle('debug-open'); // Find the debug console element and toggle its hidden class const debugElement = document.querySelector('.ritz-debug'); if (debugElement) { debugElement.classList.toggle('hidden'); } } updateDebugStats(componentName = 'Main') { // Update status with component name (inside debug console) const statusEl = document.querySelector('#ritz-debug .status'); if (statusEl) { statusEl.textContent = `✅ ${componentName} loaded!`; statusEl.className = 'status success'; } // Update component section const componentPre = document.querySelector('#ritz-debug .debug-section:nth-child(1) pre'); if (componentPre && window.ritzInstance) { const wasmSize = window.ritzInstance.exports.memory.buffer.byteLength; componentPre.textContent = `${componentName}.wasm (${Math.round(wasmSize / 1024)}KB)`; } // Update runtime stats const statsPre = document.querySelector('#ritz-debug .debug-section:nth-child(3) pre'); if (statsPre && window.ritzRuntime && window.ritzInstance) { const memory = window.ritzInstance.exports.memory.buffer.byteLength; const memoryKB = Math.round(memory / 1024); const memoryMB = (memory / 1024 / 1024).toFixed(2); const nodeCount = window.ritzRuntime.nodes.size; // Count signals (globals that start with @signal_) const exports = Object.keys(window.ritzInstance.exports); const signalCount = exports.filter(name => name.startsWith('get_') || name.startsWith('set_') ).length / 2; statsPre.textContent = `Memory: ${memoryKB}KB (${memoryMB}MB)\n` + `Signals: ${signalCount} reactive\n` + `DOM nodes: ${nodeCount} cached\n` + `Log entries: ${window.ritzRuntime.log.length}`; } } setStatus(message, type = '') { if (this.statusElement) { this.statusElement.textContent = message; this.statusElement.className = 'status ' + type; } } } // Export for use in modules or global scope if (typeof module !== 'undefined' && module.exports) { module.exports = { RitzRuntime, RitzLoader }; } else { window.RitzRuntime = RitzRuntime; window.RitzLoader = RitzLoader; }