Source: lib/core/runtime/AvenxApp.js

import { AvenxRouter } from './AvenxRouter.js';
import { AvenxPage } from './AvenxPage.js';
import { AvenxComponent } from './AvenxComponent.js';
import { AvenxError, AvenxErrorCodes } from './AvenxError.js';
import { ProxyHandlerFactory } from '../reactive/proxyHandler.js';
import { queueJob } from '../reactive/scheduler.js';

/**
 * The main application class for Avenx.
 * Manages component registration, bridge registration, and mounting.
 */
export class AvenxApp {
    /** @type {AvenxComponent[]} @private */
    #activeComponents = [];
    /** @type {Element|null} @private */
    #target = null;

    /**
     * @param {Object} config - Application configuration.
     * @param {string} config.target - Selector for the main application container.
     */
    constructor(config) {
        this.#target = document.querySelector(config.target);
        if (!this.#target) {
            throw new AvenxError(AvenxErrorCodes.MOUNT_TARGET_NOT_FOUND, config.target);
        }
        /** @type {Map<string, typeof AvenxComponent>} */
        this.components = new Map();
        /** @type {Map<string, typeof AvenxPage>} */
        this.pages = new Map();
        /** @type {Object} */
        this.bridges = {};
        /** @type {AvenxRouter|null} */
        this.router = null;
        this.updateAll = this.updateAll.bind(this);
    }

    /**
     * Registers a component with the application.
     * @param {string} name - The name of the component.
     * @param {typeof AvenxComponent} compClass - The component class.
     */
    register(name, compClass) {
        this.components.set(name, compClass);
    }

    /**
     * Registers a page with the application.
     * @param {string} name - The name of the page.
     * @param {typeof AvenxPage} pageClass - The page class.
     */
    registerPage(name, pageClass) {
        if (this.pages.has(name)) {
            console.warn(
                `Page "${name}" is already registered and will be overwritten.`
            );
        }

        this.pages.set(name, pageClass);
    }
    /**
     * Initializes the router for the application.
     * @param {Object<string, string>} routes - Route mapping.
     * @returns {AvenxRouter} The router instance.
     */
    initRouter(routes) {
        this.router = new AvenxRouter(this, routes);
        this.router.start();
        return this.router;
    }

    /**
     * Registers a bridge with the application.
     * Bridges provide shared state and logic across components.
     * @param {string} name - The name of the bridge.
     * @param {Object|Function} bridgeData - The bridge data or constructor.
     */
    registerBridge(name, bridgeData) {

        if (Object.prototype.hasOwnProperty.call(this.bridges, name)) {
            const availableBridges = Object.keys(this.bridges).join(',');
            const suggestion = `Please use a unique name`;
            throw new AvenxError(
                AvenxErrorCodes.BRIDGE_ALREADY_EXISTS,
                name,
                availableBridges || 'none',
                suggestion
            );
        }

        let instance = bridgeData;

        if (typeof bridgeData === 'function') {
            try {
                instance = new bridgeData();
            } catch (e) {
                // Keep object-style bridge behavior if construction is not possible.
            }
        }

        const handlerFactory = new ProxyHandlerFactory({
            onChange: () => queueJob(this.updateAll)
        });
        const reactiveState = new Proxy(instance, handlerFactory.create());
        this.bridges[name] = reactiveState;
    }

    /**
     * Updates all active components in the application.
     */
    updateAll() {
        this.#activeComponents.forEach(comp => comp.update());
    }

    /**
     * Mounts a page to the main application container.
     * @param {string} name - The name of the page to mount.
     * @param {Object} [params={}] - Dynamic route parameters to inject.
     */
    mountPage(name, params = {}) {
        const PageClass = this.pages.get(name);
        if (!PageClass) {
            throw new AvenxError(AvenxErrorCodes.PAGE_NOT_FOUND, name);
        }
        if (this.#target) {
            const activePage = this.#activeComponents[0];
            if (activePage && activePage instanceof PageClass) {
                // Delete keys from previous params that are not in new params
                if (activePage.params) {
                    for (const key of Object.keys(activePage.params)) {
                        if (!(key in params)) {
                            delete activePage.state[key];
                            delete activePage.params[key];
                        }
                    }
                } else {
                    activePage.params = {};
                }

                // Update or set new params
                for (const [key, val] of Object.entries(params)) {
                    activePage.state[key] = val;
                    activePage.params[key] = val;
                }
                return;
            }

            // Cleanup current components
            this.#activeComponents.forEach(comp => {
                if (typeof comp.unmount === 'function') {
                    comp.unmount();
                }
            });
            this.#activeComponents = [];
            this.#target.innerHTML = '';

            // Pages receive both bridges and the component registry for child mounting
            const pageInstance = new PageClass(this.bridges, this.components);

            pageInstance.params = params;
            for (const [key, val] of Object.entries(params)) {
                pageInstance.state[key] = val;
            }

            pageInstance.mount(this.#target);
            this.#activeComponents.push(pageInstance);
        }
    }

    /**
     * Mounts a component to a target element.
     * @param {string} name - The name of the component to mount.
     * @param {string|null} [targetSelector=null] - Optional selector for the mount target.
     */
    mount(name, targetSelector = null) {
        const Comp = this.components.get(name);
        if (!Comp) {
            const registeredList = Array.from(this.components.keys()).join(', ');
            throw new AvenxError(AvenxErrorCodes.COMPONENT_NOT_FOUND, name, registeredList);
        }
        const target = targetSelector ? document.querySelector(targetSelector) : this.#target;
        if (!target) {
            throw new AvenxError(AvenxErrorCodes.MOUNT_TARGET_NOT_FOUND, targetSelector || 'default target');
        }
        const compInstance = new Comp(this.bridges);
        compInstance.mount(target);
        this.#activeComponents.push(compInstance);
    }
}