Source: lib/core/renderer/deadlockManager.js

/**
 * @file deadlockManager.js
 * @description Manages <@deadlock> reactive boundaries, boundary tripping, and fallback UI rendering.
 */

import { deriveScope } from '../reactive/scopeProxy.js';
import { logger } from '../runtime/AvenxLogger.js';

/**
 * Manages <@deadlock> reactive boundaries, containment checks, and error recovery fallbacks.
 */
export class DeadlockManager {
  /** @type {WeakSet<Element>} */
  #tripped = new WeakSet();

  /**
   * @param {object} evaluator - Expression evaluator.
   * @param {object} renderer - Template renderer.
   * @param {object} [eventBinder] - Event binder.
   * @param {string} [componentName] - Parent component name.
   */
  constructor(evaluator, renderer, eventBinder, componentName) {
    this.evaluator = evaluator;
    this.renderer = renderer;
    this.eventBinder = eventBinder;
    this.componentName = componentName || 'AnonymousComponent';
  }

  /**
   * Checks if a boundary is currently tripped.
   * @param {Element} container - The [data-ax-deadlock] element.
   * @returns {boolean}
   */
  isTripped(container) {
    return this.#tripped.has(container);
  }

  /**
   * Finds all [data-ax-deadlock] boundary containers within a root element.
   * @param {Element} root
   * @returns {Element[]}
   */
  findBoundaries(root) {
    if (!root) return [];
    const boundaries = [];
    if (root.matches && root.matches('[data-ax-deadlock]')) {
      boundaries.push(root);
    }
    if (root.querySelectorAll) {
      root.querySelectorAll('[data-ax-deadlock]').forEach((el) => boundaries.push(el));
    }
    return boundaries;
  }

  /**
   * Trips a deadlock boundary, unmounting its active child subtree and rendering fallback UI.
   * @param {Element} container - The [data-ax-deadlock] element.
   * @param {Error|object} [error] - The error or diagnostic details.
   * @param {object} [scope] - Evaluation scope.
   */
  trip(container, error = {}, scope = {}) {
    if (!container || this.#tripped.has(container)) return;
    this.#tripped.add(container);

    const boundaryName = container.getAttribute('data-ax-deadlock-name') || 'anonymous';
    const fallbackTpl = container.querySelector('template[data-ax-deadlock-fallback]');

    if (fallbackTpl) {
      const errorAs = fallbackTpl.getAttribute('data-ax-error-as') || 'error';
      const fallbackHtml = fallbackTpl.innerHTML
        .replace(/\{%/g, '{{')
        .replace(/%\}/g, '}}');

      const errObj = error instanceof Error
        ? { message: error.message, stack: error.stack, name: error.name }
        : typeof error === 'object' && error !== null
          ? { message: error.message || 'Reactive cycle detected', ...error }
          : { message: String(error) };

      const evalScope = deriveScope(scope, {
        name: boundaryName,
        [errorAs]: errObj,
      });

      const renderedFallback = this.renderer.render(fallbackHtml, (expr) => {
        try {
          if (expr === 'name') return boundaryName;
          if (expr === `${errorAs}.message` || expr === 'error.message') return errObj.message;
          return evalScope[expr] !== undefined ? evalScope[expr] : '';
        } catch {
          return '';
        }
      });

      // Remove non-template child elements (unmount child content)
      const childrenToRemove = Array.from(container.childNodes).filter((child) => {
        return !(child.nodeType === 1 && child.tagName && child.tagName.toLowerCase() === 'template');
      });

      for (const child of childrenToRemove) {
        if (child.__avenx_comp_instance && typeof child.__avenx_comp_instance.unmount === 'function') {
          try {
            child.__avenx_comp_instance.unmount();
          } catch (e) {
            logger.error('Error unmounting deadlocked child component:', e);
          }
        }
        if (child.parentNode) {
          child.parentNode.removeChild(child);
        }
      }

      if (typeof document !== 'undefined') {
        const tempDiv = document.createElement('div');
        tempDiv.innerHTML = renderedFallback;
        while (tempDiv.firstChild) {
          container.appendChild(tempDiv.firstChild);
        }
      }
    }
  }

  /**
   * Resets a tripped boundary so it can render normally again.
   * @param {Element} container
   */
  reset(container) {
    if (container) {
      this.#tripped.delete(container);
    }
  }
}