Skip to content

Commit

Permalink
Switch to using one tree per method instead of a map (#168)
Browse files Browse the repository at this point in the history
  • Loading branch information
airhorns authored Oct 31, 2020
1 parent ce6d38b commit b9337ca
Show file tree
Hide file tree
Showing 5 changed files with 200 additions and 130 deletions.
27 changes: 27 additions & 0 deletions bench.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,32 @@ const findMyWay = new FindMyWay()
findMyWay.on('GET', '/', () => true)
findMyWay.on('GET', '/user/:id', () => true)
findMyWay.on('GET', '/user/:id/static', () => true)
findMyWay.on('POST', '/user/:id', () => true)
findMyWay.on('PUT', '/user/:id', () => true)
findMyWay.on('GET', '/customer/:name-:surname', () => true)
findMyWay.on('POST', '/customer', () => true)
findMyWay.on('GET', '/at/:hour(^\\d+)h:minute(^\\d+)m', () => true)
findMyWay.on('GET', '/abc/def/ghi/lmn/opq/rst/uvz', () => true)
findMyWay.on('GET', '/', { version: '1.2.0' }, () => true)

findMyWay.on('GET', '/products', () => true)
findMyWay.on('GET', '/products/:id', () => true)
findMyWay.on('GET', '/products/:id/options', () => true)

findMyWay.on('GET', '/posts', () => true)
findMyWay.on('POST', '/posts', () => true)
findMyWay.on('GET', '/posts/:id', () => true)
findMyWay.on('GET', '/posts/:id/author', () => true)
findMyWay.on('GET', '/posts/:id/comments', () => true)
findMyWay.on('POST', '/posts/:id/comments', () => true)
findMyWay.on('GET', '/posts/:id/comments/:id', () => true)
findMyWay.on('GET', '/posts/:id/comments/:id/author', () => true)
findMyWay.on('GET', '/posts/:id/counter', () => true)

findMyWay.on('GET', '/pages', () => true)
findMyWay.on('POST', '/pages', () => true)
findMyWay.on('GET', '/pages/:id', () => true)

suite
.add('lookup static route', function () {
findMyWay.lookup({ method: 'GET', url: '/', headers: {} }, null)
Expand Down Expand Up @@ -65,6 +86,12 @@ suite
.add('find static versioned route', function () {
findMyWay.find('GET', '/', '1.x')
})
.add('find long nested dynamic route', function () {
findMyWay.find('GET', '/posts/10/comments/42/author', undefined)
})
.add('find long nested dynamic route with other method', function () {
findMyWay.find('POST', '/posts/10/comments', undefined)
})
.on('cycle', function (event) {
console.log(String(event.target))
})
Expand Down
82 changes: 53 additions & 29 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const assert = require('assert')
const http = require('http')
const fastDecode = require('fast-decode-uri-component')
const isRegexSafe = require('safe-regex2')
const { flattenNode, compressFlattenedNode, prettyPrintFlattenedNode } = require('./lib/pretty-print')
const Node = require('./node')
const NODE_TYPES = Node.prototype.types
const httpMethods = http.METHODS
Expand Down Expand Up @@ -51,7 +52,7 @@ function Router (opts) {
this.maxParamLength = opts.maxParamLength || 100
this.allowUnsafeRegex = opts.allowUnsafeRegex || false
this.versioning = opts.versioning || acceptVersionStrategy
this.tree = new Node({ versions: this.versioning.storage() })
this.trees = {}
this.routes = []
}

Expand Down Expand Up @@ -195,14 +196,19 @@ Router.prototype._on = function _on (method, path, opts, handler, store) {

Router.prototype._insert = function _insert (method, path, kind, params, handler, store, regex, version) {
const route = path
var currentNode = this.tree
var prefix = ''
var pathLen = 0
var prefixLen = 0
var len = 0
var max = 0
var node = null

var currentNode = this.trees[method]
if (typeof currentNode === 'undefined') {
currentNode = new Node({ method: method, versions: this.versioning.storage() })
this.trees[method] = currentNode
}

while (true) {
prefix = currentNode.prefix
prefixLen = prefix.length
Expand All @@ -218,10 +224,11 @@ Router.prototype._insert = function _insert (method, path, kind, params, handler
if (len < prefixLen) {
node = new Node(
{
method: method,
prefix: prefix.slice(len),
children: currentNode.children,
kind: currentNode.kind,
handlers: new Node.Handlers(currentNode.handlers),
handler: currentNode.handler,
regex: currentNode.regex,
versions: currentNode.versions
}
Expand All @@ -239,25 +246,26 @@ Router.prototype._insert = function _insert (method, path, kind, params, handler
// the handler should be added to the current node, to a child otherwise
if (len === pathLen) {
if (version) {
assert(!currentNode.getVersionHandler(version, method), `Method '${method}' already declared for route '${route}' version '${version}'`)
currentNode.setVersionHandler(version, method, handler, params, store)
assert(!currentNode.getVersionHandler(version), `Method '${method}' already declared for route '${route}' version '${version}'`)
currentNode.setVersionHandler(version, handler, params, store)
} else {
assert(!currentNode.getHandler(method), `Method '${method}' already declared for route '${route}'`)
currentNode.setHandler(method, handler, params, store)
assert(!currentNode.handler, `Method '${method}' already declared for route '${route}'`)
currentNode.setHandler(handler, params, store)
}
currentNode.kind = kind
} else {
node = new Node({
method: method,
prefix: path.slice(len),
kind: kind,
handlers: null,
regex: regex,
versions: this.versioning.storage()
})
if (version) {
node.setVersionHandler(version, method, handler, params, store)
node.setVersionHandler(version, handler, params, store)
} else {
node.setHandler(method, handler, params, store)
node.setHandler(handler, params, store)
}
currentNode.addChild(node)
}
Expand All @@ -275,31 +283,31 @@ Router.prototype._insert = function _insert (method, path, kind, params, handler
continue
}
// there are not children within the given label, let's create a new one!
node = new Node({ prefix: path, kind: kind, handlers: null, regex: regex, versions: this.versioning.storage() })
node = new Node({ method: method, prefix: path, kind: kind, regex: regex, versions: this.versioning.storage() })
if (version) {
node.setVersionHandler(version, method, handler, params, store)
node.setVersionHandler(version, handler, params, store)
} else {
node.setHandler(method, handler, params, store)
node.setHandler(handler, params, store)
}

currentNode.addChild(node)

// the node already exist
} else if (handler) {
if (version) {
assert(!currentNode.getVersionHandler(version, method), `Method '${method}' already declared for route '${route}' version '${version}'`)
currentNode.setVersionHandler(version, method, handler, params, store)
assert(!currentNode.getVersionHandler(version), `Method '${method}' already declared for route '${route}' version '${version}'`)
currentNode.setVersionHandler(version, handler, params, store)
} else {
assert(!currentNode.getHandler(method), `Method '${method}' already declared for route '${route}'`)
currentNode.setHandler(method, handler, params, store)
assert(!currentNode.handler, `Method '${method}' already declared for route '${route}'`)
currentNode.setHandler(handler, params, store)
}
}
return
}
}

Router.prototype.reset = function reset () {
this.tree = new Node({ versions: this.versioning.storage() })
this.trees = {}
this.routes = []
}

Expand Down Expand Up @@ -358,6 +366,9 @@ Router.prototype.lookup = function lookup (req, res, ctx) {
}

Router.prototype.find = function find (method, path, version) {
var currentNode = this.trees[method]
if (!currentNode) return null

if (path.charCodeAt(0) !== 47) { // 47 is '/'
path = path.replace(FULL_PATH_REGEXP, '/')
}
Expand All @@ -370,7 +381,6 @@ Router.prototype.find = function find (method, path, version) {
}

var maxParamLength = this.maxParamLength
var currentNode = this.tree
var wildcardNode = null
var pathLenWildcard = 0
var decoded = null
Expand All @@ -388,8 +398,8 @@ Router.prototype.find = function find (method, path, version) {
// found the route
if (pathLen === 0 || path === prefix) {
var handle = version === undefined
? currentNode.handlers[method]
: currentNode.getVersionHandler(version, method)
? currentNode.handler
: currentNode.getVersionHandler(version)
if (handle !== null && handle !== undefined) {
var paramsObj = {}
if (handle.paramsLength > 0) {
Expand Down Expand Up @@ -419,13 +429,13 @@ Router.prototype.find = function find (method, path, version) {
}

var node = version === undefined
? currentNode.findChild(path, method)
: currentNode.findVersionChild(version, path, method)
? currentNode.findChild(path)
: currentNode.findVersionChild(version, path)

if (node === null) {
node = currentNode.parametricBrother
if (node === null) {
return this._getWildcardNode(wildcardNode, method, originalPath, pathLenWildcard)
return this._getWildcardNode(wildcardNode, originalPath, pathLenWildcard)
}

var goBack = previousPath.charCodeAt(0) === 47 ? previousPath : '/' + previousPath
Expand All @@ -448,7 +458,7 @@ Router.prototype.find = function find (method, path, version) {
// static route
if (kind === NODE_TYPES.STATIC) {
// if exist, save the wildcard child
if (currentNode.wildcardChild !== null && currentNode.wildcardChild.handlers[method] !== null) {
if (currentNode.wildcardChild !== null) {
wildcardNode = currentNode.wildcardChild
pathLenWildcard = pathLen
}
Expand All @@ -457,11 +467,11 @@ Router.prototype.find = function find (method, path, version) {
}

if (len !== prefixLen) {
return this._getWildcardNode(wildcardNode, method, originalPath, pathLenWildcard)
return this._getWildcardNode(wildcardNode, originalPath, pathLenWildcard)
}

// if exist, save the wildcard child
if (currentNode.wildcardChild !== null && currentNode.wildcardChild.handlers[method] !== null) {
if (currentNode.wildcardChild !== null) {
wildcardNode = currentNode.wildcardChild
pathLenWildcard = pathLen
}
Expand Down Expand Up @@ -545,15 +555,15 @@ Router.prototype.find = function find (method, path, version) {
}
}

Router.prototype._getWildcardNode = function (node, method, path, len) {
Router.prototype._getWildcardNode = function (node, path, len) {
if (node === null) return null
var decoded = fastDecode(path.slice(-len))
if (decoded === null) {
return this.onBadUrl !== null
? this._onBadUrl(path.slice(-len))
: null
}
var handle = node.handlers[method]
var handle = node.handler
if (handle !== null && handle !== undefined) {
return {
handler: handle.handler,
Expand Down Expand Up @@ -585,7 +595,21 @@ Router.prototype._onBadUrl = function (path) {
}

Router.prototype.prettyPrint = function () {
return this.tree.prettyPrint('', true)
const root = {
prefix: '/',
nodes: [],
children: {}
}

for (const node of Object.values(this.trees)) {
if (node) {
flattenNode(root, node)
}
}

compressFlattenedNode(root)

return prettyPrintFlattenedNode(root, '', true)
}

for (var i in http.METHODS) {
Expand Down
83 changes: 83 additions & 0 deletions lib/pretty-print.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
function prettyPrintFlattenedNode (flattenedNode, prefix, tail) {
var paramName = ''
var methods = new Set(flattenedNode.nodes.map(node => node.method))

if (flattenedNode.prefix.includes(':')) {
flattenedNode.nodes.forEach((node, index) => {
var params = node.handler.params
var param = params[params.length - 1]
if (methods.size > 1) {
if (index === 0) {
paramName += param + ` (${node.method})\n`
return
}
paramName += prefix + ' :' + param + ` (${node.method})`
paramName += (index === methods.size - 1 ? '' : '\n')
} else {
paramName = params[params.length - 1] + ` (${node.method})`
}
})
} else if (methods.size) {
paramName = ` (${Array.from(methods).join('|')})`
}

var tree = `${prefix}${tail ? '└── ' : '├── '}${flattenedNode.prefix}${paramName}\n`

prefix = `${prefix}${tail ? ' ' : '│ '}`
const labels = Object.keys(flattenedNode.children)
for (var i = 0; i < labels.length; i++) {
const child = flattenedNode.children[labels[i]]
tree += prettyPrintFlattenedNode(child, prefix, i === (labels.length - 1))
}
return tree
}

function flattenNode (flattened, node) {
if (node.handler) {
flattened.nodes.push(node)
}

if (node.children) {
for (const child of Object.values(node.children)) {
const childPrefixSegments = child.prefix.split(/(?=\/)/) // split on the slash separator but use a regex to lookahead and not actually match it, preserving it in the returned string segments
let cursor = flattened
let parent
for (const segment of childPrefixSegments) {
parent = cursor
cursor = cursor.children[segment]
if (!cursor) {
cursor = {
prefix: segment,
nodes: [],
children: {}
}
parent.children[segment] = cursor
}
}

flattenNode(cursor, child)
}
}
}

function compressFlattenedNode (flattenedNode) {
const childKeys = Object.keys(flattenedNode.children)
if (flattenedNode.nodes.length === 0 && childKeys.length === 1) {
const child = flattenedNode.children[childKeys[0]]
if (child.nodes.length <= 1) {
compressFlattenedNode(child)
flattenedNode.nodes = child.nodes
flattenedNode.prefix += child.prefix
flattenedNode.children = child.children
return flattenedNode
}
}

for (const key of Object.keys(flattenedNode.children)) {
compressFlattenedNode(flattenedNode.children[key])
}

return flattenedNode
}

module.exports = { flattenNode, compressFlattenedNode, prettyPrintFlattenedNode }
Loading

0 comments on commit b9337ca

Please sign in to comment.