UNPKG

@akala/core

Version:
209 lines (195 loc) 7.54 kB
import { MiddlewareCompositeAsync } from "./middlewares/composite-async.js"; import type { MiddlewareAsync, MiddlewarePromise } from "./middlewares/shared.js"; import type { Routable } from "./router/route.js"; import ErrorWithStatus, { HttpStatusCode } from "./errorWithStatus.js"; import { RouterAsync } from "./router/router-async.js"; type MiddlewareError = 'break' | 'loop' | undefined; /** * Handles URL routing and middleware processing for different protocol, host, and path components * @template T - Tuple type representing middleware context parameters [URL, ...unknown[], Partial<TResult>] * @template TResult - Result type for middleware processing (default: object) */ export class UrlHandler<T extends [URL, ...unknown[], Partial<TResult> | void], TResult = object> implements MiddlewareAsync<T> { /** * Composite middleware stack for protocol-specific processing */ protocol: MiddlewareCompositeAsync<T, MiddlewareError>; /** * Composite middleware stack for host-specific processing */ host: MiddlewareCompositeAsync<T>; /** * Router for path-based routing */ router: RouterAsync<[Routable, ...T]>; /** * Creates a new URL handler instance */ constructor(private readonly noAssign: boolean = false) { this.protocol = new MiddlewareCompositeAsync('protocols'); this.host = new MiddlewareCompositeAsync('domains'); this.router = new RouterAsync('path'); } /** * warning ! the second parameter needs to be not null as we will assign properties to it. * Processes the URL through protocol, host, and path routing middleware * @param context - Middleware context parameters * @returns Promise resolving to the final TResult object */ /** * Processes the URL through protocol, host, and path routing middleware * @param context - Middleware context parameters * @returns Promise resolving to the final TResult object */ public process(...context: T): Promise<TResult> { return this.handle(...context).then(v => { throw v }, (result) => this.noAssign ? result : context[context.length - 1] as TResult); } /** * Adds a protocol handler middleware * @param protocol Protocol to handle (colon will be automatically stripped if present) * @param action Async handler function for protocol processing * @returns Registered protocol middleware instance */ public useProtocol(protocol: string, action: (...args: T) => Promise<TResult>) { const handler = new UrlHandler.Protocol<T>(protocol); this.protocol.useMiddleware(handler); return handler.use((...context) => action(...context).then(result => { if (!this.noAssign && typeof result !== 'undefined') if (context[context.length - 1]) return Object.assign(context[context.length - 1], result); else return context[context.length - 1] = result; else if (this.noAssign) return result; })); } /** * Adds a host handler middleware * @param host Hostname to match * @param action Async handler function for host processing * @returns Registered host middleware instance */ public useHost(host: string, action: (...args: T) => Promise<TResult>) { const handler = new UrlHandler.Host<T>(host); this.host.useMiddleware(handler); return handler.use((...context) => action(...context).then(result => { if (!this.noAssign && typeof result !== 'undefined') Object.assign(context[context.length - 1] || {}, result) else if (this.noAssign) return result; })); } /** * Handles the URL processing pipeline * @param context - Middleware context parameters * @returns Promise that resolves when handling fails or rejects with the final result */ public async handle(...context: T): MiddlewarePromise { let error = await this.protocol.handle(...context); while (error === 'loop') error = await this.handle(...context); if (error) return error; error = await this.host.handle(...context); if (error) return error; let params: Routable['params']; error = await this.router.handle({ path: context[0].pathname, get params() { if (params) return params; if (context[0].search) return params; return params = Object.fromEntries(Array.from(context[0].searchParams.keys()).map(k => { const values = context[0].searchParams.getAll(k); if (values.length == 1) return [k, values[0]]; return [k, values]; })); } }, ...context); if (error) return error; return new ErrorWithStatus(HttpStatusCode.NotFound, `${context[0]} is not supported`) } } /** * Namespace containing Protocol and Host middleware classes */ export namespace UrlHandler { /** * Middleware for handling specific protocols * @template T - Middleware context type extending [URL, ...unknown[]] */ export class Protocol<T extends [URL, ...unknown[]]> extends MiddlewareCompositeAsync<T, MiddlewareError> { /** * @param protocol - The protocol to handle (automatically strips trailing colon if present) */ constructor(public readonly protocol: string) { super(); if (protocol.endsWith(':')) this.protocol = protocol.substring(0, protocol.length - 1); } /** * Handles protocol matching and processing * @param context - Middleware context parameters * @returns Promise resolving to middleware error status or undefined */ async handle(...context: T): MiddlewarePromise<MiddlewareError> { if (context[0].protocol == this.protocol + ':') { return super.handle(...context); } else if (context[0].protocol.startsWith(this.protocol + '+')) { return super.handle(...context).then(error => error, () => { context[0].protocol = context[0].protocol.substring(this.protocol.length + 2); return 'loop'; }); } return; } } /** * Middleware for handling specific hosts * @template T - Middleware context type extending [URL, ...unknown[]] */ export class Host<T extends [URL, ...unknown[]]> extends MiddlewareCompositeAsync<T, MiddlewareError> { /** * @param host - The host name to match */ constructor(private host: string) { super(); } /** * Handles host matching * @param context - Middleware context parameters * @returns Promise resolving to middleware error status or undefined */ handle(...context: T): MiddlewarePromise<MiddlewareError> { if (context[0].host === this.host) { return super.handle(...context); } return; } } }