@fluojs/platform-express 1.0.0-beta.2 → 1.0.0-beta.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 CHANGED
@@ -10,6 +10,7 @@ fluo 런타임을 위한 Express 기반 HTTP 어댑터 패키지입니다.
10
10
  - [사용 시점](#사용-시점)
11
11
  - [빠른 시작](#빠른-시작)
12
12
  - [주요 패턴](#주요-패턴)
13
+ - [어댑터 계약](#어댑터-계약)
13
14
  - [공개 API 개요](#공개-api-개요)
14
15
  - [관련 패키지](#관련-패키지)
15
16
  - [예제 소스](#예제-소스)
@@ -43,6 +44,8 @@ await app.listen();
43
44
  ### 스트리밍 응답 처리 (SSE)
44
45
  Express 어댑터는 공유 `SseResponse` 유틸리티를 통해 Server-Sent Events(SSE)를 지원하며, Express 전용 스트림 처리를 추상화합니다.
45
46
 
47
+ Express 기반 응답 스트림은 공유 fluo 백프레셔 계약도 따릅니다. `response.stream.waitForDrain()`은 `drain`, `close`, `error` 중 어느 쪽이 먼저 와도 완료되므로, 백프레셔가 풀리기 전에 클라이언트가 연결을 끊어도 스트리밍 작성기가 멈추지 않습니다.
48
+
46
49
  ```typescript
47
50
  @Get('events')
48
51
  async streamEvents(@Res() res: FrameworkResponse) {
@@ -67,6 +70,24 @@ const adapter = createExpressAdapter(
67
70
  );
68
71
  ```
69
72
 
73
+ ### 안전한 fallback을 포함한 Native Route Registration
74
+ 이제 어댑터는 의미 보존이 가능한 명시적 HTTP 메서드 라우트를 Express Router에 사전 등록하면서도, 실제 요청 처리는 계속 공유 fluo dispatcher를 통해 수행합니다.
75
+
76
+ 의미 보존이 가능한 unversioned route에서는 Express가 미리 고른 descriptor와 params를 공유 dispatcher에 전달하므로 duplicate route matching을 건너뛰면서도 guards, interceptors, observers, body parsing, raw body 캡처, SSE, 오류 응답은 기존과 같은 framework-owned 실행 경로를 유지합니다.
77
+
78
+ 어댑터가 native handoff를 붙인 뒤 app middleware가 framework request의 method 또는 path를 rewrite하면 dispatcher는 그 handoff를 stale로 보고 원래 Express match를 재사용하지 않고 rewrite된 요청을 다시 매칭합니다.
79
+
80
+ 문서화된 fluo semantics를 바꾸지 않기 위해 `/:id` 와 `/:slug`처럼 shape가 겹치는 파라미터 라우트, `@All(...)` 핸들러, `OPTIONS` 소유권, non-URI versioning, 그리고 duplicate slash/trailing slash 정규화에 의존하는 요청은 catch-all fallback 경로에 남겨둡니다.
81
+
82
+ ## 어댑터 계약
83
+
84
+ - **공유 dispatcher 소유권 유지**: Native Express Router 매치 이후에도 실제 요청은 공유 fluo dispatcher가 처리하므로 middleware, guards, interceptors, observers, params, error envelope 계약은 그대로 유지됩니다.
85
+ - **안전한 fallback 범위**: `@All(...)` 핸들러와 shape가 겹치는 파라미터 라우트는 Express Router에 강제 등록하지 않고 의도적으로 catch-all fallback 경로에 둡니다.
86
+ - **OPTIONS 소유권 parity**: 어댑터는 native route에 대해 Express Router가 `OPTIONS`를 자동 응답하지 못하게 막아, 미지원 메서드도 계속 fluo dispatcher semantics로 흘러가고 `@All(...)` 핸들러가 정의된 경우 `OPTIONS`도 그대로 소유할 수 있게 합니다.
87
+ - **경로 정규화 parity**: duplicate slash 변형처럼 Express Router와 fluo의 정규화 방식이 다를 수 있는 요청도 fallback dispatch를 통해 fluo의 normalized route contract를 유지합니다.
88
+ - **버저닝 parity**: Express Router가 최초 path match를 하더라도 header/media-type/custom version 선택은 계속 dispatcher가 최종 결정합니다.
89
+ - **Middleware rewrite parity**: App middleware가 method/path를 rewrite하면 native handoff는 무효화되고 rewrite된 요청을 기준으로 다시 매칭합니다.
90
+
70
91
  ## 공개 API 개요
71
92
 
72
93
  - `createExpressAdapter(options)`: Express HTTP 어댑터를 위한 팩토리입니다.
package/README.md CHANGED
@@ -10,6 +10,7 @@ Express-backed HTTP adapter for the fluo runtime.
10
10
  - [When to Use](#when-to-use)
11
11
  - [Quick Start](#quick-start)
12
12
  - [Common Patterns](#common-patterns)
13
+ - [Adapter Contract](#adapter-contract)
13
14
  - [Public API Overview](#public-api-overview)
14
15
  - [Related Packages](#related-packages)
15
16
  - [Example Sources](#example-sources)
@@ -43,6 +44,8 @@ await app.listen();
43
44
  ### Handling Streaming Responses (SSE)
44
45
  The Express adapter supports Server-Sent Events (SSE) via the shared `SseResponse` utility, abstracting away the Express-specific stream handling.
45
46
 
47
+ Express-backed response streams also honor the shared fluo backpressure contract: `response.stream.waitForDrain()` settles on `drain`, `close`, or `error`, so streaming writers do not hang when clients disconnect before backpressure clears.
48
+
46
49
  ```typescript
47
50
  @Get('events')
48
51
  async streamEvents(@Res() res: FrameworkResponse) {
@@ -67,6 +70,23 @@ const adapter = createExpressAdapter(
67
70
  );
68
71
  ```
69
72
 
73
+ ### Native Route Registration with Safe Fallback
74
+ The adapter now pre-registers semantically safe Express Router handlers for explicit HTTP methods and still dispatches those requests through the shared fluo dispatcher.
75
+
76
+ For semantically safe unversioned routes, Express hands the pre-matched descriptor and params to the shared dispatcher so duplicate route matching is skipped while guards, interceptors, observers, body parsing, raw body capture, SSE, and error responses stay on the same framework-owned execution path.
77
+
78
+ If app middleware rewrites the framework request method or path after the adapter attaches a native handoff, the dispatcher treats that handoff as stale and rematches the rewritten request instead of reusing the original Express match.
79
+
80
+ To avoid changing documented fluo semantics, overlapping same-shape param routes such as `/:id` and `/:slug`, `@All(...)` handlers, `OPTIONS` ownership, non-URI versioning, and requests that rely on fluo's duplicate-slash/trailing-slash normalization stay on the catch-all fallback path.
81
+
82
+ ## Adapter Contract
83
+
84
+ - **Shared dispatcher ownership**: Native Express Router matches still hand off to the shared fluo dispatcher, so middleware, guards, interceptors, observers, params, and error envelopes remain framework-defined.
85
+ - **Safe fallback scope**: `@All(...)` handlers and overlapping same-shape param routes intentionally stay on the catch-all fallback path instead of being force-registered through Express Router.
86
+ - **OPTIONS ownership parity**: The adapter prevents Express Router from auto-answering `OPTIONS` for native routes, so unsupported methods still fall through to fluo dispatcher semantics and `@All(...)` handlers can continue to own `OPTIONS` when defined.
87
+ - **Path normalization parity**: Requests that Express Router does not normalize the same way as fluo, such as duplicate-slash variants, still resolve through fallback dispatch so fluo's normalized route contract is preserved.
88
+ - **Versioning parity**: Header/media-type/custom version selection remains dispatcher-owned even when Express Router handles the initial path match.
89
+
70
90
  ## Public API Overview
71
91
 
72
92
  - `createExpressAdapter(options)`: Factory for the Express HTTP adapter.
package/dist/adapter.d.ts CHANGED
@@ -7,6 +7,9 @@ declare module '@fluojs/http' {
7
7
  rawBody?: Uint8Array;
8
8
  }
9
9
  }
10
+ /**
11
+ * Describes the express adapter options contract.
12
+ */
10
13
  export interface ExpressAdapterOptions {
11
14
  host?: string;
12
15
  https?: HttpsServerOptions;
@@ -17,8 +20,17 @@ export interface ExpressAdapterOptions {
17
20
  retryLimit?: number;
18
21
  shutdownTimeoutMs?: number;
19
22
  }
23
+ /**
24
+ * Defines the express application signal type.
25
+ */
20
26
  export type ExpressApplicationSignal = 'SIGINT' | 'SIGTERM';
27
+ /**
28
+ * Defines the cors input type.
29
+ */
21
30
  export type CorsInput = false | string | string[] | CorsOptions;
31
+ /**
32
+ * Describes the bootstrap express application options contract.
33
+ */
22
34
  export interface BootstrapExpressApplicationOptions extends Omit<CreateApplicationOptions, 'adapter' | 'logger' | 'middleware'> {
23
35
  cors?: CorsInput;
24
36
  globalPrefix?: string;
@@ -36,6 +48,9 @@ export interface BootstrapExpressApplicationOptions extends Omit<CreateApplicati
36
48
  securityHeaders?: false | SecurityHeadersOptions;
37
49
  shutdownTimeoutMs?: number;
38
50
  }
51
+ /**
52
+ * Describes the run express application options contract.
53
+ */
39
54
  export interface RunExpressApplicationOptions extends BootstrapExpressApplicationOptions {
40
55
  forceExitTimeoutMs?: number;
41
56
  shutdownSignals?: false | readonly ExpressApplicationSignal[];
@@ -44,6 +59,9 @@ interface ExpressListenTarget {
44
59
  bindTarget: string;
45
60
  url: string;
46
61
  }
62
+ /**
63
+ * Represents the express http application adapter.
64
+ */
47
65
  export declare class ExpressHttpApplicationAdapter implements HttpApplicationAdapter {
48
66
  private readonly port;
49
67
  private readonly host;
@@ -57,7 +75,9 @@ export declare class ExpressHttpApplicationAdapter implements HttpApplicationAda
57
75
  private closeInFlight?;
58
76
  private dispatcher?;
59
77
  private readonly app;
78
+ private nativeRoutesReady;
60
79
  private readonly requestResponseFactory;
80
+ private readonly router;
61
81
  private readonly server;
62
82
  private readonly sockets;
63
83
  constructor(port: number, host: string | undefined, retryDelayMs: number | undefined, retryLimit: number | undefined, httpsOptions: HttpsServerOptions | undefined, multipartOptions?: MultipartOptions | undefined, maxBodySize?: number, preserveRawBody?: boolean, shutdownTimeoutMs?: number);
@@ -67,11 +87,39 @@ export declare class ExpressHttpApplicationAdapter implements HttpApplicationAda
67
87
  listen(dispatcher: Dispatcher): Promise<void>;
68
88
  close(): Promise<void>;
69
89
  private listenWithRetry;
90
+ private registerNativeRoutes;
70
91
  private handleRequest;
71
92
  }
93
+ /**
94
+ * Create express adapter.
95
+ *
96
+ * @param options The options.
97
+ * @param multipartOptions The multipart options.
98
+ * @returns The create express adapter result.
99
+ */
72
100
  export declare function createExpressAdapter(options?: ExpressAdapterOptions, multipartOptions?: MultipartOptions): HttpApplicationAdapter;
101
+ /**
102
+ * Bootstrap express application.
103
+ *
104
+ * @param rootModule The root module.
105
+ * @param options The options.
106
+ * @returns The bootstrap express application result.
107
+ */
73
108
  export declare function bootstrapExpressApplication(rootModule: ModuleType, options: BootstrapExpressApplicationOptions): Promise<Application>;
109
+ /**
110
+ * Run express application.
111
+ *
112
+ * @param rootModule The root module.
113
+ * @param options The options.
114
+ * @returns The run express application result.
115
+ */
74
116
  export declare function runExpressApplication(rootModule: ModuleType, options: RunExpressApplicationOptions): Promise<Application>;
117
+ /**
118
+ * Is express multipart too large error.
119
+ *
120
+ * @param error The error.
121
+ * @returns The is express multipart too large error result.
122
+ */
75
123
  export declare function isExpressMultipartTooLargeError(error: unknown): boolean;
76
124
  export {};
77
125
  //# sourceMappingURL=adapter.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"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"}
package/dist/adapter.js CHANGED
@@ -4,17 +4,46 @@ import { Readable } from 'node:stream';
4
4
  import { URL } from 'node:url';
5
5
  import express from 'express';
6
6
  import { BadRequestException, createServerBackedHttpAdapterRealtimeCapability, createErrorResponse, HttpException, InternalServerErrorException, PayloadTooLargeException } from '@fluojs/http';
7
+ import { attachFrameworkRequestNativeRouteHandoff, bindRawRequestNativeRouteHandoff, consumeRawRequestNativeRouteHandoff, isRoutePathNormalizationSensitive } from '@fluojs/http/internal';
7
8
  import { createNodeShutdownSignalRegistration, defaultNodeShutdownSignals } from '@fluojs/runtime/node';
8
9
  import { parseMultipart } from '@fluojs/runtime/web';
9
10
  import { bootstrapHttpAdapterApplication, runHttpAdapterApplication } from '@fluojs/runtime/internal/http-adapter';
10
11
  import { dispatchWithRequestResponseFactory } from '@fluojs/runtime/internal/request-response-factory';
12
+
13
+ /**
14
+ * Describes the express adapter options contract.
15
+ */
16
+
17
+ /**
18
+ * Defines the express application signal type.
19
+ */
20
+
21
+ /**
22
+ * Defines the cors input type.
23
+ */
24
+
11
25
  const DEFAULT_MAX_BODY_SIZE = 1 * 1024 * 1024;
12
26
  const DEFAULT_SHUTDOWN_TIMEOUT_MS = 10_000;
27
+ const EXPRESS_NATIVE_ROUTE_METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD'];
28
+
29
+ /**
30
+ * Describes the bootstrap express application options contract.
31
+ */
32
+
33
+ /**
34
+ * Describes the run express application options contract.
35
+ */
36
+
37
+ /**
38
+ * Represents the express http application adapter.
39
+ */
13
40
  export class ExpressHttpApplicationAdapter {
14
41
  closeInFlight;
15
42
  dispatcher;
16
43
  app;
44
+ nativeRoutesReady = false;
17
45
  requestResponseFactory;
46
+ router = express.Router();
18
47
  server;
19
48
  sockets = new Set();
20
49
  constructor(port, host, retryDelayMs = 150, retryLimit = 20, httpsOptions, multipartOptions, maxBodySize = DEFAULT_MAX_BODY_SIZE, preserveRawBody = false, shutdownTimeoutMs = DEFAULT_SHUTDOWN_TIMEOUT_MS) {
@@ -30,6 +59,7 @@ export class ExpressHttpApplicationAdapter {
30
59
  this.app = express();
31
60
  this.requestResponseFactory = createExpressRequestResponseFactory(this.multipartOptions, this.maxBodySize, this.preserveRawBody);
32
61
  this.server = createExpressServer(this.httpsOptions, this.app);
62
+ this.app.use(this.router);
33
63
  this.app.use((request, response) => {
34
64
  void this.handleRequest(request, response);
35
65
  });
@@ -51,6 +81,7 @@ export class ExpressHttpApplicationAdapter {
51
81
  }
52
82
  async listen(dispatcher) {
53
83
  this.dispatcher = dispatcher;
84
+ this.registerNativeRoutes(dispatcher);
54
85
  await this.listenWithRetry();
55
86
  }
56
87
  async close() {
@@ -87,6 +118,33 @@ export class ExpressHttpApplicationAdapter {
87
118
  }
88
119
  }
89
120
  }
121
+ registerNativeRoutes(dispatcher) {
122
+ if (this.nativeRoutesReady) {
123
+ return;
124
+ }
125
+ const nativeRoutes = createExpressNativeRoutes(resolveDispatcherRouteDescriptors(dispatcher));
126
+ Reflect.set(this.router, '__fluoNativeRoutes', nativeRoutes);
127
+ for (const route of nativeRoutes) {
128
+ this.router.all(route.path, (request, response, next) => {
129
+ if (!route.methods.includes(request.method.toUpperCase())) {
130
+ next();
131
+ return;
132
+ }
133
+ const nativeMethod = request.method.toUpperCase();
134
+ const requestPath = new URL(request.originalUrl || request.url || '/', 'http://localhost').pathname;
135
+ const descriptor = route.descriptorsByMethod[nativeMethod];
136
+ const params = normalizeNativeRouteParams(request.params);
137
+ if (descriptor && !isRoutePathNormalizationSensitive(requestPath) && !hasNativeRouteParamSeparators(params)) {
138
+ bindRawRequestNativeRouteHandoff(request, {
139
+ descriptor,
140
+ params
141
+ });
142
+ }
143
+ void this.handleRequest(request, response);
144
+ });
145
+ }
146
+ this.nativeRoutesReady = true;
147
+ }
90
148
  async handleRequest(request, response) {
91
149
  await dispatchWithRequestResponseFactory({
92
150
  dispatcher: this.dispatcher,
@@ -97,6 +155,68 @@ export class ExpressHttpApplicationAdapter {
97
155
  });
98
156
  }
99
157
  }
158
+ function resolveDispatcherRouteDescriptors(dispatcher) {
159
+ return dispatcher.describeRoutes?.() ?? [];
160
+ }
161
+ function createExpressNativeRoutes(descriptors) {
162
+ const candidates = new Map();
163
+ const shapePaths = new Map();
164
+ const versionSensitiveRouteKeys = collectVersionSensitiveRouteKeys(descriptors);
165
+ for (const descriptor of descriptors) {
166
+ if (!isExpressNativeRouteDescriptor(descriptor) || versionSensitiveRouteKeys.has(`${descriptor.route.method}:${descriptor.route.path}`)) {
167
+ continue;
168
+ }
169
+ registerExpressNativeRouteCandidate(candidates, shapePaths, descriptor);
170
+ }
171
+ const routesByPath = new Map();
172
+ for (const candidate of candidates.values()) {
173
+ if (shapePaths.get(candidate.shapeKey)?.size !== 1) {
174
+ continue;
175
+ }
176
+ let route = routesByPath.get(candidate.path);
177
+ if (!route) {
178
+ route = {
179
+ descriptorsByMethod: {},
180
+ methods: new Set()
181
+ };
182
+ routesByPath.set(candidate.path, route);
183
+ }
184
+ route.methods.add(candidate.method);
185
+ route.descriptorsByMethod[candidate.method] = candidate.descriptor;
186
+ }
187
+ return [...routesByPath.entries()].map(([path, route]) => ({
188
+ descriptorsByMethod: route.descriptorsByMethod,
189
+ methods: [...route.methods],
190
+ path
191
+ }));
192
+ }
193
+ function isExpressNativeRouteDescriptor(descriptor) {
194
+ return descriptor.route.method !== 'ALL' && EXPRESS_NATIVE_ROUTE_METHODS.includes(descriptor.route.method) && descriptor.route.version === undefined;
195
+ }
196
+ function registerExpressNativeRouteCandidate(candidates, shapePaths, descriptor) {
197
+ const nativeMethod = descriptor.route.method;
198
+ const path = descriptor.route.path;
199
+ const routeKey = `${nativeMethod}:${path}`;
200
+ const shapeKey = `${nativeMethod}:${canonicalizeExpressRouteShape(path)}`;
201
+ if (!candidates.has(routeKey)) {
202
+ candidates.set(routeKey, {
203
+ descriptor,
204
+ method: nativeMethod,
205
+ path,
206
+ shapeKey
207
+ });
208
+ }
209
+ let paths = shapePaths.get(shapeKey);
210
+ if (!paths) {
211
+ paths = new Set();
212
+ shapePaths.set(shapeKey, paths);
213
+ }
214
+ paths.add(path);
215
+ }
216
+ function canonicalizeExpressRouteShape(path) {
217
+ const segments = path.split('/').filter(Boolean).map(segment => segment.startsWith(':') ? ':' : segment);
218
+ return segments.length === 0 ? '/' : `/${segments.join('/')}`;
219
+ }
100
220
  function createExpressRequestResponseFactory(multipartOptions, maxBodySize = DEFAULT_MAX_BODY_SIZE, preserveRawBody = false) {
101
221
  return {
102
222
  async createRequest(request, signal) {
@@ -118,12 +238,36 @@ function createExpressRequestResponseFactory(multipartOptions, maxBodySize = DEF
118
238
  }
119
239
  };
120
240
  }
241
+
242
+ /**
243
+ * Create express adapter.
244
+ *
245
+ * @param options The options.
246
+ * @param multipartOptions The multipart options.
247
+ * @returns The create express adapter result.
248
+ */
121
249
  export function createExpressAdapter(options = {}, multipartOptions) {
122
250
  return new ExpressHttpApplicationAdapter(resolvePort(options.port), options.host, options.retryDelayMs, options.retryLimit, options.https, multipartOptions, options.maxBodySize, options.rawBody, options.shutdownTimeoutMs);
123
251
  }
252
+
253
+ /**
254
+ * Bootstrap express application.
255
+ *
256
+ * @param rootModule The root module.
257
+ * @param options The options.
258
+ * @returns The bootstrap express application result.
259
+ */
124
260
  export async function bootstrapExpressApplication(rootModule, options) {
125
261
  return bootstrapHttpAdapterApplication(rootModule, options, createExpressAdapter(options, options.multipart));
126
262
  }
263
+
264
+ /**
265
+ * Run express application.
266
+ *
267
+ * @param rootModule The root module.
268
+ * @param options The options.
269
+ * @returns The run express application result.
270
+ */
127
271
  export async function runExpressApplication(rootModule, options) {
128
272
  const adapter = createExpressAdapter(options, options.multipart);
129
273
  return runHttpAdapterApplication(rootModule, {
@@ -156,6 +300,18 @@ function createFrameworkResponse(response) {
156
300
  this.committed = true;
157
301
  response.send(serialized.payload);
158
302
  },
303
+ async sendSimpleJson(body) {
304
+ if (response.writableEnded) {
305
+ this.committed = true;
306
+ return;
307
+ }
308
+ const serialized = serializeResponseBody(body);
309
+ if (!response.hasHeader('content-type') && serialized.defaultContentType) {
310
+ response.setHeader('content-type', serialized.defaultContentType);
311
+ }
312
+ this.committed = true;
313
+ response.send(serialized.payload);
314
+ },
159
315
  setHeader(name, value) {
160
316
  const lowerName = name.toLowerCase();
161
317
  if (lowerName === 'set-cookie') {
@@ -196,11 +352,19 @@ function createFrameworkResponseStream(response) {
196
352
  };
197
353
  },
198
354
  waitForDrain() {
199
- if (response.writableEnded) {
355
+ if (response.writableEnded || response.destroyed) {
200
356
  return Promise.resolve();
201
357
  }
202
358
  return new Promise(resolve => {
203
- response.once('drain', () => resolve());
359
+ const settle = () => {
360
+ response.removeListener('drain', settle);
361
+ response.removeListener('close', settle);
362
+ response.removeListener('error', settle);
363
+ resolve();
364
+ };
365
+ response.once('drain', settle);
366
+ response.once('close', settle);
367
+ response.once('error', settle);
204
368
  });
205
369
  },
206
370
  write(chunk) {
@@ -247,7 +411,34 @@ async function createFrameworkRequest(request, signal, multipartOptions, maxBody
247
411
  if (rawBody) {
248
412
  frameworkRequest.rawBody = rawBody;
249
413
  }
250
- return frameworkRequest;
414
+ const nativeRouteHandoff = consumeRawRequestNativeRouteHandoff(request);
415
+ return nativeRouteHandoff ? attachFrameworkRequestNativeRouteHandoff(frameworkRequest, nativeRouteHandoff) : frameworkRequest;
416
+ }
417
+ function normalizeNativeRouteParams(params) {
418
+ if (typeof params !== 'object' || params === null) {
419
+ return {};
420
+ }
421
+ return Object.fromEntries(Object.entries(params).flatMap(([key, value]) => typeof value === 'string' ? [[key, value]] : value === undefined ? [] : [[key, String(value)]]));
422
+ }
423
+ function hasNativeRouteParamSeparators(params) {
424
+ return Object.values(params).some(value => value.includes('/'));
425
+ }
426
+ function collectVersionSensitiveRouteKeys(descriptors) {
427
+ const grouped = new Map();
428
+ for (const descriptor of descriptors) {
429
+ if (!EXPRESS_NATIVE_ROUTE_METHODS.includes(descriptor.route.method)) {
430
+ continue;
431
+ }
432
+ const routeKey = `${descriptor.route.method}:${descriptor.route.path}`;
433
+ const current = grouped.get(routeKey) ?? {
434
+ count: 0,
435
+ hasVersioned: false
436
+ };
437
+ current.count += 1;
438
+ current.hasVersioned ||= descriptor.route.version !== undefined;
439
+ grouped.set(routeKey, current);
440
+ }
441
+ return new Set([...grouped.entries()].filter(([, current]) => current.count > 1 || current.hasVersioned).map(([routeKey]) => routeKey));
251
442
  }
252
443
  async function parseMultipartRequest(request, options = {}) {
253
444
  try {
@@ -267,6 +458,13 @@ async function parseMultipartRequest(request, options = {}) {
267
458
  throw error;
268
459
  }
269
460
  }
461
+
462
+ /**
463
+ * Is express multipart too large error.
464
+ *
465
+ * @param error The error.
466
+ * @returns The is express multipart too large error result.
467
+ */
270
468
  export function isExpressMultipartTooLargeError(error) {
271
469
  if (error instanceof PayloadTooLargeException) {
272
470
  return true;
File without changes
package/package.json CHANGED
@@ -8,7 +8,7 @@
8
8
  "platform",
9
9
  "server"
10
10
  ],
11
- "version": "1.0.0-beta.2",
11
+ "version": "1.0.0-beta.4",
12
12
  "private": false,
13
13
  "license": "MIT",
14
14
  "repository": {
@@ -36,12 +36,13 @@
36
36
  ],
37
37
  "dependencies": {
38
38
  "express": "^5.1.0",
39
- "@fluojs/http": "^1.0.0-beta.1",
40
- "@fluojs/runtime": "^1.0.0-beta.2"
39
+ "@fluojs/http": "^1.0.0-beta.4",
40
+ "@fluojs/runtime": "^1.0.0-beta.5"
41
41
  },
42
42
  "devDependencies": {
43
43
  "@types/express": "^5.0.3",
44
- "vitest": "^3.2.4"
44
+ "vitest": "^3.2.4",
45
+ "@fluojs/di": "^1.0.0-beta.5"
45
46
  },
46
47
  "scripts": {
47
48
  "prebuild": "node ../../tooling/scripts/clean-dist.mjs",