From 79a5f09c0190e24e5a947eea3f5f753c55fe92e0 Mon Sep 17 00:00:00 2001 From: Rajkumar Dusad Date: Wed, 22 Mar 2023 12:40:59 +0530 Subject: [PATCH] Added route cache --- package.json | 7 +++-- src/methods.cjs | 6 ++--- src/methods.mjs | 6 ++--- src/route.cjs | 40 +++++++++++----------------- src/route.mjs | 40 +++++++++++----------------- src/router.cjs | 69 ++++++++++++++++++++++++++++--------------------- src/router.mjs | 69 ++++++++++++++++++++++++++++--------------------- 7 files changed, 117 insertions(+), 120 deletions(-) diff --git a/package.json b/package.json index 3c9aee1..4b9b9de 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@routejs/router", - "version": "2.1.5", + "version": "2.1.6", "description": "Fast and lightweight http routing engine for nodejs", "main": "index.mjs", "type": "module", @@ -50,5 +50,8 @@ "index.cjs", "index.mjs", "index.d.ts" - ] + ], + "dependencies": { + "@opensnip/lrujs": "^1.0.0" + } } diff --git a/src/methods.cjs b/src/methods.cjs index 345049c..6d42fec 100644 --- a/src/methods.cjs +++ b/src/methods.cjs @@ -6,7 +6,7 @@ module.exports.use = function use(...callbacks) { if (typeof callbacks[0] === "string" || callbacks[0] instanceof String) { if (callbacks.length < 2) { throw new TypeError( - "Error: use function callback accepts function or router as an argument" + "use function callback accepts function or router as an argument" ); } return mergeRoute({ @@ -28,9 +28,7 @@ module.exports.all = function all(path, ...callbacks) { module.exports.domain = function domain(host, routes) { if (!(typeof host === "string" || host instanceof String)) { - throw new TypeError( - "Error: domain host accepts only string as an argument" - ); + throw new TypeError("domain host accepts only string as an argument"); } if (typeof routes === "function") { diff --git a/src/methods.mjs b/src/methods.mjs index 1bc999f..19e18f1 100644 --- a/src/methods.mjs +++ b/src/methods.mjs @@ -6,7 +6,7 @@ export function use(...callbacks) { if (typeof callbacks[0] === "string" || callbacks[0] instanceof String) { if (callbacks.length < 2) { throw new TypeError( - "Error: use function callback accepts function or router as an argument" + "use function callback accepts function or router as an argument" ); } return mergeRoute({ @@ -28,9 +28,7 @@ export function all(path, ...callbacks) { export function domain(host, routes) { if (!(typeof host === "string" || host instanceof String)) { - throw new TypeError( - "Error: domain host accepts only string as an argument" - ); + throw new TypeError("domain host accepts only string as an argument"); } if (typeof routes === "function") { diff --git a/src/route.cjs b/src/route.cjs index 8bd0c5c..08321b5 100644 --- a/src/route.cjs +++ b/src/route.cjs @@ -13,9 +13,7 @@ module.exports = class Route { constructor({ host, method, path, name, group, callbacks, caseSensitive }) { if (host && !(typeof host === "string" || host instanceof String)) { - throw new TypeError( - "Error: route host accepts only string as an argument" - ); + throw new TypeError("route host accepts only string as an argument"); } if (method) { @@ -23,7 +21,7 @@ module.exports = class Route { method = method.map((e) => { if (!(typeof e === "string" || e instanceof String)) { throw new TypeError( - "Error: route method accepts only string or array of string as an argument" + "route method accepts only string or array of string as an argument" ); } return e.toUpperCase(); @@ -32,20 +30,18 @@ module.exports = class Route { method = method.toUpperCase(); } else { throw new TypeError( - "Error: route method accepts only string or array of string as an argument" + "route method accepts only string or array of string as an argument" ); } } if (path && !(typeof path === "string" || path instanceof String)) { - throw new TypeError( - "Error: route path accepts only string as an argument" - ); + throw new TypeError("route path accepts only string as an argument"); } if (Array.isArray(callbacks) === false && typeof callbacks !== "function") { throw new TypeError( - "Error: route callback accepts only function as an argument" + "route callback accepts only function as an argument" ); } @@ -70,7 +66,7 @@ module.exports = class Route { ? callbacks.map((callback) => { if (typeof callback !== "function") { throw new TypeError( - "Error: " + + "" + (path ? "route" : "middleware") + " callback accepts only function as an argument" ); @@ -87,29 +83,23 @@ module.exports = class Route { match({ host, method, path }) { if (host && !(typeof host === "string" || host instanceof String)) { - throw new TypeError( - "Error: request host accepts only string as an argument" - ); + throw new TypeError("request host accepts only string as an argument"); } if (!method) { - throw new TypeError("Error: request method is required"); + throw new TypeError("request method is required"); } if (!(typeof method === "string" || method instanceof String)) { - throw new TypeError( - "Error: request method accepts only string as an argument" - ); + throw new TypeError("request method accepts only string as an argument"); } if (!path) { - throw new TypeError("Error: request path is required"); + throw new TypeError("request path is required"); } if (!(typeof path === "string" || path instanceof String)) { - throw new TypeError( - "Error: request path accepts only string as an argument" - ); + throw new TypeError("request path accepts only string as an argument"); } if (this.pathRegexp === null) { @@ -174,7 +164,7 @@ module.exports = class Route { } return { regexp, params }; } catch (err) { - throw new TypeError("Error: " + host + " invalid regular expression"); + throw new TypeError("" + host + " invalid regular expression"); } } @@ -196,7 +186,7 @@ module.exports = class Route { } return { regexp, params }; } catch (err) { - throw new TypeError("Error: " + path + " invalid regular expression"); + throw new TypeError("" + path + " invalid regular expression"); } } @@ -218,7 +208,7 @@ module.exports = class Route { } return { regexp, params }; } catch (err) { - throw new TypeError("Error: " + path + " invalid regular expression"); + throw new TypeError("" + path + " invalid regular expression"); } } @@ -256,7 +246,7 @@ module.exports = class Route { return { regexp: regexp.join("\\" + delimiter), params: params }; } catch (err) { console.log(err); - throw new TypeError("Error: " + path + " invalid regular expression"); + throw new TypeError("" + path + " invalid regular expression"); } } }; diff --git a/src/route.mjs b/src/route.mjs index dff36b8..53ac891 100644 --- a/src/route.mjs +++ b/src/route.mjs @@ -13,9 +13,7 @@ export default class Route { constructor({ host, method, path, name, group, callbacks, caseSensitive }) { if (host && !(typeof host === "string" || host instanceof String)) { - throw new TypeError( - "Error: route host accepts only string as an argument" - ); + throw new TypeError("route host accepts only string as an argument"); } if (method) { @@ -23,7 +21,7 @@ export default class Route { method = method.map((e) => { if (!(typeof e === "string" || e instanceof String)) { throw new TypeError( - "Error: route method accepts only string or array of string as an argument" + "route method accepts only string or array of string as an argument" ); } return e.toUpperCase(); @@ -32,20 +30,18 @@ export default class Route { method = method.toUpperCase(); } else { throw new TypeError( - "Error: route method accepts only string or array of string as an argument" + "route method accepts only string or array of string as an argument" ); } } if (path && !(typeof path === "string" || path instanceof String)) { - throw new TypeError( - "Error: route path accepts only string as an argument" - ); + throw new TypeError("route path accepts only string as an argument"); } if (Array.isArray(callbacks) === false && typeof callbacks !== "function") { throw new TypeError( - "Error: route callback accepts only function as an argument" + "route callback accepts only function as an argument" ); } @@ -70,7 +66,7 @@ export default class Route { ? callbacks.map((callback) => { if (typeof callback !== "function") { throw new TypeError( - "Error: " + + "" + (path ? "route" : "middleware") + " callback accepts only function as an argument" ); @@ -87,29 +83,23 @@ export default class Route { match({ host, method, path }) { if (host && !(typeof host === "string" || host instanceof String)) { - throw new TypeError( - "Error: request host accepts only string as an argument" - ); + throw new TypeError("request host accepts only string as an argument"); } if (!method) { - throw new TypeError("Error: request method is required"); + throw new TypeError("request method is required"); } if (!(typeof method === "string" || method instanceof String)) { - throw new TypeError( - "Error: request method accepts only string as an argument" - ); + throw new TypeError("request method accepts only string as an argument"); } if (!path) { - throw new TypeError("Error: request path is required"); + throw new TypeError("request path is required"); } if (!(typeof path === "string" || path instanceof String)) { - throw new TypeError( - "Error: request path accepts only string as an argument" - ); + throw new TypeError("request path accepts only string as an argument"); } if (this.pathRegexp === null) { @@ -174,7 +164,7 @@ export default class Route { } return { regexp, params }; } catch (err) { - throw new TypeError("Error: " + host + " invalid regular expression"); + throw new TypeError("" + host + " invalid regular expression"); } } @@ -196,7 +186,7 @@ export default class Route { } return { regexp, params }; } catch (err) { - throw new TypeError("Error: " + path + " invalid regular expression"); + throw new TypeError("" + path + " invalid regular expression"); } } @@ -218,7 +208,7 @@ export default class Route { } return { regexp, params }; } catch (err) { - throw new TypeError("Error: " + path + " invalid regular expression"); + throw new TypeError("" + path + " invalid regular expression"); } } @@ -256,7 +246,7 @@ export default class Route { return { regexp: regexp.join("\\" + delimiter), params: params }; } catch (err) { console.log(err); - throw new TypeError("Error: " + path + " invalid regular expression"); + throw new TypeError("" + path + " invalid regular expression"); } } } diff --git a/src/router.cjs b/src/router.cjs index 1d15896..53c03b6 100644 --- a/src/router.cjs +++ b/src/router.cjs @@ -1,5 +1,6 @@ const nodePath = require("node:path"); const url = require("node:url"); +const LRUCache = require("@opensnip/lrujs"); const Route = require("./route.cjs"); module.exports = class Router { @@ -9,14 +10,16 @@ module.exports = class Router { host: undefined, }; #route = null; - #cache = null; + #pathCache = null; + #routeCache = null; constructor(options = {}) { if (options.caseSensitive === true) { this.#config.caseSensitive = true; } this.#config.host = options.host; - this.#cache = new Map(); + this.#pathCache = new LRUCache(); + this.#routeCache = new LRUCache(); } checkout(path, ...callbacks) { @@ -131,7 +134,7 @@ module.exports = class Router { if (typeof callbacks[0] === "string" || callbacks[0] instanceof String) { if (callbacks.length < 2) { throw new TypeError( - "Error: use function callback accepts function or router as an argument" + "use function callback accepts function or router as an argument" ); } return this.#mergeRoute({ @@ -145,9 +148,7 @@ module.exports = class Router { group(path, callback) { if (!(typeof path === "string" || path instanceof String)) { - throw new TypeError( - "Error: group path accepts only string as an argument" - ); + throw new TypeError("group path accepts only string as an argument"); } if (typeof callback === "function") { @@ -161,9 +162,7 @@ module.exports = class Router { domain(host, callback) { if (!(typeof host === "string" || host instanceof String)) { - throw new TypeError( - "Error: domain host accepts only string as an argument" - ); + throw new TypeError("domain host accepts only string as an argument"); } if (typeof callback === "function") { @@ -180,7 +179,7 @@ module.exports = class Router { if (this.#route instanceof Route) { this.#route.setName(name); } else { - throw new TypeError("Error: setName can not set name for middleware"); + throw new TypeError("setName can not set name for middleware"); } return this; } @@ -203,7 +202,7 @@ module.exports = class Router { namedRoute.params.length !== params.length ) { throw new TypeError( - "Error: invalid route parameters, please provide all route parameters" + "invalid route parameters, please provide all route parameters" ); } for (const param of params) { @@ -279,16 +278,14 @@ module.exports = class Router { } handle({ requestHost, requestMethod, requestUrl, request, response }) { + let that = this; let requestPath; - if (this.#cache.has(requestUrl)) { - requestPath = this.#cache.get(requestUrl); - if (this.#cache.size > 100) { - this.#cache.clear(); - } + if (that.#pathCache.has(requestUrl)) { + requestPath = that.#pathCache.get(requestUrl); } else { const parsedUrl = url.parse(requestUrl ? requestUrl : ""); - this.#cache.set(requestUrl, decodeURI(parsedUrl.pathname)); - requestPath = this.#cache.get(requestUrl); + that.#pathCache.set(requestUrl, decodeURI(parsedUrl.pathname)); + requestPath = that.#pathCache.get(requestUrl); } const callStack = { @@ -302,12 +299,12 @@ module.exports = class Router { // No more middlewares to execute // Execute next callstack callStack.index++; - return runCallStack(error); + return runCallback(error); } if (typeof callbacks.stack[callbacks.index] !== "function") { throw new TypeError( - "Error: callback argument only accepts function as an argument" + "callback argument only accepts function as an argument" ); } @@ -320,7 +317,7 @@ module.exports = class Router { if (err === "skip") { // Skip all middlewares of current callstack and execute next callstack callStack.index++; - runCallStack(); + runCallback(); } else { // Execute next middleware callbacks.index++; @@ -341,7 +338,7 @@ module.exports = class Router { if (err === "skip") { // Skip all middlewares of current callstack and execute next callstack callStack.index++; - runCallStack(); + runCallback(); } else { // Execute next middleware callbacks.index++; @@ -364,7 +361,7 @@ module.exports = class Router { } } - function runCallStack(error = null) { + function runCallback(error = null) { if (typeof callStack.stack[callStack.index] === "undefined") { if (error !== null) { if (error instanceof Error) { @@ -376,12 +373,24 @@ module.exports = class Router { // Nothing to execute return; } + + let match; + let cacheKey = `${callStack.index};${requestHost};${requestMethod};${requestUrl}`; + if (that.#routeCache.has(cacheKey)) { + match = that.#routeCache.get(cacheKey); + } else { + that.#routeCache.set( + cacheKey, + callStack.stack[callStack.index].match({ + host: requestHost, + method: requestMethod, + path: requestPath, + }) + ); + match = that.#routeCache.get(cacheKey); + } + // Execute callbacks - const match = callStack.stack[callStack.index].match({ - host: requestHost, - method: requestMethod, - path: requestPath, - }); if (match !== false) { request.params = match.params; request.subdomains = match.subdomains; @@ -392,11 +401,11 @@ module.exports = class Router { return runMiddleware(callbacks, error); } else { callStack.index++; - return runCallStack(error); + return runCallback(error); } } - runCallStack(); + runCallback(); } handler() { diff --git a/src/router.mjs b/src/router.mjs index 1d2bb63..789c8b2 100644 --- a/src/router.mjs +++ b/src/router.mjs @@ -1,5 +1,6 @@ import nodePath from "node:path"; import url from "node:url"; +import LRUCache from "@opensnip/lrujs"; import Route from "./route.mjs"; export default class Router { @@ -9,14 +10,16 @@ export default class Router { host: undefined, }; #route = null; - #cache = null; + #pathCache = null; + #routeCache = null; constructor(options = {}) { if (options.caseSensitive === true) { this.#config.caseSensitive = true; } this.#config.host = options.host; - this.#cache = new Map(); + this.#pathCache = new LRUCache(); + this.#routeCache = new LRUCache(); } checkout(path, ...callbacks) { @@ -131,7 +134,7 @@ export default class Router { if (typeof callbacks[0] === "string" || callbacks[0] instanceof String) { if (callbacks.length < 2) { throw new TypeError( - "Error: use function callback accepts function or router as an argument" + "use function callback accepts function or router as an argument" ); } return this.#mergeRoute({ @@ -145,9 +148,7 @@ export default class Router { group(path, callback) { if (!(typeof path === "string" || path instanceof String)) { - throw new TypeError( - "Error: group path accepts only string as an argument" - ); + throw new TypeError("group path accepts only string as an argument"); } if (typeof callback === "function") { @@ -161,9 +162,7 @@ export default class Router { domain(host, callback) { if (!(typeof host === "string" || host instanceof String)) { - throw new TypeError( - "Error: domain host accepts only string as an argument" - ); + throw new TypeError("domain host accepts only string as an argument"); } if (typeof callback === "function") { @@ -180,7 +179,7 @@ export default class Router { if (this.#route instanceof Route) { this.#route.setName(name); } else { - throw new TypeError("Error: setName can not set name for middleware"); + throw new TypeError("setName can not set name for middleware"); } return this; } @@ -203,7 +202,7 @@ export default class Router { namedRoute.params.length !== params.length ) { throw new TypeError( - "Error: invalid route parameters, please provide all route parameters" + "invalid route parameters, please provide all route parameters" ); } for (const param of params) { @@ -279,16 +278,14 @@ export default class Router { } handle({ requestHost, requestMethod, requestUrl, request, response }) { + let that = this; let requestPath; - if (this.#cache.has(requestUrl)) { - requestPath = this.#cache.get(requestUrl); - if (this.#cache.size > 100) { - this.#cache.clear(); - } + if (that.#pathCache.has(requestUrl)) { + requestPath = that.#pathCache.get(requestUrl); } else { const parsedUrl = url.parse(requestUrl ? requestUrl : ""); - this.#cache.set(requestUrl, decodeURI(parsedUrl.pathname)); - requestPath = this.#cache.get(requestUrl); + that.#pathCache.set(requestUrl, decodeURI(parsedUrl.pathname)); + requestPath = that.#pathCache.get(requestUrl); } const callStack = { @@ -302,12 +299,12 @@ export default class Router { // No more middlewares to execute // Execute next callstack callStack.index++; - return runCallStack(error); + return runCallback(error); } if (typeof callbacks.stack[callbacks.index] !== "function") { throw new TypeError( - "Error: callback argument only accepts function as an argument" + "callback argument only accepts function as an argument" ); } @@ -320,7 +317,7 @@ export default class Router { if (err === "skip") { // Skip all middlewares of current callstack and execute next callstack callStack.index++; - runCallStack(); + runCallback(); } else { // Execute next middleware callbacks.index++; @@ -341,7 +338,7 @@ export default class Router { if (err === "skip") { // Skip all middlewares of current callstack and execute next callstack callStack.index++; - runCallStack(); + runCallback(); } else { // Execute next middleware callbacks.index++; @@ -364,7 +361,7 @@ export default class Router { } } - function runCallStack(error = null) { + function runCallback(error = null) { if (typeof callStack.stack[callStack.index] === "undefined") { if (error !== null) { if (error instanceof Error) { @@ -376,12 +373,24 @@ export default class Router { // Nothing to execute return; } + + let match; + let cacheKey = `${callStack.index};${requestHost};${requestMethod};${requestUrl}`; + if (that.#routeCache.has(cacheKey)) { + match = that.#routeCache.get(cacheKey); + } else { + that.#routeCache.set( + cacheKey, + callStack.stack[callStack.index].match({ + host: requestHost, + method: requestMethod, + path: requestPath, + }) + ); + match = that.#routeCache.get(cacheKey); + } + // Execute callbacks - const match = callStack.stack[callStack.index].match({ - host: requestHost, - method: requestMethod, - path: requestPath, - }); if (match !== false) { request.params = match.params; request.subdomains = match.subdomains; @@ -392,11 +401,11 @@ export default class Router { return runMiddleware(callbacks, error); } else { callStack.index++; - return runCallStack(error); + return runCallback(error); } } - runCallStack(); + runCallback(); } handler() {