Source: lib/core/runtime/AvenxRouter.js

import { AvenxErrorCodes, formatMessage } from './AvenxError.js';

/**
 * AvenxRouter handles hash-based routing for the application.
 * It maps URL hashes to specific Page components.
 */
export class AvenxRouter {
    /**
     * @param {AvenxApp} app - The main application instance.
     * @param {Object<string, string|Object>} routes - A map of hash routes to page names or route definitions.
     */
    constructor(app, routes = {}) {
        /** @type {AvenxApp} */
        this.app = app;
        /** @type {Object<string, string|Object>} */
        this.routes = routes;
        /** @type {Object|null} */
        this.currentRoute = null;
        /** @type {string|null} @private */
        this.hashToIgnore = null;

        this.hashChangeHandler = () => this.#handleRoute();
        window.addEventListener('hashchange', this.hashChangeHandler);
    }

    /**
     * Starts the router and handles the initial route.
     */
    start() {
        this.#handleRoute();
    }

    /**
     * Navigates to a specific hash route.
     * @param {string} hash - The target hash (e.g., '#/about').
     */
    navigate(hash) {
        window.location.hash = hash;
    }

    /**
     * Destroys the router and cleans up event listeners.
     */
    destroy() {
        window.removeEventListener('hashchange', this.hashChangeHandler);
    }

    /**
     * Sequentially executes an array of guards for a route transition.
     * @param {Array<typeof AvenxGuard|AvenxGuard>} guards - Route guards.
     * @param {Object} to - Target route details.
     * @param {Object|null} from - Current route details.
     * @returns {Promise<boolean|string>} Result of the guard checks (true, false, or redirect path).
     * @private
     */
    #runGuards(guards, to, from) {
        return new Promise((resolve) => {
            const nextGuard = (index) => {
                if (index >= guards.length) {
                    resolve(true);
                    return;
                }
                const Guard = guards[index];
                const instance = typeof Guard === 'function' ? new Guard() : Guard;

                Promise.resolve(instance.canActivate(to, from))
                    .then(result => {
                        if (result === false || typeof result === 'string') {
                            resolve(result);
                        } else {
                            nextGuard(index + 1);
                        }
                    })
                    .catch(err => {
                        console.error(formatMessage(AvenxErrorCodes.ROUTER_GUARD_ERROR, to.hash, err));
                        resolve(false);
                    });
            };
            nextGuard(0);
        });
    }

    /**
     * Handles the current route by matching it against patterns, executing guards,
     * and mounting the corresponding page.
     * @private
     */
    #handleRoute() {
        let hash = window.location.hash || '#/';

        // Strip any secondary anchor (e.g. #/profile#details)
        const secondHashIndex = hash.indexOf('#', 1);

        if (secondHashIndex !== -1) {
            hash = hash.substring(0, secondHashIndex);
        }

        if (this.hashToIgnore === hash) {
            this.hashToIgnore = null;
            return;
        }

        let matchedRoute = null;
        let params = {};

        for (const [routePattern, routeDef] of Object.entries(this.routes)) {
            if (routePattern === '*') continue;

            const paramNames = [];
            const regexStr = routePattern
                .replace(/[.+^${}()|[\]\\]/g, '\\$&')
                .replace(/:([a-zA-Z0-9_$]+)/g, (_, name) => {
                    paramNames.push(name);
                    return '([^/]+)';
                });
            const regex = new RegExp(`^${regexStr}$`);

            const [pathPart, queryPart] = hash.split('?');
            const match = pathPart.match(regex);

            if (match) {
                matchedRoute = { pattern: routePattern, definition: routeDef };
                paramNames.forEach((name, idx) => {
                    params[name] = decodeURIComponent(match[idx + 1]);
                });
                if (queryPart) {
                    const queryParams = new URLSearchParams(queryPart);
                    params.query = Object.fromEntries(queryParams.entries());
                }
                break;
            }
        }

        // Fallback to '*' if no route matched
        if (!matchedRoute && this.routes['*']) {
            matchedRoute = { pattern: '*', definition: this.routes['*'] };
        }

        if (!matchedRoute) {
            console.warn(`[AvenxRouter] No route defined for hash: ${hash}`);
            return;
        }

        const def = matchedRoute.definition;
        const pageName = typeof def === 'string' ? def : def.page;
        const guards = typeof def === 'object' ? (def.guards || []) : [];

        const to = { hash, page: pageName, params };
        const from = this.currentRoute;

        this.#runGuards(guards, to, from).then(result => {
            if (result === false) {
                console.warn(formatMessage(AvenxErrorCodes.ROUTER_GUARD_DENIED, to.hash));
                if (from && from.hash !== window.location.hash) {
                    this.hashToIgnore = from.hash;
                    window.location.hash = from.hash;
                }
            } else if (typeof result === 'string') {
                this.navigate(result);
            } else {
                this.currentRoute = to;
                this.app.mountPage(pageName, params);
            }
        });
    }
}