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
39 changes: 39 additions & 0 deletions example/server-benchmark.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
'use strict'

const path = require('node:path')
const fastify = require('fastify')({ logger: false })
const fastifyStatic = require(process.env.PLUGIN_PATH || '../')

const root = path.join(__dirname, '/public')
const port = Number(process.env.PORT || 3000)

fastify.register(fastifyStatic, {
root,
prefix: '/static',
decorateReply: false
})

fastify.register(fastifyStatic, {
root,
prefix: '/app/:version',
decorateReply: false
})

fastify.register(async function (child) {
child.register(fastifyStatic, {
root,
prefix: '/public',
decorateReply: false
})
}, { prefix: '/nested' })

fastify.listen({ port }, err => {
if (err) throw err

console.log(`benchmark server listening on http://127.0.0.1:${port}`)
console.log('')
console.log('Try:')
console.log(` npx autocannon -c 100 -d 10 http://127.0.0.1:${port}/static/index.css`)
console.log(` npx autocannon -c 100 -d 10 http://127.0.0.1:${port}/app/1.2.3/index.css`)
console.log(` npx autocannon -c 100 -d 10 http://127.0.0.1:${port}/nested/public/index.css`)
})
114 changes: 104 additions & 10 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -117,12 +117,16 @@ async function fastifyStatic (fastify, opts) {
throw new TypeError('"wildcard" option must be a boolean')
}
if (opts.wildcard === undefined || opts.wildcard === true) {
let matchRoutePrefix

fastify.route({
...routeOpts,
method: ['HEAD', 'GET'],
path: prefix + '*',
handler (req, reply) {
const pathname = getPathnameForSend(req.raw.url, req.routeOptions.url)
matchRoutePrefix ??= createRoutePrefixMatcher(req.routeOptions.url)

const pathname = getPathnameForSend(req.raw.url, matchRoutePrefix)
if (!pathname) {
return reply.callNotFound()
}
Expand Down Expand Up @@ -569,23 +573,113 @@ function getEncodingHeader (headers, checked) {
}

/**
* @param {string} url
* @param {string} routePrefix
* @returns {Array<string|undefined>}
*/
function createRoutePrefixTokens (routePrefix) {
const tokens = []
let routeIndex = 0
let segmentStart = 0

while (routeIndex < routePrefix.length) {
if (routePrefix[routeIndex] !== ':') {
routeIndex++
continue
}

if (segmentStart !== routeIndex) {
tokens.push(routePrefix.slice(segmentStart, routeIndex))
}

routeIndex++
while (routeIndex < routePrefix.length && routePrefix[routeIndex] !== '/') {
routeIndex++
}

tokens.push(undefined)
segmentStart = routeIndex
}

if (segmentStart !== routePrefix.length) {
tokens.push(routePrefix.slice(segmentStart))
}

return tokens
}

/**
* @param {string} pathname
* @param {number} pathnameEnd
* @param {Array<string|undefined>} tokens
* @returns {number|undefined}
*/
function getRoutePrefixMatchLength (pathname, pathnameEnd, tokens) {
let pathnameIndex = 0

for (const token of tokens) {
if (token === undefined) {
const segmentStart = pathnameIndex
const slashIndex = pathname.indexOf('/', pathnameIndex)

pathnameIndex = slashIndex === -1 || slashIndex > pathnameEnd
? pathnameEnd
: slashIndex

if (pathnameIndex === segmentStart) {
return
}

continue
}

const tokenEnd = pathnameIndex + token.length
if (tokenEnd > pathnameEnd || !pathname.startsWith(token, pathnameIndex)) {
return
}

pathnameIndex = tokenEnd
}

return pathnameIndex
}

/**
* @param {string} route
* @returns {(pathname: string, pathnameEnd: number) => number|undefined}
*/
function createRoutePrefixMatcher (route) {
const routePrefix = route.replace(/\*$/u, '')
const routePrefixLength = routePrefix.length

if (routePrefix === '/') {
return () => 0
}

if (routePrefix.includes(':') === false) {
return (pathname, pathnameEnd) => routePrefixLength <= pathnameEnd && pathname.startsWith(routePrefix)
? routePrefixLength
: undefined
}

const tokens = createRoutePrefixTokens(routePrefix)
return (pathname, pathnameEnd) => getRoutePrefixMatchLength(pathname, pathnameEnd, tokens)
}

/**
* @param {string} url
* @param {(pathname: string, pathnameEnd: number) => number|undefined} matchRoutePrefix
* @returns {string|undefined}
*/
function getPathnameForSend (url, route) {
function getPathnameForSend (url, matchRoutePrefix) {
const questionMark = url.indexOf('?')
let pathname = questionMark === -1 ? url : url.slice(0, questionMark)

const routePrefix = route.endsWith('*')
? route.slice(0, -1)
: route
const pathnameEnd = questionMark === -1 ? url.length : questionMark

if (routePrefix !== '/' && !pathname.startsWith(routePrefix)) {
const prefixLength = matchRoutePrefix(url, pathnameEnd)
if (prefixLength === undefined) {
return
}

pathname = pathname.slice(routePrefix.length)
let pathname = url.slice(prefixLength, pathnameEnd)

if (pathname === '') {
pathname = '/'
Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
"test": "npm run test:unit && npm run test:typescript",
"test:typescript": "tsd",
"test:unit": "borp -C --check-coverage --lines 100",
"example": "node example/server.js"
"example": "node example/server.js",
"example:benchmark": "node example/server-benchmark.js"
},
"repository": {
"type": "git",
Expand Down Expand Up @@ -68,6 +69,7 @@
"devDependencies": {
"@fastify/compress": "^8.0.0",
"@types/node": "^25.0.3",
"autocannon": "^8.0.0",
"borp": "^1.0.0",
"c8": "^11.0.0",
"concat-stream": "^2.0.0",
Expand Down
Loading
Loading