Skip to content

Commit

Permalink
refactor!: use events api for utils with compatibility layer
Browse files Browse the repository at this point in the history
  • Loading branch information
pi0 committed Mar 29, 2022
1 parent cdf9b7c commit 7ad1367
Show file tree
Hide file tree
Showing 12 changed files with 145 additions and 121 deletions.
52 changes: 26 additions & 26 deletions src/app.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type http from 'http'
import { withoutTrailingSlash } from 'ufo'
import { lazyHandle, promisifyHandle } from './handle'
import { toEventHandler, createEvent } from './event'
import { toEventHandler, createEvent, H3CompatibilityEvent } from './event'
import { createError, sendError } from './error'
import { send, sendStream, isStream, MIMES } from './utils'
import type { IncomingMessage, ServerResponse } from './types/node'
Expand All @@ -25,7 +26,7 @@ export interface InputLayer {

export type InputStack = InputLayer[]

export type Matcher = (url: string, req?: IncomingMessage) => boolean
export type Matcher = (url: string, event?: H3CompatibilityEvent) => boolean

export interface AppUse {
(route: string | string [], handle: Middleware | Middleware[], options?: Partial<InputLayer>): App
Expand All @@ -35,32 +36,33 @@ export interface AppUse {
(options: InputLayer): App
}

export interface App {
(req: IncomingMessage, res: ServerResponse): Promise<any>
export type AppHandler = (req: http.IncomingMessage, res: http.ServerResponse) => Promise<any>

export interface App extends AppHandler {
stack: Stack
_handle: PHandle
use: AppUse
}

export interface AppOptions {
debug?: boolean
onError?: (error: Error, req: IncomingMessage, res: ServerResponse) => any
onError?: (error: Error, event: H3CompatibilityEvent) => any
}

export function createApp (options: AppOptions = {}): App {
const stack: Stack = []

const _handle = createHandle(stack, options)

// @ts-ignore
const app: Partial<App> = function (req: IncomingMessage, res: ServerResponse) {
return _handle(req, res).catch((error: Error) => {
const app: App = function (req, res) {
const event = createEvent(req, res)
return _handle(event).catch((error: Error) => {
if (options.onError) {
return options.onError(error, req, res)
return options.onError(error, event)
}
return sendError(res, error, !!options.debug)
return sendError(event, error, !!options.debug)
})
}
} as App

app.stack = stack
app._handle = _handle
Expand Down Expand Up @@ -91,46 +93,44 @@ export function use (
return app
}

export function createHandle (stack: Stack, options: AppOptions): PHandle {
export function createHandle (stack: Stack, options: AppOptions) {
const spacing = options.debug ? 2 : undefined
return async function handle (req: IncomingMessage, res: ServerResponse) {
const event = createEvent(req, res)
return async function handle (event: H3CompatibilityEvent) {
event.req.originalUrl = event.req.originalUrl || event.req.url || '/'

// @ts-ignore express/connect compatibility
req.originalUrl = req.originalUrl || req.url || '/'
const reqUrl = req.url || '/'
const reqUrl = event.req.url || '/'
for (const layer of stack) {
if (layer.route.length > 1) {
if (!reqUrl.startsWith(layer.route)) {
continue
}
req.url = reqUrl.slice(layer.route.length) || '/'
event.req.url = reqUrl.slice(layer.route.length) || '/'
} else {
req.url = reqUrl
event.req.url = reqUrl
}
if (layer.match && !layer.match(req.url as string, req)) {
if (layer.match && !layer.match(event.req.url as string, event)) {
continue
}
const val = await layer.handler(event)
if (res.writableEnded) {
if (event.res.writableEnded) {
return
}
const type = typeof val
if (type === 'string') {
return send(res, val, MIMES.html)
return send(event, val, MIMES.html)
} else if (isStream(val)) {
return sendStream(res, val)
return sendStream(event, val)
} else if (type === 'object' || type === 'boolean' || type === 'number' /* IS_JSON */) {
if (val && (val as Buffer).buffer) {
return send(res, val)
return send(event, val)
} else if (val instanceof Error) {
throw createError(val)
} else {
return send(res, JSON.stringify(val, null, spacing), MIMES.json)
return send(event, JSON.stringify(val, null, spacing), MIMES.json)
}
}
}
if (!res.writableEnded) {
if (!event.res.writableEnded) {
throw createError({ statusCode: 404, statusMessage: 'Not Found' })
}
}
Expand Down
20 changes: 10 additions & 10 deletions src/error.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { ServerResponse } from './types/node'
import type { H3CompatibilityEvent } from './event'
import { MIMES } from './utils'

/**
Expand Down Expand Up @@ -50,12 +50,12 @@ export function createError (input: Partial<H3Error>): H3Error {
* H3 internally uses this function to handle unhandled errors.<br>
* Note that calling this function will close the connection and no other data will be sent to client afterwards.
*
* @param res {ServerResponse} The ServerResponse object is passed as the second parameter in the handler function
@param event {H3CompatibilityEvent} H3 event or req passed by h3 handler
* @param error {H3Error|Error} Raised error
* @param debug {Boolean} Whether application is in debug mode.<br>
* In the debug mode the stack trace of errors will be return in response.
*/
export function sendError (res: ServerResponse, error: Error | H3Error, debug?: boolean) {
export function sendError (event: H3CompatibilityEvent, error: Error | H3Error, debug?: boolean) {
let h3Error: H3Error
if (error instanceof H3Error) {
h3Error = error
Expand All @@ -64,16 +64,16 @@ export function sendError (res: ServerResponse, error: Error | H3Error, debug?:
h3Error = createError(error)
}

if (res.writableEnded) {
if (event.res.writableEnded) {
return
}

res.statusCode = h3Error.statusCode
res.statusMessage = h3Error.statusMessage
event.res.statusCode = h3Error.statusCode
event.res.statusMessage = h3Error.statusMessage

const responseBody = {
statusCode: res.statusCode,
statusMessage: res.statusMessage,
statusCode: event.res.statusCode,
statusMessage: event.res.statusMessage,
stack: [] as string[],
data: h3Error.data
}
Expand All @@ -82,6 +82,6 @@ export function sendError (res: ServerResponse, error: Error | H3Error, debug?:
responseBody.stack = (h3Error.stack || '').split('\n').map(l => l.trim())
}

res.setHeader('Content-Type', MIMES.json)
res.end(JSON.stringify(responseBody, null, 2))
event.res.setHeader('Content-Type', MIMES.json)
event.res.end(JSON.stringify(responseBody, null, 2))
}
40 changes: 31 additions & 9 deletions src/event.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,32 @@
import type http from 'http'
import type { IncomingMessage, ServerResponse } from './types/node'
import type { Handle, Middleware } from './handle'

export interface H3Event {
'__is_event__': true
req: IncomingMessage
res: ServerResponse,
next?: (err?: Error) => void
}

export type H3CompatibilityEvent = H3Event | IncomingMessage | ServerResponse

export type _JSONValue<T=string|number|boolean> = T | T[] | Record<string, T>
export type JSONValue = _JSONValue<_JSONValue>
export type H3Response = void | JSONValue | Buffer

export interface H3EventHandler {
__is_handler__?: true
(event: H3Event): H3Response| Promise<H3Response>
'__is_handler__'?: true
(event: H3CompatibilityEvent): H3Response| Promise<H3Response>
}

export function defineEventHandler (handler: H3EventHandler) {
handler.__is_handler__ = true
return handler
}

export function isEventHandler (handler: H3EventHandler | Handle | Middleware): handler is H3EventHandler {
return '__is_handler__' in handler
export function isEventHandler (input: any): input is H3EventHandler {
return '__is_handler__' in input
}

export function toEventHandler (handler: H3EventHandler | Handle | Middleware): H3EventHandler {
Expand All @@ -31,18 +35,36 @@ export function toEventHandler (handler: H3EventHandler | Handle | Middleware):
}
if (handler.length > 2) {
return defineEventHandler((event) => {
return (handler as Middleware)(event.req, event.res, event.next!)
return (handler as Middleware)(event.req as IncomingMessage, event.res, event.next!)
})
} else {
return defineEventHandler((event) => {
return (handler as Handle)(event.req, event.res)
return (handler as Handle)(event.req as IncomingMessage, event.res)
})
}
}

export function createEvent (req: IncomingMessage, res: ServerResponse): H3Event {
return {
export function createEvent (req: http.IncomingMessage, res: http.ServerResponse): H3CompatibilityEvent {
const event = {
__is_event__: true,
req,
res
}
} as H3Event

// Add backward comatibility for interchangable usage of {event,req,res}.{event,req,res}
event.event = event

req.event = event
req.req = req
req.res = res

res.event = event
// res.req = req
res.res = res

return event
}

export function isEvent (input: any): input is H3Event {
return '__is_event__' in input
}
2 changes: 1 addition & 1 deletion src/handle.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { withoutTrailingSlash, withoutBase } from 'ufo'
import type { IncomingMessage, ServerResponse } from './types/node'
import type { IncomingMessage, ServerResponse } from './types'

export type Handle<T = any, ReqT={}> = (req: IncomingMessage & ReqT, res: ServerResponse) => T
export type PHandle = Handle<Promise<any>>
Expand Down
2 changes: 0 additions & 2 deletions src/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,9 @@ export function createRouter (): Router {
}

// Add params
// @ts-ignore
req.params = matched.params || {}

// Call handler
// @ts-ignore
return handler(req, res)
}

Expand Down
3 changes: 3 additions & 0 deletions src/types/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './cookie'
export * from './http'
export * from './node'
19 changes: 18 additions & 1 deletion src/types/node.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,20 @@
export type { IncomingMessage, ServerResponse } from 'http'
import type http from 'http'
import type { H3Event } from '../event'

export interface H3CompatbilityAugmentions {
event: H3Event,
req: H3Event['req'],
res: H3Event['res']
}

export interface IncomingMessage extends http.IncomingMessage, H3CompatbilityAugmentions {
originalUrl?: string // Connect and Express
}
export interface ServerResponse extends http.ServerResponse, Omit<H3CompatbilityAugmentions, 'req'> {
req: http.ServerResponse['req'] & {
event: H3Event
originalUrl?: string // Connect and Express
}
}

export type Encoding = false | 'ascii' | 'utf8' | 'utf-8' | 'utf16le' | 'ucs2' | 'ucs-2' | 'base64' | 'latin1' | 'binary' | 'hex'
48 changes: 17 additions & 31 deletions src/utils/body.ts
Original file line number Diff line number Diff line change
@@ -1,57 +1,49 @@
import type { IncomingMessage } from 'http'
import destr from 'destr'
import type { Encoding } from '../types/node'
import type { HTTPMethod } from '../types/http'
import type { H3CompatibilityEvent } from '../event'
import { assertMethod } from './request'

const RawBodySymbol = Symbol('h3RawBody')
const ParsedBodySymbol = Symbol('h3RawBody')

const PayloadMethods = ['PATCH', 'POST', 'PUT', 'DELETE'] as HTTPMethod[]

interface _IncomingMessage extends IncomingMessage {
[RawBodySymbol]?: Promise<Buffer>
ParsedBodySymbol?: any
body?: any // unenv
}

/**
* Reads body of the request and returns encoded raw string (default) or `Buffer` if encoding if falsy.
* @param req {IncomingMessage} An IncomingMessage object is created by [http.Server](https://nodejs.org/api/http.html#http_class_http_server)
* @param event {H3CompatibilityEvent} H3 event or req passed by h3 handler
* @param encoding {Encoding} encoding="utf-8" - The character encoding to use.
*
* @return {String|Buffer} Encoded raw string or raw Buffer of the body
*/
export function useRawBody (req: _IncomingMessage, encoding: Encoding = 'utf-8'): Encoding extends false ? Buffer : Promise<string> {
export function useRawBody (event: H3CompatibilityEvent, encoding: Encoding = 'utf-8'): Encoding extends false ? Buffer : Promise<string | Buffer> {
// Ensure using correct HTTP method before attempt to read payload
assertMethod(req, PayloadMethods)
assertMethod(event, PayloadMethods)

if (RawBodySymbol in req) {
const promise = Promise.resolve(req[RawBodySymbol])
// @ts-ignore
if (RawBodySymbol in event.req) {
const promise = Promise.resolve((event.req as any)[RawBodySymbol])
return encoding ? promise.then(buff => buff.toString(encoding)) : promise
}

// Workaround for unenv issue https://github.com/unjs/unenv/issues/8
if ('body' in req) {
return Promise.resolve(req.body)
if ('body' in event.req) {
return Promise.resolve((event.req as any).body)
}

const promise = req[RawBodySymbol] = new Promise<Buffer>((resolve, reject) => {
const promise = (event.req as any)[RawBodySymbol] = new Promise<Buffer>((resolve, reject) => {
const bodyData: any[] = []
req
event.req
.on('error', (err) => { reject(err) })
.on('data', (chunk) => { bodyData.push(chunk) })
.on('end', () => { resolve(Buffer.concat(bodyData)) })
})

// @ts-ignore
return encoding ? promise.then(buff => buff.toString(encoding)) : promise
}

/**
* Reads request body and try to safely parse using [destr](https://github.com/unjs/destr)
* @param req {IncomingMessage} An IncomingMessage object created by [http.Server](https://nodejs.org/api/http.html#http_class_http_server)
* @param event {H3CompatibilityEvent} H3 event or req passed by h3 handler
* @param encoding {Encoding} encoding="utf-8" - The character encoding to use.
*
* @return {*} The `Object`, `Array`, `String`, `Number`, `Boolean`, or `null` value corresponding to the request JSON body
Expand All @@ -60,18 +52,12 @@ export function useRawBody (req: _IncomingMessage, encoding: Encoding = 'utf-8')
* const body = await useBody(req)
* ```
*/
export async function useBody<T=any> (req: _IncomingMessage): Promise<T> {
// @ts-ignore
if (ParsedBodySymbol in req) {
// @ts-ignore
return req[ParsedBodySymbol]
export async function useBody<T=any> (event: H3CompatibilityEvent): Promise<T> {
if (ParsedBodySymbol in event.req) {
return (event.req as any)[ParsedBodySymbol]
}

const body = await useRawBody(req)
const json = destr(body)

// @ts-ignore
req[ParsedBodySymbol] = json

const body = await useRawBody(event)
const json = destr(body) as T
(event.req as any)[ParsedBodySymbol] = json
return json
}
Loading

0 comments on commit 7ad1367

Please sign in to comment.