veloze
Version:
A modern and fast express-like webserver for the web
148 lines (135 loc) • 3.76 kB
JavaScript
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