@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.
- package/README.ko.md +200 -200
- package/README.md +200 -200
- package/package.json +52 -52
- package/src/bundler/build.ts +6 -0
- package/src/client/Link.tsx +209 -209
- package/src/client/hooks.ts +267 -267
- package/src/client/index.ts +2 -1
- package/src/client/router.ts +387 -387
- package/src/client/serialize.ts +404 -404
- package/src/contract/client.test.ts +308 -0
- package/src/contract/client.ts +345 -0
- package/src/contract/handler.ts +270 -0
- package/src/contract/index.ts +137 -1
- package/src/contract/infer.test.ts +346 -0
- package/src/contract/types.ts +83 -0
- package/src/filling/auth.ts +308 -308
- package/src/filling/context.ts +438 -438
- package/src/filling/filling.ts +5 -1
- package/src/filling/index.ts +1 -1
- package/src/generator/index.ts +3 -3
- package/src/index.ts +75 -0
- package/src/report/index.ts +1 -1
- package/src/runtime/compose.ts +222 -222
- package/src/runtime/index.ts +3 -3
- package/src/runtime/lifecycle.ts +381 -381
- package/src/runtime/ssr.ts +321 -321
- package/src/runtime/trace.ts +144 -144
- package/src/spec/index.ts +3 -3
- package/src/spec/load.ts +76 -76
- package/src/spec/lock.ts +56 -56
package/src/runtime/lifecycle.ts
CHANGED
|
@@ -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
|
+
}
|