0
0
mirror of https://github.com/honojs/hono.git synced 2024-11-21 18:18:57 +01:00

perf(router): improve performance of router (#3526)

* perf(router):  improve performance of router

* coverage
This commit is contained in:
EdamAmex 2024-10-26 11:04:04 +09:00 committed by GitHub
parent 234b083777
commit 10a5e65e85
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 130 additions and 112 deletions

View File

@ -49,4 +49,17 @@ describe('LinearRouter', () => {
expect(res[0][0]).toBe('GET /book')
})
})
describe('Skip part', () => {
const router = new LinearRouter<string>()
beforeEach(() => {
router.add('GET', '/products/:id{d+}', 'GET /products/:id{d+}')
})
it('GET /products/list', () => {
const [res] = router.match('GET', '/products/list')
expect(res.length).toBe(0)
})
})
})

View File

@ -13,117 +13,123 @@ export class LinearRouter<T> implements Router<T> {
routes: [string, string, T][] = []
add(method: string, path: string, handler: T) {
;(checkOptionalParameter(path) || [path]).forEach((p) => {
this.routes.push([method, p, handler])
})
for (
let i = 0, paths = checkOptionalParameter(path) || [path], len = paths.length;
i < len;
i++
) {
this.routes.push([method, paths[i], handler])
}
}
match(method: string, path: string): Result<T> {
const handlers: [T, Params][] = []
ROUTES_LOOP: for (let i = 0, len = this.routes.length; i < len; i++) {
const [routeMethod, routePath, handler] = this.routes[i]
if (routeMethod !== method && routeMethod !== METHOD_NAME_ALL) {
continue
}
if (routePath === '*' || routePath === '/*') {
handlers.push([handler, emptyParams])
continue
}
const hasStar = routePath.indexOf('*') !== -1
const hasLabel = routePath.indexOf(':') !== -1
if (!hasStar && !hasLabel) {
if (routePath === path || routePath + '/' === path) {
if (routeMethod === method || routeMethod === METHOD_NAME_ALL) {
if (routePath === '*' || routePath === '/*') {
handlers.push([handler, emptyParams])
continue
}
} else if (hasStar && !hasLabel) {
const endsWithStar = routePath.charCodeAt(routePath.length - 1) === 42
const parts = (endsWithStar ? routePath.slice(0, -2) : routePath).split(splitByStarRe)
const lastIndex = parts.length - 1
for (let j = 0, pos = 0, len = parts.length; j < len; j++) {
const part = parts[j]
const index = path.indexOf(part, pos)
if (index !== pos) {
continue ROUTES_LOOP
const hasStar = routePath.indexOf('*') !== -1
const hasLabel = routePath.indexOf(':') !== -1
if (!hasStar && !hasLabel) {
if (routePath === path || routePath + '/' === path) {
handlers.push([handler, emptyParams])
}
pos += part.length
if (j === lastIndex) {
if (
!endsWithStar &&
pos !== path.length &&
!(pos === path.length - 1 && path.charCodeAt(pos) === 47)
) {
continue ROUTES_LOOP
}
} else {
const index = path.indexOf('/', pos)
if (index === -1) {
continue ROUTES_LOOP
}
pos = index
}
}
handlers.push([handler, emptyParams])
} else if (hasLabel && !hasStar) {
const params: Record<string, string> = Object.create(null)
const parts = routePath.match(splitPathRe) as string[]
} else if (hasStar && !hasLabel) {
const endsWithStar = routePath.charCodeAt(routePath.length - 1) === 42
const parts = (endsWithStar ? routePath.slice(0, -2) : routePath).split(splitByStarRe)
const lastIndex = parts.length - 1
for (let j = 0, pos = 0, len = parts.length; j < len; j++) {
if (pos === -1 || pos >= path.length) {
continue ROUTES_LOOP
}
const part = parts[j]
if (part.charCodeAt(1) === 58) {
// /:label
let name = part.slice(2)
let value
if (name.charCodeAt(name.length - 1) === 125) {
// :label{pattern}
const openBracePos = name.indexOf('{')
const pattern = name.slice(openBracePos + 1, -1)
const restPath = path.slice(pos + 1)
const match = new RegExp(pattern, 'd').exec(restPath) as RegExpMatchArrayWithIndices
if (!match || match.indices[0][0] !== 0 || match.indices[0][1] === 0) {
continue ROUTES_LOOP
}
name = name.slice(0, openBracePos)
value = restPath.slice(...match.indices[0])
pos += match.indices[0][1] + 1
} else {
let endValuePos = path.indexOf('/', pos + 1)
if (endValuePos === -1) {
if (pos + 1 === path.length) {
continue ROUTES_LOOP
}
endValuePos = path.length
}
value = path.slice(pos + 1, endValuePos)
pos = endValuePos
}
params[name] ||= value as string
} else {
const lastIndex = parts.length - 1
for (let j = 0, pos = 0, len = parts.length; j < len; j++) {
const part = parts[j]
const index = path.indexOf(part, pos)
if (index !== pos) {
continue ROUTES_LOOP
}
pos += part.length
}
if (j === lastIndex) {
if (pos !== path.length && !(pos === path.length - 1 && path.charCodeAt(pos) === 47)) {
continue ROUTES_LOOP
if (j === lastIndex) {
if (
!endsWithStar &&
pos !== path.length &&
!(pos === path.length - 1 && path.charCodeAt(pos) === 47)
) {
continue ROUTES_LOOP
}
} else {
const index = path.indexOf('/', pos)
if (index === -1) {
continue ROUTES_LOOP
}
pos = index
}
}
}
handlers.push([handler, emptyParams])
} else if (hasLabel && !hasStar) {
const params: Record<string, string> = Object.create(null)
const parts = routePath.match(splitPathRe) as string[]
handlers.push([handler, params])
} else if (hasLabel && hasStar) {
throw new UnsupportedPathError()
const lastIndex = parts.length - 1
for (let j = 0, pos = 0, len = parts.length; j < len; j++) {
if (pos === -1 || pos >= path.length) {
continue ROUTES_LOOP
}
const part = parts[j]
if (part.charCodeAt(1) === 58) {
// /:label
let name = part.slice(2)
let value
if (name.charCodeAt(name.length - 1) === 125) {
// :label{pattern}
const openBracePos = name.indexOf('{')
const pattern = name.slice(openBracePos + 1, -1)
const restPath = path.slice(pos + 1)
const match = new RegExp(pattern, 'd').exec(restPath) as RegExpMatchArrayWithIndices
if (!match || match.indices[0][0] !== 0 || match.indices[0][1] === 0) {
continue ROUTES_LOOP
}
name = name.slice(0, openBracePos)
value = restPath.slice(...match.indices[0])
pos += match.indices[0][1] + 1
} else {
let endValuePos = path.indexOf('/', pos + 1)
if (endValuePos === -1) {
if (pos + 1 === path.length) {
continue ROUTES_LOOP
}
endValuePos = path.length
}
value = path.slice(pos + 1, endValuePos)
pos = endValuePos
}
params[name] ||= value as string
} else {
const index = path.indexOf(part, pos)
if (index !== pos) {
continue ROUTES_LOOP
}
pos += part.length
}
if (j === lastIndex) {
if (
pos !== path.length &&
!(pos === path.length - 1 && path.charCodeAt(pos) === 47)
) {
continue ROUTES_LOOP
}
}
}
handlers.push([handler, params])
} else if (hasLabel && hasStar) {
throw new UnsupportedPathError()
}
}
}

View File

@ -40,8 +40,10 @@ export class PatternRouter<T> implements Router<T> {
match(method: string, path: string): Result<T> {
const handlers: [T, Params][] = []
for (const [pattern, routeMethod, handler] of this.routes) {
if (routeMethod === METHOD_NAME_ALL || routeMethod === method) {
for (let i = 0, len = this.routes.length; i < len; i++) {
const [pattern, routeMethod, handler] = this.routes[i]
if (routeMethod === method || routeMethod === METHOD_NAME_ALL) {
const match = pattern.exec(path)
if (match) {
handlers.push([handler, match.groups || Object.create(null)])

View File

@ -30,9 +30,9 @@ export class SmartRouter<T> implements Router<T> {
for (; i < len; i++) {
const router = routers[i]
try {
routes.forEach((args) => {
router.add(...args)
})
for (let i = 0, len = routes.length; i < len; i++) {
router.add(...routes[i])
}
res = router.match(method, path)
} catch (e) {
if (e instanceof UnsupportedPathError) {

View File

@ -7,7 +7,6 @@ type HandlerSet<T> = {
handler: T
possibleKeys: string[]
score: number
name: string // For debug
}
type HandlerParamsSet<T> = HandlerSet<T> & {
@ -20,23 +19,20 @@ export class Node<T> {
children: Record<string, Node<T>>
patterns: Pattern[]
order: number = 0
name: string
params: Record<string, string> = Object.create(null)
constructor(method?: string, handler?: T, children?: Record<string, Node<T>>) {
this.children = children || Object.create(null)
this.methods = []
this.name = ''
if (method && handler) {
const m: Record<string, HandlerSet<T>> = Object.create(null)
m[method] = { handler, possibleKeys: [], score: 0, name: this.name }
m[method] = { handler, possibleKeys: [], score: 0 }
this.methods = [m]
}
this.patterns = []
}
insert(method: string, path: string, handler: T): Node<T> {
this.name = `${method} ${path}`
this.order = ++this.order
// eslint-disable-next-line @typescript-eslint/no-this-alias
@ -76,7 +72,6 @@ export class Node<T> {
const handlerSet: HandlerSet<T> = {
handler,
possibleKeys: possibleKeys.filter((v, i, a) => a.indexOf(v) === i),
name: this.name,
score: this.order,
}
@ -100,12 +95,14 @@ export class Node<T> {
const processedSet: Record<string, boolean> = Object.create(null)
if (handlerSet !== undefined) {
handlerSet.params = Object.create(null)
handlerSet.possibleKeys.forEach((key) => {
const processed = processedSet[handlerSet.name]
for (let i = 0, len = handlerSet.possibleKeys.length; i < len; i++) {
const key = handlerSet.possibleKeys[i]
const processed = processedSet[handlerSet.score]
handlerSet.params[key] =
params[key] && !processed ? params[key] : nodeParams[key] ?? params[key]
processedSet[handlerSet.name] = true
})
processedSet[handlerSet.score] = true
}
handlerSets.push(handlerSet)
}
}
@ -132,7 +129,7 @@ export class Node<T> {
if (nextNode) {
nextNode.params = node.params
if (isLast === true) {
if (isLast) {
// '/hello/*' => match '/hello'
if (nextNode.children['*']) {
handlerSets.push(
@ -177,10 +174,10 @@ export class Node<T> {
continue
}
if (matcher === true || (matcher instanceof RegExp && matcher.test(part))) {
if (matcher === true || matcher.test(part)) {
if (typeof key === 'string') {
params[name] = part
if (isLast === true) {
if (isLast) {
handlerSets.push(...this.gHSets(child, method, params, node.params))
if (child.children['*']) {
handlerSets.push(...this.gHSets(child.children['*'], method, params, node.params))

View File

@ -13,8 +13,8 @@ export class TrieRouter<T> implements Router<T> {
add(method: string, path: string, handler: T) {
const results = checkOptionalParameter(path)
if (results) {
for (const p of results) {
this.node.insert(method, p, handler)
for (let i = 0, len = results.length; i < len; i++) {
this.node.insert(method, results[i], handler)
}
return
}