UNPKG

veloze

Version:

A modern and fast express-like webserver for the web

148 lines (135 loc) 3.76 kB
import { LRUCache } from 'mnemonist' import { safeDecodeUriComponent } from './utils/safeDecode.js' const METHODS = Symbol('methods') const PARAM = Symbol('param') const PARAM_PART = Symbol('paramPart') const WILDCARD = Symbol('*') /** @typedef {import('./types.js').Method} Method */ /** @typedef {import('./types.js').Handler} Handler */ /** * Radix Tree Router * * - Case-sensitive router according to [RFC3986](https://www.rfc-editor.org/rfc/rfc3986). * - Duplicate slashes are NOT ignored. * - No regular expressions. * - Tailing slash resolves to different route. E.g. `/path !== /path/` * - supports wildcard routes `/path/*`. * - parameters `/users/:user`, e.g. `/users/andi` resolves to `params = { user: 'andi' }` */ export class FindRoute { _tree = {} _paths = {} _cache /** * @param {number} [size=1000] */ constructor(size = 1000) { this._cache = size > 0 ? new LRUCache(size) : null } get paths() { return this._paths } /** * add handler by method and pathname to routing tree * @param {Method} method * @param {string|string[]} pathname * @param {Function} handler */ add(method, pathname, handler) { if (Array.isArray(pathname)) { pathname.forEach((path) => this.add(method, path, handler)) return } this._paths[pathname] = this._paths[pathname] || [] this._paths[pathname].push({ method, handler }) const parts = pathname.replace(/[/]{1,5}$/, '/').split('/') let tmp = this._tree for (const part of parts) { if (part === '*') { tmp = tmp[WILDCARD] = tmp[WILDCARD] || {} } else if (part.startsWith(':')) { tmp[PARAM] = part.slice(1) tmp = tmp[PARAM_PART] = tmp[PARAM_PART] || {} } else { tmp = tmp[part] = tmp[part] || {} } } tmp[METHODS] = tmp[METHODS] || {} tmp[METHODS][method] = handler } /** * mount other router tree * @param {string} pathname * @param {FindRoute} tree * @param {(handler: Handler) => Handler} connected */ mount(pathname, tree, connected) { if (pathname === '/') { pathname = '' } for (let [path, arr] of Object.entries(tree.paths)) { for (let { method, handler } of arr) { if (path === '/' && pathname) { path = '' } this.add(method, pathname + path, connected(handler)) } } } /** * print routing tree on console */ print() { console.dir(this._tree, { depth: null }) } /** * find route handlers by method and url * @param {object} param0 * @param {Method} param0.method * @param {string} param0.url * @returns {{ * handler: Function * params: object * path: string * }|undefined} */ find({ method, url }) { const [path] = url.split('?') const cached = this._cache?.get(method + path) if (cached) { return cached } const parts = (path || '/').split('/') const params = {} let wildcard let tmp = this._tree for (let i = 0; i < parts.length; i += 1) { const part = parts[i] let next = tmp?.[part] if (!next && part && tmp[PARAM_PART]) { const param = tmp[PARAM] params[param] = safeDecodeUriComponent(part) next = tmp[PARAM_PART] } if (tmp[WILDCARD]) { wildcard = tmp[WILDCARD] } if (!next) { tmp = wildcard break } tmp = next } const handler = getHandler(tmp, method) || getHandler(wildcard, method) if (!handler) { return } this._cache?.set(method + path, { handler, params, path }) return { handler, params, path } } } const getHandler = (tmp, method) => tmp?.[METHODS]?.[method] || tmp?.[METHODS]?.ALL