Source: lib/core/events/eventExecutor.js

import { logger } from '../runtime/AvenxLogger.js';
import { AvenxErrorCodes, formatMessage } from '../runtime/AvenxError.js';
import { tracer } from '../trace/tracer.js';
import { TraceNodeType } from '../trace/schema.js';
import { describeNode } from '../trace/dom.js';

/**
 * Builds the trace node for a dispatched event.
 *
 * The handler source is recorded verbatim. Avenx keeps template expressions as
 * source text right through to evaluation, so the trace can name the exact code
 * that ran rather than a compiled closure with no identity.
 * @param {string} source - The handler source from the template.
 * @param {Event|null} event - The dispatched event.
 * @returns {object} The event node fields.
 */
export function buildEventNode(source, event) {
  const target = event && event.target;
  const node = {
    eventType: (event && event.type) || 'unknown',
    handler: source,
  };

  const ref = describeNode(target);
  if (ref) {
    node.target = { selector: ref.selector, nth: ref.nth };
    if (ref.component) {
      node.component = ref.component;
      node.uid = ref.uid;
    }
  }

  // Input values are what replay has to reproduce for a form interaction, so a
  // typed value is part of the event rather than an observation of it.
  if (target && typeof target.value === 'string' && target.value !== '') {
    node.value = target.value;
  }
  if (target && typeof target.checked === 'boolean') {
    node.checked = target.checked;
  }
  if (event && typeof event.key === 'string') {
    node.key = event.key;
  }

  return node;
}

/**
 * Handles the execution of event handlers.
 */
export class EventExecutor {
  /**
   * @param {Function} runHandler - Function that executes the event logic.
   */
  constructor(runHandler) {
    /**
     * @type {Function}
     */
    this.runHandler = runHandler;
  }

  /**
   * Executes the event handler for a given source.
   * @param {string} source - The source code or identifier for the event handler.
   * @param {Event|null} [event] - The event object, if any.
   * @param {object|null} [slotScope] - The slot scope context, if any.
   * @returns {any} The result of the event handler execution.
   */
  execute(source, event = null, slotScope = null) {
    if (!this.runHandler) {
      throw new TypeError('Handler is not configured or has been torn down.');
    }

    try {
      // A DOM event is the one thing in an Avenx application that genuinely
      // starts a causal chain, so it opens the outermost node. Everything the
      // handler goes on to do — the action, its writes, the watchers those
      // wake, the DOM patches those produce — hangs off this node.
      const token = tracer.on ? tracer.enter(TraceNodeType.EVENT, buildEventNode(source, event)) : -1;
      try {
        // The *source* is handed on, not a compiled function.
        //
        // This used to build `new Function("with(state){with(methods){...}}")`
        // for every handler and pass that down, which meant every inline
        // handler in every application took the legacy proxy sandbox and no
        // handler ever reached the AST evaluator. Two consequences, neither
        // intended: an application needed 'unsafe-eval' as soon as it had a
        // single @click, and the evaluator's guarantees — no reaching the
        // Function constructor, no `constructor` / `__proto__` / `prototype`
        // however the key is spelled — did not hold on the busiest path in the
        // framework.
        //
        // `executeStatement` already knows how to make this decision. Given
        // source it parses first and only falls back to `new Function` for
        // bodies that are real statements (`if`, `for`, `return`, a
        // declaration), which is what the deployment guide has always
        // described. Given a function it had no choice.
        return this.runHandler(source, event, slotScope);
      } finally {
        if (token >= 0) {
          tracer.leave(token);
        }
      }
    } catch (error) {
      const compContext = event?.target?.__avenx_comp_instance?.$logContext || {};
      const elTag = event?.target?.tagName || 'UNKNOWN';
      const eType = event?.type || 'unknown';
      const msg = formatMessage(AvenxErrorCodes.EVENT_HANDLER_ERROR, source, error);
      const extendedMsg = `${msg} \n[Context] Element: <${elTag}>, Event: '${eType}'`;
      logger.error(extendedMsg, compContext);
      throw error;
    }
  }

  /**
   * Cleans up the run handler closure reference to prevent parent scope memory retention.
   */
  teardown() {
    this.runHandler = null;
  }
}