@faapi/faapi 6.19.0 → 6.21.0

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/dist/testing.js CHANGED
@@ -154,17 +154,24 @@ function encodeSseEvent(event) {
154
154
  out += "\n";
155
155
  return out;
156
156
  }
157
- function createSseWriter() {
157
+ function createSseWriter(options = {}) {
158
+ const { onFirstWrite } = options;
158
159
  const encoder = new TextEncoder();
159
160
  let controller = null;
160
161
  let closed = false;
161
162
  let aborted = false;
163
+ let firstWriteTriggered = false;
162
164
  let drainWaiters = [];
163
165
  const resolveDrainWaiters = () => {
164
166
  const waiters = drainWaiters;
165
167
  drainWaiters = [];
166
168
  for (const resolve of waiters) resolve();
167
169
  };
170
+ const triggerFirstWrite = () => {
171
+ if (firstWriteTriggered) return;
172
+ firstWriteTriggered = true;
173
+ onFirstWrite?.(response);
174
+ };
168
175
  const stream = new ReadableStream(
169
176
  {
170
177
  start(c) {
@@ -198,11 +205,13 @@ function createSseWriter() {
198
205
  if (closed || !controller) return;
199
206
  const text = encodeSseEvent(event);
200
207
  controller.enqueue(encoder.encode(text));
208
+ triggerFirstWrite();
201
209
  },
202
210
  sendRaw(chunk) {
203
211
  if (closed || !controller) return;
204
212
  const bytes = typeof chunk === "string" ? encoder.encode(chunk) : chunk;
205
213
  controller.enqueue(bytes);
214
+ triggerFirstWrite();
206
215
  },
207
216
  sendError(error) {
208
217
  if (closed || !controller) return;
@@ -210,6 +219,7 @@ function createSseWriter() {
210
219
  const text = encodeSseEvent({ event: "error", data: message });
211
220
  try {
212
221
  controller.enqueue(encoder.encode(text));
222
+ triggerFirstWrite();
213
223
  } finally {
214
224
  writer.close();
215
225
  }
@@ -497,6 +507,26 @@ function createLogger(scope, options) {
497
507
  return makeLogger(void 0);
498
508
  }
499
509
 
510
+ // src/utils/queryToObject.ts
511
+ var queryCache = /* @__PURE__ */ new WeakMap();
512
+ function queryToObject(params) {
513
+ const cached = queryCache.get(params);
514
+ if (cached) return cached;
515
+ const result = {};
516
+ for (const [key, value] of params) {
517
+ const existing = result[key];
518
+ if (existing === void 0) {
519
+ result[key] = value;
520
+ } else if (Array.isArray(existing)) {
521
+ existing.push(value);
522
+ } else {
523
+ result[key] = [existing, value];
524
+ }
525
+ }
526
+ queryCache.set(params, result);
527
+ return result;
528
+ }
529
+
500
530
  // src/runtime/createContext.ts
501
531
  function resolveRequestId(request) {
502
532
  const header = request.headers.get("x-request-id");
@@ -546,7 +576,11 @@ function createContextFromUrl(request, url, params, config = {}, ip = "", regist
546
576
  log,
547
577
  request,
548
578
  params,
549
- query: url.searchParams,
579
+ rawParams: params,
580
+ // ctx.query 为对象口径(createContext 挂原始 string 值对象;handler 声明
581
+ // query 类型时管线校验后替换为转换值)。原始 URLSearchParams 恒在 rawQuery。
582
+ query: queryToObject(url.searchParams),
583
+ rawQuery: url.searchParams,
550
584
  headers: request.headers,
551
585
  method: request.method,
552
586
  path: url.pathname,
@@ -594,13 +628,19 @@ function createContextFromUrl(request, url, params, config = {}, ip = "", regist
594
628
  /**
595
629
  * 创建 SSE writer,用于流式推送事件
596
630
  *
597
- * handler 调用此方法后,通过返回的 writer 推送事件,框架自动把 writer.response
598
- * 作为 HTTP 响应(Content-Type: text/event-stream)。
631
+ * handler 调用此方法后,通过返回的 writer 推送事件。HTTP 服务场景下首次
632
+ * 写入(send/sendRaw/sendError)即触发 server 层提前接管钩子,把 writer.response
633
+ * 接入底层连接——流式期间字节即产即达(见 createServer.md「SSE 提前接管」)。
634
+ * 无钩子场景(app.inject / 测试直调 invokeHandler)回落为 handler 返回后交出。
599
635
  *
600
636
  * 与 ctx.json / ctx.html 互斥:一个 handler 只能用一种响应方式。
601
637
  */
602
638
  sse() {
603
- const writer = createSseWriter();
639
+ const writer = createSseWriter({
640
+ onFirstWrite: (response) => {
641
+ ctx.__earlyRespond?.(response);
642
+ }
643
+ });
604
644
  const ctxWithSse = ctx;
605
645
  ctxWithSse.__sseResponse = writer.response;
606
646
  ctxWithSse.__sseWriter = writer;
@@ -825,8 +865,16 @@ var PARAM_TYPE_MAP = {
825
865
  // Phase 2.3
826
866
  tasks: "tasks",
827
867
  // 任务子系统:TaskClient(入队/查询)
828
- log: "log"
868
+ log: "log",
829
869
  // 请求级日志器(ctx.log 同一实例:scope http,自动带 requestId 等字段)
870
+ // raw 系:恒原始输入(无 schema、无校验、管线永不转换),与 query/params/body
871
+ // 的「校验转换后」口径二分互补
872
+ rawQuery: "rawQuery",
873
+ // URLSearchParams(原始查询参数)
874
+ rawParams: "rawParams",
875
+ // Record<string, string>(URL 原始路径段)
876
+ rawBody: "rawBody"
877
+ // string | undefined(请求体原始文本)
830
878
  };
831
879
  var injectionCache = /* @__PURE__ */ new WeakMap();
832
880
  function resolveInjection(fn) {
@@ -904,26 +952,6 @@ function extractParamName(param, names) {
904
952
  }
905
953
  }
906
954
 
907
- // src/utils/queryToObject.ts
908
- var queryCache = /* @__PURE__ */ new WeakMap();
909
- function queryToObject(params) {
910
- const cached = queryCache.get(params);
911
- if (cached) return cached;
912
- const result = {};
913
- for (const [key, value] of params) {
914
- const existing = result[key];
915
- if (existing === void 0) {
916
- result[key] = value;
917
- } else if (Array.isArray(existing)) {
918
- existing.push(value);
919
- } else {
920
- result[key] = [existing, value];
921
- }
922
- }
923
- queryCache.set(params, result);
924
- return result;
925
- }
926
-
927
955
  // src/task/taskRegistry.ts
928
956
  function createTaskRegistry() {
929
957
  let registry = /* @__PURE__ */ new Map();
@@ -1108,12 +1136,17 @@ function getAgentHandle(ctx) {
1108
1136
  // src/injection/injectParams.ts
1109
1137
  function getBuiltinInjectionValue(type, ctx, body) {
1110
1138
  switch (type) {
1111
- case "query": {
1112
- const validated = ctx.__validatedQuery;
1113
- return validated ?? queryToObject(ctx.query);
1114
- }
1139
+ case "query":
1140
+ return ctx.query;
1115
1141
  case "params":
1116
1142
  return ctx.params;
1143
+ // raw 系:恒原始,管线永不覆盖(ctx 字段即注入值)
1144
+ case "rawQuery":
1145
+ return ctx.rawQuery;
1146
+ case "rawParams":
1147
+ return ctx.rawParams;
1148
+ case "rawBody":
1149
+ return ctx.rawBody;
1117
1150
  case "headers":
1118
1151
  return ctx.headers;
1119
1152
  case "context":
@@ -2629,7 +2662,19 @@ function extractSchema(typeNode, sourceFile) {
2629
2662
 
2630
2663
  // src/cli/collectRouteSchemaSources.ts
2631
2664
  import path4 from "path";
2632
- function collectRouteSchemaSources(routes, rootDir) {
2665
+ import ts6 from "typescript";
2666
+ function findExportedTypeName(sourceFile, name) {
2667
+ for (const statement of sourceFile.statements) {
2668
+ const modifiers = ts6.canHaveModifiers(statement) ? ts6.getModifiers(statement) : void 0;
2669
+ const isExported = modifiers?.some((m) => m.kind === ts6.SyntaxKind.ExportKeyword);
2670
+ if (!isExported) continue;
2671
+ if ((ts6.isInterfaceDeclaration(statement) || ts6.isTypeAliasDeclaration(statement)) && statement.name?.text === name) {
2672
+ return true;
2673
+ }
2674
+ }
2675
+ return false;
2676
+ }
2677
+ function collectRouteSchemaSources(routes, rootDir, wsRoutes) {
2633
2678
  const methodsByFile = /* @__PURE__ */ new Map();
2634
2679
  for (const route of routes) {
2635
2680
  const filePath = rootDir ? path4.resolve(rootDir, route.filePath) : route.filePath;
@@ -2640,9 +2685,17 @@ function collectRouteSchemaSources(routes, rootDir) {
2640
2685
  }
2641
2686
  entry.methods.add(route.method);
2642
2687
  }
2643
- const programByFile = createPrograms([...methodsByFile.keys()]);
2688
+ const wsFiles = [];
2689
+ for (const wsRoute of wsRoutes ?? []) {
2690
+ wsFiles.push({
2691
+ filePath: rootDir ? path4.resolve(rootDir, wsRoute.filePath) : wsRoute.filePath,
2692
+ urlPath: wsRoute.urlPath
2693
+ });
2694
+ }
2695
+ const allFiles = [.../* @__PURE__ */ new Set([...methodsByFile.keys(), ...wsFiles.map((f) => f.filePath)])];
2696
+ const programByFile = createPrograms(allFiles);
2644
2697
  const resolversByFile = /* @__PURE__ */ new Map();
2645
- for (const filePath of methodsByFile.keys()) {
2698
+ for (const filePath of allFiles) {
2646
2699
  resolversByFile.set(filePath, createLazyTypeResolver(programByFile.get(filePath), filePath));
2647
2700
  }
2648
2701
  const sources = [];
@@ -2674,6 +2727,50 @@ function collectRouteSchemaSources(routes, rootDir) {
2674
2727
  typeInfo: resolver.resolve(paramsArg.typeName) ?? null
2675
2728
  });
2676
2729
  }
2730
+ if (inputType === "body") {
2731
+ const queryArg = meta.params.find((p) => p.type === "query");
2732
+ if (queryArg?.typeName) {
2733
+ sources.push({
2734
+ urlPath: entry.urlPath,
2735
+ filePath,
2736
+ schemaName: getSchemaName(method, "query"),
2737
+ typeInfo: resolver.resolve(queryArg.typeName) ?? null
2738
+ });
2739
+ }
2740
+ } else if (hasBody(method)) {
2741
+ const bodyArg = meta.params.find((p) => p.type === "body") ?? meta.params.find((p) => p.type === "form");
2742
+ if (bodyArg?.typeName) {
2743
+ sources.push({
2744
+ urlPath: entry.urlPath,
2745
+ filePath,
2746
+ schemaName: getSchemaName(method, "body"),
2747
+ typeInfo: resolver.resolve(bodyArg.typeName) ?? null,
2748
+ coerce: bodyArg.type === "form" || void 0
2749
+ });
2750
+ }
2751
+ }
2752
+ }
2753
+ }
2754
+ for (const wsFile of wsFiles) {
2755
+ const program = programByFile.get(wsFile.filePath);
2756
+ const sourceFile = program?.getSourceFile(wsFile.filePath);
2757
+ if (!sourceFile) continue;
2758
+ const resolver = resolversByFile.get(wsFile.filePath);
2759
+ if (findExportedTypeName(sourceFile, "Query")) {
2760
+ sources.push({
2761
+ urlPath: wsFile.urlPath,
2762
+ filePath: wsFile.filePath,
2763
+ schemaName: getSchemaName("WS", "query"),
2764
+ typeInfo: resolver.resolve("Query") ?? null
2765
+ });
2766
+ }
2767
+ if (findExportedTypeName(sourceFile, "Params")) {
2768
+ sources.push({
2769
+ urlPath: wsFile.urlPath,
2770
+ filePath: wsFile.filePath,
2771
+ schemaName: getSchemaName("WS", "params"),
2772
+ typeInfo: resolver.resolve("Params") ?? null
2773
+ });
2677
2774
  }
2678
2775
  }
2679
2776
  return { sources, resolversByFile };
@@ -3106,9 +3203,9 @@ function generateSchemaFileSource(sources, resolveType, helpersImportPath) {
3106
3203
  lines.push(...schemaBlocks);
3107
3204
  return lines.join("\n").replace(/\n+$/, "\n");
3108
3205
  }
3109
- async function generateSchemaFiles(routes, rootDir, dist) {
3110
- if (routes.length === 0) return;
3111
- const { sources, resolversByFile } = collectRouteSchemaSources(routes, rootDir);
3206
+ async function generateSchemaFiles(routes, rootDir, dist, wsRoutes) {
3207
+ if (routes.length === 0 && (wsRoutes?.length ?? 0) === 0) return;
3208
+ const { sources, resolversByFile } = collectRouteSchemaSources(routes, rootDir, wsRoutes);
3112
3209
  await generateZodArtifacts(sources, {
3113
3210
  generateFileSource: generateSchemaFileSource,
3114
3211
  resolversByFile,
@@ -3127,7 +3224,7 @@ import fs8 from "fs";
3127
3224
  import fg2 from "fast-glob";
3128
3225
 
3129
3226
  // src/cli/aliasPlugin.ts
3130
- import ts7 from "typescript";
3227
+ import ts8 from "typescript";
3131
3228
  import path9 from "path";
3132
3229
  import fs7 from "fs";
3133
3230
 
@@ -3155,7 +3252,7 @@ function resolveAlias(specifier, config) {
3155
3252
  }
3156
3253
 
3157
3254
  // src/utils/readTsconfig.ts
3158
- import ts6 from "typescript";
3255
+ import ts7 from "typescript";
3159
3256
  import path8 from "path";
3160
3257
  import fs6 from "fs";
3161
3258
  var tsconfigCache = /* @__PURE__ */ new Map();
@@ -3172,9 +3269,9 @@ function readTsconfig(rootDir) {
3172
3269
  if (cached && cached.mtimeMs === mtimeMs) {
3173
3270
  return cached.config;
3174
3271
  }
3175
- const configFile = ts6.readConfigFile(tsconfigPath, ts6.sys.readFile);
3272
+ const configFile = ts7.readConfigFile(tsconfigPath, ts7.sys.readFile);
3176
3273
  if (configFile.error || !configFile.config) return null;
3177
- const parsed = ts6.parseJsonConfigFileContent(configFile.config, ts6.sys, rootDir);
3274
+ const parsed = ts7.parseJsonConfigFileContent(configFile.config, ts7.sys, rootDir);
3178
3275
  const baseUrl = parsed.options.baseUrl ?? rootDir;
3179
3276
  const rawPaths = parsed.options.paths;
3180
3277
  const config = rawPaths ? (() => {
@@ -3274,23 +3371,23 @@ function createAliasPlugin(config, options) {
3274
3371
  }
3275
3372
  const importer = args.path;
3276
3373
  const importerOutsideAppDir = appDirAbs ? !isInsideDir(importer, appDirAbs) : false;
3277
- const sourceFile = ts7.createSourceFile(importer, source, ts7.ScriptTarget.Latest, false);
3374
+ const sourceFile = ts8.createSourceFile(importer, source, ts8.ScriptTarget.Latest, false);
3278
3375
  const literals = [];
3279
3376
  const visit = (node) => {
3280
- if (ts7.isImportDeclaration(node)) {
3377
+ if (ts8.isImportDeclaration(node)) {
3281
3378
  if (!node.importClause?.isTypeOnly && node.moduleSpecifier) {
3282
- if (ts7.isStringLiteral(node.moduleSpecifier)) literals.push(node.moduleSpecifier);
3379
+ if (ts8.isStringLiteral(node.moduleSpecifier)) literals.push(node.moduleSpecifier);
3283
3380
  }
3284
- } else if (ts7.isExportDeclaration(node)) {
3381
+ } else if (ts8.isExportDeclaration(node)) {
3285
3382
  if (!node.isTypeOnly && node.moduleSpecifier) {
3286
- if (ts7.isStringLiteral(node.moduleSpecifier)) literals.push(node.moduleSpecifier);
3383
+ if (ts8.isStringLiteral(node.moduleSpecifier)) literals.push(node.moduleSpecifier);
3287
3384
  }
3288
- } else if (ts7.isCallExpression(node) && node.expression.kind === ts7.SyntaxKind.ImportKeyword && node.arguments[0] && ts7.isStringLiteral(node.arguments[0])) {
3385
+ } else if (ts8.isCallExpression(node) && node.expression.kind === ts8.SyntaxKind.ImportKeyword && node.arguments[0] && ts8.isStringLiteral(node.arguments[0])) {
3289
3386
  literals.push(node.arguments[0]);
3290
3387
  }
3291
- ts7.forEachChild(node, visit);
3388
+ ts8.forEachChild(node, visit);
3292
3389
  };
3293
- ts7.forEachChild(sourceFile, visit);
3390
+ ts8.forEachChild(sourceFile, visit);
3294
3391
  const replacements = [];
3295
3392
  const unresolvable = [];
3296
3393
  for (const lit of literals) {
@@ -3585,7 +3682,7 @@ function prodSourcePathToProductPath(sourceAbsPath, rootDir, dist) {
3585
3682
  const jsRel = relWithoutSrc.replace(/\.ts$/, ".js");
3586
3683
  return path12.resolve(rootDir, dist, jsRel);
3587
3684
  }
3588
- async function ensureSchemaGenerated(schemaPath, routeFilePath, routes, rootDir, dist) {
3685
+ async function ensureSchemaGenerated(schemaPath, routeFilePath, routes, rootDir, dist, wsRoutes) {
3589
3686
  const inFlight2 = state.inFlightSchemaGenerations.get(schemaPath);
3590
3687
  if (inFlight2) {
3591
3688
  await inFlight2;
@@ -3604,13 +3701,15 @@ async function ensureSchemaGenerated(schemaPath, routeFilePath, routes, rootDir,
3604
3701
  return false;
3605
3702
  }
3606
3703
  const fileRoutes = routes.filter((r) => r.filePath === routeFilePath);
3607
- if (fileRoutes.length === 0) {
3704
+ const fileWsRoutes = (wsRoutes ?? []).filter((r) => r.filePath === routeFilePath);
3705
+ if (fileRoutes.length === 0 && fileWsRoutes.length === 0) {
3608
3706
  return false;
3609
3707
  }
3610
3708
  const sourceRelPath = path12.relative(rootDir, sourceAbsPath).replace(/\\/g, "/");
3611
3709
  const sourceRoutes = fileRoutes.map((r) => ({ ...r, filePath: sourceRelPath }));
3710
+ const sourceWsRoutes = fileWsRoutes.map((r) => ({ ...r, filePath: sourceRelPath }));
3612
3711
  const generatePromise = (async () => {
3613
- await generateSchemaFiles(sourceRoutes, rootDir, dist);
3712
+ await generateSchemaFiles(sourceRoutes, rootDir, dist, sourceWsRoutes);
3614
3713
  state.generatedSchemas.add(schemaPath);
3615
3714
  })();
3616
3715
  state.inFlightSchemaGenerations.set(schemaPath, generatePromise);
@@ -4008,46 +4107,29 @@ async function parseMultipart(request) {
4008
4107
  async function resolveInputFromUrl(method, request, url) {
4009
4108
  const inputType = getInputTypeForMethod(method);
4010
4109
  if (inputType === "body") {
4011
- const contentType = request.headers.get("content-type") ?? "";
4012
- if (contentType.includes("multipart/form-data")) {
4013
- return parseMultipart(request);
4014
- }
4015
- if (contentType.includes("application/x-www-form-urlencoded")) {
4016
- const text2 = await request.text();
4017
- if (isBlankText(text2)) return null;
4018
- const params = new URLSearchParams(text2);
4019
- const obj = {};
4020
- for (const [key, value] of params) {
4021
- obj[key] = value;
4022
- }
4023
- return obj;
4024
- }
4025
- const text = await request.text();
4026
- if (isBlankText(text)) {
4027
- return null;
4028
- }
4029
- const result = parseJsonBody(text);
4030
- if (!result.success) {
4031
- throw new ValidationError("\u8BF7\u6C42\u4F53\u4E0D\u662F\u5408\u6CD5\u7684 JSON", [
4032
- {
4033
- path: "body",
4034
- code: "INVALID_FORMAT",
4035
- expected: "JSON",
4036
- received: "text",
4037
- message: "\u8BF7\u6C42\u4F53\u4E0D\u662F\u5408\u6CD5\u7684 JSON"
4038
- }
4039
- ]);
4040
- }
4041
- return result.data;
4110
+ return parseBodyByContentType(request);
4042
4111
  }
4043
- return queryToObject(url.searchParams);
4044
- }
4045
- function isBlankText(text) {
4046
- return text.length === 0 || !/\S/.test(text);
4112
+ return { input: queryToObject(url.searchParams) };
4047
4113
  }
4048
- async function resolveBodyForQueryMethod(request) {
4114
+ async function parseBodyByContentType(request) {
4115
+ const contentType = request.headers.get("content-type") ?? "";
4116
+ if (contentType.includes("multipart/form-data")) {
4117
+ return { input: await parseMultipart(request) };
4118
+ }
4119
+ if (contentType.includes("application/x-www-form-urlencoded")) {
4120
+ const text2 = await request.text();
4121
+ if (isBlankText(text2)) return { input: null };
4122
+ const params = new URLSearchParams(text2);
4123
+ const obj = {};
4124
+ for (const [key, value] of params) {
4125
+ obj[key] = value;
4126
+ }
4127
+ return { input: obj, rawBody: text2 };
4128
+ }
4049
4129
  const text = await request.text();
4050
- if (isBlankText(text)) return void 0;
4130
+ if (isBlankText(text)) {
4131
+ return { input: null };
4132
+ }
4051
4133
  const result = parseJsonBody(text);
4052
4134
  if (!result.success) {
4053
4135
  throw new ValidationError("\u8BF7\u6C42\u4F53\u4E0D\u662F\u5408\u6CD5\u7684 JSON", [
@@ -4060,7 +4142,13 @@ async function resolveBodyForQueryMethod(request) {
4060
4142
  }
4061
4143
  ]);
4062
4144
  }
4063
- return result.data;
4145
+ return { input: result.data, rawBody: text };
4146
+ }
4147
+ function isBlankText(text) {
4148
+ return text.length === 0 || !/\S/.test(text);
4149
+ }
4150
+ async function resolveBodyForQueryMethod(request) {
4151
+ return parseBodyByContentType(request);
4064
4152
  }
4065
4153
 
4066
4154
  // src/response/sendNodeResponse.ts
@@ -4645,6 +4733,7 @@ function attachWebSocket(options) {
4645
4733
  server,
4646
4734
  routesRef,
4647
4735
  rootDir,
4736
+ dist,
4648
4737
  config,
4649
4738
  globalMiddlewares,
4650
4739
  trustedProxy = false,
@@ -4679,12 +4768,50 @@ function attachWebSocket(options) {
4679
4768
  const { route, params } = match;
4680
4769
  const headers = nodeHttpToWebHeaders(req);
4681
4770
  const host = req.headers.host ?? "localhost";
4682
- const url = `http://${host}${req.url ?? "/"}`;
4683
- const request = new Request(url, { method: "GET", headers });
4771
+ const fullUrl = new URL(`http://${host}${req.url ?? "/"}`);
4772
+ const request = new Request(fullUrl.toString(), { method: "GET", headers });
4684
4773
  const ctx = createContext(request, params, config, getClientIp(req, trustedProxy), registries);
4685
4774
  const meta = ctx.meta;
4686
4775
  let upgraded = false;
4687
4776
  const finalHandler = async () => {
4777
+ const schemaPath = getRuntimeSchemaPath(route.filePath, dist, rootDir);
4778
+ if (fs11.existsSync(schemaPath)) {
4779
+ if (isDevOnDemandEnabled()) {
4780
+ const devDist = getDevDist();
4781
+ if (devDist) {
4782
+ await ensureSchemaGenerated(
4783
+ schemaPath,
4784
+ route.filePath,
4785
+ routesRef.current,
4786
+ rootDir,
4787
+ dist,
4788
+ routesRef.wsCurrent
4789
+ );
4790
+ }
4791
+ }
4792
+ const searchParams = fullUrl.searchParams;
4793
+ const rawQueryObject = queryToObject(searchParams);
4794
+ const paramsResult = await validateInput(schemaPath, "WS", "params", params);
4795
+ if (!paramsResult.valid) {
4796
+ throw new ValidationError("\u53C2\u6570\u6821\u9A8C\u5931\u8D25", paramsResult.issues);
4797
+ }
4798
+ if (paramsResult.data !== params && typeof paramsResult.data === "object" && paramsResult.data !== null) {
4799
+ ctx.params = {
4800
+ ...params,
4801
+ ...paramsResult.data
4802
+ };
4803
+ }
4804
+ const queryResult = await validateInput(schemaPath, "WS", "query", rawQueryObject);
4805
+ if (!queryResult.valid) {
4806
+ throw new ValidationError("\u53C2\u6570\u6821\u9A8C\u5931\u8D25", queryResult.issues);
4807
+ }
4808
+ if (queryResult.data !== rawQueryObject && typeof queryResult.data === "object" && queryResult.data !== null) {
4809
+ ctx.query = {
4810
+ ...rawQueryObject,
4811
+ ...queryResult.data
4812
+ };
4813
+ }
4814
+ }
4688
4815
  let handlers;
4689
4816
  try {
4690
4817
  const absoluteFilePath = path13.resolve(rootDir, route.filePath);
@@ -4891,7 +5018,8 @@ function createServer(options) {
4891
5018
  globalInjectors,
4892
5019
  bodyLimit,
4893
5020
  trustedProxy,
4894
- registries
5021
+ registries,
5022
+ routesRef.wsCurrent
4895
5023
  ).catch((err) => {
4896
5024
  console.error("[faapi] Request pipeline failed after error response attempt:", err);
4897
5025
  try {
@@ -4909,6 +5037,7 @@ function createServer(options) {
4909
5037
  server,
4910
5038
  routesRef,
4911
5039
  rootDir,
5040
+ dist,
4912
5041
  config,
4913
5042
  globalMiddlewares,
4914
5043
  trustedProxy,
@@ -4916,14 +5045,13 @@ function createServer(options) {
4916
5045
  });
4917
5046
  return { server, routesRef };
4918
5047
  }
4919
- function prepareRequest(req, config, bodyLimit, trustedProxy, registries, requestSignal) {
4920
- const { request, url } = toWebRequest(req, bodyLimit, requestSignal);
5048
+ function prepareRequest(req, request, url, config, trustedProxy, registries, preMatched) {
4921
5049
  const method = request.method.toUpperCase();
4922
5050
  const urlPath = url.pathname;
4923
5051
  const ctx = createContextFromUrl(
4924
5052
  request,
4925
5053
  url,
4926
- {},
5054
+ preMatched?.params ?? {},
4927
5055
  config,
4928
5056
  getClientIp(req, trustedProxy),
4929
5057
  registries
@@ -4954,19 +5082,33 @@ function getRoutePaths(route, rootDir, dist) {
4954
5082
  }
4955
5083
  var mergedInjectorsCache = /* @__PURE__ */ new WeakMap();
4956
5084
  function createRoutePipeline(opts) {
4957
- const { routes, method, urlPath, url, ctx, request, rootDir, dist, globalInjectors } = opts;
5085
+ const {
5086
+ routes,
5087
+ method,
5088
+ urlPath,
5089
+ url,
5090
+ ctx,
5091
+ request,
5092
+ rootDir,
5093
+ dist,
5094
+ globalInjectors,
5095
+ preMatched,
5096
+ wsRoutes
5097
+ } = opts;
4958
5098
  return async () => {
4959
- const match = resolveRouteOrThrow(routes, method, urlPath);
4960
- ctx.params = match.params;
5099
+ const match = preMatched ?? resolveRouteOrThrow(routes, method, urlPath);
4961
5100
  const { route } = match;
4962
5101
  const { absFilePath, schemaPath } = getRoutePaths(route, rootDir, dist);
4963
5102
  const routeModule = await loadRouteModule(absFilePath, route.method, rootDir);
4964
- const input = await resolveInputFromUrl(route.method, request, url);
5103
+ const { input, rawBody } = await resolveInputFromUrl(route.method, request, url);
5104
+ if (rawBody !== void 0) {
5105
+ ctx.rawBody = rawBody;
5106
+ }
4965
5107
  const inputType = getInputTypeForMethod(route.method);
4966
5108
  if (isDevOnDemandEnabled()) {
4967
5109
  const devDist = getDevDist();
4968
5110
  if (devDist) {
4969
- await ensureSchemaGenerated(schemaPath, route.filePath, routes, rootDir, dist);
5111
+ await ensureSchemaGenerated(schemaPath, route.filePath, routes, rootDir, dist, wsRoutes);
4970
5112
  }
4971
5113
  }
4972
5114
  const result = await validateInput(schemaPath, route.method, inputType, input);
@@ -4974,7 +5116,7 @@ function createRoutePipeline(opts) {
4974
5116
  throw new ValidationError("\u53C2\u6570\u6821\u9A8C\u5931\u8D25", result.issues);
4975
5117
  }
4976
5118
  if (inputType === "query" && result.data !== input && typeof result.data === "object" && result.data !== null) {
4977
- ctx.__validatedQuery = {
5119
+ ctx.query = {
4978
5120
  ...input,
4979
5121
  ...result.data
4980
5122
  };
@@ -4989,7 +5131,36 @@ function createRoutePipeline(opts) {
4989
5131
  ...paramsResult.data
4990
5132
  } : match.params;
4991
5133
  }
4992
- const body = inputType === "query" && hasBody(route.method) ? await resolveBodyForQueryMethod(request) : hasBody(route.method) ? result.data : void 0;
5134
+ if (inputType === "body") {
5135
+ const rawQuery = queryToObject(url.searchParams);
5136
+ const queryResult = await validateInput(schemaPath, route.method, "query", rawQuery);
5137
+ if (!queryResult.valid) {
5138
+ throw new ValidationError("\u53C2\u6570\u6821\u9A8C\u5931\u8D25", queryResult.issues);
5139
+ }
5140
+ if (queryResult.data !== rawQuery && typeof queryResult.data === "object" && queryResult.data !== null) {
5141
+ ctx.query = {
5142
+ ...rawQuery,
5143
+ ...queryResult.data
5144
+ };
5145
+ }
5146
+ }
5147
+ let body;
5148
+ if (inputType === "query" && hasBody(route.method)) {
5149
+ const parsed = await resolveBodyForQueryMethod(request);
5150
+ if (parsed.rawBody !== void 0) {
5151
+ ctx.rawBody = parsed.rawBody;
5152
+ }
5153
+ const bodyResult = await validateInput(schemaPath, route.method, "body", parsed.input);
5154
+ if (!bodyResult.valid) {
5155
+ throw new ValidationError("\u53C2\u6570\u6821\u9A8C\u5931\u8D25", bodyResult.issues);
5156
+ }
5157
+ body = bodyResult.data;
5158
+ } else if (hasBody(route.method)) {
5159
+ body = result.data;
5160
+ }
5161
+ if (body !== void 0 && body !== null) {
5162
+ ctx.body = body;
5163
+ }
4993
5164
  if (route.middlewares === void 0 && route.injectors === void 0 && route.middlewarePaths) {
4994
5165
  await ensureMiddlewaresCompiled(route.middlewarePaths, rootDir);
4995
5166
  const bundle = await loadMergedMiddlewares(route.middlewarePaths);
@@ -5025,25 +5196,37 @@ async function sendErrorResponse(err, meta, res, onError, ctx) {
5025
5196
  }
5026
5197
  }
5027
5198
  }
5028
- async function handleRequest(routes, rootDir, dist, req, res, outerMiddlewares, onError, config, globalInjectors, bodyLimit, trustedProxy, registries) {
5199
+ async function handleRequest(routes, rootDir, dist, req, res, outerMiddlewares, onError, config, globalInjectors, bodyLimit, trustedProxy, registries, wsRoutes) {
5029
5200
  let meta = { headers: {}, setCookies: [] };
5030
5201
  let ctx;
5202
+ let sseEarlySent = false;
5031
5203
  const abortController = new AbortController();
5032
5204
  res.on("close", () => {
5033
5205
  if (!res.writableEnded) abortController.abort();
5034
5206
  });
5035
5207
  try {
5208
+ const { request, url } = toWebRequest(req, bodyLimit, abortController.signal);
5209
+ const method = request.method.toUpperCase();
5210
+ const urlPath = url.pathname;
5211
+ const preMatched = matchRoute(routes, method, urlPath);
5036
5212
  const prepared = prepareRequest(
5037
5213
  req,
5214
+ request,
5215
+ url,
5038
5216
  config,
5039
- bodyLimit,
5040
5217
  trustedProxy,
5041
5218
  registries,
5042
- abortController.signal
5219
+ preMatched
5043
5220
  );
5044
5221
  ctx = prepared.ctx;
5045
5222
  meta = prepared.meta;
5046
- const { request, url, method, urlPath } = prepared;
5223
+ ctx.__earlyRespond = (response2) => {
5224
+ if (sseEarlySent) return;
5225
+ sseEarlySent = true;
5226
+ void sendNodeResponse(mergeMeta(response2, meta), res).catch(() => {
5227
+ if (!res.writableEnded) res.destroy();
5228
+ });
5229
+ };
5047
5230
  const routePipeline = createRoutePipeline({
5048
5231
  routes,
5049
5232
  method,
@@ -5053,12 +5236,25 @@ async function handleRequest(routes, rootDir, dist, req, res, outerMiddlewares,
5053
5236
  request,
5054
5237
  rootDir,
5055
5238
  dist,
5056
- globalInjectors
5239
+ globalInjectors,
5240
+ preMatched,
5241
+ wsRoutes
5057
5242
  });
5058
5243
  const response = outerMiddlewares.length > 0 ? await compose(outerMiddlewares, ctx, routePipeline) : await routePipeline();
5059
- await sendSuccessResponse(response, res);
5244
+ if (!sseEarlySent) {
5245
+ await sendSuccessResponse(response, res);
5246
+ }
5060
5247
  } catch (err) {
5061
5248
  if (res.destroyed || res.writableEnded) return;
5249
+ if (sseEarlySent) {
5250
+ if (onError && ctx) {
5251
+ try {
5252
+ await onError(err, ctx);
5253
+ } catch {
5254
+ }
5255
+ }
5256
+ return;
5257
+ }
5062
5258
  if (err instanceof PayloadTooLargeError) {
5063
5259
  res.setHeader("Connection", "close");
5064
5260
  res.once("finish", () => req.destroy());