UNPKG

@mojojs/core

Version:

Real-time web framework

222 lines 7.53 kB
import { Cache } from './cache.js'; import { Plan } from './router/plan.js'; import { Route } from './router/route.js'; import * as util from './util.js'; const PLACEHOLDER = {}; /** * Router class. */ export class Router extends Route { constructor() { super(); /** * Routing cache. */ this.cache = new Cache(); /** * Contains all available conditions. */ this.conditions = {}; /** * Directories to look for controllers in, first one has the highest precedence. */ this.controllerPaths = []; /** * Already loaded controllers. */ this.controllers = {}; /** * Registered placeholder types, by default only `num` is already defined. */ this.types = { num: /[0-9]+/ }; this._lookupIndex = undefined; this.root = this; } /** * Register a condition. */ addCondition(name, fn) { this.conditions[name] = fn; return this; } /** * Register a placeholder type. */ addType(name, value) { this.types[name] = value; return this; } /** * Match routes and dispatch to actions with and without controllers. */ async dispatch(ctx) { const plan = await this._getPlan(ctx); if (plan === null) return false; ctx.plan = plan; const { log, stash } = ctx; const { steps, stops } = plan; for (let i = 0; i < steps.length; i++) { const step = steps[i]; Object.assign(stash, step, { fn: undefined }); if (stops[i] === false) continue; if (typeof step.fn === 'function') { log.trace('Routing to function'); if ((await step.fn(ctx)) === false) break; } else if (typeof stash.controller === 'string' && typeof stash.action === 'string') { const controller = stash.controller; const action = stash.action; const Controller = this.controllers[controller]; if (Controller == null) { if (Controller === null) throw new Error(`Controller "${controller}" does not have a default export`); throw new Error(`Controller "${controller}" does not exist`); } const instance = new Controller(); if (instance[action] === undefined) throw new Error(`Action "${action}" does not exist`); log.trace(`Routing to "${controller}#${action}"`); if ((await instance[action](ctx)) === false) break; } } return true; } /** * Find child route by name, custom names have precedence over automatically generated ones. */ find(name) { return this._getLookupIndex()[name] ?? null; } /** * Find route by name and cache all results for future lookups. */ lookup(name) { if (this._lookupIndex === undefined) this._lookupIndex = this._getLookupIndex(); return this._lookupIndex[name] ?? null; } /** * Plot a route. */ async plot(spec) { const plan = new Plan(); const { steps, stops } = plan; for (const child of this.children) { if (await this._walk(plan, child, spec)) break; steps.pop(); stops.pop(); } return plan.endpoint === undefined ? null : plan; } /** * Prepare controllers for routing. */ async warmup() { Object.assign(this.controllers, await util.loadModules(this.controllerPaths)); } async _getPlan(ctx) { const { req } = ctx; const realMethod = req.method; if (realMethod === null) return null; let method = realMethod; if (realMethod === 'POST') { const params = req.query; const override = params.get('_method'); if (override !== null) method = override.toUpperCase(); } if (method === 'HEAD') method = 'GET'; const { path } = req; const { isWebSocket } = ctx; ctx.log.trace(`${realMethod} "${path}"`); // Cache deactivated if (this.cache === null) return await this.plot({ ctx, method, path, websocket: isWebSocket }); // Cached const cacheKey = `${method}:${path}:${isWebSocket.toString()}`; const { cache } = this; const cachedPlan = cache.get(cacheKey); if (cachedPlan !== undefined) return cachedPlan; // Not yet cached const plan = await this.plot({ ctx, method, path, websocket: isWebSocket }); if (plan === null) return null; cache.set(cacheKey, plan); return plan; } _getLookupIndex() { const defaultNames = {}; const customNames = {}; const children = [...this.children]; // eslint-disable-next-line @typescript-eslint/prefer-for-of for (let i = 0; i < children.length; i++) { const child = children[i]; if (child.customName !== undefined && customNames[child.customName] === undefined) { customNames[child.customName] = child; } else if (child.defaultName !== undefined && defaultNames[child.defaultName] === undefined) { defaultNames[child.defaultName] = child; } children.push(...child.children); } return { ...defaultNames, ...customNames }; } async _walk(plan, route, spec) { // Path const isEndpoint = route.isEndpoint(); const result = route.pattern.matchPartial(spec.path, { isEndpoint }); const { steps, stops } = plan; stops.push(isEndpoint || route.underRoute); if (result === null) { steps.push(PLACEHOLDER); return false; } steps.push(result.captures); if (isEndpoint && result.remainder.length > 0 && result.remainder !== '/') return false; // Methods const { methods } = route; if (methods.length > 0 && !methods.includes(spec.method)) return false; // WebSocket if (route.websocketRoute && !spec.websocket) return false; // Conditions if (route.requirements !== undefined) { const { root } = route; if (root === undefined) return false; const conditions = root.conditions; for (const value of route.requirements) { if (spec.ctx === undefined || (await conditions[value.condition](spec.ctx, value.requirement)) === false) { return false; } } } // Endpoint if (isEndpoint === true) { plan.endpoint = route; return true; } // Children for (const child of route.children) { const old = spec.path; spec.path = result.remainder; if ((await this._walk(plan, child, spec)) === true) return true; spec.path = old; steps.pop(); stops.pop(); } return false; } } //# sourceMappingURL=router.js.map