veloze
Version:
A modern and fast express-like webserver for the web
118 lines (108 loc) • 3.03 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
*/
/**
* 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 = {}
_cache
/**
* @param {number} [size=1000]
*/
constructor(size = 1000) {
this._cache = size > 0 ? new LRUCache(size) : null
}
/**
* 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
}
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
}
/**
* 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