All files / lib/core/renderer deadlockManager.js

87.87% Statements 116/132
40% Branches 10/25
66.66% Functions 4/6
87.87% Lines 116/132

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133403x 403x 403x 403x 403x 403x 403x 403x 403x 403x 403x 403x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x     1x 1x 1x 1x 1x 1x 1x 1x 1x 1x     1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 2x 2x 2x 2x 2x     1x 1x 1x 1x 2x 1x 1x 1x 1x             1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x         1x  
/**
 * @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);
    }
  }
}