Skip to content

Commit

Permalink
refactor!: use events api for utils with compatibility layer (#75)
Browse files Browse the repository at this point in the history
BREAKING CHANGE: All `handle` exports and properties are renamed to `hanhttps://assets.grammarly.com/emoji/v1/1f610.svgdler` with some backward compatibilities.

BREAKING CHANGE: Legacy handlers are promisified by default
  • Loading branch information
pi0 authored Mar 29, 2022
1 parent 70f03fe commit 2cf0f4b
Show file tree
Hide file tree
Showing 17 changed files with 294 additions and 333 deletions.
104 changes: 53 additions & 51 deletions src/app.ts
Original file line number Diff line number Diff line change
@@ -1,69 +1,73 @@
import type http from 'http'
import { withoutTrailingSlash } from 'ufo'
import { lazyHandle, promisifyHandle } from './handle'
import { toEventHandler, createEvent } from './event'
import { defineLazyHandler } from './handler'
import { toEventHandler, createEvent, isEventHandler } from './event'
import { createError, sendError } from './error'
import { send, sendStream, isStream, MIMES } from './utils'
import type { IncomingMessage, ServerResponse } from './types/node'
import type { Handle, LazyHandle, Middleware, PHandle } from './handle'
import type { H3EventHandler } from './event'
import type { Handler, LazyHandler, Middleware, PromisifiedHandler } from './types'
import type { EventHandler, CompatibilityEvent } from './event'

export interface Layer {
route: string
match?: Matcher
handler: H3EventHandler
handler: EventHandler
}

export type Stack = Layer[]

export interface InputLayer {
route?: string
match?: Matcher
handle: Handle | LazyHandle
handler: Handler | LazyHandler
lazy?: boolean
/**
* @deprecated
*/
promisify?: boolean
}

export type InputStack = InputLayer[]

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

export type RequestHandler = EventHandler | Handler | Middleware

export interface AppUse {
(route: string | string [], handle: Middleware | Middleware[], options?: Partial<InputLayer>): App
(route: string | string[], handle: Handle | Handle[], options?: Partial<InputLayer>): App
(handle: Middleware | Middleware[], options?: Partial<InputLayer>): App
(handle: Handle | Handle[], options?: Partial<InputLayer>): App
(route: string | string [], handler: RequestHandler | RequestHandler[], options?: Partial<InputLayer>): App
(handler: RequestHandler | Handler[], options?: Partial<InputLayer>): App
(options: InputLayer): App
}

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

export interface App extends ApPromisifiedHandlerr {
stack: Stack
_handle: PHandle
_handler: PromisifiedHandler
use: AppUse
}

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

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

const _handle = createHandle(stack, options)
const _handler = createHandler(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 _handler(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
app._handler = _handler

// @ts-ignore
app.use = (arg1, arg2, arg3) => use(app as App, arg1, arg2, arg3)
Expand All @@ -73,81 +77,79 @@ export function createApp (options: AppOptions = {}): App {

export function use (
app: App,
arg1: string | Handle | InputLayer | InputLayer[],
arg2?: Handle | Partial<InputLayer> | Handle[] | Middleware | Middleware[],
arg1: string | Handler | InputLayer | InputLayer[],
arg2?: Handler | Partial<InputLayer> | Handler[] | Middleware | Middleware[],
arg3?: Partial<InputLayer>
) {
if (Array.isArray(arg1)) {
arg1.forEach(i => use(app, i, arg2, arg3))
} else if (Array.isArray(arg2)) {
arg2.forEach(i => use(app, arg1, i, arg3))
} else if (typeof arg1 === 'string') {
app.stack.push(normalizeLayer({ ...arg3, route: arg1, handle: arg2 as Handle }))
app.stack.push(normalizeLayer({ ...arg3, route: arg1, handler: arg2 as Handler }))
} else if (typeof arg1 === 'function') {
app.stack.push(normalizeLayer({ ...arg2, route: '/', handle: arg1 as Handle }))
app.stack.push(normalizeLayer({ ...arg2, route: '/', handler: arg1 as Handler }))
} else {
app.stack.push(normalizeLayer({ ...arg1 }))
}
return app
}

export function createHandle (stack: Stack, options: AppOptions): PHandle {
export function createHandler (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: CompatibilityEvent) {
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' })
}
}
}

function normalizeLayer (input: InputLayer) {
if (input.promisify === undefined) {
input.promisify = input.handle.length > 2 /* req, res, next */
let handler = input.handler
if (!isEventHandler(handler)) {
if (input.lazy) {
handler = defineLazyHandler(handler as LazyHandler)
}
handler = toEventHandler(handler)
}

const handle = input.lazy
? lazyHandle(input.handle as LazyHandle, input.promisify)
: (input.promisify ? promisifyHandle(input.handle) : input.handle)

return {
route: withoutTrailingSlash(input.route),
match: input.match,
handler: toEventHandler(handle)
}
handler
} as Layer
}
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 { CompatibilityEvent } 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 {CompatibilityEvent} 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: CompatibilityEvent, 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))
}
95 changes: 72 additions & 23 deletions src/event.ts
Original file line number Diff line number Diff line change
@@ -1,48 +1,97 @@
import type { IncomingMessage, ServerResponse } from './types/node'
import type { Handle, Middleware } from './handle'
import type http from 'http'
import type { IncomingMessage, ServerResponse, Handler, Middleware } from './types'
import { callHandler } from './handler'

export interface H3Event {
'__is_event__': true
event: H3Event
req: IncomingMessage
res: ServerResponse,
next?: (err?: Error) => void
res: ServerResponse
/**
* Request params only filled with h3 Router handlers
*/
params?: Record<string, any>
}

export type CompatibilityEvent = 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>
export interface EventHandler {
'__is_handler__'?: true
(event: CompatibilityEvent): H3Response| Promise<H3Response>
}

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

export function isEventHandler (handler: H3EventHandler | Handle | Middleware): handler is H3EventHandler {
return '__is_handler__' in handler
export function defineLazyEventHandler (factory: () => EventHandler | Promise<EventHandler>): EventHandler {
let _promise: Promise<EventHandler>
let _resolved: EventHandler
const resolveHandler = () => {
if (_resolved) { return Promise.resolve(_resolved) }
if (!_promise) {
_promise = Promise.resolve(factory()).then((r: any) => {
_resolved = r.default || r
return _resolved
})
}
return _promise
}
return defineEventHandler((event) => {
if (_resolved) {
return _resolved(event)
}
return resolveHandler().then(handler => handler(event))
})
}

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

export function toEventHandler (handler: EventHandler | Handler | Middleware): EventHandler {
if (isEventHandler(handler)) {
return handler
}
if (handler.length > 2) {
return defineEventHandler((event) => {
return (handler as Middleware)(event.req, event.res, event.next!)
})
} else {
return defineEventHandler((event) => {
return (handler as Handle)(event.req, event.res)
})
}
return defineEventHandler((event) => {
return callHandler(handler, event.req as IncomingMessage, event.res) as Promise<H3Response>
})
}

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

// Backward comatibility for interchangable usage of {event,req,res}.{req,res}
// TODO: Remove in future versions
// @ts-ignore
event.event = event
// @ts-ignore
req.event = event
// @ts-ignore
req.req = req
// @ts-ignore
req.res = res
// @ts-ignore
res.event = event
// @ts-ignore
res.res = res
// @ts-ignore
res.req.res = res
// @ts-ignore
res.req.req = req

return event
}

export function isEvent (input: any): input is H3Event {
return '__is_event__' in input
}
Loading

0 comments on commit 2cf0f4b

Please sign in to comment.