import type { HttpConnection } from './HttpConnection' import type { HttpResponseCompleted } from './HttpResponse' import type { HttpMethod } from './utils' import type { Has } from '@principia/base/Has' import type { FIO, IO } from '@principia/base/IO' import type { URL } from 'url' import * as A from '@principia/base/Array' import * as Ev from '@principia/base/Eval' import * as FR from '@principia/base/FiberRef' import * as FL from '@principia/base/FreeList' import { flow, identity, pipe } from '@principia/base/function' import * as I from '@principia/base/IO' import * as Q from '@principia/base/Queue' import * as p2r from 'path-to-regexp' import { HttpConnectionTag } from './HttpConnection' import { HttpException } from './HttpException' import { HttpServerTag } from './HttpServer' import * as Status from './StatusCode' import { HttpContentType } from './utils' /* * ------------------------------------------- * Model * ------------------------------------------- */ export type RouteFn = ( conn: HttpConnection, next: FIO ) => IO export class Empty { readonly R!: (_: R) => void readonly E!: () => E readonly _tag = 'Empty' } export class Route { readonly _tag = 'Route' readonly R!: (_: R) => void readonly E!: () => E readonly match: (method: HttpMethod, url: URL) => boolean constructor( readonly method: HttpMethod, readonly path: string, readonly route: RouteFn, readonly middlewares = FL.Empty>() ) { this.match = (method, url) => this.method === method && p2r.pathToRegexp(path).test(url.pathname || '') } middleware(): ReadonlyArray> { return FL.toArray(this.middlewares) } } export class Combine { readonly _tag = 'Combine' constructor(readonly left: Routes, readonly right: Routes) {} } export type Routes = Route | Combine | Empty export type MiddlewareFn = ( cont: RouteFn ) => (conn: HttpConnection, next: FIO) => IO export class Middleware { constructor(readonly apply: MiddlewareFn) {} } /* * ------------------------------------------- * Constructors * ------------------------------------------- */ export const empty: Routes = new Empty() export function _route( method: HttpMethod, path: string, handler: ( conn: HttpConnection, n: IO ) => IO & R1, E1, HttpResponseCompleted> ): (routes: Routes) => Routes { return (routes) => ( new Combine( routes, new Route(method, path, (conn, n) => I.giveService(HttpConnectionTag)(conn)(handler(conn, n))) ) ) } export function route( method: HttpMethod, path: string, handler: (conn: HttpConnection) => IO & R, E, HttpResponseCompleted> ): (routes: Routes) => Routes { return _route(method, path, handler) } export function middlewareSafe( routes: Routes, middle: (cont: RouteFn) => (conn: HttpConnection, next: FIO) => IO ): Ev.Eval> { return Ev.gen(function* (_) { switch (routes._tag) { case 'Empty': { return routes as any } case 'Route': { return new Route( routes.method, routes.path, routes.route, FL.append_(routes.middlewares, new Middleware(middle as any)) ) as any } case 'Combine': { return new Combine( yield* _(middlewareSafe(routes.left, middle)), yield* _(middlewareSafe(routes.right, middle)) ) } } }) } export function middleware_( routes: Routes, middle: (cont: RouteFn) => (conn: HttpConnection, next: FIO) => IO ): Routes { return Ev.run(middlewareSafe(routes, middle)) } /* * ------------------------------------------- * Primitives * ------------------------------------------- */ const Route404 = (): RouteFn => ({ res: response }, _) => I.orHalt( I.gen(function* (_) { yield* _(response.status(Status.NotFound)) yield* _(response.set({ 'content-type': HttpContentType.TEXT_PLAIN })) yield* _(response.write('404: Not Found')) return yield* _(response.end()) }) ) export function HttpExceptionHandler(routes: Routes): Routes> { return middleware_( routes, (cont) => (ctx, next) => pipe( cont(ctx, next), I.catchAll((e) => I.gen(function* (_) { yield* _(I.succeedLazy(() => console.log(e))) if (e instanceof HttpException) { yield* _(ctx.res.status(e.data!.status)) yield* _(ctx.res.set({ 'content-type': HttpContentType.TEXT_PLAIN })) yield* _(ctx.res.write(e.message)) return yield* _(ctx.res.end()) } else { yield* _(I.fail(e)) } }) ), I.catchAll((e) => { if (e instanceof HttpException) { return I.orHaltWith_(ctx.res.end(), () => e) } else { return I.fail(>e) } }) ) ) } /* * ------------------------------------------- * Drain * ------------------------------------------- */ type RouteMatch = (method: HttpMethod, url: URL) => RouteFn function toArray(routes: Routes): ReadonlyArray> { const go = (routes: Routes): Ev.Eval>> => Ev.gen(function* (_) { switch (routes._tag) { case 'Empty': { return [] } case 'Route': { const middlewares = routes.middleware() const x = (method: HttpMethod, url: URL) => (routes.match(method, url) ? routes.route : Route404()) if (A.isNonEmpty(middlewares)) { return [A.foldl_(middlewares, x, (b, m) => (method, url) => (r, n) => m.apply(b(method, url))(r, n))] } return [x] } case 'Combine': { return A.concat_(yield* _(go(routes.left)), yield* _(go(routes.right))) } } }) return Ev.run(go(routes)) } export const isRouterDraining = FR.unsafeMake(false, identity, (a, b) => a && b) type ProcessFn = (_: HttpConnection) => IO export function drain(rs: Routes) { const routes = toArray(rs) return I.gen(function* (_) { const env = yield* _(I.ask()) const pfn = yield* _( I.succeedLazy(() => A.foldl_( routes, (({ res: response }) => I.apSecond_(response.status(Status.NotFound), response.end())), (b, a) => (ctx) => I.gen(function* (_) { const method = yield* _(ctx.req.method) const url = yield* _( pipe( ctx.req.url, I.tapError((ex) => I.succeedLazy(() => console.log(ex))), I.orHalt ) ) return yield* _(I.give_(a(method, url)(ctx, b(ctx)), env)) }) ) ) ) const { queue } = yield* _(HttpServerTag) return yield* _( pipe(isRouterDraining, FR.set(true), I.apSecond(pipe(Q.take(queue), I.chain(flow(pfn, I.fork)), I.forever))) ) }) }