@mandujs/core 0.9.2 β†’ 0.9.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,381 +1,381 @@
1
- /**
2
- * Mandu Lifecycle Hooks πŸ”„
3
- * Elysia μŠ€νƒ€μΌ 라이프사이클 ν›… 체계
4
- *
5
- * @see https://elysiajs.com/life-cycle/overview.html
6
- *
7
- * μš”μ²­ 흐름:
8
- * 1. onRequest - μš”μ²­ μ‹œμž‘
9
- * 2. onParse - λ°”λ”” νŒŒμ‹± (POST, PUT, PATCH)
10
- * 3. beforeHandle - ν•Έλ“€λŸ¬ μ „ (Guard μ—­ν• )
11
- * 4. [Handler] - 메인 ν•Έλ“€λŸ¬ μ‹€ν–‰
12
- * 5. afterHandle - ν•Έλ“€λŸ¬ ν›„ (응닡 λ³€ν™˜)
13
- * 6. mapResponse - 응닡 λ§€ν•‘
14
- * 7. afterResponse - 응닡 ν›„ (λ‘œκΉ…, 정리)
15
- *
16
- * μ—λŸ¬ λ°œμƒ μ‹œ:
17
- * - onError - μ—λŸ¬ 핸듀링
18
- */
19
-
20
- import type { ManduContext } from "../filling/context";
21
- import { createTracer } from "./trace";
22
-
23
- /**
24
- * ν›… μŠ€μ½”ν”„
25
- * - global: λͺ¨λ“  λΌμš°νŠΈμ— 적용
26
- * - scoped: ν˜„μž¬ ν”ŒλŸ¬κ·ΈμΈ/라우트 그룹에 적용
27
- * - local: ν˜„μž¬ λΌμš°νŠΈμ—λ§Œ 적용
28
- */
29
- export type HookScope = "global" | "scoped" | "local";
30
-
31
- /**
32
- * ν›… μ»¨ν…Œμ΄λ„ˆ
33
- */
34
- export interface HookContainer<T extends Function = Function> {
35
- fn: T;
36
- scope: HookScope;
37
- name?: string;
38
- checksum?: number; // 쀑볡 제거용
39
- }
40
-
41
- // ============================================
42
- // ν›… νƒ€μž… μ •μ˜
43
- // ============================================
44
-
45
- /** μš”μ²­ μ‹œμž‘ ν›… */
46
- export type OnRequestHandler = (ctx: ManduContext) => void | Promise<void>;
47
-
48
- /** λ°”λ”” νŒŒμ‹± ν›… */
49
- export type OnParseHandler = (ctx: ManduContext) => void | Promise<void>;
50
-
51
- /** ν•Έλ“€λŸ¬ μ „ ν›… (Guard μ—­ν• ) - Response λ°˜ν™˜ μ‹œ 체인 쀑단 */
52
- export type BeforeHandleHandler = (
53
- ctx: ManduContext
54
- ) => Response | void | Promise<Response | void>;
55
-
56
- /** ν•Έλ“€λŸ¬ ν›„ ν›… - 응닡 λ³€ν™˜ κ°€λŠ₯ */
57
- export type AfterHandleHandler = (
58
- ctx: ManduContext,
59
- response: Response
60
- ) => Response | Promise<Response>;
61
-
62
- /** 응닡 λ§€ν•‘ ν›… */
63
- export type MapResponseHandler = (
64
- ctx: ManduContext,
65
- response: Response
66
- ) => Response | Promise<Response>;
67
-
68
- /** 응닡 ν›„ ν›… (비동기, 응닡에 영ν–₯ μ—†μŒ) */
69
- export type AfterResponseHandler = (ctx: ManduContext) => void | Promise<void>;
70
-
71
- /** μ—λŸ¬ 핸듀링 ν›… - Response λ°˜ν™˜ μ‹œ μ—λŸ¬ μ‘λ‹΅μœΌλ‘œ μ‚¬μš© */
72
- export type OnErrorHandler = (
73
- ctx: ManduContext,
74
- error: Error
75
- ) => Response | void | Promise<Response | void>;
76
-
77
- // ============================================
78
- // 라이프사이클 μŠ€ν† μ–΄
79
- // ============================================
80
-
81
- /**
82
- * 라이프사이클 ν›… μŠ€ν† μ–΄
83
- */
84
- export interface LifecycleStore {
85
- onRequest: HookContainer<OnRequestHandler>[];
86
- onParse: HookContainer<OnParseHandler>[];
87
- beforeHandle: HookContainer<BeforeHandleHandler>[];
88
- afterHandle: HookContainer<AfterHandleHandler>[];
89
- mapResponse: HookContainer<MapResponseHandler>[];
90
- afterResponse: HookContainer<AfterResponseHandler>[];
91
- onError: HookContainer<OnErrorHandler>[];
92
- }
93
-
94
- /**
95
- * 빈 라이프사이클 μŠ€ν† μ–΄ 생성
96
- */
97
- export function createLifecycleStore(): LifecycleStore {
98
- return {
99
- onRequest: [],
100
- onParse: [],
101
- beforeHandle: [],
102
- afterHandle: [],
103
- mapResponse: [],
104
- afterResponse: [],
105
- onError: [],
106
- };
107
- }
108
-
109
- // ============================================
110
- // 라이프사이클 μ‹€ν–‰
111
- // ============================================
112
-
113
- /**
114
- * 라이프사이클 μ‹€ν–‰ μ˜΅μ…˜
115
- */
116
- export interface ExecuteOptions {
117
- /** λ°”λ”” νŒŒμ‹±μ΄ ν•„μš”ν•œ λ©”μ„œλ“œ */
118
- parseBodyMethods?: string[];
119
- /** 트레이슀 ν™œμ„±ν™” */
120
- trace?: boolean;
121
- }
122
-
123
- const DEFAULT_PARSE_BODY_METHODS = ["POST", "PUT", "PATCH"];
124
-
125
- /**
126
- * 라이프사이클 μ‹€ν–‰
127
- *
128
- * @param lifecycle 라이프사이클 μŠ€ν† μ–΄
129
- * @param ctx ManduContext
130
- * @param handler 메인 ν•Έλ“€λŸ¬
131
- * @param options μ˜΅μ…˜
132
- *
133
- * @example
134
- * ```typescript
135
- * const lifecycle = createLifecycleStore();
136
- * lifecycle.onRequest.push({ fn: (ctx) => console.log('Request started'), scope: 'local' });
137
- * lifecycle.beforeHandle.push({ fn: authGuard, scope: 'local' });
138
- *
139
- * const response = await executeLifecycle(
140
- * lifecycle,
141
- * ctx,
142
- * async () => ctx.ok({ data: 'hello' })
143
- * );
144
- * ```
145
- */
146
- export async function executeLifecycle(
147
- lifecycle: LifecycleStore,
148
- ctx: ManduContext,
149
- handler: () => Promise<Response>,
150
- options: ExecuteOptions = {}
151
- ): Promise<Response> {
152
- const { parseBodyMethods = DEFAULT_PARSE_BODY_METHODS } = options;
153
- const tracer = createTracer(ctx, options.trace);
154
- let response: Response;
155
-
156
- try {
157
- // 1. onRequest
158
- const endRequest = tracer.begin("request");
159
- for (const hook of lifecycle.onRequest) {
160
- await hook.fn(ctx);
161
- }
162
- endRequest();
163
-
164
- // 2. onParse (λ°”λ””κ°€ μžˆλŠ” λ©”μ„œλ“œλ§Œ)
165
- if (parseBodyMethods.includes(ctx.req.method)) {
166
- const endParse = tracer.begin("parse");
167
- for (const hook of lifecycle.onParse) {
168
- await hook.fn(ctx);
169
- }
170
- endParse();
171
- }
172
-
173
- // 3. beforeHandle (Guard μ—­ν• )
174
- const endBefore = tracer.begin("beforeHandle");
175
- for (const hook of lifecycle.beforeHandle) {
176
- const result = await hook.fn(ctx);
177
- if (result instanceof Response) {
178
- // Response λ°˜ν™˜ μ‹œ 체인 쀑단, afterHandle/mapResponse κ±΄λ„ˆλœ€
179
- response = result;
180
- endBefore();
181
- // afterResponseλŠ” μ‹€ν–‰
182
- scheduleAfterResponse(lifecycle.afterResponse, ctx, tracer);
183
- return response;
184
- }
185
- }
186
- endBefore();
187
-
188
- // 4. 메인 ν•Έλ“€λŸ¬ μ‹€ν–‰
189
- const endHandle = tracer.begin("handle");
190
- response = await handler();
191
- endHandle();
192
-
193
- // 5. afterHandle
194
- const endAfter = tracer.begin("afterHandle");
195
- for (const hook of lifecycle.afterHandle) {
196
- response = await hook.fn(ctx, response);
197
- }
198
- endAfter();
199
-
200
- // 6. mapResponse
201
- const endMap = tracer.begin("mapResponse");
202
- for (const hook of lifecycle.mapResponse) {
203
- response = await hook.fn(ctx, response);
204
- }
205
- endMap();
206
-
207
- // 7. afterResponse (비동기)
208
- scheduleAfterResponse(lifecycle.afterResponse, ctx, tracer);
209
-
210
- return response;
211
- } catch (err) {
212
- // onError 처리
213
- const error = err instanceof Error ? err : new Error(String(err));
214
- tracer.error("error", error);
215
-
216
- for (const hook of lifecycle.onError) {
217
- const result = await hook.fn(ctx, error);
218
- if (result instanceof Response) {
219
- // afterResponseλŠ” μ—λŸ¬ μ‹œμ—λ„ μ‹€ν–‰
220
- scheduleAfterResponse(lifecycle.afterResponse, ctx, tracer);
221
- return result;
222
- }
223
- }
224
-
225
- // μ—λŸ¬ ν•Έλ“€λŸ¬κ°€ Responseλ₯Ό λ°˜ν™˜ν•˜μ§€ μ•ŠμœΌλ©΄ 재throw
226
- throw error;
227
- }
228
- }
229
-
230
- /**
231
- * afterResponse ν›… 비동기 μ‹€ν–‰ (응닡 ν›„)
232
- */
233
- function scheduleAfterResponse(
234
- hooks: HookContainer<AfterResponseHandler>[],
235
- ctx: ManduContext,
236
- tracer?: ReturnType<typeof createTracer>
237
- ): void {
238
- if (hooks.length === 0) return;
239
-
240
- // queueMicrotask둜 응닡 ν›„ μ‹€ν–‰
241
- queueMicrotask(async () => {
242
- const endAfterResponse = tracer?.begin("afterResponse") ?? (() => {});
243
- for (const hook of hooks) {
244
- try {
245
- await hook.fn(ctx);
246
- } catch (err) {
247
- console.error("[Mandu] afterResponse hook error:", err);
248
- }
249
- }
250
- endAfterResponse();
251
- });
252
- }
253
-
254
- // ============================================
255
- // 라이프사이클 λΉŒλ”
256
- // ============================================
257
-
258
- /**
259
- * 라이프사이클 λΉŒλ”
260
- *
261
- * @example
262
- * ```typescript
263
- * const lifecycle = new LifecycleBuilder()
264
- * .onRequest((ctx) => console.log('Request:', ctx.req.url))
265
- * .beforeHandle(authGuard)
266
- * .afterHandle((ctx, res) => {
267
- * // 응닡 헀더 μΆ”κ°€
268
- * res.headers.set('X-Custom', 'value');
269
- * return res;
270
- * })
271
- * .onError((ctx, err) => ctx.json({ error: err.message }, 500))
272
- * .build();
273
- * ```
274
- */
275
- export class LifecycleBuilder {
276
- private store: LifecycleStore = createLifecycleStore();
277
-
278
- /**
279
- * μš”μ²­ μ‹œμž‘ ν›… μΆ”κ°€
280
- */
281
- onRequest(fn: OnRequestHandler, scope: HookScope = "local"): this {
282
- this.store.onRequest.push({ fn, scope });
283
- return this;
284
- }
285
-
286
- /**
287
- * λ°”λ”” νŒŒμ‹± ν›… μΆ”κ°€
288
- */
289
- onParse(fn: OnParseHandler, scope: HookScope = "local"): this {
290
- this.store.onParse.push({ fn, scope });
291
- return this;
292
- }
293
-
294
- /**
295
- * ν•Έλ“€λŸ¬ μ „ ν›… μΆ”κ°€ (Guard μ—­ν• )
296
- */
297
- beforeHandle(fn: BeforeHandleHandler, scope: HookScope = "local"): this {
298
- this.store.beforeHandle.push({ fn, scope });
299
- return this;
300
- }
301
-
302
- /**
303
- * ν•Έλ“€λŸ¬ ν›„ ν›… μΆ”κ°€
304
- */
305
- afterHandle(fn: AfterHandleHandler, scope: HookScope = "local"): this {
306
- this.store.afterHandle.push({ fn, scope });
307
- return this;
308
- }
309
-
310
- /**
311
- * 응닡 λ§€ν•‘ ν›… μΆ”κ°€
312
- */
313
- mapResponse(fn: MapResponseHandler, scope: HookScope = "local"): this {
314
- this.store.mapResponse.push({ fn, scope });
315
- return this;
316
- }
317
-
318
- /**
319
- * 응닡 ν›„ ν›… μΆ”κ°€
320
- */
321
- afterResponse(fn: AfterResponseHandler, scope: HookScope = "local"): this {
322
- this.store.afterResponse.push({ fn, scope });
323
- return this;
324
- }
325
-
326
- /**
327
- * μ—λŸ¬ 핸듀링 ν›… μΆ”κ°€
328
- */
329
- onError(fn: OnErrorHandler, scope: HookScope = "local"): this {
330
- this.store.onError.push({ fn, scope });
331
- return this;
332
- }
333
-
334
- /**
335
- * 라이프사이클 μŠ€ν† μ–΄ λΉŒλ“œ
336
- */
337
- build(): LifecycleStore {
338
- return { ...this.store };
339
- }
340
-
341
- /**
342
- * λ‹€λ₯Έ 라이프사이클과 병합
343
- */
344
- merge(other: LifecycleStore): this {
345
- this.store.onRequest.push(...other.onRequest);
346
- this.store.onParse.push(...other.onParse);
347
- this.store.beforeHandle.push(...other.beforeHandle);
348
- this.store.afterHandle.push(...other.afterHandle);
349
- this.store.mapResponse.push(...other.mapResponse);
350
- this.store.afterResponse.push(...other.afterResponse);
351
- this.store.onError.push(...other.onError);
352
- return this;
353
- }
354
- }
355
-
356
- // ============================================
357
- // μœ ν‹Έλ¦¬ν‹°
358
- // ============================================
359
-
360
- /**
361
- * ν›… 쀑볡 제거 (checksum 기반)
362
- */
363
- export function deduplicateHooks<T extends HookContainer>(hooks: T[]): T[] {
364
- const seen = new Set<number>();
365
- return hooks.filter((hook) => {
366
- if (hook.checksum === undefined) return true;
367
- if (seen.has(hook.checksum)) return false;
368
- seen.add(hook.checksum);
369
- return true;
370
- });
371
- }
372
-
373
- /**
374
- * μŠ€μ½”ν”„λ³„ ν›… 필터링
375
- */
376
- export function filterHooksByScope<T extends HookContainer>(
377
- hooks: T[],
378
- scopes: HookScope[]
379
- ): T[] {
380
- return hooks.filter((hook) => scopes.includes(hook.scope));
381
- }
1
+ /**
2
+ * Mandu Lifecycle Hooks πŸ”„
3
+ * Elysia μŠ€νƒ€μΌ 라이프사이클 ν›… 체계
4
+ *
5
+ * @see https://elysiajs.com/life-cycle/overview.html
6
+ *
7
+ * μš”μ²­ 흐름:
8
+ * 1. onRequest - μš”μ²­ μ‹œμž‘
9
+ * 2. onParse - λ°”λ”” νŒŒμ‹± (POST, PUT, PATCH)
10
+ * 3. beforeHandle - ν•Έλ“€λŸ¬ μ „ (Guard μ—­ν• )
11
+ * 4. [Handler] - 메인 ν•Έλ“€λŸ¬ μ‹€ν–‰
12
+ * 5. afterHandle - ν•Έλ“€λŸ¬ ν›„ (응닡 λ³€ν™˜)
13
+ * 6. mapResponse - 응닡 λ§€ν•‘
14
+ * 7. afterResponse - 응닡 ν›„ (λ‘œκΉ…, 정리)
15
+ *
16
+ * μ—λŸ¬ λ°œμƒ μ‹œ:
17
+ * - onError - μ—λŸ¬ 핸듀링
18
+ */
19
+
20
+ import type { ManduContext } from "../filling/context";
21
+ import { createTracer } from "./trace";
22
+
23
+ /**
24
+ * ν›… μŠ€μ½”ν”„
25
+ * - global: λͺ¨λ“  λΌμš°νŠΈμ— 적용
26
+ * - scoped: ν˜„μž¬ ν”ŒλŸ¬κ·ΈμΈ/라우트 그룹에 적용
27
+ * - local: ν˜„μž¬ λΌμš°νŠΈμ—λ§Œ 적용
28
+ */
29
+ export type HookScope = "global" | "scoped" | "local";
30
+
31
+ /**
32
+ * ν›… μ»¨ν…Œμ΄λ„ˆ
33
+ */
34
+ export interface HookContainer<T extends Function = Function> {
35
+ fn: T;
36
+ scope: HookScope;
37
+ name?: string;
38
+ checksum?: number; // 쀑볡 제거용
39
+ }
40
+
41
+ // ============================================
42
+ // ν›… νƒ€μž… μ •μ˜
43
+ // ============================================
44
+
45
+ /** μš”μ²­ μ‹œμž‘ ν›… */
46
+ export type OnRequestHandler = (ctx: ManduContext) => void | Promise<void>;
47
+
48
+ /** λ°”λ”” νŒŒμ‹± ν›… */
49
+ export type OnParseHandler = (ctx: ManduContext) => void | Promise<void>;
50
+
51
+ /** ν•Έλ“€λŸ¬ μ „ ν›… (Guard μ—­ν• ) - Response λ°˜ν™˜ μ‹œ 체인 쀑단 */
52
+ export type BeforeHandleHandler = (
53
+ ctx: ManduContext
54
+ ) => Response | void | Promise<Response | void>;
55
+
56
+ /** ν•Έλ“€λŸ¬ ν›„ ν›… - 응닡 λ³€ν™˜ κ°€λŠ₯ */
57
+ export type AfterHandleHandler = (
58
+ ctx: ManduContext,
59
+ response: Response
60
+ ) => Response | Promise<Response>;
61
+
62
+ /** 응닡 λ§€ν•‘ ν›… */
63
+ export type MapResponseHandler = (
64
+ ctx: ManduContext,
65
+ response: Response
66
+ ) => Response | Promise<Response>;
67
+
68
+ /** 응닡 ν›„ ν›… (비동기, 응닡에 영ν–₯ μ—†μŒ) */
69
+ export type AfterResponseHandler = (ctx: ManduContext) => void | Promise<void>;
70
+
71
+ /** μ—λŸ¬ 핸듀링 ν›… - Response λ°˜ν™˜ μ‹œ μ—λŸ¬ μ‘λ‹΅μœΌλ‘œ μ‚¬μš© */
72
+ export type OnErrorHandler = (
73
+ ctx: ManduContext,
74
+ error: Error
75
+ ) => Response | void | Promise<Response | void>;
76
+
77
+ // ============================================
78
+ // 라이프사이클 μŠ€ν† μ–΄
79
+ // ============================================
80
+
81
+ /**
82
+ * 라이프사이클 ν›… μŠ€ν† μ–΄
83
+ */
84
+ export interface LifecycleStore {
85
+ onRequest: HookContainer<OnRequestHandler>[];
86
+ onParse: HookContainer<OnParseHandler>[];
87
+ beforeHandle: HookContainer<BeforeHandleHandler>[];
88
+ afterHandle: HookContainer<AfterHandleHandler>[];
89
+ mapResponse: HookContainer<MapResponseHandler>[];
90
+ afterResponse: HookContainer<AfterResponseHandler>[];
91
+ onError: HookContainer<OnErrorHandler>[];
92
+ }
93
+
94
+ /**
95
+ * 빈 라이프사이클 μŠ€ν† μ–΄ 생성
96
+ */
97
+ export function createLifecycleStore(): LifecycleStore {
98
+ return {
99
+ onRequest: [],
100
+ onParse: [],
101
+ beforeHandle: [],
102
+ afterHandle: [],
103
+ mapResponse: [],
104
+ afterResponse: [],
105
+ onError: [],
106
+ };
107
+ }
108
+
109
+ // ============================================
110
+ // 라이프사이클 μ‹€ν–‰
111
+ // ============================================
112
+
113
+ /**
114
+ * 라이프사이클 μ‹€ν–‰ μ˜΅μ…˜
115
+ */
116
+ export interface ExecuteOptions {
117
+ /** λ°”λ”” νŒŒμ‹±μ΄ ν•„μš”ν•œ λ©”μ„œλ“œ */
118
+ parseBodyMethods?: string[];
119
+ /** 트레이슀 ν™œμ„±ν™” */
120
+ trace?: boolean;
121
+ }
122
+
123
+ const DEFAULT_PARSE_BODY_METHODS = ["POST", "PUT", "PATCH"];
124
+
125
+ /**
126
+ * 라이프사이클 μ‹€ν–‰
127
+ *
128
+ * @param lifecycle 라이프사이클 μŠ€ν† μ–΄
129
+ * @param ctx ManduContext
130
+ * @param handler 메인 ν•Έλ“€λŸ¬
131
+ * @param options μ˜΅μ…˜
132
+ *
133
+ * @example
134
+ * ```typescript
135
+ * const lifecycle = createLifecycleStore();
136
+ * lifecycle.onRequest.push({ fn: (ctx) => console.log('Request started'), scope: 'local' });
137
+ * lifecycle.beforeHandle.push({ fn: authGuard, scope: 'local' });
138
+ *
139
+ * const response = await executeLifecycle(
140
+ * lifecycle,
141
+ * ctx,
142
+ * async () => ctx.ok({ data: 'hello' })
143
+ * );
144
+ * ```
145
+ */
146
+ export async function executeLifecycle(
147
+ lifecycle: LifecycleStore,
148
+ ctx: ManduContext,
149
+ handler: () => Promise<Response>,
150
+ options: ExecuteOptions = {}
151
+ ): Promise<Response> {
152
+ const { parseBodyMethods = DEFAULT_PARSE_BODY_METHODS } = options;
153
+ const tracer = createTracer(ctx, options.trace);
154
+ let response: Response;
155
+
156
+ try {
157
+ // 1. onRequest
158
+ const endRequest = tracer.begin("request");
159
+ for (const hook of lifecycle.onRequest) {
160
+ await hook.fn(ctx);
161
+ }
162
+ endRequest();
163
+
164
+ // 2. onParse (λ°”λ””κ°€ μžˆλŠ” λ©”μ„œλ“œλ§Œ)
165
+ if (parseBodyMethods.includes(ctx.req.method)) {
166
+ const endParse = tracer.begin("parse");
167
+ for (const hook of lifecycle.onParse) {
168
+ await hook.fn(ctx);
169
+ }
170
+ endParse();
171
+ }
172
+
173
+ // 3. beforeHandle (Guard μ—­ν• )
174
+ const endBefore = tracer.begin("beforeHandle");
175
+ for (const hook of lifecycle.beforeHandle) {
176
+ const result = await hook.fn(ctx);
177
+ if (result instanceof Response) {
178
+ // Response λ°˜ν™˜ μ‹œ 체인 쀑단, afterHandle/mapResponse κ±΄λ„ˆλœ€
179
+ response = result;
180
+ endBefore();
181
+ // afterResponseλŠ” μ‹€ν–‰
182
+ scheduleAfterResponse(lifecycle.afterResponse, ctx, tracer);
183
+ return response;
184
+ }
185
+ }
186
+ endBefore();
187
+
188
+ // 4. 메인 ν•Έλ“€λŸ¬ μ‹€ν–‰
189
+ const endHandle = tracer.begin("handle");
190
+ response = await handler();
191
+ endHandle();
192
+
193
+ // 5. afterHandle
194
+ const endAfter = tracer.begin("afterHandle");
195
+ for (const hook of lifecycle.afterHandle) {
196
+ response = await hook.fn(ctx, response);
197
+ }
198
+ endAfter();
199
+
200
+ // 6. mapResponse
201
+ const endMap = tracer.begin("mapResponse");
202
+ for (const hook of lifecycle.mapResponse) {
203
+ response = await hook.fn(ctx, response);
204
+ }
205
+ endMap();
206
+
207
+ // 7. afterResponse (비동기)
208
+ scheduleAfterResponse(lifecycle.afterResponse, ctx, tracer);
209
+
210
+ return response;
211
+ } catch (err) {
212
+ // onError 처리
213
+ const error = err instanceof Error ? err : new Error(String(err));
214
+ tracer.error("error", error);
215
+
216
+ for (const hook of lifecycle.onError) {
217
+ const result = await hook.fn(ctx, error);
218
+ if (result instanceof Response) {
219
+ // afterResponseλŠ” μ—λŸ¬ μ‹œμ—λ„ μ‹€ν–‰
220
+ scheduleAfterResponse(lifecycle.afterResponse, ctx, tracer);
221
+ return result;
222
+ }
223
+ }
224
+
225
+ // μ—λŸ¬ ν•Έλ“€λŸ¬κ°€ Responseλ₯Ό λ°˜ν™˜ν•˜μ§€ μ•ŠμœΌλ©΄ 재throw
226
+ throw error;
227
+ }
228
+ }
229
+
230
+ /**
231
+ * afterResponse ν›… 비동기 μ‹€ν–‰ (응닡 ν›„)
232
+ */
233
+ function scheduleAfterResponse(
234
+ hooks: HookContainer<AfterResponseHandler>[],
235
+ ctx: ManduContext,
236
+ tracer?: ReturnType<typeof createTracer>
237
+ ): void {
238
+ if (hooks.length === 0) return;
239
+
240
+ // queueMicrotask둜 응닡 ν›„ μ‹€ν–‰
241
+ queueMicrotask(async () => {
242
+ const endAfterResponse = tracer?.begin("afterResponse") ?? (() => {});
243
+ for (const hook of hooks) {
244
+ try {
245
+ await hook.fn(ctx);
246
+ } catch (err) {
247
+ console.error("[Mandu] afterResponse hook error:", err);
248
+ }
249
+ }
250
+ endAfterResponse();
251
+ });
252
+ }
253
+
254
+ // ============================================
255
+ // 라이프사이클 λΉŒλ”
256
+ // ============================================
257
+
258
+ /**
259
+ * 라이프사이클 λΉŒλ”
260
+ *
261
+ * @example
262
+ * ```typescript
263
+ * const lifecycle = new LifecycleBuilder()
264
+ * .onRequest((ctx) => console.log('Request:', ctx.req.url))
265
+ * .beforeHandle(authGuard)
266
+ * .afterHandle((ctx, res) => {
267
+ * // 응닡 헀더 μΆ”κ°€
268
+ * res.headers.set('X-Custom', 'value');
269
+ * return res;
270
+ * })
271
+ * .onError((ctx, err) => ctx.json({ error: err.message }, 500))
272
+ * .build();
273
+ * ```
274
+ */
275
+ export class LifecycleBuilder {
276
+ private store: LifecycleStore = createLifecycleStore();
277
+
278
+ /**
279
+ * μš”μ²­ μ‹œμž‘ ν›… μΆ”κ°€
280
+ */
281
+ onRequest(fn: OnRequestHandler, scope: HookScope = "local"): this {
282
+ this.store.onRequest.push({ fn, scope });
283
+ return this;
284
+ }
285
+
286
+ /**
287
+ * λ°”λ”” νŒŒμ‹± ν›… μΆ”κ°€
288
+ */
289
+ onParse(fn: OnParseHandler, scope: HookScope = "local"): this {
290
+ this.store.onParse.push({ fn, scope });
291
+ return this;
292
+ }
293
+
294
+ /**
295
+ * ν•Έλ“€λŸ¬ μ „ ν›… μΆ”κ°€ (Guard μ—­ν• )
296
+ */
297
+ beforeHandle(fn: BeforeHandleHandler, scope: HookScope = "local"): this {
298
+ this.store.beforeHandle.push({ fn, scope });
299
+ return this;
300
+ }
301
+
302
+ /**
303
+ * ν•Έλ“€λŸ¬ ν›„ ν›… μΆ”κ°€
304
+ */
305
+ afterHandle(fn: AfterHandleHandler, scope: HookScope = "local"): this {
306
+ this.store.afterHandle.push({ fn, scope });
307
+ return this;
308
+ }
309
+
310
+ /**
311
+ * 응닡 λ§€ν•‘ ν›… μΆ”κ°€
312
+ */
313
+ mapResponse(fn: MapResponseHandler, scope: HookScope = "local"): this {
314
+ this.store.mapResponse.push({ fn, scope });
315
+ return this;
316
+ }
317
+
318
+ /**
319
+ * 응닡 ν›„ ν›… μΆ”κ°€
320
+ */
321
+ afterResponse(fn: AfterResponseHandler, scope: HookScope = "local"): this {
322
+ this.store.afterResponse.push({ fn, scope });
323
+ return this;
324
+ }
325
+
326
+ /**
327
+ * μ—λŸ¬ 핸듀링 ν›… μΆ”κ°€
328
+ */
329
+ onError(fn: OnErrorHandler, scope: HookScope = "local"): this {
330
+ this.store.onError.push({ fn, scope });
331
+ return this;
332
+ }
333
+
334
+ /**
335
+ * 라이프사이클 μŠ€ν† μ–΄ λΉŒλ“œ
336
+ */
337
+ build(): LifecycleStore {
338
+ return { ...this.store };
339
+ }
340
+
341
+ /**
342
+ * λ‹€λ₯Έ 라이프사이클과 병합
343
+ */
344
+ merge(other: LifecycleStore): this {
345
+ this.store.onRequest.push(...other.onRequest);
346
+ this.store.onParse.push(...other.onParse);
347
+ this.store.beforeHandle.push(...other.beforeHandle);
348
+ this.store.afterHandle.push(...other.afterHandle);
349
+ this.store.mapResponse.push(...other.mapResponse);
350
+ this.store.afterResponse.push(...other.afterResponse);
351
+ this.store.onError.push(...other.onError);
352
+ return this;
353
+ }
354
+ }
355
+
356
+ // ============================================
357
+ // μœ ν‹Έλ¦¬ν‹°
358
+ // ============================================
359
+
360
+ /**
361
+ * ν›… 쀑볡 제거 (checksum 기반)
362
+ */
363
+ export function deduplicateHooks<T extends HookContainer>(hooks: T[]): T[] {
364
+ const seen = new Set<number>();
365
+ return hooks.filter((hook) => {
366
+ if (hook.checksum === undefined) return true;
367
+ if (seen.has(hook.checksum)) return false;
368
+ seen.add(hook.checksum);
369
+ return true;
370
+ });
371
+ }
372
+
373
+ /**
374
+ * μŠ€μ½”ν”„λ³„ ν›… 필터링
375
+ */
376
+ export function filterHooksByScope<T extends HookContainer>(
377
+ hooks: T[],
378
+ scopes: HookScope[]
379
+ ): T[] {
380
+ return hooks.filter((hook) => scopes.includes(hook.scope));
381
+ }