Skip to content

Commit

Permalink
perf: Reduce object generation and optimize performance.
Browse files Browse the repository at this point in the history
  • Loading branch information
usualoma committed Nov 13, 2023
1 parent 7f4ba25 commit 443c3ab
Show file tree
Hide file tree
Showing 2 changed files with 132 additions and 26 deletions.
148 changes: 122 additions & 26 deletions src/listener.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,46 +8,142 @@ import { writeFromReadableStream } from './utils'
const regBuffer = /^no$/i
const regContentType = /^(application\/json\b|text\/(?!event-stream\b))/i

export const getRequestListener = (fetchCallback: FetchCallback) => {
return async (
incoming: IncomingMessage | Http2ServerRequest,
outgoing: ServerResponse | Http2ServerResponse
) => {
const method = incoming.method || 'GET'
const url = `http://${incoming.headers.host}${incoming.url}`

const headerRecord: [string, string][] = []
const len = incoming.rawHeaders.length
for (let i = 0; i < len; i += 2) {
headerRecord.push([incoming.rawHeaders[i], incoming.rawHeaders[i + 1]])
class CustomResponse extends global.Response {
public __cache: [string, Record<string, string>] | undefined
constructor(body: BodyInit | null, init?: ResponseInit) {
super(body, init)
if (typeof body === 'string' && !(init?.headers instanceof Headers)) {
this.__cache = [body, (init?.headers || {}) as Record<string, string>]
}
}
get headers() {
// discard cache if headers are retrieved as they may change
this.__cache = undefined
return super.headers
}
}
Object.defineProperty(global, 'Response', {
value: CustomResponse,
})

const init = {
method: method,
headers: headerRecord,
} as RequestInit
function newRequestFromIncoming(
method: string,
url: string,
incoming: IncomingMessage | Http2ServerRequest
): Request {
const headerRecord: [string, string][] = []
const len = incoming.rawHeaders.length
for (let i = 0; i < len; i += 2) {
headerRecord.push([incoming.rawHeaders[i], incoming.rawHeaders[i + 1]])
}

if (!(method === 'GET' || method === 'HEAD')) {
// lazy-consume request body
init.body = Readable.toWeb(incoming) as ReadableStream<Uint8Array>
// node 18 fetch needs half duplex mode when request body is stream
;(init as any).duplex = 'half'
}
const init = {
method: method,
headers: headerRecord,
} as RequestInit

let res: Response
if (!(method === 'GET' || method === 'HEAD')) {
// lazy-consume request body
init.body = Readable.toWeb(incoming) as ReadableStream<Uint8Array>
// node 18 fetch needs half duplex mode when request body is stream
;(init as any).duplex = 'half'
}

return new Request(url, init)
}

const requestPrototype: Record<string, any> = {
request() {
return (this.requestCache ||= newRequestFromIncoming(this.method, this.url, this.incoming))
},
get body() {
return this.request().body
},
get bodyUsed() {
return this.request().bodyUsed
},
get cache() {
return this.request().cache
},
get credentials() {
return this.request().credentials
},
get destination() {
return this.request().destination
},
get headers() {
return this.request().headers
},
get integrity() {
return this.request().integrity
},
get mode() {
return this.request().mode
},
get redirect() {
return this.request().redirect
},
get referrer() {
return this.request().referrer
},
get referrerPolicy() {
return this.request().referrerPolicy
},
get signal() {
return this.request().signal
},
arrayBuffer() {
return this.request().arrayBuffer()
},
blob() {
return this.request().blob()
},
clone() {
return this.request().clone()
},
formData() {
return this.request().formData()
},
json() {
return this.request().json()
},
text() {
return this.request().text()
},
}

export const getRequestListener = (fetchCallback: FetchCallback) => {
return async (
incoming: IncomingMessage | Http2ServerRequest,
outgoing: ServerResponse | Http2ServerResponse
) => {
let res: CustomResponse
const req = {
method: incoming.method || 'GET',
url: `http://${incoming.headers.host}${incoming.url}`,
incoming,
} as unknown as Request
Object.setPrototypeOf(req, requestPrototype)
try {
res = (await fetchCallback(new Request(url, init))) as Response
res = (await fetchCallback(req)) as CustomResponse
} catch (e: unknown) {
res = new Response(null, { status: 500 })
res = new CustomResponse(null, { status: 500 })
if (e instanceof Error) {
// timeout error emits 504 timeout
if (e.name === 'TimeoutError' || e.constructor.name === 'TimeoutError') {
res = new Response(null, { status: 504 })
res = new CustomResponse(null, { status: 504 })
}
}
}

if (res.__cache) {
const [body, header] = res.__cache
header['content-length'] ||= '' + Buffer.byteLength(body)
outgoing.writeHead(res.status, header)
outgoing.end(body)
return
}

const resHeaderRecord: OutgoingHttpHeaders = {}
const cookies = []
for (const [k, v] of res.headers) {
Expand Down
10 changes: 10 additions & 0 deletions test/server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ describe('Basic', () => {
app.get('/posts', (c) => {
return c.text(`Page ${c.req.query('page')}`)
})
app.get('/user-agent', (c) => {
return c.text(c.req.header('user-agent') as string)
})
app.post('/posts', (c) => {
return c.redirect('/posts')
})
Expand All @@ -37,6 +40,13 @@ describe('Basic', () => {
expect(res.text).toBe('Page 2')
})

it('Should return 200 response - GET /user-agent', async () => {
const res = await request(server).get('/user-agent').set('user-agent', 'Hono')
expect(res.status).toBe(200)
expect(res.headers['content-type']).toMatch(/text\/plain/)
expect(res.text).toBe('Hono')
})

it('Should return 302 response - POST /posts', async () => {
const res = await request(server).post('/posts')
expect(res.status).toBe(302)
Expand Down

0 comments on commit 443c3ab

Please sign in to comment.