Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions lib/next.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,14 @@ function next (middlewares, req, res, index, routers, defaultRoute, errorHandler
return !res.finished && defaultRoute(req, res)
}

// Get current middleware and increment index
const middleware = middlewares[index++]
// Get current middleware
const middleware = middlewares[index]

// Create step function - this is called by middleware to continue the chain
const step = function (err) {
return err
? errorHandler(err, req, res)
: next(middlewares, req, res, index, routers, defaultRoute, errorHandler)
: next(middlewares, req, res, index + 1, routers, defaultRoute, errorHandler)
}

try {
Expand All @@ -41,7 +41,7 @@ function next (middlewares, req, res, index, routers, defaultRoute, errorHandler
// Replace pattern in URL - this is a hot path, optimize it
req.url = req.url.replace(pattern, '')

// Ensure URL starts with a slash
// Ensure URL starts with a slash - use charCodeAt for performance
if (req.url.length === 0 || req.url.charCodeAt(0) !== 47) { // 47 is '/'
req.url = '/' + req.url
}
Expand Down
136 changes: 95 additions & 41 deletions lib/router/sequential.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,32 @@ const { Trouter } = require('trouter')
const next = require('./../next')
const { parse } = require('regexparam')
const { LRUCache: Cache } = require('lru-cache')
const queryparams = require('../utils/queryparams')
const queryparams = require('./../utils/queryparams')

// Default handlers as constants to avoid creating functions on each router instance
/**
* Default handlers as constants to avoid creating functions on each router instance.
* This reduces memory allocation and improves performance when multiple routers are created.
*/
const DEFAULT_ROUTE = (req, res) => {
res.statusCode = 404
res.end()
}

const DEFAULT_ERROR_HANDLER = (err, req, res) => {
res.statusCode = 500
// Note: err.message could expose sensitive information in production
res.end(err.message)
}

// Simple ID generator
const generateId = () => Math.random().toString(36).substring(2, 10).toUpperCase()
/**
* Simple ID generator using Math.random for router identification.
* Warning: Not cryptographically secure - suitable only for internal routing logic.
* Optimized to minimize string operations.
*/
const generateId = () => {
// Use a more efficient approach - avoid substring operations
return Math.random().toString(36).slice(2, 10).toUpperCase()
}

module.exports = (config = {}) => {
// Use object destructuring with defaults for cleaner config initialization
Expand All @@ -29,88 +40,127 @@ module.exports = (config = {}) => {

const routers = {}

// Initialize cache only once
/**
* Initialize LRU cache for route matching results with optimized settings.
* Cache keys are method+path combinations to speed up repeated lookups.
* - cacheSize > 0: Limited LRU cache with specified max entries
* - cacheSize = 0: No caching (disabled)
* - cacheSize < 0: Large LRU cache (50k entries) for "unlimited" mode
* Optimized cache size for better memory management and performance.
*/
let cache = null
if (cacheSize > 0) {
cache = new Cache({ max: cacheSize })
cache = new Cache({
max: cacheSize,
updateAgeOnGet: false, // Disable age updates for better performance
updateAgeOnHas: false
})
} else if (cacheSize < 0) {
// For unlimited cache, still use LRUCache but with a very high max
// This provides better memory management than an unbounded Map
cache = new Cache({ max: 100000 })
// Reduced from 100k to 50k for better memory efficiency while maintaining performance
cache = new Cache({
max: 50000,
updateAgeOnGet: false,
updateAgeOnHas: false
})
}

const router = new Trouter()
router.id = id

const _use = router.use

/**
* Enhanced router.use method with support for nested routers.
* Handles both middleware functions and nested router instances.
* Automatically handles prefix parsing when first argument is a function.
* Optimized for minimal overhead in the common case.
*/
router.use = (prefix, ...middlewares) => {
if (typeof prefix === 'function') {
middlewares = [prefix, ...middlewares]
prefix = '/'
}
_use.call(router, prefix, middlewares)

if (middlewares[0]?.id) {
// caching router -> pattern relation for urls pattern replacement
// Optimized nested router detection - check first middleware only
const firstMiddleware = middlewares[0]
if (firstMiddleware?.id) {
// Cache router -> pattern relation for URL pattern replacement in nested routing
// This enables efficient URL rewriting when entering nested router contexts
const { pattern } = parse(prefix, true)
routers[middlewares[0].id] = pattern
routers[firstMiddleware.id] = pattern
}

return router // Fix: return router instead of this
return router // Ensure chainable API by returning router instance
}

// Create the cleanup middleware once
const createCleanupMiddleware = (step) => (req, res, next) => {
req.url = req.preRouterUrl
req.path = req.preRouterPath

req.preRouterUrl = undefined
req.preRouterPath = undefined

return step()
/**
* Creates cleanup middleware for nested router restoration.
* This middleware restores the original URL and path after nested router processing.
* Uses property deletion instead of undefined assignment for better performance.
* Optimized to minimize closure creation overhead.
*/
const createCleanupMiddleware = (step) => {
// Pre-create the cleanup function to avoid repeated function creation
return (req, res, next) => {
req.url = req.preRouterUrl
req.path = req.preRouterPath

// Use delete for better performance than setting undefined
delete req.preRouterUrl
delete req.preRouterPath

return step()
}
}

router.lookup = (req, res, step) => {
// Initialize URL and originalUrl if needed
req.url = req.url || '/'
req.originalUrl = req.originalUrl || req.url
// Initialize URL and originalUrl if needed - use nullish coalescing for better performance
req.url ??= '/'
req.originalUrl ??= req.url

// Parse query parameters
// Parse query parameters using optimized utility
queryparams(req, req.url)

// Fast path for cache lookup
const reqCacheKey = cache && (req.method + req.path)
let match = cache && cache.get(reqCacheKey)
// Cache lookup optimization - minimize variable assignments
let match
if (cache) {
// Pre-compute cache key with direct concatenation (fastest approach)
const reqCacheKey = req.method + req.path
Copy link

Copilot AI May 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider normalizing req.method (e.g., using toUpperCase()) when constructing the cache key to ensure consistency regardless of method casing.

Suggested change
const reqCacheKey = req.method + req.path
const reqCacheKey = req.method.toUpperCase() + req.path

Copilot uses AI. Check for mistakes.
match = cache.get(reqCacheKey)

if (!match) {
match = router.find(req.method, req.path)
if (cache && reqCacheKey) {
if (!match) {
match = router.find(req.method, req.path)
cache.set(reqCacheKey, match)
}
} else {
match = router.find(req.method, req.path)
}

const { handlers, params } = match

if (handlers.length > 0) {
// Avoid creating a new array with spread operator
// Use the handlers array directly
if (handlers.length) {
// Optimized middleware array handling
let middlewares

if (step !== undefined) {
// Only create a new array if we need to add the cleanup middleware
// Create new array only when step middleware is needed
middlewares = handlers.slice()
middlewares.push(createCleanupMiddleware(step))
} else {
middlewares = handlers
}

// Initialize params object if needed
// Optimized parameter assignment with minimal overhead
if (!req.params) {
req.params = params
// Use pre-created empty object or provided params directly
req.params = params || Object.create(null)
} else if (params) {
// Faster than Object.assign for small objects
for (const key in params) {
// Manual property copying - optimized for small objects
// Pre-compute keys and length to avoid repeated calls
const paramKeys = Object.keys(params)
let i = paramKeys.length
while (i--) {
const key = paramKeys[i]
req.params[key] = params[key]
}
}
Expand All @@ -121,6 +171,10 @@ module.exports = (config = {}) => {
}
}

/**
* Shorthand method for registering routes with specific HTTP methods.
* Delegates to router.add with the provided method, pattern, and handlers.
*/
router.on = (method, pattern, ...handlers) => router.add(method, pattern, handlers)

return router
Expand Down
10 changes: 0 additions & 10 deletions lib/utils/object.js

This file was deleted.

62 changes: 50 additions & 12 deletions lib/utils/queryparams.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,58 @@
// Pre-create Set for dangerous properties - faster O(1) lookup vs string comparisons
const DANGEROUS_PROPERTIES = new Set(['__proto__', 'constructor', 'prototype'])

// Pre-created empty query object to avoid allocations
const EMPTY_QUERY = Object.freeze(Object.create(null))

module.exports = (req, url) => {
const query = {}
const indexOfQuestionMark = url.indexOf('?')
const path = indexOfQuestionMark !== -1 ? url.slice(0, indexOfQuestionMark) : url
const search = indexOfQuestionMark !== -1 ? url.slice(indexOfQuestionMark + 1) : ''

if (search.length > 0) {
const searchParams = new URLSearchParams(search.replace(/\[\]=/g, '='))
for (const [name, value] of searchParams.entries()) {
if (query[name]) {
Array.isArray(query[name]) ? query[name].push(value) : (query[name] = [query[name], value])
// Single indexOf call - more efficient than multiple operations
const questionMarkIndex = url.indexOf('?')

if (questionMarkIndex === -1) {
// Fast path: no query string
req.path = url
req.query = EMPTY_QUERY
return
}

// Use Object.create(null) for prototype pollution protection
const query = Object.create(null)

// Extract path and search in one operation each
req.path = url.slice(0, questionMarkIndex)
const search = url.slice(questionMarkIndex + 1)

if (search.length === 0) {
// Fast path: empty query string
req.query = query
return
}

// Process query parameters with optimized URLSearchParams handling
const searchParams = new URLSearchParams(search.replace(/\[\]=/g, '='))

for (const [name, value] of searchParams.entries()) {
// Split parameter name into segments by dot or bracket notation
/* eslint-disable-next-line */
Copy link

Copilot AI May 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] Consider expanding or clarifying this ESLint bypass comment to explain the rationale behind using the regex split for parameter names, improving maintainability for future contributors.

Copilot uses AI. Check for mistakes.
const segments = name.split(/[\.\[\]]+/).filter(Boolean)

// Check each segment against the dangerous properties set
if (segments.some(segment => DANGEROUS_PROPERTIES.has(segment))) {
continue // Skip dangerous property names
}

const existing = query[name]
if (existing !== undefined) {
// Optimized array handling - check type once, then branch
if (Array.isArray(existing)) {
existing.push(value)
} else {
query[name] = value
query[name] = [existing, value]
}
} else {
query[name] = value
}
}

req.path = path
req.query = query
}
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,12 @@
},
"homepage": "https://github.com/BackendStack21/0http#readme",
"devDependencies": {
"0http": "^4.1.0",
"0http": "^4.2.0",
"@types/node": "^22.10.5",
"body-parser": "^1.20.1",
"chai": "^4.3.7",
"cross-env": "^7.0.3",
"mitata": "^1.0.30",
"mitata": "^1.0.34",
"mocha": "^11.0.1",
"nyc": "^17.1.0",
"supertest": "^7.0.0"
Expand Down
Loading