- Add comprehensive health check system with multiple endpoints - Add Prometheus metrics endpoint - Add production logging configurations (5 strategies) - Add complete deployment documentation suite: * QUICKSTART.md - 30-minute deployment guide * DEPLOYMENT_CHECKLIST.md - Printable verification checklist * DEPLOYMENT_WORKFLOW.md - Complete deployment lifecycle * PRODUCTION_DEPLOYMENT.md - Comprehensive technical reference * production-logging.md - Logging configuration guide * ANSIBLE_DEPLOYMENT.md - Infrastructure as Code automation * README.md - Navigation hub * DEPLOYMENT_SUMMARY.md - Executive summary - Add deployment scripts and automation - Add DEPLOYMENT_PLAN.md - Concrete plan for immediate deployment - Update README with production-ready features All production infrastructure is now complete and ready for deployment.
430 lines
14 KiB
JavaScript
430 lines
14 KiB
JavaScript
/**
|
|
* Nested Component Handler
|
|
*
|
|
* Manages parent-child relationships for nested LiveComponents on the client-side.
|
|
* Coordinates event bubbling, state synchronization, and lifecycle management.
|
|
*
|
|
* Features:
|
|
* - Parent-child relationship tracking
|
|
* - Event bubbling from child to parent
|
|
* - Child lifecycle coordination
|
|
* - State synchronization between parent and children
|
|
* - Automatic cleanup on component destruction
|
|
*
|
|
* Architecture:
|
|
* - Scans DOM for nested components (data-parent-component attribute)
|
|
* - Registers hierarchies with LiveComponentManager
|
|
* - Intercepts events to enable bubbling
|
|
* - Coordinates updates between parents and children
|
|
*/
|
|
|
|
export class NestedComponentHandler {
|
|
constructor(liveComponentManager) {
|
|
this.liveComponentManager = liveComponentManager;
|
|
|
|
// Registry: componentId → { parentId, childIds, depth }
|
|
this.hierarchyRegistry = new Map();
|
|
|
|
// Parent → [Children] mapping
|
|
this.childrenRegistry = new Map();
|
|
|
|
// Event bubbling callbacks: componentId → [callbacks]
|
|
this.bubbleCallbacks = new Map();
|
|
}
|
|
|
|
/**
|
|
* Initialize nested component system
|
|
* Scans DOM for nested components and registers hierarchies
|
|
*/
|
|
init() {
|
|
this.scanNestedComponents();
|
|
console.log(`[NestedComponents] Initialized with ${this.hierarchyRegistry.size} components`);
|
|
}
|
|
|
|
/**
|
|
* Scan DOM for nested components
|
|
* Looks for data-parent-component attribute to establish parent-child relationships
|
|
*/
|
|
scanNestedComponents() {
|
|
// Find all components with parent
|
|
const nestedComponents = document.querySelectorAll('[data-parent-component]');
|
|
|
|
nestedComponents.forEach(element => {
|
|
const componentId = element.dataset.liveComponent;
|
|
const parentId = element.dataset.parentComponent;
|
|
const depth = parseInt(element.dataset.nestingDepth) || 1;
|
|
|
|
if (componentId && parentId) {
|
|
this.registerHierarchy(componentId, parentId, depth);
|
|
}
|
|
});
|
|
|
|
// Register root components (no parent)
|
|
const rootComponents = document.querySelectorAll('[data-live-component]:not([data-parent-component])');
|
|
|
|
rootComponents.forEach(element => {
|
|
const componentId = element.dataset.liveComponent;
|
|
if (componentId && !this.hierarchyRegistry.has(componentId)) {
|
|
this.registerRoot(componentId);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Register root component (no parent)
|
|
*
|
|
* @param {string} componentId - Component ID
|
|
*/
|
|
registerRoot(componentId) {
|
|
this.hierarchyRegistry.set(componentId, {
|
|
parentId: null,
|
|
childIds: [],
|
|
depth: 0,
|
|
path: [componentId]
|
|
});
|
|
|
|
console.log(`[NestedComponents] Registered root: ${componentId}`);
|
|
}
|
|
|
|
/**
|
|
* Register component hierarchy
|
|
*
|
|
* @param {string} componentId - Child component ID
|
|
* @param {string} parentId - Parent component ID
|
|
* @param {number} depth - Nesting depth
|
|
*/
|
|
registerHierarchy(componentId, parentId, depth = 1) {
|
|
// Get parent's path
|
|
const parentHierarchy = this.hierarchyRegistry.get(parentId);
|
|
const parentPath = parentHierarchy ? parentHierarchy.path : [parentId];
|
|
|
|
// Create hierarchy entry
|
|
this.hierarchyRegistry.set(componentId, {
|
|
parentId,
|
|
childIds: [],
|
|
depth,
|
|
path: [...parentPath, componentId]
|
|
});
|
|
|
|
// Add to parent's children
|
|
if (!this.childrenRegistry.has(parentId)) {
|
|
this.childrenRegistry.set(parentId, []);
|
|
}
|
|
|
|
const children = this.childrenRegistry.get(parentId);
|
|
if (!children.includes(componentId)) {
|
|
children.push(componentId);
|
|
}
|
|
|
|
console.log(`[NestedComponents] Registered child: ${componentId} (parent: ${parentId}, depth: ${depth})`);
|
|
}
|
|
|
|
/**
|
|
* Register dynamic nested component at runtime
|
|
*
|
|
* @param {string} componentId - Component ID
|
|
* @param {string} parentId - Parent component ID
|
|
*/
|
|
registerDynamicChild(componentId, parentId) {
|
|
const parentHierarchy = this.hierarchyRegistry.get(parentId);
|
|
if (!parentHierarchy) {
|
|
console.warn(`[NestedComponents] Cannot register child - parent not found: ${parentId}`);
|
|
return;
|
|
}
|
|
|
|
const depth = parentHierarchy.depth + 1;
|
|
this.registerHierarchy(componentId, parentId, depth);
|
|
}
|
|
|
|
/**
|
|
* Get component hierarchy
|
|
*
|
|
* @param {string} componentId - Component ID
|
|
* @returns {Object|null} Hierarchy object or null
|
|
*/
|
|
getHierarchy(componentId) {
|
|
return this.hierarchyRegistry.get(componentId) || null;
|
|
}
|
|
|
|
/**
|
|
* Get parent component ID
|
|
*
|
|
* @param {string} componentId - Component ID
|
|
* @returns {string|null} Parent ID or null if root
|
|
*/
|
|
getParentId(componentId) {
|
|
const hierarchy = this.getHierarchy(componentId);
|
|
return hierarchy ? hierarchy.parentId : null;
|
|
}
|
|
|
|
/**
|
|
* Get child component IDs
|
|
*
|
|
* @param {string} componentId - Parent component ID
|
|
* @returns {Array<string>} Array of child IDs
|
|
*/
|
|
getChildIds(componentId) {
|
|
return this.childrenRegistry.get(componentId) || [];
|
|
}
|
|
|
|
/**
|
|
* Check if component has children
|
|
*
|
|
* @param {string} componentId - Component ID
|
|
* @returns {boolean} True if has children
|
|
*/
|
|
hasChildren(componentId) {
|
|
const children = this.getChildIds(componentId);
|
|
return children.length > 0;
|
|
}
|
|
|
|
/**
|
|
* Check if component is root
|
|
*
|
|
* @param {string} componentId - Component ID
|
|
* @returns {boolean} True if root component
|
|
*/
|
|
isRoot(componentId) {
|
|
const hierarchy = this.getHierarchy(componentId);
|
|
return hierarchy ? hierarchy.parentId === null : true;
|
|
}
|
|
|
|
/**
|
|
* Get nesting depth
|
|
*
|
|
* @param {string} componentId - Component ID
|
|
* @returns {number} Nesting depth (0 for root)
|
|
*/
|
|
getDepth(componentId) {
|
|
const hierarchy = this.getHierarchy(componentId);
|
|
return hierarchy ? hierarchy.depth : 0;
|
|
}
|
|
|
|
/**
|
|
* Get all ancestors (parent, grandparent, etc.)
|
|
*
|
|
* @param {string} componentId - Component ID
|
|
* @returns {Array<string>} Array of ancestor IDs (parent first, root last)
|
|
*/
|
|
getAncestors(componentId) {
|
|
const hierarchy = this.getHierarchy(componentId);
|
|
if (!hierarchy || !hierarchy.path) {
|
|
return [];
|
|
}
|
|
|
|
// Path includes current component, remove it
|
|
const ancestors = [...hierarchy.path];
|
|
ancestors.pop();
|
|
|
|
// Return in reverse order (parent first, root last)
|
|
return ancestors.reverse();
|
|
}
|
|
|
|
/**
|
|
* Bubble event up through component hierarchy
|
|
*
|
|
* Dispatches custom event to each ancestor until stopped or root reached.
|
|
*
|
|
* @param {string} sourceId - Component that dispatched the event
|
|
* @param {string} eventName - Event name
|
|
* @param {Object} payload - Event payload
|
|
* @returns {boolean} True if bubbled to root, false if stopped
|
|
*/
|
|
bubbleEvent(sourceId, eventName, payload) {
|
|
console.log(`[NestedComponents] Bubbling event: ${eventName} from ${sourceId}`, payload);
|
|
|
|
let currentId = sourceId;
|
|
let bubbled = 0;
|
|
|
|
while (true) {
|
|
const parentId = this.getParentId(currentId);
|
|
|
|
// Reached root
|
|
if (parentId === null) {
|
|
console.log(`[NestedComponents] Event bubbled to root (${bubbled} levels)`);
|
|
return true;
|
|
}
|
|
|
|
// Get parent element
|
|
const parentElement = document.querySelector(`[data-live-component="${parentId}"]`);
|
|
if (!parentElement) {
|
|
console.warn(`[NestedComponents] Parent element not found: ${parentId}`);
|
|
return false;
|
|
}
|
|
|
|
// Dispatch custom event to parent
|
|
const bubbleEvent = new CustomEvent(`livecomponent:child:${eventName}`, {
|
|
detail: {
|
|
sourceId,
|
|
eventName,
|
|
payload,
|
|
currentLevel: bubbled
|
|
},
|
|
bubbles: false, // We handle bubbling manually
|
|
cancelable: true
|
|
});
|
|
|
|
const dispatched = parentElement.dispatchEvent(bubbleEvent);
|
|
|
|
// Event was cancelled - stop bubbling
|
|
if (!dispatched) {
|
|
console.log(`[NestedComponents] Event bubbling stopped at ${parentId}`);
|
|
return false;
|
|
}
|
|
|
|
// Check for registered callbacks
|
|
const callbacks = this.bubbleCallbacks.get(parentId);
|
|
if (callbacks) {
|
|
for (const callback of callbacks) {
|
|
const shouldContinue = callback(sourceId, eventName, payload);
|
|
if (shouldContinue === false) {
|
|
console.log(`[NestedComponents] Event bubbling stopped by callback at ${parentId}`);
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Move to next level
|
|
currentId = parentId;
|
|
bubbled++;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Register callback for child events
|
|
*
|
|
* @param {string} parentId - Parent component ID
|
|
* @param {Function} callback - Callback function (sourceId, eventName, payload) => boolean
|
|
*/
|
|
onChildEvent(parentId, callback) {
|
|
if (!this.bubbleCallbacks.has(parentId)) {
|
|
this.bubbleCallbacks.set(parentId, []);
|
|
}
|
|
|
|
this.bubbleCallbacks.get(parentId).push(callback);
|
|
|
|
console.log(`[NestedComponents] Registered child event callback for ${parentId}`);
|
|
}
|
|
|
|
/**
|
|
* Sync state from parent to children
|
|
*
|
|
* Useful for broadcasting shared state to all children.
|
|
*
|
|
* @param {string} parentId - Parent component ID
|
|
* @param {Object} sharedState - State to share with children
|
|
*/
|
|
syncStateToChildren(parentId, sharedState) {
|
|
const childIds = this.getChildIds(parentId);
|
|
|
|
console.log(`[NestedComponents] Syncing state to ${childIds.length} children of ${parentId}`);
|
|
|
|
childIds.forEach(childId => {
|
|
const childElement = document.querySelector(`[data-live-component="${childId}"]`);
|
|
if (!childElement) return;
|
|
|
|
// Dispatch state sync event
|
|
childElement.dispatchEvent(new CustomEvent('livecomponent:parent:state-sync', {
|
|
detail: { parentId, sharedState }
|
|
}));
|
|
|
|
// Update child state if applicable
|
|
// Child components can listen to this event and update accordingly
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Update all children when parent changes
|
|
*
|
|
* @param {string} parentId - Parent component ID
|
|
* @param {Object} updates - Updates to apply to children
|
|
*/
|
|
async updateChildren(parentId, updates) {
|
|
const childIds = this.getChildIds(parentId);
|
|
|
|
console.log(`[NestedComponents] Updating ${childIds.length} children of ${parentId}`);
|
|
|
|
// Update children in parallel for performance
|
|
const updatePromises = childIds.map(async childId => {
|
|
const childElement = document.querySelector(`[data-live-component="${childId}"]`);
|
|
if (!childElement) return;
|
|
|
|
// Trigger child component update via LiveComponent action
|
|
// This depends on how your components handle updates
|
|
// For now, just dispatch an event
|
|
childElement.dispatchEvent(new CustomEvent('livecomponent:parent:update', {
|
|
detail: { parentId, updates }
|
|
}));
|
|
});
|
|
|
|
await Promise.all(updatePromises);
|
|
}
|
|
|
|
/**
|
|
* Unregister component and cleanup
|
|
*
|
|
* @param {string} componentId - Component to unregister
|
|
*/
|
|
unregister(componentId) {
|
|
// Remove from hierarchy registry
|
|
const hierarchy = this.hierarchyRegistry.get(componentId);
|
|
this.hierarchyRegistry.delete(componentId);
|
|
|
|
// Remove from parent's children
|
|
if (hierarchy && hierarchy.parentId) {
|
|
const siblings = this.childrenRegistry.get(hierarchy.parentId);
|
|
if (siblings) {
|
|
const index = siblings.indexOf(componentId);
|
|
if (index !== -1) {
|
|
siblings.splice(index, 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Remove children registry
|
|
this.childrenRegistry.delete(componentId);
|
|
|
|
// Remove bubble callbacks
|
|
this.bubbleCallbacks.delete(componentId);
|
|
|
|
console.log(`[NestedComponents] Unregistered: ${componentId}`);
|
|
}
|
|
|
|
/**
|
|
* Get hierarchy statistics
|
|
*
|
|
* @returns {Object} Statistics
|
|
*/
|
|
getStats() {
|
|
let rootCount = 0;
|
|
let maxDepth = 0;
|
|
|
|
this.hierarchyRegistry.forEach(hierarchy => {
|
|
if (hierarchy.parentId === null) {
|
|
rootCount++;
|
|
}
|
|
maxDepth = Math.max(maxDepth, hierarchy.depth);
|
|
});
|
|
|
|
return {
|
|
total_components: this.hierarchyRegistry.size,
|
|
root_components: rootCount,
|
|
child_components: this.hierarchyRegistry.size - rootCount,
|
|
max_nesting_depth: maxDepth,
|
|
parents_with_children: this.childrenRegistry.size
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Destroy nested component handler
|
|
*/
|
|
destroy() {
|
|
this.hierarchyRegistry.clear();
|
|
this.childrenRegistry.clear();
|
|
this.bubbleCallbacks.clear();
|
|
console.log('[NestedComponents] Destroyed');
|
|
}
|
|
}
|
|
|
|
export default NestedComponentHandler;
|