/** * 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_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}")`); } } // ===== 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); } } } updateSignalDisplay(signalName, value) { // Find DOM elements that should display this signal // For now, update any element with class matching signal name or data-signal attribute const selectors = [ `[data-signal="${signalName}"]`, `.${signalName}`, '.count', // Hardcoded for demo - TODO: track bindings properly ]; selectors.forEach(selector => { const elements = document.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(`Marker ${markerId} not found`); return; } const parent = marker.parentNode; if (!parent) { console.error(`Marker has no parent`); return; } // Remove old node if it exists (0 means no old node) if (oldNodeId !== 0) { const oldNode = this.nodes.get(oldNodeId); if (oldNode && oldNode.parentNode) { oldNode.parentNode.removeChild(oldNode); this._log(`Removed old node ${oldNodeId}`); } } // Insert new node after marker (only if newNodeId is not 0) if (newNodeId !== 0) { const newNode = this.nodes.get(newNodeId); if (!newNode) { console.error(`New node ${newNodeId} not found`); return; } parent.insertBefore(newNode, marker.nextSibling); this._log(`Inserted node ${newNodeId} at marker ${markerId}`); } } // ===== 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.onLoad = options.onLoad || (() => {}); this.onError = options.onError || ((err) => console.error(err)); } async load(wasmPath, componentName, props = {}) { try { 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_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_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 render function with props const renderFn = instance.exports[`render_${componentName}`]; if (!renderFn) { throw new Error(`render_${componentName} not found in WASM exports`); } // Pass props as positional arguments const propValues = Object.values(props); renderFn(...propValues); // 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); return { runtime, instance }; } catch (error) { this.setStatus(`❌ Error: ${error.message}`, 'error'); this.onError(error); throw error; } } 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; }