@fluojs/http 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.
Files changed (71) hide show
  1. package/README.ko.md +4 -1
  2. package/README.md +4 -1
  3. package/dist/adapter.d.ts +31 -0
  4. package/dist/adapter.d.ts.map +1 -1
  5. package/dist/adapter.js +37 -0
  6. package/dist/adapters/binding.d.ts +6 -0
  7. package/dist/adapters/binding.d.ts.map +1 -1
  8. package/dist/adapters/binding.js +20 -51
  9. package/dist/adapters/dto-binding-plan.d.ts +19 -0
  10. package/dist/adapters/dto-binding-plan.d.ts.map +1 -0
  11. package/dist/adapters/dto-binding-plan.js +54 -0
  12. package/dist/adapters/dto-validation-adapter.d.ts +3 -0
  13. package/dist/adapters/dto-validation-adapter.d.ts.map +1 -1
  14. package/dist/adapters/dto-validation-adapter.js +8 -4
  15. package/dist/dispatch/dispatch-content-negotiation.d.ts +17 -0
  16. package/dist/dispatch/dispatch-content-negotiation.d.ts.map +1 -1
  17. package/dist/dispatch/dispatch-content-negotiation.js +21 -0
  18. package/dist/dispatch/dispatch-error-policy.d.ts +8 -0
  19. package/dist/dispatch/dispatch-error-policy.d.ts.map +1 -1
  20. package/dist/dispatch/dispatch-error-policy.js +9 -0
  21. package/dist/dispatch/dispatch-handler-policy.d.ts +11 -1
  22. package/dist/dispatch/dispatch-handler-policy.d.ts.map +1 -1
  23. package/dist/dispatch/dispatch-handler-policy.js +12 -2
  24. package/dist/dispatch/dispatch-response-policy.d.ts +10 -0
  25. package/dist/dispatch/dispatch-response-policy.d.ts.map +1 -1
  26. package/dist/dispatch/dispatch-response-policy.js +43 -0
  27. package/dist/dispatch/dispatch-routing-policy.d.ts +13 -0
  28. package/dist/dispatch/dispatch-routing-policy.d.ts.map +1 -1
  29. package/dist/dispatch/dispatch-routing-policy.js +14 -0
  30. package/dist/dispatch/dispatcher.d.ts +6 -1
  31. package/dist/dispatch/dispatcher.d.ts.map +1 -1
  32. package/dist/dispatch/dispatcher.js +171 -28
  33. package/dist/dispatch/native-route-handoff.d.ts +53 -0
  34. package/dist/dispatch/native-route-handoff.d.ts.map +1 -0
  35. package/dist/dispatch/native-route-handoff.js +94 -0
  36. package/dist/errors.d.ts +3 -0
  37. package/dist/errors.d.ts.map +1 -1
  38. package/dist/errors.js +4 -0
  39. package/dist/guards.d.ts +7 -0
  40. package/dist/guards.d.ts.map +1 -1
  41. package/dist/guards.js +11 -0
  42. package/dist/input-error-detail.d.ts +9 -0
  43. package/dist/input-error-detail.d.ts.map +1 -1
  44. package/dist/input-error-detail.js +10 -0
  45. package/dist/interceptors.d.ts +8 -0
  46. package/dist/interceptors.d.ts.map +1 -1
  47. package/dist/interceptors.js +14 -1
  48. package/dist/internal.d.ts +1 -0
  49. package/dist/internal.d.ts.map +1 -1
  50. package/dist/internal.js +2 -1
  51. package/dist/mapping.d.ts +7 -0
  52. package/dist/mapping.d.ts.map +1 -1
  53. package/dist/mapping.js +93 -11
  54. package/dist/middleware/correlation.d.ts +5 -0
  55. package/dist/middleware/correlation.d.ts.map +1 -1
  56. package/dist/middleware/correlation.js +6 -0
  57. package/dist/middleware/cors.d.ts +9 -0
  58. package/dist/middleware/cors.d.ts.map +1 -1
  59. package/dist/middleware/cors.js +11 -0
  60. package/dist/middleware/middleware.d.ts +34 -0
  61. package/dist/middleware/middleware.d.ts.map +1 -1
  62. package/dist/middleware/middleware.js +47 -0
  63. package/dist/middleware/security-headers.d.ts +9 -0
  64. package/dist/middleware/security-headers.d.ts.map +1 -1
  65. package/dist/middleware/security-headers.js +11 -0
  66. package/dist/route-path.d.ts +41 -0
  67. package/dist/route-path.d.ts.map +1 -1
  68. package/dist/route-path.js +50 -0
  69. package/dist/types.d.ts +5 -0
  70. package/dist/types.d.ts.map +1 -1
  71. package/package.json +2 -2
@@ -11,6 +11,45 @@ function resolveDefaultSuccessStatus(handler, value) {
11
11
  return 200;
12
12
  }
13
13
  }
14
+ function canUseSimpleJsonFastPath(response, value) {
15
+ return isSimpleJsonResponseBody(value) && !isResponseBodyForbidden(response.statusCode) && hasJsonCompatibleContentType(response);
16
+ }
17
+ function hasSimpleJsonResponseWriter(response) {
18
+ return typeof response.sendSimpleJson === 'function';
19
+ }
20
+ function isSimpleJsonResponseBody(value) {
21
+ if (Array.isArray(value)) {
22
+ return true;
23
+ }
24
+ return typeof value === 'object' && value !== null && Object.getPrototypeOf(value) === Object.prototype;
25
+ }
26
+ function isResponseBodyForbidden(status) {
27
+ return status === 204 || status === 205 || status === 304;
28
+ }
29
+ function hasJsonCompatibleContentType(response) {
30
+ const contentType = readHeader(response.headers, 'content-type');
31
+ return contentType === undefined || isJsonContentType(contentType);
32
+ }
33
+ function readHeader(headers, name) {
34
+ const lowerName = name.toLowerCase();
35
+ const entry = Object.entries(headers).find(([headerName]) => headerName.toLowerCase() === lowerName);
36
+ const value = entry?.[1];
37
+ return typeof value === 'string' ? value : undefined;
38
+ }
39
+ function isJsonContentType(contentType) {
40
+ return contentType.toLowerCase().includes('application/json') || contentType.toLowerCase().endsWith('+json');
41
+ }
42
+
43
+ /**
44
+ * Write success response.
45
+ *
46
+ * @param handler The handler.
47
+ * @param request The request.
48
+ * @param response The response.
49
+ * @param value The value.
50
+ * @param contentNegotiation The content negotiation.
51
+ * @returns The write success response result.
52
+ */
14
53
  export async function writeSuccessResponse(handler, request, response, value, contentNegotiation) {
15
54
  if (response.committed) {
16
55
  return;
@@ -39,6 +78,10 @@ export async function writeSuccessResponse(handler, request, response, value, co
39
78
  } else if (response.statusSet !== true) {
40
79
  response.setStatus(resolveDefaultSuccessStatus(handler, value));
41
80
  }
81
+ if (!formatter && hasSimpleJsonResponseWriter(response) && canUseSimpleJsonFastPath(response, value)) {
82
+ await response.sendSimpleJson(value);
83
+ return;
84
+ }
42
85
  const responseBody = formatter ? formatter.format(value) : value;
43
86
  await response.send(responseBody);
44
87
  }
@@ -1,4 +1,17 @@
1
1
  import type { FrameworkRequest, HandlerMapping, HandlerMatch, RequestContext } from '../types.js';
2
+ /**
3
+ * Match handler or throw.
4
+ *
5
+ * @param handlerMapping The handler mapping.
6
+ * @param request The request.
7
+ * @returns The match handler or throw result.
8
+ */
2
9
  export declare function matchHandlerOrThrow(handlerMapping: HandlerMapping, request: FrameworkRequest): HandlerMatch;
10
+ /**
11
+ * Update request params.
12
+ *
13
+ * @param requestContext The request context.
14
+ * @param params The params.
15
+ */
3
16
  export declare function updateRequestParams(requestContext: RequestContext, params: Readonly<Record<string, string>>): void;
4
17
  //# sourceMappingURL=dispatch-routing-policy.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"dispatch-routing-policy.d.ts","sourceRoot":"","sources":["../../src/dispatch/dispatch-routing-policy.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,gBAAgB,EAAE,cAAc,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAElG,wBAAgB,mBAAmB,CAAC,cAAc,EAAE,cAAc,EAAE,OAAO,EAAE,gBAAgB,GAAG,YAAY,CAQ3G;AAED,wBAAgB,mBAAmB,CAAC,cAAc,EAAE,cAAc,EAAE,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,GAAG,IAAI,CAKlH"}
1
+ {"version":3,"file":"dispatch-routing-policy.d.ts","sourceRoot":"","sources":["../../src/dispatch/dispatch-routing-policy.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,gBAAgB,EAAE,cAAc,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAElG;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,cAAc,EAAE,cAAc,EAAE,OAAO,EAAE,gBAAgB,GAAG,YAAY,CAQ3G;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,cAAc,EAAE,cAAc,EAAE,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,GAAG,IAAI,CAKlH"}
@@ -1,4 +1,11 @@
1
1
  import { HandlerNotFoundError } from '../errors.js';
2
+ /**
3
+ * Match handler or throw.
4
+ *
5
+ * @param handlerMapping The handler mapping.
6
+ * @param request The request.
7
+ * @returns The match handler or throw result.
8
+ */
2
9
  export function matchHandlerOrThrow(handlerMapping, request) {
3
10
  const match = handlerMapping.match(request);
4
11
  if (!match) {
@@ -6,6 +13,13 @@ export function matchHandlerOrThrow(handlerMapping, request) {
6
13
  }
7
14
  return match;
8
15
  }
16
+
17
+ /**
18
+ * Update request params.
19
+ *
20
+ * @param requestContext The request context.
21
+ * @param params The params.
22
+ */
9
23
  export function updateRequestParams(requestContext, params) {
10
24
  requestContext.request = {
11
25
  ...requestContext.request,
@@ -1,5 +1,5 @@
1
1
  import type { Container } from '@fluojs/di';
2
- import type { Binder, ContentNegotiationOptions, Dispatcher, DispatcherLogger, FrameworkRequest, FrameworkResponse, HandlerMapping, InterceptorLike, MiddlewareLike, RequestObserverLike } from '../types.js';
2
+ import type { Binder, ContentNegotiationOptions, ConverterLike, Dispatcher, DispatcherLogger, FrameworkRequest, FrameworkResponse, HandlerMapping, InterceptorLike, MiddlewareLike, RequestObserverLike } from '../types.js';
3
3
  /**
4
4
  * Type definition for a global HTTP error handler function.
5
5
  */
@@ -22,6 +22,11 @@ export interface CreateDispatcherOptions {
22
22
  observers?: RequestObserverLike[];
23
23
  /** Optional global error handler. */
24
24
  onError?: ErrorHandler;
25
+ /** Request-scope optimization hints supplied by runtime bootstrap. */
26
+ requestScope?: {
27
+ /** Global DTO converters used by the default binder. */
28
+ converterDefinitions?: readonly ConverterLike[];
29
+ };
25
30
  logger?: DispatcherLogger;
26
31
  /** Root DI container for creating request scopes. */
27
32
  rootContainer: Container;
@@ -1 +1 @@
1
- {"version":3,"file":"dispatcher.d.ts","sourceRoot":"","sources":["../../src/dispatch/dispatcher.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAW5C,OAAO,KAAK,EACV,MAAM,EACN,yBAAyB,EACzB,UAAU,EACV,gBAAgB,EAChB,gBAAgB,EAChB,iBAAiB,EAGjB,cAAc,EACd,eAAe,EAGf,cAAc,EAId,mBAAmB,EACpB,MAAM,aAAa,CAAC;AAErB;;GAEG;AACH,MAAM,MAAM,YAAY,GAAG,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,gBAAgB,EAAE,QAAQ,EAAE,iBAAiB,EAAE,SAAS,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,GAAG,OAAO,GAAG,IAAI,CAAC;AAEpK;;GAEG;AACH,MAAM,WAAW,uBAAuB;IACtC,iDAAiD;IACjD,aAAa,CAAC,EAAE,cAAc,EAAE,CAAC;IACjC,kFAAkF;IAClF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,kDAAkD;IAClD,kBAAkB,CAAC,EAAE,yBAAyB,CAAC;IAC/C,sDAAsD;IACtD,cAAc,EAAE,cAAc,CAAC;IAC/B,2DAA2D;IAC3D,YAAY,CAAC,EAAE,eAAe,EAAE,CAAC;IACjC,0DAA0D;IAC1D,SAAS,CAAC,EAAE,mBAAmB,EAAE,CAAC;IAClC,qCAAqC;IACrC,OAAO,CAAC,EAAE,YAAY,CAAC;IACvB,MAAM,CAAC,EAAE,gBAAgB,CAAC;IAC1B,qDAAqD;IACrD,aAAa,EAAE,SAAS,CAAC;CAC1B;AAmQD;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,uBAAuB,GAAG,UAAU,CA8B7E"}
1
+ {"version":3,"file":"dispatcher.d.ts","sourceRoot":"","sources":["../../src/dispatch/dispatcher.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAyB,MAAM,YAAY,CAAC;AAanE,OAAO,KAAK,EACV,MAAM,EACN,yBAAyB,EACzB,aAAa,EACb,UAAU,EACV,gBAAgB,EAChB,gBAAgB,EAChB,iBAAiB,EAGjB,cAAc,EACd,eAAe,EAEf,cAAc,EAId,mBAAmB,EACpB,MAAM,aAAa,CAAC;AAErB;;GAEG;AACH,MAAM,MAAM,YAAY,GAAG,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,gBAAgB,EAAE,QAAQ,EAAE,iBAAiB,EAAE,SAAS,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,GAAG,OAAO,GAAG,IAAI,CAAC;AAEpK;;GAEG;AACH,MAAM,WAAW,uBAAuB;IACtC,iDAAiD;IACjD,aAAa,CAAC,EAAE,cAAc,EAAE,CAAC;IACjC,kFAAkF;IAClF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,kDAAkD;IAClD,kBAAkB,CAAC,EAAE,yBAAyB,CAAC;IAC/C,sDAAsD;IACtD,cAAc,EAAE,cAAc,CAAC;IAC/B,2DAA2D;IAC3D,YAAY,CAAC,EAAE,eAAe,EAAE,CAAC;IACjC,0DAA0D;IAC1D,SAAS,CAAC,EAAE,mBAAmB,EAAE,CAAC;IAClC,qCAAqC;IACrC,OAAO,CAAC,EAAE,YAAY,CAAC;IACvB,sEAAsE;IACtE,YAAY,CAAC,EAAE;QACb,wDAAwD;QACxD,oBAAoB,CAAC,EAAE,SAAS,aAAa,EAAE,CAAC;KACjD,CAAC;IACF,MAAM,CAAC,EAAE,gBAAgB,CAAC;IAC1B,qDAAqD;IACrD,aAAa,EAAE,SAAS,CAAC;CAC1B;AA8bD;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,uBAAuB,GAAG,UAAU,CAuD7E"}
@@ -1,10 +1,12 @@
1
+ import { readFrameworkRequestNativeRouteHandoff } from './native-route-handoff.js';
1
2
  import { invokeControllerHandler } from './dispatch-handler-policy.js';
2
3
  import { resolveContentNegotiation, writeErrorResponse, writeSuccessResponse } from './dispatch-response-policy.js';
3
4
  import { matchHandlerOrThrow, updateRequestParams } from './dispatch-routing-policy.js';
5
+ import { getCompiledDtoBindingPlan } from '../adapters/dto-binding-plan.js';
4
6
  import { RequestAbortedError } from '../errors.js';
5
7
  import { runGuardChain } from '../guards.js';
6
8
  import { runInterceptorChain } from '../interceptors.js';
7
- import { runMiddlewareChain } from '../middleware/middleware.js';
9
+ import { isMiddlewareRouteConfig, matchRoutePattern, runMiddlewareChain } from '../middleware/middleware.js';
8
10
  import { createRequestContext, runWithRequestContext } from '../context/request-context.js';
9
11
  import { SseResponse } from '../context/sse.js';
10
12
 
@@ -31,20 +33,126 @@ function createDispatchRequest(request) {
31
33
  }
32
34
  };
33
35
  }
36
+ function cloneHandlerDescriptor(descriptor) {
37
+ return {
38
+ ...descriptor,
39
+ metadata: {
40
+ ...descriptor.metadata,
41
+ moduleMiddleware: [...descriptor.metadata.moduleMiddleware],
42
+ pathParams: [...descriptor.metadata.pathParams]
43
+ },
44
+ route: {
45
+ ...descriptor.route,
46
+ guards: descriptor.route.guards ? [...descriptor.route.guards] : undefined,
47
+ headers: descriptor.route.headers?.map(header => ({
48
+ ...header
49
+ })),
50
+ interceptors: descriptor.route.interceptors ? [...descriptor.route.interceptors] : undefined,
51
+ produces: descriptor.route.produces ? [...descriptor.route.produces] : undefined,
52
+ redirect: descriptor.route.redirect ? {
53
+ ...descriptor.route.redirect
54
+ } : undefined
55
+ }
56
+ };
57
+ }
34
58
  function readRequestId(request) {
35
59
  const raw = request.headers['x-request-id'] ?? request.headers['X-Request-Id'];
36
60
  const value = Array.isArray(raw) ? raw[0] : raw;
37
61
  const normalized = value?.trim();
38
62
  return normalized ? normalized : undefined;
39
63
  }
40
- function createDispatchContext(request, response, rootContainer) {
41
- return createRequestContext({
42
- container: rootContainer.createRequestScope(),
64
+ function createDispatchContext(request, response, container, promoteOnContainerAccess) {
65
+ const context = createRequestContext({
66
+ container,
43
67
  metadata: {},
44
68
  request,
45
69
  requestId: readRequestId(request),
46
70
  response
47
71
  });
72
+ if (!promoteOnContainerAccess) {
73
+ return context;
74
+ }
75
+ let activeContainer = container;
76
+ Object.defineProperty(context, 'container', {
77
+ configurable: true,
78
+ enumerable: true,
79
+ get() {
80
+ activeContainer = promoteOnContainerAccess();
81
+ return activeContainer;
82
+ },
83
+ set(value) {
84
+ activeContainer = value;
85
+ }
86
+ });
87
+ return context;
88
+ }
89
+ function createRootDispatchScope(rootContainer) {
90
+ return {
91
+ container: rootContainer,
92
+ requestScoped: false
93
+ };
94
+ }
95
+ function createRequestDispatchScope(rootContainer) {
96
+ return {
97
+ container: rootContainer.createRequestScope(),
98
+ requestScoped: true
99
+ };
100
+ }
101
+ function activeMiddlewareMayRequireRequestScope(definitions, request) {
102
+ return definitions.some(definition => {
103
+ if (!isMiddlewareRouteConfig(definition)) {
104
+ return true;
105
+ }
106
+ return definition.routes.length === 0 || definition.routes.some(route => matchRoutePattern(route, request.path));
107
+ });
108
+ }
109
+ function requestDtoMayRequireRequestScope(handler, options) {
110
+ if (!handler.route.request) {
111
+ return false;
112
+ }
113
+ if ((options.requestScope?.converterDefinitions ?? []).length > 0) {
114
+ return true;
115
+ }
116
+ if (options.binder) {
117
+ return true;
118
+ }
119
+ const plan = getCompiledDtoBindingPlan(handler.route.request);
120
+ return plan.entries.some(entry => entry.converter !== undefined);
121
+ }
122
+ function handlerMethodMayUseRequestContext(handler) {
123
+ const method = handler.controllerToken.prototype[handler.methodName];
124
+ return typeof method === 'function' && method.length >= 2;
125
+ }
126
+ function hasRequestScopeInspector(container) {
127
+ return typeof container === 'object' && container !== null && 'hasRequestScopedDependency' in container && typeof container.hasRequestScopedDependency === 'function';
128
+ }
129
+ function handlerMayRequireRequestScope(handler, request, options) {
130
+ if (handler.route.guards && handler.route.guards.length > 0) {
131
+ return true;
132
+ }
133
+ if ((options.interceptors ?? []).length > 0 || (handler.route.interceptors ?? []).length > 0) {
134
+ return true;
135
+ }
136
+ if (activeMiddlewareMayRequireRequestScope(handler.metadata.moduleMiddleware, request)) {
137
+ return true;
138
+ }
139
+ if (requestDtoMayRequireRequestScope(handler, options)) {
140
+ return true;
141
+ }
142
+ if (handlerMethodMayUseRequestContext(handler)) {
143
+ return true;
144
+ }
145
+ return hasRequestScopeInspector(options.rootContainer) ? options.rootContainer.hasRequestScopedDependency(handler.controllerToken) : true;
146
+ }
147
+ function dispatchStartMayRequireRequestScope(request, observers, options) {
148
+ return observers.length > 0 || activeMiddlewareMayRequireRequestScope(options.appMiddleware ?? [], request);
149
+ }
150
+ function ensureRequestScope(context) {
151
+ if (context.dispatchScope.requestScoped) {
152
+ return;
153
+ }
154
+ context.dispatchScope = createRequestDispatchScope(context.options.rootContainer);
155
+ context.requestContext.container = context.dispatchScope.container;
48
156
  }
49
157
  function ensureRequestNotAborted(request) {
50
158
  if (request.signal?.aborted) {
@@ -71,29 +179,41 @@ async function notifyObservers(observers, requestContext, callback, handler) {
71
179
  }
72
180
  }
73
181
  async function notifyObserversSafely(observers, requestContext, callback, logger, handler) {
182
+ if (observers.length === 0) {
183
+ return;
184
+ }
74
185
  try {
75
186
  await notifyObservers(observers, requestContext, callback, handler);
76
187
  } catch (error) {
77
188
  logDispatchFailure(logger, 'Request observer threw an unhandled error.', error);
78
189
  }
79
190
  }
80
- async function dispatchMatchedHandler(handler, requestContext, observers, contentNegotiation, binder, globalInterceptors, logger) {
81
- const guardContext = {
82
- handler,
83
- requestContext
84
- };
85
- const interceptorContext = {
86
- handler,
87
- requestContext
88
- };
89
- await runGuardChain(handler.route.guards ?? [], guardContext);
191
+ function mergeInterceptors(globalInterceptors, routeInterceptors) {
192
+ if (globalInterceptors.length === 0) {
193
+ return routeInterceptors;
194
+ }
195
+ if (routeInterceptors.length === 0) {
196
+ return globalInterceptors;
197
+ }
198
+ return [...globalInterceptors, ...routeInterceptors];
199
+ }
200
+ async function dispatchMatchedHandler(handler, requestContext, controllerContainer, observers, contentNegotiation, binder, globalInterceptors, logger) {
201
+ const routeGuards = handler.route.guards ?? [];
202
+ if (routeGuards.length > 0) {
203
+ const guardContext = {
204
+ handler,
205
+ requestContext
206
+ };
207
+ await runGuardChain(routeGuards, guardContext);
208
+ }
90
209
  if (requestContext.response.committed) {
91
210
  return;
92
211
  }
93
- const interceptors = [...(globalInterceptors ?? []), ...(handler.route.interceptors ?? [])];
94
- const result = await runInterceptorChain(interceptors, interceptorContext, async () => {
95
- return invokeControllerHandler(handler, requestContext, binder);
96
- });
212
+ const routeInterceptors = handler.route.interceptors ?? [];
213
+ const result = globalInterceptors.length === 0 && routeInterceptors.length === 0 ? await invokeControllerHandler(handler, requestContext, binder, controllerContainer) : await runInterceptorChain(mergeInterceptors(globalInterceptors, routeInterceptors), {
214
+ handler,
215
+ requestContext
216
+ }, async () => invokeControllerHandler(handler, requestContext, binder, controllerContainer));
97
217
  ensureRequestNotAborted(requestContext.request);
98
218
  if (!(result instanceof SseResponse) && !requestContext.response.committed) {
99
219
  await writeSuccessResponse(handler, requestContext.request, requestContext.response, result, contentNegotiation);
@@ -133,8 +253,11 @@ async function runDispatchPipeline(context) {
133
253
  if (context.response.committed) {
134
254
  return;
135
255
  }
136
- const match = matchHandlerOrThrow(context.options.handlerMapping, appMiddlewareContext.request);
256
+ const match = readFrameworkRequestNativeRouteHandoff(appMiddlewareContext.request) ?? matchHandlerOrThrow(context.options.handlerMapping, appMiddlewareContext.request);
137
257
  context.matchedHandler = match.descriptor;
258
+ if (handlerMayRequireRequestScope(match.descriptor, appMiddlewareContext.request, context.options)) {
259
+ ensureRequestScope(context);
260
+ }
138
261
  updateRequestParams(context.requestContext, match.params);
139
262
  await notifyHandlerMatched(context, match.descriptor);
140
263
  const moduleMiddlewareContext = {
@@ -143,7 +266,7 @@ async function runDispatchPipeline(context) {
143
266
  response: context.response
144
267
  };
145
268
  await runMiddlewareChain(match.descriptor.metadata.moduleMiddleware ?? [], moduleMiddlewareContext, async () => {
146
- await dispatchMatchedHandler(match.descriptor, context.requestContext, context.observers, context.contentNegotiation, context.options.binder, context.options.interceptors, context.options.logger);
269
+ await dispatchMatchedHandler(match.descriptor, context.requestContext, context.dispatchScope.container, context.observers, context.contentNegotiation, context.options.binder, context.options.interceptors ?? [], context.options.logger);
147
270
  });
148
271
  });
149
272
  }
@@ -167,13 +290,29 @@ async function handleDispatchError(context, error) {
167
290
  */
168
291
  export function createDispatcher(options) {
169
292
  const contentNegotiation = resolveContentNegotiation(options.contentNegotiation);
170
- return {
293
+ const observers = options.observers ?? [];
294
+ const dispatcher = {
295
+ describeRoutes() {
296
+ return options.handlerMapping.descriptors.map(descriptor => cloneHandlerDescriptor(descriptor));
297
+ },
171
298
  async dispatch(request, response) {
172
- const phaseContext = {
299
+ const dispatchRequest = createDispatchRequest(request);
300
+ const dispatchScope = dispatchStartMayRequireRequestScope(dispatchRequest, observers, options) ? createRequestDispatchScope(options.rootContainer) : createRootDispatchScope(options.rootContainer);
301
+ let phaseContext;
302
+ let containerPromotionOpen = true;
303
+ const requestContext = createDispatchContext(dispatchRequest, response, dispatchScope.container, () => {
304
+ if (!containerPromotionOpen) {
305
+ return phaseContext.dispatchScope.container;
306
+ }
307
+ ensureRequestScope(phaseContext);
308
+ return phaseContext.dispatchScope.container;
309
+ });
310
+ phaseContext = {
173
311
  contentNegotiation,
174
- observers: options.observers ?? [],
312
+ dispatchScope,
313
+ observers,
175
314
  options,
176
- requestContext: createDispatchContext(createDispatchRequest(request), response, options.rootContainer),
315
+ requestContext,
177
316
  response
178
317
  };
179
318
  await runWithRequestContext(phaseContext.requestContext, async () => {
@@ -184,13 +323,17 @@ export function createDispatcher(options) {
184
323
  await handleDispatchError(phaseContext, error);
185
324
  } finally {
186
325
  await notifyRequestFinish(phaseContext);
187
- try {
188
- await phaseContext.requestContext.container.dispose();
189
- } catch (error) {
190
- logDispatchFailure(options.logger, 'Request-scoped container dispose threw an error.', error);
326
+ containerPromotionOpen = false;
327
+ if (phaseContext.dispatchScope.requestScoped) {
328
+ try {
329
+ await phaseContext.dispatchScope.container.dispose();
330
+ } catch (error) {
331
+ logDispatchFailure(options.logger, 'Request-scoped container dispose threw an error.', error);
332
+ }
191
333
  }
192
334
  }
193
335
  });
194
336
  }
195
337
  };
338
+ return dispatcher;
196
339
  }
@@ -0,0 +1,53 @@
1
+ import type { FrameworkRequest, HandlerMatch } from '../types.js';
2
+ /** Internal handoff payload that lets adapters skip duplicate route matching safely. */
3
+ export type NativeRouteHandoff = HandlerMatch;
4
+ /**
5
+ * Associates one adapter-selected route handoff with a raw platform request.
6
+ *
7
+ * Platform adapters call this before translating the native request into a
8
+ * `FrameworkRequest`, allowing the shared dispatcher to reuse the semantically
9
+ * safe native match without changing the public dispatcher surface.
10
+ *
11
+ * @param rawRequest Raw platform request object used as the lookup key.
12
+ * @param handoff Pre-matched descriptor and params selected by the adapter.
13
+ */
14
+ export declare function bindRawRequestNativeRouteHandoff(rawRequest: object, handoff: NativeRouteHandoff): void;
15
+ /**
16
+ * Reads and clears a native route handoff previously bound to a raw request.
17
+ *
18
+ * Request factories consume this once while constructing `FrameworkRequest`
19
+ * instances so the handoff remains request-local and does not leak across
20
+ * platform object reuse.
21
+ *
22
+ * @param rawRequest Raw platform request object used as the lookup key.
23
+ * @returns The cloned handoff when one was registered for this request.
24
+ */
25
+ export declare function consumeRawRequestNativeRouteHandoff(rawRequest: unknown): NativeRouteHandoff | undefined;
26
+ /**
27
+ * Stores a pre-matched native route handoff on one framework request.
28
+ *
29
+ * @param request Framework request that should carry the adapter-native match.
30
+ * @param handoff Pre-matched descriptor and params selected by the adapter.
31
+ * @returns The same request instance for fluent adapter construction.
32
+ */
33
+ export declare function attachFrameworkRequestNativeRouteHandoff(request: FrameworkRequest, handoff: NativeRouteHandoff): FrameworkRequest;
34
+ /**
35
+ * Reads a pre-matched native route handoff from one framework request.
36
+ *
37
+ * @param request Framework request being dispatched.
38
+ * @returns The cloned handoff when the adapter supplied one.
39
+ */
40
+ export declare function readFrameworkRequestNativeRouteHandoff(request: FrameworkRequest): NativeRouteHandoff | undefined;
41
+ /**
42
+ * Reports whether a request path depends on fluo's normalization semantics.
43
+ *
44
+ * Duplicate slashes and trailing slashes are intentionally normalized by the
45
+ * shared matcher. Adapters use this helper to keep those requests on the
46
+ * generic dispatcher path when native routing may not preserve identical path
47
+ * selection semantics.
48
+ *
49
+ * @param path Raw framework request path.
50
+ * @returns `true` when normalization would change the incoming path.
51
+ */
52
+ export declare function isRoutePathNormalizationSensitive(path: string): boolean;
53
+ //# sourceMappingURL=native-route-handoff.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"native-route-handoff.d.ts","sourceRoot":"","sources":["../../src/dispatch/native-route-handoff.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAsBlE,wFAAwF;AACxF,MAAM,MAAM,kBAAkB,GAAG,YAAY,CAAC;AAE9C;;;;;;;;;GASG;AACH,wBAAgB,gCAAgC,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,kBAAkB,GAAG,IAAI,CAEtG;AAED;;;;;;;;;GASG;AACH,wBAAgB,mCAAmC,CAAC,UAAU,EAAE,OAAO,GAAG,kBAAkB,GAAG,SAAS,CAavG;AAED;;;;;;GAMG;AACH,wBAAgB,wCAAwC,CACtD,OAAO,EAAE,gBAAgB,EACzB,OAAO,EAAE,kBAAkB,GAC1B,gBAAgB,CAYlB;AAED;;;;;GAKG;AACH,wBAAgB,sCAAsC,CACpD,OAAO,EAAE,gBAAgB,GACxB,kBAAkB,GAAG,SAAS,CAWhC;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,iCAAiC,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEvE"}
@@ -0,0 +1,94 @@
1
+ import { normalizeRoutePath } from '../route-path.js';
2
+ const FRAMEWORK_REQUEST_NATIVE_ROUTE_HANDOFF = Symbol('fluo.http.nativeRouteHandoff');
3
+ const RAW_REQUEST_NATIVE_ROUTE_HANDOFFS = new WeakMap();
4
+ function cloneNativeRouteHandoff(handoff) {
5
+ return {
6
+ descriptor: handoff.descriptor,
7
+ params: {
8
+ ...handoff.params
9
+ }
10
+ };
11
+ }
12
+
13
+ /** Internal handoff payload that lets adapters skip duplicate route matching safely. */
14
+
15
+ /**
16
+ * Associates one adapter-selected route handoff with a raw platform request.
17
+ *
18
+ * Platform adapters call this before translating the native request into a
19
+ * `FrameworkRequest`, allowing the shared dispatcher to reuse the semantically
20
+ * safe native match without changing the public dispatcher surface.
21
+ *
22
+ * @param rawRequest Raw platform request object used as the lookup key.
23
+ * @param handoff Pre-matched descriptor and params selected by the adapter.
24
+ */
25
+ export function bindRawRequestNativeRouteHandoff(rawRequest, handoff) {
26
+ RAW_REQUEST_NATIVE_ROUTE_HANDOFFS.set(rawRequest, cloneNativeRouteHandoff(handoff));
27
+ }
28
+
29
+ /**
30
+ * Reads and clears a native route handoff previously bound to a raw request.
31
+ *
32
+ * Request factories consume this once while constructing `FrameworkRequest`
33
+ * instances so the handoff remains request-local and does not leak across
34
+ * platform object reuse.
35
+ *
36
+ * @param rawRequest Raw platform request object used as the lookup key.
37
+ * @returns The cloned handoff when one was registered for this request.
38
+ */
39
+ export function consumeRawRequestNativeRouteHandoff(rawRequest) {
40
+ if (typeof rawRequest !== 'object' || rawRequest === null) {
41
+ return undefined;
42
+ }
43
+ const handoff = RAW_REQUEST_NATIVE_ROUTE_HANDOFFS.get(rawRequest);
44
+ if (!handoff) {
45
+ return undefined;
46
+ }
47
+ RAW_REQUEST_NATIVE_ROUTE_HANDOFFS.delete(rawRequest);
48
+ return cloneNativeRouteHandoff(handoff);
49
+ }
50
+
51
+ /**
52
+ * Stores a pre-matched native route handoff on one framework request.
53
+ *
54
+ * @param request Framework request that should carry the adapter-native match.
55
+ * @param handoff Pre-matched descriptor and params selected by the adapter.
56
+ * @returns The same request instance for fluent adapter construction.
57
+ */
58
+ export function attachFrameworkRequestNativeRouteHandoff(request, handoff) {
59
+ Reflect.set(request, FRAMEWORK_REQUEST_NATIVE_ROUTE_HANDOFF, {
60
+ handoff: cloneNativeRouteHandoff(handoff),
61
+ method: request.method,
62
+ path: request.path
63
+ });
64
+ return request;
65
+ }
66
+
67
+ /**
68
+ * Reads a pre-matched native route handoff from one framework request.
69
+ *
70
+ * @param request Framework request being dispatched.
71
+ * @returns The cloned handoff when the adapter supplied one.
72
+ */
73
+ export function readFrameworkRequestNativeRouteHandoff(request) {
74
+ const record = Reflect.get(request, FRAMEWORK_REQUEST_NATIVE_ROUTE_HANDOFF);
75
+ if (!record || record.method !== request.method || record.path !== request.path) {
76
+ return undefined;
77
+ }
78
+ return cloneNativeRouteHandoff(record.handoff);
79
+ }
80
+
81
+ /**
82
+ * Reports whether a request path depends on fluo's normalization semantics.
83
+ *
84
+ * Duplicate slashes and trailing slashes are intentionally normalized by the
85
+ * shared matcher. Adapters use this helper to keep those requests on the
86
+ * generic dispatcher path when native routing may not preserve identical path
87
+ * selection semantics.
88
+ *
89
+ * @param path Raw framework request path.
90
+ * @returns `true` when normalization would change the incoming path.
91
+ */
92
+ export function isRoutePathNormalizationSensitive(path) {
93
+ return normalizeRoutePath(path) !== path;
94
+ }
package/dist/errors.d.ts CHANGED
@@ -5,6 +5,9 @@ import { FluoError } from '@fluojs/core';
5
5
  export declare class RouteConflictError extends FluoError {
6
6
  constructor(message: string);
7
7
  }
8
+ /**
9
+ * Represents the invalid route path error.
10
+ */
8
11
  export declare class InvalidRoutePathError extends FluoError {
9
12
  constructor(message: string);
10
13
  }
@@ -1 +1 @@
1
- {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAEzC;;GAEG;AACH,qBAAa,kBAAmB,SAAQ,SAAS;gBACnC,OAAO,EAAE,MAAM;CAG5B;AAED,qBAAa,qBAAsB,SAAQ,SAAS;gBACtC,OAAO,EAAE,MAAM;CAG5B;AAED;;GAEG;AACH,qBAAa,oBAAqB,SAAQ,SAAS;gBACrC,OAAO,EAAE,MAAM;CAG5B;AAED;;GAEG;AACH,qBAAa,mBAAoB,SAAQ,SAAS;gBACpC,OAAO,SAA4C;CAGhE"}
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAEzC;;GAEG;AACH,qBAAa,kBAAmB,SAAQ,SAAS;gBACnC,OAAO,EAAE,MAAM;CAG5B;AAED;;GAEG;AACH,qBAAa,qBAAsB,SAAQ,SAAS;gBACtC,OAAO,EAAE,MAAM;CAG5B;AAED;;GAEG;AACH,qBAAa,oBAAqB,SAAQ,SAAS;gBACrC,OAAO,EAAE,MAAM;CAG5B;AAED;;GAEG;AACH,qBAAa,mBAAoB,SAAQ,SAAS;gBACpC,OAAO,SAA4C;CAGhE"}
package/dist/errors.js CHANGED
@@ -10,6 +10,10 @@ export class RouteConflictError extends FluoError {
10
10
  });
11
11
  }
12
12
  }
13
+
14
+ /**
15
+ * Represents the invalid route path error.
16
+ */
13
17
  export class InvalidRoutePathError extends FluoError {
14
18
  constructor(message) {
15
19
  super(message, {
package/dist/guards.d.ts CHANGED
@@ -1,3 +1,10 @@
1
1
  import type { GuardContext, GuardLike } from './types.js';
2
+ /**
3
+ * Run guard chain.
4
+ *
5
+ * @param definitions The definitions.
6
+ * @param context The context.
7
+ * @returns The run guard chain result.
8
+ */
2
9
  export declare function runGuardChain(definitions: GuardLike[], context: GuardContext): Promise<void>;
3
10
  //# sourceMappingURL=guards.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"guards.d.ts","sourceRoot":"","sources":["../src/guards.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAS,YAAY,EAAE,SAAS,EAAkB,MAAM,YAAY,CAAC;AAcjF,wBAAsB,aAAa,CAAC,WAAW,EAAE,SAAS,EAAE,EAAE,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CASlG"}
1
+ {"version":3,"file":"guards.d.ts","sourceRoot":"","sources":["../src/guards.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAS,YAAY,EAAE,SAAS,EAAkB,MAAM,YAAY,CAAC;AAcjF;;;;;;GAMG;AACH,wBAAsB,aAAa,CAAC,WAAW,EAAE,SAAS,EAAE,EAAE,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAalG"}
package/dist/guards.js CHANGED
@@ -8,7 +8,18 @@ async function resolveGuard(definition, requestContext) {
8
8
  }
9
9
  return requestContext.container.resolve(definition);
10
10
  }
11
+
12
+ /**
13
+ * Run guard chain.
14
+ *
15
+ * @param definitions The definitions.
16
+ * @param context The context.
17
+ * @returns The run guard chain result.
18
+ */
11
19
  export async function runGuardChain(definitions, context) {
20
+ if (definitions.length === 0) {
21
+ return;
22
+ }
12
23
  for (const definition of definitions) {
13
24
  const guard = await resolveGuard(definition, context.requestContext);
14
25
  const result = await guard.canActivate(context);
@@ -1,10 +1,19 @@
1
1
  import type { MetadataSource } from '@fluojs/core';
2
2
  import type { HttpExceptionDetail } from './exceptions.js';
3
+ /**
4
+ * Describes the input error detail contract.
5
+ */
3
6
  export interface InputErrorDetail {
4
7
  code: string;
5
8
  field?: string;
6
9
  message: string;
7
10
  source?: MetadataSource;
8
11
  }
12
+ /**
13
+ * To input error detail.
14
+ *
15
+ * @param detail The detail.
16
+ * @returns The to input error detail result.
17
+ */
9
18
  export declare function toInputErrorDetail(detail: InputErrorDetail): HttpExceptionDetail;
10
19
  //# sourceMappingURL=input-error-detail.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"input-error-detail.d.ts","sourceRoot":"","sources":["../src/input-error-detail.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAEnD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AAE3D,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,cAAc,CAAC;CACzB;AAED,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,gBAAgB,GAAG,mBAAmB,CAOhF"}
1
+ {"version":3,"file":"input-error-detail.d.ts","sourceRoot":"","sources":["../src/input-error-detail.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAEnD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AAE3D;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,cAAc,CAAC;CACzB;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,gBAAgB,GAAG,mBAAmB,CAOhF"}