/** * Mandu Lifecycle Hooks πŸ”„ * Elysia μŠ€νƒ€μΌ 라이프사이클 ν›… 체계 * * @see https://elysiajs.com/life-cycle/overview.html * * μš”μ²­ 흐름: * 1. onRequest - μš”μ²­ μ‹œμž‘ * 2. onParse - λ°”λ”” νŒŒμ‹± (POST, PUT, PATCH) * 3. beforeHandle - ν•Έλ“€λŸ¬ μ „ (Guard μ—­ν• ) * 4. [Handler] - 메인 ν•Έλ“€λŸ¬ μ‹€ν–‰ * 5. afterHandle - ν•Έλ“€λŸ¬ ν›„ (응닡 λ³€ν™˜) * 6. mapResponse - 응닡 λ§€ν•‘ * 7. afterResponse - 응닡 ν›„ (λ‘œκΉ…, 정리) * * μ—λŸ¬ λ°œμƒ μ‹œ: * - onError - μ—λŸ¬ 핸듀링 */ import type { ManduContext } from "../filling/context"; import { createTracer } from "./trace"; /** * ν›… μŠ€μ½”ν”„ * - global: λͺ¨λ“  λΌμš°νŠΈμ— 적용 * - scoped: ν˜„μž¬ ν”ŒλŸ¬κ·ΈμΈ/라우트 그룹에 적용 * - local: ν˜„μž¬ λΌμš°νŠΈμ—λ§Œ 적용 */ export type HookScope = "global" | "scoped" | "local"; /** * ν›… μ»¨ν…Œμ΄λ„ˆ */ export interface HookContainer { fn: T; scope: HookScope; name?: string; checksum?: number; // 쀑볡 제거용 } // ============================================ // ν›… νƒ€μž… μ •μ˜ // ============================================ /** μš”μ²­ μ‹œμž‘ ν›… */ export type OnRequestHandler = (ctx: ManduContext) => void | Promise; /** λ°”λ”” νŒŒμ‹± ν›… */ export type OnParseHandler = (ctx: ManduContext) => void | Promise; /** ν•Έλ“€λŸ¬ μ „ ν›… (Guard μ—­ν• ) - Response λ°˜ν™˜ μ‹œ 체인 쀑단 */ export type BeforeHandleHandler = ( ctx: ManduContext ) => Response | void | Promise; /** ν•Έλ“€λŸ¬ ν›„ ν›… - 응닡 λ³€ν™˜ κ°€λŠ₯ */ export type AfterHandleHandler = ( ctx: ManduContext, response: Response ) => Response | Promise; /** 응닡 λ§€ν•‘ ν›… */ export type MapResponseHandler = ( ctx: ManduContext, response: Response ) => Response | Promise; /** 응닡 ν›„ ν›… (비동기, 응닡에 영ν–₯ μ—†μŒ) */ export type AfterResponseHandler = (ctx: ManduContext) => void | Promise; /** μ—λŸ¬ 핸듀링 ν›… - Response λ°˜ν™˜ μ‹œ μ—λŸ¬ μ‘λ‹΅μœΌλ‘œ μ‚¬μš© */ export type OnErrorHandler = ( ctx: ManduContext, error: Error ) => Response | void | Promise; // ============================================ // 라이프사이클 μŠ€ν† μ–΄ // ============================================ /** * 라이프사이클 ν›… μŠ€ν† μ–΄ */ export interface LifecycleStore { onRequest: HookContainer[]; onParse: HookContainer[]; beforeHandle: HookContainer[]; afterHandle: HookContainer[]; mapResponse: HookContainer[]; afterResponse: HookContainer[]; onError: HookContainer[]; } /** * 빈 라이프사이클 μŠ€ν† μ–΄ 생성 */ export function createLifecycleStore(): LifecycleStore { return { onRequest: [], onParse: [], beforeHandle: [], afterHandle: [], mapResponse: [], afterResponse: [], onError: [], }; } // ============================================ // 라이프사이클 μ‹€ν–‰ // ============================================ /** * 라이프사이클 μ‹€ν–‰ μ˜΅μ…˜ */ export interface ExecuteOptions { /** λ°”λ”” νŒŒμ‹±μ΄ ν•„μš”ν•œ λ©”μ„œλ“œ */ parseBodyMethods?: string[]; /** 트레이슀 ν™œμ„±ν™” */ trace?: boolean; } const DEFAULT_PARSE_BODY_METHODS = ["POST", "PUT", "PATCH"]; /** * 라이프사이클 μ‹€ν–‰ * * @param lifecycle 라이프사이클 μŠ€ν† μ–΄ * @param ctx ManduContext * @param handler 메인 ν•Έλ“€λŸ¬ * @param options μ˜΅μ…˜ * * @example * ```typescript * const lifecycle = createLifecycleStore(); * lifecycle.onRequest.push({ fn: (ctx) => console.log('Request started'), scope: 'local' }); * lifecycle.beforeHandle.push({ fn: authGuard, scope: 'local' }); * * const response = await executeLifecycle( * lifecycle, * ctx, * async () => ctx.ok({ data: 'hello' }) * ); * ``` */ export async function executeLifecycle( lifecycle: LifecycleStore, ctx: ManduContext, handler: () => Promise, options: ExecuteOptions = {} ): Promise { const { parseBodyMethods = DEFAULT_PARSE_BODY_METHODS } = options; const tracer = createTracer(ctx, options.trace); let response: Response; try { // 1. onRequest const endRequest = tracer.begin("request"); for (const hook of lifecycle.onRequest) { await hook.fn(ctx); } endRequest(); // 2. onParse (λ°”λ””κ°€ μžˆλŠ” λ©”μ„œλ“œλ§Œ) if (parseBodyMethods.includes(ctx.req.method)) { const endParse = tracer.begin("parse"); for (const hook of lifecycle.onParse) { await hook.fn(ctx); } endParse(); } // 3. beforeHandle (Guard μ—­ν• ) const endBefore = tracer.begin("beforeHandle"); for (const hook of lifecycle.beforeHandle) { const result = await hook.fn(ctx); if (result instanceof Response) { // Response λ°˜ν™˜ μ‹œ 체인 쀑단, afterHandle/mapResponse κ±΄λ„ˆλœ€ response = result; endBefore(); // afterResponseλŠ” μ‹€ν–‰ scheduleAfterResponse(lifecycle.afterResponse, ctx, tracer); return response; } } endBefore(); // 4. 메인 ν•Έλ“€λŸ¬ μ‹€ν–‰ const endHandle = tracer.begin("handle"); response = await handler(); endHandle(); // 5. afterHandle const endAfter = tracer.begin("afterHandle"); for (const hook of lifecycle.afterHandle) { response = await hook.fn(ctx, response); } endAfter(); // 6. mapResponse const endMap = tracer.begin("mapResponse"); for (const hook of lifecycle.mapResponse) { response = await hook.fn(ctx, response); } endMap(); // 7. afterResponse (비동기) scheduleAfterResponse(lifecycle.afterResponse, ctx, tracer); return response; } catch (err) { // onError 처리 const error = err instanceof Error ? err : new Error(String(err)); tracer.error("error", error); for (const hook of lifecycle.onError) { const result = await hook.fn(ctx, error); if (result instanceof Response) { // afterResponseλŠ” μ—λŸ¬ μ‹œμ—λ„ μ‹€ν–‰ scheduleAfterResponse(lifecycle.afterResponse, ctx, tracer); return result; } } // μ—λŸ¬ ν•Έλ“€λŸ¬κ°€ Responseλ₯Ό λ°˜ν™˜ν•˜μ§€ μ•ŠμœΌλ©΄ 재throw throw error; } } /** * afterResponse ν›… 비동기 μ‹€ν–‰ (응닡 ν›„) */ function scheduleAfterResponse( hooks: HookContainer[], ctx: ManduContext, tracer?: ReturnType ): void { if (hooks.length === 0) return; // queueMicrotask둜 응닡 ν›„ μ‹€ν–‰ queueMicrotask(async () => { const endAfterResponse = tracer?.begin("afterResponse") ?? (() => {}); for (const hook of hooks) { try { await hook.fn(ctx); } catch (err) { console.error("[Mandu] afterResponse hook error:", err); } } endAfterResponse(); }); } // ============================================ // 라이프사이클 λΉŒλ” // ============================================ /** * 라이프사이클 λΉŒλ” * * @example * ```typescript * const lifecycle = new LifecycleBuilder() * .onRequest((ctx) => console.log('Request:', ctx.req.url)) * .beforeHandle(authGuard) * .afterHandle((ctx, res) => { * // 응닡 헀더 μΆ”κ°€ * res.headers.set('X-Custom', 'value'); * return res; * }) * .onError((ctx, err) => ctx.json({ error: err.message }, 500)) * .build(); * ``` */ export class LifecycleBuilder { private store: LifecycleStore = createLifecycleStore(); /** * μš”μ²­ μ‹œμž‘ ν›… μΆ”κ°€ */ onRequest(fn: OnRequestHandler, scope: HookScope = "local"): this { this.store.onRequest.push({ fn, scope }); return this; } /** * λ°”λ”” νŒŒμ‹± ν›… μΆ”κ°€ */ onParse(fn: OnParseHandler, scope: HookScope = "local"): this { this.store.onParse.push({ fn, scope }); return this; } /** * ν•Έλ“€λŸ¬ μ „ ν›… μΆ”κ°€ (Guard μ—­ν• ) */ beforeHandle(fn: BeforeHandleHandler, scope: HookScope = "local"): this { this.store.beforeHandle.push({ fn, scope }); return this; } /** * ν•Έλ“€λŸ¬ ν›„ ν›… μΆ”κ°€ */ afterHandle(fn: AfterHandleHandler, scope: HookScope = "local"): this { this.store.afterHandle.push({ fn, scope }); return this; } /** * 응닡 λ§€ν•‘ ν›… μΆ”κ°€ */ mapResponse(fn: MapResponseHandler, scope: HookScope = "local"): this { this.store.mapResponse.push({ fn, scope }); return this; } /** * 응닡 ν›„ ν›… μΆ”κ°€ */ afterResponse(fn: AfterResponseHandler, scope: HookScope = "local"): this { this.store.afterResponse.push({ fn, scope }); return this; } /** * μ—λŸ¬ 핸듀링 ν›… μΆ”κ°€ */ onError(fn: OnErrorHandler, scope: HookScope = "local"): this { this.store.onError.push({ fn, scope }); return this; } /** * 라이프사이클 μŠ€ν† μ–΄ λΉŒλ“œ */ build(): LifecycleStore { return { ...this.store }; } /** * λ‹€λ₯Έ 라이프사이클과 병합 */ merge(other: LifecycleStore): this { this.store.onRequest.push(...other.onRequest); this.store.onParse.push(...other.onParse); this.store.beforeHandle.push(...other.beforeHandle); this.store.afterHandle.push(...other.afterHandle); this.store.mapResponse.push(...other.mapResponse); this.store.afterResponse.push(...other.afterResponse); this.store.onError.push(...other.onError); return this; } } // ============================================ // μœ ν‹Έλ¦¬ν‹° // ============================================ /** * ν›… 쀑볡 제거 (checksum 기반) */ export function deduplicateHooks(hooks: T[]): T[] { const seen = new Set(); return hooks.filter((hook) => { if (hook.checksum === undefined) return true; if (seen.has(hook.checksum)) return false; seen.add(hook.checksum); return true; }); } /** * μŠ€μ½”ν”„λ³„ ν›… 필터링 */ export function filterHooksByScope( hooks: T[], scopes: HookScope[] ): T[] { return hooks.filter((hook) => scopes.includes(hook.scope)); }