Source: lib/core/runtime/AvenxGuard.js

/**
 * @file AvenxGuard.js
 * @description The base class for route guards, and the context they receive.
 *
 * ## Why a guard has a context
 *
 * A guard used to receive nothing. It answered `canActivate(to, from)` from the
 * route alone, and anything else it needed had to be reached through whatever
 * happened to be in lexical scope. For a guard compiled into the application
 * bundle that was an accident of concatenation, and when the compiler stopped
 * emitting a binding the guard's identifier silently became `undefined`.
 *
 * That left the ordinary case unwritable. "Is this visitor signed in?" is the
 * reason route guards exist, and the answer lives in a bridge — which a guard
 * had no supported way to read: it received no injection, and the template
 * sandbox refuses `window`. The only decisions a guard could make were the ones
 * the URL already carried.
 *
 * The context makes that dependency explicit rather than lexical. A guard is
 * handed the framework-level capabilities it is allowed to use, by the router
 * that invoked it, at the moment it runs.
 *
 * ## What the context deliberately does not carry
 *
 * Browser globals. The context exposes the application's bridges and the
 * router that is asking; it is not an escape hatch to `document`,
 * `localStorage` or `fetch`. A guard that genuinely needs a browser API should
 * read it through a bridge, which is the same rule the rest of the framework
 * follows and the reason a bridge read is traceable at all.
 * @module lib/core/runtime/AvenxGuard
 */

/**
 * The capabilities a guard is given when it runs.
 *
 * Constructed per navigation by the router. Nothing here is mutable state: the
 * bridges are the live reactive objects the rest of the application uses, so a
 * guard reading `context.bridge('session').signedIn` sees exactly what a
 * component reading the same bridge sees.
 */
export class GuardContext {
  /**
   * @param {object} [options] - Context sources.
   * @param {object} [options.bridges] - The application's bridge registry.
   * @param {object} [options.app] - The application instance.
   * @param {object} [options.router] - The router performing the navigation.
   */
  constructor({ bridges = {}, app = null, router = null } = {}) {
    /**
     * Registered bridges, by name.
     * @type {object}
     */
    this.bridges = bridges;
    /**
     * The application instance.
     * @type {object|null}
     */
    this.app = app;
    /**
     * The router performing this navigation.
     * @type {object|null}
     */
    this.router = router;
  }

  /**
   * Looks a bridge up by name.
   *
   * Returns `undefined` rather than throwing for an unknown name, so a guard
   * can decide what an absent bridge means. A guard that imports its bridge
   * directly does not need this; it exists for guards registered as plain
   * functions, and for looking a bridge up dynamically.
   * @param {string} name - The registered bridge name.
   * @returns {any} The bridge, or undefined when it is not registered.
   */
  bridge(name) {
    return this.bridges ? this.bridges[name] : undefined;
  }
}

/**
 * Base class for all route guards in Avenx.
 * Guards determine if a route transition should proceed, abort, or redirect.
 */
export class AvenxGuard {
  /**
   * Guards are constructed by the router, once per navigation, with the
   * context for that navigation.
   *
   * Subclasses that declare no constructor get this one, so an existing guard
   * written before contexts existed keeps working unchanged and simply gains
   * `this.$context`.
   * @param {GuardContext} [context] - The capabilities for this navigation.
   */
  constructor(context = new GuardContext()) {
    /**
     * The capabilities this guard was given.
     * @type {GuardContext}
     */
    this.$context = context;
  }

  /**
   * The application's bridges, by name.
   * @returns {object} The bridge registry.
   */
  get $bridges() {
    return this.$context ? this.$context.bridges : {};
  }

  /**
   * Looks a bridge up by name.
   * @param {string} name - The registered bridge name.
   * @returns {any} The bridge, or undefined.
   */
  $bridge(name) {
    return this.$context ? this.$context.bridge(name) : undefined;
  }

  /**
   * Determines whether the route can be activated.
   * Can return a boolean (true to allow, false to abort), a string (to redirect),
   * a custom control object (e.g. { cancel: true, silent: true } or { redirect: string, state?: object }),
   * or a Promise resolving to any of these.
   * @param {object} [to] - The target route.
   * @param {object} [from] - The route being left.
   * @param {GuardContext} [context] - The capabilities for this navigation, also
   *   available as `this.$context`. Passed as a third argument so that a guard
   *   registered as a plain function can reach it too.
   * @returns {boolean|string|object|Promise<boolean|string|object>} The decision.
   */
  // The parameters document what a subclass receives; the base implementation
  // admits every navigation and reads none of them.
  // eslint-disable-next-line no-unused-vars
  canActivate(to, from, context) {
    return true;
  }
}