@faapi/faapi 2.0.1 → 3.1.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
@@ -103,6 +103,144 @@ function createSseWriter() {
103
103
  return writer;
104
104
  }
105
105
 
106
+ // src/errors/FaapiError.ts
107
+ var FaapiError = class extends Error {
108
+ constructor(code, message, statusCode) {
109
+ super(message);
110
+ this.code = code;
111
+ this.statusCode = statusCode;
112
+ this.name = "FaapiError";
113
+ }
114
+ code;
115
+ statusCode;
116
+ };
117
+
118
+ // src/errors/httpErrors.ts
119
+ function deriveStatusCode(issues) {
120
+ const has400 = issues.some((i) => i.code === "INVALID_FORMAT" || i.code === "MISSING_FIELD");
121
+ return has400 ? 400 : 422;
122
+ }
123
+ var ValidationError = class extends FaapiError {
124
+ constructor(message, issues) {
125
+ super("VALIDATION_ERROR", message, deriveStatusCode(issues));
126
+ this.issues = issues;
127
+ this.name = "ValidationError";
128
+ }
129
+ issues;
130
+ };
131
+ var RouteNotFoundError = class extends FaapiError {
132
+ constructor(path11) {
133
+ super("ROUTE_NOT_FOUND", `Route not found: ${path11}`, 404);
134
+ this.name = "RouteNotFoundError";
135
+ }
136
+ };
137
+ var MethodNotAllowedError = class extends FaapiError {
138
+ constructor(method, path11, allowedMethods) {
139
+ super("METHOD_NOT_ALLOWED", `Method ${method} not allowed for ${path11}`, 405);
140
+ this.allowedMethods = allowedMethods;
141
+ this.name = "MethodNotAllowedError";
142
+ }
143
+ allowedMethods;
144
+ };
145
+ var InternalError = class extends FaapiError {
146
+ constructor(message) {
147
+ super("INTERNAL_ERROR", message, 500);
148
+ this.name = "InternalError";
149
+ }
150
+ };
151
+ var PayloadTooLargeError = class extends FaapiError {
152
+ constructor(maxSize) {
153
+ super("PAYLOAD_TOO_LARGE", `Request body exceeds size limit of ${maxSize} bytes`, 413);
154
+ this.name = "PayloadTooLargeError";
155
+ }
156
+ };
157
+
158
+ // src/response/responseFormatter.ts
159
+ function defaultOk(data) {
160
+ return { data };
161
+ }
162
+ function defaultFail(e) {
163
+ const error = { message: e.message };
164
+ if (e.code !== void 0) error.code = e.code;
165
+ return { error };
166
+ }
167
+ function getResponseConfig(config) {
168
+ return config?.response;
169
+ }
170
+ function resolveOkFn(config) {
171
+ return getResponseConfig(config)?.ok ?? defaultOk;
172
+ }
173
+ function resolveFailFn(config) {
174
+ return getResponseConfig(config)?.fail ?? defaultFail;
175
+ }
176
+ function jsonOk(body, status = 200, extraHeaders) {
177
+ return jsonRaw(body, status, extraHeaders);
178
+ }
179
+ function jsonRaw(body, status, extraHeaders) {
180
+ const headers = new Headers({ "Content-Type": "application/json" });
181
+ if (extraHeaders) {
182
+ const extra = new Headers(extraHeaders);
183
+ extra.forEach((value, key) => headers.set(key, value));
184
+ }
185
+ return new Response(JSON.stringify(body), { status, headers });
186
+ }
187
+ function wrapOkResult(result, config) {
188
+ if (result instanceof Response) return result;
189
+ return resolveOkFn(config)(result);
190
+ }
191
+ function formatFailResponse(options, config) {
192
+ const failFn = resolveFailFn(config);
193
+ const body = failFn({
194
+ status: options.status,
195
+ code: options.code,
196
+ message: options.message
197
+ });
198
+ return jsonOk(body, options.status ?? 500);
199
+ }
200
+ function formatErrorResponse(error, config) {
201
+ const failFn = resolveFailFn(config);
202
+ if (error instanceof ValidationError) {
203
+ const body2 = failFn({
204
+ status: error.statusCode,
205
+ code: error.code,
206
+ message: error.message
207
+ });
208
+ const bodyObj = typeof body2 === "object" && body2 !== null ? body2 : { error: body2 };
209
+ const errorObj = bodyObj.error ?? bodyObj;
210
+ if (errorObj) {
211
+ errorObj.issues = error.issues;
212
+ }
213
+ return jsonOk(bodyObj, error.statusCode);
214
+ }
215
+ if (error instanceof MethodNotAllowedError) {
216
+ const body2 = failFn({
217
+ status: error.statusCode,
218
+ code: error.code,
219
+ message: error.message
220
+ });
221
+ return jsonOk(body2, error.statusCode, { Allow: error.allowedMethods.join(", ") });
222
+ }
223
+ if (error instanceof PayloadTooLargeError) {
224
+ const body2 = failFn({
225
+ status: error.statusCode,
226
+ code: error.code,
227
+ message: error.message
228
+ });
229
+ return jsonOk(body2, error.statusCode);
230
+ }
231
+ if (error instanceof FaapiError) {
232
+ const body2 = failFn({
233
+ status: error.statusCode,
234
+ code: error.code,
235
+ message: error.message
236
+ });
237
+ return jsonOk(body2, error.statusCode);
238
+ }
239
+ const message = error instanceof Error ? error.message : "An unknown error occurred";
240
+ const body = failFn({ status: 500, code: "INTERNAL_ERROR", message });
241
+ return jsonOk(body, 500);
242
+ }
243
+
106
244
  // src/runtime/createContext.ts
107
245
  function parseCookies(cookieHeader) {
108
246
  const cookies = /* @__PURE__ */ new Map();
@@ -163,11 +301,7 @@ function createContext(request, params, config = {}, ip = "") {
163
301
  });
164
302
  },
165
303
  json(data, status) {
166
- const headers = { "Content-Type": "application/json" };
167
- return new Response(JSON.stringify(data), {
168
- status: status ?? 200,
169
- headers
170
- });
304
+ return jsonOk(data, status ?? 200);
171
305
  },
172
306
  html(html, status) {
173
307
  const headers = { "Content-Type": "text/html; charset=utf-8" };
@@ -203,18 +337,22 @@ function createContext(request, params, config = {}, ip = "") {
203
337
  /**
204
338
  * 显式包装成功响应(返回 Response,不会被自动包裹再次包装)
205
339
  *
340
+ * 实现委托给 [responseFormatter.wrapOkResult](../response/responseFormatter.ts),
341
+ * 与 handler `return data` 走的自动包裹路径共享同一套 ok 函数。
342
+ *
206
343
  * 用 config.response.ok(或默认 (data) => ({ data })) 包裹 data 并返回 JSON Response。
207
344
  * handler 也可直接 return data,框架会自动用 ok 包裹,两者等价。
208
345
  */
209
346
  ok(data) {
210
- const responseConfig = config.response;
211
- const okFn = responseConfig?.ok ?? ((d) => ({ data: d }));
212
- const body = okFn(data);
213
- return ctx.json(body);
347
+ const body = wrapOkResult(data, config);
348
+ return jsonOk(body, 200);
214
349
  },
215
350
  /**
216
351
  * 返回错误响应(对象形式参数,status 和 code 均可省略)
217
352
  *
353
+ * 实现委托给 [responseFormatter.formatFailResponse](../response/responseFormatter.ts),
354
+ * 与 formatErrorResponse(handler 抛错兜底)共享同一套 fail 函数,确保错误格式一致。
355
+ *
218
356
  * - status 省略时 HTTP 状态码默认 500
219
357
  * - code 省略时响应 body 里不含 code 字段(默认 fail 函数只放非 undefined 的字段)
220
358
  * - status 和 code 独立无关联
@@ -222,18 +360,7 @@ function createContext(request, params, config = {}, ip = "") {
222
360
  * body 用 config.response.fail(或默认实现)包装。
223
361
  */
224
362
  fail(options) {
225
- const responseConfig = config.response;
226
- const failFn = responseConfig?.fail ?? ((e) => {
227
- const error = { message: e.message };
228
- if (e.code !== void 0) error.code = e.code;
229
- return { error };
230
- });
231
- const body = failFn({
232
- status: options.status,
233
- code: options.code,
234
- message: options.message
235
- });
236
- return ctx.json(body, options.status ?? 500);
363
+ return formatFailResponse(options, config);
237
364
  }
238
365
  };
239
366
  const extend = config?.extendContext;
@@ -379,7 +506,11 @@ var PARAM_TYPE_MAP = {
379
506
  ip: "ip",
380
507
  ua: "ua",
381
508
  files: "files",
382
- fields: "fields"
509
+ fields: "fields",
510
+ agent: "agent",
511
+ // Phase 2.3
512
+ agents: "agents"
513
+ // Phase 2.3
383
514
  };
384
515
  function resolveInjection(fn) {
385
516
  const fnStr = fn.toString();
@@ -459,6 +590,28 @@ function queryToObject(params) {
459
590
  return result;
460
591
  }
461
592
 
593
+ // src/injection/skillRegistry.ts
594
+ var registry = /* @__PURE__ */ new Map();
595
+ function listSkills() {
596
+ return Array.from(registry.values());
597
+ }
598
+
599
+ // src/injection/agentRegistry.ts
600
+ var registry2 = /* @__PURE__ */ new Map();
601
+ function listAgents() {
602
+ const merged = /* @__PURE__ */ new Map();
603
+ for (const agent of registry2.values()) merged.set(agent.name, agent);
604
+ for (const skill of listSkills()) merged.set(skill.name, skill);
605
+ return Array.from(merged.values());
606
+ }
607
+
608
+ // src/injection/agentHandle.ts
609
+ var currentFactory = null;
610
+ function getAgentHandle(ctx) {
611
+ if (currentFactory === null) return void 0;
612
+ return currentFactory(ctx);
613
+ }
614
+
462
615
  // src/injection/injectParams.ts
463
616
  function getBuiltinInjectionValue(type, ctx, body) {
464
617
  switch (type) {
@@ -492,6 +645,12 @@ function getBuiltinInjectionValue(type, ctx, body) {
492
645
  return body.fields;
493
646
  }
494
647
  return {};
648
+ // Phase 2.3:注入所有已注册 agent 元数据列表
649
+ case "agents":
650
+ return listAgents();
651
+ // Phase 3.5:调 @faapi/agent 插件注册的工厂获取 AgentHandle
652
+ case "agent":
653
+ return getAgentHandle(ctx);
495
654
  default:
496
655
  return void 0;
497
656
  }
@@ -517,10 +676,7 @@ async function injectParamsAsync(handler, ctx, body, injectors) {
517
676
 
518
677
  // src/runtime/invokeHandler.ts
519
678
  function wrapResult(result, ctx) {
520
- if (result instanceof Response) return result;
521
- const responseConfig = ctx.config.response;
522
- const okFn = responseConfig?.ok ?? ((d) => ({ data: d }));
523
- return okFn(result);
679
+ return wrapOkResult(result, ctx.config);
524
680
  }
525
681
  function mergeMeta(response, meta) {
526
682
  const hasMeta = meta.status !== void 0 || Object.keys(meta.headers).length > 0 || meta.setCookies.length > 0;
@@ -703,37 +859,42 @@ function setCachedMiddlewares(absPath, bundle) {
703
859
  middlewareCache.set(absPath, bundle);
704
860
  }
705
861
  async function loadMiddlewaresFile(filePath) {
862
+ let module;
706
863
  try {
707
- const module = await importWithCacheBust(filePath);
708
- const middlewares = module.default ?? module.middlewares ?? [];
709
- if (!Array.isArray(middlewares)) {
710
- console.warn(`[faapi] middlewares.ts \u5E94\u5BFC\u51FA\u6570\u7EC4\uFF0C\u5DF2\u5FFD\u7565: ${filePath}`);
711
- return { middlewares: [], injectors: {} };
712
- }
713
- const validMiddlewares = middlewares.filter((m) => {
714
- if (typeof m !== "function") {
715
- console.warn(`[faapi] \u65E0\u6548\u7684\u4E2D\u95F4\u4EF6\u9879\uFF08\u5E94\u4E3A\u51FD\u6570\uFF09\uFF0C\u5DF2\u5FFD\u7565: ${typeof m}`);
716
- return false;
717
- }
718
- return true;
719
- });
720
- const injectors = module.injectors ?? {};
721
- if (typeof injectors !== "object" || injectors === null) {
722
- console.warn(`[faapi] injectors \u5E94\u5BFC\u51FA\u5BF9\u8C61\uFF0C\u5DF2\u5FFD\u7565: ${filePath}`);
723
- return { middlewares: validMiddlewares, injectors: {} };
724
- }
725
- const validInjectors = {};
726
- for (const [name, injector] of Object.entries(injectors)) {
727
- if (typeof injector !== "function") {
728
- console.warn(`[faapi] \u6CE8\u5165\u5668 ${name} \u5E94\u4E3A\u51FD\u6570\uFF0C\u5DF2\u5FFD\u7565`);
729
- continue;
730
- }
731
- validInjectors[name] = injector;
732
- }
733
- return { middlewares: validMiddlewares, injectors: validInjectors };
734
- } catch {
864
+ module = await importWithCacheBust(filePath);
865
+ } catch (err) {
866
+ console.error(
867
+ `[faapi] Failed to load middlewares from ${filePath}:`,
868
+ err instanceof Error ? err.stack ?? err.message : err
869
+ );
870
+ return { middlewares: [], injectors: {} };
871
+ }
872
+ const middlewares = module.default ?? module.middlewares ?? [];
873
+ if (!Array.isArray(middlewares)) {
874
+ console.warn(`[faapi] middlewares.ts \u5E94\u5BFC\u51FA\u6570\u7EC4\uFF0C\u5DF2\u5FFD\u7565: ${filePath}`);
735
875
  return { middlewares: [], injectors: {} };
736
876
  }
877
+ const validMiddlewares = middlewares.filter((m) => {
878
+ if (typeof m !== "function") {
879
+ console.warn(`[faapi] \u65E0\u6548\u7684\u4E2D\u95F4\u4EF6\u9879\uFF08\u5E94\u4E3A\u51FD\u6570\uFF09\uFF0C\u5DF2\u5FFD\u7565: ${typeof m}`);
880
+ return false;
881
+ }
882
+ return true;
883
+ });
884
+ const injectors = module.injectors ?? {};
885
+ if (typeof injectors !== "object" || injectors === null) {
886
+ console.warn(`[faapi] injectors \u5E94\u5BFC\u51FA\u5BF9\u8C61\uFF0C\u5DF2\u5FFD\u7565: ${filePath}`);
887
+ return { middlewares: validMiddlewares, injectors: {} };
888
+ }
889
+ const validInjectors = {};
890
+ for (const [name, injector] of Object.entries(injectors)) {
891
+ if (typeof injector !== "function") {
892
+ console.warn(`[faapi] \u6CE8\u5165\u5668 ${name} \u5E94\u4E3A\u51FD\u6570\uFF0C\u5DF2\u5FFD\u7565`);
893
+ continue;
894
+ }
895
+ validInjectors[name] = injector;
896
+ }
897
+ return { middlewares: validMiddlewares, injectors: validInjectors };
737
898
  }
738
899
  async function loadMergedMiddlewares(middlewarePaths) {
739
900
  if (middlewarePaths.length === 0) return void 0;
@@ -2049,52 +2210,6 @@ async function writeSchemaFile(outputPath, source) {
2049
2210
  await fs2.writeFile(outputPath, source, "utf-8");
2050
2211
  }
2051
2212
 
2052
- // src/errors/FaapiError.ts
2053
- var FaapiError = class extends Error {
2054
- constructor(code, message, statusCode) {
2055
- super(message);
2056
- this.code = code;
2057
- this.statusCode = statusCode;
2058
- this.name = "FaapiError";
2059
- }
2060
- code;
2061
- statusCode;
2062
- };
2063
-
2064
- // src/errors/httpErrors.ts
2065
- function deriveStatusCode(issues) {
2066
- const has400 = issues.some((i) => i.code === "INVALID_FORMAT" || i.code === "MISSING_FIELD");
2067
- return has400 ? 400 : 422;
2068
- }
2069
- var ValidationError = class extends FaapiError {
2070
- constructor(message, issues) {
2071
- super("VALIDATION_ERROR", message, deriveStatusCode(issues));
2072
- this.issues = issues;
2073
- this.name = "ValidationError";
2074
- }
2075
- issues;
2076
- };
2077
- var RouteNotFoundError = class extends FaapiError {
2078
- constructor(path11) {
2079
- super("ROUTE_NOT_FOUND", `Route not found: ${path11}`, 404);
2080
- this.name = "RouteNotFoundError";
2081
- }
2082
- };
2083
- var MethodNotAllowedError = class extends FaapiError {
2084
- constructor(method, path11, allowedMethods) {
2085
- super("METHOD_NOT_ALLOWED", `Method ${method} not allowed for ${path11}`, 405);
2086
- this.allowedMethods = allowedMethods;
2087
- this.name = "MethodNotAllowedError";
2088
- }
2089
- allowedMethods;
2090
- };
2091
- var InternalError = class extends FaapiError {
2092
- constructor(message) {
2093
- super("INTERNAL_ERROR", message, 500);
2094
- this.name = "InternalError";
2095
- }
2096
- };
2097
-
2098
2213
  // src/cli/compileOnDemand.ts
2099
2214
  import path7 from "path";
2100
2215
  import fs6 from "fs";
@@ -2348,9 +2463,25 @@ function isProductFresh(sourceAbsPath, productAbsPath) {
2348
2463
  return false;
2349
2464
  }
2350
2465
  }
2351
- var compiledFiles = /* @__PURE__ */ new Set();
2466
+ function createDevOnDemandState() {
2467
+ return {
2468
+ enabled: false,
2469
+ distDir: void 0,
2470
+ compiledFiles: /* @__PURE__ */ new Set(),
2471
+ generatedSchemas: /* @__PURE__ */ new Set(),
2472
+ inFlightCompilations: /* @__PURE__ */ new Map(),
2473
+ inFlightSchemaGenerations: /* @__PURE__ */ new Map()
2474
+ };
2475
+ }
2476
+ var state = createDevOnDemandState();
2352
2477
  async function ensureCompiled(sourceAbsPath, rootDir, dist) {
2353
- if (compiledFiles.has(sourceAbsPath)) {
2478
+ const inFlight = state.inFlightCompilations.get(sourceAbsPath);
2479
+ if (inFlight) {
2480
+ await inFlight.catch(() => {
2481
+ });
2482
+ return false;
2483
+ }
2484
+ if (state.compiledFiles.has(sourceAbsPath)) {
2354
2485
  return false;
2355
2486
  }
2356
2487
  if (!fs6.existsSync(sourceAbsPath)) {
@@ -2358,17 +2489,25 @@ async function ensureCompiled(sourceAbsPath, rootDir, dist) {
2358
2489
  }
2359
2490
  const productPath = prodSourcePathToProductPath(sourceAbsPath, rootDir, dist);
2360
2491
  if (productPath && isProductFresh(sourceAbsPath, productPath)) {
2361
- compiledFiles.add(sourceAbsPath);
2492
+ state.compiledFiles.add(sourceAbsPath);
2362
2493
  return false;
2363
2494
  }
2364
- await compileDevRoutes({
2365
- rootDir,
2366
- dist,
2367
- files: [sourceAbsPath],
2368
- logLevel: "silent"
2369
- });
2370
- compiledFiles.add(sourceAbsPath);
2371
- return true;
2495
+ const compilePromise = (async () => {
2496
+ await compileDevRoutes({
2497
+ rootDir,
2498
+ dist,
2499
+ files: [sourceAbsPath],
2500
+ logLevel: "silent"
2501
+ });
2502
+ state.compiledFiles.add(sourceAbsPath);
2503
+ })();
2504
+ state.inFlightCompilations.set(sourceAbsPath, compilePromise);
2505
+ try {
2506
+ await compilePromise;
2507
+ return true;
2508
+ } finally {
2509
+ state.inFlightCompilations.delete(sourceAbsPath);
2510
+ }
2372
2511
  }
2373
2512
  function prodSourcePathToProductPath(sourceAbsPath, rootDir, dist) {
2374
2513
  const rel = path7.relative(rootDir, sourceAbsPath).replace(/\\/g, "/");
@@ -2377,9 +2516,14 @@ function prodSourcePathToProductPath(sourceAbsPath, rootDir, dist) {
2377
2516
  const jsRel = relWithoutSrc.replace(/\.ts$/, ".js");
2378
2517
  return path7.resolve(rootDir, dist, jsRel);
2379
2518
  }
2380
- var generatedSchemas = /* @__PURE__ */ new Set();
2381
2519
  async function ensureSchemaGenerated(schemaPath, routeFilePath, routes, rootDir, dist) {
2382
- if (generatedSchemas.has(schemaPath)) {
2520
+ const inFlight = state.inFlightSchemaGenerations.get(schemaPath);
2521
+ if (inFlight) {
2522
+ await inFlight.catch(() => {
2523
+ });
2524
+ return false;
2525
+ }
2526
+ if (state.generatedSchemas.has(schemaPath)) {
2383
2527
  return false;
2384
2528
  }
2385
2529
  const prodAbsPath = path7.resolve(rootDir, routeFilePath);
@@ -2388,7 +2532,7 @@ async function ensureSchemaGenerated(schemaPath, routeFilePath, routes, rootDir,
2388
2532
  return false;
2389
2533
  }
2390
2534
  if (isProductFresh(sourceAbsPath, schemaPath)) {
2391
- generatedSchemas.add(schemaPath);
2535
+ state.generatedSchemas.add(schemaPath);
2392
2536
  return false;
2393
2537
  }
2394
2538
  const fileRoutes = routes.filter((r) => r.filePath === routeFilePath);
@@ -2397,9 +2541,17 @@ async function ensureSchemaGenerated(schemaPath, routeFilePath, routes, rootDir,
2397
2541
  }
2398
2542
  const sourceRelPath = path7.relative(rootDir, sourceAbsPath).replace(/\\/g, "/");
2399
2543
  const sourceRoutes = fileRoutes.map((r) => ({ ...r, filePath: sourceRelPath }));
2400
- await generateSchemaFiles(sourceRoutes, rootDir, dist);
2401
- generatedSchemas.add(schemaPath);
2402
- return true;
2544
+ const generatePromise = (async () => {
2545
+ await generateSchemaFiles(sourceRoutes, rootDir, dist);
2546
+ state.generatedSchemas.add(schemaPath);
2547
+ })();
2548
+ state.inFlightSchemaGenerations.set(schemaPath, generatePromise);
2549
+ try {
2550
+ await generatePromise;
2551
+ return true;
2552
+ } finally {
2553
+ state.inFlightSchemaGenerations.delete(schemaPath);
2554
+ }
2403
2555
  }
2404
2556
  function prodPathToSourcePath(prodAbsPath, rootDir, dist) {
2405
2557
  const rel = path7.relative(rootDir, prodAbsPath).replace(/\\/g, "/");
@@ -2413,13 +2565,11 @@ function prodPathToSourcePath(prodAbsPath, rootDir, dist) {
2413
2565
  if (fs6.existsSync(tsAbs)) return tsAbs;
2414
2566
  return path7.resolve(rootDir, srcRel);
2415
2567
  }
2416
- var devOnDemandEnabled = false;
2417
2568
  function isDevOnDemandEnabled() {
2418
- return devOnDemandEnabled;
2569
+ return state.enabled;
2419
2570
  }
2420
- var devDistDir;
2421
2571
  function getDevDist() {
2422
- return devDistDir;
2572
+ return state.distDir;
2423
2573
  }
2424
2574
 
2425
2575
  // src/validator/validateInput.ts
@@ -2942,52 +3092,6 @@ import fs8 from "fs";
2942
3092
  import { WebSocketServer, WebSocket } from "ws";
2943
3093
  import path8 from "path";
2944
3094
 
2945
- // src/errors/formatErrorResponse.ts
2946
- function formatErrorResponse(error) {
2947
- if (error instanceof ValidationError) {
2948
- const body2 = {
2949
- code: error.code,
2950
- message: error.message,
2951
- issues: error.issues
2952
- };
2953
- return new Response(JSON.stringify({ error: body2 }), {
2954
- status: error.statusCode,
2955
- headers: { "Content-Type": "application/json" }
2956
- });
2957
- }
2958
- if (error instanceof MethodNotAllowedError) {
2959
- const body2 = {
2960
- code: error.code,
2961
- message: error.message
2962
- };
2963
- return new Response(JSON.stringify({ error: body2 }), {
2964
- status: error.statusCode,
2965
- headers: {
2966
- "Content-Type": "application/json",
2967
- Allow: error.allowedMethods.join(", ")
2968
- }
2969
- });
2970
- }
2971
- if (error instanceof FaapiError) {
2972
- const body2 = {
2973
- code: error.code,
2974
- message: error.message
2975
- };
2976
- return new Response(JSON.stringify({ error: body2 }), {
2977
- status: error.statusCode,
2978
- headers: { "Content-Type": "application/json" }
2979
- });
2980
- }
2981
- const body = {
2982
- code: "INTERNAL_ERROR",
2983
- message: error instanceof Error ? error.message : "An unknown error occurred"
2984
- };
2985
- return new Response(JSON.stringify({ error: body }), {
2986
- status: 500,
2987
- headers: { "Content-Type": "application/json" }
2988
- });
2989
- }
2990
-
2991
3095
  // src/server/serverUtils.ts
2992
3096
  function nodeHttpToWebHeaders(req) {
2993
3097
  const headers = new Headers();
@@ -3001,9 +3105,9 @@ function nodeHttpToWebHeaders(req) {
3001
3105
  }
3002
3106
  return headers;
3003
3107
  }
3004
- function buildErrorResponse(err) {
3108
+ function buildErrorResponse(err, config) {
3005
3109
  try {
3006
- return formatErrorResponse(err);
3110
+ return formatErrorResponse(err, config);
3007
3111
  } catch {
3008
3112
  return new Response(
3009
3113
  JSON.stringify({ error: { code: "INTERNAL_ERROR", message: "Internal Server Error" } }),
@@ -3198,25 +3302,51 @@ function toWebRequest(req, bodyLimit = DEFAULT_BODY_LIMIT) {
3198
3302
  }
3199
3303
  function limitStreamSize(stream, maxSize) {
3200
3304
  let totalSize = 0;
3201
- const reader = stream.getReader();
3202
- return new ReadableStream({
3203
- async pull(controller) {
3204
- const { done, value } = await reader.read();
3205
- if (done) {
3206
- controller.close();
3305
+ let reader;
3306
+ let errored = false;
3307
+ const releaseReader = () => {
3308
+ if (reader) {
3309
+ try {
3207
3310
  reader.releaseLock();
3208
- return;
3311
+ } catch {
3209
3312
  }
3210
- totalSize += value.byteLength;
3211
- if (totalSize > maxSize) {
3212
- controller.error(new Error(`\u8BF7\u6C42\u4F53\u8D85\u8FC7\u5927\u5C0F\u9650\u5236 ${maxSize} \u5B57\u8282`));
3213
- reader.releaseLock();
3214
- return;
3313
+ reader = void 0;
3314
+ }
3315
+ };
3316
+ const failStream = (controller, err) => {
3317
+ if (errored) return;
3318
+ errored = true;
3319
+ controller.error(err instanceof Error ? err : new Error(String(err)));
3320
+ releaseReader();
3321
+ };
3322
+ return new ReadableStream({
3323
+ async pull(controller) {
3324
+ if (!reader) reader = stream.getReader();
3325
+ try {
3326
+ const { done, value } = await reader.read();
3327
+ if (done) {
3328
+ controller.close();
3329
+ releaseReader();
3330
+ return;
3331
+ }
3332
+ totalSize += value.byteLength;
3333
+ if (totalSize > maxSize) {
3334
+ failStream(controller, new PayloadTooLargeError(maxSize));
3335
+ return;
3336
+ }
3337
+ controller.enqueue(value);
3338
+ } catch (err) {
3339
+ failStream(controller, err);
3215
3340
  }
3216
- controller.enqueue(value);
3217
3341
  },
3218
3342
  cancel(reason) {
3219
- reader.cancel(reason);
3343
+ if (reader) {
3344
+ try {
3345
+ reader.cancel(reason);
3346
+ } catch {
3347
+ }
3348
+ releaseReader();
3349
+ }
3220
3350
  }
3221
3351
  });
3222
3352
  }
@@ -3297,21 +3427,27 @@ function createServer(options) {
3297
3427
  }
3298
3428
  return { server, routesRef };
3299
3429
  }
3300
- async function handleRequest(routes, rootDir, dist, req, res, configMiddlewares, onError, config, globalMiddlewares, globalInjectors, bodyLimit) {
3430
+ function prepareRequest(req, config, bodyLimit) {
3301
3431
  const request = toWebRequest(req, bodyLimit);
3302
3432
  const method = request.method.toUpperCase();
3303
3433
  const urlPath = new URL(request.url).pathname;
3304
3434
  const ctx = createContext(request, {}, config, getClientIp(req));
3305
3435
  const meta = ctx.meta;
3306
- const routePipeline = async () => {
3307
- const match = matchRoute(routes, method, urlPath);
3308
- if (!match) {
3309
- const allowedMethods = findAllowedMethods(routes, urlPath);
3310
- if (allowedMethods.length > 0) {
3311
- throw new MethodNotAllowedError(method, urlPath, allowedMethods);
3312
- }
3313
- throw new RouteNotFoundError(urlPath);
3314
- }
3436
+ return { request, ctx, meta, method, urlPath };
3437
+ }
3438
+ function resolveRouteOrThrow(routes, method, urlPath) {
3439
+ const match = matchRoute(routes, method, urlPath);
3440
+ if (match) return match;
3441
+ const allowedMethods = findAllowedMethods(routes, urlPath);
3442
+ if (allowedMethods.length > 0) {
3443
+ throw new MethodNotAllowedError(method, urlPath, allowedMethods);
3444
+ }
3445
+ throw new RouteNotFoundError(urlPath);
3446
+ }
3447
+ function createRoutePipeline(opts) {
3448
+ const { routes, method, urlPath, ctx, request, rootDir, dist, globalInjectors } = opts;
3449
+ return async () => {
3450
+ const match = resolveRouteOrThrow(routes, method, urlPath);
3315
3451
  ctx.params = match.params;
3316
3452
  const { route } = match;
3317
3453
  const absoluteFilePath = path9.resolve(rootDir, route.filePath);
@@ -3341,37 +3477,44 @@ async function handleRequest(routes, rootDir, dist, req, res, configMiddlewares,
3341
3477
  }
3342
3478
  }
3343
3479
  const mergedInjectors = globalInjectors ? { ...globalInjectors, ...route.injectors } : route.injectors;
3344
- const response = await invokeHandler(
3345
- routeModule.handler,
3346
- ctx,
3347
- body,
3348
- route.middlewares,
3349
- mergedInjectors
3350
- );
3351
- return response;
3480
+ return await invokeHandler(routeModule.handler, ctx, body, route.middlewares, mergedInjectors);
3352
3481
  };
3353
- try {
3354
- let response;
3355
- const outerMiddlewares = [];
3356
- if (configMiddlewares.length > 0) outerMiddlewares.push(...configMiddlewares);
3357
- if (globalMiddlewares && globalMiddlewares.length > 0) {
3358
- outerMiddlewares.push(...globalMiddlewares);
3359
- }
3360
- if (outerMiddlewares.length > 0) {
3361
- response = await compose(outerMiddlewares, ctx, routePipeline);
3362
- } else {
3363
- response = await routePipeline();
3482
+ }
3483
+ async function sendSuccessResponse(response, res) {
3484
+ await sendNodeResponse(response, res);
3485
+ }
3486
+ async function sendErrorResponse(err, meta, res, onError, ctx) {
3487
+ await sendNodeResponse(mergeMeta(buildErrorResponse(err, ctx.config), meta), res);
3488
+ if (onError) {
3489
+ try {
3490
+ await onError(err, ctx);
3491
+ } catch {
3364
3492
  }
3365
- await sendNodeResponse(response, res);
3493
+ }
3494
+ }
3495
+ async function handleRequest(routes, rootDir, dist, req, res, configMiddlewares, onError, config, globalMiddlewares, globalInjectors, bodyLimit) {
3496
+ const { request, ctx, meta, method, urlPath } = prepareRequest(req, config, bodyLimit);
3497
+ const routePipeline = createRoutePipeline({
3498
+ routes,
3499
+ method,
3500
+ urlPath,
3501
+ ctx,
3502
+ request,
3503
+ rootDir,
3504
+ dist,
3505
+ globalMiddlewares,
3506
+ globalInjectors
3507
+ });
3508
+ const outerMiddlewares = [];
3509
+ if (configMiddlewares.length > 0) outerMiddlewares.push(...configMiddlewares);
3510
+ if (globalMiddlewares && globalMiddlewares.length > 0) {
3511
+ outerMiddlewares.push(...globalMiddlewares);
3512
+ }
3513
+ try {
3514
+ const response = outerMiddlewares.length > 0 ? await compose(outerMiddlewares, ctx, routePipeline) : await routePipeline();
3515
+ await sendSuccessResponse(response, res);
3366
3516
  } catch (err) {
3367
- const errorResponse = buildErrorResponse(err);
3368
- await sendNodeResponse(mergeMeta(errorResponse, meta), res);
3369
- if (onError) {
3370
- try {
3371
- await onError(err, ctx);
3372
- } catch {
3373
- }
3374
- }
3517
+ await sendErrorResponse(err, meta, res, onError, ctx);
3375
3518
  }
3376
3519
  }
3377
3520