Source: lib/core/tooling/inspect.js

/**
 * Recursively clones an object and strips non-cloneable elements
 * (like functions, circular references, and DOM nodes).
 * @param {*} val - The value to sanitize.
 * @param {WeakSet<object>} [seen] - WeakSet tracking visited objects to prevent circular loops.
 * @returns {*} The safe clone.
 */
export function serializeSafe(val, seen = new WeakSet()) {
  if (val === null || typeof val !== 'object') {
    if (typeof val === 'function') {
      return '[Function]';
    }
    return val;
  }

  // Handle DOM elements and window references
  if (typeof window !== 'undefined' && ((typeof Node !== 'undefined' && val instanceof Node) || val === window)) {
    return `[DOM Element: ${val.nodeName || 'Window'}]`;
  }

  if (seen.has(val)) {
    return '[Circular]';
  }

  if (Array.isArray(val)) {
    seen.add(val);
    const res = val.map(item => serializeSafe(item, seen));
    seen.delete(val);
    return res;
  }

  // Use toJSON if available
  if (typeof val.toJSON === 'function') {
    try {
      return val.toJSON();
    } catch {
      // Ignored fallback
    }
  }

  seen.add(val);
  const res = {};
  for (const [key, value] of Object.entries(val)) {
    if (typeof value === 'function') {
      res[key] = '[Function]';
    } else if (key.startsWith('__')) {
      res[key] = '[Internal]';
    } else {
      res[key] = serializeSafe(value, seen);
    }
  }
  seen.delete(val);
  return res;
}

/**
 * Collects all active component instances and application registration metadata.
 * @param {object} app - The AvenxApp instance.
 * @returns {object} Inspector data payload.
 */
function getInspectorData(app) {
  const activeComponents = [];
  if (typeof document !== 'undefined') {
    const elements = document.querySelectorAll('[data-avenx-comp], [data-avenx-comp-dynamic]');
    elements.forEach((el) => {
      if (el.__avenx_comp_instance) {
        const comp = el.__avenx_comp_instance;
        activeComponents.push({
          name: comp.constructor.name,
          state: comp.state || {},
          props: comp.props || {},
        });
      }
    });
  }

  const registeredBridges = {};
  for (const [name, bridge] of Object.entries(app.bridges || {})) {
    registeredBridges[name] = bridge;
  }

  const registeredComponents = app.components ? Array.from(app.components.keys()) : [];
  const registeredPages = app.pages ? Array.from(app.pages.keys()) : [];

  const routes = app.router ? app.router.routes : {};
  const currentRoute = app.router ? app.router.currentRoute : null;

  return serializeSafe({
    activeComponents,
    registeredBridges,
    registeredComponents,
    registeredPages,
    routes,
    currentRoute,
  });
}

/**
 * Initializes the inspector for an AvenxApp.
 * @param {object} app - The AvenxApp instance.
 */
export function initInspector(app) {
  if (typeof window === 'undefined' || !window.__avenx_inspect_enabled || typeof globalThis.BroadcastChannel === 'undefined') {
    return;
  }

  const channel = new globalThis.BroadcastChannel('avenx-inspector-channel');

  const broadcast = () => {
    try {
      channel.postMessage({
        type: 'inspect-data',
        data: getInspectorData(app),
      });
    } catch (err) {
      // Gracefully catch any remaining clone/postMessage errors
      console.warn('[Avenx Inspector] Failed to broadcast state:', err);
    }
  };

  channel.onmessage = (event) => {
    if (event.data === 'request-inspect-data') {
      broadcast();
    }
  };

  // Automatically broadcast on page transitions or component lifecycles/updates.
  const originalUpdateAll = app.updateAll;
  app.updateAll = function (...args) {
    const res = originalUpdateAll.apply(this, args);
    broadcast();
    return res;
  };

  const originalMountPage = app.mountPage;
  app.mountPage = function (...args) {
    const res = originalMountPage.apply(this, args);
    broadcast();
    return res;
  };

  // Intercept component lifecycles using capturing phase listeners (since they don't bubble)
  window.addEventListener('avenx:update', () => broadcast(), true);
  window.addEventListener('avenx:mount', () => broadcast(), true);
  window.addEventListener('avenx:unmount', () => broadcast(), true);
}