@faapi/faapi 6.20.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 = [];
@@ -2698,6 +2751,28 @@ function collectRouteSchemaSources(routes, rootDir) {
2698
2751
  }
2699
2752
  }
2700
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
+ });
2774
+ }
2775
+ }
2701
2776
  return { sources, resolversByFile };
2702
2777
  }
2703
2778
 
@@ -3128,9 +3203,9 @@ function generateSchemaFileSource(sources, resolveType, helpersImportPath) {
3128
3203
  lines.push(...schemaBlocks);
3129
3204
  return lines.join("\n").replace(/\n+$/, "\n");
3130
3205
  }
3131
- async function generateSchemaFiles(routes, rootDir, dist) {
3132
- if (routes.length === 0) return;
3133
- 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);
3134
3209
  await generateZodArtifacts(sources, {
3135
3210
  generateFileSource: generateSchemaFileSource,
3136
3211
  resolversByFile,
@@ -3149,7 +3224,7 @@ import fs8 from "fs";
3149
3224
  import fg2 from "fast-glob";
3150
3225
 
3151
3226
  // src/cli/aliasPlugin.ts
3152
- import ts7 from "typescript";
3227
+ import ts8 from "typescript";
3153
3228
  import path9 from "path";
3154
3229
  import fs7 from "fs";
3155
3230
 
@@ -3177,7 +3252,7 @@ function resolveAlias(specifier, config) {
3177
3252
  }
3178
3253
 
3179
3254
  // src/utils/readTsconfig.ts
3180
- import ts6 from "typescript";
3255
+ import ts7 from "typescript";
3181
3256
  import path8 from "path";
3182
3257
  import fs6 from "fs";
3183
3258
  var tsconfigCache = /* @__PURE__ */ new Map();
@@ -3194,9 +3269,9 @@ function readTsconfig(rootDir) {
3194
3269
  if (cached && cached.mtimeMs === mtimeMs) {
3195
3270
  return cached.config;
3196
3271
  }
3197
- const configFile = ts6.readConfigFile(tsconfigPath, ts6.sys.readFile);
3272
+ const configFile = ts7.readConfigFile(tsconfigPath, ts7.sys.readFile);
3198
3273
  if (configFile.error || !configFile.config) return null;
3199
- const parsed = ts6.parseJsonConfigFileContent(configFile.config, ts6.sys, rootDir);
3274
+ const parsed = ts7.parseJsonConfigFileContent(configFile.config, ts7.sys, rootDir);
3200
3275
  const baseUrl = parsed.options.baseUrl ?? rootDir;
3201
3276
  const rawPaths = parsed.options.paths;
3202
3277
  const config = rawPaths ? (() => {
@@ -3296,23 +3371,23 @@ function createAliasPlugin(config, options) {
3296
3371
  }
3297
3372
  const importer = args.path;
3298
3373
  const importerOutsideAppDir = appDirAbs ? !isInsideDir(importer, appDirAbs) : false;
3299
- const sourceFile = ts7.createSourceFile(importer, source, ts7.ScriptTarget.Latest, false);
3374
+ const sourceFile = ts8.createSourceFile(importer, source, ts8.ScriptTarget.Latest, false);
3300
3375
  const literals = [];
3301
3376
  const visit = (node) => {
3302
- if (ts7.isImportDeclaration(node)) {
3377
+ if (ts8.isImportDeclaration(node)) {
3303
3378
  if (!node.importClause?.isTypeOnly && node.moduleSpecifier) {
3304
- if (ts7.isStringLiteral(node.moduleSpecifier)) literals.push(node.moduleSpecifier);
3379
+ if (ts8.isStringLiteral(node.moduleSpecifier)) literals.push(node.moduleSpecifier);
3305
3380
  }
3306
- } else if (ts7.isExportDeclaration(node)) {
3381
+ } else if (ts8.isExportDeclaration(node)) {
3307
3382
  if (!node.isTypeOnly && node.moduleSpecifier) {
3308
- if (ts7.isStringLiteral(node.moduleSpecifier)) literals.push(node.moduleSpecifier);
3383
+ if (ts8.isStringLiteral(node.moduleSpecifier)) literals.push(node.moduleSpecifier);
3309
3384
  }
3310
- } 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])) {
3311
3386
  literals.push(node.arguments[0]);
3312
3387
  }
3313
- ts7.forEachChild(node, visit);
3388
+ ts8.forEachChild(node, visit);
3314
3389
  };
3315
- ts7.forEachChild(sourceFile, visit);
3390
+ ts8.forEachChild(sourceFile, visit);
3316
3391
  const replacements = [];
3317
3392
  const unresolvable = [];
3318
3393
  for (const lit of literals) {
@@ -3607,7 +3682,7 @@ function prodSourcePathToProductPath(sourceAbsPath, rootDir, dist) {
3607
3682
  const jsRel = relWithoutSrc.replace(/\.ts$/, ".js");
3608
3683
  return path12.resolve(rootDir, dist, jsRel);
3609
3684
  }
3610
- async function ensureSchemaGenerated(schemaPath, routeFilePath, routes, rootDir, dist) {
3685
+ async function ensureSchemaGenerated(schemaPath, routeFilePath, routes, rootDir, dist, wsRoutes) {
3611
3686
  const inFlight2 = state.inFlightSchemaGenerations.get(schemaPath);
3612
3687
  if (inFlight2) {
3613
3688
  await inFlight2;
@@ -3626,13 +3701,15 @@ async function ensureSchemaGenerated(schemaPath, routeFilePath, routes, rootDir,
3626
3701
  return false;
3627
3702
  }
3628
3703
  const fileRoutes = routes.filter((r) => r.filePath === routeFilePath);
3629
- if (fileRoutes.length === 0) {
3704
+ const fileWsRoutes = (wsRoutes ?? []).filter((r) => r.filePath === routeFilePath);
3705
+ if (fileRoutes.length === 0 && fileWsRoutes.length === 0) {
3630
3706
  return false;
3631
3707
  }
3632
3708
  const sourceRelPath = path12.relative(rootDir, sourceAbsPath).replace(/\\/g, "/");
3633
3709
  const sourceRoutes = fileRoutes.map((r) => ({ ...r, filePath: sourceRelPath }));
3710
+ const sourceWsRoutes = fileWsRoutes.map((r) => ({ ...r, filePath: sourceRelPath }));
3634
3711
  const generatePromise = (async () => {
3635
- await generateSchemaFiles(sourceRoutes, rootDir, dist);
3712
+ await generateSchemaFiles(sourceRoutes, rootDir, dist, sourceWsRoutes);
3636
3713
  state.generatedSchemas.add(schemaPath);
3637
3714
  })();
3638
3715
  state.inFlightSchemaGenerations.set(schemaPath, generatePromise);
@@ -4030,46 +4107,29 @@ async function parseMultipart(request) {
4030
4107
  async function resolveInputFromUrl(method, request, url) {
4031
4108
  const inputType = getInputTypeForMethod(method);
4032
4109
  if (inputType === "body") {
4033
- const contentType = request.headers.get("content-type") ?? "";
4034
- if (contentType.includes("multipart/form-data")) {
4035
- return parseMultipart(request);
4036
- }
4037
- if (contentType.includes("application/x-www-form-urlencoded")) {
4038
- const text2 = await request.text();
4039
- if (isBlankText(text2)) return null;
4040
- const params = new URLSearchParams(text2);
4041
- const obj = {};
4042
- for (const [key, value] of params) {
4043
- obj[key] = value;
4044
- }
4045
- return obj;
4046
- }
4047
- const text = await request.text();
4048
- if (isBlankText(text)) {
4049
- return null;
4050
- }
4051
- const result = parseJsonBody(text);
4052
- if (!result.success) {
4053
- throw new ValidationError("\u8BF7\u6C42\u4F53\u4E0D\u662F\u5408\u6CD5\u7684 JSON", [
4054
- {
4055
- path: "body",
4056
- code: "INVALID_FORMAT",
4057
- expected: "JSON",
4058
- received: "text",
4059
- message: "\u8BF7\u6C42\u4F53\u4E0D\u662F\u5408\u6CD5\u7684 JSON"
4060
- }
4061
- ]);
4062
- }
4063
- return result.data;
4110
+ return parseBodyByContentType(request);
4064
4111
  }
4065
- return queryToObject(url.searchParams);
4066
- }
4067
- function isBlankText(text) {
4068
- return text.length === 0 || !/\S/.test(text);
4112
+ return { input: queryToObject(url.searchParams) };
4069
4113
  }
4070
- 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
+ }
4071
4129
  const text = await request.text();
4072
- if (isBlankText(text)) return void 0;
4130
+ if (isBlankText(text)) {
4131
+ return { input: null };
4132
+ }
4073
4133
  const result = parseJsonBody(text);
4074
4134
  if (!result.success) {
4075
4135
  throw new ValidationError("\u8BF7\u6C42\u4F53\u4E0D\u662F\u5408\u6CD5\u7684 JSON", [
@@ -4082,7 +4142,13 @@ async function resolveBodyForQueryMethod(request) {
4082
4142
  }
4083
4143
  ]);
4084
4144
  }
4085
- 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);
4086
4152
  }
4087
4153
 
4088
4154
  // src/response/sendNodeResponse.ts
@@ -4667,6 +4733,7 @@ function attachWebSocket(options) {
4667
4733
  server,
4668
4734
  routesRef,
4669
4735
  rootDir,
4736
+ dist,
4670
4737
  config,
4671
4738
  globalMiddlewares,
4672
4739
  trustedProxy = false,
@@ -4701,12 +4768,50 @@ function attachWebSocket(options) {
4701
4768
  const { route, params } = match;
4702
4769
  const headers = nodeHttpToWebHeaders(req);
4703
4770
  const host = req.headers.host ?? "localhost";
4704
- const url = `http://${host}${req.url ?? "/"}`;
4705
- 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 });
4706
4773
  const ctx = createContext(request, params, config, getClientIp(req, trustedProxy), registries);
4707
4774
  const meta = ctx.meta;
4708
4775
  let upgraded = false;
4709
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
+ }
4710
4815
  let handlers;
4711
4816
  try {
4712
4817
  const absoluteFilePath = path13.resolve(rootDir, route.filePath);
@@ -4913,7 +5018,8 @@ function createServer(options) {
4913
5018
  globalInjectors,
4914
5019
  bodyLimit,
4915
5020
  trustedProxy,
4916
- registries
5021
+ registries,
5022
+ routesRef.wsCurrent
4917
5023
  ).catch((err) => {
4918
5024
  console.error("[faapi] Request pipeline failed after error response attempt:", err);
4919
5025
  try {
@@ -4931,6 +5037,7 @@ function createServer(options) {
4931
5037
  server,
4932
5038
  routesRef,
4933
5039
  rootDir,
5040
+ dist,
4934
5041
  config,
4935
5042
  globalMiddlewares,
4936
5043
  trustedProxy,
@@ -4938,14 +5045,13 @@ function createServer(options) {
4938
5045
  });
4939
5046
  return { server, routesRef };
4940
5047
  }
4941
- function prepareRequest(req, config, bodyLimit, trustedProxy, registries, requestSignal) {
4942
- const { request, url } = toWebRequest(req, bodyLimit, requestSignal);
5048
+ function prepareRequest(req, request, url, config, trustedProxy, registries, preMatched) {
4943
5049
  const method = request.method.toUpperCase();
4944
5050
  const urlPath = url.pathname;
4945
5051
  const ctx = createContextFromUrl(
4946
5052
  request,
4947
5053
  url,
4948
- {},
5054
+ preMatched?.params ?? {},
4949
5055
  config,
4950
5056
  getClientIp(req, trustedProxy),
4951
5057
  registries
@@ -4976,19 +5082,33 @@ function getRoutePaths(route, rootDir, dist) {
4976
5082
  }
4977
5083
  var mergedInjectorsCache = /* @__PURE__ */ new WeakMap();
4978
5084
  function createRoutePipeline(opts) {
4979
- 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;
4980
5098
  return async () => {
4981
- const match = resolveRouteOrThrow(routes, method, urlPath);
4982
- ctx.params = match.params;
5099
+ const match = preMatched ?? resolveRouteOrThrow(routes, method, urlPath);
4983
5100
  const { route } = match;
4984
5101
  const { absFilePath, schemaPath } = getRoutePaths(route, rootDir, dist);
4985
5102
  const routeModule = await loadRouteModule(absFilePath, route.method, rootDir);
4986
- 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
+ }
4987
5107
  const inputType = getInputTypeForMethod(route.method);
4988
5108
  if (isDevOnDemandEnabled()) {
4989
5109
  const devDist = getDevDist();
4990
5110
  if (devDist) {
4991
- await ensureSchemaGenerated(schemaPath, route.filePath, routes, rootDir, dist);
5111
+ await ensureSchemaGenerated(schemaPath, route.filePath, routes, rootDir, dist, wsRoutes);
4992
5112
  }
4993
5113
  }
4994
5114
  const result = await validateInput(schemaPath, route.method, inputType, input);
@@ -4996,7 +5116,7 @@ function createRoutePipeline(opts) {
4996
5116
  throw new ValidationError("\u53C2\u6570\u6821\u9A8C\u5931\u8D25", result.issues);
4997
5117
  }
4998
5118
  if (inputType === "query" && result.data !== input && typeof result.data === "object" && result.data !== null) {
4999
- ctx.__validatedQuery = {
5119
+ ctx.query = {
5000
5120
  ...input,
5001
5121
  ...result.data
5002
5122
  };
@@ -5018,7 +5138,7 @@ function createRoutePipeline(opts) {
5018
5138
  throw new ValidationError("\u53C2\u6570\u6821\u9A8C\u5931\u8D25", queryResult.issues);
5019
5139
  }
5020
5140
  if (queryResult.data !== rawQuery && typeof queryResult.data === "object" && queryResult.data !== null) {
5021
- ctx.__validatedQuery = {
5141
+ ctx.query = {
5022
5142
  ...rawQuery,
5023
5143
  ...queryResult.data
5024
5144
  };
@@ -5027,16 +5147,20 @@ function createRoutePipeline(opts) {
5027
5147
  let body;
5028
5148
  if (inputType === "query" && hasBody(route.method)) {
5029
5149
  const parsed = await resolveBodyForQueryMethod(request);
5030
- if (parsed !== void 0) {
5031
- const bodyResult = await validateInput(schemaPath, route.method, "body", parsed);
5032
- if (!bodyResult.valid) {
5033
- throw new ValidationError("\u53C2\u6570\u6821\u9A8C\u5931\u8D25", bodyResult.issues);
5034
- }
5035
- body = bodyResult.data;
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);
5036
5156
  }
5157
+ body = bodyResult.data;
5037
5158
  } else if (hasBody(route.method)) {
5038
5159
  body = result.data;
5039
5160
  }
5161
+ if (body !== void 0 && body !== null) {
5162
+ ctx.body = body;
5163
+ }
5040
5164
  if (route.middlewares === void 0 && route.injectors === void 0 && route.middlewarePaths) {
5041
5165
  await ensureMiddlewaresCompiled(route.middlewarePaths, rootDir);
5042
5166
  const bundle = await loadMergedMiddlewares(route.middlewarePaths);
@@ -5072,25 +5196,37 @@ async function sendErrorResponse(err, meta, res, onError, ctx) {
5072
5196
  }
5073
5197
  }
5074
5198
  }
5075
- 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) {
5076
5200
  let meta = { headers: {}, setCookies: [] };
5077
5201
  let ctx;
5202
+ let sseEarlySent = false;
5078
5203
  const abortController = new AbortController();
5079
5204
  res.on("close", () => {
5080
5205
  if (!res.writableEnded) abortController.abort();
5081
5206
  });
5082
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);
5083
5212
  const prepared = prepareRequest(
5084
5213
  req,
5214
+ request,
5215
+ url,
5085
5216
  config,
5086
- bodyLimit,
5087
5217
  trustedProxy,
5088
5218
  registries,
5089
- abortController.signal
5219
+ preMatched
5090
5220
  );
5091
5221
  ctx = prepared.ctx;
5092
5222
  meta = prepared.meta;
5093
- 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
+ };
5094
5230
  const routePipeline = createRoutePipeline({
5095
5231
  routes,
5096
5232
  method,
@@ -5100,12 +5236,25 @@ async function handleRequest(routes, rootDir, dist, req, res, outerMiddlewares,
5100
5236
  request,
5101
5237
  rootDir,
5102
5238
  dist,
5103
- globalInjectors
5239
+ globalInjectors,
5240
+ preMatched,
5241
+ wsRoutes
5104
5242
  });
5105
5243
  const response = outerMiddlewares.length > 0 ? await compose(outerMiddlewares, ctx, routePipeline) : await routePipeline();
5106
- await sendSuccessResponse(response, res);
5244
+ if (!sseEarlySent) {
5245
+ await sendSuccessResponse(response, res);
5246
+ }
5107
5247
  } catch (err) {
5108
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
+ }
5109
5258
  if (err instanceof PayloadTooLargeError) {
5110
5259
  res.setHeader("Connection", "close");
5111
5260
  res.once("finish", () => req.destroy());