light-router
Advanced tools
Comparing version 0.0.211 to 0.1.0
//Dependencies | ||
var Cache = require('./cache/') | ||
var Cache = require('base-cache') | ||
var Routes = require('./routes') | ||
@@ -4,0 +4,0 @@ |
//Dependencies | ||
var url = require('url'); | ||
var Routes = require('./routes') | ||
@@ -20,3 +19,3 @@ | ||
//Split the path into paths to start indexing | ||
var paths = pathname.split('/') | ||
var paths = splitPath(pathname) | ||
@@ -26,27 +25,104 @@ //Size, this will be used as an index later on | ||
//Discover parameters in the route | ||
var parameters = pathname2ParamsArray(paths) | ||
//Check if its a static route | ||
var params = pathname2ParamsArray(paths) | ||
//Static asset, add it to the static table inside the routing table | ||
if(parameters.length === 0) { | ||
addStatic2Table(table, pathname, handler) | ||
//Its static, add to static routes and finish | ||
if(params.length === 0) { | ||
table.staticRoutes[pathname] = { | ||
handler: handler | ||
} | ||
table = table.staticRoutes[pathname] //recurse into | ||
} | ||
//Add this to the matching table | ||
//Else, its a dynamic route, recurse into dynamicRoutes | ||
else { | ||
var match = add2MatchingTable(table, size, paths, parameters) | ||
addHandler2MatchingTable(match, handler) | ||
} | ||
} | ||
table = table.dynamicRoutes | ||
//Check if a routing table for the given size filter already exists, if not create | ||
if(typeof table[size] === 'undefined') { | ||
table[size] = { | ||
params: [], | ||
bases: {}, | ||
} | ||
} | ||
//Helper function to check if this base_path exists in an array of routes | ||
function indexOfInRouter(base, router) { | ||
for(var i in router) { | ||
if(router[i].base === base) { | ||
return i | ||
//Recurse into table filtered by size | ||
table = table[size] | ||
//Add route to dynamic table table | ||
for(var i = 0; i < size; i++) { | ||
var path = paths[i] | ||
var param_index = indexOfParam(i, params) | ||
//Its an parameter | ||
if(param_index > -1) { | ||
var param = params[ param_index ] | ||
var length = table.params.push({ | ||
param: param.name, | ||
}) | ||
//Recurse into table created | ||
table = table.params[ length - 1 ] | ||
//Check if a regexp was passed | ||
if(typeof param.regexp !== 'undefined') { | ||
table.regexp = new RegExp(param.regexp) | ||
} | ||
//Add routing scheme | ||
table.routes = { | ||
params: [], | ||
bases: {} | ||
} | ||
//Check if this is the last iteration, if so, add the handler | ||
if(i >= size - 1) { | ||
table = table.routes //recurse into | ||
table.handler = handler | ||
} | ||
//Else recurse into next table we will be working in | ||
else { | ||
table = table.routes | ||
} | ||
} | ||
//Its a base path | ||
else { | ||
//Create a new table, if not exists | ||
if(typeof table.bases[path] === 'undefined') { | ||
table.bases[path] = { | ||
params: [], | ||
bases: {}, | ||
} | ||
} | ||
//Recurse into routing table | ||
table = table.bases[path] | ||
//Check if this is the last iteration, if so, add the handler | ||
if(i >= size - 1) { | ||
table.handler = handler | ||
} | ||
} | ||
} | ||
} | ||
return -1 | ||
//Pointer to final table | ||
//The final table contains all metadata of the route | ||
this.table = table | ||
//Set caching on by default | ||
this.table.cache = true | ||
//Cache controller | ||
this.cache = function setCache(bool) { | ||
this.table.cache = bool | ||
return this | ||
} | ||
return this | ||
} | ||
//Helper function to trim the pathnames first and last slash | ||
@@ -75,6 +151,28 @@ //@return string (trimed pathname) | ||
if(path.charAt(0) === ':') { | ||
params.push({ | ||
pos: parseInt(p), | ||
name: path.substring(1) | ||
}) | ||
var push_data = { | ||
pos: parseInt(p) | ||
} | ||
//Remove `:` from path string | ||
path = path.substring(1) | ||
//Check if an regexp was passed | ||
if(path.indexOf('(') > -1 && path.lastIndexOf(')') > -1) { | ||
//Start, end | ||
var start = path.indexOf('(') | ||
var end = path.lastIndexOf(')') | ||
//Extract regexp string from path | ||
var regexp = path.substring(start + 1, end) | ||
//Remove regexp from path identifier | ||
path = path.substring(0, start) | ||
push_data.regexp = regexp | ||
} | ||
//Append parameter identifier | ||
push_data.name = path | ||
params.push(push_data) | ||
} | ||
@@ -97,80 +195,63 @@ } | ||
//Helper function to add a static route to a givens table static routes | ||
//@return void | ||
function addStatic2Table(table, path, handler) { | ||
table.static[path] = handler | ||
} | ||
//Helper function to properly parse a url, this replaces require('url').parse(path).pathname | ||
function urlParse(url) { | ||
var query = url.indexOf('?') | ||
//Helper function to add a entry to a determined matching table | ||
//@return object | ||
function add2MatchingTable(table, size, paths, params) { | ||
var matching_table = table.matching | ||
if(query !== -1) { | ||
url = url.substring(0, query) | ||
} | ||
//Check if this table was already initialized | ||
initMatchingTable(matching_table, size) | ||
return url | ||
} | ||
//Generate a match entry | ||
var entry = [] | ||
//Helper function to split the path into smaller base paths | ||
//A simple pathname.split('/') wont work, since regexp can contain slashes | ||
//@input string | ||
//@return array | ||
function splitPath(path) { | ||
var paths = [] | ||
var buffer = '' | ||
var i = 0 | ||
for(var i = 0; i < size; i++) { | ||
//Defaults | ||
var type = 0 | ||
var _static = "", param = "" | ||
//Modes | ||
var inParameter = false, inRegexp = false | ||
//Index of parameter in parameters array, if exists | ||
var index = indexOfParam(i, params) | ||
while(i < path.length) { | ||
//Check if a parameter is starting | ||
if(path[i - 1] == '/' && path[i] == ':') { | ||
inParameter = true | ||
} | ||
//No parameter found in the given position, so its a static part of the path | ||
if(index === -1) { | ||
type = 0 //Use the static algorith matching | ||
_static = paths[i] | ||
//Check if they passed a regexp | ||
//This initialize the regexp buffer | ||
if(inParameter === true && inRegexp === false && path[i] == '(') { | ||
inRegexp = true | ||
} | ||
//Its a parameter | ||
//Deactivate | ||
else if(inRegexp === true && path[i] == ')' && path[i + 1] == '/') { | ||
inRegexp = false | ||
} | ||
//Clear buffer or append | ||
if(path[i] == '/' && inRegexp === false) { | ||
//Clear buffer | ||
paths.push(buffer) | ||
buffer = '' | ||
inParameter = false //default | ||
} | ||
else { | ||
type = 1 //Use the parameter algorith matching | ||
param = params[index].name | ||
buffer = buffer + path[i] | ||
} | ||
//Push this position to the matching size table | ||
entry.push({ | ||
type: type, | ||
static: _static, | ||
param: param | ||
}) | ||
i++ //next character | ||
} | ||
//Push the entry to the given size matching_table | ||
var length = matching_table[size].push(entry) | ||
return matching_table[size][length - 1] | ||
} | ||
//Helper function to asign a handler to the final condition of a match | ||
//@return void | ||
function addHandler2MatchingTable(match, handler) { | ||
var last = match.length - 1; | ||
match[last].handler = handler | ||
} | ||
//Helper function to initialize a matching table given its size | ||
//@return void | ||
function initMatchingTable(matching_table, size) { | ||
if(typeof matching_table[size] === 'undefined') { | ||
matching_table[size] = [] | ||
//Push any remaining buffers | ||
if(buffer) { | ||
paths.push(buffer) | ||
} | ||
} | ||
//Helper function to properly parse a url, this replaces require('url').parse(path).pathname | ||
function urlParse(url) { | ||
var query = url.indexOf('?') | ||
if(query !== -1) { | ||
url = url.substring(0, query) | ||
} | ||
return url | ||
return paths | ||
} | ||
//Exports | ||
@@ -177,0 +258,0 @@ Parser.trimPathname = trimPathname |
@@ -1,2 +0,2 @@ | ||
//Dependencies | ||
//Route parser | ||
var Parser = require('./parser') | ||
@@ -10,2 +10,8 @@ | ||
//Return a 404 http code | ||
var notFound = function defaultNotFound(req, res) { | ||
res.statusCode = 404 | ||
res.end('404 - Not found') | ||
} | ||
//Route finder, if nothing is matched nothing is done | ||
@@ -20,7 +26,13 @@ //@return void | ||
//Parse the url | ||
var path = Parser.url(req.url) | ||
//Remove the first and last slash frm the path if present | ||
path = Parser.trimPathname(path) | ||
//Try to make a cache hit here | ||
var hit = cache.find(req.url) | ||
var hit = cache.find(path) | ||
if(typeof hit !== 'undefined') { | ||
req.params = hit.params || null | ||
req.params = hit.params || {} | ||
hit.handler(req, res) | ||
@@ -30,14 +42,13 @@ return | ||
//Parse the url | ||
var path = Parser.url(req.url) | ||
//Try to match one of the static routes first, only then fallback to the dynamic routes | ||
var static_match = table.staticRoutes[path] | ||
//Remove the first and last slash frm the path if present | ||
path = Parser.trimPathname(path) | ||
if(typeof static_match !== 'undefined') { | ||
var handler = static_match.handler | ||
//Try to match one of the static routes first, only then fallback to the dynamic routes | ||
var static_match = table.static[path] | ||
if(static_match.cache === true) { | ||
cache.add(path, {handler: handler}) | ||
} | ||
if(typeof static_match !== 'undefined') { | ||
cache.add(req.url, {handler: static_match}) | ||
static_match(req, res) | ||
handler(req, res) | ||
return | ||
@@ -52,57 +63,79 @@ } | ||
//Matching/parameter router based on the size | ||
var matching = table.matching[size] | ||
//Recurse into dynamicRoutes table | ||
table = table.dynamicRoutes[size] | ||
//Check if there are any dynamic routes this size, if not, dont even bother | ||
if(typeof matching !== 'undefined') { | ||
//Itenerate throught the matches of the current table we are in based in the size of paths | ||
for(var m in matching) { | ||
var match = matching[m] | ||
if(typeof table === 'undefined') { | ||
return notFound(req, res) | ||
} | ||
//Clear the request parameters holder | ||
req.params = {} | ||
//Clear the request parameters holder | ||
req.params = {} | ||
//Loop through all parts of this match | ||
current_match: | ||
for(var p in match) { | ||
var condition = match[p] | ||
//Start iterating throught the routing tree | ||
continue_to_next_value: | ||
for(var i = 0; i < size; i++) { | ||
//Vars | ||
var value = paths[i] //The part of the path we are currently working with | ||
//How to match this part of the path | ||
switch(condition.type) { | ||
//Must equal to this | ||
case 0: | ||
//Check if doesnt match this condition | ||
if(condition.static !== paths[p]) { | ||
break current_match | ||
} | ||
break; | ||
//Can equal anything, just save its value | ||
case 1: | ||
//This isnt really a condition, anything will match here :) | ||
req.params[condition.param] = paths[p] | ||
//First try matching with the hashtable with base paths | ||
var base = table.bases[value] | ||
//Check if some base path was matched | ||
if(typeof base !== 'undefined') { | ||
table = base //recurse into | ||
continue //to next match | ||
} | ||
//Else, check the parameters table | ||
else if(table.params.length > 0) { | ||
var params = table.params | ||
for(var p in params) { | ||
var key = params[p].param | ||
var rule = params[p].regexp | ||
if(typeof rule !== 'undefined') { | ||
if(rule.test(value) === false) { | ||
continue //Try matching next parameter, if exists | ||
} | ||
} | ||
//We got a handler, run it :) it must be the last part of the path | ||
if(typeof condition.handler === 'function') { | ||
cache.add(req.url, { | ||
handler: condition.handler, | ||
params: req.params | ||
}) | ||
condition.handler(req, res) | ||
return | ||
} | ||
//If we got here, we found a possible parameter :) | ||
req.params[key] = value | ||
table = params[p].routes //recurse into | ||
continue continue_to_next_value //Break to next part of the paths | ||
} | ||
} | ||
//If we got here, nothing was found, return 404 | ||
//Add to cache, this 404, to save cpu in future requests. | ||
//We dont need to cache other 404 cause they dont take so much cpu as this one did | ||
cache.add(path, {handler: notFound}) | ||
return notFound(req, res) | ||
} | ||
//If we are here, the dynamicRoutes algorithm must have found a route | ||
//But just in case my algorithm has a logic error, i'll add the default notFound :) | ||
var routeHandler = table.handler || notFound | ||
var shouldCache = table.cache || true | ||
if(shouldCache === true) { | ||
cache.add(path, {handler: routeHandler, params: req.params}) | ||
} | ||
routeHandler(req, res) | ||
} | ||
//Router methods | ||
Router.get = Parser.bind({method: "GET"}) | ||
Router.post = Parser.bind({method: "POST"}) | ||
Router.put = Parser.bind({method: "PUT"}) | ||
Router.head = Parser.bind({method: "HEAD"}) | ||
Router.delete = Parser.bind({method: "DELETE"}) | ||
Router.get = Parser.bind({method: "GET"}) | ||
Router.post = Parser.bind({method: "POST"}) | ||
Router.put = Parser.bind({method: "PUT"}) | ||
Router.head = Parser.bind({method: "HEAD"}) | ||
Router.delete = Parser.bind({method: "DELETE"}) | ||
Router.options = Parser.bind({method: "OPTIONS"}) | ||
Router.trace = Parser.bind({method: "TRACE"}) | ||
Router.connect = Parser.bind({method: "CONNECT"}) | ||
//Expose routing table | ||
//Return the routing table | ||
Router.routingTable = function RoutingTable() { | ||
@@ -112,3 +145,28 @@ return Routes | ||
//Expose the cache table, used in tests | ||
Router.baseCache = Cache | ||
//Overide custom 404 not found | ||
Router.notFound = function overideDefaultNotFound(fnc) { | ||
notFound = fnc | ||
} | ||
//Cache controller methods | ||
Router.cache = {} | ||
//Set cache max size, default 10000 | ||
Router.cache.maxSize = function setCacheMaxSize(size) { | ||
for(var prop in Cache) { | ||
Cache[prop].max_size = size | ||
} | ||
} | ||
//Clear the cache tables | ||
Router.cache.clear = function clearCacheTables() { | ||
for(var prop in Cache) { | ||
Cache[prop].clear() | ||
} | ||
} | ||
//Exports | ||
module.exports = Router |
@@ -1,19 +0,16 @@ | ||
//Routing table struct | ||
var Routing_Table = { | ||
matching: {}, //indexed by size of paths | ||
static: {}, //indexed by full path | ||
} | ||
//Methods | ||
var Methods = ['GET', 'POST', 'PUT', 'HEAD', 'DELETE', 'OPTIONS', 'TRACE', 'CONNECT'] | ||
//Methods table | ||
var Methods_Table = { | ||
GET: Routing_Table, | ||
POST: Routing_Table, | ||
HEAD: Routing_Table, | ||
PUT: Routing_Table, | ||
DELETE: Routing_Table, | ||
//Pseudo method, this is an route that will match all other routes | ||
ALL_OF_THE_ABOVE: Routing_Table, | ||
//Routing hashtable | ||
var Table = {} | ||
//Build the table | ||
for(var i in Methods) { | ||
Table[ Methods[i] ] = { | ||
staticRoutes: {}, | ||
dynamicRoutes: {}, | ||
} | ||
} | ||
//Exports final table | ||
module.exports = Methods_Table; | ||
//Export it | ||
module.exports = Table |
{ | ||
"name": "light-router", | ||
"version": "0.0.211", | ||
"description": "A light http request router for nodejs", | ||
"version": "0.1.0", | ||
"description": "A node.js perfomance efficient http router.", | ||
"main": "index.js", | ||
@@ -10,3 +10,3 @@ "directories": { | ||
"scripts": { | ||
"test": "node --prof ./node_modules/.bin/nodeunit test" | ||
"test": "node ./node_modules/.bin/nodeunit test" | ||
}, | ||
@@ -31,3 +31,6 @@ "repository": { | ||
"nodeunit": "*" | ||
}, | ||
"dependencies": { | ||
"base-cache": "0.0.12" | ||
} | ||
} |
@@ -5,13 +5,96 @@ light-router | ||
A light node.js http request router, it doesn't use regexp for matching, thus doesn't support complex patterns. It aims for performance and a more implicit route declaration model. This module follows a singletone design pattern, see below. | ||
A router for node.js performance junkies :) | ||
* Note that this router is in an alpha version, I don't guarantee forward compatibility with future versions. | ||
Why? | ||
--------- | ||
Most node.js http routers I tested had a really high overhead for a router, and in some cases it became the bottleneck of really simple API's I wrote. [See benchmarks below](#benchmarks) | ||
**Under development** | ||
##TODO | ||
- Add cache layer | ||
- Think about adding regexp | ||
- Add all http methods to routing table | ||
- Remove url.parse dependencie, this thing is slow as hell! | ||
Install | ||
--------- | ||
``` | ||
npm install light-router | ||
``` | ||
Sample usage | ||
--------- | ||
```javascript | ||
var router = require('light-router') | ||
router.get('/v1/account/:user', function(req, res) { | ||
res.end('Hello, ' + req.params.user) | ||
}) | ||
``` | ||
* Note that this module has a singleton design pattern. | ||
* The only thing that this router does to the `req` object is attach params. | ||
Features | ||
---------- | ||
* **Hashtable based** routing | ||
* **Cache layer** for faster delivery | ||
* **Cache control** for setting cache max size and disabling cache for some routes | ||
* **RegExp** for parameter testing | ||
* **404**, set custom not found handler | ||
Methods overview | ||
------------ | ||
I recommend reading everything if you want to get a general overview of the architecture of this router. | ||
####router[method]\(route, handler) | ||
The following http methods are avaialble: **get, post, put, head, delete, options, trace, connect** | ||
Writing routes follows the express/rails model. You can write **:param** to extract params from the request url and **:param(regexp)** to create rules for the parameter. **Be careful with the order you declare your routes!** | ||
```javascript | ||
//Will only match integer userIDs | ||
router.get('/v1/user/:id([0-9])', function(req, res) { | ||
res.end('Your numeric userID is: ' + req.params.id) | ||
}) | ||
//Will only match all other ids that werent numerical only | ||
router.get('/v1/user/:id)', function(req, res) { | ||
res.end('Your userID is: ' + req.params.id) | ||
}) | ||
``` | ||
####route.cache(boolean) | ||
Control the caching for this route, you should disable caching for highly dynamic routes. | ||
```javascript | ||
router.put('/v1/user/:id', handler).cache(false) | ||
``` | ||
####router.notFound(handler) | ||
Set a custom handler for the 404 not found. | ||
```javascript | ||
router.notFound(function(req, res) { | ||
res.statusCode = 404 | ||
res.end('Sorry this page was not found :(') | ||
}) | ||
``` | ||
####router.cache.maxSize(boolean) | ||
Set the max cache table size of each http method, by default its set to **10,000**. Each http method has its own cache table, so the total cached routes you can have is: **maxSize * http_verbs** | ||
```javascript | ||
router.cache.maxSize(10) | ||
``` | ||
####router.cache.clear() | ||
Clear the cache table, maybe it could be useful :) | ||
```javascript | ||
router.cache.clear() | ||
``` | ||
<a name="benchmarks"></a>Benchmarks | ||
--------- | ||
TODO: |
@@ -71,3 +71,3 @@ /** | ||
//Dynamic routes | ||
exports.testDynamicRoute = function(test) { | ||
exports.testDynamicRouteSimple = function(test) { | ||
router.get('/document/when/:date/set/:tomorrow', function(req, res) { | ||
@@ -84,3 +84,3 @@ test.expect(2) | ||
//Strange dynamic route | ||
exports.testDynamicRoute2 = function(test) { | ||
exports.testDynamicRouteStranger = function(test) { | ||
router.get('/document/:name/:date/set/:tomorrow', function(req, res) { | ||
@@ -97,14 +97,104 @@ test.expect(3) | ||
//Various dynamic route | ||
exports.testDynamicRoute3 = function(test) { | ||
router.get('/:document/:name/:date/set/:tomorrow', function(req, res) { | ||
test.expect(4) | ||
//Various dynamic route w/ regexp | ||
exports.testDynamicRouteRegExp = function(test) { | ||
router.get('/:document/:name/:date(^05102014$)/set', function(req, res) { | ||
test.expect(3) | ||
test.equal(req.params.document, 'shala', 'data param did not match') | ||
test.equal(req.params.name, 'something', 'data param did not match') | ||
test.equal(req.params.date, '05102014', 'data param did not match') | ||
test.equal(req.params.tomorrow, '06102014', 'data param did not match') | ||
test.done() | ||
}) | ||
TestReqTo('GET', '/shala/something/05102014/set/06102014') | ||
TestReqTo('GET', '/shala/something/05102014/set') | ||
} | ||
//Various dynamic route w/ possible complexitys | ||
exports.testDynamicRouteComplex = function(test) { | ||
test.expect(3) | ||
router.get('/api/base/:id', function(req, res) { | ||
test.equal(req.params.id, '12312', 'data param did not match') | ||
}) | ||
router.get('/api/:name(^john$)/:id', function(req, res) { | ||
test.equal(req.params.name, 'john', 'data param did not match') | ||
}) | ||
router.get('/:api/:name/:id', function(req, res) { | ||
test.equal(req.params.name, 'lucas', 'data param did not match') | ||
test.done() | ||
}) | ||
TestReqTo('GET', '/api/base/12312') | ||
TestReqTo('GET', '/api/john/12312') | ||
TestReqTo('GET', '/something/lucas/12312') | ||
} | ||
//Test 404 | ||
exports.testNotFound = function(test) { | ||
router.notFound(function(req, res) { | ||
test.done() | ||
}) | ||
TestReqTo('GET', '/this/route/should/not/exist') | ||
} | ||
//Cache on and off | ||
exports.testRouteNoCache = function(test) { | ||
test.expect(1) | ||
router.get('/test/cache', function(req, res) { | ||
}).cache(false) | ||
TestReqTo('GET', '/test/cache') | ||
//Check if a cache entry was created | ||
if(typeof router.baseCache.GET.table['test/cache'] === 'undefined') { | ||
test.ok(true) | ||
} | ||
//Error, the cache was created | ||
else { | ||
test.ok(false, 'A cache entry was actually created') | ||
} | ||
test.done() | ||
} | ||
//Change cache max size | ||
exports.testSetCacheMaxSize = function(test) { | ||
test.expect(1) | ||
router.cache.maxSize(100) | ||
test.equal(router.baseCache.GET.max_size, 100, 'Set cache max size didnt seem to work') | ||
test.done() | ||
} | ||
//Check if cache layer was working | ||
exports.testCacheLayer = function(test) { | ||
test.expect(1) | ||
test.ok(router.baseCache.GET.size() > 0, 'Cache layer wasnt really working') | ||
test.done() | ||
} | ||
//Clear cache table | ||
exports.testClearCacheTable = function(test) { | ||
test.expect(1) | ||
router.cache.clear() | ||
test.equal(router.baseCache.GET.size(), 0, 'Cache table did not clear') | ||
test.done() | ||
} | ||
//Display routing table final | ||
exports.testRoutingTable = function(test) { | ||
var table = router.routingTable() | ||
//console.log(JSON.stringify(table, 2, " ")) | ||
test.done() | ||
} |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
25685
634
100
1
13
2
+ Addedbase-cache@0.0.12
+ Addedbase-cache@0.0.12(transitive)