import { Context } from "./context.ts";import { Status, STATUS_TEXT } from "./deps.ts";import { FlashServer } from "./http_server_flash.ts";import { HttpServer } from "./http_server_native.ts";import { NativeRequest } from "./http_server_native_request.ts";import { KeyStack } from "./keyStack.ts";import { compose, Middleware } from "./middleware.ts";import { cloneState } from "./structured_clone.ts";import { Key, Listener, Server, ServerConstructor, ServerRequest,} from "./types.d.ts";import { assert, isConn } from "./util.ts";
export interface ListenOptionsBase { port?: number; hostname?: string; secure?: false; signal?: AbortSignal;}
export interface ListenOptionsTls extends Deno.ListenTlsOptions { alpnProtocols?: string[]; secure: true; signal?: AbortSignal;}
export interface HandleMethod { ( request: Request, conn?: Deno.Conn, secure?: boolean, ): Promise<Response | undefined>;}
export type ListenOptions = ListenOptionsTls | ListenOptionsBase;
interface ApplicationErrorEventListener<S extends AS, AS> { (evt: ApplicationErrorEvent<S, AS>): void | Promise<void>;}
interface ApplicationErrorEventListenerObject<S extends AS, AS> { handleEvent(evt: ApplicationErrorEvent<S, AS>): void | Promise<void>;}
interface ApplicationErrorEventInit<S extends AS, AS extends State> extends ErrorEventInit { context?: Context<S, AS>;}
type ApplicationErrorEventListenerOrEventListenerObject<S extends AS, AS> = | ApplicationErrorEventListener<S, AS> | ApplicationErrorEventListenerObject<S, AS>;
interface ApplicationListenEventListener { (evt: ApplicationListenEvent): void | Promise<void>;}
interface ApplicationListenEventListenerObject { handleEvent(evt: ApplicationListenEvent): void | Promise<void>;}
interface ApplicationListenEventInit extends EventInit { hostname: string; listener: Listener; port: number; secure: boolean; serverType: "native" | "flash" | "custom";}
type ApplicationListenEventListenerOrEventListenerObject = | ApplicationListenEventListener | ApplicationListenEventListenerObject;
export interface ApplicationOptions<S, R extends ServerRequest> { contextState?: "clone" | "prototype" | "alias" | "empty";
jsonBodyReplacer?: ( key: string, value: unknown, context: Context<S>, ) => unknown;
jsonBodyReviver?: ( key: string, value: unknown, context: Context<S>, ) => unknown;
keys?: KeyStack | Key[];
logErrors?: boolean;
proxy?: boolean;
serverConstructor?: ServerConstructor<R>;
state?: S;}
interface RequestState { handling: Set<Promise<void>>; closing: boolean; closed: boolean; server: Server<ServerRequest>;}
export type State = Record<string | number | symbol, any>;
const ADDR_REGEXP = /^\[?([^\]]*)\]?:([0-9]{1,5})$/;
const DEFAULT_SERVER: ServerConstructor<ServerRequest> = HttpServer;
export class ApplicationErrorEvent<S extends AS, AS extends State> extends ErrorEvent { context?: Context<S, AS>;
constructor(eventInitDict: ApplicationErrorEventInit<S, AS>) { super("error", eventInitDict); this.context = eventInitDict.context; }}
function logErrorListener<S extends AS, AS extends State>( { error, context }: ApplicationErrorEvent<S, AS>,) { if (error instanceof Error) { console.error( `[uncaught application error]: ${error.name} - ${error.message}`, ); } else { console.error(`[uncaught application error]\n`, error); } if (context) { let url: string; try { url = context.request.url.toString(); } catch { url = "[malformed url]"; } console.error(`\nrequest:`, { url, method: context.request.method, hasBody: context.request.hasBody, }); console.error(`response:`, { status: context.response.status, type: context.response.type, hasBody: !!context.response.body, writable: context.response.writable, }); } if (error instanceof Error && error.stack) { console.error(`\n${error.stack.split("\n").slice(1).join("\n")}`); }}
export class ApplicationListenEvent extends Event { hostname: string; listener: Listener; port: number; secure: boolean; serverType: "native" | "flash" | "custom";
constructor(eventInitDict: ApplicationListenEventInit) { super("listen", eventInitDict); this.hostname = eventInitDict.hostname; this.listener = eventInitDict.listener; this.port = eventInitDict.port; this.secure = eventInitDict.secure; this.serverType = eventInitDict.serverType; }}
export class Application<AS extends State = Record<string, any>> extends EventTarget { #composedMiddleware?: (context: Context<AS, AS>) => Promise<unknown>; #contextOptions: Pick< ApplicationOptions<AS, ServerRequest>, "jsonBodyReplacer" | "jsonBodyReviver" >; #contextState: "clone" | "prototype" | "alias" | "empty"; #keys?: KeyStack; #middleware: Middleware<State, Context<State, AS>>[] = []; #serverConstructor: ServerConstructor<ServerRequest>;
get keys(): KeyStack | Key[] | undefined { return this.#keys; }
set keys(keys: KeyStack | Key[] | undefined) { if (!keys) { this.#keys = undefined; return; } else if (Array.isArray(keys)) { this.#keys = new KeyStack(keys); } else { this.#keys = keys; } }
proxy: boolean;
state: AS;
constructor(options: ApplicationOptions<AS, ServerRequest> = {}) { super(); const { state, keys, proxy, serverConstructor = DEFAULT_SERVER, contextState = "clone", logErrors = true, ...contextOptions } = options;
this.proxy = proxy ?? false; this.keys = keys; this.state = state ?? {} as AS; this.#serverConstructor = serverConstructor; this.#contextOptions = contextOptions; this.#contextState = contextState;
if (logErrors) { this.addEventListener("error", logErrorListener); } }
#getComposed(): (context: Context<AS, AS>) => Promise<unknown> { if (!this.#composedMiddleware) { this.#composedMiddleware = compose(this.#middleware); } return this.#composedMiddleware; }
#getContextState(): AS { switch (this.#contextState) { case "alias": return this.state; case "clone": return cloneState(this.state); case "empty": return {} as AS; case "prototype": return Object.create(this.state); } }
#handleError(context: Context<AS>, error: any): void { if (!(error instanceof Error)) { error = new Error(`non-error thrown: ${JSON.stringify(error)}`); } const { message } = error; this.dispatchEvent(new ApplicationErrorEvent({ context, message, error })); if (!context.response.writable) { return; } for (const key of [...context.response.headers.keys()]) { context.response.headers.delete(key); } if (error.headers && error.headers instanceof Headers) { for (const [key, value] of error.headers) { context.response.headers.set(key, value); } } context.response.type = "text"; const status: Status = context.response.status = Deno.errors && error instanceof Deno.errors.NotFound ? 404 : error.status && typeof error.status === "number" ? error.status : 500; context.response.body = error.expose ? error.message : STATUS_TEXT[status]; }
async #handleRequest( request: ServerRequest, secure: boolean, state: RequestState, ): Promise<void> { const context = new Context( this, request, this.#getContextState(), { secure, ...this.#contextOptions }, ); let resolve: () => void; const handlingPromise = new Promise<void>((res) => resolve = res); state.handling.add(handlingPromise); if (!state.closing && !state.closed) { try { await this.#getComposed()(context); } catch (err) { this.#handleError(context, err); } } if (context.respond === false) { context.response.destroy(); resolve!(); state.handling.delete(handlingPromise); return; } let closeResources = true; let response: Response; try { closeResources = false; response = await context.response.toDomResponse(); } catch (err) { this.#handleError(context, err); response = await context.response.toDomResponse(); } assert(response); try { await request.respond(response); } catch (err) { this.#handleError(context, err); } finally { context.response.destroy(closeResources); resolve!(); state.handling.delete(handlingPromise); if (state.closing) { await state.server.close(); state.closed = true; } } }
addEventListener<S extends AS>( type: "error", listener: ApplicationErrorEventListenerOrEventListenerObject<S, AS> | null, options?: boolean | AddEventListenerOptions, ): void; addEventListener( type: "listen", listener: ApplicationListenEventListenerOrEventListenerObject | null, options?: boolean | AddEventListenerOptions, ): void; addEventListener( type: "error" | "listen", listener: EventListenerOrEventListenerObject | null, options?: boolean | AddEventListenerOptions, ): void { super.addEventListener(type, listener, options); }
handle = (async ( request: Request, secureOrConn: Deno.Conn | boolean | undefined, secure: boolean | undefined = false, ): Promise<Response | undefined> => { if (!this.#middleware.length) { throw new TypeError("There is no middleware to process requests."); } assert(isConn(secureOrConn) || typeof secureOrConn === "undefined"); const contextRequest = new NativeRequest({ request, respondWith() { return Promise.resolve(undefined); }, }, { conn: secureOrConn }); const context = new Context( this, contextRequest, this.#getContextState(), { secure, ...this.#contextOptions }, ); try { await this.#getComposed()(context); } catch (err) { this.#handleError(context, err); } if (context.respond === false) { context.response.destroy(); return; } try { const response = await context.response.toDomResponse(); context.response.destroy(false); return response; } catch (err) { this.#handleError(context, err); throw err; } }) as HandleMethod;
async listen(addr: string): Promise<void>; async listen(options?: ListenOptions): Promise<void>; async listen(options: string | ListenOptions = { port: 0 }): Promise<void> { if (!this.#middleware.length) { throw new TypeError("There is no middleware to process requests."); } if (typeof options === "string") { const match = ADDR_REGEXP.exec(options); if (!match) { throw TypeError(`Invalid address passed: "${options}"`); } const [, hostname, portStr] = match; options = { hostname, port: parseInt(portStr, 10) }; } options = Object.assign({ port: 0 }, options); const server = new this.#serverConstructor( this, options as Deno.ListenOptions, ); const { signal } = options; const state = { closed: false, closing: false, handling: new Set<Promise<void>>(), server, }; if (signal) { signal.addEventListener("abort", () => { if (!state.handling.size) { server.close(); state.closed = true; } state.closing = true; }); } const { secure = false } = options; const serverType = server instanceof HttpServer ? "native" : server instanceof FlashServer ? "flash" : "custom"; const listener = await server.listen(); const { hostname, port } = listener.addr as Deno.NetAddr; this.dispatchEvent( new ApplicationListenEvent({ hostname, listener, port, secure, serverType, }), ); try { for await (const request of server) { this.#handleRequest(request, secure, state); } await Promise.all(state.handling); } catch (error) { const message = error instanceof Error ? error.message : "Application Error"; this.dispatchEvent( new ApplicationErrorEvent({ message, error }), ); } }
use<S extends State = AS>( middleware: Middleware<S, Context<S, AS>>, ...middlewares: Middleware<S, Context<S, AS>>[] ): Application<S extends AS ? S : (S & AS)>; use<S extends State = AS>( ...middleware: Middleware<S, Context<S, AS>>[] ): Application<S extends AS ? S : (S & AS)> { this.#middleware.push(...middleware); this.#composedMiddleware = undefined; return this as Application<any>; }
[Symbol.for("Deno.customInspect")](inspect: (value: unknown) => string) { const { keys, proxy, state } = this; return `${this.constructor.name} ${ inspect({ "#middleware": this.#middleware, keys, proxy, state }) }`; }
[Symbol.for("nodejs.util.inspect.custom")]( depth: number, options: any, inspect: (value: unknown, options?: unknown) => string, ) { if (depth < 0) { return options.stylize(`[${this.constructor.name}]`, "special"); }
const newOptions = Object.assign({}, options, { depth: options.depth === null ? null : options.depth - 1, }); const { keys, proxy, state } = this; return `${options.stylize(this.constructor.name, "special")} ${ inspect( { "#middleware": this.#middleware, keys, proxy, state }, newOptions, ) }`; }}