Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor!: use events api for utils with compatibility layer #75

Merged
merged 7 commits into from
Mar 29, 2022
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
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