juris
Version:
JavaScript Unified Reactive Interface Solution - Transforms web development through comprehensive object-first architecture that makes reactivity an intentional choice rather than automatic behavior
1,319 lines (1,101 loc) • 111 kB
JavaScript
/**
* Caution: This is not just a framework, its a paradigm-shifting platform.
* Juris (JavaScript Unified Reactive Interface Solution)
* Transforms web development through its comprehensive object-first architecture that makes
* reactivity an intentional choice rather than an automatic behavior. By expressing interfaces
* as pure JavaScript objects where functions explicitly define reactivity, Juris delivers a
* complete solution for applications that are universally deployable, precisely controlled,
* and designed from the ground up for seamless AI collaboration—all while maintaining the
* simplicity and debuggability of native JavaScript patterns.
*
* Author: Resti Guay
* Maintained by: Juris Github Team
* Version: 0.5.2 (stable)
* License: MIT
* GitHub: https://github.com/jurisjs/juris
* Website: https://jurisjs.com
* Documentation: https://jurisjs.com/#docs
*/
(function () {
'use strict';
/**
* Utility functions
*/
function isValidPath(path) {
return typeof path === 'string' && path.trim().length > 0 && !path.includes('..');
}
function getPathParts(path) {
return path.split('.').filter(Boolean);
}
function deepEquals(a, b) {
if (a === b) return true;
if (a == null || b == null) return false;
if (typeof a !== typeof b) return false;
if (typeof a === 'object') {
if (Array.isArray(a) !== Array.isArray(b)) return false;
const keysA = Object.keys(a);
const keysB = Object.keys(b);
if (keysA.length !== keysB.length) return false;
for (let key of keysA) {
if (!keysB.includes(key) || !deepEquals(a[key], b[key])) {
return false;
}
}
return true;
}
return false;
}
/**
* State Manager - Handles reactive state with middleware support
*/
class StateManager {
constructor(initialState = {}, middleware = []) {
this.state = { ...initialState };
this.middleware = [...middleware];
this.subscribers = new Map();
this.externalSubscribers = new Map();
this.currentTracking = null;
this.isUpdating = false;
// Batch update system
this.updateQueue = [];
this.batchTimeout = null;
this.batchUpdateInProgress = false;
this.maxBatchSize = 50;
this.batchDelayMs = 0; // ✅ FIXED: Non-zero default for batching
this.batchingEnabled = true; // ✅ FIXED: Add batching control
this.initialState = JSON.parse(JSON.stringify(initialState));
}
reset(preserve = []) {
// Collect preserved values
const preserved = {};
preserve.forEach(path => {
const value = this.getState(path);
if (value !== null) {
preserved[path] = value;
}
});
// Clear current state
this.state = {};
// Restore initial state
Object.entries(this.initialState).forEach(([path, value]) => {
this.setState(path, JSON.parse(JSON.stringify(value)));
});
// Restore preserved values
Object.entries(preserved).forEach(([path, value]) => {
this.setState(path, value);
});
}
getState(path, defaultValue = null) {
if (!isValidPath(path)) {
console.warn('Invalid state path:', path);
return defaultValue;
}
if (this.currentTracking) {
this.currentTracking.add(path);
}
const parts = getPathParts(path);
let current = this.state;
for (const part of parts) {
if (current == null || current[part] === undefined) {
return defaultValue;
}
current = current[part];
}
return current;
}
setState(path, value, context = {}) {
if (!isValidPath(path)) {
console.warn('Invalid state path:', path);
return;
}
if (this._hasCircularUpdate(path)) {
return;
}
// Route to batching if enabled
if (this.batchingEnabled && this.batchDelayMs > 0) {
this._queueUpdate(path, value, context);
return;
}
this._setStateImmediate(path, value, context);
}
_setStateImmediate(path, value, context = {}) {
const oldValue = this.getState(path);
let finalValue = value;
for (const middleware of this.middleware) {
try {
const result = middleware({
path,
oldValue,
newValue: finalValue,
context,
state: this.state
});
if (result !== undefined) {
finalValue = result;
}
} catch (error) {
console.error('Middleware error:', error);
}
}
if (deepEquals(oldValue, finalValue)) {
return;
}
const parts = getPathParts(path);
let current = this.state;
for (let i = 0; i < parts.length - 1; i++) {
const part = parts[i];
if (current[part] == null || typeof current[part] !== 'object') {
current[part] = {};
}
current = current[part];
}
current[parts[parts.length - 1]] = finalValue;
if (!this.isUpdating) {
this.isUpdating = true;
if (!this.currentlyUpdating) {
this.currentlyUpdating = new Set();
}
this.currentlyUpdating.add(path);
this._notifySubscribers(path, finalValue, oldValue);
this._notifyExternalSubscribers(path, finalValue, oldValue); // ✅ Now hierarchical
this.currentlyUpdating.delete(path);
this.isUpdating = false;
}
}
_processBatchedUpdates() {
if (this.batchUpdateInProgress || this.updateQueue.length === 0) {
return;
}
this.batchUpdateInProgress = true;
if (this.batchTimeout) {
clearTimeout(this.batchTimeout);
this.batchTimeout = null;
}
const batchSize = Math.min(this.maxBatchSize, this.updateQueue.length);
const currentBatch = this.updateQueue.splice(0, batchSize);
try {
// ✅ FIXED: Group updates by path and take latest value
const pathGroups = new Map();
currentBatch.forEach(update => {
pathGroups.set(update.path, update); // Latest update wins
});
// ✅ FIXED: Process unique paths only
const affectedPaths = new Set();
pathGroups.forEach((update) => {
this._setStateImmediate(update.path, update.value, update.context);
affectedPaths.add(update.path);
});
console.log(`Batched ${currentBatch.length} updates into ${pathGroups.size} unique state changes`);
} catch (error) {
console.error('Error processing batched updates:', error);
} finally {
this.batchUpdateInProgress = false;
if (this.updateQueue.length > 0) {
setTimeout(() => this._processBatchedUpdates(), 0);
}
}
}
configureBatching(options = {}) {
this.maxBatchSize = options.maxBatchSize || this.maxBatchSize;
this.batchDelayMs = options.batchDelayMs !== undefined ? options.batchDelayMs : this.batchDelayMs;
if (options.enabled !== undefined) {
this.batchingEnabled = options.enabled;
}
console.log(`Batching configured: enabled=${this.batchingEnabled}, delay=${this.batchDelayMs}ms, maxSize=${this.maxBatchSize}`);
}
getBatchStatus() {
return {
enabled: this.batchingEnabled,
queueLength: this.updateQueue.length,
inProgress: this.batchUpdateInProgress,
hasTimeout: !!this.batchTimeout,
delayMs: this.batchDelayMs,
maxBatchSize: this.maxBatchSize
};
}
_queueUpdate(path, value, context) {
this.updateQueue.push({ path, value, context, timestamp: Date.now() });
if (this.updateQueue.length > this.maxBatchSize * 2) {
console.warn('Update queue is getting large, processing immediately');
this._processBatchedUpdates();
return;
}
if (!this.batchTimeout) {
this.batchTimeout = setTimeout(() => {
this._processBatchedUpdates();
}, this.batchDelayMs);
}
}
subscribe(path, callback, hierarchical = true) {
if (!this.externalSubscribers.has(path)) {
this.externalSubscribers.set(path, new Set());
}
// Store callback with hierarchical flag
const subscription = { callback, hierarchical };
this.externalSubscribers.get(path).add(subscription);
return () => {
const subs = this.externalSubscribers.get(path);
if (subs) {
subs.delete(subscription);
if (subs.size === 0) {
this.externalSubscribers.delete(path);
}
}
};
}
subscribeExact(path, callback) {
return this.subscribe(path, callback, false);
}
subscribeInternal(path, callback) {
if (!this.subscribers.has(path)) {
this.subscribers.set(path, new Set());
}
this.subscribers.get(path).add(callback);
return () => {
const subs = this.subscribers.get(path);
if (subs) {
subs.delete(callback);
if (subs.size === 0) {
this.subscribers.delete(path);
}
}
};
}
_notifySubscribers(path, newValue, oldValue) {
this._triggerPathSubscribers(path);
const parts = getPathParts(path);
for (let i = parts.length - 1; i > 0; i--) {
const parentPath = parts.slice(0, i).join('.');
this._triggerPathSubscribers(parentPath);
}
const prefix = path ? path + '.' : '';
const allSubscriberPaths = new Set([
...this.subscribers.keys(),
...this.externalSubscribers.keys()
]);
allSubscriberPaths.forEach(subscriberPath => {
if (subscriberPath.startsWith(prefix) && subscriberPath !== path) {
this._triggerPathSubscribers(subscriberPath);
}
});
}
_notifyExternalSubscribers(changedPath, newValue, oldValue) {
// Check all external subscribers
this.externalSubscribers.forEach((subscriptions, subscribedPath) => {
subscriptions.forEach(subscription => {
const { callback, hierarchical } = subscription;
let shouldNotify = false;
if (hierarchical) {
// Hierarchical: notify if changedPath is under subscribedPath
if (changedPath === subscribedPath ||
changedPath.startsWith(subscribedPath + '.')) {
shouldNotify = true;
}
} else {
// Exact: notify only if paths match exactly
if (changedPath === subscribedPath) {
shouldNotify = true;
}
}
if (shouldNotify) {
try {
callback(newValue, oldValue, changedPath);
} catch (error) {
console.error('External subscriber error:', error);
}
}
});
});
}
_triggerPathSubscribers(path) {
const subs = this.subscribers.get(path);
if (subs) {
const subscribersCopy = new Set(subs);
subscribersCopy.forEach(callback => {
try {
const oldTracking = this.currentTracking;
const newTracking = new Set();
this.currentTracking = newTracking;
callback();
this.currentTracking = oldTracking;
newTracking.forEach(newPath => {
const existingSubs = this.subscribers.get(newPath);
if (!existingSubs || !existingSubs.has(callback)) {
this.subscribeInternal(newPath, callback);
}
});
} catch (error) {
console.error('Subscriber error:', error);
this.currentTracking = oldTracking;
}
});
}
}
_hasCircularUpdate(path) {
if (!this.currentlyUpdating) {
this.currentlyUpdating = new Set();
}
if (this.currentlyUpdating.has(path)) {
console.warn(`Circular dependency detected for path: ${path}`);
return true;
}
return false;
}
startTracking() {
const dependencies = new Set();
this.currentTracking = dependencies;
return dependencies;
}
endTracking() {
const tracking = this.currentTracking;
this.currentTracking = null;
return tracking || new Set();
}
}
/**
* Headless Manager - Enhanced with better lifecycle management
*/
class HeadlessManager {
constructor(juris) {
this.juris = juris;
this.components = new Map();
this.instances = new Map();
this.context = {};
this.initQueue = new Set();
this.lifecycleHooks = new Map();
}
register(name, componentFn, options = {}) {
this.components.set(name, { fn: componentFn, options });
if (options.autoInit) {
this.initQueue.add(name);
}
}
initialize(name, props = {}) {
const component = this.components.get(name);
if (!component) {
console.warn(`Headless component '${name}' not found`);
return null;
}
try {
const context = this.juris.createHeadlessContext();
const instance = component.fn(props, context);
if (!instance || typeof instance !== 'object') {
console.warn(`Headless component '${name}' must return an object`);
return null;
}
this.instances.set(name, instance);
if (instance.hooks) {
this.lifecycleHooks.set(name, instance.hooks);
}
if (instance.api) {
this.context[name] = instance.api;
if (!this.juris.headlessAPIs) {
this.juris.headlessAPIs = {};
}
this.juris.headlessAPIs[name] = instance.api;
this.juris._updateComponentContexts();
}
if (instance.hooks?.onRegister) {
try {
instance.hooks.onRegister();
} catch (error) {
console.error(`Error in onRegister for headless component '${name}':`, error);
}
}
return instance;
} catch (error) {
console.error(`Error initializing headless component '${name}':`, error);
return null;
}
}
initializeQueued() {
this.initQueue.forEach(name => {
if (!this.instances.has(name)) {
const component = this.components.get(name);
this.initialize(name, component.options || {});
}
});
this.initQueue.clear();
}
getInstance(name) {
return this.instances.get(name);
}
getAPI(name) {
return this.context[name];
}
getAllAPIs() {
return { ...this.context };
}
reinitialize(name, props = {}) {
if (this.instances.has(name)) {
const instance = this.instances.get(name);
if (instance.hooks?.onUnregister) {
try {
instance.hooks.onUnregister();
} catch (error) {
console.error(`Error in onUnregister for '${name}':`, error);
}
}
}
if (this.context[name]) {
delete this.context[name];
}
if (this.juris.headlessAPIs?.[name]) {
delete this.juris.headlessAPIs[name];
}
this.instances.delete(name);
this.lifecycleHooks.delete(name);
return this.initialize(name, props);
}
cleanup() {
this.instances.forEach((instance, name) => {
if (instance.hooks?.onUnregister) {
try {
instance.hooks.onUnregister();
} catch (error) {
console.error(`Error in onUnregister for '${name}':`, error);
}
}
});
this.instances.clear();
this.context = {};
this.lifecycleHooks.clear();
if (this.juris.headlessAPIs) {
this.juris.headlessAPIs = {};
}
}
getStatus() {
return {
registered: Array.from(this.components.keys()),
initialized: Array.from(this.instances.keys()),
queued: Array.from(this.initQueue),
apis: Object.keys(this.context)
};
}
}
/**
* Component Manager - Handles UI components with lifecycle
*/
class ComponentManager {
constructor(juris) {
this.juris = juris;
this.components = new Map();
this.instances = new WeakMap();
this.componentCounters = new Map(); // Track instances per component name
this.componentStates = new WeakMap();
}
register(name, componentFn) {
this.components.set(name, componentFn);
}
create(name, props = {}) {
const componentFn = this.components.get(name);
if (!componentFn) {
console.error(`Component '${name}' not found`);
return null;
}
try {
if (!this.componentCounters.has(name)) {
this.componentCounters.set(name, 0);
}
const currentCount = this.componentCounters.get(name);
const instanceIndex = currentCount + 1;
this.componentCounters.set(name, instanceIndex);
const componentId = `${name}_${instanceIndex}`;
const componentStates = new Set();
const context = this.juris.createContext();
// Add newState function to context
context.newState = (key, initialValue) => {
const statePath = `__local.${componentId}.${key}`;
// Set initial value if not exists
if (this.juris.stateManager.getState(statePath, Symbol('not-found')) === Symbol('not-found')) {
this.juris.stateManager.setState(statePath, initialValue);
}
// Track this state for cleanup
componentStates.add(statePath);
const getter = () => this.juris.stateManager.getState(statePath, initialValue);
const setter = (value) => this.juris.stateManager.setState(statePath, value);
return [getter, setter];
};
const result = componentFn(props, context);
if (result && typeof result === 'object') {
// Check for lifecycle component first
if (result.onMount || result.onUpdate || result.onUnmount ||
(typeof result.render === 'function' && (result.onMount !== undefined || result.onUpdate !== undefined || result.onUnmount !== undefined))) {
return this._createLifecycleComponent(result, name, props, componentStates);
}
// Check for render function pattern
if (typeof result.render === 'function' && !result.onMount && !result.onUpdate && !result.onUnmount) {
const renderResult = result.render();
console.log(`Render function for '${name}' returned:`, renderResult);
const element = this.juris.domRenderer.render(renderResult);
if (element && componentStates.size > 0) {
this.componentStates.set(element, componentStates);
}
return element;
}
// Direct VDOM return - check if it has valid tag names
const keys = Object.keys(result);
if (keys.length === 1) {
const tagName = keys[0];
// Valid HTML tag names or registered components
if (typeof tagName === 'string' && tagName.length > 0) {
const element = this.juris.domRenderer.render(result);
if (element && componentStates.size > 0) {
this.componentStates.set(element, componentStates);
}
return element;
}
}
}
// Fallback
console.warn(`Component '${name}' returned unexpected structure, attempting to render:`, result);
const element = this.juris.domRenderer.render(result);
if (element && componentStates.size > 0) {
this.componentStates.set(element, componentStates);
}
return element;
} catch (error) {
console.error(`Error creating component '${name}':`, error);
return this._createErrorElement(error);
}
}
// Helper method to detect VDOM structure
_isVDOMStructure(obj) {
if (!obj || typeof obj !== 'object') return false;
const keys = Object.keys(obj);
if (keys.length !== 1) return false;
const tagName = keys[0];
// Check if it's a valid HTML tag name or component name
return typeof tagName === 'string' &&
(this.juris.componentManager.components.has(tagName) ||
/^[a-zA-Z][a-zA-Z0-9-]*$/.test(tagName));
}
_createLifecycleComponent(componentResult, name, props, componentStates) {
const instance = {
name,
props,
hooks: componentResult.hooks || {},
api: componentResult.api || {},
render: componentResult.render
};
const element = this.juris.domRenderer.render(instance.render());
if (element) {
this.instances.set(element, instance);
// Store component states for cleanup
if (componentStates && componentStates.size > 0) {
this.componentStates.set(element, componentStates);
}
if (instance.hooks.onMount) {
setTimeout(() => {
if (element.isConnected) {
try {
instance.hooks.onMount();
} catch (error) {
console.error(`onMount error in ${name}:`, error);
}
}
}, 0);
}
}
return element;
}
updateInstance1(element, newProps) {
const instance = this.instances.get(element);
if (!instance) return;
const oldProps = instance.props;
// ✅ NEW: Check if props have actually changed using deep comparison
if (deepEquals(oldProps, newProps)) {
console.log(`Skipping re-render for ${instance.name} - props unchanged`);
return; // Skip re-render if props are identical
}
instance.props = newProps;
if (instance.hooks.onUpdate) {
try {
instance.hooks.onUpdate(oldProps, newProps);
} catch (error) {
console.error(`onUpdate error in ${instance.name}:`, error);
}
}
try {
const newContent = instance.render();
this.juris.domRenderer.updateElementContent(element, newContent);
} catch (error) {
console.error(`Re-render error in ${instance.name}:`, error);
}
}
cleanup(element) {
const instance = this.instances.get(element);
if (instance && instance.hooks.onUnmount) {
try {
instance.hooks.onUnmount();
} catch (error) {
console.error(`onUnmount error in ${instance.name}:`, error);
}
}
// Cleanup component local states
const states = this.componentStates.get(element);
if (states) {
states.forEach(statePath => {
// Remove from global state
const pathParts = statePath.split('.');
let current = this.juris.stateManager.state;
for (let i = 0; i < pathParts.length - 1; i++) {
if (current[pathParts[i]]) {
current = current[pathParts[i]];
} else {
return; // Path doesn't exist
}
}
delete current[pathParts[pathParts.length - 1]];
});
this.componentStates.delete(element);
}
this.instances.delete(element);
}
_createErrorElement(error) {
const element = document.createElement('div');
element.style.cssText = 'color: red; border: 1px solid red; padding: 8px; background: #ffe6e6;';
element.textContent = `Component Error: ${error.message}`;
return element;
}
}
/**
* OPTIMIZED DOM Renderer with renderMode support
*/
class DOMRenderer {
constructor(juris) {
this.juris = juris;
this.subscriptions = new WeakMap();
this.eventMap = {
ondoubleclick: 'dblclick',
onmousedown: 'mousedown',
onmouseup: 'mouseup',
onmouseover: 'mouseover',
onmouseout: 'mouseout',
onmousemove: 'mousemove',
onkeydown: 'keydown',
onkeyup: 'keyup',
onkeypress: 'keypress',
onfocus: 'focus',
onblur: 'blur',
onchange: 'change',
oninput: 'input',
onsubmit: 'submit',
onload: 'load',
onresize: 'resize',
onscroll: 'scroll'
};
// VDOM-style optimizations
this.elementCache = new Map();
this.recyclePool = new Map();
this.renderQueue = [];
this.isRendering = false;
this.scheduledRender = null;
// Performance settings
this.batchSize = 20;
this.recyclePoolSize = 100;
// RENDER MODE: Choose between fine-grained and batch rendering
this.renderMode = 'fine-grained'; // 'batch' or 'fine-grained'
this.failureCount = 0;
this.maxFailures = 3;
this.renderStats = {
totalUpdates: 0,
skippedUpdates: 0,
lastReset: Date.now()
};
}
getRenderStats() {
const now = Date.now();
const duration = (now - this.renderStats.lastReset) / 1000;
const skipRate = this.renderStats.totalUpdates > 0
? (this.renderStats.skippedUpdates / this.renderStats.totalUpdates * 100).toFixed(1)
: 0;
return {
totalUpdates: this.renderStats.totalUpdates,
skippedUpdates: this.renderStats.skippedUpdates,
skipRate: `${skipRate}%`,
duration: `${duration.toFixed(1)}s`
};
}
resetRenderStats() {
this.renderStats = {
totalUpdates: 0,
skippedUpdates: 0,
lastReset: Date.now()
};
}
// PUBLIC: Set render mode
setRenderMode(mode) {
if (mode === 'fine-grained' || mode === 'batch') {
this.renderMode = mode;
console.log(`Juris: Render mode set to '${mode}'`);
if (mode === 'fine-grained') {
console.log(' → Using direct DOM updates (more compatible)');
} else {
console.log(' → Using VDOM-style reconciliation (higher performance)');
}
} else {
console.warn(`Invalid render mode '${mode}'. Use 'fine-grained' or 'batch'`);
}
}
getRenderMode() {
return this.renderMode;
}
isFineGrained() {
return this.renderMode === 'fine-grained';
}
isBatchMode() {
return this.renderMode === 'batch';
}
// DEPRECATED: Legacy method names for backward compatibility
setLegacyMode(enabled) {
console.warn('setLegacyMode() is deprecated. Use setRenderMode() instead.');
this.setRenderMode(enabled ? 'fine-grained' : 'batch');
}
isLegacyMode() {
console.warn('isLegacyMode() is deprecated. Use isFineGrained() instead.');
return this.isFineGrained();
}
render(vnode) {
if (!vnode || typeof vnode !== 'object') {
return null;
}
// ✅ NEW: Handle arrays of vnodes
if (Array.isArray(vnode)) {
//console.log('DOMRenderer.render received array:', vnode);
const fragment = document.createDocumentFragment();
vnode.forEach(child => {
const childElement = this.render(child);
if (childElement) {
fragment.appendChild(childElement);
}
});
return fragment;
}
// Debug log
//console.log('DOMRenderer.render received:', vnode);
const tagName = Object.keys(vnode)[0];
const props = vnode[tagName] || {};
// Debug log
//console.log('Extracted tagName:', tagName, 'type:', typeof tagName);
// Check if it's a registered component
if (this.juris.componentManager.components.has(tagName)) {
const parentTracking = this.juris.stateManager.currentTracking;
this.juris.stateManager.currentTracking = null;
const result = this.juris.componentManager.create(tagName, props);
this.juris.stateManager.currentTracking = parentTracking;
return result;
}
// Validate tagName before creating element
if (typeof tagName !== 'string' || tagName.length === 0) {
console.error('Invalid tagName:', tagName, 'from vnode:', vnode);
return null;
}
// FINE-GRAINED MODE: Use direct DOM updates
if (this.renderMode === 'fine-grained') {
return this._createElementFineGrained(tagName, props);
}
// BATCH MODE: Try optimized reconciliation with automatic fallback
try {
const key = props.key || this._generateKey(tagName, props);
const cachedElement = this.elementCache.get(key);
if (cachedElement && this._canReuseElement(cachedElement, tagName, props)) {
this._updateElementProperties(cachedElement, props);
return cachedElement;
}
return this._createElementOptimized(tagName, props, key);
} catch (error) {
console.warn('Batch rendering failed, falling back to fine-grained mode:', error.message);
this.failureCount++;
if (this.failureCount >= this.maxFailures) {
console.log('Too many batch failures, switching to fine-grained mode permanently');
this.renderMode = 'fine-grained';
}
return this._createElementFineGrained(tagName, props);
}
}
// FINE-GRAINED: Direct DOM manipulation method
_createElementFineGrained(tagName, props) {
// Debug logging to catch the issue
/*console.log('_createElementFineGrained called with:', {
tagName: tagName,
tagNameType: typeof tagName,
props: props
});*/
// Validate inputs
if (typeof tagName !== 'string') {
console.error('Invalid tagName in _createElementFineGrained:', tagName);
return null;
}
const element = document.createElement(tagName);
const subscriptions = [];
const eventListeners = [];
Object.keys(props).forEach(key => {
const value = props[key];
if (key === 'children') {
this._handleChildrenFineGrained(element, value, subscriptions);
} else if (key === 'text') {
this._handleText(element, value, subscriptions);
} else if (key === 'style') {
this._handleStyleFineGrained(element, value, subscriptions);
} else if (key.startsWith('on')) {
this._handleEvent(element, key, value, eventListeners);
} else if (typeof value === 'function') {
this._handleReactiveAttribute(element, key, value, subscriptions);
} else if (key !== 'key') {
this._setStaticAttribute(element, key, value);
}
});
if (subscriptions.length > 0 || eventListeners.length > 0) {
this.subscriptions.set(element, { subscriptions, eventListeners });
}
return element;
}
_handleChildrenFineGrained(element, children, subscriptions) {
if (typeof children === 'function') {
let lastChildrenResult = null;
let isInitialized = false;
const updateChildren = () => {
try {
const result = children();
// Only update if children result has actually changed
if (isInitialized && deepEquals(result, lastChildrenResult)) {
return; // Skip update if children haven't changed
}
if (result !== "ignore") {
this._updateChildrenFineGrained(element, result);
lastChildrenResult = result;
isInitialized = true;
}
} catch (error) {
console.error('Error in children function:', error);
}
};
// Set up reactive subscription
this._createReactiveUpdate(element, updateChildren, subscriptions);
// Initial render - call updateChildren directly instead of duplicating children() call
//updateChildren(); // To be removed when all test is
} else {
this._updateChildrenFineGrained(element, children);
}
}
_updateChildrenFineGrained(element, children) {
if (children === "ignore") {
return;
}
const childrenToRemove = Array.from(element.children);
childrenToRemove.forEach(child => {
this.cleanup(child);
});
element.textContent = '';
const fragment = document.createDocumentFragment();
if (Array.isArray(children)) {
children.forEach(child => {
const childElement = this.render(child);
if (childElement) {
fragment.appendChild(childElement);
}
});
} else if (children) {
const childElement = this.render(children);
if (childElement) {
fragment.appendChild(childElement);
}
}
if (fragment.hasChildNodes()) {
element.appendChild(fragment);
}
}
_handleStyleFineGrained(element, style, subscriptions) {
if (typeof style === 'function') {
let lastStyleValue = null;
let isInitialized = false;
const updateStyle = () => {
const styleObj = style();
// Only update if style has actually changed
if (isInitialized && deepEquals(styleObj, lastStyleValue)) {
return; // Skip update if style hasn't changed
}
if (typeof styleObj === 'object') {
Object.assign(element.style, styleObj);
lastStyleValue = { ...styleObj };
isInitialized = true;
}
};
// Set up reactive subscription
this._createReactiveUpdate(element, updateStyle, subscriptions);
// Initial render - call updateStyle directly instead of duplicating style() call
//updateStyle();
} else if (typeof style === 'object') {
Object.assign(element.style, style);
}
}
// BATCH MODE: Optimized element creation
_createElementOptimized(tagName, props, key) {
let element = this._getRecycledElement(tagName);
if (!element) {
element = document.createElement(tagName);
}
if (key) {
this.elementCache.set(key, element);
element._jurisKey = key;
}
const subscriptions = [];
const eventListeners = [];
this._processProperties(element, props, subscriptions, eventListeners);
if (subscriptions.length > 0 || eventListeners.length > 0) {
this.subscriptions.set(element, { subscriptions, eventListeners });
}
return element;
}
_processProperties(element, props, subscriptions, eventListeners) {
Object.keys(props).forEach(key => {
const value = props[key];
if (key === 'children') {
this._handleChildrenOptimized(element, value, subscriptions);
} else if (key === 'text') {
this._handleText(element, value, subscriptions);
} else if (key === 'innerHTML') {
if (typeof value === 'function') {
this._handleReactiveAttribute(element, key, value, subscriptions);
} else {
element.innerHTML = value;
}
} else if (key === 'style') {
this._handleStyle(element, value, subscriptions);
} else if (key.startsWith('on')) {
this._handleEvent(element, key, value, eventListeners);
} else if (typeof value === 'function') {
this._handleReactiveAttribute(element, key, value, subscriptions);
} else if (key !== 'key') {
this._setStaticAttribute(element, key, value);
}
});
}
_handleChildrenOptimized(element, children, subscriptions) {
if (typeof children === 'function') {
let lastChildrenState = null;
let childElements = [];
let useOptimizedPath = true;
const updateChildren = () => {
try {
const newChildren = children();
if (newChildren !== "ignore" && !this._childrenEqual(lastChildrenState, newChildren)) {
if (useOptimizedPath) {
try {
childElements = this._reconcileChildren(element, childElements, newChildren);
lastChildrenState = newChildren;
} catch (error) {
console.warn('Reconciliation failed, falling back to safe rendering:', error.message);
useOptimizedPath = false;
this._updateChildrenSafe(element, newChildren);
lastChildrenState = newChildren;
}
} else {
this._updateChildrenSafe(element, newChildren);
lastChildrenState = newChildren;
}
}
} catch (error) {
console.error('Error in children function:', error);
useOptimizedPath = false;
try {
this._updateChildrenSafe(element, []);
} catch (fallbackError) {
console.error('Even safe fallback failed:', fallbackError);
}
}
};
this._createReactiveUpdate(element, updateChildren, subscriptions);
try {
const initialChildren = children();
childElements = this._reconcileChildren(element, [], initialChildren);
lastChildrenState = initialChildren;
} catch (error) {
console.warn('Initial reconciliation failed, using safe method:', error.message);
useOptimizedPath = false;
const initialChildren = children();
this._updateChildrenSafe(element, initialChildren);
lastChildrenState = initialChildren;
}
} else {
try {
this._reconcileChildren(element, [], children);
} catch (error) {
console.warn('Static reconciliation failed, using safe method:', error.message);
this._updateChildrenSafe(element, children);
}
}
}
_updateChildrenSafe(element, children) {
if (children === "ignore") {
return;
}
const childrenToRemove = Array.from(element.children);
childrenToRemove.forEach(child => {
try {
this.cleanup(child);
} catch (error) {
console.warn('Error cleaning up child:', error);
}
});
element.textContent = '';
const fragment = document.createDocumentFragment();
if (Array.isArray(children)) {
children.forEach(child => {
try {
const childElement = this.render(child);
if (childElement && childElement !== element) {
fragment.appendChild(childElement);
}
} catch (error) {
console.warn('Error rendering child:', error);
}
});
} else if (children) {
try {
const childElement = this.render(children);
if (childElement && childElement !== element) {
fragment.appendChild(childElement);
}
} catch (error) {
console.warn('Error rendering single child:', error);
}
}
try {
if (fragment.hasChildNodes()) {
element.appendChild(fragment);
}
} catch (error) {
console.error('Failed to append fragment, trying individual children:', error);
Array.from(fragment.children).forEach(child => {
try {
if (child && child !== element) {
element.appendChild(child);
}
} catch (individualError) {
console.warn('Failed to append individual child:', individualError);
}
});
}
}
_reconcileChildren(parent, oldChildren, newChildren) {
if (!Array.isArray(newChildren)) {
newChildren = newChildren ? [newChildren] : [];
}
const newChildElements = [];
const fragment = document.createDocumentFragment();
const oldChildrenByKey = new Map();
oldChildren.forEach((child, index) => {
const key = child._jurisKey || `auto-${index}`;
oldChildrenByKey.set(key, child);
});
const usedElements = new Set();
newChildren.forEach((newChild, index) => {
if (!newChild || typeof newChild !== 'object') return;
const tagName = Object.keys(newChild)[0];
const props = newChild[tagName] || {};
const key = props.key || this._generateKey(tagName, props, index);
const existingElement = oldChildrenByKey.get(key);
if (existingElement &&
!usedElements.has(existingElement) &&